Modified Joy division

Sand dunes inspired by Joy division album cover

Log in to post a comment.

// ═══════════════════════════════════════════════════════════════
// Sand Dunes — Joy Division style waves with asymmetric clustering
// Inspired/adapted from https://www.gorillasun.de/blog/smooth-curves-with-perlin-noise-and-recreating-the-unknown-pleasures-album-cover-in-p5
// Also inspired by the following turtles:
// https://turtletoy.net/turtle/ca12448d34
// https://turtletoy.net/turtle/f60c49e6f7
// https://turtletoy.net/turtle/fa14c628d4
// ═══════════════════════════════════════════════════════════════

// ── Tunable parameters ──────────────────
const numLines     = 120;    // min=40  max=170  step=1 
const noiseFreqX   = 0.04;   // min=0.01 max=0.18 step=0.005 
const noiseFreqY   = 0.018;  // min=0.005 max=0.09 step=0.005 
const maxAmplitude = 28;     // min=4   max=60   step=1 
const clusterSide  = 0.12;   // min=0   max=1    step=0.01
const penOpacity   = 0.72;   // min=0.1 max=1.0  step=0.05

// ── Canvas & turtle setup ────────────────────────────────────
Canvas.setpenopacity(penOpacity);
const turtle = new Turtle();
turtle.penup();

// ── Perlin noise (smooth gradient noise, 2-D, multi-octave) ──
// I used AI to help generate pseudocode adapted from http://mrl.nyu.edu/~perlin/noise/ which I then reworked
class Noise {
    constructor(octaves = 1) {
        this.p = new Uint8Array(512);
        this.octaves = octaves;
        for (let i = 0; i < 512; ++i) this.p[i] = Math.random() * 256 * 100;
    }
    _lerp(t, a, b) { return a + t * (b - a); }
    _fade(t)       { return t * t * (3 - 2 * t); }          // smooth-step
    _grad(i, x, y) {
        const v = (i & 1) === 0 ? x : y;
        return (i & 2) === 0 ? -v : v;
    }
    noise2d(x, y) {
        const X  = Math.floor(x) & 255,  Y  = Math.floor(y) & 255;
        const xf = x - Math.floor(x),    yf = y - Math.floor(y);
        const u  = this._fade(xf),        v  = this._fade(yf);
        const p0 = this.p[X] + Y,         p1 = this.p[X + 1] + Y;
        return this._lerp(v,
            this._lerp(u, this._grad(this.p[p0],     xf,     yf    ),
                          this._grad(this.p[p1],     xf - 1, yf    )),
            this._lerp(u, this._grad(this.p[p0 + 1], xf,     yf - 1),
                          this._grad(this.p[p1 + 1], xf - 1, yf - 1)));
    }
    // Multi-octave noise, normalised to [0, 1]
    sample(x, y, persistence = 0.5) {
        let amp = 1, freq = 1, total = 0, norm = 0;
        for (let o = 0; o < this.octaves; o++) {
            total += amp * (1 + this.noise2d(freq * x, freq * y)) / 2;
            norm  += amp;
            amp   *= persistence;
            freq  *= 2;
        }
        return total / norm;   // → [0, 1]
    }
}
const perlin = new Noise(3);


function envelope(t) {
    // Squared-sine bell centred at clusterSide, width ~0.6 of canvas
    const width = 0.62;
    const raw   = Math.cos(Math.PI * (t - clusterSide) / width);
    return Math.max(0, raw) * Math.max(0, raw);   // cos²  → smooth, non-negative
}

function walk(i) {
    // Map line index to y-position: bottom → top  (-88 … +88)
    const yBase = -88 + (i / Math.max(numLines - 1, 1)) * 176;

    turtle.penup();
    let penDown = false;

    for (let xi = -100; xi <= 100; xi++) {
        const t   = (xi + 100) / 200;                          // normalised x ∈ [0,1]
        const env = envelope(t);                               // amplitude envelope

        // Two octaves of noise: coarse shape + fine detail
        const n   = perlin.sample(xi * noiseFreqX, i * noiseFreqY, 0.55);
        const y   = yBase + env * maxAmplitude * (n - 0.5) * 2;  // centred offset

        turtle.goto(xi, y);

        if (!penDown) { turtle.pendown(); penDown = true; }
    }

    turtle.penup();
    return i < numLines - 1;
}