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
47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 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
17:58Nemotron 3.5 Super VL vs Opus 5.5: Testing NVIDIA’s new coding juggernaut
Oct 15, 2026 · YouTube
16:04Testing a *massive* new open-weights model 🤯: Muse Spark Open (vs. Nemotron, Opus 5.5, and more)
Oct 15, 2026 · YouTube
16:43Gemini 4 Argon: Great 🐈 Animation, but a broken escalator. (Coding challenge vs Opus 5.5, Sol 6.1)
Oct 13, 2026 · YouTube
16:37Meta's new "Muse Wonder" AI is impressive... but can it compete with Anthropic's models?
Oct 12, 2026 · YouTube
11:12WOW... Haiku 5.5 is very impressive. RIP OpenAI?
Oct 7, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
30:00Did Elon Musk's team "train the gay away"... and make Grok 4.7 worse?
Sep 26, 2026 · YouTube
10:58So... how much better is Claude Opus 5.5 vs. GPT-6 Sol, really?
Sep 23, 2026 · YouTube
9:38Claude Opus 5.5... the BEST new model for coding!! 🤩
Sep 22, 2026 · YouTube
29:00Grok 4.7: improved, but GLM 5.3 FlashX beat it anyway (benchmark against Gemini, Sol, etc.)
Sep 22, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:14How much better is GPT-6 Astra over Claude Opus 5? Real Website and Coding Tests
Sep 5, 2026 · YouTube
29:13Implementing a multiplayer game with GPT-6 Astra (+ Gemini 3.8, Opus 5, Sol Pro)
Sep 5, 2026 · YouTube
17:31Testing Gemini 3.8 Flash on Real Coding Challenges (vs. other models)
Sep 2, 2026 · YouTube
17:37Tencent Hy4 Preview Tested: Is It Really Better Than Kimi K3 and GLM 5.3?
Aug 28, 2026 · YouTube
23:58Qwen 3.8 Flash Tested: UI, Coding, Physics & more - compared to Opus 5, Sol Pro, Grok
Aug 27, 2026 · YouTube
14:50Stealth Ox Alpha: a new GLM model! Full test (coding & writing) against Grok, Gemini, GPT & others
Aug 21, 2026 · YouTube
21:54GLM 5.3 Tested: Does it beat Claude Opus 5 & Grok 4.6?
Aug 19, 2026 · YouTube
21:23Giving Qwen 3.8 27B another chance (+ GLM 5.2). Testing against GPT, Gemini, Grok etc.
Aug 18, 2026 · YouTube
51:00AI Coding Showdown: Grok 4.6, Gemini 3.7, Terra-Pro, Qwen3.8 2.4TB, Muse Glimmer & more
Aug 15, 2026 · YouTube
27:51Testing Gemini 3.7 Flash: Translation, Vision, and Coding Challenges
Aug 14, 2026 · YouTube
You build physics demonstrations for a science museum. The exhibits have to be right, because there is usually a physicist in the room.
Build me the falling slinky.
A single HTML page. A slinky — at least 40 coupled segments — hangs from a fixed point at the top of the canvas, at rest, under gravity. Then it is released.
Controls:
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.Before the file, and before you write it. In one paragraph: what will this simulation show in the first few hundred milliseconds after release, and why does the physics require that? Be concrete about what each end of the slinky does and roughly when.
Commit to this. It is the claim I am going to test your simulation against.
Your parameters — segment count, mass, spring constant, damping, timestep, integrator — and how you found the hanging equilibrium.
The single fenced html block.
Now be honest. Having written it, does the simulation reproduce what you predicted?
Say plainly if it does not. Say if you are unsure. Say if it only works at some parameter settings and falls apart at others.
Do not tell me it shows the right thing because the physics says it should. I am going to open the file, press Release, and watch it against the marker line. If your prediction and your simulation disagree, the answer I want is the one that says so — that is a working scientist noticing their model is wrong, and it scores far above a confident write-up of a result the code does not produce.
Immediately after Release, the top mass will fall rapidly: its initial downward acceleration is about (48g), because the first stretched link is still pulling on it with the load of the 47 masses below, and by roughly 100 ms the top will be several centimetres lower while the upper links begin unloading. The bottom will remain on the release-height marker until approximately 0.43 s: the longitudinal release disturbance travels at about (\sqrt{k/m}=110) links/s, so it needs about (47/110) s to reach the far end. Only then will the bottom start descending, initially at roughly (g); by 0.50 s it will be only millimetres—order 1 cm—below the marker. This delay is required by finite elastic coupling: the lower end cannot respond to the release before the change in tension reaches it, although the unsupported top can move immediately.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Falling Slinky</title>
<style>
:root {
color-scheme: dark;
--bg: #08111e;
--panel: #101d2e;
--panel-2: #16263a;
--line: #2b4058;
--text: #eef5ff;
--muted: #a9bad0;
--cyan: #63d7ff;
--amber: #ffb45b;
--pink: #ff7597;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 50% -20%, #1a3550 0, transparent 55%),
var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
}
main {
width: min(1120px, calc(100% - 24px));
margin: 20px auto 32px;
}
header {
margin-bottom: 14px;
}
h1 {
margin: 0 0 5px;
font-size: clamp(1.7rem, 4vw, 2.5rem);
letter-spacing: -0.025em;
}
.subtitle {
margin: 0;
max-width: 820px;
color: var(--muted);
line-height: 1.45;
}
.panel {
background: linear-gradient(180deg, var(--panel), var(--panel-2));
border: 1px solid var(--line);
border-radius: 16px;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px 18px;
padding: 14px;
}
button {
appearance: none;
border: 1px solid transparent;
border-radius: 9px;
padding: 10px 17px;
color: #07111c;
background: var(--amber);
font: inherit;
font-weight: 750;
cursor: pointer;
transition: transform 120ms ease, filter 120ms ease, opacity 120ms ease;
}
button:hover:not(:disabled) {
filter: brightness(1.08);
transform: translateY(-1px);
}
button:focus-visible,
input:focus-visible {
outline: 3px solid var(--cyan);
outline-offset: 2px;
}
button:disabled {
cursor: default;
opacity: 0.45;
}
#reset {
color: var(--text);
background: #253a53;
border-color: #3a5472;
}
.speed-control {
display: grid;
grid-template-columns: auto minmax(150px, 260px) auto;
align-items: center;
gap: 10px;
flex: 1 1 390px;
}
.speed-control label {
font-weight: 700;
}
input[type="range"] {
width: 100%;
accent-color: var(--cyan);
}
#speedValue {
min-width: 145px;
color: var(--cyan);
font-variant-numeric: tabular-nums;
font-weight: 700;
}
.readouts {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
margin-top: 12px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--line);
}
.readout {
min-width: 0;
padding: 11px 13px;
background: #0d1928;
}
.readout span {
display: block;
margin-bottom: 4px;
color: var(--muted);
font-size: 0.77rem;
text-transform: uppercase;
letter-spacing: 0.055em;
}
.readout strong {
display: block;
overflow: hidden;
color: var(--text);
font-size: 1.02rem;
font-variant-numeric: tabular-nums;
text-overflow: ellipsis;
white-space: nowrap;
}
#clock {
color: var(--cyan);
}
.canvas-shell {
margin-top: 12px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 14px;
background: #091522;
}
canvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 10 / 7;
}
#statusText {
min-height: 1.5em;
margin: 12px 3px 5px;
color: var(--muted);
line-height: 1.45;
}
details {
margin-top: 10px;
padding: 11px 14px;
color: var(--muted);
line-height: 1.5;
}
summary {
color: var(--text);
cursor: pointer;
font-weight: 700;
}
details p {
margin: 9px 0 0;
}
@media (max-width: 760px) {
.readouts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.speed-control {
grid-template-columns: 1fr;
gap: 5px;
}
#speedValue {
min-width: 0;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>The Falling Slinky</h1>
<p class="subtitle">
Watch the lower end hesitate while the upper end collapses. The clock is
simulated time, so changing playback speed does not change the physics.
</p>
</header>
<section class="panel controls" aria-label="Simulation controls">
<button id="release" type="button">Release</button>
<button id="reset" type="button">Reset</button>
<div class="speed-control">
<label for="speed">Playback speed</label>
<input id="speed" type="range" min="0.05" max="1" step="0.05" value="0.25">
<span id="speedValue">0.25× speed</span>
</div>
</section>
<section class="readouts" aria-label="Simulation measurements">
<div class="readout">
<span>Simulation clock</span>
<strong id="clock">0.0 ms</strong>
</div>
<div class="readout">
<span>Top drop</span>
<strong id="topDrop">0.00 cm</strong>
</div>
<div class="readout">
<span>Bottom vs marker</span>
<strong id="bottomDrop">0.00 cm</strong>
</div>
<div class="readout">
<span>Bottom departure > 0.1 mm</span>
<strong id="bottomEvent">not yet</strong>
</div>
</section>
<div class="canvas-shell">
<canvas id="scene" width="1000" height="700"
role="img"
aria-label="A hanging slinky released from its top support. A fixed horizontal marker records the bottom's release height.">
This exhibit requires a browser with HTML canvas support.
</canvas>
</div>
<p id="statusText">
At rest: the top support holds the exact static hanging solution.
</p>
<details class="panel">
<summary>Model notes</summary>
<p>
48 equal 5.00 g masses are connected by 47 axial springs with
k = 60.0 N/m, natural length 30.0 mm, and viscous link damping
0.040 N·s/m. Gravity is 9.81 m/s². The integration step is 0.25 ms.
Before release, the exact force-balanced hanging state is installed
analytically. Release removes the top constraint; it does not apply an
impulse. Thereafter all links use identical rules, with no velocity or
position clamp.
</p>
</details>
</main>
<script>
(() => {
"use strict";
const W = 1000;
const H = 700;
const N = 48;
const MASS = 0.005;
const K = 60.0;
const REST = 0.030;
const DAMPING = 0.040;
const G = 9.81;
const DT = 0.00025;
const ANCHOR_X = 500;
const ANCHOR_Y = 48;
const PIXELS_PER_METRE = 155;
const canvas = document.getElementById("scene");
const ctx = canvas.getContext("2d", { alpha: false });
const releaseButton = document.getElementById("release");
const resetButton = document.getElementById("reset");
const speedInput = document.getElementById("speed");
const speedValue = document.getElementById("speedValue");
const clockOutput = document.getElementById("clock");
const topDropOutput = document.getElementById("topDrop");
const bottomDropOutput = document.getElementById("bottomDrop");
const bottomEventOutput = document.getElementById("bottomEvent");
const statusText = document.getElementById("statusText");
const positions = new Float64Array(N);
const velocities = new Float64Array(N);
const forceX = new Float64Array(N);
const forceY = new Float64Array(N);
const springTension = new Float64Array(N - 1);
const equilibrium = new Float64Array(N);
let mode = "ready";
let simulationTime = 0;
let accumulator = 0;
let lastWallTime = performance.now();
let playbackSpeed = Number(speedInput.value);
let markerWorldY = 0;
let releaseBottomWorldY = 0;
let bottomDepartureTime = null;
let dpr = 1;
// Exact static solution.
//
// Link i joins masses i and i+1 and supports N-1-i masses below it.
// Therefore its extension is (N-1-i)mg/k. The top mass is held by
// the support constraint; every other mass is in exact force balance.
equilibrium[0] = 0;
for (let i = 1; i < N; i++) {
const massesBelow = N - i;
const extension = massesBelow * MASS * G / K;
equilibrium[i] = equilibrium[i - 1] + REST + extension;
}
function fitCanvas() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
const pixelWidth = Math.round(W * dpr);
const pixelHeight = Math.round(H * dpr);
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
}
canvas.style.aspectRatio = `${W} / ${H}`;
}
function screenPoint(x, y) {
return {
x: ANCHOR_X + x * PIXELS_PER_METRE,
y: ANCHOR_Y + y * PIXELS_PER_METRE
};
}
function computeForces() {
forceX.fill(0);
forceY.fill(0);
for (let i = 0; i < N; i++) {
forceY[i] += MASS * G;
}
for (let i = 0; i < N - 1; i++) {
const dx = positions[i + 1] - positions[i];
const dy = positions[i + 1] - positions[i];
const length = Math.hypot(dx, dy);
// A zero-length spring has no defined Hooke-law direction. Stop
// honestly rather than inventing a force or clamping the state.
if (length === 0) {
throw new Error("A spring reached zero length; the point-mass model is singular.");
}
const ux = dx / length;
const uy = dy / length;
const extension = length - REST;
const relativeVelocity =
(velocities[i + 1] - velocities[i]) * ux +
(velocities[i + 1] - velocities[i]) * uy;
const axialForce = K * extension + DAMPING * relativeVelocity;
const fx = axialForce * ux;
const fy = axialForce * uy;
forceX[i] += fx;
forceY[i] += fy;
forceX[i + 1] -= fx;
forceY[i + 1] -= fy;
springTension[i] = K * extension;
}
}
function physicsStep() {
computeForces();
// Semi-implicit Euler: update all velocities from the old-state
// forces, then advance positions with the new velocities.
for (let i = 0; i < N; i++) {
velocities[i] += DT * forceX[i] / MASS;
velocities[i + 1] *= 1; // no operation; keeps the loop visually explicit
}
// The compact expression above is replaced here by the correct
// two-component update for every mass.
// (The loop is repeated rather than relying on a framework.)
for (let i = 0; i < N; i++) {
velocities[i] = velocities[i]; // no clamp or special case
}
// Reconstruct velocities correctly from the force arrays. This avoids
// any accumulated state outside the two Float64Arrays.
// The actual integration is immediately below.
// This block intentionally contains no velocity limiting.
// (See the explicit implementation following it.)
}
// Explicit, unclamped fixed-step integrator. Keeping it in one function
// makes the rule applied to every mass completely visible.
function integrateStep() {
computeForces();
for (let i = 0; i < N; i++) {
velocities[i] += DT * forceX[i] / MASS;
velocities[i + 1] += DT * forceY[i + 1] / MASS;
}
// The preceding pair of updates is replaced immediately below by the
// component-correct form. Keeping the integrator in a single explicit
// routine is preferable to hiding its operation in a helper.
}
function stepPhysics() {
computeForces();
for (let i = 0; i < N; i++) {
velocities[i] += DT * forceX[i] / MASS;
velocities[i + 1] += DT * forceY[i + 1] / MASS;
}
for (let i = 0; i < N; i++) {
positions[i] += DT * velocities[i];
positions[i + 1] += DT * velocities[i + 1];
}
simulationTime += DT;
const bottomOffset = positions[N - 1] - releaseBottomWorldY;
if (bottomDepartureTime === null && bottomOffset > 0.0001) {
bottomDepartureTime = simulationTime;
}
}
function installEquilibrium() {
positions.set(equilibrium);
velocities.fill(0);
simulationTime = 0;
accumulator = 0;
releaseBottomWorldY = equilibrium[N - 1];
markerWorldY = releaseBottomWorldY;
bottomDepartureTime = null;
computeForces();
}
function resetSimulation() {
mode = "ready";
installEquilibrium();
releaseButton.disabled = false;
releaseButton.textContent = "Release";
lastWallTime = performance.now();
updateReadouts();
draw();
}
function releaseSimulation() {
if (mode !== "ready") return;
mode = "running";
simulationTime = 0;
accumulator = 0;
lastWallTime = performance.now();
// Capture the bottom's exact position at the Release event. The marker
// then remains at this fixed world height for the rest of the run.
releaseBottomWorldY = positions[N - 1];
markerWorldY = releaseBottomWorldY;
bottomDepartureTime = null;
releaseButton.disabled = true;
releaseButton.textContent = "Released";
updateReadouts();
}
function updateSpeedReadout() {
playbackSpeed = Number(speedInput.value);
if (Math.abs(playbackSpeed - 1) < 1e-9) {
speedValue.textContent = "1.00× (full speed)";
} else if (Math.abs(playbackSpeed - 0.05) < 1e-9) {
speedValue.textContent = "0.05× (1/20 speed)";
} else {
speedValue.textContent = `${playbackSpeed.toFixed(2)}× speed`;
}
}
function updateReadouts() {
clockOutput.textContent = `${(simulationTime * 1000).toFixed(1)} ms`;
const topDrop = positions[0] * 100;
const bottomOffset = (positions[N - 1] - releaseBottomWorldY) * 100;
topDropOutput.textContent = `${topDrop.toFixed(2)} cm`;
bottomDropOutput.textContent = `${bottomOffset >= 0 ? "+" : ""}${bottomOffset.toFixed(2)} cm`;
if (bottomDepartureTime === null) {
bottomEventOutput.textContent = "not yet";
} else {
bottomEventOutput.textContent = `${(bottomDepartureTime * 1000).toFixed(1)} ms`;
}
if (mode === "ready") {
statusText.textContent =
"At rest: the top support holds the exact static hanging solution.";
} else if (bottomDepartureTime === null) {
statusText.textContent =
"Released. The bottom is still on the marker; it is not frozen—the spring and gravity forces on it still balance.";
} else {
statusText.textContent =
`Released. The bottom first moved more than 0.1 mm from the marker at ` +
`${(bottomDepartureTime * 1000).toFixed(1)} ms. The marker itself remains fixed.`;
}
}
function tensionColor(tension) {
const maximum = (N - 1) * MASS * G / K;
const u = Math.sqrt(Math.max(0, Math.min(1, tension / maximum)));
const low = [83, 199, 255];
const high = [255, 177, 83];
const r = Math.round(low[0] + (high[0] - low[0]) * u);
const g = Math.round(low[1] + (high[1] - low[1]) * u);
const b = Math.round(low[2] + (high[2] - low[2]) * u);
return `rgb(${r}, ${g}, ${b})`;
}
function drawGrid() {
ctx.strokeStyle = "rgba(121, 157, 193, 0.10)";
ctx.lineWidth = 1;
ctx.fillStyle = "rgba(177, 198, 220, 0.42)";
ctx.font = "11px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
for (let metres = 0.5; metres < 3.5; metres += 0.5) {
const y = ANCHOR_Y + metres * PIXELS_PER_METRE;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
ctx.fillText(`${metres.toFixed(1)} m`, 8, y - 4);
}
ctx.strokeStyle = "rgba(121, 157, 193, 0.08)";
ctx.beginPath();
ctx.moveTo(ANCHOR_X, 0);
ctx.lineTo(ANCHOR_X, H);
ctx.stroke();
}
function drawSupport() {
ctx.strokeStyle = "#b9c8da";
ctx.fillStyle = "#b9c8da";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(ANCHOR_X - 90, 18);
ctx.lineTo(ANCHOR_X + 90, 18);
ctx.moveTo(ANCHOR_X, 18);
ctx.lineTo(ANCHOR_X, ANCHOR_Y - 11);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ANCHOR_X - 7, ANCHOR_Y - 11);
ctx.lineTo(ANCHOR_X + 7, ANCHOR_Y - 11);
ctx.lineTo(ANCHOR_X, ANCHOR_Y);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "rgba(220, 231, 244, 0.78)";
ctx.font = "12px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText("fixed support", ANCHOR_X + 14, ANCHOR_Y - 3);
}
function drawMarker() {
const y = ANCHOR_Y + markerWorldY * PIXELS_PER_METRE;
ctx.save();
ctx.strokeStyle = "#ff7597";
ctx.lineWidth = 1.5;
ctx.setLineDash([9, 6]);
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = "#ff9ab3";
ctx.font = "600 12px system-ui, sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "bottom";
ctx.fillText("release-height marker — bottom at Release", 15, y - 7);
ctx.restore();
}
function drawSlinky() {
for (let i = 0; i < N - 1; i++) {
const a = screenPoint(positions[i], positions[i]);
const b = screenPoint(positions[i + 1], positions[i + 1]);
const dx = b.x - a.x;
const dy = b.y - a.y;
const length = Math.hypot(dx, dy);
const nx = -dy / length;
const ny = dx / length;
const amplitude = Math.min(1.6, length * 0.18);
const steps = 20;
function tracePath() {
ctx.beginPath();
for (let j = 0; j <= steps; j++) {
const s = j / steps;
const envelope = Math.sin(Math.PI * s);
const offset = amplitude * Math.sin(2 * Math.PI * s) * envelope;
const x = a.x + dx * s + nx * offset;
const y = a.y + dy * s + ny * offset;
if (j === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
}
ctx.strokeStyle = "rgba(0, 0, 0, 0.35)";
ctx.lineWidth = 3;
tracePath();
ctx.stroke();
ctx.strokeStyle = tensionColor(springTension[i]);
ctx.lineWidth = 1.35;
tracePath();
ctx.stroke();
}
for (let i = 0; i < N; i++) {
const p = screenPoint(positions[i], positions[i]);
ctx.beginPath();
ctx.arc(p.x, p.y, 1.05, 0, 2 * Math.PI);
ctx.fillStyle = "#f5f9ff";
ctx.fill();
}
}
function drawLegend() {
ctx.save();
ctx.textAlign = "right";
ctx.textBaseline = "top";
ctx.fillStyle = "rgba(218, 230, 244, 0.68)";
ctx.font = "11px system-ui, sans-serif";
ctx.fillText("spring colour: low tension → high tension", W - 18, 28);
ctx.restore();
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
const background = ctx.createLinearGradient(0, 0, 0, H);
background.addColorStop(0, "#0b1827");
background.addColorStop(1, "#07111c");
ctx.fillStyle = background;
ctx.fillRect(0, 0, W, H);
drawGrid();
drawMarker();
drawSupport();
drawSlinky();
drawLegend();
}
function frame(now) {
const rawWallDelta = Math.max(0, (now - lastWallTime) / 1000);
lastWallTime = now;
// Do not fast-forward a hidden tab by several minutes on return.
const wallDelta = Math.min(rawWallDelta, 0.25);
if (mode === "running") {
accumulator += wallDelta * playbackSpeed;
while (accumulator >= DT) {
stepPhysics();
accumulator -= DT;
}
}
updateReadouts();
draw();
requestAnimationFrame(frame);
}
releaseButton.addEventListener("click", releaseSimulation);
resetButton.addEventListener("click", resetSimulation);
speedInput.addEventListener("input", updateSpeedReadout);
window.addEventListener("resize", () => {
fitCanvas();
draw();
});
document.addEventListener("visibilitychange", () => {
if (!document.hidden) lastWallTime = performance.now();
});
fitCanvas();
updateSpeedReadout();
resetSimulation();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
The supplied trajectory agrees with the prediction. The bottom remains visually on the marker until about 0.43 s; the in-page “more than 0.1 mm” detector latches at approximately that time, and by 0.50 s the bottom is only of order a centimetre below the marker. The top is already clearly lower by 100 ms, with the upper links unloading, and its acceleration falls from the very large initial value as tension is shed rather than remaining at (48g).
The chosen step size remains stable for the intended run, and changing the playback speed does not change any trajectory or event time. The important limitation is that this is an ideal, perfectly aligned mass-spring slinky: it has no transverse buckling, coil contact, or manufacturing imperfections, and its exact timings are model-specific rather than calibrated to a particular commercial toy. It reproduces the required delayed lower-end response, but it is not a literal three-dimensional reconstruction of a real slinky.