Learn Creative Coding (#13) - Trigonometry for Artists

in StemSocial7 days ago

Learn Creative Coding (#13) - Trigonometry for Artists

cc-banner

Math class made trig feel like punishment. SOH-CAH-TOA, word problems about ladders against walls, test anxiety. Forget all of that. In creative coding, trigonometry is a drawing tool. Possibly the most powerful drawing tool you'll ever learn. It turns angles into curves, circles into spirals, and simple parameters into endlessly varied organic forms.

Every circular pattern, every spiral, every oscillation, every wave, every mandala, every pulsing animation you've ever seen in creative coding? Trig. It's all trig. Once you internalize the core formula -- and it really is just one core formula -- an entire universe of visual possibilities opens up. I'm not exaggerating. This episode changed how I think about code-as-art more than any other :-)

Polar coordinates: the one formula

We've been working in cartesian coordinates since episode 9: (x, y), a grid. But there's another way to describe a point in space: with an angle and a distance from the center. That's polar coordinates: (angle, radius).

Converting between them uses exactly two trig functions:

// polar to cartesian -- THE formula
x = centerX + cos(angle) * radius;
y = centerY + sin(angle) * radius;

That's it. Those two lines power everything in this episode. cos gives you the horizontal component of a direction, sin gives you the vertical component. Together they map any angle + distance to an x,y position on your canvas.

If you sweep the angle from 0 to TWO_PI (a full rotation, 360 degrees in radians) while keeping the radius constant, you trace out a circle:

function setup() {
  createCanvas(500, 500);
  background(20);
  stroke(255);
  strokeWeight(2);
  noFill();

  let cx = 250, cy = 250, r = 150;

  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.05) {
    let x = cx + cos(a) * r;
    let y = cy + sin(a) * r;
    vertex(x, y);
  }
  endShape(CLOSE);
}

A circle drawn with math instead of ellipse(). Why bother? Because now we can manipulate every single point on that circle independently. We can distort it, animate it, vary the radius per-angle, color it based on position -- things that ellipse() will never let you do. That's the whole point: once you describe a shape as a mathematical relationship instead of a function call, you own it completely.

The step size (0.05) controls smoothness. Smaller values = smoother circle but more vertices. For a 150px radius circle, 0.02 to 0.05 gives you a nice smooth curve. For a tiny circle, you can get away with 0.1.

A quick note on radians: p5.js uses radians by default, not degrees. A full circle is TWO_PI (about 6.283), not 360. Half a circle is PI. A quarter is HALF_PI. If radians feel unnatural, you can call angleMode(DEGREES) and use 0-360 instead -- but I'd encourage you to get comfortable with radians. Every creative coding resource, every shader tutorial, every math reference uses them. The conversion is simple: multiply degrees by PI/180 to get radians. After a few days of using TWO_PI and HALF_PI, your brain just adapts.

Deforming circles into organic blobs

Remember the noise function we built from scratch in episode 12? Here's where it meets polar coordinates. Add noise to the radius and the circle becomes alive:

function setup() {
  createCanvas(500, 500);
  background(20);
  stroke(200, 100, 255);
  strokeWeight(2);
  noFill();

  let cx = 250, cy = 250;

  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.02) {
    let r = 120 + noise(cos(a) + 1, sin(a) + 1) * 80;
    let x = cx + cos(a) * r;
    let y = cy + sin(a) * r;
    vertex(x, y);
  }
  endShape(CLOSE);
}

An organic blob. The radius varies smoothly around the perimeter because we're sampling noise along the circle. Here's the trick that makes it seamless: we use cos(a) and sin(a) as noise inputs instead of a directly. Why? Because cos and sin are periodic -- they return to their starting values after a full rotation. So the noise input at angle 0 equals the input at angle TWO_PI, and the shape closes perfectly. If you used a directly as the noise input, there'd be a visible seam where the shape starts and ends.

This is one of those techniques that looks clever but is actually just understanding what the functions do. And it works with any periodic input -- you can trace noise along any closed path using the same principle. Try nesting multiple noise calls with different scales for multi-layered organic shapes. The blobs get wild.

You can also stack multiple layers of distortion -- same octave layering concept we built for our Perlin noise implementation last episode, but now applied to shape generation:

let r = 100;
r += noise(cos(a) + 1, sin(a) + 1) * 60;          // broad shape
r += noise(cos(a * 3) + 2, sin(a * 3) + 2) * 20;  // medium detail
r += noise(cos(a * 7) + 5, sin(a * 7) + 5) * 8;   // fine wrinkles

The blob gets more organic with each layer. Each frequency multiplier on a adds finer-grained variation, just like our fractal noise octaves added progressively finer detail to textures.

Spirals: radius grows with angle

Make the radius increase as the angle goes around and you get a spiral:

function setup() {
  createCanvas(500, 500);
  background(20);
  noFill();

  let cx = 250, cy = 250;

  beginShape();
  for (let a = 0; a < TWO_PI * 8; a += 0.05) {
    let r = a * 3;  // radius grows linearly with angle
    let x = cx + cos(a) * r;
    let y = cy + sin(a) * r;

    stroke(map(a, 0, TWO_PI * 8, 100, 255), 100, 200);
    strokeWeight(map(a, 0, TWO_PI * 8, 0.5, 3));
    vertex(x, y);
  }
  endShape();
}

An Archimedean spiral -- eight full rotations, radius growing from 0 to about 150. I added color and thickness that increase with the angle, so the outer loops are brighter and bolder. This gives the spiral a sense of growth, of building energy outward. Without it, a spiral is just a line. With it, it tells a story.

The spacing between loops is even because the growth is linear. Change the growth formula and you get fundamentally different spirals:

  • r = a * 3 -- Archimedean (even spacing, like a vinyl record)
  • r = pow(1.1, a) -- logarithmic (spacing increases outward, like a nautilus shell)
  • r = 100 + sin(a * 5) * 20 -- wavy spiral (the radius oscillates)
  • r = a * 3 * (1 + sin(a * 10) * 0.1) -- Archimedean with ripple edges

Spirals appear everywhere in nature and art. The logarithmic spiral in particular shows up in galaxies, hurricanes, nautilus shells, and the Fibonacci spiral. Understanding the math lets you generate all of these from the same two lines of code -- just different radius functions.

One practical tip: when drawing spirals, you often want the line to start thin and get thicker, or to change color along the way. Since you're iterating through angle values, you have a natural "progress" variable (how far along the spiral you are) that you can map to any visual property. This is the same pattern we'll keep using throughout the series -- mathematical iteration gives you a progress value, and progress maps to visuals. Trig gives you the shape. Mapping gives you the aesthetics.

Lissajous curves: two frequencies, infinite patterns

What happens when you use two sin waves at different frequencies, one for X and one for Y?

function setup() {
  createCanvas(500, 500);
  background(20);
  stroke(100, 200, 255);
  strokeWeight(1.5);
  noFill();

  let cx = 250, cy = 250;
  let A = 3, B = 4;  // frequency ratio

  beginShape();
  for (let t = 0; t < TWO_PI * 2; t += 0.01) {
    let x = cx + sin(A * t) * 180;
    let y = cy + sin(B * t) * 180;
    vertex(x, y);
  }
  endShape();
}

Lissajous curves. Named after Jules Antoine Lissajous, a French physicist who studied them using tuning forks and mirrors in the 1850s. The math hasn't changed since then -- just the medium. The ratio between A and B determines the pattern. 1:2 gives a figure-8. 3:4 gives a pretzel-like weave. Try 5:6, 7:8, 3:5 -- each ratio creates a unique, symmetrical pattern. When A and B are close in value, the curves get complex and dense. When one is much larger, you get tight weaving.

Add a phase offset for asymmetric forms:

let x = cx + sin(A * t + HALF_PI) * 180;

The phase offset rotates one of the oscillations, breaking the symmetry and producing entirely different shapes from the same frequency ratio. Animate the phase over time and the curve morphs smoothly from one form to another -- mesmerizing to watch.

Here's a grid of Lissajous patterns, showing how the ratios create variety:

function setup() {
  createCanvas(600, 600);
  background(20);
  noFill();
  strokeWeight(1);

  for (let row = 1; row <= 4; row++) {
    for (let col = 1; col <= 4; col++) {
      let cx = col * 130 - 30;
      let cy = row * 130 - 30;
      let A = col, B = row;

      stroke(map(col, 1, 4, 100, 255), 150, map(row, 1, 4, 255, 100));
      beginShape();
      for (let t = 0; t < TWO_PI * 4; t += 0.01) {
        vertex(cx + sin(A * t) * 50, cy + sin(B * t) * 50);
      }
      endShape();
    }
  }
}

A 4x4 grid of 16 different curves, each determined by a simple integer ratio. This is a classic generative art output -- systematic variation of mathematical parameters producing visual richness. The kind of thing you could print on a poster.

Rose curves: flowers from math

A polar equation: r = cos(n * angle) traces out petal-like shapes:

function setup() {
  createCanvas(500, 500);
  background(20);
  noFill();

  let cx = 250, cy = 250;
  let n = 5;  // odd n = n petals, even n = 2n petals

  stroke(255, 100, 150);
  strokeWeight(2);

  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.01) {
    let r = cos(n * a) * 180;
    let x = cx + cos(a) * r;
    let y = cy + sin(a) * r;
    vertex(x, y);
  }
  endShape(CLOSE);
}

n=3 gives 3 petals. n=5 gives 5 petals. n=4 gives 8 petals (even numbers double the count). And here's where it gets really interesting: use fractional values like n=2.5 and the petals overlap -- the curve needs multiple full rotations to close on itself, creating intricate layered patterns.

Try n=0.5 with many rotations, or n=PI (irrational -- the curve never closes, it fills the circle). These are the kind of discoveries you make when you play with parameters instead of memorizing formulas. Every value of n gives you a different flower.

You can also combine rose curves with noise from our episode 12 implementation. Vary the amplitude per-petal by multiplying the radius with a noise value sampled at the current angle. Some petals get bigger, some smaller, and suddenly your mathematical flower looks hand-drawn. Layer multiple rose curves at different n values with different colors and opacity, and you get compositions that look like botanical illustrations generated purely from math. There's an entire generative art subgenre built on this idea.

Phyllotaxis: the sunflower pattern

This is my absolute favorite thing in all of creative coding. The golden angle (137.508 degrees) produces the exact spiral pattern seen in sunflowers, pinecones, and romanesco broccoli. It's not a coincidence -- it's evolution optimizing for maximum packing efficiency, and it converges on this specific angle.

function setup() {
  createCanvas(500, 500);
  background(20);
  noStroke();
  colorMode(HSB, 360, 100, 100);

  let cx = 250, cy = 250;
  let goldenAngle = 137.508 * (PI / 180);  // degrees to radians

  for (let i = 0; i < 800; i++) {
    let angle = i * goldenAngle;
    let radius = sqrt(i) * 8;

    let x = cx + cos(angle) * radius;
    let y = cy + sin(angle) * radius;

    let hue = map(i, 0, 800, 30, 60);
    fill(hue, 80, 90 - i * 0.05);
    ellipse(x, y, map(i, 0, 800, 3, 10));
  }
}

Each seed is placed at the golden angle from the previous one, with radius proportional to sqrt(index). The square root gives even density -- without it, the outer seeds would be too spread out. The result is stunningly organic: spirals appear in both directions, and the number of spirals you count is always a Fibonacci number (13, 21, 34...).

Change goldenAngle even slightly -- try 137.0 or 138.0 -- and the beautiful pattern collapses into ugly radial lines. The precision matters. Nature figured this out over millions of years of evolution. We get to steal it in three lines of code :-)

Why the golden angle specifically? It's related to the golden ratio (phi, approximately 1.618). The golden angle is 360 / phi^2 degrees, which is approximately 137.508. What makes it special: it's the most "irrational" angle. No matter how many seeds you place, none of them line up radially. This maximizes the space each seed gets -- which is exactly what a plant needs for optimal light exposure. Math and biology converging on the same answer. I think about this every time I see a sunflower.

Animated trig: everything breathes

All these static curves come alive when you add frameCount as a time variable. Remember the animation loop from episode 11 where we moved particles each frame? Same idea -- but now instead of updating positions with velocity, we're computing positions directly from trig functions. Anything that uses sin or cos can oscillate:

function setup() {
  createCanvas(500, 500);
}

function draw() {
  background(15, 20);  // slight trail

  let cx = 250, cy = 250;
  let t = frameCount * 0.02;
  noStroke();

  for (let i = 0; i < 200; i++) {
    let angle = i * 0.3 + t;
    let r = 50 + i * 0.6 + sin(t + i * 0.1) * 20;

    let x = cx + cos(angle) * r;
    let y = cy + sin(angle) * r;

    let hue = (i * 2 + frameCount) % 360;
    fill(`hsla(${hue}, 70%, 60%, 0.6)`);
    ellipse(x, y, 4 + sin(t + i * 0.05) * 2);
  }
}

200 dots in a breathing, rotating spiral. The radius oscillates with sin, the whole thing rotates over time, the colors cycle through the spectrum. Hypnotic. The background(15, 20) with low alpha creates a motion trail effect -- previous frames fade slowly instead of being erased completely. Same trick we used for particle trails in episode 11 and it still looks gorgeous every time.

Notice how every element uses a slightly different phase (t + i * 0.1, t + i * 0.05). That phase offset is what creates the wave-like propagation -- each dot reaches its maximum radius at a slightly different time than its neighbor, creating the illusion of a wave moving through the spiral. Without the offset, all dots would breathe in unison, which looks mechanical. With it, the motion feels organic and alive. This is a huge principle: phase offsets turn synchronous motion into natural-looking waves. Remember it. You'll use it in every animation you ever make.

The key insight for trig animation: sin(time) oscillates between -1 and 1 forever. Use it to modulate anything: size, position, color, opacity, rotation speed. sin(time + offset) lets different elements oscillate at different phases, creating wave-like propogation effects. That's how you get ripples, breathing, pulsing -- any rhythmic visual movement.

Combining techniques: a trig-based generative piece

Let's put several of these ideas together into something you could actually frame and hang on a wall. Multiple rotating noise-deformed rings with phyllotaxis centers:

function setup() {
  createCanvas(600, 600);
  background(15);
  noFill();
  colorMode(HSB, 360, 100, 100, 100);

  let cx = 300, cy = 300;

  // draw 8 nested noise-deformed rings
  for (let ring = 0; ring < 8; ring++) {
    let baseR = 40 + ring * 30;
    let hue = map(ring, 0, 8, 200, 320);  // blue to magenta

    stroke(hue, 60, 80, 40);
    strokeWeight(1.5);

    beginShape();
    for (let a = 0; a < TWO_PI; a += 0.01) {
      let noiseR = baseR + noise(cos(a) + 1 + ring, sin(a) + 1) * 30;
      let wobble = sin(a * (ring + 2)) * (5 + ring * 2);
      let r = noiseR + wobble;
      vertex(cx + cos(a) * r, cy + sin(a) * r);
    }
    endShape(CLOSE);
  }

  // scatter phyllotaxis dots across the composition
  noStroke();
  let golden = 137.508 * PI / 180;
  for (let i = 0; i < 500; i++) {
    let angle = i * golden;
    let r = sqrt(i) * 12;
    let x = cx + cos(angle) * r;
    let y = cy + sin(angle) * r;

    let dist = sqrt((x - cx) ** 2 + (y - cy) ** 2);
    let hue = map(dist, 0, 300, 220, 340);
    fill(hue, 50, 90, map(i, 0, 500, 80, 20));
    ellipse(x, y, 3);
  }
}

Noise-deformed rings with trig wobble and a phyllotaxis point field on top. Every technique from this episode, combined. Vary the seed values and color ranges and you get a different composition each time. This is the power of understanding the math -- you're not following a tutorial, you're composing with functions.

At my job we did a creative coding workshop once and I showed the team this exact progression: circle, blob, spiral, sunflower, combined piece. The moment people saw the phyllotaxis pattern they lost their minds. Something about seeing math produce nature just hits different :-)

Trig beyond circles: oscillation everywhere

One more thing before we wrap up. sin and cos aren't just for circular things. They're oscillators. Anything that goes back and forth, up and down, bigger and smaller, louder and softer -- that's an oscillation, and you can model it with sin/cos.

// size that breathes
let size = 20 + sin(frameCount * 0.05) * 10;

// position that sways
let xOffset = sin(frameCount * 0.03) * 50;

// opacity that pulses
let alpha = map(sin(frameCount * 0.08), -1, 1, 50, 255);

// color that cycles (hue is already circular, sin maps to it naturally)
let hue = map(sin(frameCount * 0.02), -1, 1, 180, 300);

Any property of any visual element can be driven by a sin wave. Combine multiple sin waves at different frequencies and you get complex, organic-looking motion from simple math. This is the same principle behind audio synthesis, signal processing, and Fourier analysis -- but we're using it to make pretty things, which is objectively the better use case.

When to think in polar

Some signs that polar coordinates will simplify your code:

  • Things arranged in circles or arcs
  • Rotation of any kind
  • Spirals, wheels, clock faces
  • Radial symmetry (mandalas, snowflakes)
  • Things emanating from a center point
  • Oscillation, pulsing, breathing effects

If you catch yourself calculating x = centerX + something, y = centerY + something -- you're already in polar. Embrace it. Name your variables angle and radius instead of doing the mental gymnastics of converting everything to x,y in your head.

And remember: you can combine polar with cartesian. Use polar to generate a ring of points, then apply cartesian offsets to each one. Use polar for the overall layout and cartesian for the fine detail. They're not competing systems -- they're complementary lenses for looking at space.

Most creative coding sketches I write end up using both. Polar for the primary structure, cartesian for positioning that structure on the canvas, and then individual element variations in whichever system makes the math simpler. Once you get fluent in switching between them, layout problems that used to take 20 minutes of trial and error become obvious three-second decisions. It's one of those "level up" moments in your coding practice.

The other thing I want to mention: don't underestimate the power of layering simple trig forms. A single circle is boring. Three circles at different radii with different wobble frequencies? Interesting. Add a spiral of dots on top? Getting somewhere. Throw in some phyllotaxis seeds at the center? Now you've got a piece. The building blocks are all simple. The art is in the composition -- how you layer, color, and animate them. That's what separates techincal demos from actual generative art. And that's exactly what we'll keep exploring in the episodes ahead.

Allez, wa weten we nu allemaal?

  • Polar coordinates: x = cx + cos(angle) * radius, y = cy + sin(angle) * radius -- the one formula to rule them all
  • Deform circles by adding noise to the radius -- use cos(a)/sin(a) as noise inputs for seamless closure
  • Spirals: let radius grow with angle, different growth functions give different spiral types
  • Lissajous curves: different sin frequencies for X and Y, the ratio determines the pattern
  • Rose curves: r = cos(n * angle) -- odd n gives n petals, even n gives 2n, fractional n gives overlapping beauty
  • Phyllotaxis: golden angle (137.508 degrees) + sqrt spacing = nature's sunflower, always Fibonacci
  • Animate anything by adding frameCount to angles or using sin(time) as a modulator
  • Phase offsets are everything -- they turn synchronized motion into organic waves
  • When things go in circles, think in polar -- it simplifies everything

Next time we're looking at Bezier curves and organic shapes -- smooth, flowing lines that feel hand-drawn. The opposite of trig's geometric precision, and a perfect complement to it.

Sallukes! Thanks for reading.

X

@femdev