|
7 | 7 | (function() { |
8 | 8 | 'use strict'; |
9 | 9 |
|
10 | | - // Avoid double injection |
| 10 | + // Avoid double injection (extension loaded twice) |
11 | 11 | if (window.__slowmoExtensionLoaded) return; |
12 | 12 | window.__slowmoExtensionLoaded = true; |
13 | 13 |
|
|
20 | 20 | let isInstalled = false; |
21 | 21 | let originalRAF; |
22 | 22 | let originalPerformanceNow; |
| 23 | + let originalDateNow; |
| 24 | + let originalSetTimeout; |
| 25 | + let originalSetInterval; |
23 | 26 | let virtualTime = 0; |
24 | 27 | let lastRealTime = 0; |
25 | 28 | let pauseTime = 0; |
| 29 | + // Date.now tracking (epoch milliseconds) |
| 30 | + let virtualDateNow = 0; |
| 31 | + let lastRealDateNow = 0; |
| 32 | + let pauseDateNow = 0; |
26 | 33 |
|
27 | 34 | const trackedAnimations = new WeakMap(); |
28 | 35 | const trackedMedia = new WeakMap(); |
|
35 | 42 | return virtualTime + elapsed * effectiveSpeed; |
36 | 43 | } |
37 | 44 |
|
| 45 | + function getVirtualDateNow(realDateNow) { |
| 46 | + if (isPaused) return pauseDateNow; |
| 47 | + const elapsed = realDateNow - lastRealDateNow; |
| 48 | + const effectiveSpeed = currentSpeed === Infinity ? 1000 : currentSpeed; |
| 49 | + return virtualDateNow + elapsed * effectiveSpeed; |
| 50 | + } |
| 51 | + |
38 | 52 | function updateWebAnimations() { |
39 | 53 | if (typeof document.getAnimations !== 'function') return; |
40 | 54 | const animations = document.getAnimations(); |
|
136 | 150 |
|
137 | 151 | function install() { |
138 | 152 | if (isInstalled || typeof window === 'undefined') return; |
139 | | - originalRAF = window.requestAnimationFrame.bind(window); |
140 | | - originalPerformanceNow = performance.now.bind(performance); |
141 | | - lastRealTime = originalPerformanceNow(); |
142 | | - virtualTime = lastRealTime; |
| 153 | + |
| 154 | + // Capture the REAL original functions before any patching |
| 155 | + if (!originalRAF) { |
| 156 | + originalRAF = window.requestAnimationFrame.bind(window); |
| 157 | + } |
| 158 | + if (!originalPerformanceNow) { |
| 159 | + originalPerformanceNow = performance.now.bind(performance); |
| 160 | + } |
| 161 | + if (!originalDateNow) { |
| 162 | + originalDateNow = Date.now.bind(Date); |
| 163 | + } |
| 164 | + |
| 165 | + // Store originals globally so embedded library can use them if it loads later |
| 166 | + // This allows the library to take over with the real functions (not our patches) |
| 167 | + window.__slowmoOriginals = { |
| 168 | + requestAnimationFrame: originalRAF, |
| 169 | + performanceNow: originalPerformanceNow, |
| 170 | + dateNow: originalDateNow, |
| 171 | + setTimeout: window.setTimeout.bind(window), |
| 172 | + setInterval: window.setInterval.bind(window), |
| 173 | + }; |
| 174 | + |
| 175 | + // Mark that extension is present (library will check this) |
| 176 | + window.__slowmoExtension = true; |
| 177 | + |
| 178 | + // Initialize virtual time to current real time (if not already set) |
| 179 | + if (lastRealTime === 0) { |
| 180 | + lastRealTime = originalPerformanceNow(); |
| 181 | + virtualTime = lastRealTime; |
| 182 | + } |
| 183 | + |
| 184 | + // Initialize Date.now tracking (if not already set) |
| 185 | + if (lastRealDateNow === 0) { |
| 186 | + lastRealDateNow = originalDateNow(); |
| 187 | + virtualDateNow = lastRealDateNow; |
| 188 | + } |
| 189 | + |
| 190 | + // Check if already installed globally (another extension instance) |
| 191 | + if (window.__slowmoInstalled) { |
| 192 | + isInstalled = true; |
| 193 | + return; |
| 194 | + } |
| 195 | + window.__slowmoInstalled = true; |
143 | 196 |
|
144 | 197 | const patchedRAF = (callback) => { |
145 | 198 | return originalRAF((realTimestamp) => { |
|
158 | 211 | } |
159 | 212 |
|
160 | 213 | performance.now = () => getVirtualTime(originalPerformanceNow()); |
| 214 | + // Patch Date.now for libraries like Motion/Framer Motion |
| 215 | + Date.now = () => getVirtualDateNow(originalDateNow()); |
| 216 | + |
| 217 | + // Patch setTimeout/setInterval - scale delays by inverse of speed |
| 218 | + originalSetTimeout = window.setTimeout.bind(window); |
| 219 | + originalSetInterval = window.setInterval.bind(window); |
| 220 | + |
| 221 | + window.setTimeout = (callback, delay, ...args) => { |
| 222 | + const effectiveSpeed = currentSpeed || 0.0001; |
| 223 | + const scaledDelay = (delay ?? 0) / effectiveSpeed; |
| 224 | + return originalSetTimeout(callback, scaledDelay, ...args); |
| 225 | + }; |
| 226 | + |
| 227 | + window.setInterval = (callback, delay, ...args) => { |
| 228 | + const effectiveSpeed = currentSpeed || 0.0001; |
| 229 | + const scaledDelay = (delay ?? 0) / effectiveSpeed; |
| 230 | + return originalSetInterval(callback, scaledDelay, ...args); |
| 231 | + }; |
| 232 | + |
161 | 233 | originalRAF(pollAnimations); |
162 | 234 | isInstalled = true; |
163 | 235 | } |
|
167 | 239 | const realNow = originalPerformanceNow(); |
168 | 240 | virtualTime = getVirtualTime(realNow); |
169 | 241 | lastRealTime = realNow; |
| 242 | + // Checkpoint Date.now |
| 243 | + const realDateNowValue = originalDateNow(); |
| 244 | + virtualDateNow = getVirtualDateNow(realDateNowValue); |
| 245 | + lastRealDateNow = realDateNowValue; |
| 246 | + |
170 | 247 | currentSpeed = speed; |
171 | 248 | isPaused = speed === 0; |
172 | 249 | if (isPaused) { |
173 | 250 | pauseTime = virtualTime; |
| 251 | + pauseDateNow = virtualDateNow; |
174 | 252 | } |
175 | 253 | updateWebAnimations(); |
176 | 254 | updateMediaElements(); |
|
601 | 679 | // Expose slowmo function globally for programmatic control |
602 | 680 | // This allows pages and tests to call window.slowmo(0.5) |
603 | 681 | window.slowmo = function(speed) { |
604 | | - if (isTopFrame) { |
605 | | - uiSpeed = speed; |
606 | | - uiPaused = speed === 0; |
607 | | - setSpeed(speed); |
608 | | - saveState(); |
609 | | - updateUI(); |
610 | | - } else { |
611 | | - setSpeed(speed); |
612 | | - } |
| 682 | + setSpeed(speed); |
613 | 683 | }; |
614 | 684 |
|
615 | 685 | // Also expose helper methods |
616 | 686 | window.slowmo.getSpeed = function() { |
617 | | - return isTopFrame ? uiSpeed : currentSpeed; |
| 687 | + return currentSpeed; |
618 | 688 | }; |
619 | 689 |
|
620 | 690 | window.slowmo.pause = function() { |
621 | 691 | window.slowmo(0); |
622 | 692 | }; |
623 | 693 |
|
624 | 694 | window.slowmo.play = function() { |
625 | | - if (isTopFrame) { |
626 | | - uiPaused = false; |
627 | | - setSpeed(uiSpeed || 1); |
628 | | - saveState(); |
629 | | - updateUI(); |
630 | | - } else { |
631 | | - setSpeed(currentSpeed || 1); |
632 | | - } |
| 695 | + setSpeed(currentSpeed || 1); |
633 | 696 | }; |
634 | 697 |
|
635 | 698 | // ============================================ |
|
0 commit comments