Skip to content

Instantly share code, notes, and snippets.

@wschutzer
Last active October 28, 2025 14:14
Show Gist options
  • Save wschutzer/f026a6ffbd859e75e707ed952e0c2c7f to your computer and use it in GitHub Desktop.
Save wschutzer/f026a6ffbd859e75e707ed952e0c2c7f to your computer and use it in GitHub Desktop.
Ribbon wrapping an infinite cylinder
/* Wrapping ribbon
* ---------------
* Shows a ribbon gradually wrapping up an infinite cylinder.
* 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;
PGraphics gtex0; // Each frame is drawn here
// Vertex shader code
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;",
"}"};
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",
"}"};
class thing
{
PVector p1, p2;
PVector normal;
PVector tangent;
float r;
float disp;
thing(PVector p1_, PVector p2_, PVector nv, PVector tv, float r_)
{
p1 = p1_.copy();
p2 = p2_.copy();
normal = nv.copy().normalize();
tangent = tv.copy().normalize();
r = r_;
}
void draw(float t)
{
push();
if (p1.z < 0)
{
float d = p1.z/fac;
stroke(255*exp(-d*d/(163840*2*fac)));
}
else
stroke(255);
noFill(); //fill(0);
beginShape();
PVector right = tangent.cross(normal).normalize().mult(r);
vertex(p1.x, p1.y, p1.z);
vertex(p2.x, p2.y, p2.z);
PVector p = PVector.add(p2, right);
vertex(p.x, p.y, p.z);
p = PVector.add(p1, right);
vertex(p.x, p.y, p.z);
endShape(CLOSE);
stroke(color(255,0,0));
/* draw the coordinate vectors
float ell = 20*fac;
line(pos.x, pos.y, pos.z, pos.x + normal.x*ell, pos.y+normal.y*ell,pos.z+normal.z*ell);
stroke(color(0,255,0));
line(pos.x, pos.y, pos.z, pos.x + right.x*ell, pos.y+right.y*ell,pos.z+right.z*ell);
stroke(color(0,0,255));
line(pos.x, pos.y, pos.z, pos.x + tangent.x*ell, pos.y+tangent.y*ell,pos.z+tangent.z*ell);
*/
pop();
}
}
void settings()
{
size(frameWidth, frameHeight, P3D); // Use P2D renderer for shader support
}
int num_segs = 12;
int num_rings = 100;
float R = 100*fac;
float h = R*cos(PI/num_segs);
float stretch = 2;
float ell = stretch*2*R*sin(PI/num_segs);
thing rings[][];
float g(float t)
{
// this is:
// (1) a fixed base: -1000*fac
// (2) a backwards constant movement: t*ell/TWO_PI
// (3) a nonlinear movement to create the wrapping effect: pow(t*ell/(TWO_PI*100*fac),3.5)
return -1000*fac + t*ell/TWO_PI + pow(t*ell/(TWO_PI*100*fac)/stretch,3.5);
}
float h(float t)
{
return g(t+TWO_PI);
}
PVector gamma1(float theta)
{
return new PVector(R*cos(theta),R*sin(theta),g(theta)*t+(1-t)*h(theta));
}
void setup()
{
gtex0 = createGraphics(width, height, P3D);
motionBlur = new PShader(this, motionBlurVertShader, motionBlurFragShader);
//ortho();
rings = new thing[num_rings][num_segs];
}
void draw()
{
background(0);
for(int i=0; i<samples_per_frame; i++)
{
t = map(((recording || testing) ? 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");
println(frameCount,"/",num_frames);
if (frameCount >= num_frames)
exit();
}
}
void draw_(float t)
{
background(0); // Clear the canvas
noStroke();
// Computes the the ribbon
float theta = 0;
for(int i=0; i<num_rings; i++)
{
for(int k=0; k<num_segs; k++)
{
float theta1 = theta + PI/num_segs;
PVector p1 = gamma1(theta);
PVector p2 = gamma1(theta1);
PVector nv = PVector.add(p1, p2); // Normal vector
nv.z = 0;
PVector tv = new PVector(nv.y, -nv.x, 0); // Tangent vector
rings[i][k] = new thing(p1, p2, nv, tv, ell);
theta = theta1;
}
}
push();
strokeWeight(1.5*fac);
translate(width/2,height/2,0);
for (thing[] ring : rings) { // iterate each ring (row)
for (thing th : ring) { // iterate each thing within that ring
th.draw(t);
}
}
pop();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment