Skip to content

Commit e2dcb22

Browse files
sefllessclaude
andauthored
Add Date.now, setTimeout/setInterval patching and extension handoff (#18)
* Add Date.now, setTimeout/setInterval patching and extension handoff - Add Date.now() patching for Motion/Framer Motion compatibility - Add setTimeout/setInterval delay scaling based on speed - Extension stores original functions for library to use when taking over - Library detects extension and takes over with real originals - Prevent double-patching when both extension and library are present - Add Motion demo card to demo page - Update documentation with new APIs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix build by externalizing Node.js modules Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix demo build by wrapping Motion import in async IIFE Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a5ac09e commit e2dcb22

8 files changed

Lines changed: 602 additions & 44 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ function App() {
9090
| Videos & Audio | `playbackRate` property |
9191
| requestAnimationFrame | Patched timestamps |
9292
| performance.now() | Returns virtual time |
93+
| Date.now() | Returns virtual epoch time |
94+
| setTimeout/setInterval| Scaled delays |
9395
| GSAP | `globalTimeline.timeScale()` (auto-detected) |
9496
| Three.js | Uses rAF, works automatically |
95-
| Framer Motion | Uses Web Animations API, works automatically |
97+
| Framer Motion/Motion | Uses Date.now(), works automatically |
9698
| Canvas animations | Uses rAF, works automatically |
9799

98100
## Animation Recreation (AI-Powered)
@@ -153,10 +155,10 @@ Set your API key via environment variable (`GEMINI_API_KEY`, `OPENAI_API_KEY`, o
153155
## Limitations
154156

155157
- **Frame-based animations** that don't use timestamps can't be smoothly slowed (they increment by a fixed amount each frame regardless of time)
156-
- **Libraries that cache time function references** before slowmo loads may not be affected
158+
- **Libraries that cache time function references** before slowmo loads may not be affected (the Chrome extension runs early to avoid this)
157159
- **Video/audio** have browser-imposed limits (~0.0625x to 16x in Chrome)
158-
- **iframes** won't be affected unless slowmo is also loaded inside them
159-
- **Web Workers & Worklets** run in separate threads with their own timing APIs that can't be patched from the main thread (audio worklets, paint worklets, animation worklets)
160+
- **iframes** won't be affected unless slowmo is also loaded inside them (the extension handles this automatically)
161+
- **Service Workers & Worklets** run in separate threads that can't be patched (audio worklets, paint worklets, animation worklets)
160162
- **WebGL shaders** with custom time uniforms need manual integration
161163
- **Server-synced animations** that rely on server timestamps rather than local time
162164

demo/index.html

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,16 @@
336336

337337
.demos-grid {
338338
display: grid;
339-
grid-template-columns: repeat(4, 1fr);
339+
grid-template-columns: repeat(5, 1fr);
340340
gap: 1rem;
341341
}
342342

343+
@media (max-width: 900px) {
344+
.demos-grid {
345+
grid-template-columns: repeat(3, 1fr);
346+
}
347+
}
348+
343349
@media (max-width: 768px) {
344350
.demos-grid {
345351
grid-template-columns: repeat(2, 1fr);
@@ -421,6 +427,11 @@
421427
background: linear-gradient(135deg, #4ade80 0%, #34d399 100%);
422428
}
423429

430+
.motion-box {
431+
background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%);
432+
cursor: pointer;
433+
}
434+
424435
.demo-visual video {
425436
width: 100%;
426437
height: 100%;
@@ -806,6 +817,16 @@ <h3>Video</h3>
806817
<p>HTML5 video</p>
807818
</div>
808819
</div>
820+
821+
<div class="demo-card">
822+
<div class="demo-visual">
823+
<div class="anim-box motion-box" id="motion-box"></div>
824+
</div>
825+
<div class="demo-label">
826+
<h3>Motion</h3>
827+
<p>Hover to animate</p>
828+
</div>
829+
</div>
809830
</div>
810831
</section>
811832

@@ -948,6 +969,25 @@ <h2>Full API</h2>
948969
easing: "ease-in-out",
949970
},
950971
);
972+
973+
// Motion library demo (hover animation)
974+
const motionBox = document.getElementById("motion-box");
975+
(async () => {
976+
try {
977+
const { animate } = await import("https://cdn.jsdelivr.net/npm/motion@12/+esm");
978+
979+
motionBox.addEventListener("mouseenter", () => {
980+
animate(motionBox, { scale: [null, 1.1, 1.5] }, { duration: 0.5, ease: "easeOut" });
981+
});
982+
983+
motionBox.addEventListener("mouseleave", () => {
984+
animate(motionBox, { scale: 1 }, { duration: 0.3, ease: "easeOut" });
985+
});
986+
} catch (e) {
987+
// Motion library failed to load, skip this demo
988+
console.warn("Motion library not available:", e);
989+
}
990+
})();
951991
</script>
952992
</body>
953993
</html>

extension/content.js

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
(function() {
88
'use strict';
99

10-
// Avoid double injection
10+
// Avoid double injection (extension loaded twice)
1111
if (window.__slowmoExtensionLoaded) return;
1212
window.__slowmoExtensionLoaded = true;
1313

@@ -20,9 +20,16 @@
2020
let isInstalled = false;
2121
let originalRAF;
2222
let originalPerformanceNow;
23+
let originalDateNow;
24+
let originalSetTimeout;
25+
let originalSetInterval;
2326
let virtualTime = 0;
2427
let lastRealTime = 0;
2528
let pauseTime = 0;
29+
// Date.now tracking (epoch milliseconds)
30+
let virtualDateNow = 0;
31+
let lastRealDateNow = 0;
32+
let pauseDateNow = 0;
2633

2734
const trackedAnimations = new WeakMap();
2835
const trackedMedia = new WeakMap();
@@ -35,6 +42,13 @@
3542
return virtualTime + elapsed * effectiveSpeed;
3643
}
3744

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+
3852
function updateWebAnimations() {
3953
if (typeof document.getAnimations !== 'function') return;
4054
const animations = document.getAnimations();
@@ -136,10 +150,49 @@
136150

137151
function install() {
138152
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;
143196

144197
const patchedRAF = (callback) => {
145198
return originalRAF((realTimestamp) => {
@@ -158,6 +211,25 @@
158211
}
159212

160213
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+
161233
originalRAF(pollAnimations);
162234
isInstalled = true;
163235
}
@@ -167,10 +239,16 @@
167239
const realNow = originalPerformanceNow();
168240
virtualTime = getVirtualTime(realNow);
169241
lastRealTime = realNow;
242+
// Checkpoint Date.now
243+
const realDateNowValue = originalDateNow();
244+
virtualDateNow = getVirtualDateNow(realDateNowValue);
245+
lastRealDateNow = realDateNowValue;
246+
170247
currentSpeed = speed;
171248
isPaused = speed === 0;
172249
if (isPaused) {
173250
pauseTime = virtualTime;
251+
pauseDateNow = virtualDateNow;
174252
}
175253
updateWebAnimations();
176254
updateMediaElements();
@@ -601,35 +679,20 @@
601679
// Expose slowmo function globally for programmatic control
602680
// This allows pages and tests to call window.slowmo(0.5)
603681
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);
613683
};
614684

615685
// Also expose helper methods
616686
window.slowmo.getSpeed = function() {
617-
return isTopFrame ? uiSpeed : currentSpeed;
687+
return currentSpeed;
618688
};
619689

620690
window.slowmo.pause = function() {
621691
window.slowmo(0);
622692
};
623693

624694
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);
633696
};
634697

635698
// ============================================

extension/manifest.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
{
2222
"matches": ["<all_urls>"],
2323
"js": ["content.js"],
24-
"run_at": "document_end",
25-
"all_frames": true
24+
"run_at": "document_start",
25+
"all_frames": true,
26+
"world": "MAIN"
2627
}
2728
]
2829
}

specs/SPEC.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ import { Slowmo } from 'slowmo/react';
6767
| Animation Type | How It's Controlled |
6868
|---------------|---------------------|
6969
| CSS Animations | Web Animations API `playbackRate` |
70-
| CSS Transitions | Inject `transition-duration` multiplier |
70+
| CSS Transitions | Web Animations API `playbackRate` |
7171
| Videos/Audio | `playbackRate` property |
72-
| requestAnimationFrame | Monkey-patch to scale time delta |
72+
| requestAnimationFrame | Monkey-patch to scale timestamps |
73+
| performance.now() | Returns virtual time |
74+
| Date.now() | Returns virtual epoch time |
75+
| setTimeout/setInterval | Scaled delays |
7376
| GSAP | `gsap.globalTimeline.timeScale()` (if available) |
7477
| Three.js | Uses rAF, so handled automatically |
75-
| Framer Motion | Uses Web Animations API, handled automatically |
78+
| Framer Motion/Motion | Uses Date.now(), handled automatically |
7679

7780
## Speed Limits
7881

0 commit comments

Comments
 (0)