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.
I predict that about 17.8 ft/s is the lowest speed at which everything stays on the table, and that below about 5.4 ft/s everything falls off—except at exactly zero, when nothing is pulled. These are estimates, not guarantees: treating each object independently, the 2.4 m cloth, 0.32 bare-table friction coefficient, and the rightmost glass at 2.15 m with cloth friction 0.20 give a stopping-distance estimate of (v=2.15\sqrt{9.81(0.20)(1+0.20/0.32)/(2(2.4-2.15))}=5.43) m/s, or 17.8 ft/s. For the slow estimate, the first plate, at 0.35 m with friction 0.40, gives (v=\sqrt{2(0.40)(9.81)(0.35)}=1.66) m/s, or 5.4 ft/s. The masses range from 0.35 to 1.4 kg; they cancel out of those simplified sliding estimates. Tipping and collisions can change both results.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The dinner-table experiment</title>
<style>
:root {
color-scheme: light;
font-family: system-ui, -apple-system, sans-serif;
background: #f2eee5;
color: #29332f;
}
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1100px; margin: auto; padding: 24px 20px 30px; }
header { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 12px; }
h1 { font-family: Georgia, serif; font-weight: normal; font-size: clamp(2rem, 5vw, 3.5rem); margin: 0; }
.eyebrow { text-transform: uppercase; letter-spacing: .16em; font-size: .7rem; font-weight: 750; color: #a44f37; margin: 0 0 5px; }
.intro { margin: 5px 0 0; color: #637069; max-width: 48ch; }
.controls { display: flex; flex-wrap: wrap; align-items: end; gap: 10px; margin: 22px 0 12px; }
label { font-size: .78rem; font-weight: 750; color: #526058; display: grid; gap: 5px; }
.input-wrap { display: flex; align-items: center; background: white; border: 1px solid #c8cfc7; border-radius: 7px; overflow: hidden; }
input { width: 100px; padding: 11px; border: 0; font: inherit; font-size: 1rem; color: #24342d; outline-offset: -3px; }
.unit { padding: 0 11px 0 0; color: #718078; font-weight: 600; }
button { border: 0; border-radius: 7px; padding: 12px 19px; font: inherit; font-weight: 750; cursor: pointer; }
button:focus-visible, input:focus-visible { outline: 3px solid #db9b52; }
#pull { background: #a64d35; color: white; }
#pull:hover { background: #873b29; }
#reset { background: #dce3da; color: #344c3e; }
#reset:hover { background: #c9d5c8; }
button:disabled { opacity: .5; cursor: default; }
.scene { border: 1px solid #d7d9cc; border-radius: 12px; overflow: hidden; background: #e9eee4; box-shadow: 0 8px 30px #354b3410; }
canvas { display: block; width: 100%; height: auto; aspect-ratio: 1000 / 510; }
.bottom { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; align-items: baseline; padding: 13px 2px; }
#status { font-weight: 700; min-height: 1.4em; }
.hint { font-size: .85rem; color: #68776d; }
.key { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: .8rem; color: #637069; }
.key span::before { content: ""; display: inline-block; width: 9px; height: 9px; margin-right: 6px; border-radius: 50%; background: var(--c); }
</style>
</head>
<body>
<main>
<header>
<div>
<p class="eyebrow">A small experiment in inertia</p>
<h1>The dinner-table trick</h1>
<p class="intro">Pull the cloth to the right. The dishes have different weights, shapes and grips.</p>
</div>
</header>
<div class="controls">
<label for="speed">Pull speed
<span class="input-wrap"><input id="speed" type="number" step="any" value="12"><span class="unit">ft/s</span></span>
</label>
<button id="pull" type="button">Yank the cloth →</button>
<button id="reset" type="button">Reset table</button>
</div>
<div class="scene">
<canvas id="scene" width="1000" height="510" aria-label="Side view of a dinner table and its dishes"></canvas>
</div>
<div class="bottom">
<div id="status" role="status" aria-live="polite">Ready for dinner.</div>
<div class="hint">Tip: try a slow pull, then a quick one.</div>
</div>
<div class="key">
<span style="--c:#f1e6cd">plates</span>
<span style="--c:#77aeb3">glasses</span>
<span style="--c:#c48b47">candlestick</span>
<span style="--c:#b76551">bowl</span>
</div>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const speedInput = document.getElementById("speed");
const pullButton = document.getElementById("pull");
const resetButton = document.getElementById("reset");
const status = document.getElementById("status");
// SI units in the simulation; the interface is in feet per second.
const G = 9.81, TABLE = 2.4, TOP = 0.85, DT = 1 / 240;
const BARE = 0.32, FLOOR_MU = 0.48;
const specs = [
{kind:"plate", x:.35, w:.30, h:.045, mass:1.10, mu:.40},
{kind:"glass", x:.68, w:.14, h:.22, mass:.35, mu:.28},
{kind:"plate", x:1.00,w:.30, h:.045, mass:1.10, mu:.40},
{kind:"candle", x:1.30,w:.13, h:.37, mass:.75, mu:.24},
{kind:"glass", x:1.60,w:.14, h:.22, mass:.35, mu:.28},
{kind:"bowl", x:1.88,w:.30, h:.14, mass:1.40, mu:.34},
{kind:"glass", x:2.15,w:.14, h:.22, mass:.40, mu:.20}
];
let bodies, running, finished, clothX, pullV, accumulator, lastTime;
function reset() {
bodies = specs.map(s => ({
...s, y: TOP + s.h / 2, a:0, vx:0, vy:0, wv:0,
invM: 1/s.mass,
invI: 12/(s.mass*(s.w*s.w+s.h*s.h)),
contact:false, quiet:0, asleep:false
}));
running = false; finished = false; clothX = 0; pullV = 0;
accumulator = 0; lastTime = 0;
pullButton.disabled = false;
status.textContent = "Ready for dinner.";
draw();
}
function start() {
if (running || finished) return;
const n = Number(speedInput.value);
if (!Number.isFinite(n)) {
status.textContent = "Enter a finite pull speed.";
return;
}
// Negative inputs are interpreted as a speed, not a direction.
pullV = Math.abs(n) * 0.3048;
running = true;
pullButton.disabled = true;
status.textContent = pullV === 0 ? "No pull: letting the table settle…" : "The cloth is moving…";
lastTime = 0;
requestAnimationFrame(frame);
}
const dot = (a,b) => a.x*b.x + a.y*b.y;
const cross = (a,b) => a.x*b.y - a.y*b.x;
function axes(b) {
const c = Math.cos(b.a), s = Math.sin(b.a);
return [{x:c,y:s}, {x:-s,y:c}];
}
function corners(b) {
const [u,v] = axes(b), hw=b.w/2, hh=b.h/2;
return [
{x:b.x-u.x*hw-v.x*hh, y:b.y-u.y*hw-v.y*hh},
{x:b.x+u.x*hw-v.x*hh, y:b.y+u.y*hw-v.y*hh},
{x:b.x+u.x*hw+v.x*hh, y:b.y+u.y*hw+v.y*hh},
{x:b.x-u.x*hw+v.x*hh, y:b.y-u.y*hw+v.y*hh}
];
}
function pointVelocity(b, r) {
return {x:b.vx-b.wv*r.y, y:b.vy+b.wv*r.x};
}
function wake(b) { b.asleep=false; b.quiet=0; }
function impulse(b, p, jx, jy) {
b.vx += jx*b.invM;
b.vy += jy*b.invM;
b.wv += ((p.x-b.x)*jy-(p.y-b.y)*jx)*b.invI;
}
// Contact with an infinite-mass horizontal surface. Tangential velocity
// belongs to the moving cloth or to a stationary table/floor.
function surfaceContact(b, p, level, surfaceV, mu) {
const penetration = level-p.y;
if (penetration < -0.002) return;
b.contact = true;
if (b.asleep && Math.abs(surfaceV) > 0.001) wake(b);
if (b.asleep) return;
const r = {x:p.x-b.x, y:p.y-b.y};
let vel = pointVelocity(b,r);
const normalDenom = b.invM + r.x*r.x*b.invI;
const jn = Math.max(0, -vel.y/normalDenom);
if (jn > 0) impulse(b,p,0,jn);
vel = pointVelocity(b,r);
const tangentDenom = b.invM + r.y*r.y*b.invI;
let jt = (surfaceV-vel.x)/tangentDenom;
jt = Math.max(-mu*jn, Math.min(mu*jn, jt));
impulse(b,p,jt,0);
if (penetration > 0) b.y += Math.min(penetration*0.35, 0.015);
}
function inside(p,b) {
const [u,v] = axes(b), d={x:p.x-b.x,y:p.y-b.y};
return Math.abs(dot(d,u)) <= b.w/2+1e-7 &&
Math.abs(dot(d,v)) <= b.h/2+1e-7;
}
function collide(a,b) {
const ca=corners(a), cb=corners(b);
const allAxes=[...axes(a),...axes(b)];
let depth=Infinity, normal=null;
for (const n of allAxes) {
const pa=ca.map(p=>dot(p,n)), pb=cb.map(p=>dot(p,n));
const overlap=Math.min(...pa, ...[]) - Math.max(...pb, ...[]);
// Explicit interval intersection (the expression above is not used).
const d=Math.min(Math.max(...pa),Math.max(...pb))-
Math.max(Math.min(...pa),Math.min(...pb));
if (d <= 0) return;
if (d < depth) { depth=d; normal={x:n.x,y:n.y}; }
}
if (dot({x:b.x-a.x,y:b.y-a.y},normal)<0) {
normal.x=-normal.x; normal.y=-normal.y;
}
let points=ca.filter(p=>inside(p,b)).concat(cb.filter(p=>inside(p,a)));
let p;
if (points.length) {
p={x:points.reduce((s,q)=>s+q.x,0)/points.length,
y:points.reduce((s,q)=>s+q.y,0)/points.length};
} else {
const ap=ca.reduce((best,q)=>dot(q,normal)>dot(best,normal)?q:best);
const bp=cb.reduce((best,q)=>dot(q,normal)<dot(best,normal)?q:best);
p={x:(ap.x+bp.x)/2,y:(ap.y+bp.y)/2};
}
const ra={x:p.x-a.x,y:p.y-a.y}, rb={x:p.x-b.x,y:p.y-b.y};
let va=pointVelocity(a,ra), vb=pointVelocity(b,rb);
let rv={x:vb.x-va.x,y:vb.y-va.y};
const vn=dot(rv,normal);
if (vn < -0.025) {
if (a.asleep) wake(a);
if (b.asleep) wake(b);
}
const denom=a.invM+b.invM+
cross(ra,normal)**2*a.invI+cross(rb,normal)**2*b.invI;
const j=Math.max(0,-1.04*vn/denom);
if (j>0) {
impulse(a,p,-j*normal.x,-j*normal.y);
impulse(b,p, j*normal.x, j*normal.y);
va=pointVelocity(a,ra); vb=pointVelocity(b,rb);
rv={x:vb.x-va.x,y:vb.y-va.y};
const t={x:-normal.y,y:normal.x};
const td=a.invM+b.invM+cross(ra,t)**2*a.invI+cross(rb,t)**2*b.invI;
let jt=-dot(rv,t)/td;
jt=Math.max(-.22*j,Math.min(.22*j,jt));
impulse(a,p,-jt*t.x,-jt*t.y);
impulse(b,p, jt*t.x, jt*t.y);
}
// A small position correction prevents persistent interpenetration.
const correction=Math.max(0,depth-.001)*.18/(a.invM+b.invM);
a.x-=normal.x*correction*a.invM; a.y-=normal.y*correction*a.invM;
b.x+=normal.x*correction*b.invM; b.y+=normal.y*correction*b.invM;
}
function step() {
if (pullV>0 && clothX<TABLE) clothX += pullV*DT;
for (const b of bodies) {
b.contact=false;
if (!b.asleep) {
b.vy -= G*DT;
b.x += b.vx*DT; b.y += b.vy*DT; b.a += b.wv*DT;
}
}
for (let iteration=0; iteration<5; iteration++) {
for (const b of bodies) {
for (const p of corners(b)) {
if (p.y < 0.003) surfaceContact(b,p,0,0,FLOOR_MU);
// Only the upper face of the finite table supports a dish.
if (p.x>=0 && p.x<=TABLE && p.y<=TOP+.002 &&
p.y>TOP-.10 && b.y>TOP-.02) {
const onCloth = clothX<TABLE && p.x>=clothX && p.x<=clothX+TABLE;
surfaceContact(b,p,TOP,onCloth?pullV:0,onCloth?b.mu:BARE);
}
}
}
for (let i=0;i<bodies.length;i++)
for (let j=i+1;j<bodies.length;j++) collide(bodies[i],bodies[j]);
}
for (const b of bodies) {
if (!b.asleep && b.contact &&
Math.hypot(b.vx,b.vy)<.035 && Math.abs(b.wv)<.14) {
b.quiet += DT;
if (b.quiet>.8) {
b.vx=b.vy=b.wv=0;
b.asleep=true;
}
} else if (!b.asleep) b.quiet=0;
}
const clothDone = pullV===0 || clothX>=TABLE;
if (clothDone && bodies.every(b=>b.asleep)) finish();
}
function finish() {
running=false; finished=true;
const onTable=bodies.filter(b=>b.x>=0 && b.x<=TABLE && b.y>TOP-.04);
const upright=bodies.filter(b=>{
const angle=Math.atan2(Math.sin(b.a),Math.cos(b.a));
return Math.abs(angle)<Math.PI/4;
});
status.textContent = `${onTable.length} of ${bodies.length} on the table · ${upright.length} upright (including the floor)`;
}
// The picture is a side view, scaled in metres.
const SCALE=245, OX=130, OY=438;
const X=x=>OX+x*SCALE, Y=y=>OY-y*SCALE;
function line(x1,y1,x2,y2,color,width) {
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2);
ctx.strokeStyle=color; ctx.lineWidth=width; ctx.stroke();
}
function rounded(x,y,w,h,r,fill) {
ctx.beginPath(); ctx.roundRect(x,y,w,h,r); ctx.fillStyle=fill; ctx.fill();
}
function drawBody(b) {
ctx.save();
ctx.translate(X(b.x),Y(b.y));
ctx.rotate(-b.a);
const w=b.w*SCALE, h=b.h*SCALE;
if (b.kind==="plate") {
rounded(-w/2,-h/2,w,h,4,"#f7eddb");
ctx.strokeStyle="#b9aa93"; ctx.lineWidth=1.5;
ctx.strokeRect(-w/2+2,-h/2+2,w-4,Math.max(1,h-4));
line(-w*.34,0,w*.34,0,"#d6c9b2",1);
} else if (b.kind==="glass") {
rounded(-w/2,-h/2,w,h,4,"#b7d7d6");
ctx.strokeStyle="#6b9da1"; ctx.lineWidth=2;
ctx.strokeRect(-w/2+1,-h/2+1,w-2,h-2);
ctx.fillStyle="#8bb9b6"; ctx.fillRect(-w/2+3,h*.05,w-6,h*.36);
line(-w*.28,-h*.36,-w*.28,h*.3,"#effaf4",2);
} else if (b.kind==="candle") {
// The simulated collision shape is the enclosing rectangle.
rounded(-w/2,h*.31,w,h*.19,3,"#a26b32");
rounded(-w*.19,-h*.19,w*.38,h*.52,3,"#c5904d");
rounded(-w*.13,-h*.46,w*.26,h*.31,2,"#f7e8c9");
ctx.fillStyle="#e9a44c";
ctx.beginPath(); ctx.ellipse(0,-h*.49,3,6,0,0,Math.PI*2); ctx.fill();
} else {
rounded(-w/2,-h/2,w,h,7,"#b76551");
rounded(-w/2,-h/2,w,h*.20,3,"#d98e73");
line(-w*.32,h*.2,w*.32,h*.2,"#dd9980",2);
}
ctx.restore();
}
function draw() {
ctx.clearRect(0,0,1000,510);
ctx.fillStyle="#e9eee4"; ctx.fillRect(0,0,1000,510);
// Wall and floor.
ctx.fillStyle="#dbe5da"; ctx.fillRect(0,0,1000,Y(0));
ctx.fillStyle="#d5c5aa"; ctx.fillRect(0,Y(0),1000,510-Y(0));
line(0,Y(0),1000,Y(0),"#bba98e",2);
for (let i=0;i<7;i++) line(i*190,Y(0)+35,(i+1)*190,Y(0)+35,"#cbb99d",1);
// A little wall decoration.
rounded(50,42,118,80,4,"#cbd9ca");
rounded(57,49,104,66,2,"#edf1e6");
ctx.fillStyle="#9bb49f"; ctx.beginPath(); ctx.arc(109,82,20,0,Math.PI*2); ctx.fill();
// Table legs and apron.
ctx.fillStyle="#7d5b41";
ctx.fillRect(X(.14),Y(TOP)-2,25,Y(0)-Y(TOP)+2);
ctx.fillRect(X(2.15),Y(TOP)-2,25,Y(0)-Y(TOP)+2);
ctx.fillStyle="#9d7653";
ctx.fillRect(X(0),Y(TOP),TABLE*SCALE,21);
ctx.fillStyle="#76523b";
ctx.fillRect(X(0),Y(TOP)+17,TABLE*SCALE,5);
// Visible portion of cloth is clipped by the table's top.
if (clothX<TABLE) {
const left=Math.max(0,clothX), right=Math.min(TABLE,clothX+TABLE);
ctx.fillStyle="#e8c4a6";
ctx.fillRect(X(left),Y(TOP)-5,(right-left)*SCALE,7);
ctx.fillStyle="#c97f6b";
for (let x=left+.09;x<right;x+=.20)
ctx.fillRect(X(x),Y(TOP)-5,3,7);
if (clothX>0) {
ctx.fillStyle="#e8c4a6";
ctx.fillRect(X(TABLE)-2,Y(TOP)-5,8,Math.min(55,clothX*SCALE*.22));
} else {
ctx.fillStyle="#e8c4a6";
ctx.fillRect(X(0),Y(TOP)-5,8,38);
}
}
for (const b of bodies) drawBody(b);
ctx.fillStyle="#67766b";
ctx.font="12px system-ui, sans-serif";
ctx.fillText("TABLE",X(.05),Y(TOP)+37);
ctx.fillText("FLOOR",20,Y(0)+25);
}
function frame(t) {
if (!running) return;
if (!lastTime) lastTime=t;
accumulator += Math.min((t-lastTime)/1000,.05);
lastTime=t;
while (accumulator>=DT && running) {
step(); accumulator-=DT;
}
draw();
if (running) requestAnimationFrame(frame);
}
pullButton.addEventListener("click",start);
resetButton.addEventListener("click",reset);
reset();
const hash = new URLSearchParams(location.hash.slice(1));
if (hash.has("speed")) {
const value=Number(hash.get("speed"));
if (Number.isFinite(value)) {
speedInput.value=String(value);
setTimeout(() => { if (!running && !finished) start(); },1000);
}
}
})();
</script>
</body>
</html>
The cloth applies Coulomb friction at contact points; gravity, inertia, rotational impulses and collisions determine the motion. There are no speed-keyed outcomes or scripted wobbles. The counts appear after the cloth has cleared and all objects have settled; upright includes objects on the floor.
This is a 2D approximation: each object collides as a rectangle, the cloth moves at a prescribed constant speed, and the table has no solid side faces. Contact resolution and sleeping are approximate, so piles can jitter or settle imperfectly, and very high speeds can cause tunnelling. The prediction ignores collisions and tipping, so its boundaries may not match a run. Negative speed inputs are treated as their magnitude; zero leaves the setting in place.