Created
September 30, 2024 16:03
-
-
Save wschutzer/7f89d66eaa2fc94fae08a3a61e6d6741 to your computer and use it in GitHub Desktop.
Trefoil knot with digits
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
// Digits on a trefoil knot | |
// ------------------------ | |
// | |
// Processing code by Waldeck Schützer (@infinitymathart) | |
// Motion blur template by @beesandbombs, explanation/article: https://bleuje.com/tutorial6/ | |
// Idea and concept by @etinjcb | |
// | |
import peasy.*; | |
import processing.core.PMatrix3D; | |
PeasyCam cam; | |
boolean recording = true; | |
int frame_size = recording ? 2160 : 800; | |
float fac = frame_size/800.0; | |
int numSegments = 600; // Number of segments along the Möbius ring (higher means smoother) | |
int num_faces = 60; | |
float surfaceRadius = frame_size/10; // 200*fac; // Radius of the Möbius ring | |
float tf_radius = 0.5; // Model radius | |
int num_rows = 360; // For the digits | |
int num_cols = 30; | |
float gscl = 50.0/fac; // Proportional font scaling (digits) | |
int FPS = 60; | |
int numFrames = 60*FPS; | |
int samplesPerFrame = 5; | |
float shutterAngle = 2.1; | |
float bht = 2.0; // Brightness adjustment | |
color c_digit = color(255); | |
color c_digit_flip = color(200,180); | |
PFont courier; | |
PFont fdigits; | |
int[][] result; | |
float t, c; | |
int rows = 10; | |
int cols = 10; | |
thing[] things; | |
float ease(float p) { | |
return 3*p*p - 2*p*p*p; | |
} | |
float ease(float p, float g) { | |
if (p < 0.5) | |
return 0.5 * pow(2*p, g); | |
else | |
return 1 - 0.5 * pow(2*(1 - p), g); | |
} | |
float ease_wide(float p, float g) // p in range -1 to 1 instead of 0-1 | |
{ | |
return 2*ease((p+1)/2,g)-1; | |
} | |
float mn = .5*sqrt(3), ia = atan(sqrt(.5)); | |
void push() { | |
pushMatrix(); | |
pushStyle(); | |
} | |
void pop() { | |
popStyle(); | |
popMatrix(); | |
} | |
void draw() { | |
if (!recording) { | |
t = mouseX*1.0/width; | |
c = mouseY*1.0/height; | |
// if (mousePressed) | |
// println(c); | |
draw_(); | |
} else { | |
for (int i=0; i<width*height; i++) | |
for (int a=0; a<3; a++) | |
result[i][a] = 0; | |
c = 0; | |
for (int sa=0; sa<samplesPerFrame; sa++) { | |
t = map(frameCount-1 + sa*shutterAngle/samplesPerFrame, 0, numFrames, 0, 1); | |
push(); | |
draw_(); | |
loadPixels(); | |
for (int i=0; i<pixels.length; i++) { | |
result[i][0] += pixels[i] >> 16 & 0xff; | |
result[i][1] += pixels[i] >> 8 & 0xff; | |
result[i][2] += pixels[i] & 0xff; | |
} | |
pop(); | |
} | |
loadPixels(); | |
for (int i=0; i<pixels.length; i++) | |
{ | |
int r = int(constrain(1.0*result[i][0]/samplesPerFrame*bht,0,255)); | |
int g = int(constrain(1.0*result[i][1]/samplesPerFrame*bht,0,255)); | |
int b = int(constrain(1.0*result[i][2]/samplesPerFrame*bht,0,255)); | |
pixels[i] = 0xff << 24 | | |
r << 16 | | |
g << 8 | | |
b; | |
} | |
updatePixels(); | |
saveFrame("/tmp/frame_####.png"); | |
println(frameCount,"/",numFrames); | |
if (frameCount==numFrames) | |
exit(); | |
} | |
} | |
// End of Template | |
// Begin of Trefoil Knot with Digits code | |
void settings() | |
{ | |
size(frame_size,frame_size,P3D); | |
pixelDensity(1); | |
smooth(8); | |
} | |
void setup() | |
{ | |
randomSeed(1337); | |
result = new int[width*height][3]; | |
//cam = new PeasyCam(this, 0*fac, 0*fac, 0*fac, 600*fac); | |
cam = new PeasyCam(this, 3.4882555 *fac, -9.970151 *fac, -0.10424426 *fac, 600.0 *fac); | |
cam.setRotations( 0.0449344 , -0.09821731 , 0.33823663 ); | |
if (recording) | |
cam.setActive(false); | |
courier = createFont("Courier New",24*fac,true); | |
fdigits = createFont("HelveticaNeue-Ultralight",36*fac,true); | |
// Things are random digits to be drawn on each of the 4 faces of the surface | |
// (actually there are only two faces) | |
things = new thing[num_rows*num_cols]; | |
for(int j=0; j<num_rows*num_cols;j++) | |
things[j] = new thing(); | |
} | |
void draw_() | |
{ | |
background(0); | |
noStroke();//stroke(128); // Draw lines around the rectangles | |
fill(0); //fill(150*lvl, 200*lvl, 255*lvl); | |
lights(); | |
directionalLight(255, 255, 255, 0, 0, 1); | |
pushMatrix(); | |
drawSurface(); | |
popMatrix(); | |
// texts | |
pushMatrix(); | |
float[] rot = cam.getRotations(); | |
rotateX(rot[0]); | |
rotateY(rot[1]); | |
rotateZ(rot[2]); | |
translate(0,0,(float)cam.getDistance()-260*fac); | |
fill(255);stroke(255); | |
textAlign(CENTER, CENTER); | |
textFont(courier); | |
textSize(5*fac); | |
text("@infinitymathart • 2024",0.0*width,0.13*height); | |
popMatrix(); | |
} | |
// Rendering the Möbius ring using the segment class for each segment | |
// | |
void drawSurface() | |
{ | |
// Draw the surface | |
// | |
rotateY(TAU*t); | |
rotateX(2*TAU*t); | |
for (int i = 0; i < numSegments; i++) | |
{ | |
float u1 = map(i, 0, numSegments, 0, TAU); // u parameter for current segment | |
float u2 = map(i + 1, 0, numSegments, 0, TAU); // u parameter for next segment | |
beginShape(QUAD_STRIP); | |
for(int j=0; j<num_faces; j++) | |
{ | |
float v = map(j, 0, num_faces-1, 0, TAU); | |
PVector p1 = s(u1, v, tf_radius).mult(surfaceRadius); | |
PVector p2 = s(u2, v, tf_radius).mult(surfaceRadius); | |
vertex(p1.x, p1.y, p1.z); | |
vertex(p2.x, p2.y, p2.z); | |
} | |
endShape(CLOSE); | |
} | |
// Draw text on the surface | |
for(int i=0; i<num_rows-1; i++) | |
{ | |
float u = map(i, 0, num_rows-1, 0, TAU)+TAU*t; | |
for(int j=0; j<num_cols-1; j++) | |
{ | |
float v = map(j, 0, num_cols-1, 0, TAU)-6*TAU*t + ((i % 2 == 0) ? TAU/num_cols*0.5 : 0); | |
noStroke(); | |
thing tg = things[j*num_cols+i]; | |
if (tg.flipping()) fill(c_digit_flip); else fill(c_digit); | |
textFont(fdigits); | |
textSize(4*fac); | |
drawText(tg.get(), u, v); | |
} | |
} | |
} | |
// The parametric equations of the surface | |
// | |
PVector s(float u, float v, float r) | |
{ | |
float q = 8*cos(3*u) + 17; | |
float p = 6*cos(3*u); | |
return new PVector( | |
sin(u) + 2*sin(2*u) - (4*sin(2*u)-sin(u))/sqrt(q)*r*cos(v) + p*(4*cos(2*u)+cos(u))/sqrt(q*p*p+q*q)*r*sin(v), | |
cos(u) - 2*cos(2*u) + (cos(u)+4*cos(2*u))/sqrt(q)*r*cos(v) - p*(sin(u)-4*sin(2*u))/sqrt(q*p*p+q*q)*r*sin(v), | |
q/sqrt(q*p*p+q*q)*r*sin(v)-2*sin(3*u) | |
); | |
} | |
// Numerical partial derivative with respect to u | |
// | |
PVector dsdu(float u, float v, float r) | |
{ | |
float h = 0.01; | |
return (s(u+h, v, r).sub(s(u-h, v, r))).mult(0.5*h); | |
} | |
// Numerical partial derivative with respect to v | |
// | |
PVector dsdv(float u, float v, float r) | |
{ | |
float h = 0.01; | |
return (s(u, v+h, r).sub(s(u, v-h, r))).mult(0.5*h); | |
} | |
// Normal vector at the point | |
// | |
PVector normal_vector(float u, float v, float r) { | |
PVector du = dsdu(u, v, r); | |
PVector dv = dsdv(u, v, r); | |
PVector n = dv.cross(du); | |
return n.normalize(); | |
} | |
// System of coordinates at a point on the surface | |
// | |
Coords surfCoords(float u, float v, float r) | |
{ | |
PVector du = dsdu(u,v,r).normalize(); | |
PVector dv = dsdv(u,v,r).normalize(); | |
PVector n = dv.cross(du).normalize(); | |
return new Coords( s(u,v,r).mult(surfaceRadius), du, dv, n ); | |
} | |
// Coords class to represent the coordinate system at a point on the surface | |
// | |
class Coords { | |
PVector p; // The point on the surface | |
PVector du; // Tangent vector along u | |
PVector dv; // Tangent vector along v | |
PVector n; // Normal vector | |
Coords(PVector p_, PVector du_, PVector dv_, PVector n_) { | |
p = p_.copy(); | |
du = du_.copy(); | |
dv = dv_.copy(); | |
n = n_.copy(); | |
} | |
PMatrix3D basisChange(Coords other) { | |
PMatrix3D inv = other.getBasisMatrix(); | |
inv.invert(); // Get the inverse of the other system's basis | |
PMatrix3D result = getBasisMatrix(); | |
result.apply(inv); // Apply inverse to get change of basis | |
return result; | |
} | |
PMatrix3D getBasisMatrix() { | |
return new PMatrix3D( | |
du.x, dv.x, n.x, 0, | |
du.y, dv.y, n.y, 0, | |
du.z, dv.z, n.z, 0, | |
0, 0, 0, 1 | |
); | |
} | |
} | |
// General function to draw text on the surface using basis transformation | |
// | |
void drawTextOnSurface(PVector position, PMatrix3D basisChangeMatrix, String txt, float scl) | |
{ | |
pushMatrix(); // Save the current transformation matrix | |
// Move to the position where the text should be placed | |
translate(position.x, position.y, position.z); | |
// Apply the basis change matrix to align the text correctly | |
applyMatrix(basisChangeMatrix); | |
// Draw the text | |
translate(0,0,0.05*fac); // move text up and away from the surface slightly | |
scale(scl); | |
text(txt, 0, 0); // Draw text at the origin | |
popMatrix(); // Restore the original transformation matrix | |
} | |
// Draw text on the surface | |
// | |
void drawText(String txt, float u, float v) | |
{ | |
Coords globalCoords = new Coords( | |
new PVector(0, 0, 0), | |
new PVector(0, -1, 0), | |
new PVector(-1, 0, 0), | |
new PVector(0, 0, 1) | |
); // Global coordinate system | |
// Surface coordinate system | |
Coords localCoords = surfCoords(u, v, tf_radius); | |
// Compute the change of basis matrix | |
PMatrix3D basisChangeMatrix = localCoords.basisChange(globalCoords); | |
// Get the length of the tangent vector along the u-direction | |
float g = gscl*dsdu(u, v, tf_radius).mult(surfaceRadius).mag(); | |
// Draw the text on the top face | |
drawTextOnSurface(localCoords.p, basisChangeMatrix, txt, g); | |
} | |
void mousePressed() | |
{ | |
float[] rotations = cam.getRotations(); | |
float[] look = cam.getLookAt(); | |
double dist = cam.getDistance(); | |
println("..:"); | |
println(" cam = new PeasyCam(this, ", look[0], "*fac, ", look[1], "*fac, ", look[2], "*fac, ", dist,"*fac);" ); | |
println(" cam.setRotations(", rotations[0], ", ", rotations[1], ", ", rotations[2],");\n\n"); | |
} | |
class thing | |
{ | |
char c; // First digit | |
char c1; // Second digit (flipped) | |
float offset; // Offset at which it begins to flip | |
boolean flips; // Does this thing flip or not? | |
thing() | |
{ | |
c = "0123456789".charAt(int(random(0,10))); | |
do { | |
c1 = "0123456789".charAt(int(random(0,10))); | |
} | |
while (c1 == c); | |
flips = random(0,1) > 0.6; | |
offset = random(0, 1); | |
} | |
boolean flipping() | |
{ | |
if (flips) | |
{ | |
float tt = (t+offset)%1; | |
if ( (tt > 0.38 && tt < 0.40) || (tt > 0.68 && tt < 0.70 ) ) | |
return true; | |
} | |
return false; | |
} | |
// Returns c, unless it flips and it's time to flip, in which case it returns c1 | |
String get() | |
{ | |
return flipping() ? ""+c1 : ""+c; | |
} | |
} | |
/* 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