-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcapture.mjs
More file actions
153 lines (129 loc) · 4.67 KB
/
Copy pathcapture.mjs
File metadata and controls
153 lines (129 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* Capture sky wallpaper animation as a video.
*
* Usage:
* node capture.mjs # 5-second test at half res
* node capture.mjs --full # 60-second full capture at native res
*
* Requires: npm install puppeteer, brew install ffmpeg
* Also requires a local server: python3 -m http.server 8080
*/
import puppeteer from 'puppeteer';
import { execSync, spawn } from 'child_process';
import { mkdirSync, readdirSync, rmSync } from 'fs';
import { join } from 'path';
const isFullCapture = process.argv.includes('--full');
// Config
const WIDTH = isFullCapture ? 3024 : 1512;
const HEIGHT = isFullCapture ? 1964 : 982;
const FPS = 75;
const DURATION = isFullCapture ? 60 : 5; // seconds
const TOTAL_FRAMES = FPS * DURATION;
const FRAME_INTERVAL = 1000 / FPS; // ms per frame
const URL = 'http://localhost:8080/index.html';
const FRAME_DIR = join(import.meta.dirname, 'capture-frames');
const OUTPUT = join(import.meta.dirname, isFullCapture ? 'sky-loop.mp4' : 'test-capture.mp4');
console.log(`Capturing ${DURATION}s at ${WIDTH}x${HEIGHT} @ ${FPS}fps (${TOTAL_FRAMES} frames)`);
console.log(`Output: ${OUTPUT}`);
// Clean/create frame directory
rmSync(FRAME_DIR, { recursive: true, force: true });
mkdirSync(FRAME_DIR, { recursive: true });
const browser = await puppeteer.launch({
headless: true,
args: [
`--window-size=${WIDTH},${HEIGHT}`,
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-gpu-sandbox',
'--use-gl=angle',
'--use-angle=metal',
],
});
const page = await browser.newPage();
await page.setViewport({ width: WIDTH, height: HEIGHT, deviceScaleFactor: 1 });
// Override time functions BEFORE page loads so animation runs on virtual clock
await page.evaluateOnNewDocument(() => {
let virtualTime = 0;
window.__virtualTime = 0;
window.__captureReady = false;
// Override performance.now
const origPerfNow = performance.now.bind(performance);
performance.now = () => window.__virtualTime;
// Override Date.now
const origDateNow = Date.now;
const startDate = origDateNow.call(Date);
Date.now = () => startDate + window.__virtualTime;
// Override requestAnimationFrame to be manually steppable
const rafCallbacks = [];
window.__originalRAF = window.requestAnimationFrame;
window.requestAnimationFrame = (cb) => {
rafCallbacks.push(cb);
return rafCallbacks.length;
};
// Step function: advance virtual time and fire RAF callbacks
window.__stepFrame = (dt) => {
window.__virtualTime += dt;
const cbs = rafCallbacks.splice(0);
for (const cb of cbs) {
cb(window.__virtualTime);
}
};
});
console.log('Loading page...');
await page.goto(URL, { waitUntil: 'networkidle0', timeout: 30000 });
// Wait for skyReady flag
console.log('Waiting for animation to initialize...');
await page.waitForFunction('window.skyReady === true', { timeout: 15000 });
console.log('Animation ready!');
// Pump a few warmup frames to get everything settled
for (let i = 0; i < 10; i++) {
await page.evaluate((dt) => window.__stepFrame(dt), FRAME_INTERVAL);
}
// Reset virtual time to 0 for clean loop start
await page.evaluate(() => {
window.__virtualTime = 0;
});
console.log('Starting capture...');
const startTime = Date.now();
for (let frame = 0; frame < TOTAL_FRAMES; frame++) {
// Step the virtual clock forward by one frame
await page.evaluate((dt) => window.__stepFrame(dt), FRAME_INTERVAL);
// Capture screenshot
const padded = String(frame).padStart(6, '0');
await page.screenshot({
path: join(FRAME_DIR, `frame_${padded}.png`),
type: 'png',
});
// Progress
if (frame % 30 === 0) {
const pct = ((frame / TOTAL_FRAMES) * 100).toFixed(1);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(` Frame ${frame}/${TOTAL_FRAMES} (${pct}%) — ${elapsed}s elapsed`);
}
}
const captureTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`Captured ${TOTAL_FRAMES} frames in ${captureTime}s`);
await browser.close();
// Stitch frames into video with ffmpeg
console.log('Encoding video with ffmpeg...');
try {
execSync(
`ffmpeg -y -framerate ${FPS} -i "${FRAME_DIR}/frame_%06d.png" ` +
`-c:v hevc_videotoolbox -q:v 50 -tag:v hvc1 -pix_fmt yuv420p ` +
`"${OUTPUT}"`,
{ stdio: 'inherit' }
);
console.log(`Video saved to: ${OUTPUT}`);
} catch (e) {
console.error('ffmpeg encoding failed, trying H.264 fallback...');
execSync(
`ffmpeg -y -framerate ${FPS} -i "${FRAME_DIR}/frame_%06d.png" ` +
`-c:v libx264 -crf 18 -pix_fmt yuv420p ` +
`"${OUTPUT}"`,
{ stdio: 'inherit' }
);
console.log(`Video saved to: ${OUTPUT}`);
}
// Cleanup frames
rmSync(FRAME_DIR, { recursive: true, force: true });
console.log('Done! Frames cleaned up.');