Stiff donut with sliders

Stiffy

Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax

// ---------- SLIDERS ----------
const penOpacity   = 1.0;  // min=0.05, max=1,    step=0.05
const worldSize    = 90;   // min=60,   max=120,  step=1
const gridCells    = 6;    // min=2,    max=24,   step=1
const stepLen      = 0.9;  // min=0.2,  max=2.0,  step=0.05
const stepsPerPath = 160;  // min=40,   max=400,  step=10
const pathCount    = 260;  // min=40,   max=1200, step=20
const angleMul     = 1.0;  // min=0.5,  max=6,    step=0.1   (multiplies field rotation)
const jitter       = 0.00; // min=0.00, max=0.50, step=0.01  (adds randomness per step)
const seed         = 1337; // min=1,    max=9999, step=1

Canvas.setpenopacity(penOpacity);

// ---------- RNG (seeded) ----------
function RNG(s) { this.s = s>>>0; }
RNG.prototype.next = function () {
    // LCG (Numerical Recipes)
    this.s = (1664525 * this.s + 1013904223) >>> 0;
    return this.s / 4294967296;
};
const rng = new RNG(seed);

// ---------- Turtle ----------
const turtle = new Turtle();

// ---------- Perlin Noise ----------
class Perlin {
    constructor(worldHalfSize, gridSize, randFn) {
        this.worldHalfSize = worldHalfSize;
        this.gridSize = gridSize;
        this.rand = randFn;
        this.grid = [];
        for (let i = 0; i <= gridSize; i++) {
            const row = [];
            for (let j = 0; j <= gridSize; j++) {
                const a = this.rand() * Math.PI * 2;
                row.push([Math.cos(a), Math.sin(a)]);
            }
            this.grid.push(row);
        }
    }
    fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
    lerp(a, b, t) { return a + (b - a) * t; }
    dot(ix, iy, x, y) {
        const g = this.grid[ix][iy];
        return g[0] * (x - ix) + g[1] * (y - iy);
    }
    // Sample Perlin noise at canvas coords cx,cy in [-worldHalfSize, +worldHalfSize]
    get(cx, cy) {
        const u = ((cx + this.worldHalfSize) / (2 * this.worldHalfSize)) * this.gridSize;
        const v = ((cy + this.worldHalfSize) / (2 * this.worldHalfSize)) * this.gridSize;

        let x0 = Math.floor(u), y0 = Math.floor(v);
        let x1 = Math.min(this.gridSize, x0 + 1);
        let y1 = Math.min(this.gridSize, y0 + 1);
        x0 = Math.max(0, x0);
        y0 = Math.max(0, y0);

        const sx = u - x0, sy = v - y0;

        const n00 = this.dot(x0, y0, u, v);
        const n10 = this.dot(x1, y0, u, v);
        const n01 = this.dot(x0, y1, u, v);
        const n11 = this.dot(x1, y1, u, v);

        const fx = this.fade(sx), fy = this.fade(sy);
        const ix0 = this.lerp(n00, n10, fx);
        const ix1 = this.lerp(n01, n11, fx);
        return this.lerp(ix0, ix1, fy); // ~[-1,1]
    }
}

const perlin = new Perlin(worldSize, gridCells, () => rng.next());

// ---------- Flow Field State ----------
let path = 0, step = 0;
let x = 0, y = 0;

function beginPath() {
    // start inside a central box
    x = (rng.next() * 2 - 1) * (worldSize * 0.9);
    y = (rng.next() * 2 - 1) * (worldSize * 0.9);
    turtle.penup();
    turtle.goto(x, y);
    turtle.pendown();
    step = 0;
}
beginPath();

// ---------- Walk Loop ----------
function walk(i) {
    // Noise -> angle
    const n = perlin.get(x, y);               // ~ [-1, 1]
    let angle = (n + 1) * Math.PI;            // 0..2π
    angle *= angleMul;                        // extra swirl
    // Optional jitter
    angle += (rng.next() - 0.5) * jitter;

    // Integrate step
    x += Math.cos(angle) * stepLen;
    y += Math.sin(angle) * stepLen;

    // Stop if we leave the canvas bounds
    if (x < -100 || x > 100 || y < -100 || y > 100) {
        path++;
        if (path >= pathCount) return false;
        beginPath();
        return true;
    }

    turtle.goto(x, y);

    step++;
    if (step >= stepsPerPath) {
        path++;
        if (path >= pathCount) return false;
        beginPath();
    }
    return true;
}