27:18Gemini 4 Hydrogen: Cheaper Tokens, More Expensive Answers?
Nov 13, 2026 · YouTube
18:40ERNIE 5.5 Flash Preview vs GLM 5.3 Flash: Baidu's new model is very impressive!
Nov 11, 2026 · YouTube
36:22Fable 5.2 robbing me blind, plus: Nex AGI's N2.6 vs GLM 5.3 Flash
Nov 11, 2026 · YouTube
14:03Gemini 4 Carbon: Can it handle my challenges? (First look with one-shot coding tests)
Nov 10, 2026 · YouTube
10:57"Crouton Alpha": the latest super-efficient coding model by Meta?
Nov 3, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
22:14GPT-6.1 Terra Pro vs Terra: How Much Better Is Pro?
Oct 27, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
You build physics toys, and you have watched someone try the tablecloth trick at a family dinner.
Build me a single HTML page: a table set for dinner, and a tablecloth I can yank out from under it.
#speed=12 sets the pull speed to 12 ft/s and pulls one second after load. Any number works.data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file: the lowest pull speed at which every object stays on the table, and the speed below which no object stays on the table, as two numbers in ft/s. State the arithmetic using your masses, friction coefficients, and cloth length.
The single fenced html block.
Where it behaves like the physical trick and where it does not. Name any special cases, scripted wobbles, or fall thresholds. Everyone has seen this trick fail; say how yours fails before I find out.
My pre-run, sliding-only prediction is 27.0 ft/s for every object to remain on the table, and 1.7 ft/s below which none remain. These are estimates, not enforced outcomes. The six masses are 0.80, 0.22, 0.70, 0.55, 0.60, and 0.25 kg; their cloth friction coefficients are 0.30, 0.25, 0.31, 0.38, 0.28, and 0.26, and bare-table friction is 0.16. With a 2.44 m cloth on a 2.20 m table, each object initially has ( \ell=x+0.12 ) metres of cloth to pass beneath it. Using (a=\mu mg/m=\mu g), predicted displacement including subsequent braking is (s=\mu g\ell^2(1+\mu/0.16)/(2v^2)). Equating that to each object's clearance from the right edge gives thresholds of approximately 1.7, 3.1, 6.7, 11.9, 16.0, and 27.0 ft/s. Rotation, moving contact points, and collisions can shift those boundaries.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>A small act of dinner-table physics</title>
<style>
:root{color-scheme:dark;--ink:#ece5d6;--muted:#aebbb8;--mint:#bde2c3;--line:#ffffff21}
*{box-sizing:border-box}
body{margin:0;background:#142525;color:var(--ink);font:15px/1.5 system-ui,sans-serif}
main{max-width:1160px;margin:auto;padding:30px 24px}
header{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:22px}
.eyebrow{font-size:11px;letter-spacing:.22em;color:var(--mint);text-transform:uppercase}
h1{font:normal clamp(30px,5vw,54px)/1.05 Georgia,serif;margin:8px 0 12px}
header p{margin:0;color:var(--muted);max-width:490px}
.tag{border:1px solid var(--line);padding:7px 12px;border-radius:30px;font-size:12px;white-space:nowrap}
.scene{position:relative;background:#ece6d8;border-radius:14px;overflow:hidden;box-shadow:0 16px 60px #0003}
canvas{display:block;width:100%;height:465px;touch-action:pan-y}
.scene-top{position:absolute;top:16px;left:20px;right:20px;display:flex;justify-content:space-between;pointer-events:none;color:#4c5c53;font-size:11px;letter-spacing:.12em;text-transform:uppercase}
.controls{display:flex;gap:22px;align-items:center;padding:22px 0;border-bottom:1px solid var(--line);flex-wrap:wrap}
.speed{flex:1;min-width:240px}
.speed label{font-size:12px;color:var(--muted)}
.readout{display:flex;align-items:center;gap:8px;margin:5px 0 9px}
input[type=number]{font:26px Georgia,serif;width:115px;color:var(--ink);background:transparent;border:0;border-bottom:1px solid #7a9b8b;outline:none}
input[type=range]{width:100%;accent-color:var(--mint);display:block}
button{border:0;border-radius:7px;padding:14px 24px;font:600 14px system-ui;cursor:pointer}
button:disabled{opacity:.4;cursor:default}
#yank{background:var(--mint);color:#142525}
#reset{background:transparent;color:var(--ink);border:1px solid #ffffff42}
.result{min-width:235px;max-width:320px}
#status{font:23px Georgia,serif}
#detail{color:var(--muted);font-size:12px;margin-top:4px}
.inventory{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin:23px 0}
.item{border-top:2px solid var(--c);padding-top:9px}
.item b{display:block;font-size:12px;font-weight:500}
.item span{font-size:11px;color:var(--muted)}
.notes{display:flex;gap:30px;color:var(--muted);font-size:12px;border-top:1px solid var(--line);padding-top:16px}
.notes p{flex:1;margin:0}
@media(max-width:650px){main{padding:20px 14px}header{display:block}.tag{display:none}canvas{height:370px}.inventory{grid-template-columns:repeat(3,1fr)}.notes{display:block}.notes p{margin-bottom:8px}.controls{gap:12px}.result{width:100%;max-width:none}}
</style>
</head>
<body>
<main>
<header>
<div>
<div class="eyebrow">The dinner experiment / No. 01</div>
<h1>Leave the dinner.<br>Take the tablecloth.</h1>
<p>A little friction. A little inertia. Six reasons not to try this with the good china.</p>
</div>
<div class="tag">Live rigid-body simulation · side view</div>
</header>
<div class="scene">
<div class="scene-top"><span id="sceneLabel">The table is set</span><span id="clock">0.00 s</span></div>
<canvas id="canvas" aria-label="Side-view simulation of six dinner objects on a table"></canvas>
</div>
<div class="controls">
<div class="speed">
<label for="speed">PULL SPEED · feet per second</label>
<div class="readout"><input id="speed" type="number" value="12" step="any" aria-label="Pull speed in feet per second"><span>ft/s →</span></div>
<input id="range" type="range" min="0" max="45" step=".1" value="12" aria-label="Pull speed slider">
</div>
<button id="yank">Yank the cloth ↗</button>
<button id="reset">Reset dinner</button>
<div class="result" aria-live="polite">
<div id="status">Will dinner survive?</div>
<div id="detail">Choose a speed, then pull. Results appear at rest.</div>
</div>
</div>
<div class="inventory" id="inventory"></div>
<div class="notes">
<p><b>Read the experiment.</b> Coral is cloth, walnut is table. Objects exchange impulses at their contact points: friction can translate them, torque can tip them, and collisions can rescue or ruin dinner.</p>
<p><b>Try the awkward speeds.</b> Around 4–20 ft/s is interesting. Negative numbers pull left; zero leaves the cloth parked. Masses and contact footprints are different; friction coefficients describe the different materials, not an invented area multiplier.</p>
</div>
</main>
<script>
"use strict";
(() => {
const canvas=document.getElementById("canvas"), ctx=canvas.getContext("2d");
const $=id=>document.getElementById(id);
const L=2.20, TOP=.85, MUWOOD=.16, G=9.81, DT=1/240;
const specs=[
{name:"Dinner plate",kind:"plate",x:.22,w:.27,h:.035,m:.80,mu:.30,color:"#507e79"},
{name:"Water glass",kind:"glass",x:.56,w:.09,h:.22,m:.22,mu:.25,color:"#69a6af"},
{name:"Side plate",kind:"plate",x:.92,w:.26,h:.035,m:.70,mu:.31,color:"#b88358"},
{name:"Candlestick",kind:"candle",x:1.25,w:.10,h:.34,m:.55,mu:.38,color:"#bd9950"},
{name:"Serving bowl",kind:"bowl",x:1.62,w:.20,h:.12,m:.60,mu:.28,color:"#78936b"},
{name:"Wine glass",kind:"wine",x:1.94,w:.10,h:.24,m:.25,mu:.26,color:"#9a789b"}
];
$("inventory").innerHTML=specs.map((s,i)=>`<div class="item" style="--c:${s.color}"><b>${i+1}. ${s.name}</b><span>${s.m.toFixed(2)} kg · μ ${s.mu.toFixed(2)}<br>${Math.round(s.w*100)} cm contact width</span></div>`).join("");
let bodies, running=false, finished=false, elapsed=0, shift=0, oldShift=0, pullV=0;
let quiet=0, accumulator=0, last=performance.now(), viewLeft=-.38, viewRight=2.94;
let cw=1000,ch=465,scale=1,originX=0,originY=0;
const cross=(a,b)=>a.x*b.y-a.y*b.x;
const dot=(a,b)=>a.x*b.x+a.y*b.y;
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function body(s){
return {...s,y:TOP+s.h/2+.0002,a:0,vx:0,vy:0,wv:0,
im:1/s.m,ii:12/(s.m*(s.w*s.w+s.h*s.h))};
}
const table={x:L/2,y:TOP-.075,w:L,h:.15,a:0,vx:0,vy:0,wv:0,im:0,ii:0,table:true};
const floor={x:0,y:-.15,w:200,h:.30,a:0,vx:0,vy:0,wv:0,im:0,ii:0};
function vertices(b){
const c=Math.cos(b.a),s=Math.sin(b.a);
return [[-b.w/2,-b.h/2],[b.w/2,-b.h/2],[b.w/2,b.h/2],[-b.w/2,b.h/2]].map(([x,y])=>({x:b.x+c*x-s*y,y:b.y+s*x+c*y}));
}
function project(p,n){
let lo=Infinity,hi=-Infinity;
for(const v of p){const d=dot(v,n);lo=Math.min(lo,d);hi=Math.max(hi,d)}
return [lo,hi];
}
function clip(poly,a,b){
const edge={x:b.x-a.x,y:b.y-a.y};
const side=p=>cross(edge,{x:p.x-a.x,y:p.y-a.y});
const out=[];
if(!poly.length)return out;
let p=poly[poly.length-1],dp=side(p);
for(const q of poly){
const dq=side(q);
if((dp>=0)!=(dq>=0)){
const t=dp/(dp-dq);
out.push({x:p.x+(q.x-p.x)*t,y:p.y+(q.y-p.y)*t});
}
if(dq>=0)out.push(q);
p=q;dp=dq;
}
return out;
}
function collide(A,B){
const pa=vertices(A),pb=vertices(B);
let depth=Infinity,n=null;
for(const poly of [pa,pb])for(let i=0;i<2;i++){
const e={x:poly[i+1].x-poly[i].x,y:poly[i+1].y-poly[i].y};
const len=Math.hypot(e.x,e.y),axis={x:-e.y/len,y:e.x/len};
const aa=project(pa,axis),bb=project(pb,axis);
const d=Math.min(aa[1],bb[1])-Math.max(aa[0],bb[0]);
if(d<=0)return [];
if(d<depth){depth=d;n=axis}
}
if((B.x-A.x)*n.x+(B.y-A.y)*n.y<0)n={x:-n.x,y:-n.y};
let intersection=pa;
for(let i=0;i<4;i++)intersection=clip(intersection,pb[i],pb[(i+1)%4]);
if(!intersection.length)return [];
const t={x:-n.y,y:n.x};
const ts=project(intersection,t),ns=project(intersection,n);
const mid=(ns[0]+ns[1])/2;
const ends=ts[1]-ts[0]>.005?[ts[0],ts[1]]:[(ts[0]+ts[1])/2];
return ends.map(z=>{
const p={x:t.x*z+n.x*mid,y:t.y*z+n.y*mid};
const ra={x:p.x-A.x,y:p.y-A.y},rb={x:p.x-B.x,y:p.y-B.y};
const kn=A.im+B.im+A.ii*cross(ra,n)**2+B.ii*cross(rb,n)**2;
const kt=A.im+B.im+A.ii*cross(ra,t)**2+B.ii*cross(rb,t)**2;
const c={A,B,n,t,p,ra,rb,kn,kt,ln:0,lc:0,lw:0,depth};
const vn=relative(c,n);
c.bounce=vn<-.7?-.10*vn:0;
c.cloth=B.table&&n.y<-.7&&p.y>TOP-.03;
c.f=c.cloth?clothFraction(p.x):0;
return c;
});
}
function relative(c,d){
const {A,B,ra,rb}=c;
return (B.vx-B.wv*rb.y-A.vx+A.wv*ra.y)*d.x+
(B.vy+B.wv*rb.x-A.vy-A.wv*ra.x)*d.y;
}
function impulse(c,d,j){
const {A,B,ra,rb}=c;
A.vx-=d.x*j*A.im;A.vy-=d.y*j*A.im;A.wv-=cross(ra,d)*j*A.ii;
B.vx+=d.x*j*B.im;B.vy+=d.y*j*B.im;B.wv+=cross(rb,d)*j*B.ii;
}
/* Exact fraction of this timestep for which this point is covered
by the translating, finite cloth. Avoids skipping friction at high speed. */
function clothFraction(x){
const lo=x-(L+.12),hi=x+.12,d=shift-oldShift;
if(Math.abs(d)<1e-14)return oldShift>=lo&&oldShift<=hi?1:0;
let a=(lo-oldShift)/d,b=(hi-oldShift)/d;
if(a>b)[a,b]=[b,a];
return Math.max(0,Math.min(1,b)-Math.max(0,a));
}
function solve(c){
const bias=.16*Math.max(0,c.depth-.00035)/DT;
let next=Math.max(0,c.ln+(bias+c.bounce-relative(c,c.n))/c.kn);
impulse(c,c.n,next-c.ln);c.ln=next;
if(c.cloth){
let limit=c.A.mu*c.ln*c.f;
// Conveyor surface velocity belongs to the static table's cloth.
let j=clamp(c.lc-(relative(c,c.t)+pullV*c.t.x)/c.kt,-limit,limit);
impulse(c,c.t,j-c.lc);c.lc=j;
limit=MUWOOD*c.ln*(1-c.f);
j=clamp(c.lw-relative(c,c.t)/c.kt,-limit,limit);
impulse(c,c.t,j-c.lw);c.lw=j;
}else{
const mu=c.B.im?Math.sqrt(c.A.mu*c.B.mu):MUWOOD;
const limit=mu*c.ln;
const j=clamp(c.lw-relative(c,c.t)/c.kt,-limit,limit);
impulse(c,c.t,j-c.lw);c.lw=j;
}
}
function clothGone(){return shift>L+.12||shift<-(L+.12)}
function onTable(b){
const p=vertices(b),bottom=Math.min(...p.map(v=>v.y));
return b.x>=0&&b.x<=L&&bottom>TOP-.025&&bottom<TOP+.025;
}
function upright(b){return Math.abs(Math.atan2(Math.sin(b.a),Math.cos(b.a)))<.22}
function finish(){
running=false;finished=true;
for(const b of bodies){b.vx=0;b.vy=0;b.wv=0}
const kept=bodies.filter(onTable),up=kept.filter(upright);
$("status").textContent=`${kept.length} on table · ${up.length} upright`;
$("detail").textContent=`${6-kept.length} on the floor. Settled after ${elapsed.toFixed(1)} s. Upright counts are of the objects still on the table.`;
$("sceneLabel").textContent=kept.length===6?"Dinner is (mostly) served":"Some assembly required";
}
function step(){
oldShift=shift;
if(!clothGone())shift+=pullV*DT;
for(const b of bodies){
b.vy-=G*DT;
b.x+=b.vx*DT;b.y+=b.vy*DT;b.a+=b.wv*DT;
}
let contacts=[];
for(let i=0;i<bodies.length;i++){
const a=bodies[i];
contacts.push(...collide(a,table),...collide(a,floor));
for(let j=i+1;j<bodies.length;j++)contacts.push(...collide(a,bodies[j]));
}
for(let k=0;k<14;k++)for(const c of contacts)solve(c);
elapsed+=DT;
const still=bodies.every(b=>Math.hypot(b.vx,b.vy)<.025&&Math.abs(b.wv)<.12);
if(still&&(clothGone()||pullV===0))quiet+=DT;else quiet=0;
if(quiet>1.25)finish();
}
function reset(){
bodies=specs.map(body);running=false;finished=false;elapsed=0;shift=0;oldShift=0;quiet=0;accumulator=0;
table.vx=table.vy=table.wv=floor.vx=floor.vy=floor.wv=0;
viewLeft=-.38;viewRight=2.94;
$("yank").disabled=false;$("speed").disabled=false;$("range").disabled=false;
$("status").textContent="Will dinner survive?";
$("detail").textContent="Choose a speed, then pull. Results appear at rest.";
$("sceneLabel").textContent="The table is set";
}
function yank(){
if(running||finished)return;
let v=Number($("speed").value);
if(!Number.isFinite(v)){v=12;$("speed").value=v}
pullV=v*.3048;running=true;quiet=0;last=performance.now();accumulator=0;
$("yank").disabled=true;$("speed").disabled=true;$("range").disabled=true;
$("status").textContent="Dinner is in motion…";
$("detail").textContent=`${v} ft/s · waiting for every object to settle`;
$("sceneLabel").textContent=v===0?"No pull. Just gravity.":v<0?"Pulling left ←":"Pulling right →";
}
$("yank").onclick=yank;
$("reset").onclick=reset;
$("range").oninput=()=>{$("speed").value=$("range").value};
$("speed").oninput=()=>{const n=Number($("speed").value);if(Number.isFinite(n))$("range").value=clamp(n,0,45)};
function resize(){
const r=canvas.getBoundingClientRect(),d=Math.min(devicePixelRatio||1,2);
cw=r.width;ch=r.height;canvas.width=Math.round(cw*d);canvas.height=Math.round(ch*d);
ctx.setTransform(d,0,0,d,0,0);
}
window.addEventListener("resize",resize);
const X=x=>originX+x*scale,Y=y=>originY-y*scale;
function line(x1,y1,x2,y2,color,width=1){
ctx.beginPath();ctx.moveTo(X(x1),Y(y1));ctx.lineTo(X(x2),Y(y2));ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke();
}
function rect(x,y,w,h,fill){
ctx.fillStyle=fill;ctx.fillRect(X(x),Y(y+h),w*scale,h*scale);
}
function ellipse(x,y,rx,ry,fill){
ctx.beginPath();ctx.ellipse(x,y,rx,ry,0,0,Math.PI*2);ctx.fillStyle=fill;ctx.fill();
}
function drawObject(b,i){
ctx.save();ctx.translate(X(b.x),Y(b.y));ctx.rotate(-b.a);ctx.scale(scale,-scale);
const w=b.w,h=b.h;
ctx.lineWidth=.004;
if(b.kind==="plate"){
ctx.fillStyle=b.color;ctx.beginPath();ctx.moveTo(-w/2,h/2);ctx.lineTo(w/2,h/2);
ctx.lineTo(w*.38,-h/2);ctx.lineTo(-w*.38,-h/2);ctx.closePath();ctx.fill();
ctx.strokeStyle="#f6edd6";ctx.beginPath();ctx.moveTo(-w*.47,h*.15);ctx.lineTo(w*.47,h*.15);ctx.stroke();
}else if(b.kind==="glass"){
ctx.beginPath();ctx.moveTo(-w/2,h/2);ctx.lineTo(w/2,h/2);ctx.lineTo(w*.40,-h/2);ctx.lineTo(-w*.40,-h/2);ctx.closePath();
ctx.fillStyle="#8eb8bb55";ctx.fill();ctx.strokeStyle="#477d85";ctx.stroke();
ctx.fillStyle="#8aafa666";ctx.fillRect(-w*.37,-h*.43,w*.74,h*.46);
ctx.strokeStyle="#fff9";ctx.beginPath();ctx.moveTo(-w*.28,h*.33);ctx.lineTo(-w*.25,-h*.24);ctx.stroke();
}else if(b.kind==="wine"){
ctx.strokeStyle=b.color;ctx.lineWidth=.006;ctx.beginPath();ctx.moveTo(-w*.46,-h/2);ctx.lineTo(w*.46,-h/2);
ctx.moveTo(0,-h/2);ctx.lineTo(0,h*.02);ctx.stroke();
ctx.beginPath();ctx.moveTo(-w/2,h/2);ctx.lineTo(w/2,h/2);ctx.bezierCurveTo(w*.56,-h*.04,w*.12,-h*.04,0,-h*.02);
ctx.bezierCurveTo(-w*.12,-h*.04,-w*.56,-h*.04,-w/2,h/2);ctx.closePath();
ctx.fillStyle="#b5a4bb55";ctx.fill();ctx.stroke();
ctx.fillStyle="#9b576b88";ctx.beginPath();ctx.ellipse(0,h*.17,w*.39,h*.14,0,0,Math.PI*2);ctx.fill();
}else if(b.kind==="candle"){
ctx.fillStyle=b.color;ctx.fillRect(-w/2,-h/2,w,.018);
ctx.beginPath();ctx.moveTo(-w*.4,-h*.44);ctx.lineTo(w*.4,-h*.44);ctx.lineTo(w*.13,-h*.24);ctx.lineTo(-w*.13,-h*.24);ctx.closePath();ctx.fill();
ctx.fillRect(-w*.09,-h*.3,w*.18,h*.51);
ctx.fillRect(-w*.30,h*.14,w*.60,.014);
ctx.fillStyle="#eee1b2";ctx.fillRect(-w*.19,h*.18,w*.38,h*.32);
ctx.fillStyle="#a98b59";ctx.fillRect(-.002,h*.46,.004,.014);
}else{
ctx.beginPath();ctx.moveTo(-w/2,h/2);ctx.lineTo(w/2,h/2);
ctx.quadraticCurveTo(w*.40,-h*.50,w*.22,-h/2);ctx.lineTo(-w*.22,-h/2);
ctx.quadraticCurveTo(-w*.40,-h*.5,-w/2,h/2);ctx.fillStyle=b.color;ctx.fill();
ctx.strokeStyle="#d7dfbc";ctx.beginPath();ctx.moveTo(-w*.46,h*.37);ctx.lineTo(w*.46,h*.37);ctx.stroke();
}
ctx.restore();
if(!running&&!finished){
ctx.fillStyle="#52625c";ctx.font="10px system-ui";ctx.textAlign="center";
ctx.fillText(String(i+1),X(b.x),Y(b.y+b.h/2)-12);
}
}
function drawCloth(){
const left=Math.max(0,-.12+shift),right=Math.min(L,L+.12+shift);
if(right>left){
line(left,TOP+.003,right,TOP+.003,"#bc5d47",6);
line(left,TOP+.006,right,TOP+.006,"#e8a18c",1);
const end=pullV<0?left:right;
const dir=pullV<0?-1:1;
if((dir>0&&right===L)||(dir<0&&left===0)){
const length=Math.min(.76,.12+Math.abs(shift));
ctx.beginPath();ctx.moveTo(X(end),Y(TOP));
ctx.bezierCurveTo(X(end+dir*.035),Y(TOP-.08),X(end+dir*(.06+length*.22)),Y(TOP-length*.65),X(end+dir*(.09+length*.12)),Y(TOP-length));
ctx.strokeStyle="#bc5d47";ctx.lineWidth=6;ctx.stroke();
ctx.strokeStyle="#e5a08c";ctx.lineWidth=1;ctx.stroke();
}
}else{
const dir=pullV<0?-1:1,base=dir>0?L+.06:-.06;
ctx.beginPath();ctx.moveTo(X(base),Y(.025));
for(let i=1;i<=36;i++)ctx.lineTo(X(base+dir*i*.023),Y(.025+.014*Math.sin(i*1.8)));
ctx.strokeStyle="#bc5d47";ctx.lineWidth=7;ctx.stroke();
}
}
function draw(){
let lo=-.38,hi=2.94;
for(const b of bodies){lo=Math.min(lo,b.x-.24);hi=Math.max(hi,b.x+.24)}
viewLeft+=(lo-viewLeft)*.06;viewRight+=(hi-viewRight)*.06;
scale=Math.min((cw-40)/(viewRight-viewLeft),(ch-85)/1.35);
originX=(cw-(viewRight-viewLeft)*scale)/2-viewLeft*scale;
originY=ch-42;
ctx.clearRect(0,0,cw,ch);
const gradient=ctx.createLinearGradient(0,0,0,ch);
gradient.addColorStop(0,"#eee8da");gradient.addColorStop(1,"#ddd8c7");
ctx.fillStyle=gradient;ctx.fillRect(0,0,cw,ch);
// Quiet architectural details.
line(viewLeft,1.13,viewRight,1.13,"#d4d3c1",1);
rect(viewLeft,-.2,viewRight-viewLeft,.2,"#c7c7b2");
line(viewLeft,0,viewRight,0,"#929d89",1);
for(const b of bodies){
const altitude=Math.max(0,b.y-b.h/2);
const rx=Math.max(3,b.w*scale*.52);
ellipse(X(b.x),Y(0)+3,rx,3,"#35463816");
if(b.x>0&&b.x<L&&b.y>=TOP)ellipse(X(b.x),Y(TOP)-1,rx,2,"#273d3325");
}
rect(.13,0,.09,TOP-.10,"#6a5140");
rect(L-.22,0,.09,TOP-.10,"#6a5140");
rect(.16,.10,L-.32,.045,"#82654c");
rect(0,TOP-.10,L,.10,"#74543e");
rect(0,TOP-.025,L,.025,"#9e7855");
line(.025,TOP-.067,L-.025,TOP-.067,"#b18d673d",1);
drawCloth();
bodies.forEach(drawObject);
// Scale bar remains one real-world foot, even as the camera widens.
const sx=22,sy=ch-17,bar=.3048*scale;
ctx.strokeStyle="#5f6a5e";ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(sx,sy);ctx.lineTo(sx+bar,sy);ctx.moveTo(sx,sy-3);ctx.lineTo(sx,sy+3);ctx.moveTo(sx+bar,sy-3);ctx.lineTo(sx+bar,sy+3);ctx.stroke();
ctx.font="10px system-ui";ctx.fillStyle="#5f6a5e";ctx.textAlign="left";ctx.fillText("1 ft",sx+bar+7,sy+3);
$("clock").textContent=elapsed.toFixed(2)+" s";
}
function frame(now){
const delta=Math.min(.05,(now-last)/1000);last=now;
if(running){
accumulator+=delta;
while(accumulator>=DT&&running){step();accumulator-=DT}
}
draw();requestAnimationFrame(frame);
}
reset();resize();requestAnimationFrame(frame);
const params=new URLSearchParams(location.hash.slice(1));
if(params.has("speed")){
const value=Number(params.get("speed"));
if(Number.isFinite(value)){
$("speed").value=String(value);$("range").value=clamp(value,0,45);
setTimeout(()=>{if(!running&&!finished)yank()},1000);
}
}
})();
</script>
</body>
</html>