Monday, September 13, 2021

stuntcoding and the power of sin (and cos)

 A while back I made this (somewhat pretentious) business card:

It has a small Processing program on the back - in 2014 I impressed Ben Fry, co-founder of Processing, by having it there.

You can see the program in action at kirk.is/card/. The code is 
//spiro.pde - see processing.org
float a,b;
void draw(){
  noStroke(); fill(0,10);
  rect(0,0,100,100); fill(255);
  ellipse(50+(20*cos(a)+10*cos(b)),
          50+(20*sin(a)+10*sin(b)),
          3,3);
  a += map(mouseX,0,100,-.5,.5);
  b += map(mouseY,0,100,-.5,.5);
} //see in action: http://kirk.is/card/
It's nothing too fancy... just using the mouseX and mouseY as speed controls for a pair of nested circles - very much like a spirograph. ("draw()" is the p5 function called every tick of the clock)

One clever trick is rather than erasing the window each click, it puts a rectangle over of black at 10% transparency, leading to a nice little fade out effect and letting the user see the pattern as it is made.

But then the other day I ran across this beauty at fyprocessing on tumblr: (written in p5, processing's Javascript descendent)

which is made from this snippet, that fits in 1 280-char tweet (barely!)
t=0
draw=_=>{createCanvas(w=500,w)
n=sin
t+=.03
for(c=0;c<w;c+=31)for(r=0;r<w;r+=31)if((r+c)%2<1){beginShape(fill(0))
for(i=0;i<4;i++)for(b=i*PI/2+max(min(n(t+dist(w,w,c,r))*1.5+n(t/2)/2,1),
-1)*PI,a=b+3.9;a>b+2.3;a-=.1)vertex(c+cos(b)*32+cos(a)*18,r+n(b)*32+n(a)*18)
endShape()}}
Here's the tweet that it came from.  You can see it in an editor here.

That is amazing and beautiful, and I didn't understand it all. First I need to start to unobfuscate it: (to be fair it wasn't obfuscated to make it confusing, just concise.)

My first step was to just reformat it and change just a few of the syntatic shortcuts:
let t=0;
function draw(){
  createCanvas(w=500,w);
  let n=sin;
  t+=.03;
  for(c=0;c<w;c+=31){
    for(r=0;r<w;r+=31){
      if((r+c)%2<1){
        beginShape();
        fill(0);
        for(i=0;i<4;i++) {
          for(b=i*PI/2+max(min(n(t+dist(w,w,c,r))*1.5+n(t/2)/2,1),-1)*PI,
          a=b+3.9; a>b+2.3; a-=.1) {
            vertex(c+cos(b)*32+cos(a)*18,r+n(b)*32+n(a)*18);
          }
        }
        endShape()
      }
    }
  }
}
I swapped in `function draw()` for `draw=_=>` - I was looking up what "_" meant before I realized it was just a throw away variable for the fat arrow notation. 

He also uses "n" as a shortcut for "sin". I'm going to undo that, and then change the complicated for() loop into its "while" equivalent.
let t=0;
const SZ = 500;
function draw(){
  createCanvas(SZ,SZ);
  t+=.03;
  for(c=0;c<SZ;c+=31){
    for(r=0;r<SZ;r+=31){
      if((r+c)%2<1) {
        beginShape();
        fill(0);
        for(i=0;i<4;i++){
            b=i*PI/2+max(min(sin(t+dist(SZ,SZ,c,r))*1.5+sin(t/2)/2,1),-1)*PI;
            a=b+3.9; 
            while(a>b+2.3) {
              vertex(c+cos(b)*32+cos(a)*18,r+sin(b)*32+sin(a)*18);
              a-=.1;
            }
          }
        endShape()
      }
    }
  }
}
Also I replaced the cleverly initialized "w" variable with a big old const SZ, my favorite shortcut for the size of a thing. 

So now I'm at a place to start poking at variables. "t" seems to be the basic timer; by incrementing by more per click I can speed it up, for instance. Or make it stay at 0.0 -- things are frozen (but when you freeze time like that, many of the spinners are at odd angles... interesting ) And it's pretty clear "r" and "c" are for rows and columns - incrementing more than that "31" spaces them out.  

So we're making a bunch of shapes. 
      if((r+c)%2<1) {
is making it like a checkerboard pattern... I added
fill(200,0,0); circle(c,r,10);
after the EndShape and then I can see a the center of each formation.

So now, looking at the code... the loop with i is being done 4 times.

To try and make things more clear, I replace the vertex() in the shape
    vertex(c+cos(b)*32+cos(a)*18,r+sin(b)*32+sin(a)*18);
 with a tiny box, so I can see whats being drawn:
    rect(c+cos(b)*32+cos(a)*18,r+sin(b)*32+sin(a)*18,1,1);
 but it doesn't help me see much, I guess things are tightly packed within the shape. I see a is being adjusted by .1 each iteration... bumping that up to .3 and the way things are drawn is more clear, especially with my dots placed at the center of each thing.
So a is iterating to draw one of the 4 sides. It starts at whatever b is plus 3.9, and then goes down to b plus 2.3... in other words, ranges PI * ( 5 / 4) down to PI * (3 / 4)

So I put the vertex() line back, because now I roughly get it what's going on with the draw shape stuff. There's still some "spirograph" cleverness with a big circle plus a small circle (and at related angles) but still.

If I throw away b's fancy computed value and replace it with  b = i * PI/2; I see everything locked down- and I can add a constant to that see all the widgets move at the same angle, and 
b = mouseX / 10 +  i * PI/2;
is a very nice effect I can control. (remember, i is just 0, 1, 2 and 3, so we're just doing 4 sides of the circle, so to speak.)

So that just leaves us with this monster that effectively sets b for each spinner to a constant (outside of the i * PI /2 boost):
max(min(sin(t+dist(SZ/2,SZ/2,c,r))*1.5+sin(t/2)/2,1),-1)*PI

yikes!

One thing I notice is that dist() call - this is what sets each spinner at a different place in the math, based on its position on screen. That's pretty clever! 

Ok, so to get a grip on this, I simplify the r + c counters so I just see a single spinner:
  for(c=100;c<101;c+=31){
    for(r=100;r<101;r+=31){
this will make it easier to see the patterns in side the massive b equation.  Even just watching one alone, it has a kind of hypnotic chaos to it. 

So every draw() it is createCavas()ing, I move that to a separate setup() so it just happens once, then I actually plot *one* of the values of B for my single spinner, sort of like a cardiogram:
if(i == 0) {
  stroke(128); rect(t*6,SZ/2 + b*10,2,2); 
  stroke(200); rect(t*6,SZ/2 ,2,2); 
}
that looks pretty cool actually:

So that's informative! We see how B gets clamped at 1 and -1 - but sometimes that's at the top of the arch, and sometimes it's at the bottom

I see that one part of the equation is really the heart, everything else is min/max. So I isolate it and call it minguts:
const minguts = sin(t+dist(SZ/2,SZ/2,c,r))*1.5 + sin(t/2)/2;  
then the equation for b is much simpler:
b=i*PI/2+max(min(minguts,1),-1)*PI;
Using a variation of the "cariogram" above, I plot out minguts in red, then min(minguts,1) in green then max(min(minguts,1),-1) in blue:
So the blue line is basically what we get for b, it's the end result. And red, the minguts... looking at its euation we see it's just adding two cycles of sin waves - the larger one influenced by that dist() stuff, the smaller one changing more slowly (t/2) and having less of an effect (the /2). Then the min and the max is just putting a ceiling and a floor to it- when it's at 1 or -1, I'm betting the spinner is straight one way or the other, and all the other values represent some level of spin.

If you want to see my "testbed" I hacked apart- kind of like dissecting a frog - it's here

So wow. I am in awe of this! It uses so much spatial smarts to make something really visually appealing. (I've seen that "spinners move to form negative space circles with their neighbors around) 

You can see more of Andy Makes tweet length code examples or more of his art and games on his website.

No comments:

Post a Comment