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.
Each object slides on the cloth at friction-limited acceleration μc·g until the cloth's leading edge passes it. Its cloth-relative slip must cover D = x + w/2 + 3.5 ft, where 3.5 ft is the cloth's left overhang and the cloth is 8 ft long, so it covers all six objects at the start. Once exposed, it slides on bare wood (μt = 0.45) and must travel M = 3 − x ft to reach the table edge. Combining the two phases gives the speed where an object just reaches the edge: solve ½·μc·g·t²·(1 + μc/μt) = M for t, set a = μc·g·t, then v = (2·μc·g·D + a²)/(2a). Using g = 32.2 ft/s², the six objects give: left plate (μ 0.30, D 2.0, M 4.9) → 6.3 ft/s; left glass (0.25, 2.7, 3.9) → 6.6; bowl (0.30, 3.5, 3.3) → 8.5; candle (0.35, 3.8, 2.8) → 10.2; right plate (0.30, 5.2, 1.7) → 13.5; right glass (0.25, 5.8, 0.8) → 17.7. The lowest speed at which every object stays on the table is therefore 17.7 ft/s, and the speed below which no object stays is 6.3 ft/s. Object masses (0.2–0.9 kg) cancel out of every sliding threshold, since friction is μN with N = mg; they matter only for collisions. This estimate ignores tipping, object-to-object hits, and the 0.08 s cloth ramp in the page, so treat these as hand-computed predictions I have not measured.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>The Tablecloth Trick</title>
<style>
html,body{margin:0;background:#17130f;color:#eadcc2;font:15px/1.45 Georgia,"Times New Roman",serif}
#wrap{max-width:880px;margin:0 auto;padding:14px}
h1{font-size:20px;font-weight:normal;letter-spacing:.04em;margin:4px 0 10px;color:#f1dfb8}
canvas{display:block;width:100%;height:auto;border-radius:10px;border:1px solid #3c3129}
.row{display:flex;flex-wrap:wrap;gap:12px 18px;align-items:center;margin:12px 0}
button{font:inherit;color:#f6e9cc;background:#4a3525;border:1px solid #b99a64;border-radius:7px;padding:7px 16px;cursor:pointer}
button:disabled{opacity:.45;cursor:default}
input[type=range]{width:280px;accent-color:#c9a25f}
#spdLabel{min-width:9em;font-variant-numeric:tabular-nums;color:#f6e9cc}
#hud{font-variant-numeric:tabular-nums;opacity:.85}
#out{background:#211b16;border:1px solid #3c3129;border-radius:8px;padding:10px 14px;min-height:4.5em}
#out ul{margin:6px 0 0;padding-left:1.2em}
</style>
</head>
<body>
<div id="wrap">
<h1>The tablecloth trick: a friction-and-inertia toy</h1>
<canvas id="c" width="840" height="480"></canvas>
<div class="row">
<label>Pull speed <input id="spd" type="range" min="0" max="40" step="0.5" value="12"></label>
<span id="spdLabel">12 ft/s</span>
<button id="yank">Yank the cloth</button>
<button id="reset">Reset table</button>
<span id="hud"></span>
</div>
<div id="out">Choose a pull speed and yank. The simulation runs until everything stops.</div>
</div>
<script>
(()=>{
'use strict';
const cv=document.getElementById('c'), ctx=cv.getContext('2d');
const W=cv.width, H=cv.height, S=50, OX=W/2, OY=300;
const spd=document.getElementById('spd'), spdLabel=document.getElementById('spdLabel');
const btnYank=document.getElementById('yank'), btnReset=document.getElementById('reset');
const hud=document.getElementById('hud'), out=document.getElementById('out');
// ---- constants (feet, seconds, kg as labels) ----
const G=32.2, TABLE_HALF=3.0, TABLE_DEPTH=0.3, FLOOR_Y=-3.0;
const MU_TABLE=0.45, MU_FLOOR=0.5, MU_OBJ=0.35;
const CLOTH_S0=-3.5, CLOTH_L=8.0, RAMP=0.08;
const DT=1/1440, SUB=24, ITER=12, BAUM=0.2, SLOP=0.002, REST=0.1;
const LAYOUT=[
{name:'Dinner plate, left', kind:'plate', x:-1.9, w:0.80, h:0.06, m:0.45, mu:0.30},
{name:'Dinner plate, right', kind:'plate', x: 1.3, w:0.80, h:0.06, m:0.45, mu:0.30},
{name:'Wine glass, left', kind:'glass', x:-0.9, w:0.25, h:0.55, m:0.20, mu:0.25},
{name:'Wine glass, right', kind:'glass', x: 2.2, w:0.25, h:0.55, m:0.20, mu:0.25},
{name:'Candlestick', kind:'candle',x: 0.2, w:0.20, h:0.90, m:0.90, mu:0.35},
{name:'Soup bowl', kind:'bowl', x:-0.3, w:0.50, h:0.25, m:0.60, mu:0.30},
];
function fresh(){
return LAYOUT.map(d=>({
name:d.name, kind:d.kind, m:d.m, mu:d.mu,
hw:d.w/2, hh:d.h/2,
x:d.x, y:d.h/2-0.001, th:0, vx:0, vy:0, om:0,
invM:1/d.m, invI:12/(d.m*(d.w*d.w+d.h*d.h)),
onT:false, up:false
}));
}
function newCloth(){ return {s:CLOTH_S0,u:0,v:0,t:0,pulling:false,used:false}; }
let bodies=fresh(), cloth=newCloth();
let running=false, finished=false, tSim=0, restT=0, pullSpeed=12, autoTimer=0;
const wrap=a=>{ a=(a+Math.PI)%(2*Math.PI); if(a<0)a+=2*Math.PI; return a-Math.PI; };
// ---- geometry ----
function corners(b){
const c=Math.cos(b.th), s=Math.sin(b.th);
return [[-b.hw,-b.hh],[b.hw,-b.hh],[b.hw,b.hh],[-b.hw,b.hh]].map(([lx,ly])=>
({x:b.x+c*lx-s*ly, y:b.y+s*lx+c*ly}));
}
function toLocal(b,p){
const c=Math.cos(b.th), s=Math.sin(b.th), dx=p.x-b.x, dy=p.y-b.y;
return {x:c*dx+s*dy, y:-s*dx+c*dy};
}
function toWorldDir(b,lx,ly){
const c=Math.cos(b.th), s=Math.sin(b.th);
return {x:c*lx-s*ly, y:s*lx+c*ly};
}
function coveredAt(x){
return cloth.pulling && x>=cloth.s && x<=cloth.s+CLOTH_L && x>=-TABLE_HALF && x<=TABLE_HALF;
}
// ---- contacts ----
function boxVsBox(P,Q,pts,out){
for(const p of pts){
const q=toLocal(Q,p);
if(Math.abs(q.x)<Q.hw && Math.abs(q.y)<Q.hh){
const dx=Q.hw-Math.abs(q.x), dy=Q.hh-Math.abs(q.y);
let lx=0, ly=0, d;
if(dx<dy){ lx=Math.sign(q.x)||1; d=dx; }
else { ly=Math.sign(q.y)||1; d=dy; }
const n=toWorldDir(Q,lx,ly);
out.push({a:P,b:Q,p,n,d,mu:MU_OBJ,sv:0});
}
}
}
function buildContacts(){
const cs=[];
const pts=bodies.map(corners);
bodies.forEach((b,i)=>{
for(const p of pts[i]){
if(p.x>=-TABLE_HALF && p.x<=TABLE_HALF && p.y<0 && p.y>-TABLE_DEPTH){
const on=coveredAt(p.x);
cs.push({a:b,b:null,p,n:{x:0,y:1},d:-p.y,
mu:on?b.mu:MU_TABLE, sv:on?cloth.u:0});
} else if(p.y<FLOOR_Y){
cs.push({a:b,b:null,p,n:{x:0,y:1},d:FLOOR_Y-p.y,mu:MU_FLOOR,sv:0});
}
}
});
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 reach=Math.hypot(A.hw,A.hh)+Math.hypot(B.hw,B.hh);
if(Math.hypot(A.x-B.x,A.y-B.y)>reach) continue;
boxVsBox(A,B,pts[i],cs);
boxVsBox(B,A,pts[j],cs);
}
return cs;
}
// ---- impulse solver ----
function pointVel(b,r){ return {x:b.vx-b.om*r.y, y:b.vy+b.om*r.x}; }
function relVel(c){
const va=pointVel(c.a,c.ra);
const vb=c.b?pointVel(c.b,c.rb):{x:c.sv,y:0};
return {x:va.x-vb.x, y:va.y-vb.y};
}
function eff(c,d){
const a=c.a, r=c.ra;
const ra=r.x*d.y-r.y*d.x;
let k=a.invM+ra*ra*a.invI;
if(c.b){
const b=c.b, q=c.rb;
const rb=q.x*d.y-q.y*d.x;
k+=b.invM+rb*rb*b.invI;
}
return k;
}
function applyImp(c,px,py){
const a=c.a;
a.vx+=px*a.invM; a.vy+=py*a.invM;
a.om+=(c.ra.x*py-c.ra.y*px)*a.invI;
if(c.b){
const b=c.b, q=c.rb;
b.vx-=px*b.invM; b.vy-=py*b.invM;
b.om-=(q.x*py-q.y*px)*b.invI;
}
}
function solve(cs){
for(const c of cs){
c.ra={x:c.p.x-c.a.x, y:c.p.y-c.a.y};
c.rb=c.b?{x:c.p.x-c.b.x, y:c.p.y-c.b.y}:null;
c.t={x:-c.n.y, y:c.n.x};
c.kn=eff(c,c.n); c.kt=eff(c,c.t);
const v0=relVel(c);
c.vn0=v0.x*c.n.x+v0.y*c.n.y;
const restB=c.vn0<-0.5 ? -REST*c.vn0 : 0;
c.bias=Math.max(restB, BAUM/DT*Math.max(c.d-SLOP,0));
c.ln=0; c.lt=0;
}
for(let it=0;it<ITER;it++){
for(const c of cs){
const v1=relVel(c);
const vn=v1.x*c.n.x+v1.y*c.n.y;
const old=c.ln;
c.ln=Math.max(0, old+(c.bias-vn)/c.kn);
const dn=c.ln-old;
if(dn!==0) applyImp(c,dn*c.n.x,dn*c.n.y);
const v2=relVel(c);
const vt=v2.x*c.t.x+v2.y*c.t.y;
const oldT=c.lt, mx=c.mu*c.ln;
c.lt=Math.max(-mx, Math.min(mx, oldT-vt/c.kt));
const dT=c.lt-oldT;
if(dT!==0) applyImp(c,dT*c.t.x,dT*c.t.y);
}
}
}
// ---- stepping ----
function physStep(){
if(cloth.pulling){
cloth.t+=DT;
cloth.u=cloth.v*Math.min(1,cloth.t/RAMP);
cloth.s+=cloth.u*DT;
if(cloth.v===0 || cloth.s>TABLE_HALF+0.2){ cloth.pulling=false; cloth.u=0; }
}
const cs=buildContacts();
for(const b of bodies) b.vy-=G*DT;
solve(cs);
for(const b of bodies){
b.x+=b.vx*DT; b.y+=b.vy*DT; b.th+=b.om*DT;
}
let vmax=0;
for(const b of bodies) vmax=Math.max(vmax, Math.hypot(b.vx,b.vy), Math.abs(b.om)*0.3);
tSim+=DT;
if(!cloth.pulling && vmax<0.03) restT+=DT; else restT=0;
if(restT>0.5 || tSim>40) finish();
}
function finish(){
running=false; finished=true;
for(const b of bodies){
b.onT = Math.abs(b.x)<=TABLE_HALF && b.y>-0.1;
b.up = b.onT && Math.abs(wrap(b.th))<0.61;
}
const on=bodies.filter(b=>b.onT).length;
const up=bodies.filter(b=>b.up).length;
out.innerHTML=`<b>Settled at ${tSim.toFixed(2)} s.</b> Still on the table: <b>${on} of ${bodies.length}</b>. Still upright: <b>${up} of ${bodies.length}</b>.<ul>`+
bodies.map(b=>`<li>${b.name}: ${b.onT?'on table':'off table'}${b.onT?(b.up?', upright':', knocked over'):''}</li>`).join('')+`</ul>`;
}
function yank(){
if(cloth.pulling || cloth.used) return;
cloth.v=pullSpeed; cloth.t=0; cloth.u=0; cloth.pulling=true; cloth.used=true;
running=true; finished=false; tSim=0; restT=0;
btnYank.disabled=true;
out.textContent=`Pulling at ${+pullSpeed.toFixed(2)} ft/s...`;
}
function reset(){
clearTimeout(autoTimer);
bodies=fresh(); cloth=newCloth();
running=false; finished=false; tSim=0; restT=0;
btnYank.disabled=false;
out.textContent='Table reset. Choose a pull speed and yank.';
}
// ---- drawing ----
function shape(b){
ctx.lineWidth=0.012;
if(b.kind==='plate'){
ctx.fillStyle='#fbfaf5'; ctx.strokeStyle='#7f93ab';
ctx.beginPath(); ctx.ellipse(0,0,0.4,0.03,0,0,Math.PI*2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.ellipse(0,0,0.27,0.018,0,0,Math.PI*2); ctx.stroke();
} else if(b.kind==='glass'){
ctx.fillStyle='rgba(190,225,240,.6)'; ctx.strokeStyle='#d6eefa';
ctx.beginPath();
ctx.moveTo(-0.12,0.275); ctx.lineTo(0.12,0.275); ctx.lineTo(0.08,0.0);
ctx.lineTo(0.03,-0.15); ctx.lineTo(0.03,-0.24); ctx.lineTo(0.09,-0.275);
ctx.lineTo(-0.09,-0.275); ctx.lineTo(-0.03,-0.24); ctx.lineTo(-0.03,-0.15);
ctx.lineTo(-0.08,0.0); ctx.closePath(); ctx.fill(); ctx.stroke();
} else if(b.kind==='candle'){
ctx.fillStyle='#efe2bf'; ctx.strokeStyle='#9c8a62';
ctx.fillRect(-0.1,-0.45,0.2,0.9); ctx.strokeRect(-0.1,-0.45,0.2,0.9);
ctx.fillStyle='#ffb14a';
ctx.beginPath(); ctx.ellipse(0,0.5,0.05,0.1,0,0,Math.PI*2); ctx.fill();
} else if(b.kind==='bowl'){
ctx.fillStyle='#4f7fa8'; ctx.strokeStyle='#2c4d68';
ctx.beginPath(); ctx.moveTo(-0.25,0.125); ctx.lineTo(0.25,0.125);
ctx.quadraticCurveTo(0.2,-0.12,0,-0.125);
ctx.quadraticCurveTo(-0.2,-0.12,-0.25,0.125);
ctx.closePath(); ctx.fill(); ctx.stroke();
}
}
function draw(){
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle='#2b2420'; ctx.fillRect(0,0,W,H);
// world transform: x right, y up, 1 ft = S px
ctx.setTransform(S,0,0,-S,OX,OY);
ctx.fillStyle='#0f0c0a'; ctx.fillRect(-9,-3.4,18,0.4);
ctx.fillStyle='#4a3324';
ctx.fillRect(-2.8,-3,0.14,2.7); ctx.fillRect(2.66,-3,0.14,2.7);
ctx.fillStyle='#7a5536'; ctx.fillRect(-TABLE_HALF,-TABLE_DEPTH,2*TABLE_HALF,TABLE_DEPTH);
ctx.fillStyle='#8c6640'; ctx.fillRect(-TABLE_HALF,0,2*TABLE_HALF,0.01);
// cloth
const s=cloth.s, e=s+CLOTH_L;
ctx.fillStyle='#efe6d2';
const a=Math.max(s,-TABLE_HALF), b=Math.min(e,TABLE_HALF);
if(b>a) ctx.fillRect(a,0,b-a,0.025);
if(s<-TABLE_HALF) ctx.fillRect(s,-0.6,-TABLE_HALF-s,0.625);
if(e>TABLE_HALF) ctx.fillRect(TABLE_HALF,-0.6,Math.min(e,8.4)-TABLE_HALF,0.625);
for(const bd of bodies){
ctx.save(); ctx.translate(bd.x,bd.y); ctx.rotate(bd.th); shape(bd); ctx.restore();
}
}
function updLabel(){ spdLabel.textContent=`${+pullSpeed.toFixed(2)} ft/s`; }
function frame(){
if(running){ for(let i=0;i<SUB && running;i++) physStep(); }
draw();
const moving=bodies.filter(b=>Math.hypot(b.vx,b.vy)>0.03).length;
hud.textContent = running ? `t = ${tSim.toFixed(2)} s · cloth ${cloth.u.toFixed(1)} ft/s · ${moving} moving`
: (finished ? 'settled' : 'ready');
requestAnimationFrame(frame);
}
// ---- controls ----
spd.addEventListener('input',()=>{ pullSpeed=Number(spd.value); updLabel(); });
btnYank.addEventListener('click',yank);
btnReset.addEventListener('click',reset);
// #speed=12 : set pull speed, yank one second after load
const hm=/speed=([^&]+)/.exec(location.hash||'');
if(hm){
const v=Number(hm[1]);
if(isFinite(v)){
pullSpeed=Math.abs(v);
spd.value=Math.min(40,pullSpeed);
updLabel();
autoTimer=setTimeout(yank,1000);
}
}
updLabel();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Where it resembles the trick. Cloth friction acts on each object's base corners, so objects are dragged, and tipping comes out of the contact torques rather than a script. Slow pulls let each object ride the cloth to the right edge. Fast pulls shorten the slip time, so objects stay put. In between, the candlestick tips, and objects near the right edge slide off.
Where it does not.