Skip to content

Instantly share code, notes, and snippets.

@wschutzer
Created October 11, 2024 14:07
Show Gist options
  • Save wschutzer/a1e30fda8fbe73d8bbbc6e3a0299b08e to your computer and use it in GitHub Desktop.
Save wschutzer/a1e30fda8fbe73d8bbbc6e3a0299b08e to your computer and use it in GitHub Desktop.
2D shape transitions
// 2D shape transitions
//
// Mathematics and Processing code by Waldeck Schützer (@infinitymathart)
// Motion blur template by @davidbeesandbombs, explanation/article: https://bleuje.com/tutorial6/
// Rotater transition by Etienne Jacob (@etinjcb)
//
import complexnumbers.*;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
// Dragon shape by Nagesh Talagani, @nagesh1805 on github
// t-rex shape from https://www.dinosaurmama.com/free-svg-files/t-rex-silhouette-svg
// Dancing couple Designed by Freepik www.freepik.com (include link)
// others: www.svgrepo.com, CC0 license
float fac = 1.0;
int FPS = 60;
int total_frames = 40*FPS;
float R = 0;
boolean recording = false;
boolean truncating = false;
int num_points = 4789; // recording ? 15000 : 2048;
int num_stars = 1000;
PFont courier;
OpenSimplexNoise noise;
// -----------------
// Various parametric curves follow, all from 0 to 2pi
float rho(float theta, int k, float r) // For regular polygons
{
// if(k % 2 == 1)
// theta -= PI/(2*k);
return 1.1*r*cos(PI/k)/cos(theta - TWO_PI/k * floor((k*theta + PI)/TWO_PI));
}
PVector rsquare(float theta, float d)
{
float x = rho(theta, 4, d)*cos(theta);
float y = rho(theta, 4, d)*sin(theta);
return new PVector(x,y);
}
PVector rcircle(float theta, float r)
{
return new PVector(r*cos(theta), r*sin(theta));
}
PVector rastroid(float theta, float r)
{
float c = cos(theta);
float s = sin(theta);
float x = r*c*c*c;
float y = r*s*s*s;
return new PVector(x,y);
}
PVector rheart(float theta, float r)
{
r*=0.9;
float s = sin(theta);
float x = r*s*s*s;
float y = r*(13*cos(theta)-5*cos(2*theta)-2*cos(3*theta)-cos(4*theta))/16;
return new PVector(x, y);
}
class ftshape
{
Complex[] coefs;
int[] freqs;
int size = 0;
PVector[] path;
ftshape(String fname, int max_points)
{
load(dataPath("")+"/"+fname, max_points);
render();
}
void render()
{
path = new PVector[num_points+1];
for(int i=0; i<=num_points; i++)
{
float theta = TWO_PI*i/num_points;
path[i] = dftv(theta, this).copy();
}
}
void load(String fileName, int max_points)
{
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(fileName));
String line;
// First line contains num_points
String firstLine = reader.readLine();
size = int(firstLine.trim());
if (truncating && size > max_points)
size = max_points;
freqs = new int[size];
coefs = new Complex[size];
println(fileName,": fourier size=",size);
int i = 0;
while (i < size && (line = reader.readLine()) != null) {
// Split the line by commas
String[] tokens = line.split(" ");
// Convert each field to float and store it in the appropriate array
freqs[i] = int(tokens[0]);
float re = float(tokens[1]);
float im = float(tokens[2]);
coefs[i] = new Complex(re, im);
i++;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ftshape butterfly;
ftshape letterpi;
ftshape dragon;
ftshape t_rex;
ftshape dance;
ftshape homer;
ftshape dove;
ftshape rocket;
ftshape love;
ftshape yourheart;
ftshape tree;
ftshape labyrinth;
ftshape phoenix;
ftshape spiral;
class thing
{
float x;
float y;
float offset = 0;
float scl = 1.0;
thing(float x_, float y_)
{
x = x_;
y = y_;
offset = 0;//(-x-y+width)/(width*num_curves);
}
void draw(float t)
{
float tt = (100+t+offset) %1;
int ti = 0;
float[] ta = new float[num_curves];
for(int i=0; i<num_curves; i++)
ta[i] = constrain((num_curves)*tt-i,0,1);
ti = int(num_curves*tt) % num_curves;
beginShape();
PVector ppos = curve(ti, 0);
for(int i=0; i<=num_points; i++)
{
float a = TWO_PI*i/num_points;
PVector p = curve(ti, a);
PVector q = curve(ti+1, a);
float th = holdon(ta[ti]);
PVector pos = transition(p, q, th, ti % 2 == 0, ti);
if ( pos.copy().sub(ppos).mag() > 20*fac)
{
endShape();
beginShape();
}
vertex(scl*pos.x, -scl*pos.y);
//point(scl*pos.x, -scl*pos.y);
ppos = pos;
}
endShape();
}
}
class star
{
boolean spike = random(0,1)>0.95;
float x = random(-width/2,width/2);
float y = random(-height/2,height/2);
float size = spike ? random(3,5) : random(2,5);
float offset = random(0,0.5);
void draw(float t)
{
push();
float tt = t+offset;
float nscl = 10;
float n = map((float)noise.eval(offset*1000+0.02*x/fac, 0.02*y/fac, nscl*cos(TAU*tt), offset*1000+nscl*sin(TAU*tt) ),-1,1,spike?0.2:0.05,1);
if (spike)
{
stroke(200,200);
strokeWeight(fac);
fill(255,200);
beginShape();
for(int i=0; i<50; i++)
{
float theta = TAU*i/50;
PVector pos = rastroid(theta,fac*size*n);
vertex(x+pos.x,y+pos.y);
}
endShape(CLOSE);
}
else
{
strokeWeight(n*size*fac);
stroke(255*n,180);
point(x,y);
}
pop();
}
}
star[] stars;
PVector rot(PVector v, float angle)
{
float c = cos(angle);
float s = sin(angle);
return new PVector(v.x * c - v.y * s, v.x * s + v.y * c);
}
float easeInOutCubic(float x)
{
return x < 0.5 ? 4 * x * x * x : 1 - pow(-2 * x + 2, 3) / 2;
}
// easing function taken from https://easings.net/#easeOutElastic
float easeOutElastic(float x)
{
float c4 = (2*PI)/3;
if(x<=0) return 0;
if(x>=1) return 1;
return pow(2, -10 * x) * sin((x * 10 - 0.75) * c4) + 1;
}
float holdon(float t)
{
//return easeInOutCubic(t);
float d = 5.0;
if (t <= 1.0/d)
return 0;
else if (t <= (d-1)/d)
return easeInOutCubic((t*d-1.0)/(d-2));
else
return 1.0;
}
//
// rotater: go from v1 to v2 with rotation around their middle
// Original code by Ethienne Jacob
//
PVector rotater(PVector v1,PVector v2,float p,boolean orientation) // p is the progress in this interpolation, in [0,1]
{
PVector middle = v1.copy().add(v2).mult(0.5);
PVector middleToV1 = v1.copy().sub(middle);
float angle = atan2(middleToV1.y,middleToV1.x);
float o = (orientation?-1:1);
float r = middleToV1.mag()*(1+(p*(1-p)*6));
return new PVector(middle.x+r*cos(angle+o*PI*p),middle.y+r*sin(angle+o*PI*p));
}
PVector rotater2(PVector v1, PVector v2, float p, boolean orientation) {
// Eased progress for smoother transitions
float easedProgress = p;// easeInOut(p);
// Middle point between v1 and v2
PVector middle = v1.copy().add(v2).mult(0.5);
// Vector from middle to v1 (defines the radius)
PVector middleToV1 = v1.copy().sub(middle);
// Calculate the initial angle and adjust orientation
float angle = atan2(middleToV1.y, middleToV1.x);
float o = (orientation ? -1 : 1); // Adjust rotation direction
// Smoother radius growth (cubic adjustment)
float r = middleToV1.mag() * (1 + pow(easedProgress * (1 - easedProgress), 2) * 10);
// Calculate new point with refined angle and radius
return new PVector(
middle.x + r * cos(angle + o * PI * easedProgress),
middle.y + r * sin(angle + o * PI * easedProgress)
);
}
PVector bezierTransition(PVector v1, PVector v2, PVector control, float p) {
float t = p; // p is progress from 0 to 1
float oneMinusT = 1 - t;
// Quadratic Bezier formula
PVector result = v1.copy().mult(oneMinusT * oneMinusT)
.add(control.copy().mult(2 * oneMinusT * t))
.add(v2.copy().mult(t * t));
return result;
}
PVector sineWaveTransition(PVector v1, PVector v2, float p, float amplitude, float frequency) {
PVector linearPath = PVector.lerp(v1, v2, p);
float distance = PVector.dist(v1, v2);
PVector perpendicular = new PVector(-(v2.y - v1.y), v2.x - v1.x).normalize();
float offset = amplitude * sin(TWO_PI * frequency * p);
return linearPath.add(perpendicular.mult(offset));
}
PVector elasticBounceTransition(PVector v1, PVector v2, float p) {
float elasticity = 1.5;
float easeOutElastic = pow(2, -10 * p) * sin((p - elasticity / 4) * (TWO_PI) / elasticity) + 1;
return PVector.lerp(v1, v2, easeOutElastic);
}
PVector springDampedTransition(PVector v1, PVector v2, float p) {
float dampingFactor = 0.5;
float springEffect = (1 - exp(-p / dampingFactor)) * sin(TWO_PI * p * 3);
return PVector.lerp(v1, v2, p + springEffect * (1 - p));
}
PVector circularPathTransition(PVector v1, PVector v2, float p, boolean clockwise) {
// Calculate the midpoint between v1 and v2
PVector middle = v1.copy().add(v2).mult(0.5);
// Calculate the vector from the middle point to v1 (this defines the radius)
PVector middleToV1 = v1.copy().sub(middle);
float radius = middleToV1.mag();
// Calculate the initial angle from middle to v1
float angle = atan2(middleToV1.y, middleToV1.x);
// Determine rotation direction based on the `clockwise` boolean
float rotationDirection = clockwise ? 1 : -1;
// Rotate by `p` (progress) from 0 to PI to move from v1 to v2
float newAngle = angle + rotationDirection * PI * p;
// Calculate the new x, y coordinates using the new angle and radius
float newX = middle.x + radius * cos(newAngle);
float newY = middle.y + radius * sin(newAngle);
return new PVector(newX, newY);
}
PVector spiralTransition(PVector v1, PVector v2, float p, float spiralTightness, float rotations) {
// Linearly interpolate between v1 and v2
PVector linearPath = PVector.lerp(v1, v2, p);
// Vector from v1 to v2 (direction vector)
PVector direction = PVector.sub(v2, v1).normalize();
// Perpendicular vector to create the spiral effect
PVector perpendicular = new PVector(-direction.y, direction.x); // Perpendicular to direction
// Adjust the radius for the spiral to smoothly grow and decay
float angle = rotations * TWO_PI * p; // Spiral rotates based on progress
float radius = spiralTightness * p * (1 - p); // Max radius at the middle of the transition
// Apply the spiral offset perpendicular to the linear path
float spiralX = radius * cos(angle);
float spiralY = radius * sin(angle);
// Add the spiral effect, making sure it starts at v1 and ends at v2
return linearPath.add(perpendicular.copy().mult(spiralX)).add(perpendicular.copy().rotate(HALF_PI).mult(spiralY));
}
PVector zoomTransition(PVector a, PVector b, float t)
{
if (t<0.5)
return PVector.lerp(a, a.copy().mult(width/2), 2*t);
else
return PVector.lerp(new PVector(0,0), b, 2*(t-0.5));
}
PVector vflip(PVector a, PVector b, float t)
{
if (t<0.5)
return new PVector(lerp(a.x,0,2*t),a.y);
else
return new PVector(lerp(0,b.x,2*(t-0.5)),b.y);
}
PVector hflip(PVector a, PVector b, float t)
{
if (t<0.5)
return new PVector(a.x,lerp(a.y,0,2*t));
else
return new PVector(b.x,lerp(0,b.y,2*(t-0.5)));
}
PVector morph(PVector a, PVector b, float p, float t)
{
PVector mid = a.copy().add(b).mult(0.5);
float n = 15*map(noise(0.02*mid.x, 0.02*mid.y, 0.02*t),-1,1,0,1)*p*(1-p);
return PVector.lerp(a,b,p*(1-n));
}
// Curve reconstruction using the Discrete Fourier Transform
//
Complex dft(float t, ftshape s)
{
Complex sm = new Complex(0, 0);
for(int j=0; j<s.size; j++)
{
Complex ker = new Complex(cos(s.freqs[j]*t), sin(s.freqs[j]*t));
Complex cf = s.coefs[j].mul(ker);
if (cf.abs() < 1e-6) break; // Truncate when terms become too small
sm = sm.add(cf);
}
return sm;
}
PVector dftv(float t, ftshape s)
{
Complex c = dft(t, s);
return new PVector((float)c.re, (float)c.im);
}
void settings()
{
if (recording)
size(2160, 2160, P2D);
else
size(800, 800, P2D);
smooth(8);
}
void setup()
{
noise = new OpenSimplexNoise(1335);
randomSeed(1335);
fac = width/800.0;
R = width/2;
courier = createFont("Courier New",28*fac,true);
butterfly = new ftshape("butterfly.txt", 600);
letterpi = new ftshape("pi.txt", 400);
dragon = new ftshape("dragon.txt", 1000);
t_rex = new ftshape("t-rex.txt", 1000);
dance = new ftshape("dancing.txt", 1000);
homer = new ftshape("homer.txt", 1000);
dove = new ftshape("dove.txt", 800);
rocket = new ftshape("rocket.txt", 800);
love = new ftshape("love.txt", 1000);
yourheart = new ftshape("yourheart.txt", 1000);
tree = new ftshape("tree.txt", 1000);
labyrinth = new ftshape("labyrinth.txt", 1000);
phoenix = new ftshape("phoenix.txt", 1000);
spiral = new ftshape("spiral.txt", 1000);
stars = new star[num_stars];
for(int i=0; i<num_stars; i++)
{
stars[i] = new star();
}
}
int num_curves = 17;
PVector curve(int i, float theta)
{
//i=1;
/* switch( i % num_curves )
{
case 0:
return rcircle(theta, 1.4*R/2); // Simplicity
case 1:
return dftv(theta, butterfly).mult(1.2*R); // Transformation
case 2:
return dftv(theta, tree).mult(1.4*R); // Growth
case 3:
return dftv(theta, spiral).mult(1.4*R); // Evolution
case 4:
return dftv(theta, labyrinth).mult(2.5*R); // Challenges
case 5:
return dftv(theta, dragon).mult(1.8*R); // Fears
case 6:
return dftv(theta, t_rex).mult(1.4*R); // Power and Survival
case 7:
return dftv(theta, rocket).mult(3*R); // Ambition
case 8:
return dftv(theta, letterpi).mult(1.2*R); // Knowledge
case 9:
return dftv(theta, dance).mult(1.5*R); // Harmony
case 10:
return dftv(theta, love).mult(1.6*R); // Emotion
case 11:
return rheart(theta, 0.7*R); // Deeper emotion
case 12:
return dftv(theta, phoenix).mult(1.4*R); // Rebirth
case 13:
return dftv(theta, homer).mult(1.4*R); // Humanity
case 14:
return dftv(theta, dove).mult(1.4*R); // Peace
case 15:
return dftv(theta, yourheart).mult(1.4*R); // Introspection
default:
return rsquare(theta, 1.4*R/2);
} */
int j = int(num_points*theta/TWO_PI);
switch( i % num_curves )
{
case 0:
return rcircle(theta, 1.4*R/2); // Simplicity
case 1:
return butterfly.path[j].copy().mult(1.2*R); // Transformation
case 2:
return tree.path[j].copy().mult(1.4*R); // Growth
case 3:
return spiral.path[j].copy().mult(1.4*R); // Evolution
case 4:
return labyrinth.path[j].copy().mult(2.5*R); // Challenges
case 5:
return dragon.path[j].copy().mult(1.8*R); // Fears
case 6:
return t_rex.path[j].copy().mult(1.4*R); // Power and Survival
case 7:
return rocket.path[j].copy().mult(3*R); // Ambition
case 8:
return letterpi.path[j].copy().mult(1.2*R); // Knowledge
case 9:
return dance.path[j].copy().mult(1.5*R); // Harmony
case 10:
return love.path[j].copy().mult(1.6*R); // Emotion
case 11:
return rheart(theta, 0.7*R); // Deeper emotion
case 12:
return phoenix.path[j].copy().mult(1.4*R); // Rebirth
case 13:
return homer.path[j].copy().mult(1.4*R); // Humanity
case 14:
return dove.path[j].copy().mult(1.4*R); // Peace
case 15:
return yourheart.path[j].copy().mult(1.4*R); // Introspection
default:
return rsquare(theta, 1.4*R/2);
}
}
PVector transition(PVector p, PVector q, float t, boolean orient, int i)
{
switch(i)
{
case 0:
return vflip(p, q, t); // rotater2(p, q, t, orient);
case 1:
return sineWaveTransition(p, q, t, width/3, 0.5);
case 2:
return circularPathTransition(p, q, easeOutElastic(t), orient);
case 3:
return bezierTransition(p, q, new PVector(0,0),t);
case 4:
return hflip(p, q, t);
case 5:
return elasticBounceTransition(p, q, t);
case 6:
return springDampedTransition(p, q, t);
case 7:
return spiralTransition(p, q, t, 50, 3);
case 8:
return rotater2(p, q, t, orient);
case 9:
return rotater2(p, q, t, orient);
case 10:
return springDampedTransition(p, q, t);
case 11:
return bezierTransition(p, q, new PVector(width/2,height/2),t);
case 12:
return elasticBounceTransition(p, q, t);
case 13:
return spiralTransition(p, q, t, 50, 3);
case 14:
return bezierTransition(p, q, new PVector(-width/3,height/3),t);
case 15:
return rotater(p, q, t, orient);
default:
return zoomTransition(p, q, easeOutElastic(t));
}
}
void draw()
{
float t = (1.0*(frameCount-1)/total_frames) % 1;
background(0);
translate(width/2, height/2);
// stars
for(star s: stars)
s.draw(t);
fill(0);//noFill();
stroke(255);
strokeWeight(1.5);
thing tg = new thing(0, 0);
tg.draw(t);
// texts
fill(255);stroke(255);
textAlign(CENTER, CENTER);
textFont(courier);
scale(0.5);
text("@infinitymathart • 2024",0.0*width,0.8*height);
if (recording)
{
if (frameCount <= total_frames)
{
// saveFrame("/Volumes/waldeck/frames/s/frame_####.png");
saveFrame("/tmp/s/frame_####.png");
println(frameCount+"/"+total_frames);
}
else
{
println("All frames were saved");
stop();
exit();
}
}
}
/* License:
*
* Copyright (c) 2024 Waldeck Schützer
*
* All rights reserved.
*
* This code after the template and the related animations are the property of the
* copyright holder. Any reproduction, distribution, or use of this material,
* in whole or in part, without the express written permission of the copyright
* holder is strictly prohibited.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment