3D wireframe with sliders

:)

Log in to post a comment.

// TurtleToy — EDM Cone Wireframe (radial 3D -> 2D projection)
// Docs: global code runs once; walk(i) is called until it returns false.
// Adjustable Variables syntax is supported by TurtleToy UI sliders.  https://turtletoy.net/syntax
Canvas.setpenopacity(1);
const turtle = new Turtle();

// ---------- Adjustable controls ----------
const elev = 55;           // min=0 max=85 step=1   Camera tilt (deg)
const azim = 110;          // min=0 max=360 step=1  Camera yaw (deg)
const persp = 0.00;        // min=0 max=1 step=0.01 Perspective amount (0=ortho)
const rMin = 0.6;          // min=0.1 max=2 step=0.05  Inner radius (bass center)
const rMax = 5.0;          // min=2 max=8 step=0.05    Outer radius (highs)
const Nr   = 80;           // min=30 max=200 step=1    Radial samples
const Nt   = 240;          // min=120 max=720 step=1   Angular samples (0..6π)
const cone = 0.35;         // min=0 max=1 step=0.01    Outward lift like a cone
const spokeStep = 12;      // min=1 max=60 step=1      Draw a spoke every N angles
const drawRings  = 1;      // min=0 max=1 step=1 (Off,On)
const drawSpokes = 1;      // min=0 max=1 step=1 (Off,On)

// ---------- EDM-like amplitude model (synthetic) ----------
const TWO_PI = Math.PI * 2;
const thetaStart = 0, thetaEnd = 6*Math.PI;  // "song length" in radians

function gauss(x, mu, sigma) {
    const d = (x - mu) / sigma;
    return Math.exp(-d * d);
}

const theta0 = 3*Math.PI, theta1 = 3.8*Math.PI; // "drop" window

function amp(r, theta) {
    const bass = (0.9 + 0.4 * Math.sin(4*theta)) * gauss(r, 1.0, 0.35);
    const mids = (0.6*Math.sin(0.6*theta) + 0.3*Math.cos(1.2*theta)) * gauss(r, 2.6, 0.6);
    const highs = (0.25*Math.sin(3.5*theta) + 0.15*Math.sin(8*theta)) * gauss(r, 4.2, 0.4);
    // triangular-ish gate for the drop, normalized to ~[0,1]
    let gate = 0;
    if (theta >= theta0 && theta <= theta1) {
        gate = (theta - theta0) * (theta1 - theta);
        const gmax = Math.pow((theta1 - theta0)/2, 2);
        gate = gate / (gmax + 1e-9);
    }
    const drop = 0.8 * gauss(r, 2.2, 1.2) * gate;
    return bass + mids + highs + drop;
}

// ---------- 3D -> 2D projection helpers ----------
function deg2rad(d){ return d * Math.PI / 180; }

function rotateX(p, a){
    const ca = Math.cos(a), sa = Math.sin(a);
    return { x: p.x, y: p.y*ca - p.z*sa, z: p.y*sa + p.z*ca };
}
function rotateZ(p, a){
    const ca = Math.cos(a), sa = Math.sin(a);
    return { x: p.x*ca - p.y*sa, y: p.x*sa + p.y*ca, z: p.z };
}
function project(p){
    if (persp > 0) {
        const f = 1.0; // focal-ish
        const s = f / (f + p.z * persp);
        return { x: p.x * s, y: p.y * s };
    }
    return { x: p.x, y: p.y };
}

function point3D(r, theta) {
    const z = amp(r, theta) + cone * (r - rMin);
    const x = r * Math.cos(theta);
    const y = r * Math.sin(theta);
    let p = {x, y, z};
    p = rotateX(p, deg2rad(elev));
    p = rotateZ(p, deg2rad(azim));
    return project(p);
}

// ---------- Build grid of points (rings x angles) ----------
const rings = [];
for (let i = 0; i < Nr; i++) {
    const r = rMin + (rMax - rMin) * (i/(Nr - 1));
    const ring = [];
    for (let j = 0; j < Nt; j++) {
        const theta = thetaStart + (thetaEnd - thetaStart) * (j/(Nt - 1));
        ring.push(point3D(r, theta));
    }
    rings.push(ring);
}

// ---------- Convert to segments (line list) ----------
const segments = [];

// ring segments (wrap around)
if (drawRings) {
    for (let i = 0; i < Nr; i++) {
        const ring = rings[i];
        for (let j = 0; j < Nt; j++) {
            const a = ring[j];
            const b = ring[(j+1) % Nt];
            segments.push([a.x, a.y, b.x, b.y]);
        }
    }
}

// spoke segments (radial)
if (drawSpokes) {
    for (let j = 0; j < Nt; j += spokeStep) {
        for (let i = 0; i < Nr - 1; i++) {
            const a = rings[i][j];
            const b = rings[i+1][j];
            segments.push([a.x, a.y, b.x, b.y]);
        }
    }
}

// ---------- Fit to canvas (center & scale to ~[-95,95]) ----------
let minx=1e9, miny=1e9, maxx=-1e9, maxy=-1e9;
for (const s of segments) {
    if (s[0] < minx) minx = s[0]; if (s[2] < minx) minx = s[2];
    if (s[1] < miny) miny = s[1]; if (s[3] < miny) miny = s[3];
    if (s[0] > maxx) maxx = s[0]; if (s[2] > maxx) maxx = s[2];
    if (s[1] > maxy) maxy = s[1]; if (s[3] > maxy) maxy = s[3];
}
const cx = (minx + maxx)/2, cy = (miny + maxy)/2;
const scale = 190 / Math.max(maxx - minx, maxy - miny + 1e-9); // keep some margin

for (let k=0; k<segments.length; k++) {
    const s = segments[k];
    segments[k] = [
        (s[0] - cx) * scale, (s[1] - cy) * scale,
        (s[2] - cx) * scale, (s[3] - cy) * scale
    ];
}

// ---------- Draw incrementally ----------
function walk(i) {
    if (i >= segments.length) return false;
    const s = segments[i];
    turtle.jump(s[0], s[1]);
    turtle.goto(s[2], s[3]);
    return true;
}