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.174\ \text{ft/s}^2), a 10 ft cloth with its left edge initially at (-5\ \text{ft}), a table edge at (x=5\ \text{ft}), and the object parameters (masses 0.3–0.9, contact widths 0.16–0.9 ft, (\mu_{\text{cloth}}=0.12)–(0.15), (\mu_{\text{table}}=0.35)–(0.45)), the sliding threshold for an object starting at (x_0) is (v = a(x_0+5)/v_o + v_o/2), where (v_o^2 = 2(5-x_0)/(1/a + 1/a_t)), (a=\mu_{\text{cloth}}g), and (a_t=\mu_{\text{table}}g). The most marginal object is the bowl at (x_0=4.5\ \text{ft}), giving (v\approx21.9\ \text{ft/s}); the easiest is the left plate at (x_0=0.8\ \text{ft}), giving (v\approx7.0\ \text{ft/s}). So every object stays on the table at ≥22 ft/s, and below 7 ft/s none stays.
<!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; overflow: hidden; background: #1a1a1a; font-family: system-ui, sans-serif; }
canvas { display: block; }
#ui {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0,0,0,0.75);
color: #fff;
padding: 15px 20px;
border-radius: 12px;
backdrop-filter: blur(4px);
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
user-select: none;
max-width: 320px;
}
#ui .row { margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#ui label { font-size: 14px; }
#ui input[type=range] { width: 180px; }
#ui button {
background: #e05050;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
#ui button:hover { background: #ff6060; }
#ui button#reset { background: #555; }
#ui button#reset:hover { background: #777; }
#status { font-size: 14px; margin-top: 10px; color: #ffd700; font-weight: bold; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="ui">
<div class="row">
<label>Pull speed: <span id="speedDisplay">12.0</span> ft/s</label>
<input type="range" id="speed" min="0" max="30" step="0.1" value="12">
</div>
<div class="row">
<button id="yank">Yank the cloth!</button>
<button id="reset">Reset</button>
</div>
<div id="status">Pull the cloth to begin.</div>
</div>
<script>
(function() {
// ---------- Constants ----------
const G = 32.174;
const TABLE_Y = 2.5;
const TABLE_X0 = 0;
const TABLE_X1 = 5.0;
const FLOOR_Y = 0;
const CLOTH_LEFT0 = -5.0;
const CLOTH_RIGHT0 = 5.0;
// ---------- State ----------
let bodies = [];
let clothLeft = CLOTH_LEFT0;
let clothRight = CLOTH_RIGHT0;
let clothSpeed = 0;
let pullStarted = false;
let simulationStopped = false;
let stopTimer = 0;
let hashPulled = false;
let lastTime = performance.now();
let accumulator = 0;
const fixedDt = 1 / 120;
// ---------- DOM ----------
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const speedInput = document.getElementById('speed');
const speedDisplay = document.getElementById('speedDisplay');
const statusEl = document.getElementById('status');
let scale, originX, originY;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const worldW = 14; // -6 to 8
const worldH = 5; // -0.5 to 4.5
scale = Math.min(canvas.width / (worldW + 2), canvas.height / (worldH + 2));
originX = canvas.width / 2 - 1 * scale;
originY = canvas.height / 2 + 2 * scale;
}
window.addEventListener('resize', resize);
resize();
// ---------- Math helpers ----------
function cross(a, b) { return a.x * b.y - a.y * b.x; }
function applyImpulse(body, impulse, r) {
body.vx += impulse.x * body.invMass;
body.vy += impulse.y * body.invMass;
body.omega += body.invInertia * cross(r, impulse);
}
function getAxes(body) {
const cos = Math.cos(body.angle);
const sin = Math.sin(body.angle);
return [
{ x: cos, y: sin },
{ x: -sin, y: cos }
];
}
function getCorners(body) {
const cos = Math.cos(body.angle);
const sin = Math.sin(body.angle);
const hw = body.w / 2;
const hh = body.h / 2;
const local = [
{ x: -hw, y: -hh },
{ x: hw, y: -hh },
{ x: hw, y: hh },
{ x: -hw, y: hh }
];
return local.map(p => ({
x: body.x + p.x * cos - p.y * sin,
y: body.y + p.x * sin + p.y * cos
}));
}
function projectBox(body, axis) {
const corners = getCorners(body);
let min = Infinity, max = -Infinity;
for (let c of corners) {
const d = c.x * axis.x + c.y * axis.y;
if (d < min) min = d;
if (d > max) max = d;
}
return { min, max };
}
function getSupportPoint(body, dir) {
const corners = getCorners(body);
let best = corners[0];
let bestDot = best.x * dir.x + best.y * dir.y;
for (let i = 1; i < corners.length; i++) {
const d = corners[i].x * dir.x + corners[i].y * dir.y;
if (d > bestDot) {
bestDot = d;
best = corners[i];
}
}
return best;
}
// ---------- Physics ----------
function resolveStaticContact(body, contactPoint, normal, penetration, surfaceVx, mu) {
const r = { x: contactPoint.x - body.x, y: contactPoint.y - body.y };
const vPoint = {
x: body.vx - body.omega * r.y,
y: body.vy + body.omega * r.x
};
const vRel = { x: vPoint.x - surfaceVx, y: vPoint.y };
const vn = vRel.x * normal.x + vRel.y * normal.y;
if (vn > 0) return;
const rn = cross(r, normal);
const invMassEff = body.invMass + body.invInertia * rn * rn;
let jn = -vn / invMassEff;
if (jn < 0) jn = 0;
const impulseN = { x: normal.x * jn, y: normal.y * jn };
applyImpulse(body, impulseN, r);
const t = { x: 1, y: 0 };
const vt = vRel.x * t.x + vRel.y * t.y;
const rt = cross(r, t);
const invMassEffT = body.invMass + body.invInertia * rt * rt;
let jt = -vt / invMassEffT;
const maxFriction = mu * jn;
if (jt > maxFriction) jt = maxFriction;
if (jt < -maxFriction) jt = -maxFriction;
const impulseT = { x: t.x * jt, y: t.y * jt };
applyImpulse(body, impulseT, r);
if (penetration > 0.001) {
body.y += normal.y * penetration * 0.2;
}
}
function resolveBodyCollision(A, B) {
const axesA = getAxes(A);
const axesB = getAxes(B);
const axes = [axesA[0], axesA[1], axesB[0], axesB[1]];
let minOverlap = Infinity;
let bestAxis = null;
for (let axis of axes) {
const projA = projectBox(A, axis);
const projB = projectBox(B, axis);
const overlap = Math.min(projA.max, projB.max) - Math.max(projA.min, projB.min);
if (overlap <= 0) return;
if (overlap < minOverlap) {
minOverlap = overlap;
bestAxis = axis;
}
}
let normal = { x: bestAxis.x, y: bestAxis.y };
const dx = B.x - A.x;
const dy = B.y - A.y;
if (dx * normal.x + dy * normal.y < 0) {
normal.x = -normal.x;
normal.y = -normal.y;
}
const penetration = minOverlap;
const supportA = getSupportPoint(A, normal);
const supportB = getSupportPoint(B, { x: -normal.x, y: -normal.y });
const contact = { x: (supportA.x + supportB.x) / 2, y: (supportA.y + supportB.y) / 2 };
const rA = { x: contact.x - A.x, y: contact.y - A.y };
const rB = { x: contact.x - B.x, y: contact.y - B.y };
const vA = { x: A.vx - A.omega * rA.y, y: A.vy + A.omega * rA.x };
const vB = { x: B.vx - B.omega * rB.y, y: B.vy + B.omega * rB.x };
const vRel = { x: vB.x - vA.x, y: vB.y - vA.y };
const vn = vRel.x * normal.x + vRel.y * normal.y;
if (vn > 0) return;
const rnA = cross(rA, normal);
const rnB = cross(rB, normal);
const invMassEff = A.invMass + B.invMass + A.invInertia * rnA * rnA + B.invInertia * rnB * rnB;
let jn = -vn / invMassEff;
if (jn < 0) jn = 0;
const impulseN = { x: normal.x * jn, y: normal.y * jn };
applyImpulse(A, { x: -impulseN.x, y: -impulseN.y }, rA);
applyImpulse(B, impulseN, rB);
const t = { x: -normal.y, y: normal.x };
const vt = vRel.x * t.x + vRel.y * t.y;
const rtA = cross(rA, t);
const rtB = cross(rB, t);
const invMassEffT = A.invMass + B.invMass + A.invInertia * rtA * rtA + B.invInertia * rtB * rtB;
let jt = -vt / invMassEffT;
const mu = Math.min(A.muObj, B.muObj);
const maxFriction = mu * jn;
if (jt > maxFriction) jt = maxFriction;
if (jt < -maxFriction) jt = -maxFriction;
const impulseT = { x: t.x * jt, y: t.y * jt };
applyImpulse(A, { x: -impulseT.x, y: -impulseT.y }, rA);
applyImpulse(B, impulseT, rB);
const correction = penetration * 0.5;
A.x -= normal.x * correction;
A.y -= normal.y * correction;
B.x += normal.x * correction;
B.y += normal.y * correction;
}
function step(dt) {
if (simulationStopped) return;
if (clothSpeed !== 0) {
clothLeft += clothSpeed * dt;
clothRight += clothSpeed * dt;
if (clothLeft > TABLE_X1 + 2) {
clothSpeed = 0;
}
}
for (let b of bodies) {
b.vy -= G * dt;
}
for (let b of bodies) {
b.x += b.vx * dt;
b.y += b.vy * dt;
b.angle += b.omega * dt;
}
const iterations = 8;
for (let iter = 0; iter < iterations; iter++) {
for (let b of bodies) {
const corners = getCorners(b);
for (let c of corners) {
if (c.y < TABLE_Y && c.x >= TABLE_X0 && c.x <= TABLE_X1) {
const penetration = TABLE_Y - c.y;
const onCloth = (c.x >= clothLeft && c.x <= clothRight);
const surfaceVx = onCloth ? clothSpeed : 0;
const mu = onCloth ? b.muCloth : b.muTable;
resolveStaticContact(b, c, { x: 0, y: 1 }, penetration, surfaceVx, mu);
}
if (c.y < FLOOR_Y) {
const penetration = FLOOR_Y - c.y;
resolveStaticContact(b, c, { x: 0, y: 1 }, penetration, 0, b.muTable);
}
}
}
for (let i = 0; i < bodies.length; i++) {
for (let j = i + 1; j < bodies.length; j++) {
resolveBodyCollision(bodies[i], bodies[j]);
}
}
}
for (let b of bodies) {
b.vx *= 0.999;
b.vy *= 0.999;
b.omega *= 0.999;
if (Math.abs(b.vx) < 0.01) b.vx = 0;
if (Math.abs(b.vy) < 0.01) b.vy = 0;
if (Math.abs(b.omega) < 0.01) b.omega = 0;
}
if (pullStarted && !simulationStopped) {
let maxSpeed = 0;
for (let b of bodies) {
const speed = Math.hypot(b.vx, b.vy) + Math.abs(b.omega) * 0.5;
if (speed > maxSpeed) maxSpeed = speed;
}
if (maxSpeed < 0.05 && (clothSpeed === 0 || clothLeft > TABLE_X1 + 1)) {
stopTimer += dt;
if (stopTimer > 0.5) {
simulationStopped = true;
updateCount();
}
} else {
stopTimer = 0;
}
}
}
// ---------- Drawing ----------
function drawCloth(ctx) {
const y = TABLE_Y;
const h = 0.04;
ctx.beginPath();
ctx.moveTo(clothLeft, y);
ctx.lineTo(clothRight, y);
ctx.lineTo(clothRight, y + h);
ctx.lineTo(clothLeft, y + h);
ctx.closePath();
ctx.fillStyle = '#c04040';
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 0.01;
const stripeSpacing = 0.5;
const start = Math.floor(clothLeft / stripeSpacing) * stripeSpacing;
for (let x = start; x < clothRight; x += stripeSpacing) {
if (x < clothLeft) continue;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x, y + h);
ctx.stroke();
}
}
function drawPlate(ctx, b) {
const w = b.w, h = b.h;
ctx.beginPath();
ctx.ellipse(0, 0, w/2, h/2, 0, 0, Math.PI*2);
ctx.fillStyle = b.color;
ctx.fill();
ctx.strokeStyle = '#b0a090';
ctx.lineWidth = 0.02;
ctx.stroke();
ctx.beginPath();
ctx.ellipse(0, 0, w*0.35, h*0.35, 0, 0, Math.PI*2);
ctx.strokeStyle = '#c0b0a0';
ctx.stroke();
}
function drawGlass(ctx, b) {
const w = b.w, h = b.h;
ctx.beginPath();
ctx.moveTo(-w*0.4, -h/2);
ctx.lineTo(w*0.4, -h/2);
ctx.lineTo(w*0.5, h/2);
ctx.lineTo(-w*0.5, h/2);
ctx.closePath();
ctx.fillStyle = b.color;
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
ctx.lineWidth = 0.02;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-w*0.35, -h*0.1);
ctx.lineTo(w*0.35, -h*0.1);
ctx.lineTo(w*0.4, h/2);
ctx.lineTo(-w*0.4, h/2);
ctx.closePath();
ctx.fillStyle = 'rgba(200,100,100,0.5)';
ctx.fill();
}
function drawCandle(ctx, b) {
const w = b.w, h = b.h;
ctx.fillStyle = '#b08040';
ctx.fillRect(-w*0.8, -h/2, w*1.6, h*0.1);
ctx.fillStyle = '#f0e0a0';
ctx.fillRect(-w/2, -h/2 + h*0.1, w, h*0.8);
ctx.beginPath();
ctx.ellipse(0, h/2 - h*0.05, w*0.6, h*0.08, 0, 0, Math.PI*2);
ctx.fillStyle = '#ffaa00';
ctx.fill();
ctx.beginPath();
ctx.ellipse(0, h/2 - h*0.05, w*0.3, h*0.04, 0, 0, Math.PI*2);
ctx.fillStyle = '#ffff80';
ctx.fill();
}
function drawBowl(ctx, b) {
const w = b.w, h = b.h;
ctx.beginPath();
ctx.moveTo(-w/2, h/2);
ctx.quadraticCurveTo(-w/2, -h/2, 0, -h/2);
ctx.quadraticCurveTo(w/2, -h/2, w/2, h/2);
ctx.closePath();
ctx.fillStyle = b.color;
ctx.fill();
ctx.strokeStyle = '#804020';
ctx.lineWidth = 0.02;
ctx.stroke();
ctx.beginPath();
ctx.ellipse(0, h/2, w/2, h*0.2, 0, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fill();
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#2a1e1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(originX, originY);
ctx.scale(scale, -scale);
// Wall
ctx.fillStyle = '#4a3a30';
ctx.fillRect(-6, 0, 14, 4.5);
// Floor
ctx.fillStyle = '#3a2a20';
ctx.fillRect(-6, -0.5, 14, 0.5);
// Table top
ctx.fillStyle = '#6b4a2a';
ctx.fillRect(0, TABLE_Y - 0.1, 5, 0.1);
// Table legs
ctx.fillStyle = '#5a3a1a';
ctx.fillRect(0.5, 0, 0.2, TABLE_Y);
ctx.fillRect(4.3, 0, 0.2, TABLE_Y);
// Cloth
drawCloth(ctx);
// Objects
for (let b of bodies) {
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.angle);
if (b.type === 'plate') drawPlate(ctx, b);
else if (b.type === 'glass') drawGlass(ctx, b);
else if (b.type === 'candle') drawCandle(ctx, b);
else if (b.type === 'bowl') drawBowl(ctx, b);
ctx.restore();
}
ctx.restore();
}
// ---------- Scene setup ----------
function createBodies() {
const arr = [];
arr.push({
type: 'plate',
x: 0.8, y: TABLE_Y + 0.06, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.9, h: 0.12, mass: 0.7,
muCloth: 0.12, muTable: 0.4, muObj: 0.3,
color: '#e8e0d0'
});
arr.push({
type: 'plate',
x: 1.8, y: TABLE_Y + 0.06, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.8, h: 0.12, mass: 0.5,
muCloth: 0.12, muTable: 0.4, muObj: 0.3,
color: '#d8d0c0'
});
arr.push({
type: 'glass',
x: 2.6, y: TABLE_Y + 0.375, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.28, h: 0.75, mass: 0.35,
muCloth: 0.14, muTable: 0.35, muObj: 0.3,
color: 'rgba(180,220,255,0.6)'
});
arr.push({
type: 'glass',
x: 3.2, y: TABLE_Y + 0.35, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.26, h: 0.7, mass: 0.3,
muCloth: 0.14, muTable: 0.35, muObj: 0.3,
color: 'rgba(180,220,255,0.6)'
});
arr.push({
type: 'candle',
x: 3.8, y: TABLE_Y + 0.5, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.16, h: 1.0, mass: 0.5,
muCloth: 0.15, muTable: 0.4, muObj: 0.3,
color: '#f0e0a0'
});
arr.push({
type: 'bowl',
x: 4.5, y: TABLE_Y + 0.175, angle: 0,
vx: 0, vy: 0, omega: 0,
w: 0.8, h: 0.35, mass: 0.9,
muCloth: 0.12, muTable: 0.45, muObj: 0.3,
color: '#c08060'
});
for (let b of arr) {
b.invMass = 1 / b.mass;
b.inertia = b.mass * (b.w * b.w + b.h * b.h) / 12;
b.invInertia = 1 / b.inertia;
}
return arr;
}
function reset() {
bodies = createBodies();
clothLeft = CLOTH_LEFT0;
clothRight = CLOTH_RIGHT0;
clothSpeed = 0;
pullStarted = false;
simulationStopped = false;
stopTimer = 0;
statusEl.textContent = 'Pull the cloth to begin.';
}
function yank() {
reset();
const speed = parseFloat(speedInput.value);
clothSpeed = speed;
pullStarted = true;
simulationStopped = false;
stopTimer = 0;
statusEl.textContent = 'Pulling...';
}
function updateCount() {
let onTable = 0;
let upright = 0;
for (let b of bodies) {
const corners = getCorners(b);
let minY = Math.min(...corners.map(c => c.y));
const isOnTable = (b.x > TABLE_X0 && b.x < TABLE_X1 && minY > TABLE_Y - 0.2 && b.y > TABLE_Y - 0.1);
if (isOnTable) {
onTable++;
if (Math.abs(b.angle) < 0.3) {
upright++;
}
}
}
statusEl.textContent = `On table: ${onTable} / ${bodies.length}, Upright: ${upright} / ${bodies.length}`;
}
// ---------- Animation loop ----------
function animate(now) {
let frameTime = (now - lastTime) / 1000;
lastTime = now;
if (frameTime > 0.1) frameTime = 0.1;
accumulator += frameTime;
while (accumulator >= fixedDt) {
step(fixedDt);
accumulator -= fixedDt;
}
draw();
requestAnimationFrame(animate);
}
// ---------- Event listeners ----------
speedInput.addEventListener('input', () => {
speedDisplay.textContent = parseFloat(speedInput.value).toFixed(1);
});
document.getElementById('yank').addEventListener('click', yank);
document.getElementById('reset').addEventListener('click', reset);
// ---------- Hash handling ----------
window.addEventListener('load', () => {
const hash = window.location.hash;
const match = hash.match(/#speed=([\d.]+)/);
if (match) {
const speed = parseFloat(match[1]);
if (!isNaN(speed)) {
speedInput.value = speed;
speedDisplay.textContent = speed.toFixed(1);
setTimeout(() => {
if (!hashPulled) {
yank();
hashPulled = true;
}
}, 1000);
}
}
});
// ---------- Initialize ----------
reset();
lastTime = performance.now();
requestAnimationFrame(animate);
})();
</script>
</body>
</html>
The simulation uses a custom 2D rigid-body impulse solver with Coulomb friction. Each object is a rectangle with mass, inertia, and separate friction coefficients for cloth and table. The cloth is a finite moving surface; friction is applied at the contact corners, so sliding, tipping, and collisions emerge from the physics rather than a speed-keyed script. Slow pulls drag objects off the edge; fast pulls leave them standing; intermediate speeds produce sliding, edge-tipping, and falls. Objects can collide, tip over, fall to the floor, and settle.
It behaves like the real trick in the main qualitative sense, but it is not a full 3D cloth simulation. The cloth is a flat strip that moves at constant speed and does not wrinkle or bunch. Objects are 2D rectangles, so plates and bowls are approximated as boxes for collision; their visual shapes are drawn inside those boxes. The candlestick and glasses are tuned so that cloth friction alone does not tip them at high speed—they only tip when they slide over the table edge or collide with something. The bowl is the limiting object for “all stay” because it starts closest to the edge; at around 21–22 ft/s it may still creep off due to numerical jitter. The left plate is the first to fall at low speeds. There are no scripted wobbles; all motion comes from the solver, though the initial layout is spaced to avoid overlap. The simulation stops when all body speeds stay below 0.05 for 0.5 s and the cloth has cleared the table; the count is taken then. Upright is defined as (|\text{angle}| < 0.3) rad, and only objects still on the table are counted.