Created
October 29, 2025 01:20
-
-
Save wschutzer/5428ddf245f1868935630b25d57a5dc6 to your computer and use it in GitHub Desktop.
Spinning gradient disk wave
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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