|
55 | 55 | const GRAVITY = 0.15; |
56 | 56 | let gravityOn = true; |
57 | 57 | let unraveling = false; |
| 58 | + let unravelIdx = -1; |
58 | 59 | window.addEventListener('keydown', (e) => { |
59 | 60 | if (e.key === 'f' || e.key === 'F') { |
60 | 61 | gravityOn = !gravityOn; |
61 | 62 | if (gravityOn && !unraveling) { |
62 | 63 | unraveling = true; |
63 | 64 | hint.style.opacity = '0'; |
64 | | - // Unlock letters from the tail end, one every few frames |
65 | | - let nextUnlock = letters.length - 1; |
66 | | - // Find first still-locked from the tail |
67 | | - while (nextUnlock >= 0 && !letters[nextUnlock].locked) nextUnlock--; |
68 | | - function unravelStep() { |
69 | | - if (!gravityOn || nextUnlock < 0) { unraveling = false; return; } |
70 | | - for (let j = 0; j < 1 && nextUnlock >= 0; j++) { |
71 | | - if (letters[nextUnlock].locked) { |
72 | | - letters[nextUnlock].locked = false; |
73 | | - letters[nextUnlock].px = letters[nextUnlock].x; |
74 | | - letters[nextUnlock].py = letters[nextUnlock].y - 0.5; |
75 | | - } |
76 | | - nextUnlock--; |
77 | | - } |
78 | | - requestAnimationFrame(unravelStep); |
79 | | - } |
80 | | - unravelStep(); |
| 65 | + unravelIdx = letters.length - 1; |
| 66 | + while (unravelIdx >= 0 && !letters[unravelIdx].locked) unravelIdx--; |
81 | 67 | } |
82 | 68 | } |
83 | 69 | }); |
|
309 | 295 |
|
310 | 296 | // Physics |
311 | 297 | function simulate() { |
| 298 | + // Unravel step (one letter per fixed tick) |
| 299 | + if (unraveling) { |
| 300 | + if (!gravityOn || unravelIdx < 0) { unraveling = false; } |
| 301 | + else if (letters[unravelIdx].locked) { |
| 302 | + letters[unravelIdx].locked = false; |
| 303 | + letters[unravelIdx].px = letters[unravelIdx].x; |
| 304 | + letters[unravelIdx].py = letters[unravelIdx].y - 0.5; |
| 305 | + unravelIdx--; |
| 306 | + } else { |
| 307 | + unravelIdx--; |
| 308 | + } |
| 309 | + } |
| 310 | + |
312 | 311 | // Unlock propagation |
313 | 312 | for (let i = letters.length - 2; i >= 0; i--) { |
314 | 313 | if (letters[i].locked && !letters[i + 1].locked) { |
|
402 | 401 | } |
403 | 402 | } |
404 | 403 |
|
405 | | - function render() { |
406 | | - simulate(); |
| 404 | + // Fixed-timestep loop: simulate at 60Hz regardless of display refresh rate |
| 405 | + const FIXED_DT = 1 / 60; |
| 406 | + const MAX_STEPS = 4; // cap to avoid spiral of death |
| 407 | + let accumulator = 0; |
| 408 | + let lastTime = -1; |
| 409 | + |
| 410 | + function render(now) { |
| 411 | + if (lastTime < 0) { lastTime = now; requestAnimationFrame(render); return; } |
| 412 | + const dt = Math.min((now - lastTime) / 1000, MAX_STEPS * FIXED_DT); |
| 413 | + lastTime = now; |
| 414 | + accumulator += dt; |
| 415 | + |
| 416 | + while (accumulator >= FIXED_DT) { |
| 417 | + simulate(); |
| 418 | + accumulator -= FIXED_DT; |
| 419 | + } |
| 420 | + |
407 | 421 | for (let i = 0; i < letters.length; i++) { |
408 | 422 | if (!letters[i].locked) els[i].classList.add('draggable'); |
409 | 423 | els[i].style.transform = `translate(${letters[i].x}px, ${letters[i].y}px)`; |
|
0 commit comments