JIT
Grid - Untitled
In one html file create a triple pendulum animation that follows the laws of gravity. No controls, no interactions, simply the animation. Make sure each intercept from one pendulum to another leaves a trail.
<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <title>Triple Pendulum — Chaotic Trails</title> <style> :root{ --bg1:#0b1020; --bg2:#0a0d18; --grid:#1a2346; --rod:#e8eefc; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; background: radial-gradient(120% 100% at 50% 0%, var(--bg1), var(--bg2) 55%); color:#cfe3ff; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; overflow:hidden; } .stage{ position:fixed; inset:0; display:block; width:100vw; height:100vh; touch-action:none; user-select:none; } canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; } .overlay{ position:absolute; left:2.2vw; bottom:2vh; color:#a9bde8; letter-spacing:.06em; font-weight:600; font-size:clamp(12px, 1.4vw, 18px); opacity:.85; text-shadow:0 1px 0 rgba(0,0,0,.35); mix-blend-mode:screen; } .title{ position:absolute; left:50%; top:5vh; transform:translateX(-50%); font-weight:800; font-size:clamp(20px, 4.4vw, 54px); letter-spacing:.04em; color:#eaf2ff; text-shadow:0 6px 32px rgba(0,0,0,.55); opacity:.9; pointer-events:none; white-space:nowrap; } .grid{ position:absolute; inset:0; background: radial-gradient(circle at 50% 15%, rgba(255,255,255,.06), rgba(255,255,255,0) 35%), repeating-linear-gradient(to right, transparent 0 94px, rgba(255,255,255,.02) 94px 95px), repeating-linear-gradient(to bottom, transparent 0 94px, rgba(255,255,255,.02) 94px 95px); mix-blend-mode:overlay; opacity:.4; pointer-events:none; } .legend{ position:absolute; right:2vw; bottom:2vh; display:flex; gap:1.2em; font-size:clamp(10px, 1.2vw, 14px); color:#dbe8ff; opacity:.9; text-shadow:0 1px 0 rgba(0,0,0,.35); } .chip{ display:flex;align-items:center;gap:.5em } .swatch{ width:1em;height:.5em;border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.2) inset } </style> </head> <body> <div class="stage"> <div class="grid"></div> <canvas id="trail"></canvas> <canvas id="scene"></canvas> <div class="title">Triple Pendulum</div> <div class="overlay">Chaotic motion under gravity · No controls</div> <div class="legend"> <div class="chip"><span class="swatch" style="background:#00e5ff"></span>Joint 1 trail</div> <div class="chip"><span class="swatch" style="background:#ff00c8"></span>Joint 2 trail</div> <div class="chip"><span class="swatch" style="background:#ffcc00"></span>Bob trail</div> </div> </div> <script> (function(){ const canvasTrail = document.getElementById('trail'); const canvasScene = document.getElementById('scene'); const ctxT = canvasTrail.getContext('2d'); const ctx = canvasScene.getContext('2d'); let DPR = Math.max(1, Math.min(window.devicePixelRatio || 1, 2)); let W = 0, H = 0, MIN = 0; // Pendulum parameters (scaled with viewport) let anchor = {x:0, y:0}; let L1=0, L2=0, L3=0; // rod lengths (px) let r1=0, r2=0, r3=0; // bob radii (px) let g = 980; // gravity (px/s^2) ~ 1px ~ 1/100 cm @ typical scale; tuned below on resize let damping = 0.999; // velocity damping for numerical stability let subSteps = 2; // verlet substeps per frame let iterations = 10; // constraint iterations per substep // Particles: 0: anchor (fixed), 1: joint1, 2: joint2, 3: bob const pts = [ {x:0,y:0, px:0,py:0, fixed:true, m:Infinity}, {x:0,y:0, px:0,py:0, fixed:false, m:1}, {x:0,y:0, px:0,py:0, fixed:false, m:1}, {x:0,y:0, px:0,py:0, fixed:false, m:1}, ]; // Trails (store last position to draw segments) const trailColors = ['#00e5ff', '#ff00c8', '#ffcc00']; const lastTrail = [ {x:null, y:null}, // joint1 {x:null, y:null}, // joint2 {x:null, y:null}, // bob ]; function resetTransforms(ctx){ ctx.setTransform(1,0,0,1,0,0); ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); } function resize(){ const prevW = W, prevH = H, prevMIN = MIN; const prevAnchor = {...anchor}; DPR = Math.max(1, Math.min(window.devicePixelRatio || 1, 2.5)); W = Math.floor(window.innerWidth); H = Math.floor(window.innerHeight); MIN = Math.min(W,H); // set canvas sizes with DPR [canvasTrail, canvasScene].forEach(c=>{ c.width = Math.floor(W * DPR); c.height = Math.floor(H * DPR); c.style.width = W + 'px'; c.style.height = H + 'px'; }); resetTransforms(ctxT); resetTransforms(ctx); ctxT.scale(DPR, DPR); ctx.scale(DPR, DPR); // Anchor near top center anchor.x = W * 0.5; anchor.y = H * (H<600 ? 0.10 : 0.14); // Lengths scale with viewport L1 = MIN * 0.18; L2 = MIN * 0.22; L3 = MIN * 0.26; // Bob sizes r1 = Math.max(2, MIN * 0.012); r2 = Math.max(2, MIN * 0.013); r3 = Math.max(2, MIN * 0.014); // Gravity tuned to scale so the motion time-scale feels natural (~1s per swing) g = 1.3 * MIN; // px/s^2 // Line widths scale ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctxT.lineCap = 'round'; ctxT.lineJoin = 'round'; // If first init (prevMIN===0), initialize fresh if (!prevMIN){ initializeState(); // Clear trails fully ctxT.clearRect(0,0,W,H); // Seed trail starting points lastTrail[0].x = pts[1].x; lastTrail[0].y = pts[1].y; lastTrail[1].x = pts[2].x; lastTrail[1].y = pts[2].y; lastTrail[2].x = pts[3].x; lastTrail[2].y = pts[3].y; return; } // Otherwise, scale positions to new viewport smoothly const s = MIN / prevMIN; const dx = anchor.x - prevAnchor.x * s; const dy = anchor.y - prevAnchor.y * s; pts.forEach(p=>{ p.x = p.x * s + (anchor.x - prevAnchor.x * s); p.y = p.y * s + (anchor.y - prevAnchor.y * s); p.px = p.px * s + (anchor.x - prevAnchor.x * s); p.py = p.py * s + (anchor.y - prevAnchor.y * s); }); // Move trails too lastTrail.forEach(t=>{ if(t.x!=null){ t.x = t.x * s + (anchor.x - prevAnchor.x * s); t.y = t.y * s + (anchor.y - prevAnchor.y * s); } }); } function initializeState(){ // Set anchor pts[0].x = pts[0].px = anchor.x; pts[0].y = pts[0].py = anchor.y; // Initial angles (slightly different to spark chaos) const th1 = Math.PI*0.70 + (Math.random()*0.04 - 0.02); const th2 = Math.PI*0.70 + (Math.random()*0.08 - 0.04); const th3 = Math.PI*0.70 + (Math.random()*0.12 - 0.06); // Forward kinematics from anchor const p1 = { x: anchor.x + L1*Math.sin(th1), y: anchor.y + L1*Math.cos(th1) }; const p2 = { x: p1.x + L2*Math.sin(th2), y: p1.y + L2*Math.cos(th2) }; const p3 = { x: p2.x + L3*Math.sin(th3), y: p2.y + L3*Math.cos(th3) }; [pts[1],pts[2],pts[3]].forEach((p,i)=>{ const src = [p1,p2,p3][i]; p.x = p.px = src.x; p.y = p.py = src.y; }); // Give a tiny initial nudge pts[3].px = pts[3].x - 0.5; pts[2].px = pts[2].x + 0.3; pts[1].py = pts[1].y - 0.2; // Reset trails lastTrail[0].x = pts[1].x; lastTrail[0].y = pts[1].y; lastTrail[1].x = pts[2].x; lastTrail[1].y = pts[2].y; lastTrail[2].x = pts[3].x; lastTrail[2].y = pts[3].y; ctxT.clearRect(0,0,W,H); } function integrate(dt){ const dt2 = dt*dt; // Verlet integration with gravity for(let i=1;i<=3;i++){ const p = pts[i]; if(p.fixed) continue; const vx = (p.x - p.px) * damping; const vy = (p.y - p.py) * damping; const nx = p.x + vx; const ny = p.y + vy + g*dt2; p.px = p.x; p.py = p.y; p.x = nx; p.y = ny; } // Constraint solver iterations (keep rods rigid) for(let k=0;k<iterations;k++){ // 0-1 satisfyStick(0,1,L1); // 1-2 satisfyStick(1,2,L2); // 2-3 satisfyStick(2,3,L3); // Re-pin anchor precisely pts[0].x = anchor.x; pts[0].y = anchor.y; pts[0].px = anchor.x; pts[0].py = anchor.y; } } function satisfyStick(iA,iB,rest){ const A = pts[iA], B = pts[iB]; let dx = B.x - A.x; let dy = B.y - A.y; let dist = Math.hypot(dx,dy) || 1e-8; const diff = (dist - rest) / dist; const invA = A.fixed ? 0 : 1/A.m; const invB = B.fixed ? 0 : 1/B.m; const invSum = invA + invB || 1e-8; const corrAx = invA / invSum * dx * diff; const corrAy = invA / invSum * dy * diff; const corrBx = invB / invSum * dx * diff; const corrBy = invB / invSum * dy * diff; if(!A.fixed){ A.x += corrAx; A.y += corrAy; } if(!B.fixed){ B.x -= corrBx; B.y -= corrBy; } } function draw(){ // Clear scene canvas only (not trail) ctx.clearRect(0,0,W,H); // Faint fade on the trail to create smooth decaying paths ctxT.save(); ctxT.globalCompositeOperation = 'destination-out'; ctxT.globalAlpha = 0.04; ctxT.fillRect(0,0,W,H); ctxT.restore(); // Draw new trail segments for each joint (1,2,3) const joints = [pts[1], pts[2], pts[3]]; const widths = [Math.max(1.2, r1*0.35), Math.max(1.4, r2*0.35), Math.max(1.6, r3*0.35)]; for(let i=0;i<3;i++){ const p = joints[i]; const last = lastTrail[i]; if(last.x!=null){ ctxT.beginPath(); ctxT.moveTo(last.x, last.y); ctxT.lineTo(p.x, p.y); ctxT.strokeStyle = trailColors[i]; ctxT.lineWidth = widths[i]; ctxT.globalAlpha = 0.9; ctxT.stroke(); } last.x = p.x; last.y = p.y; } // Draw rods ctx.strokeStyle = 'rgba(234,242,255,0.9)'; ctx.lineWidth = Math.max(1.25, MIN*0.004); ctx.beginPath(); ctx.moveTo(anchor.x, anchor.y); ctx.lineTo(pts[1].x, pts[1].y); ctx.lineTo(pts[2].x, pts[2].y); ctx.lineTo(pts[3].x, pts[3].y); ctx.stroke(); // Draw joints/bobs drawNode(anchor.x, anchor.y, Math.max(2, MIN*0.006), '#90a8ff', true); drawNode(pts[1].x, pts[1].y, r1, '#00e5ff'); drawNode(pts[2].x, pts[2].y, r2, '#ff00c8'); drawNode(pts[3].x, pts[3].y, r3, '#ffcc00'); } function drawNode(x,y,r,color, isAnchor=false){ const glow = isAnchor ? 'rgba(144,168,255,0.65)' : color+'55'; ctx.save(); // Glow ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(x,y,r*2.2,0,Math.PI*2); ctx.fill(); // Core ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fillStyle = color; ctx.fill(); // Rim ctx.lineWidth = Math.max(1, r*0.25); ctx.strokeStyle = 'rgba(255,255,255,0.85)'; ctx.stroke(); ctx.restore(); } // Animation loop let lastTime = performance.now(); let accumulator = 0; const fixedDt = 1/240; // physics step function tick(now){ const frameDt = Math.min(0.032, (now - lastTime)/1000); // clamp lastTime = now; accumulator += frameDt; // Multiple sub-steps for stability while(accumulator >= fixedDt){ for(let n=0;n<subSteps;n++){ integrate(fixedDt / subSteps); } accumulator -= fixedDt; } draw(); requestAnimationFrame(tick); } window.addEventListener('resize', resize); resize(); requestAnimationFrame(tick); // In case of visibility change, keep timing sane document.addEventListener('visibilitychange', ()=>{ lastTime = performance.now(); }); })(); </script> </body> </html>*
Updated files
v1