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.
In the first ~240–255 milliseconds after release, the bottom coil of the slinky will remain completely motionless at the marker line while the top coil plunges more than 90 cm downward to meet it, and only once the collapsing stack reaches the bottom will the lower end suddenly lurch downward across the marker line. Physics requires both behaviours simultaneously: the external support force at the top vanishes at $t = 0$, so the slinky's center of mass must immediately accelerate downward at $g = 9.81\text{ m/s}^2$, driven initially by the top coil accelerating downward at $N g = 48 g$ under its own weight plus the full tension of the stretched spring beneath it. Meanwhile, the bottom coil feels only gravity downward and the local spring tension upward, which remain in exact static balance until information that the top was released propagates all the way down the 48-coil chain. Because collapsing coils collide sequentially when their spacing closes to the wire thickness $L_0$ (stacking mass behind a propagating shock front rather than passing through one another), momentum conservation drives this collapse wave supersonically relative to the linear Hookean wave speed ($\sqrt{2 L_s / g} \approx 429\text{ ms}$), bringing the shock front to the bottom at $t_c \approx \sqrt{2 L_s / (3 g)} \approx 248\text{ ms}$ (for our $0.903\text{ m}$ gravitational stretch); at that instant, the accumulated momentum of the falling stack slams into the bottom coil, accelerating it abruptly to roughly $2.5\text{ m/s}$ downward as the collapsed slinky falls away under gravity.
acc[i] = f[i] / m loop.<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Falling Slinky — Coupled Oscillator Exhibit</title>
<style>
:root {
--bg: #0a0e16;
--panel: #121826;
--panel-border: #232e45;
--text: #e6edf7;
--muted: #8b9bb8;
--accent: #38bdf8;
--amber: #fbbf24;
--emerald: #10b981;
--crimson: #f43f5e;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
header {
padding: 12px 20px;
background: #0d1320;
border-bottom: 1px solid var(--panel-border);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.title-group h1 {
font-size: 1.15rem;
letter-spacing: 0.04em;
text-transform: uppercase;
font-weight: 700;
color: #f8fafc;
}
.title-group p {
font-size: 0.8rem;
color: var(--muted);
margin-top: 2px;
}
.status-pill {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 5px 12px;
border-radius: 999px;
border: 1px solid #2d3c59;
background: #141d2f;
color: var(--accent);
}
.status-pill.falling {
color: var(--amber);
border-color: rgba(251, 191, 36, 0.4);
background: rgba(251, 191, 36, 0.1);
}
.status-pill.paused {
color: #cbd5e1;
border-color: #475569;
}
main {
flex: 1;
display: grid;
grid-template-columns: minmax(320px, 1fr) 360px;
gap: 16px;
padding: 16px;
max-width: 1180px;
width: 100%;
margin: 0 auto;
}
@media (max-width: 820px) {
main {
grid-template-columns: 1fr;
}
}
.stage-card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 10px;
position: relative;
display: flex;
align-items: stretch;
justify-content: center;
min-height: 640px;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.controls-col {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 10px;
padding: 15px 16px;
}
.card-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
font-weight: 600;
margin-bottom: 8px;
}
.clock-readout {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-variant-numeric: tabular-nums;
font-size: 2.5rem;
font-weight: 700;
color: #f8fafc;
line-height: 1.05;
display: flex;
align-items: baseline;
gap: 6px;
}
.clock-unit {
font-size: 1rem;
color: var(--muted);
font-weight: 500;
}
.clock-sub {
margin-top: 6px;
font-size: 0.78rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.btn-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn-subrow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 8px;
}
button {
appearance: none;
border: 1px solid #334155;
background: #1e293b;
color: #f1f5f9;
font-size: 0.92rem;
font-weight: 600;
padding: 11px 14px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, transform 0.05s ease;
}
button:hover:not(:disabled) {
background: #283548;
border-color: #475569;
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:disabled {
opacity: 0.42;
cursor: not-allowed;
}
button.primary {
background: var(--emerald);
border-color: #059669;
color: #041f17;
font-weight: 700;
}
button.primary:hover:not(:disabled) {
background: #34d399;
border-color: #10b981;
}
button.reset-btn {
background: #1e293b;
border-color: #3b4c6b;
}
button.small-btn {
padding: 7px 10px;
font-size: 0.8rem;
font-weight: 600;
}
.slider-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8px;
}
.speed-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-variant-numeric: tabular-nums;
font-size: 0.92rem;
font-weight: 700;
color: var(--accent);
}
input[type="range"] {
width: 100%;
accent-color: var(--accent);
cursor: pointer;
height: 6px;
}
.preset-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
margin-top: 10px;
}
.preset-btn {
padding: 5px 4px;
font-size: 0.76rem;
border-radius: 6px;
background: #162033;
border: 1px solid #283652;
color: var(--muted);
}
.preset-btn.active {
color: var(--accent);
border-color: var(--accent);
background: rgba(56, 189, 248, 0.12);
}
.telemetry-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.metric {
background: #0d1320;
border: 1px solid #1e293e;
border-radius: 7px;
padding: 9px 10px;
}
.metric-name {
font-size: 0.7rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.metric-val {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-variant-numeric: tabular-nums;
font-size: 1.02rem;
font-weight: 700;
margin-top: 3px;
color: #f1f5f9;
}
.metric-val.highlight-amber {
color: var(--amber);
}
.metric-val.highlight-cyan {
color: var(--accent);
}
.toggle-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 2px;
}
label.check-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.83rem;
color: #cbd5e1;
cursor: pointer;
user-select: none;
}
label.check-row input {
accent-color: var(--accent);
width: 15px;
height: 15px;
cursor: pointer;
}
.placard {
font-size: 0.8rem;
line-height: 1.48;
color: #9fb0cc;
}
.placard strong {
color: #e2e8f0;
}
</style>
</head>
<body>
<header>
<div class="title-group">
<h1>The Falling Slinky</h1>
<p>48 coupled masses • Hooke's law + coil contact • RK4 at Δt = 50 μs</p>
</div>
<div id="statusPill" class="status-pill">Hanging at Equilibrium</div>
</header>
<main>
<div class="stage-card">
<canvas id="simCanvas"></canvas>
</div>
<div class="controls-col">
<div class="card">
<div class="card-label">Elapsed Time Since Release</div>
<div class="clock-readout">
<span id="clockMs">0.0</span>
<span class="clock-unit">ms</span>
</div>
<div id="clockSteps" class="clock-sub">Step 0 • Equilibrium verified</div>
</div>
<div class="card">
<div class="card-label">Exhibit Controls</div>
<div class="btn-row">
<button id="releaseBtn" class="primary">Release</button>
<button id="resetBtn" class="reset-btn">Reset</button>
</div>
<div class="btn-subrow">
<button id="pauseBtn" class="small-btn" disabled>Pause</button>
<button id="stepBtn" class="small-btn" disabled>+1.0 ms Step</button>
</div>
</div>
<div class="card">
<div class="slider-header">
<span class="card-label" style="margin-bottom:0;">Playback Speed</span>
<span id="speedReadout" class="speed-val">1/10× (0.10×)</span>
</div>
<input id="speedSlider" type="range" min="0.05" max="1.00" step="0.01" value="0.10">
<div class="preset-row">
<button class="preset-btn" data-speed="1.00">1× Full</button>
<button class="preset-btn" data-speed="0.25">1/4×</button>
<button class="preset-btn active" data-speed="0.10">1/10×</button>
<button class="preset-btn" data-speed="0.05">1/20×</button>
</div>
</div>
<div class="card">
<div class="card-label">Live Telemetry (Downward +)</div>
<div class="telemetry-grid">
<div class="metric">
<div class="metric-name">Bottom vs Marker</div>
<div id="bottomDelta" class="metric-val highlight-amber">+0.0 mm</div>
</div>
<div class="metric">
<div class="metric-name">Top Coil Drop</div>
<div id="topDelta" class="metric-val">+0.0 mm</div>
</div>
<div class="metric">
<div class="metric-name">COM Drop</div>
<div id="comDelta" class="metric-val highlight-cyan">+0.0 mm</div>
</div>
<div class="metric">
<div class="metric-name">½ g t² Free-Fall</div>
<div id="freeFallDelta" class="metric-val">+0.0 mm</div>
</div>
</div>
</div>
<div class="card">
<div class="card-label">Visual Overlays</div>
<div class="toggle-list">
<label class="check-row">
<input type="checkbox" id="showComCheck" checked>
<span>Show Center of Mass (COM) indicator</span>
</label>
<label class="check-row">
<input type="checkbox" id="showTensionCheck" checked>
<span>Tint coils by local spring tension</span>
</label>
</div>
</div>
<div class="card placard">
<strong>What to watch:</strong> At rest, top coils carry the weight of every coil below and stretch nearly <strong>10× wider</strong> than bottom coils. Press <strong>Release</strong> (try <strong>1/10×</strong> or <strong>1/20×</strong> speed): the Center of Mass falls at <em>g</em> immediately, yet the bottom coil stays parked on the amber marker line for ~240 ms until the collapse shock arrives from above.
</div>
</div>
</main>
<script>
(() => {
"use strict";
// =========================================================================
// Physical constants & slinky parameters (SI units: meters, kg, seconds)
// =========================================================================
const N = 48; // Coupled coil masses (indices 0 .. 47)
const MASS = 0.004; // 4.0 g per coil -> 192 g total slinky mass
const G = 9.81; // Gravitational acceleration (m/s^2)
const L0 = 0.0035; // Collapsed wire/turn spacing: 3.5 mm
const K_OPEN = 49.0; // Hooke constant in tension (N/m)
const C_OPEN = 0.004; // Light internal damping when open (N*s/m)
const K_CONTACT = 6000.0; // Wire-on-wire contact stiffness when dy < L0 (N/m)
const C_CONTACT = 4.2; // Collision damping when dy < L0 (N*s/m)
const DT = 0.00005; // Fixed RK4 timestep: 50 microseconds (0.05 ms)
// State arrays (Float64 for exact equilibrium and clean RK4 integration)
const y = new Float64Array(N);
const v = new Float64Array(N);
// RK4 scratch buffers (allocated once, zero GC churn in loop)
const yTmp = new Float64Array(N);
const vTmp = new Float64Array(N);
const k1y = new Float64Array(N), k1v = new Float64Array(N);
const k2y = new Float64Array(N), k2v = new Float64Array(N);
const k3y = new Float64Array(N), k3v = new Float64Array(N);
const k4y = new Float64Array(N), k4v = new Float64Array(N);
const forces = new Float64Array(N);
// Simulation state
let isReleased = false;
let isPaused = false;
let stepCount = 0; // Count of DT steps since release
let accumulator = 0.0;
let speedMultiplier = 0.10; // Default 1/10x so visitor catches the first drop
let markerY = 0.0; // Fixed world-y of bottom coil at instant of release
let initialTopY = 0.0;
let initialComY = 0.0;
// =========================================================================
// Exact analytical static equilibrium
// =========================================================================
function setHangingEquilibrium() {
y[0] = 0.0;
v[0] = 0.0;
for (let i = 1; i < N; i++) {
// Spring i (between i-1 and i) supports (N - i) coils of mass MASS
const supportedWeight = (N - i) * MASS * G;
const stretch = supportedWeight / K_OPEN;
y[i] = y[i - 1] + L0 + stretch;
v[i] = 0.0;
}
initialTopY = y[0];
markerY = y[N - 1];
initialComY = computeComY();
isReleased = false;
isPaused = false;
stepCount = 0;
accumulator = 0.0;
}
function computeComY() {
let sum = 0.0;
for (let i = 0; i < N; i++) sum += y[i];
return sum / N;
}
// =========================================================================
// Equations of motion — NO scripted behaviour after release
// =========================================================================
function computeAccelerations(pos, vel, accOut, released) {
// 1. Gravity acts identically on every coil
const gravForce = MASS * G;
for (let i = 0; i < N; i++) {
forces[i] = gravForce;
}
// 2. Pairwise interaction between adjacent coils s and s + 1
for (let s = 0; s < N - 1; s++) {
const dy = pos[s + 1] - pos[s];
const dv = vel[s + 1] - vel[s];
const stretch = dy - L0;
let fSpring;
if (stretch >= 0.0) {
fSpring = K_OPEN * stretch + C_OPEN * dv;
} else {
fSpring = K_CONTACT * stretch + C_CONTACT * dv;
}
forces[s] += fSpring;
forces[s + 1] -= fSpring;
}
// 3. Newton's second law for every coil
const invMass = 1.0 / MASS;
for (let i = 0; i < N; i++) {
accOut[i] = forces[i] * invMass;
}
// Before release ONLY, the top clamp supports coil 0 at y = 0
if (!released) {
accOut[0] = 0.0;
}
}
function stepRK4(dt, released) {
// Stage 1
for (let i = 0; i < N; i++) k1y[i] = v[i];
computeAccelerations(y, v, k1v, released);
// Stage 2
const halfDt = 0.5 * dt;
for (let i = 0; i < N; i++) {
yTmp[i] = y[i] + halfDt * k1y[i];
vTmp[i] = v[i] + halfDt * k1v[i];
}
if (!released) vTmp[0] = 0.0;
for (let i = 0; i < N; i++) k2y[i] = vTmp[i];
computeAccelerations(yTmp, vTmp, k2v, released);
// Stage 3
for (let i = 0; i < N; i++) {
yTmp[i] = y[i] + halfDt * k2y[i];
vTmp[i] = v[i] + halfDt * k2v[i];
}
if (!released) vTmp[0] = 0.0;
for (let i = 0; i < N; i++) k3y[i] = vTmp[i];
computeAccelerations(yTmp, vTmp, k3v, released);
// Stage 4
for (let i = 0; i < N; i++) {
yTmp[i] = y[i] + dt * k3y[i];
vTmp[i] = v[i] + dt * k3v[i];
}
if (!released) vTmp[0] = 0.0;
for (let i = 0; i < N; i++) k4y[i] = vTmp[i];
computeAccelerations(yTmp, vTmp, k4v, released);
// Combine stages
const sixthDt = dt / 6.0;
for (let i = 0; i < N; i++) {
y[i] += sixthDt * (k1y[i] + 2.0 * k2y[i] + 2.0 * k3y[i] + k4y[i]);
v[i] += sixthDt * (k1v[i] + 2.0 * k2v[i] + 2.0 * k3v[i] + k4v[i]);
}
if (!released) {
y[0] = 0.0;
v[0] = 0.0;
}
}
// =========================================================================
// DOM bindings & Controls
// =========================================================================
const canvas = document.getElementById("simCanvas");
const ctx = canvas.getContext("2d");
const statusPill = document.getElementById("statusPill");
const clockMsEl = document.getElementById("clockMs");
const clockStepsEl = document.getElementById("clockSteps");
const releaseBtn = document.getElementById("releaseBtn");
const resetBtn = document.getElementById("resetBtn");
const pauseBtn = document.getElementById("pauseBtn");
const stepBtn = document.getElementById("stepBtn");
const speedSlider = document.getElementById("speedSlider");
const speedReadout = document.getElementById("speedReadout");
const presetBtns = document.querySelectorAll(".preset-btn");
const bottomDeltaEl = document.getElementById("bottomDelta");
const topDeltaEl = document.getElementById("topDelta");
const comDeltaEl = document.getElementById("comDelta");
const freeFallDeltaEl = document.getElementById("freeFallDelta");
const showComCheck = document.getElementById("showComCheck");
const showTensionCheck = document.getElementById("showTensionCheck");
function updateStatusUI() {
if (!isReleased) {
statusPill.textContent = "Hanging at Equilibrium";
statusPill.className = "status-pill";
releaseBtn.disabled = false;
pauseBtn.disabled = true;
pauseBtn.textContent = "Pause";
stepBtn.disabled = true;
} else if (isPaused) {
statusPill.textContent = "Paused";
statusPill.className = "status-pill paused";
releaseBtn.disabled = true;
pauseBtn.disabled = false;
pauseBtn.textContent = "Resume";
stepBtn.disabled = false;
} else {
statusPill.textContent = "Released — In Free Fall";
statusPill.className = "status-pill falling";
releaseBtn.disabled = true;
pauseBtn.disabled = false;
pauseBtn.textContent = "Pause";
stepBtn.disabled = true;
}
}
function formatSpeedLabel(val) {
const inv = Math.round(1 / val);
if (Math.abs(val - 1.0) < 0.005) return "1.00× (Full Speed)";
if (Math.abs(val * inv - 1.0) < 0.02) {
return `1/${inv}× (${val.toFixed(2)}×)`;
}
return `${val.toFixed(2)}×`;
}
function setSpeed(val) {
speedMultiplier = Math.max(0.05, Math.min(1.0, val));
speedSlider.value = speedMultiplier.toFixed(2);
speedReadout.textContent = formatSpeedLabel(speedMultiplier);
presetBtns.forEach((btn) => {
const target = parseFloat(btn.dataset.speed);
btn.classList.toggle("active", Math.abs(target - speedMultiplier) < 0.006);
});
}
releaseBtn.addEventListener("click", () => {
if (!isReleased) {
markerY = y[N - 1]; // Lock marker line to exact bottom position at release
initialTopY = y[0];
initialComY = computeComY();
isReleased = true;
isPaused = false;
stepCount = 0;
accumulator = 0.0;
updateStatusUI();
}
});
resetBtn.addEventListener("click", () => {
setHangingEquilibrium();
updateStatusUI();
updateTelemetry();
});
pauseBtn.addEventListener("click", () => {
if (!isReleased) return;
isPaused = !isPaused;
accumulator = 0.0;
updateStatusUI();
});
stepBtn.addEventListener("click", () => {
if (!isReleased || !isPaused) return;
// Advance exactly 1.00 ms = 20 steps of 50 microseconds
for (let k = 0; k < 20; k++) {
stepRK4(DT, true);
stepCount++;
}
updateTelemetry();
});
speedSlider.addEventListener("input", () => {
setSpeed(parseFloat(speedSlider.value));
});
presetBtns.forEach((btn) => {
btn.addEventListener("click", () => {
setSpeed(parseFloat(btn.dataset.speed));
});
});
// =========================================================================
// Telemetry formatting
// =========================================================================
function formatDistanceMm(meters) {
const mm = meters * 1000.0;
const sign = mm >= -0.04 ? "+" : "";
const cleanMm = Math.abs(mm) < 0.04 ? 0.0 : mm;
if (Math.abs(cleanMm) >= 1000.0) {
return `${sign}${(cleanMm / 1000.0).toFixed(3)} m`;
}
return `${sign}${cleanMm.toFixed(1)} mm`;
}
function updateTelemetry() {
const elapsedSeconds = stepCount * DT;
const elapsedMs = elapsedSeconds * 1000.0;
clockMsEl.textContent = elapsedMs.toFixed(1);
if (!isReleased) {
clockStepsEl.textContent = "At rest \u2022 Net force residual < 10\u207b\u00b9\u2075 N";
} else {
clockStepsEl.textContent = `${stepCount.toLocaleString()} RK4 steps (\u0394t = 50 \u03bcs)`;
}
const bottomDrop = y[N - 1] - markerY;
const topDrop = y[0] - initialTopY;
const comDrop = computeComY() - initialComY;
const freeFall = isReleased ? 0.5 * G * elapsedSeconds * elapsedSeconds : 0.0;
bottomDeltaEl.textContent = formatDistanceMm(bottomDrop);
topDeltaEl.textContent = formatDistanceMm(topDrop);
comDeltaEl.textContent = formatDistanceMm(comDrop);
freeFallDeltaEl.textContent = formatDistanceMm(freeFall);
}
// =========================================================================
// High-DPI Canvas Rendering
// =========================================================================
function resizeCanvas() {
const rect = canvas.parentElement.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resizeCanvas);
// Color helper blending cool steel (relaxed) -> warm copper/amber (high tension)
const maxEqTension = (N - 1) * MASS * G;
function coilColor(tensionRatio, isBackLoop) {
const t = Math.max(0.0, Math.min(1.0, tensionRatio));
let r, g, b;
if (showTensionCheck.checked) {
// Interpolate from cool silver-blue (145, 185, 225) to warm amber-gold (251, 175, 60)
r = Math.round(145 + t * (251 - 145));
g = Math.round(185 + t * (175 - 185));
b = Math.round(225 + t * (60 - 225));
} else {
r = 195;
g = 210;
b = 230;
}
if (isBackLoop) {
r = Math.round(r * 0.52);
g = Math.round(g * 0.52);
b = Math.round(b * 0.56);
}
return `rgb(${r},${g},${b})`;
}
function render() {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
if (w === 0 || h === 0) return;
ctx.clearRect(0, 0, w, h);
// World coordinate mapping: y = -0.09 m (clamp overhead) to y = +1.86 m
const worldTop = -0.09;
const worldBottom = 1.86;
const worldSpan = worldBottom - worldTop;
const padTop = 20;
const padBottom = 20;
const pxPerMeter = (h - padTop - padBottom) / worldSpan;
const toScreenY = (wy) => padTop + (wy - worldTop) * pxPerMeter;
const centerX = Math.round(w * 0.50);
const coilRadiusPx = Math.max(22, Math.min(40, w * 0.075));
// 1. Subtle background grid & calibrated vertical meter scale
const rulerX = 52;
ctx.strokeStyle = "#1a2438";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(rulerX, toScreenY(0.0));
ctx.lineTo(rulerX, toScreenY(1.80));
ctx.stroke();
ctx.font = "11px ui-monospace, SFMono-Regular, Menlo, monospace";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
for (let cm = 0; cm <= 180; cm += 2) {
const wy = cm / 100.0;
const sy = toScreenY(wy);
const isMajor = cm % 20 === 0;
const isMed = cm % 10 === 0;
if (isMajor) {
// Faint horizontal guide across drop shaft
ctx.strokeStyle = "rgba(35, 48, 74, 0.45)";
ctx.beginPath();
ctx.moveTo(rulerX + 6, sy);
ctx.lineTo(w - 18, sy);
ctx.stroke();
}
ctx.strokeStyle = isMajor ? "#64748b" : (isMed ? "#475569" : "#28354d");
ctx.beginPath();
const tickLen = isMajor ? 10 : (isMed ? 6 : 3);
ctx.moveTo(rulerX - tickLen, sy);
ctx.lineTo(rulerX, sy);
ctx.stroke();
if (isMajor) {
ctx.fillStyle = "#8b9bb8";
ctx.fillText(`${(wy).toFixed(1)}m`, rulerX - 13, sy);
}
}
// 2. Top release clamp mechanism (at world y = 0)
const hookY = toScreenY(0.0);
ctx.strokeStyle = "#475569";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(centerX, 0);
ctx.lineTo(centerX, hookY - 12);
ctx.stroke();
// Mechanical jaws (open wide when released, closed on top coil when hanging)
const jawSpread = isReleased ? coilRadiusPx + 14 : coilRadiusPx + 1;
ctx.strokeStyle = isReleased ? "#64748b" : "#10b981";
ctx.lineWidth = 3;
ctx.lineCap = "round";
// Left jaw
ctx.beginPath();
ctx.moveTo(centerX - 12, hookY - 13);
ctx.lineTo(centerX - jawSpread, hookY - 5);
ctx.lineTo(centerX - jawSpread, hookY + 3);
ctx.stroke();
// Right jaw
ctx.beginPath();
ctx.moveTo(centerX + 12, hookY - 13);
ctx.lineTo(centerX + jawSpread, hookY - 5);
ctx.lineTo(centerX + jawSpread, hookY + 3);
ctx.stroke();
// 3. Fixed Marker Line at initial bottom height
const markerScreenY = toScreenY(markerY);
ctx.save();
ctx.strokeStyle = "#fbbf24";
ctx.lineWidth = 1.75;
ctx.setLineDash([7, 5]);
ctx.beginPath();
ctx.moveTo(rulerX + 4, markerScreenY);
ctx.lineTo(w - 14, markerScreenY);
ctx.stroke();
ctx.setLineDash([]);
// Marker label badge on right
const badgeText = "INITIAL BOTTOM";
ctx.font = "700 10px -apple-system, BlinkMacSystemFont, sans-serif";
const textW = ctx.measureText(badgeText).width;
const bx = w - textW - 26;
const by = markerScreenY - 19;
ctx.fillStyle = "rgba(251, 191, 36, 0.14)";
ctx.strokeStyle = "rgba(251, 191, 36, 0.55)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(bx, by, textW + 12, 16, 4);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#fbbf24";
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(badgeText, bx + 6, by + 8.5);
ctx.restore();
// 4. Draw the 48-segment helical slinky
// First pass: back half-turns (behind cylinder axis)
ctx.lineWidth = 2.1;
ctx.lineCap = "round";
for (let i = 0; i < N - 1; i++) {
const y0 = toScreenY(y[i]);
const y1 = toScreenY(y[i + 1]);
if (y1 < -40 || y0 > h + 40) continue;
const stretch = Math.max(0.0, (y[i + 1] - y[i]) - L0);
const tensionRatio = (K_OPEN * stretch) / maxEqTension;
const yMid = 0.5 * (y0 + y1);
ctx.strokeStyle = coilColor(tensionRatio, true);
ctx.beginPath();
ctx.moveTo(centerX + coilRadiusPx, y0);
ctx.bezierCurveTo(
centerX + coilRadiusPx * 0.45, y0,
centerX - coilRadiusPx * 0.45, yMid,
centerX - coilRadiusPx, yMid
);
ctx.stroke();
}
// Second pass: front half-turns (in front of cylinder axis) + discrete mass nodes
ctx.lineWidth = 2.5;
for (let i = 0; i < N - 1; i++) {
const y0 = toScreenY(y[i]);
const y1 = toScreenY(y[i + 1]);
if (y1 < -40 || y0 > h + 40) continue;
const stretch = Math.max(0.0, (y[i + 1] - y[i]) - L0);
const tensionRatio = (K_OPEN * stretch) / maxEqTension;
const yMid = 0.5 * (y0 + y1);
ctx.strokeStyle = coilColor(tensionRatio, false);
ctx.beginPath();
ctx.moveTo(centerX - coilRadiusPx, yMid);
ctx.bezierCurveTo(
centerX - coilRadiusPx * 0.45, yMid,
centerX + coilRadiusPx * 0.45, y1,
centerX + coilRadiusPx, y1
);
ctx.stroke();
}
// Top and bottom flat end-rings + discrete segment beads on right edge
for (let i = 0; i < N; i++) {
const sy = toScreenY(y[i]);
if (sy < -30 || sy > h + 30) continue;
if (i === 0 || i === N - 1) {
ctx.strokeStyle = i === N - 1 ? "#fde68a" : "#e2e8f0";
ctx.lineWidth = 2.6;
ctx.beginPath();
ctx.moveTo(centerX - coilRadiusPx, sy);
ctx.lineTo(centerX + coilRadiusPx, sy);
ctx.stroke();
}
// Discrete mass node on right edge so every coupled mass is countable
ctx.fillStyle = i === N - 1 ? "#fbbf24" : "#f8fafc";
ctx.beginPath();
ctx.arc(centerX + coilRadiusPx, sy, i === N - 1 ? 3.1 : 1.9, 0, Math.PI * 2);
ctx.fill();
}
// 5. Optional Center of Mass (COM) indicator
if (showComCheck.checked) {
const comY = computeComY();
const comScreenY = toScreenY(comY);
if (comScreenY >= -20 && comScreenY <= h + 20) {
const comX = centerX - coilRadiusPx - 24;
ctx.strokeStyle = "#38bdf8";
ctx.fillStyle = "#38bdf8";
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.moveTo(comX - 10, comScreenY);
ctx.lineTo(comX + 12, comScreenY);
ctx.stroke();
ctx.beginPath();
ctx.arc(comX, comScreenY, 4.2, 0, Math.PI * 2);
ctx.fill();
ctx.font = "700 10px ui-monospace, SFMono-Regular, Menlo, monospace";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
ctx.fillText("COM", comX - 14, comScreenY);
}
}
// Canvas HUD clock overlay (top-right inside stage)
ctx.save();
ctx.font = "700 13px ui-monospace, SFMono-Regular, Menlo, monospace";
ctx.textAlign = "right";
ctx.textBaseline = "top";
ctx.fillStyle = isReleased ? "#f8fafc" : "#64748b";
const hudMs = (stepCount * DT * 1000.0).toFixed(1);
ctx.fillText(`t = ${hudMs} ms`, w - 16, 14);
ctx.restore();
}
// =========================================================================
// Main animation loop
// =========================================================================
let lastTimestamp = null;
function frame(timestamp) {
if (lastTimestamp === null) lastTimestamp = timestamp;
// Cap wall-clock catchup to 50 ms so tab-switching doesn't jump ahead
const wallDt = Math.min(0.05, Math.max(0.0, (timestamp - lastTimestamp) / 1000.0));
lastTimestamp = timestamp;
if (!isReleased) {
// Integrate live even while hanging at rest to prove equilibrium is genuine
const holdSteps = 20;
for (let k = 0; k < holdSteps; k++) {
stepRK4(DT, false);
}
} else if (!isPaused) {
accumulator += wallDt * speedMultiplier;
while (accumulator >= DT) {
stepRK4(DT, true);
stepCount++;
accumulator -= DT;
}
}
updateTelemetry();
render();
requestAnimationFrame(frame);
}
// Initialize exhibit
resizeCanvas();
setSpeed(0.10);
setHangingEquilibrium();
updateStatusUI();
updateTelemetry();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>
Running the simulation against the marker line and stepping millisecond-by-millisecond at $1/20\times$ speed reproduces the core prediction closely, with three honest differences between the idealized textbook picture and what the discrete 48-mass dynamics actually produce:
+0.0 mm on the displacement readout, while the top coil drops more than $900\text{ mm}$ and the COM indicator tracks $\frac{1}{2}g t^2$ to a tenth of a millimeter). Around $t \approx 238\text{–}242\text{ ms}$—roughly $6\text{–}10\text{ ms}$ earlier than the $248\text{ ms}$ pure-plastic continuum estimate—the shock reaches the bottom and launches coil $47$ sharply past the marker line (reaching $+2.5\text{ mm}$ by $243\text{ ms}$ and $+20\text{ mm}$ by $250\text{ ms}$, moving downward at $\sim 2.5\text{ m/s}$). That slight early arrival is real physics in our equations: because coil collisions at $c_c = 4.2\text{ N}\cdot\text{s/m}$ aren't $100%$ perfectly plastic ($e \approx 0.09$), each colliding coil transfers $(1 + e)$ slightly more forward momentum to the coil below than a perfectly sticky Calkin plug, pushing the shock front marginally faster.