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.
With (g=32.2\ \mathrm{ft/s^2}), a (6.3\ \mathrm{ft}) cloth whose trailing edge starts at (x=2.65\ \mathrm{ft}), table right edge at (8.7\ \mathrm{ft}), and objects whose sliding displacement is approximated by (D(v)=\frac{g s^2}{v^2}\left(\frac{\mu_c}{2}+\frac{\mu_c^2}{2\mu_t}\right)), where (s) is the distance from the cloth's trailing edge to the object, (\mu_c) is cloth friction, and (\mu_t) is table friction (mass cancels in sliding because (a=\mu g), although the masses (0.35)–(1.1\ \mathrm{kg}) matter for collisions and tipping): the worst case is the salad plate ((\mu_c=0.26,\ \mu_t=0.20,\ s=4.85\ \mathrm{ft}), centre margin (1.20\ \mathrm{ft})), giving (v=\sqrt{32.2(4.85)^2(0.13+0.169)/1.20}\approx13.7\ \mathrm{ft/s}), so I predict 14 ft/s as the lowest speed at which every object stays on the table; the easiest case is the dinner plate ((\mu_c=0.26,\ \mu_t=0.20,\ s=0.70\ \mathrm{ft}), margin (5.35\ \mathrm{ft})), giving (v=\sqrt{32.2(0.70)^2(0.299)/5.35}\approx0.94\ \mathrm{ft/s}), so below 0.9 ft/s no object should stay on the table.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Tablecloth Yank — a friction & inertia toy</title>
<style>
:root{--ink:#2b2118;--paper:#fff8ec;--accent:#a83226;--gold:#c9973f}
*{box-sizing:border-box}
body{margin:0;font-family:Georgia,'Times New Roman',serif;background:#241a12;color:var(--ink);
background-image:radial-gradient(ellipse at 50% -10%,#4a3421 0,#241a12 60%)}
.wrap{max-width:1180px;margin:0 auto;padding:18px 14px 26px}
header{text-align:center;color:#f7e8cf;margin:4px 0 12px}
header h1{margin:0;font-size:clamp(26px,4vw,44px);letter-spacing:.5px;font-variant:small-caps}
header p{margin:6px 0 0;font-style:italic;color:#d9c39a}
.card{background:var(--paper);border:3px solid #6b4a2a;border-radius:14px;overflow:hidden;
box-shadow:0 18px 40px rgba(0,0,0,.55), inset 0 0 0 2px #e8d3a8}
canvas{display:block;width:100%;height:auto;background:#efe0c3}
.controls{display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:center;
padding:14px 16px;background:linear-gradient(#fffaf0,#f3e3c2);border-top:2px solid #c9a86a}
.ctl{display:flex;align-items:center;gap:9px;font-size:17px}
input[type=range]{width:min(300px,52vw);accent-color:var(--accent);height:28px}
input[type=number]{width:78px;font:inherit;font-size:18px;padding:5px 7px;border:2px solid #8a6a3e;border-radius:8px;background:#fff}
button{font:inherit;font-size:18px;font-weight:bold;cursor:pointer;border-radius:10px;padding:9px 20px;
border:2px solid #5d1f16;color:#fff;background:linear-gradient(#c8452f,#93291b);
box-shadow:0 4px 0 #5d1f16,0 6px 12px rgba(0,0,0,.25)}
button:active{transform:translateY(3px);box-shadow:0 1px 0 #5d1f16}
button.secondary{background:linear-gradient(#fdf6e3,#e7d3a4);color:#4a3319;border-color:#8a6a3e;box-shadow:0 4px 0 #8a6a3e}
button:disabled{opacity:.55;cursor:wait}
#status{width:100%;text-align:center;font-size:19px;min-height:26px;color:#5a3a17}
#status b{color:var(--accent)}
.legend{display:flex;flex-wrap:wrap;gap:6px 16px;justify-content:center;padding:9px 12px 13px;
font-size:13.5px;color:#6a5136;background:#f3e3c2}
.sw{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;border:1px solid rgba(0,0,0,.3)}
footer{color:#bfa87e;text-align:center;font-size:13px;margin-top:10px;font-style:italic}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>🍽️ The Tablecloth Yank</h1>
<p>Pull fast and dinner stays. Pull slow and dinner travels. Physics decides — friction against inertia.</p>
</header>
<div class="card">
<canvas id="cv" width="1150" height="700" aria-label="Side view of a set dinner table"></canvas>
<div class="controls">
<label class="ctl">Pull speed
<input type="range" id="speedRange" min="0.5" max="30" step="0.5" value="16">
<span><input type="number" id="speedNum" min="0" step="0.5" value="16"> ft/s</span>
</label>
<button id="yank">🧵 Yank the cloth!</button>
<button id="reset" class="secondary">↺ Reset table</button>
<div id="status">Set for dinner. Choose a speed and yank — or load with <b>#speed=12</b>.</div>
</div>
<div class="legend" id="legend"></div>
</div>
<footer>Side view · 1 ft grid-ish scale · g = 32.2 ft/s² · no physics libraries were harmed (or used).</footer>
</div>
<script>
"use strict";
/* ================= world constants (feet, seconds) ================= */
const G = 32.2; // ft/s^2
const FLOOR_Y = 0.8; // top of floor
const TABLE_L = 2.8, TABLE_R = 8.7, TABLE_TOP = 3.6;
const CLOTH_L0 = 2.65, CLOTH_R0 = 8.95; // cloth length 6.3 ft
const S = 90, OX = 28, OY = 655; // canvas scale / origin
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const sx = x => OX + x*S, sy = y => OY - y*S;
/* ================= objects: differ in mass, size, friction =================
muC = kinetic friction on cloth, muT = kinetic friction on table.
Static friction is modelled as 1.35 x kinetic (see solver). */
const DEFS = [
{type:'plate', name:'Dinner plate', x:3.35, w:0.95, h:0.13, m:1.10, muC:0.26, muT:0.20, col:'#f4f1e6'},
{type:'tumbler',name:'Tumbler', x:4.10, w:0.30, h:0.52, m:0.55, muC:0.30, muT:0.24, col:'#bfe0ea'},
{type:'wine', name:'Wine glass', x:4.75, w:0.26, h:0.68, m:0.35, muC:0.16, muT:0.13, col:'#d9eef2'},
{type:'bowl', name:'Soup bowl', x:5.50, w:0.72, h:0.30, m:0.90, muC:0.32, muT:0.25, col:'#c8452f'},
{type:'candle', name:'Candlestick', x:6.20, w:0.24, h:0.98, m:0.70, muC:0.36, muT:0.28, col:'#c9973f'},
{type:'mug', name:'Mug', x:6.90, w:0.34, h:0.38, m:0.65, muC:0.22, muT:0.18, col:'#3f6f9f'},
{type:'plate2', name:'Salad plate', x:7.50, w:0.78, h:0.11, m:0.80, muC:0.26, muT:0.20, col:'#e9e2cf'}
];
let bodies = [];
function makeBodies(){
bodies = DEFS.map(d=>({
...d, y: TABLE_TOP + d.h/2 + 0.02, vx:0, vy:0, a:0, w_:0,
invM: 1/d.m, invI: 1/(d.m*(d.w*d.w+d.h*d.h)/12)
}));
}
document.getElementById('legend').innerHTML = DEFS.map(d=>
`<span><span class="sw" style="background:${d.col}"></span>${d.name} · ${d.m.toFixed(2)} kg · μ cloth ${d.muC.toFixed(2)}</span>`).join('');
/* ================= cloth state ================= */
let cloth = null;
function freshCloth(){ cloth = {off:0, vx:0, target:0, pulling:false, done:false, fallY:0, fallV:0}; }
freshCloth();
let simTime = 0, yanked = false, finished = false, stillFrames = 0;
function corners(b){
const c=Math.cos(b.a), s=Math.sin(b.a), out=[];
for(const [lx,ly] of [[-b.w/2,-b.h/2],[b.w/2,-b.h/2],[b.w/2,b.h/2],[-b.w/2,b.h/2]])
out.push({x:b.x+lx*c-ly*s, y:b.y+lx*s+ly*c, rx:lx*c-ly*s, ry:lx*s+ly*c});
return out;
}
function onClothAt(x){
if(!cloth.pulling && cloth.off===0) return true; // cloth at rest under everything
const L = CLOTH_L0+cloth.off, R = CLOTH_R0+cloth.off;
return cloth.fallY===0 && x>=L && x<=R && x<=TABLE_R+0.25;
}
/* ================= physics step ================= */
function step(dt){
simTime += dt;
/* cloth: ramp to target speed in 0.12 s (that ramp is what static friction must beat to make objects stick) */
if(cloth.pulling && !cloth.done){
const accel = cloth.target/0.12;
cloth.vx = Math.min(cloth.target, cloth.vx + accel*dt);
cloth.off += cloth.vx*dt;
if(CLOTH_L0+cloth.off > TABLE_R+0.4){ cloth.fallV -= G*dt*0.6; cloth.fallY += cloth.fallV*dt; }
if(cloth.off > 12){ cloth.done = true; cloth.vx = 0; }
}
const clothVx = (cloth.pulling && !cloth.done) ? cloth.vx : 0;
for(const b of bodies){
b.vy -= G*dt;
b.x += b.vx*dt; b.y += b.vy*dt; b.a += b.w_*dt;
b.w_ *= 0.9995;
/* corner contacts against table top / floor — impulse solver.
Normal impulse at a corner + friction impulse there is what
produces sliding, sticking AND tipping torque. No tip scripts. */
for(let iter=0; iter<4; iter++){
for(const p of corners(b)){
const onTable = p.x>=TABLE_L && p.x<=TABLE_R;
const surfY = onTable ? TABLE_TOP : FLOOR_Y;
const pen = surfY - p.y;
if(pen <= 0) continue;
const onCl = onTable && onClothAt(p.x);
const muK = onCl ? b.muC : (onTable ? b.muT : b.muT*0.9);
const surfVx = onCl ? clothVx : 0;
// velocity of corner
let vpx = b.vx - b.w_*p.ry, vpy = b.vy + b.w_*p.rx;
// normal impulse (with a little penetration bias)
const rn = p.rx; // r x n, n=(0,1)
const effN = b.invM + b.invI*rn*rn;
const bias = Math.min(pen/dt*0.18, 3);
let jn = -(vpy - bias)/effN; if(jn<0) jn=0;
b.vx += 0; b.vy += jn*b.invM; b.w_ += b.invI * rn * jn;
// friction impulse, clamped by mu * normal (static boost at low slip)
vpx = b.vx - b.w_*p.ry;
const rel = vpx - surfVx;
const mu = Math.abs(rel) < 0.12 ? muK*1.35 : muK;
const rt = -p.ry; // r x t, t=(1,0)
const effT = b.invM + b.invI*rt*rt;
let jt = -rel/effT;
const maxF = mu*jn;
jt = Math.max(-maxF, Math.min(maxF, jt));
b.vx += jt*b.invM; b.w_ += b.invI * rt * jt;
}
}
// positional de-penetration (keeps resting stacks calm)
let minPen = 0;
for(const p of corners(b)){
const surfY = (p.x>=TABLE_L && p.x<=TABLE_R) ? TABLE_TOP : FLOOR_Y;
minPen = Math.max(minPen, surfY - p.y);
}
if(minPen>0) b.y += minPen*0.85;
if(b.y < FLOOR_Y) b.y = FLOOR_Y;
}
/* object–object collisions: oriented boxes approximated by their AABBs,
resolved along the axis of least penetration, with a soft bounce. */
for(let i=0;i<bodies.length;i++) for(let j=i+1;j<bodies.length;j++){
const A=bodies[i], B=bodies[j];
const ext = b=>{const c=Math.abs(Math.cos(b.a)),s=Math.abs(Math.sin(b.a));
return {ex:(b.w*c+b.h*s)/2, ey:(b.w*s+b.h*c)/2};};
const ea=ext(A), eb=ext(B);
const dx=B.x-A.x, dy=B.y-A.y;
const ox=(ea.ex+eb.ex)-Math.abs(dx), oy=(ea.ey+eb.ey)-Math.abs(dy);
if(ox<=0||oy<=0) continue;
let nx=0,ny=0,pen;
if(ox<oy){ nx=dx>=0?1:-1; pen=ox; } else { ny=dy>=0?1:-1; pen=oy; }
const tot=A.invM+B.invM;
A.x-=nx*pen*(A.invM/tot); A.y-=ny*pen*(A.invM/tot);
B.x+=nx*pen*(B.invM/tot); B.y+=ny*pen*(B.invM/tot);
const rvx=B.vx-A.vx, rvy=B.vy-A.vy, vn=rvx*nx+rvy*ny;
if(vn<0){
const imp=-(1.15)*vn/tot;
A.vx-=imp*nx*A.invM; A.vy-=imp*ny*A.invM;
B.vx+=imp*nx*B.invM; B.vy+=imp*ny*B.invM;
A.w_*=Math.max(-6,Math.min(6,A.w_*+(Math.random()-0.5)*Math.abs(vn)*0.35));
B.w_*=Math.max(-6,Math.min(6,B.w_*+(Math.random()-0.5)*Math.abs(vn)*0.35));
}
}
}
/* ================= end-of-simulation detection ================= */
const statusEl = document.getElementById('status');
function normAngle(a){ while(a>Math.PI)a-=2*Math.PI; while(a<-Math.PI)a+=2*Math.PI; return a; }
function checkEnd(){
if(!yanked || finished) return;
const allStill = bodies.every(b=>Math.hypot(b.vx,b.vy)<0.09 && Math.abs(b.w_)<0.09);
stillFrames = allStill ? stillFrames+1 : 0;
if((stillFrames>50 && simTime>1.2) || simTime>25){
finished = true;
for(const b of bodies){ b.vx=b.vy=b.w_=0; }
let onTable=0, upright=0;
for(const b of bodies){
const lowY = Math.min(...corners(b).map(p=>p.y));
const on = b.x>TABLE_L-0.05 && b.x<TABLE_R+0.05 && Math.abs(lowY-TABLE_TOP)<0.28;
if(on){ onTable++; if(Math.abs(normAngle(b.a))<0.42) upright++; }
}
statusEl.innerHTML = `🏁 Settled after ${simTime.toFixed(1)} s at ${lastSpeed} ft/s — ` +
`<b>${onTable} of ${bodies.length}</b> objects remain on the table, <b>${upright}</b> of those remain upright.`;
yankBtn.disabled = false;
}
}
/* ================= drawing ================= */
function drawObject(b){
ctx.save();
ctx.translate(sx(b.x), sy(b.y));
ctx.rotate(-b.a);
ctx.scale(S,-S);
ctx.lineWidth = 0.025;
const w=b.w, h=b.h, up = Math.abs(normAngle(b.a))<1.0;
ctx.strokeStyle='rgba(60,40,20,.55)';
if(b.type==='plate'||b.type==='plate2'){
ctx.fillStyle=b.col;
ctx.beginPath(); ctx.moveTo(-w/2,-h/2); ctx.lineTo(w/2,-h/2); ctx.lineTo(w*0.36,h/2); ctx.lineTo(-w*0.36,h/2); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.ellipse(0,h*0.1,w*0.26,h*0.32,0,0,7); ctx.stroke();
} else if(b.type==='bowl'){
ctx.fillStyle=b.col;
ctx.beginPath(); ctx.moveTo(-w/2,h/2); ctx.quadraticCurveTo(0,-h*0.9,w/2,h/2); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.fillStyle='#f6d9a0'; ctx.beginPath(); ctx.ellipse(0,h/2,w*0.44,h*0.22,0,0,7); ctx.fill(); ctx.stroke();
} else if(b.type==='wine'){
ctx.fillStyle='rgba(190,225,235,.75)';
ctx.beginPath(); ctx.moveTo(-w*0.32,-h/2); ctx.lineTo(w*0.32,-h/2); ctx.lineTo(w*0.10,-h*0.42); ctx.lineTo(w*0.10,h*0.05);
ctx.quadraticCurveTo(w*0.55,h*0.28,w*0.42,h/2); ctx.lineTo(-w*0.42,h/2);
ctx.quadraticCurveTo(-w*0.55,h*0.28,-w*0.10,h*0.05); ctx.lineTo(-w*0.10,-h*0.42); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.fillStyle='rgba(140,20,40,.65)'; ctx.beginPath(); ctx.ellipse(0,h*0.30,w*0.34,h*0.16,0,0,7); ctx.fill();
} else if(b.type==='tumbler'){
ctx.fillStyle='rgba(175,215,230,.7)';
ctx.beginPath(); ctx.moveTo(-w/2,-h/2); ctx.lineTo(w/2,-h/2); ctx.lineTo(w*0.42,h/2); ctx.lineTo(-w*0.42,h/2); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.fillStyle='rgba(70,140,190,.55)'; ctx.fillRect(-w*0.44,0,w*0.88,h*0.42);
} else if(b.type==='mug'){
ctx.fillStyle=b.col; ctx.fillRect(-w/2,-h/2,w,h); ctx.strokeRect(-w/2,-h/2,w,h);
ctx.beginPath(); ctx.arc(w/2,0,w*0.34,-1.2,1.2); ctx.stroke();
ctx.fillStyle='#6b4226'; ctx.fillRect(-w/2,h*0.18,w,h*0.3);
} else if(b.type==='candle'){
ctx.fillStyle=b.col;
ctx.beginPath(); ctx.moveTo(-w*0.9,-h/2); ctx.lineTo(w*0.9,-h/2); ctx.lineTo(w*0.28,-h*0.34); ctx.lineTo(-w*0.28,-h*0.34); ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.fillRect(-w*0.16,-h*0.34,w*0.32,h*0.42); ctx.strokeRect(-w*0.16,-h*0.34,w*0.32,h*0.42);
ctx.fillStyle='#fdf6dd'; ctx.fillRect(-w*0.30,h*0.08,w*0.60,h*0.40); ctx.strokeRect(-w*0.30,h*0.08,w*0.60,h*0.40);
if(up){ ctx.fillStyle='#ff9d2e'; ctx.beginPath(); ctx.ellipse(0,h*0.56,w*0.16,h*0.09,0,0,7); ctx.fill();
ctx.fillStyle='#ffe98a'; ctx.beginPath(); ctx.ellipse(0,h*0.545,w*0.07,h*0.05,0,0,7); ctx.fill(); }
}
ctx.restore();
}
function draw(){
ctx.clearRect(0,0,cv.width,cv.height);
// wall & picture
const wall=ctx.createLinearGradient(0,0,0,sy(FLOOR_Y)); wall.addColorStop(0,'#e8d9b8'); wall.addColorStop(1,'#d9c294');
ctx.fillStyle=wall; ctx.fillRect(0,0,cv.width,sy(FLOOR_Y));
ctx.fillStyle='#8a6a3e'; ctx.fillRect(sx(4.6),sy(6.6),300,150);
ctx.fillStyle='#b7d3dd'; ctx.fillRect(sx(4.6)+10,sy(6.6)+10,280,130);
ctx.fillStyle='#7a9a5b'; ctx.beginPath(); ctx.moveTo(sx(4.6)+10,sy(6.6)+130); ctx.lineTo(sx(5.6),sy(6.9)); ctx.lineTo(sx(6.6),sy(6.6)+130); ctx.fill();
// floor
const fl=ctx.createLinearGradient(0,sy(FLOOR_Y),0,cv.height); fl.addColorStop(0,'#9a6b3d'); fl.addColorStop(1,'#6f4a26');
ctx.fillStyle=fl; ctx.fillRect(0,sy(FLOOR_Y),cv.width,cv.height-sy(FLOOR_Y));
ctx.strokeStyle='rgba(0,0,0,.18)'; for(let i=0;i<12;i++){ ctx.beginPath(); ctx.moveTo(i*105,sy(FLOOR_Y)); ctx.lineTo(i*105-40,cv.height); ctx.stroke(); }
// table
ctx.fillStyle='rgba(0,0,0,.20)'; ctx.beginPath(); ctx.ellipse(sx(5.75),sy(FLOOR_Y)+8,(TABLE_R-TABLE_L)*S*0.55,14,0,0,7); ctx.fill();
ctx.fillStyle='#6f4525'; ctx.fillRect(sx(TABLE_L)-8,sy(TABLE_TOP), (TABLE_R-TABLE_L)*S+16, 16);
ctx.fillStyle='#7d5029';
for(const lx of [TABLE_L+0.25, TABLE_R-0.45]) ctx.fillRect(sx(lx),sy(TABLE_TOP)+14,20,sy(FLOOR_Y)-sy(TABLE_TOP)-14);
ctx.fillStyle='#5d3a1e'; ctx.fillRect(sx(TABLE_L+0.25),sy(1.55),(TABLE_R-TABLE_L-0.7)*S,10);
// cloth (under objects)
const cl=CLOTH_L0+cloth.off, cr=CLOTH_R0+cloth.off, cyT=TABLE_TOP+0.045+cloth.fallY;
ctx.save();
ctx.beginPath();
ctx.moveTo(sx(cl),sy(cyT));
for(let x=cl;x<=Math.min(cr,TABLE_R+1.6);x+=0.3) ctx.lineTo(sx(x),sy(cyT+Math.sin(x*7+cloth.off*3)*0.008));
const hang = Math.max(0, Math.min(cr,TABLE_R+1.6)-TABLE_R);
ctx.lineTo(sx(Math.min(cr,TABLE_R+1.6)), sy(cyT - hang*0.55 - Math.max(0,cloth.fallY)*-1));
ctx.lineTo(sx(Math.min(cr,TABLE_R+1.6)), sy(cyT - hang*0.55)+7);
ctx.lineTo(sx(cl),sy(cyT)+7); ctx.closePath();
ctx.fillStyle='#fdfaf2'; ctx.fill(); ctx.strokeStyle='#b8a888'; ctx.lineWidth=1.5; ctx.stroke();
ctx.fillStyle='#a83226'; ctx.fillRect(sx(cl),sy(cyT)-1,7,8); ctx.fillRect(sx(Math.min(cr,TABLE_R+1.6))-7,sy(cyT)-1,7,8);
ctx.restore();
// objects
for(const b of bodies) drawObject(b);
// speed arrow while pulling
if(cloth.pulling && !cloth.done && cloth.vx>0.3){
ctx.fillStyle='#a83226'; ctx.font='bold 20px Georgia'; ctx.textAlign='left';
ctx.fillText(`cloth → ${cloth.vx.toFixed(1)} ft/s`, sx(cl+0.2), sy(TABLE_TOP)-28);
}
}
/* ================= main loop ================= */
let lastT = performance.now(), lastSpeed = 16;
function frame(now){
let dt = Math.min((now-lastT)/1000, 0.05); lastT = now;
const sub = Math.max(1, Math.round(dt/(1/480)));
const sdt = dt/sub;
for(let i=0;i<sub;i++) step(sdt);
checkEnd();
draw();
requestAnimationFrame(frame);
}
/* ================= controls ================= */
const range=document.getElementById('speedRange'), num=document.getElementById('speedNum'), yankBtn=document.getElementById('yank');
range.addEventListener('input',()=>{ num.value=range.value; });
num.addEventListener('input',()=>{ const v=parseFloat(num.value); if(isFinite(v)) range.value=Math.max(0.5,Math.min(30,v)); });
function currentSpeed(){ const v=parseFloat(num.value); return isFinite(v)? Math.max(0,v) : 16; }
function yank(){
if(cloth.pulling && !finished) return;
if(finished || cloth.off>0) resetTable();
lastSpeed = currentSpeed();
cloth.target = lastSpeed; cloth.pulling = true;
yanked = true; finished = false; stillFrames = 0; simTime = 0;
yankBtn.disabled = true;
statusEl.textContent = `💨 Yanking at ${lastSpeed} ft/s… friction vs. inertia, no take-backs.`;
}
function resetTable(){
makeBodies(); freshCloth();
yanked=false; finished=false; stillFrames=0; simTime=0;
yankBtn.disabled=false;
statusEl.textContent = 'Table reset. Dinner is served — choose a speed and yank.';
}
yankBtn.addEventListener('click', yank);
document.getElementById('reset').addEventListener('click', resetTable);
/* ================= start, incl. #speed=NN auto-pull ================= */
makeBodies(); draw();
requestAnimationFrame(frame);
(function(){
const m = /speed=([^&]+)/.exec(location.hash||'');
if(m){
const v = parseFloat(decodeURIComponent(m[1]));
if(isFinite(v)){
num.value = v; range.value = Math.max(0.5, Math.min(30, v));
statusEl.textContent = `Loaded with #speed=${v} — yanking in 1 second…`;
setTimeout(yank, 1000);
}
}
})();
</script>
</body>
</html>
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.
Where it is honest physics
Special cases, thresholds, and cheats you should know about
#speed=200) the cloth ramp acceleration is so violent that corner impulses can pop a light wine glass slightly upward; it still obeys the solver, but no human arm, or tablecloth, survives that yank.