let p = [], v = []; let lines = []; let theta = (3 / 2) * Math.PI, t = 0; let paused = false; let showLabels = false; let showInterPoints = true; let hoverIndex = -1; let selectedIndex = -1; let prevMouse = { x: 0, y: 0 }; let draggingAll = false; const HIT_R = 10; const MAX_TRAIL = 1000; function setup() { createCanvas(1000, 800); initCubicCurve(true); } function draw() { background(32); stroke(120, 120, 120, 180); strokeWeight(2); noFill(); beginShape(); vertex(p[0].x, p[0].y); vertex(p[1].x, p[1].y); vertex(p[2].x, p[2].y); vertex(p[3].x, p[3].y); endShape(); if (!paused) { if (theta > TWO_PI) theta = 0; theta += 0.01; t = (sin(theta) + 1) / 2; // 0..1 } lerpCubicCurve(); for (let i = 0; i < lines.length; i++) { lines[i].drawLine(); } drawCurve(); drawPointsAndLabels(); drawHUD(); } function drawCurve() { if (v.length > MAX_TRAIL) v.splice(0, v.length - MAX_TRAIL); stroke(0, 255, 0, 200); strokeWeight(5); noFill(); beginShape(); for (let i = 0; i < v.length; i++) { vertex(v[i].x, v[i].y); } endShape(); } function drawPointsAndLabels() { push(); noStroke(); fill(255, 70, 70, 230); circle(p[9].x, p[9].y, 14); pop(); for (let i = 0; i < 4; i++) { const hovered = (i === hoverIndex); const selected = (i === selectedIndex); p[i].drawPoint(hovered, selected); if (showLabels) { noStroke(); fill(255); textSize(14); textAlign(LEFT, BOTTOM); text(`P${i}`, p[i].x + 10, p[i].y - 10); } } if (showLabels) { noStroke(); fill(220); textSize(12); textAlign(LEFT, BOTTOM); text(`P4`, p[4].x + 8, p[4].y - 8); } if (showInterPoints) { const styleMid = { stroke: [80, 180, 255, 100], fill: [80, 180, 255, 50] }; for (let i = 4; i <= 8; i++) { p[i].drawPoint(false, false, styleMid); if (showLabels) { noStroke(); fill(200); textSize(12); textAlign(LEFT, BOTTOM); text(`P${i}`, p[i].x + 8, p[i].y - 8); } } } } function drawHUD() { noStroke(); fill(255); textSize(14); textAlign(LEFT, TOP); const hud = [ `t = ${t.toFixed(3)}`, `Space = ${paused ? 'Resume' : 'Pause'}`, `H = Toggle points 5..8 | L = Toggle labels | R = Reset`, `Drag a handle (P0..P3). Hold Shift to move the entire curve.` ]; for (let i = 0; i < hud.length; i++) { text(hud[i], 12, 12 + i * 18); } } function initCubicCurve(resetPositions = true) { p.length = 0; v.length = 0; lines.length = 0; theta = (3 / 2) * Math.PI; t = 0; if (resetPositions) { p[0] = new cPoint(width * 0.15, height * 0.75, 'end'); p[1] = new cPoint(width * 0.35, height * 0.20, 'control'); p[2] = new cPoint(width * 0.65, height * 0.20, 'control'); p[3] = new cPoint(width * 0.85, height * 0.75, 'end'); } else { for (let i = 0; i < 4; i++) { if (!p[i]) p[i] = new cPoint(width * (0.15 + i * 0.2), height / 2, i === 0 || i === 3 ? 'end' : 'control'); } } // Derived points p[4] = new cPoint(0, 0, 'derived'); p[5] = new cPoint(0, 0, 'derived'); p[6] = new cPoint(0, 0, 'derived'); p[7] = new cPoint(0, 0, 'derived'); p[8] = new cPoint(0, 0, 'derived'); p[9] = new cPoint(0, 0, 'derived'); lines[0] = new cLine(p[4], p[5], color(160, 160, 160, 200), 2); lines[1] = new cLine(p[5], p[6], color(160, 160, 160, 200), 2); lines[2] = new cLine(p[7], p[8], color(200, 200, 200, 220), 3); } function lerpCubicCurve() { // Level 1 p[4].changeX = (1 - t) * p[0].x + t * p[1].x; p[4].changeY = (1 - t) * p[0].y + t * p[1].y; p[5].changeX = (1 - t) * p[1].x + t * p[2].x; p[5].changeY = (1 - t) * p[1].y + t * p[2].y; p[6].changeX = (1 - t) * p[2].x + t * p[3].x; p[6].changeY = (1 - t) * p[2].y + t * p[3].y; // Level 2 p[7].changeX = (1 - t) * p[4].x + t * p[5].x; p[7].changeY = (1 - t) * p[4].y + t * p[5].y; p[8].changeX = (1 - t) * p[5].x + t * p[6].x; p[8].changeY = (1 - t) * p[5].y + t * p[6].y; // Final on-curve p[9].changeX = (1 - t) * p[7].x + t * p[8].x; p[9].changeY = (1 - t) * p[7].y + t * p[8].y; v.push(new cPoint(p[9].x, p[9].y, 'derived')); } function mouseMoved() { hoverIndex = -1; for (let i = 0; i < 4; i++) { if (p[i].isHit(mouseX, mouseY)) { hoverIndex = i; break; } } } function mousePressed() { prevMouse.x = mouseX; prevMouse.y = mouseY; selectedIndex = -1; for (let i = 0; i < 4; i++) { if (p[i].isHit(mouseX, mouseY)) { selectedIndex = i; break; } } draggingAll = keyIsDown(SHIFT) && selectedIndex === -1; } function mouseDragged() { const dx = mouseX - prevMouse.x; const dy = mouseY - prevMouse.y; if (selectedIndex >= 0) { p[selectedIndex].changeX = mouseX; p[selectedIndex].changeY = mouseY; v.length = 0; } else if (draggingAll) { for (let i = 0; i < 4; i++) { p[i].changeX = p[i].x + dx; p[i].changeY = p[i].y + dy; } v.length = 0; } prevMouse.x = mouseX; prevMouse.y = mouseY; return false; } function mouseReleased() { selectedIndex = -1; draggingAll = false; } function keyPressed() { if (key === ' ') { paused = !paused; } else if (key === 'R' || key === 'r') { initCubicCurve(true); } else if (key === 'H' || key === 'h') { showInterPoints = !showInterPoints; } else if (key === 'L' || key === 'l') { showLabels = !showLabels; } }