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.
Using (g = 32.2\ \text{ft/s}^2), a table 8 ft wide, and a 20 ft cloth whose left edge starts at (-10) ft, an object at (x) needs the cloth to travel (s = x + 10) ft before it is exposed. In the fast-slip regime its displacement is (d = \mu_k g s^2 / (2v^2)). Setting (d = 8 - x) gives the stay threshold
[
v_{\text{stay}} = \sqrt{\frac{\mu_k g (x+10)^2}{2(8-x)}}.
]
For the six objects (masses 0.8, 0.25, 1.2, 0.6, 0.35, 0.4; (\mu_k = 0.30, 0.25, 0.22, 0.35, 0.25, 0.28); (x = 1,2,3,4,5,6) ft) the thresholds are 9.1, 9.8, 10.9, 16.6, 17.4, and 24.0 ft/s. So every object stays only above about 24 ft/s, and below about 9.1 ft/s none stays. Mass cancels in the sliding acceleration; it only sets the normal force and the tipping threshold (\mu_k > a/b).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tablecloth Trick</title>
<style>
body {
margin: 0;
background: #1a1a1a;
color: #eee;
font-family: system-ui, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
}
#ui {
display: flex;
gap: 16px;
align-items: center;
margin: 12px;
padding: 8px 16px;
background: #2a2a2a;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
#ui label {
display: flex;
align-items: center;
gap: 8px;
}
input[type=range] {
width: 180px;
}
button {
background: #c0392b;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #e74c3c;
}
button:disabled {
background: #555;
cursor: not-allowed;
}
#reset {
background: #2980b9;
}
#reset:hover {
background: #3498db;
}
#status {
margin: 8px;
font-size: 1.1rem;
min-height: 1.5em;
}
canvas {
border: 2px solid #444;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.6);
background: #2b2b2b;
}
</style>
</head>
<body>
<div id="ui">
<label>
Pull speed:
<input type="range" id="speed" min="0" max="30" step="0.1" value="12">
<span id="speedVal">12.0</span> ft/s
</label>
<button id="yank">Yank cloth</button>
<button id="reset">Reset</button>
</div>
<div id="status">Ready. Set speed and yank.</div>
<canvas id="canvas" width="900" height="600"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const speedInput = document.getElementById('speed');
const speedVal = document.getElementById('speedVal');
const yankBtn = document.getElementById('yank');
const resetBtn = document.getElementById('reset');
const statusEl = document.getElementById('status');
const g = 32.2;
const scale = 80;
const tableLeft = 0;
const tableRight = 8;
const tableTopPx = 300;
const floorY = -2.5;
const initialClothLeft = -10;
const initialClothRight = 10;
const initialObjects = [
{ name: 'Plate', x: 1.0, m: 0.8, a: 0.45, b: 0.06, mu_s: 0.40, mu_k: 0.30, color: '#f5f0e1', type: 'plate' },
{ name: 'Glass', x: 2.0, m: 0.25, a: 0.08, b: 0.35, mu_s: 0.32, mu_k: 0.25, color: '#a8d8ea', type: 'glass' },
{ name: 'Candlestick', x: 3.0, m: 1.2, a: 0.10, b: 0.5, mu_s: 0.30, mu_k: 0.22, color: '#d4af37', type: 'candlestick' },
{ name: 'Bowl', x: 4.0, m: 0.6, a: 0.35, b: 0.175, mu_s: 0.45, mu_k: 0.35, color: '#c97b63', type: 'bowl' },
{ name: 'Mug', x: 5.0, m: 0.35, a: 0.12, b: 0.2, mu_s: 0.32, mu_k: 0.25, color: '#e0e0e0', type: 'mug' },
{ name: 'Vase', x: 6.0, m: 0.4, a: 0.06, b: 0.3, mu_s: 0.35, mu_k: 0.28, color: '#7fb069', type: 'vase' }
];
let objects = [];
let clothLeft = initialClothLeft;
let clothRight = initialClothRight;
let clothVel = 0;
let yanked = false;
let simulationStopped = false;
let lastTime = 0;
let accumulator = 0;
const fixedDt = 1 / 120;
function reset() {
objects = initialObjects.map(o => ({
...o,
x: o.x,
y: 0,
vx: 0,
vy: 0,
theta: 0,
omega: 0,
onTable: true,
tipped: false,
stopped: false
}));
clothLeft = initialClothLeft;
clothRight = initialClothRight;
clothVel = 0;
yanked = false;
simulationStopped = false;
lastTime = 0;
accumulator = 0;
statusEl.textContent = 'Ready. Set speed and yank.';
yankBtn.disabled = false;
}
function yank() {
if (yanked) return;
yanked = true;
clothVel = parseFloat(speedInput.value);
statusEl.textContent = 'Pulling...';
yankBtn.disabled = true;
}
function physicsStep(dt) {
if (clothVel !== 0) {
clothLeft += clothVel * dt;
clothRight += clothVel * dt;
if (clothLeft > 12) clothVel = 0;
}
for (let o of objects) {
if (o.stopped) continue;
if (!o.onTable) {
o.vy -= g * dt;
o.y += o.vy * dt;
o.x += o.vx * dt;
o.theta += o.omega * dt;
o.omega *= 0.99;
if (o.y <= floorY) {
o.y = floorY;
if (o.vy < 0) {
o.vy = -o.vy * 0.2;
if (Math.abs(o.vy) < 0.5) o.vy = 0;
}
o.vx *= 0.8;
o.omega *= 0.8;
if (Math.abs(o.vx) < 0.05 && Math.abs(o.vy) < 0.05 && Math.abs(o.omega) < 0.05) {
o.vx = 0; o.vy = 0; o.omega = 0;
o.stopped = true;
}
}
continue;
}
const onCloth = (o.x >= clothLeft && o.x <= clothRight);
const v_surf = onCloth ? clothVel : 0;
const v_rel = o.vx - v_surf;
let mu = onCloth ? o.mu_k : Math.min(0.8, o.mu_k * 1.5);
if (Math.abs(v_rel) > 0.01) {
const sign = Math.sign(v_rel);
const F_fric = -sign * mu * o.m * g;
o.vx += (F_fric / o.m) * dt;
if (!o.tipped) {
const torqueFric = -F_fric * o.b;
const torqueGrav = -o.m * g * o.a * Math.sin(o.theta);
const I = o.m * (o.a * o.a + o.b * o.b) / 3;
o.omega += ((torqueFric + torqueGrav) / I) * dt;
}
} else {
o.vx = v_surf;
if (!o.tipped) {
const torqueGrav = -o.m * g * o.a * Math.sin(o.theta);
const I = o.m * (o.a * o.a + o.b * o.b) / 3;
o.omega += (torqueGrav / I) * dt;
}
}
o.omega *= 0.98;
o.x += o.vx * dt;
o.theta += o.omega * dt;
if (!o.tipped && Math.abs(o.theta) >= Math.PI / 2) {
o.tipped = true;
o.theta = Math.sign(o.theta) * Math.PI / 2;
o.omega = 0;
}
if (o.x < tableLeft || o.x > tableRight) {
o.onTable = false;
o.y = 0;
o.vy = 0;
}
if (o.onTable) {
if (o.tipped) {
if (Math.abs(o.vx) < 0.02) o.vx = 0;
} else {
if (Math.abs(o.vx) < 0.02 && Math.abs(o.omega) < 0.02 && Math.abs(o.theta) < 0.01) {
o.vx = 0; o.omega = 0; o.theta = 0;
}
}
}
}
for (let i = 0; i < objects.length; i++) {
for (let j = i + 1; j < objects.length; j++) {
const a = objects[i], b = objects[j];
if (!a.onTable || !b.onTable) continue;
const a_eff_a = a.tipped ? a.b : a.a;
const a_eff_b = b.tipped ? b.b : b.a;
const dx = b.x - a.x;
const minDist = a_eff_a + a_eff_b;
if (Math.abs(dx) < minDist) {
const overlap = minDist - Math.abs(dx);
const sign = Math.sign(dx) || 1;
const invMassA = 1 / a.m;
const invMassB = 1 / b.m;
const totalInv = invMassA + invMassB;
a.x -= sign * overlap * (invMassA / totalInv);
b.x += sign * overlap * (invMassB / totalInv);
const v_rel = b.vx - a.vx;
if (v_rel * sign < 0) {
const e = 0.2;
const impulse = -(1 + e) * v_rel / totalInv;
a.vx -= impulse * invMassA;
b.vx += impulse * invMassB;
}
}
}
}
}
function checkStopped() {
if (simulationStopped || !yanked) return;
if (clothVel !== 0 && clothLeft < 12) return;
for (let o of objects) {
if (o.onTable) {
if (Math.abs(o.vx) > 0.05) return;
if (!o.tipped && Math.abs(o.omega) > 0.05) return;
} else {
if (!o.stopped) return;
}
}
simulationStopped = true;
const onTable = objects.filter(o => o.onTable).length;
const upright = objects.filter(o => o.onTable && !o.tipped && Math.abs(o.theta) < 0.1).length;
statusEl.textContent = `Stopped. On table: ${onTable} / ${objects.length}. Upright: ${upright} / ${objects.length}.`;
}
function drawObject(o) {
const x_px = 50 + o.x * scale;
const com_y_world = o.y + (o.tipped ? o.a : o.b);
const com_y_px = tableTopPx - com_y_world * scale;
ctx.save();
ctx.translate(x_px, com_y_px);
ctx.rotate(o.theta);
switch (o.type) {
case 'plate':
ctx.fillStyle = o.color;
ctx.beginPath();
ctx.ellipse(0, 0, o.a * scale, o.b * scale, 0, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.stroke();
break;
case 'glass':
ctx.fillStyle = o.color;
ctx.beginPath();
ctx.ellipse(0, -o.b * scale * 0.3, o.a * scale, o.b * scale * 0.5, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillRect(-1, -o.b * scale * 0.1, 2, o.b * scale * 0.8);
ctx.beginPath();
ctx.ellipse(0, o.b * scale * 0.8, o.a * scale * 1.5, o.b * scale * 0.1, 0, 0, Math.PI * 2);
ctx.fill();
break;
case 'candlestick':
ctx.fillStyle = '#d4af37';
ctx.fillRect(-o.a * scale, o.b * scale * 0.5, o.a * scale * 2, o.b * scale * 0.5);
ctx.fillRect(-o.a * scale * 0.3, -o.b * scale * 0.5, o.a * scale * 0.6, o.b * scale);
ctx.fillStyle = '#fff8e7';
ctx.fillRect(-o.a * scale * 0.2, -o.b * scale * 0.9, o.a * scale * 0.4, o.b * scale * 0.5);
ctx.fillStyle = '#ffaa00';
ctx.beginPath();
ctx.ellipse(0, -o.b * scale * 0.95, o.a * scale * 0.15, o.b * scale * 0.15, 0, 0, Math.PI * 2);
ctx.fill();
break;
case 'bowl':
ctx.fillStyle = o.color;
ctx.beginPath();
ctx.ellipse(0, 0, o.a * scale, o.b * scale, 0, 0, Math.PI);
ctx.fill();
ctx.beginPath();
ctx.ellipse(0, 0, o.a * scale, o.b * scale * 0.3, 0, 0, Math.PI * 2);
ctx.fill();
break;
case 'mug':
ctx.fillStyle = o.color;
ctx.fillRect(-o.a * scale, -o.b * scale, o.a * scale * 2, o.b * scale * 2);
ctx.strokeStyle = o.color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(o.a * scale, 0, o.b * scale * 0.5, -Math.PI / 2, Math.PI / 2);
ctx.stroke();
break;
case 'vase':
ctx.fillStyle = o.color;
ctx.beginPath();
ctx.moveTo(-o.a * scale, o.b * scale);
ctx.quadraticCurveTo(-o.a * scale * 1.5, 0, -o.a * scale * 0.5, -o.b * scale);
ctx.lineTo(o.a * scale * 0.5, -o.b * scale);
ctx.quadraticCurveTo(o.a * scale * 1.5, 0, o.a * scale, o.b * scale);
ctx.closePath();
ctx.fill();
break;
}
ctx.restore();
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#2b2b2b';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(0, 0, canvas.width, tableTopPx);
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, tableTopPx + 200, canvas.width, 100);
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, tableTopPx + 200);
ctx.lineTo(canvas.width, tableTopPx + 200);
ctx.stroke();
const tableLeftPx = 50 + tableLeft * scale;
const tableRightPx = 50 + tableRight * scale;
ctx.fillStyle = '#5c3a21';
ctx.fillRect(tableLeftPx + 20, tableTopPx, 20, 200);
ctx.fillRect(tableRightPx - 40, tableTopPx, 20, 200);
ctx.fillStyle = '#8b5a2b';
ctx.fillRect(tableLeftPx - 10, tableTopPx - 10, tableRightPx - tableLeftPx + 20, 10);
ctx.fillStyle = '#a06b3a';
ctx.fillRect(tableLeftPx - 10, tableTopPx - 12, tableRightPx - tableLeftPx + 20, 2);
const clothLeftPx = 50 + clothLeft * scale;
const clothRightPx = 50 + clothRight * scale;
ctx.save();
ctx.beginPath();
ctx.rect(clothLeftPx, tableTopPx - 4, clothRightPx - clothLeftPx, 8);
ctx.clip();
ctx.fillStyle = '#f8f8f8';
ctx.fillRect(clothLeftPx, tableTopPx - 4, clothRightPx - clothLeftPx, 8);
ctx.fillStyle = '#c0392b';
const stripeWidth = 0.5 * scale;
for (let sx = clothLeftPx; sx < clothRightPx; sx += stripeWidth * 2) {
ctx.fillRect(sx, tableTopPx - 4, stripeWidth, 8);
}
ctx.restore();
for (let o of objects) drawObject(o);
}
function loop(timestamp) {
if (!lastTime) lastTime = timestamp;
let dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
if (dt > 0.1) dt = 0.1;
accumulator += dt;
while (accumulator >= fixedDt) {
if (!simulationStopped) physicsStep(fixedDt);
accumulator -= fixedDt;
}
render();
checkStopped();
requestAnimationFrame(loop);
}
speedInput.addEventListener('input', () => {
speedVal.textContent = parseFloat(speedInput.value).toFixed(1);
});
yankBtn.addEventListener('click', yank);
resetBtn.addEventListener('click', reset);
window.addEventListener('load', () => {
reset();
const match = window.location.hash.match(/speed=([\d.]+)/);
if (match) {
const val = parseFloat(match[1]);
speedInput.value = val;
speedVal.textContent = val.toFixed(1);
setTimeout(yank, 1000);
}
requestAnimationFrame(loop);
});
</script>
</body>
</html>
It behaves like the trick in the important ways: the cloth is a moving surface, objects feel Coulomb friction, they slide when the cloth slips under them, they stick and get dragged when the pull is slow, and they tip when the friction torque at the base exceeds the gravity restoring torque. Collisions are 1D along the table, with mass-weighted separation and a small restitution. Objects fall off the table when their centre crosses the edge, then fall under gravity, bounce on the floor, and settle.
Where it is not a real tablecloth: the simulation is a 2D side view, so nothing rolls sideways or spins in 3D. Tipping is a simplified pendulum about the base; once (|\theta| \ge \pi/2) the object is marked tipped and its effective half-width swaps from (a) to (b). Collisions ignore rotation and only happen while both objects are on the table. The cloth is drawn as a rigid striped rectangle and does not wrinkle or bunch. The table edge is a hard cutoff at the object’s centre, so an object can look half-off before it drops. Stopping is thresholded: velocities below 0.02 ft/s are zeroed, and the run is declared stopped when all on-table speeds are below 0.05 ft/s and all fallen objects have settled. There are no scripted wobbles; every motion comes from the friction, gravity, and collision forces. The main failure mode is that the prediction ignores collisions and tipping, so at intermediate speeds the actual count can differ from the simple sliding arithmetic—e.g. at 12 ft/s the plate, glass, and candlestick may stay, but the glass and candlestick tip, and a sliding plate can bump into something before the cloth is fully clear.