Created
October 11, 2024 14:07
-
-
Save wschutzer/a1e30fda8fbe73d8bbbc6e3a0299b08e to your computer and use it in GitHub Desktop.
2D shape transitions
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
// 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