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.
A center-of-mass estimate predicts about 9.2 ft/s for every object to stay on the table, and below about 2.4 ft/s for none to stay. The cloth is 4 ft long; the objects have masses from 0.7 to 2.0 lb, cloth friction coefficient 0.20, and bare-table coefficient 0.40. Mass cancels from the sliding acceleration: (a=0.20(32.2)=6.44) ft/s². For the first object, 0.45 ft from the trailing edge, the estimate is (\sqrt{2(6.44)(0.45)}=2.4) ft/s. For the last, at 3.2 ft with 0.8 ft remaining, allowing for subsequent bare-table sliding gives (d=0.8/(1+0.20/0.40)=0.533) ft and speed ((3.2+d)/\sqrt{2d/6.44}=9.2) ft/s. These are estimates, not guarantees: they ignore object width, rotation, and collisions.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The dinner-table yank</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #171b25;
color: #f5efe3;
}
* { box-sizing: border-box; }
body { margin: 0; }
main { max-width: 1060px; margin: auto; padding: 24px; }
h1 { margin: 0 0 6px; font-size: clamp(1.8rem, 4vw, 2.7rem); }
p { color: #c9c6c2; line-height: 1.5; }
.controls {
display: flex; flex-wrap: wrap; align-items: end;
gap: 12px; margin: 18px 0;
}
label { display: grid; gap: 5px; font-weight: 650; }
input, button {
font: inherit; border-radius: 8px; padding: 10px 14px;
border: 1px solid #6c7180;
}
input { width: 150px; background: #252b38; color: white; }
button { cursor: pointer; background: #f0b35a; color: #21190e; font-weight: 750; }
button.secondary { background: #343b4b; color: #fff; }
button:disabled { opacity: .5; cursor: not-allowed; }
canvas {
display: block; width: 100%; height: auto;
border: 1px solid #525462; border-radius: 12px;
background: #eee1ce;
}
#status {
min-height: 3em; margin: 12px 0 0;
color: #fff3d9; font-weight: 650;
}
.note { font-size: .9rem; }
</style>
</head>
<body>
<main>
<h1>The dinner-table yank</h1>
<p>Pull the cloth to the right. The table is 4 feet wide; the setting is simulated in a side view.</p>
<div class="controls">
<label>Pull speed (ft/s)
<input id="speed" type="number" step="any" value="12">
</label>
<button id="yank">Yank cloth</button>
<button id="reset" class="secondary">Reset</button>
</div>
<canvas id="scene" width="1000" height="650"
aria-label="Side view of a table, cloth, dinner setting and floor"></canvas>
<p id="status" role="status">Ready. Choose a speed and yank.</p>
<p class="note">Try a very slow pull, then a fast one. Negative speeds are treated as their magnitude; zero leaves the cloth still.</p>
</main>
<script>
(() => {
"use strict";
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d");
const speedInput = document.getElementById("speed");
const yankButton = document.getElementById("yank");
const resetButton = document.getElementById("reset");
const status = document.getElementById("status");
// Feet, seconds, pounds as mass units. Gravity is in ft/s².
const G = 32.2, DT = 1 / 240;
const TABLE_Y = 2, FLOOR_Y = 4.2, TABLE_LENGTH = 4;
const MU_CLOTH = 0.20, MU_TABLE = 0.40, MU_FLOOR = 0.48;
const SCALE = 150, OX = 180, OY = -40;
const V = (x, y) => ({x, y});
const add = (a, b) => V(a.x + b.x, a.y + b.y);
const sub = (a, b) => V(a.x - b.x, a.y - b.y);
const mul = (a, s) => V(a.x * s, a.y * s);
const dot = (a, b) => a.x * b.x + a.y * b.y;
const cross = (a, b) => a.x * b.y - a.y * b.x;
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const rect = (w, h) => [
V(-w/2, -h/2), V(w/2, -h/2),
V(w/2, h/2), V(-w/2, h/2)
];
function body(x, y, points, mass, kind, color, w, h) {
// Box approximation to moment of inertia; collision geometry is polygonal.
return {
p: V(x,y), v: V(0,0), angle: 0, omega: 0,
points, mass, inv: 1/mass,
invI: 12/(mass*(w*w+h*h)),
kind, color, w, h, fixed: false
};
}
function fixedBox(x, y, w, h, kind) {
return {p: V(x,y), v: V(0,0), angle: 0, omega: 0,
points: rect(w,h), inv: 0, invI: 0, fixed: true, kind};
}
const table = fixedBox(2, 2.09, 4, .18, "table");
const floor = fixedBox(0, 4.35, 2000, .3, "floor");
let objects, running = false, finished = false;
let pullSpeed = 0, trailing = 0, quiet = 0, accumulator = 0;
let lastFrame = 0, autoTimer = null;
function makeObjects() {
const plate = (x, color) =>
body(x, TABLE_Y-.035, rect(.48,.07), 1.2, "plate", color, .48,.07);
const glass = (x, color) =>
body(x, TABLE_Y-.19,
[V(-.13,-.19), V(.13,-.19), V(.095,.19), V(-.095,.19)],
.7, "glass", color, .26,.38);
const bowl = (x) =>
body(x, TABLE_Y-.13,
[V(-.25,-.13), V(.25,-.13), V(.16,.13), V(-.16,.13)],
2.0, "bowl", "#bc6950", .5,.26);
const candle = (x) =>
body(x, TABLE_Y-.36, rect(.20,.72), 1.1, "candle", "#e7c681", .20,.72);
return [
plate(.45, "#f8eee0"),
glass(1.0, "#b9dde0"),
bowl(1.55),
candle(2.10),
plate(2.65, "#f8eee0"),
glass(3.20, "#b9dde0")
];
}
function reset() {
if (autoTimer !== null) { clearTimeout(autoTimer); autoTimer = null; }
objects = makeObjects();
running = false; finished = false;
pullSpeed = 0; trailing = 0; quiet = 0; accumulator = 0;
yankButton.disabled = false;
status.textContent = "Ready. Choose a speed and yank.";
draw();
}
function yank() {
if (running || finished) return;
let n = Number(speedInput.value);
if (!Number.isFinite(n)) {
status.textContent = "Enter a finite pull speed.";
return;
}
pullSpeed = Math.abs(n);
running = true;
quiet = 0;
accumulator = 0;
yankButton.disabled = true;
status.textContent = pullSpeed === 0
? "The cloth is stationary. Waiting for the setting to settle…"
: "Pulling… waiting for everything to settle.";
}
function worldPoints(b) {
const c = Math.cos(b.angle), s = Math.sin(b.angle);
return b.points.map(p => V(
b.p.x + c*p.x - s*p.y,
b.p.y + s*p.x + c*p.y
));
}
function project(points, axis) {
let lo = Infinity, hi = -Infinity;
for (const p of points) {
const d = dot(p, axis);
lo = Math.min(lo, d); hi = Math.max(hi, d);
}
return [lo,hi];
}
function inside(p, poly) {
let sign = 0;
for (let i=0; i<poly.length; i++) {
const z = cross(sub(poly[(i+1)%poly.length], poly[i]), sub(p,poly[i]));
if (Math.abs(z) < 1e-8) continue;
const s = Math.sign(z);
if (sign && s !== sign) return false;
sign = s;
}
return true;
}
function intersection(a,b,c,d) {
const r = sub(b,a), s = sub(d,c), den = cross(r,s);
if (Math.abs(den) < 1e-10) return null;
const t = cross(sub(c,a),s)/den, u = cross(sub(c,a),r)/den;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) return add(a,mul(r,t));
return null;
}
// Separating-axis collision test, followed by up to two contact points.
function collide(a,b) {
const pa = worldPoints(a), pb = worldPoints(b);
let depth = Infinity, normal = null;
for (const poly of [pa,pb]) {
for (let i=0; i<poly.length; i++) {
const e = sub(poly[(i+1)%poly.length],poly[i]);
const len = Math.hypot(e.x,e.y);
const axis = V(-e.y/len,e.x/len);
const [amin,amax] = project(pa,axis);
const [bmin,bmax] = project(pb,axis);
const overlap = Math.min(amax,bmax)-Math.max(amin,bmin);
if (overlap <= 0) return null;
if (overlap < depth) { depth = overlap; normal = axis; }
}
}
if (dot(sub(b.p,a.p),normal) < 0) normal = mul(normal,-1);
const candidates = [];
for (const p of pa) if (inside(p,pb)) candidates.push(p);
for (const p of pb) if (inside(p,pa)) candidates.push(p);
for (let i=0; i<pa.length; i++) for (let j=0; j<pb.length; j++) {
const p = intersection(pa[i],pa[(i+1)%pa.length],pb[j],pb[(j+1)%pb.length]);
if (p) candidates.push(p);
}
if (!candidates.length) return null;
const tangent = V(-normal.y,normal.x);
candidates.sort((p,q) => dot(p,tangent)-dot(q,tangent));
const points = [candidates[0]];
if (dot(sub(candidates[candidates.length-1],candidates[0]),tangent) > .015)
points.push(candidates[candidates.length-1]);
return {a,b,normal,depth, contacts: points.map(p => ({p,jn:0,jt:0}))};
}
function velocityAt(b, r) {
return V(b.v.x-b.omega*r.y, b.v.y+b.omega*r.x);
}
function impulse(b, r, j) {
if (b.fixed) return;
b.v.x += j.x*b.inv; b.v.y += j.y*b.inv;
b.omega += cross(r,j)*b.invI;
}
function surfaceProperties(c, p) {
const other = c.a.fixed ? c.a : c.b.fixed ? c.b : null;
if (other === table) {
const onTop = Math.abs(p.y-TABLE_Y) < .08 && Math.abs(c.normal.y) > .7;
const onCloth = onTop && p.x >= trailing && p.x <= trailing+TABLE_LENGTH;
return {mu: onCloth ? MU_CLOTH : MU_TABLE,
belt: onCloth ? pullSpeed : 0};
}
if (other === floor) return {mu: MU_FLOOR, belt: 0};
return {mu: .30, belt: 0};
}
function solve(c) {
const {a,b,normal:n} = c;
const t = V(-n.y,n.x);
for (const contact of c.contacts) {
const p = contact.p, ra = sub(p,a.p), rb = sub(p,b.p);
const props = surfaceProperties(c,p);
const beltA = a === table ? props.belt : 0;
const beltB = b === table ? props.belt : 0;
let va = velocityAt(a,ra), vb = velocityAt(b,rb);
va.x += beltA; vb.x += beltB;
let rv = sub(vb,va);
const rnA = cross(ra,n), rnB = cross(rb,n);
const kn = a.inv+b.inv+rnA*rnA*a.invI+rnB*rnB*b.invI;
if (!kn) continue;
// No bounce for gentle contacts, a little restitution for impacts.
const vn = dot(rv,n);
const bounce = vn < -1 ? -.06*vn : 0;
const oldN = contact.jn;
contact.jn = Math.max(0, oldN + (-vn+bounce)/kn);
const jn = contact.jn-oldN;
impulse(a,ra,mul(n,-jn)); impulse(b,rb,mul(n,jn));
va = velocityAt(a,ra); vb = velocityAt(b,rb);
va.x += beltA; vb.x += beltB;
rv = sub(vb,va);
const rtA = cross(ra,t), rtB = cross(rb,t);
const kt = a.inv+b.inv+rtA*rtA*a.invI+rtB*rtB*b.invI;
if (!kt) continue;
const oldT = contact.jt, limit = props.mu*contact.jn;
contact.jt = clamp(oldT-dot(rv,t)/kt,-limit,limit);
const jt = contact.jt-oldT;
impulse(a,ra,mul(t,-jt)); impulse(b,rb,mul(t,jt));
}
}
function step() {
if (pullSpeed > 0) trailing += pullSpeed*DT;
for (const b of objects) {
b.v.y += G*DT;
b.p.x += b.v.x*DT; b.p.y += b.v.y*DT;
b.angle += b.omega*DT;
}
const collisions = [];
for (let i=0; i<objects.length; i++) {
for (const fixed of [table,floor]) {
const c = collide(objects[i],fixed);
if (c) collisions.push(c);
}
for (let j=i+1; j<objects.length; j++) {
const c = collide(objects[i],objects[j]);
if (c) collisions.push(c);
}
}
for (let i=0; i<12; i++) for (const c of collisions) solve(c);
// Small penetration correction, separate from the velocity impulses.
for (const c of collisions) {
const sum = c.a.inv+c.b.inv;
if (!sum) continue;
const correction = Math.max(0,c.depth-.001)*.65/sum;
if (!c.a.fixed) c.a.p = sub(c.a.p,mul(c.normal,correction*c.a.inv));
if (!c.b.fixed) c.b.p = add(c.b.p,mul(c.normal,correction*c.b.inv));
}
const clothDone = pullSpeed === 0 || trailing >= TABLE_LENGTH;
const still = objects.every(b => Math.hypot(b.v.x,b.v.y) < .10 && Math.abs(b.omega) < .25);
quiet = clothDone && still ? quiet+DT : 0;
if (quiet > 1.2) finish();
}
function finish() {
running = false; finished = true;
for (const b of objects) { b.v = V(0,0); b.omega = 0; }
let onTable = 0, upright = 0;
for (const b of objects) {
const pts = worldPoints(b);
const bottom = Math.max(...pts.map(p => p.y));
const on = b.p.x >= 0 && b.p.x <= 4 &&
b.p.y < TABLE_Y && bottom >= TABLE_Y-.08 && bottom <= TABLE_Y+.12;
if (on) {
onTable++;
const a = Math.atan2(Math.sin(b.angle),Math.cos(b.angle));
if (Math.abs(a) < Math.PI/4) upright++;
}
}
status.textContent = `Settled: ${onTable} of ${objects.length} on the table; ${upright} of those upright. Reset to try again.`;
}
function pathPolygon(points) {
ctx.beginPath();
points.forEach((p,i) => i ? ctx.lineTo(p.x,p.y) : ctx.moveTo(p.x,p.y));
ctx.closePath();
}
function drawObject(b) {
ctx.save();
ctx.translate(b.p.x,b.p.y);
ctx.rotate(b.angle);
pathPolygon(b.points);
ctx.fillStyle = b.color;
ctx.strokeStyle = "#524a45";
ctx.lineWidth = .018;
ctx.fill(); ctx.stroke();
if (b.kind === "plate") {
ctx.strokeStyle = "#c8b7a2"; ctx.lineWidth = .012;
ctx.beginPath(); ctx.moveTo(-.18,-.014); ctx.lineTo(.18,-.014); ctx.stroke();
} else if (b.kind === "glass") {
ctx.strokeStyle = "#eefafa"; ctx.lineWidth = .018;
ctx.beginPath(); ctx.moveTo(-.085,-.14); ctx.lineTo(-.065,.10); ctx.stroke();
} else if (b.kind === "bowl") {
ctx.strokeStyle = "#f1ba96"; ctx.lineWidth = .018;
ctx.beginPath(); ctx.moveTo(-.21,-.09); ctx.lineTo(.21,-.09); ctx.stroke();
} else if (b.kind === "candle") {
ctx.fillStyle = "#faf1dc"; ctx.fillRect(-.055,-.32,.11,.43);
ctx.fillStyle = "#775032"; ctx.fillRect(-.12,.25,.24,.10);
ctx.strokeStyle = "#6d5140"; ctx.lineWidth = .012;
ctx.beginPath(); ctx.moveTo(0,-.32); ctx.lineTo(0,-.39); ctx.stroke();
ctx.fillStyle = "#f6a43d";
ctx.beginPath(); ctx.ellipse(0,-.42,.035,.055,0,0,Math.PI*2); ctx.fill();
}
ctx.restore();
}
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "#e9dcc9"; ctx.fillRect(0,0,1000,650);
ctx.fillStyle = "#d0b89e"; ctx.fillRect(0,590,1000,60);
ctx.strokeStyle = "#b59d85"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0,590); ctx.lineTo(1000,590); ctx.stroke();
ctx.save();
ctx.translate(OX,OY); ctx.scale(SCALE,SCALE);
// Decorative legs have no collision geometry.
ctx.fillStyle = "#6c493a";
ctx.fillRect(.18,2.15,.18,2.05);
ctx.fillRect(3.64,2.15,.18,2.05);
ctx.fillStyle = "#895e49";
ctx.fillRect(0,2,4,.18);
ctx.fillStyle = "#a87457";
ctx.fillRect(0,2,4,.045);
if (trailing < 4) {
const left = Math.max(0,trailing);
ctx.fillStyle = "#a53e4d";
ctx.fillRect(left,1.975,4-left,.055);
ctx.fillRect(3.96,2.015,.07,.5);
ctx.strokeStyle = "#e5b6a8"; ctx.lineWidth = .012;
for (let x=left+.12; x<4; x+=.32) {
ctx.beginPath(); ctx.moveTo(x,1.982); ctx.lineTo(x+.07,2.02); ctx.stroke();
}
}
for (const b of objects) drawObject(b);
ctx.restore();
ctx.fillStyle = "#705d50";
ctx.font = "15px system-ui";
ctx.fillText("FLOOR", 25, 617);
}
function frame(now) {
if (!lastFrame) lastFrame = now;
const elapsed = Math.min((now-lastFrame)/1000,.08);
lastFrame = now;
if (running) {
accumulator += elapsed;
while (accumulator >= DT && running) {
step(); accumulator -= DT;
}
}
draw();
requestAnimationFrame(frame);
}
yankButton.addEventListener("click", yank);
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);
autoTimer = setTimeout(() => { autoTimer = null; yank(); },1000);
}
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The cloth supplies frictional impulses while it passes beneath each object; inertia, gravity, contact friction, and collisions determine the motion. Objects can rotate, strike one another, leave the table, and land on the floor.
It is a 2D side view, not a full 3D setting. Objects are solid convex silhouettes, the candle and flame are decorative parts of one body, and the table legs are decorative. Friction is independent of contact area, and the motor pulling the cloth never slows under load. There are no scripted wobbles or speed-based fall thresholds. The simulation declares rest after 1.2 seconds below small velocity tolerances; the reported upright count uses a 45° angle cutoff. Numerical contact behavior and collisions can make the actual outcomes differ from the prediction.