Skip to content

Instantly share code, notes, and snippets.

@wschutzer
Created October 29, 2025 01:20
Show Gist options
  • Save wschutzer/5428ddf245f1868935630b25d57a5dc6 to your computer and use it in GitHub Desktop.
Save wschutzer/5428ddf245f1868935630b25d57a5dc6 to your computer and use it in GitHub Desktop.
Spinning gradient disk wave
/* Gradient disk wave
* ------------------
* Draws a grid of tangent-gradient filled spinning disks whose rotation speed
* varies in waves travelling through the grid.
*
* This code uses shaders to aproximate the motion blur effect by averaging the pixels along a sequence
* of frames.
*
* Copyright (C) 2025 Waldeck Schutzer (@infinitymathart)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
final boolean recording = false;
final boolean testing = false;
final boolean gif = true;
final int duration = 2; // Seconds
final int fps = gif ? 30 : 60; // Frames Per Second
final float num_frames = fps*duration;
// Motion blur:
final int samples_per_frame = 7; // How many frames to average?
final float shutter_angle = 0.5; // How distant the frames are in time?
final int frameWidth = gif ? 600 : (recording ? 2160 : 800);
final float fac = 1.0*frameWidth/800; // Reference scaling factor
final float aspect = gif ? 1.0 : 1350.0/1080; // 1920.0/1080;
final int frameHeight = int(aspect * frameWidth);
float t = 0; // Normalized animation time in the interval 0 - 1.
PShader motionBlur;
PShader tangentShader;
PGraphics gtex0; // Each frame is drawn here
// Vertex shader code
final String[] motionBlurVertShader = {
"#version 100",
"#define PROCESSING_TEXTURE_SHADER",
"uniform mat4 transformMatrix;",
"uniform mat4 texMatrix;",
"attribute vec4 position;",
"attribute vec4 color;",
"attribute vec2 texCoord;",
"varying vec4 vertColor;",
"varying vec4 vertTexCoord;",
"void main() {",
"gl_Position = transformMatrix * position;",
"vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);",
"vertColor = color;",
"}"};
final String[] motionBlurFragShader = {
"#version 100",
"#ifdef GL_ES",
"precision mediump float;",
"precision mediump int;",
"#endif",
"#define PROCESSING_TEXTURE_SHADER",
"uniform sampler2D texture; // Source and result frame, works as an accumulator",
"uniform sampler2D tex0; // Currently drawn frame to be added",
"uniform float alpha;",
"uniform float beta;",
"varying vec4 vertColor; // Seems to be white",
"varying vec4 vertTexCoord;",
"void main()",
"{",
" vec4 curColor = texture2D(tex0, vertTexCoord.xy); // Color of current pixel ",
" vec4 accColor = texture2D(texture, vertTexCoord.xy); // Color in accumulator",
" gl_FragColor = beta*(alpha*accColor + curColor); // Blending",
"}"};
final String[] tangentVertShader = {
"#version 100",
"#define PROCESSING_COLOR_SHADER",
"uniform mat4 transformMatrix;",
"uniform mat4 texMatrix;",
"attribute vec4 position;",
"attribute vec4 color;",
"attribute vec2 texCoord;",
"varying vec4 vertColor;",
"varying vec4 vertTexCoord;",
"void main() {",
" gl_Position = transformMatrix * position;",
" vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);",
" vertColor = color;",
"}"
};
final String[] tangentFragShader = {
"#version 100",
"#ifdef GL_ES",
"precision mediump float;",
"precision mediump int;",
"#endif",
"#define PROCESSING_COLOR_SHADER",
"uniform vec2 resolution;", // (w, h) of this rect
"uniform vec2 center;", // circle center in local coords
"uniform float radius;", // fraction of min(resolution)
"uniform vec2 offset;", // rect top-left in Processing coords
"uniform float screenHeight;", // full sketch height
"uniform float theta0;", // angular offset
"uniform float useTransparency;", // 0.0 opaque, 1.0 transparent
"varying vec4 vertColor;",
"varying vec4 vertTexCoord;",
"void main() {",
" // local pixel coords (Processing-style y-down)",
" vec2 uv = vec2(",
" gl_FragCoord.x - offset.x,",
" (screenHeight - gl_FragCoord.y) - offset.y",
" );",
" // physical circle radius in px",
" float R = radius * min(resolution.x, resolution.y) / 2.0;",
" vec2 d = uv - center;",
" float r = length(d);",
" // angle for tangential gradient (wrapped to [-pi,pi])",
" float angle = atan(d.y, d.x) + theta0;",
" angle = mod(angle + 3.14159265, 6.28318530) - 3.14159265;",
" float t = (angle + 3.14159265) / (2.0 * 3.14159265);",
" float gray = t;",
" vec3 diskColor = vec3(gray);",
" vec3 bgColor = vec3(0.0);",
" // radial feather on circle edge",
" float feather = 1.5;", // pixels of softness on the rim",
" float alphaEdge = 1.0 - smoothstep(R, R + feather, r);",
" // alphaEdge ~1 inside, ~0 outside",
" // --- seam softening logic ---",
" // how close in ANGLE we are to the wrap seam (|angle| ~ pi)",
" float seamBand = 0.05; // radians (~3 deg) range around seam",
" float seamDist = abs(abs(angle) - 3.14159265);",
" float seamFadeAngular = 1.0 - smoothstep(0.0, seamBand, seamDist);",
" // seamFadeAngular: 1.0 right at seam, 0.0 away from it",
" // how close in RADIUS we are to the rim",
" // we only want seam fade near the circle boundary, not near center.",
" float rimBandPx = 4.0; // how wide (inward) from the rim we soften the seam",
" float rimStart = R - rimBandPx; // start fading this close to the rim",
" // rimFactor ~0 in the middle, ~1 only near rim (within rimBandPx of R)",
" float rimFactor = smoothstep(rimStart, R, r);",
" rimFactor = clamp(rimFactor, 0.0, 1.0);",
" // final seam fade strength",
" float seamFade = seamFadeAngular * rimFactor;",
" // seamFade ~1 = at seam near rim, ~0 = elsewhere or toward center",
" // reduce alpha only where seamFade is active",
" // 0.5 = how much we dim the seam; tweak this",
" float alphaSeam = 1.0 - 0.5 * seamFade;",
" // combine alpha terms",
" float alphaFinal = alphaEdge * alphaSeam;",
" // hard clamp: outside feather must not draw",
" if (r > R + feather) {",
" alphaFinal = 0.0;",
" }",
" if (useTransparency > 0.5) {",
" // transparent mode: don't bake bgColor, just output diskColor with alpha",
" gl_FragColor = vec4(diskColor, alphaFinal);",
" } else {",
" // opaque mode: fade into black background and force alpha=1",
" vec3 color = mix(bgColor, diskColor, alphaFinal);",
" gl_FragColor = vec4(color, 1.0);",
" }",
"}"
};
void draw()
{
background(0);
for(int i=0; i<samples_per_frame; i++)
{
t = map((recording ? 1.0*(frameCount-1): 1.0*mouseX/width*num_frames) + i*shutter_angle/samples_per_frame, 0, num_frames, 0, 1)%1;
draw_(t); // Draw on gtex1
motionBlur.set("tex0", g.get());
motionBlur.set("alpha", (float)(i));
motionBlur.set("beta", (float)(1.0/(i+1)));
gtex0.filter(motionBlur); // Averages the frames with a moving average
}
image(gtex0,0,0);
if (recording)
{
saveFrame("/tmp/r/frame_####.png");
if (frameCount >= num_frames)
exit();
}
}
final int num_points = 1001;
float FT[];
float f(float t)
{
return pow( (4*t*(1-t)), 4 );
}
void integrate()
{
FT = new float[num_points];
float h = 1.0/(num_points-1);
float acc = 0;
for(int i=0; i<num_points; i++)
{
FT[i] = acc;
int j = (i+1)%num_points; // periodic function
acc += 0.5 * h * (f(h*i) + f(h*j)); // trapezoid rule
}
int k = ceil(FT[num_points-1]);
float alpha = k/FT[num_points-1];
for(int i=0; i<num_points; i++)
{
FT[i] *= alpha;
}
}
float fmod(float x, float y) {
return x - y * floor(x / y);
}
float F(float t) {
t = fmod(t,1.0); // F is periodic
float fIndex = t * (num_points - 1);
int i = int(floor(fIndex));
int j = (i + 1) % num_points;
float alpha = fIndex - i; // fractional part between i and j
return (1 - alpha) * FT[i] + alpha * FT[j];
}
void settings()
{
size(frameWidth, frameHeight, P2D);
}
void setup()
{
gtex0 = createGraphics(width, height, P2D);
motionBlur = new PShader(this, motionBlurVertShader, motionBlurFragShader);
tangentShader = new PShader(this, tangentVertShader, tangentFragShader);
offset = new float[nrows][ncols];
for(int i=0; i<nrows; i++)
for(int j=0; j<ncols; j++)
offset[i][j] = 1.0*(i+j)/nrows;
integrate();
}
void drawCircle(float x, float y, float w, float h, float theta0, boolean transparent)
{
tangentShader.set("resolution", w, h);
tangentShader.set("center", w * 0.5f, h * 0.5f);
// radius is interpreted as "fraction of half the rect size"
tangentShader.set("radius", 0.9/ncols);
tangentShader.set("offset", x, y);
tangentShader.set("theta0", theta0);
tangentShader.set("screenHeight", (float)height);
tangentShader.set("useTransparency", transparent ? 1.0f : 0.0f);
shader(tangentShader);
rect(x, y, w, h);
resetShader(); // switch shader off right after use
}
final int nrows = 15;
final int ncols = 15;
final int margin = 10;
float[][] offset;
float angpos(float seed, float t)
{
final float tscl = 0.15;
return 16*TWO_PI*map(noise(tscl*cos(TWO_PI*t),tscl*sin(TWO_PI*t),seed),0.2,0.8,0,1);
}
void draw_(float t) {
final float cw = 1.0*(height-margin)/ncols;
final float ch = 1.0*(width-margin)/nrows;
background(255); // comment out if you want accumulation trails
for(int i=0; i<nrows; i++)
{
float y = margin/2 + i*ch;
for(int j=0; j<ncols; j++)
{
float x = margin/2 + j*cw;
drawCircle(x, y, cw, ch, TWO_PI*(t + F(-offset[i][j] + t)), false);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment