@opendirectory.dev/skills 0.1.66 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry.json +8 -0
- package/skills/blog-cover-image-cli/README.md +2 -2
- package/skills/brand-alchemy/README.md +2 -2
- package/skills/claude-md-generator/README.md +2 -2
- package/skills/cold-email-verifier/README.md +2 -2
- package/skills/competitor-pr-finder/README.md +2 -2
- package/skills/cook-the-blog/README.md +2 -2
- package/skills/dependency-update-bot/README.md +2 -2
- package/skills/docs-from-code/README.md +2 -2
- package/skills/email-newsletter/README.md +2 -2
- package/skills/explain-this-pr/README.md +2 -2
- package/skills/gh-issue-to-demand-signal/README.md +2 -2
- package/skills/google-trends-api-skills/README.md +2 -2
- package/skills/graphic-case-study/README.md +2 -2
- package/skills/graphic-ebook/README.md +2 -2
- package/skills/graphic-slide-deck/README.md +2 -2
- package/skills/hackernews-intel/README.md +2 -2
- package/skills/human-tone/README.md +2 -2
- package/skills/kill-the-standup/README.md +2 -2
- package/skills/linkedin-job-post-to-buyer-pain-map/README.md +2 -2
- package/skills/linkedin-post-generator/README.md +2 -2
- package/skills/llms-txt-generator/README.md +2 -2
- package/skills/map-your-market/README.md +2 -2
- package/skills/meeting-brief-generator/README.md +2 -2
- package/skills/meta-ads-skill/README.md +2 -2
- package/skills/meta-tribeV2-skill/README.md +2 -2
- package/skills/newsletter-digest/README.md +2 -2
- package/skills/noise2blog/README.md +2 -2
- package/skills/npm-downloads-to-leads/README.md +2 -2
- package/skills/outreach-sequence-builder/README.md +2 -2
- package/skills/position-me/README.md +2 -2
- package/skills/pr-description-writer/README.md +2 -2
- package/skills/pricing-finder/README.md +2 -2
- package/skills/pricing-page-psychology-audit/README.md +2 -2
- package/skills/product-update-logger/README.md +2 -2
- package/skills/producthunt-launch-kit/README.md +2 -2
- package/skills/reddit-icp-monitor/README.md +2 -2
- package/skills/reddit-post-engine/README.md +2 -2
- package/skills/schema-markup-generator/README.md +2 -2
- package/skills/sdk-adoption-tracker/README.md +2 -2
- package/skills/show-hn-writer/README.md +2 -2
- package/skills/tweet-thread-from-blog/README.md +2 -2
- package/skills/twitter-GTM-find-skill/README.md +2 -2
- package/skills/vc-finder/README.md +2 -2
- package/skills/vid-motion-graphics/README.md +2 -2
- package/skills/vid-product-launch/README.md +138 -0
- package/skills/vid-product-launch/SKILL.md +713 -0
- package/skills/vid-product-launch/references/scene-library.md +896 -0
- package/skills/vid-product-launch/references/style-presets.md +180 -0
- package/skills/vid-product-launch/scripts/capture-frames.mjs +187 -0
- package/skills/vid-product-launch/scripts/export-video.sh +495 -0
- package/skills/yc-intent-radar-skill/README.md +2 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Style Presets — vid-product-launch
|
|
2
|
+
|
|
3
|
+
Four tone-matched presets. Read this file before generating any HTML.
|
|
4
|
+
Apply ALL tokens from the chosen preset. No free hex values or font-family strings.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## cinematic
|
|
9
|
+
|
|
10
|
+
**Reference feel:** Apple product reveal. Deliberate, dark, premium.
|
|
11
|
+
|
|
12
|
+
**Font CDN:**
|
|
13
|
+
```html
|
|
14
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
15
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
16
|
+
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=Cormorant:wght@700;800&display=swap" rel="stylesheet">
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**CSS tokens:**
|
|
20
|
+
```css
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #050505;
|
|
23
|
+
--bg-secondary: #0D0D0D;
|
|
24
|
+
--text-primary: #F5F5F5;
|
|
25
|
+
--text-secondary: #999999;
|
|
26
|
+
--accent: #D4AF37;
|
|
27
|
+
--accent-soft: rgba(212,175,55,0.15);
|
|
28
|
+
--divider: rgba(255,255,255,0.08);
|
|
29
|
+
--font-display: 'Cormorant', 'Cormorant Garamond', Georgia, serif;
|
|
30
|
+
--font-body: 'Cormorant Garamond', Georgia, serif;
|
|
31
|
+
--tracking-tight: -0.02em;
|
|
32
|
+
--tracking-wide: 0.12em;
|
|
33
|
+
--product-size: 160px; /* product name on 16:9; use 100px for 9:16 */
|
|
34
|
+
--tagline-size: 36px;
|
|
35
|
+
--stat-size: 220px;
|
|
36
|
+
--body-size: 22px;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Effects:**
|
|
41
|
+
- `film-grain: true` — canvas noise overlay, opacity 0.025
|
|
42
|
+
- `vignette: true` — radial gradient dark edges
|
|
43
|
+
- `letterbox: user-param` — 2.35:1 black bars if enabled
|
|
44
|
+
- `light-leak: true` — warm gold sweep at reveal moment
|
|
45
|
+
- Text reveal: slow materialise (`blur 8px→0, opacity 0→1, 700ms`)
|
|
46
|
+
- Tease text: word-by-word, 200ms stagger per word, all-caps, letter-spacing 0.12em
|
|
47
|
+
|
|
48
|
+
**Background treatment:** Pure `#050505`. No gradients on background.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## energetic
|
|
53
|
+
|
|
54
|
+
**Reference feel:** Product Hunt launch day. Fast, bold, electric.
|
|
55
|
+
|
|
56
|
+
**Font CDN:**
|
|
57
|
+
```html
|
|
58
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
59
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
60
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@700;900&display=swap" rel="stylesheet">
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**CSS tokens:**
|
|
64
|
+
```css
|
|
65
|
+
:root {
|
|
66
|
+
--bg: #000000;
|
|
67
|
+
--bg-secondary: #0A0A0A;
|
|
68
|
+
--text-primary: #FFFFFF;
|
|
69
|
+
--text-secondary: #888888;
|
|
70
|
+
--accent: #0066FF;
|
|
71
|
+
--accent-soft: rgba(0,102,255,0.15);
|
|
72
|
+
--divider: rgba(255,255,255,0.12);
|
|
73
|
+
--font-display: 'DM Sans', system-ui, sans-serif;
|
|
74
|
+
--font-body: 'DM Mono', 'Fira Code', monospace;
|
|
75
|
+
--tracking-tight: -0.03em;
|
|
76
|
+
--tracking-wide: 0.08em;
|
|
77
|
+
--product-size: 180px;
|
|
78
|
+
--tagline-size: 32px;
|
|
79
|
+
--stat-size: 240px;
|
|
80
|
+
--body-size: 20px;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Effects:**
|
|
85
|
+
- `film-grain: false`
|
|
86
|
+
- `vignette: false`
|
|
87
|
+
- `letterbox: false`
|
|
88
|
+
- `light-leak: false` — instead: white flash (`opacity 0→1→0, 80ms`) at reveal
|
|
89
|
+
- Text reveal: slam-in (`scale 1.12→1, opacity 0→1, 120ms`)
|
|
90
|
+
- Tease text: full block appears, no stagger
|
|
91
|
+
|
|
92
|
+
**Background treatment:** Hard black. Optional: 1px accent-color horizontal rule as scene separator element.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## minimal
|
|
97
|
+
|
|
98
|
+
**Reference feel:** Linear / Vercel announcement. Refined, spacious, confident.
|
|
99
|
+
|
|
100
|
+
**Font CDN:**
|
|
101
|
+
```html
|
|
102
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
103
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
104
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**CSS tokens:**
|
|
108
|
+
```css
|
|
109
|
+
:root {
|
|
110
|
+
--bg: #FFFFFF;
|
|
111
|
+
--bg-secondary: #FAFAFA;
|
|
112
|
+
--text-primary: #0A0A0A;
|
|
113
|
+
--text-secondary: #666666;
|
|
114
|
+
--accent: #0A0A0A;
|
|
115
|
+
--accent-soft: rgba(10,10,10,0.06);
|
|
116
|
+
--divider: rgba(0,0,0,0.08);
|
|
117
|
+
--font-display: 'Inter', system-ui, sans-serif;
|
|
118
|
+
--font-body: 'Inter', system-ui, sans-serif;
|
|
119
|
+
--tracking-tight: -0.04em;
|
|
120
|
+
--tracking-wide: 0.06em;
|
|
121
|
+
--product-size: 140px;
|
|
122
|
+
--tagline-size: 30px;
|
|
123
|
+
--stat-size: 200px;
|
|
124
|
+
--body-size: 20px;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Effects:**
|
|
129
|
+
- `film-grain: false`
|
|
130
|
+
- `vignette: false`
|
|
131
|
+
- `letterbox: false`
|
|
132
|
+
- `light-leak: false`
|
|
133
|
+
- Text reveal: typewriter (one character at a time, 40ms per char) OR gentle fade (`opacity 0→1, 400ms`)
|
|
134
|
+
- Tease text: gentle fade-up (`translateY 16px→0, opacity 0→1, 500ms`)
|
|
135
|
+
|
|
136
|
+
**Background treatment:** Pure white. Extremely fine `1px solid var(--divider)` borders on proof elements allowed.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## emotional
|
|
141
|
+
|
|
142
|
+
**Reference feel:** Kickstarter campaign. Warm, human, mission-driven.
|
|
143
|
+
|
|
144
|
+
**Font CDN:**
|
|
145
|
+
```html
|
|
146
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
147
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
148
|
+
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Playfair+Display+SC:wght@400;700&display=swap" rel="stylesheet">
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**CSS tokens:**
|
|
152
|
+
```css
|
|
153
|
+
:root {
|
|
154
|
+
--bg: #FAF7F2;
|
|
155
|
+
--bg-secondary: #F3EFE8;
|
|
156
|
+
--text-primary: #2C1810;
|
|
157
|
+
--text-secondary: #7A6355;
|
|
158
|
+
--accent: #B87333;
|
|
159
|
+
--accent-soft: rgba(184,115,51,0.12);
|
|
160
|
+
--divider: rgba(44,24,16,0.10);
|
|
161
|
+
--font-display: 'Playfair Display', 'Playfair Display SC', Georgia, serif;
|
|
162
|
+
--font-body: 'Playfair Display', Georgia, serif;
|
|
163
|
+
--tracking-tight: -0.01em;
|
|
164
|
+
--tracking-wide: 0.10em;
|
|
165
|
+
--product-size: 120px;
|
|
166
|
+
--tagline-size: 34px;
|
|
167
|
+
--stat-size: 180px;
|
|
168
|
+
--body-size: 22px;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Effects:**
|
|
173
|
+
- `film-grain: true` — canvas noise overlay, opacity 0.018, warm tint (add rgba(180,120,60,0.03) blend)
|
|
174
|
+
- `vignette: true` — soft radial gradient, opacity 0.4
|
|
175
|
+
- `letterbox: false`
|
|
176
|
+
- `light-leak: true` — warm copper sweep at reveal moment
|
|
177
|
+
- Text reveal: word-by-word (`opacity 0→1, translateY 12px→0, 300ms per word, 150ms stagger`)
|
|
178
|
+
- Tease text: slow fade per line, 400ms each
|
|
179
|
+
|
|
180
|
+
**Background treatment:** Warm ivory. Subtle texture suggestion via CSS `background-image: radial-gradient(circle at 20% 80%, rgba(184,115,51,0.04) 0%, transparent 60%)`.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// capture-frames.mjs — Capture HTML motion graphic as PNG frame sequence
|
|
2
|
+
//
|
|
3
|
+
// Args: <serve-dir> <html-filename> <frames-dir> <width> <height> <duration-seconds> <fps>
|
|
4
|
+
//
|
|
5
|
+
// Architecture: JS-driven renderFrame (NOT CSS @keyframes seeking)
|
|
6
|
+
//
|
|
7
|
+
// The HTML exposes window.renderFrame(timeMs) — a pure JS function that computes
|
|
8
|
+
// element styles (opacity, transform) directly from the time value. Playwright calls
|
|
9
|
+
// this once per frame. No CSS animation state, no WAAPI seeking, no timing races.
|
|
10
|
+
//
|
|
11
|
+
// Why NOT CSS @keyframes + currentTime:
|
|
12
|
+
// Chromium silently ignores anim.currentTime backward seeks on CSS animations.
|
|
13
|
+
// The animation "sticks" at whatever time it was when we injected pause CSS.
|
|
14
|
+
// For long animations (12s) with networkidle wait (8-12s for Google Fonts CDN),
|
|
15
|
+
// the animation finishes BEFORE we can pause it → all frames capture final state.
|
|
16
|
+
//
|
|
17
|
+
// The renderFrame approach:
|
|
18
|
+
// - Works for any duration, any seek direction
|
|
19
|
+
// - Fully deterministic — same input → same output
|
|
20
|
+
// - Browser preview also works (rAF loop in the HTML calls renderFrame)
|
|
21
|
+
// - No race conditions with font loading or network timing
|
|
22
|
+
|
|
23
|
+
import { chromium } from 'playwright';
|
|
24
|
+
import { createServer } from 'http';
|
|
25
|
+
import { readFileSync, statSync } from 'fs';
|
|
26
|
+
import { mkdir } from 'fs/promises';
|
|
27
|
+
import { join, extname } from 'path';
|
|
28
|
+
|
|
29
|
+
const SERVE_DIR = process.argv[2];
|
|
30
|
+
const HTML_FILE = process.argv[3];
|
|
31
|
+
const FRAMES_DIR = process.argv[4];
|
|
32
|
+
const VP_WIDTH = parseInt(process.argv[5]) || 1080;
|
|
33
|
+
const VP_HEIGHT = parseInt(process.argv[6]) || 1080;
|
|
34
|
+
const DURATION_S = parseFloat(process.argv[7]) || 9;
|
|
35
|
+
const FPS = parseInt(process.argv[8]) || 30;
|
|
36
|
+
|
|
37
|
+
const DURATION_MS = DURATION_S * 1000;
|
|
38
|
+
const TOTAL_FRAMES = Math.round(DURATION_S * FPS);
|
|
39
|
+
|
|
40
|
+
// ─── Static file server ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const MIME_TYPES = {
|
|
43
|
+
'.html': 'text/html',
|
|
44
|
+
'.css': 'text/css',
|
|
45
|
+
'.js': 'application/javascript',
|
|
46
|
+
'.json': 'application/json',
|
|
47
|
+
'.png': 'image/png',
|
|
48
|
+
'.jpg': 'image/jpeg',
|
|
49
|
+
'.jpeg': 'image/jpeg',
|
|
50
|
+
'.svg': 'image/svg+xml',
|
|
51
|
+
'.woff': 'font/woff',
|
|
52
|
+
'.woff2':'font/woff2',
|
|
53
|
+
'.ttf': 'font/ttf',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const server = createServer((req, res) => {
|
|
57
|
+
const decoded = decodeURIComponent(req.url);
|
|
58
|
+
const filePath = join(SERVE_DIR, decoded === '/' ? HTML_FILE : decoded);
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(filePath);
|
|
61
|
+
const ext = extname(filePath).toLowerCase();
|
|
62
|
+
res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
|
|
63
|
+
res.end(content);
|
|
64
|
+
} catch {
|
|
65
|
+
res.writeHead(404);
|
|
66
|
+
res.end('Not found');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const port = await new Promise((resolve) => {
|
|
71
|
+
server.listen(0, () => resolve(server.address().port));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(` Local server on port ${port}`);
|
|
75
|
+
|
|
76
|
+
// ─── Launch browser ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const browser = await chromium.launch({
|
|
79
|
+
args: [
|
|
80
|
+
'--no-sandbox',
|
|
81
|
+
'--disable-dev-shm-usage',
|
|
82
|
+
'--font-render-hinting=none',
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const context = await browser.newContext({
|
|
87
|
+
viewport: { width: VP_WIDTH, height: VP_HEIGHT },
|
|
88
|
+
deviceScaleFactor: 2,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const page = await context.newPage();
|
|
92
|
+
|
|
93
|
+
const pageErrors = [];
|
|
94
|
+
page.on('console', msg => { if (msg.type() === 'error') pageErrors.push(msg.text()); });
|
|
95
|
+
page.on('pageerror', err => pageErrors.push(err.message));
|
|
96
|
+
|
|
97
|
+
// ─── Navigate and wait for ready ─────────────────────────────────────────────
|
|
98
|
+
// networkidle is safe now — no CSS animation timing race.
|
|
99
|
+
// renderFrame(t) is pure JS math; it doesn't care how long the page took to load.
|
|
100
|
+
|
|
101
|
+
await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle' });
|
|
102
|
+
await page.evaluate(() => document.fonts.ready);
|
|
103
|
+
|
|
104
|
+
console.log(' Waiting for window.__videoReady...');
|
|
105
|
+
try {
|
|
106
|
+
await page.waitForFunction(() => window.__videoReady === true, { timeout: 15000 });
|
|
107
|
+
} catch {
|
|
108
|
+
const bodyHTML = await page.evaluate(() => document.body.innerHTML.substring(0, 500));
|
|
109
|
+
console.error(' ERROR: window.__videoReady was never set after 15s.');
|
|
110
|
+
if (pageErrors.length > 0) {
|
|
111
|
+
console.error(' Browser console errors:');
|
|
112
|
+
pageErrors.forEach(e => console.error(' ', e));
|
|
113
|
+
}
|
|
114
|
+
console.error(' Ensure your HTML contains:');
|
|
115
|
+
console.error(' window.__videoReady = false;');
|
|
116
|
+
console.error(' document.fonts.ready.then(() => {');
|
|
117
|
+
console.error(' window.renderFrame(0);');
|
|
118
|
+
console.error(' window.__videoReady = true;');
|
|
119
|
+
console.error(' });');
|
|
120
|
+
console.error(' Page body preview:', bodyHTML);
|
|
121
|
+
await browser.close();
|
|
122
|
+
server.close();
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Verify renderFrame exists
|
|
127
|
+
const hasRenderFrame = await page.evaluate(() => typeof window.renderFrame === 'function');
|
|
128
|
+
if (!hasRenderFrame) {
|
|
129
|
+
console.error(' ERROR: window.renderFrame is not a function.');
|
|
130
|
+
console.error(' The HTML must expose window.renderFrame(timeMs) for frame-accurate capture.');
|
|
131
|
+
console.error(' See SKILL.md Step 3 for the required HTML structure.');
|
|
132
|
+
await browser.close();
|
|
133
|
+
server.close();
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(' window.renderFrame confirmed — ready to capture');
|
|
138
|
+
|
|
139
|
+
// Stop browser preview rAF loop — it races with Playwright's evaluate/screenshot:
|
|
140
|
+
// between renderFrame(t) and screenshot(), rAF fires renderFrame(elapsed) where
|
|
141
|
+
// elapsed >> t, overwriting the correct frame state.
|
|
142
|
+
await page.evaluate(() => {
|
|
143
|
+
if (typeof window.__stopPreview === 'function') window.__stopPreview();
|
|
144
|
+
});
|
|
145
|
+
await page.waitForTimeout(100); // drain any in-flight rAF before capture
|
|
146
|
+
|
|
147
|
+
// ─── Capture frames ───────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
await mkdir(FRAMES_DIR, { recursive: true });
|
|
150
|
+
|
|
151
|
+
console.log(` Capturing ${TOTAL_FRAMES} frames (${DURATION_S}s @ ${FPS}fps)...`);
|
|
152
|
+
|
|
153
|
+
for (let f = 0; f < TOTAL_FRAMES; f++) {
|
|
154
|
+
const ms = (f / FPS) * 1000;
|
|
155
|
+
|
|
156
|
+
// Call renderFrame with the exact timestamp for this frame.
|
|
157
|
+
// The function directly sets element styles — no CSS animation state involved.
|
|
158
|
+
// Force synchronous style recalculation so screenshot captures the updated frame.
|
|
159
|
+
await page.evaluate((t) => {
|
|
160
|
+
window.renderFrame(t);
|
|
161
|
+
void document.body.offsetHeight; // Trigger synchronous reflow
|
|
162
|
+
}, ms);
|
|
163
|
+
|
|
164
|
+
// One rAF cycle for GPU compositing
|
|
165
|
+
await page.waitForTimeout(16);
|
|
166
|
+
|
|
167
|
+
const padded = String(f + 1).padStart(4, '0');
|
|
168
|
+
await page.screenshot({
|
|
169
|
+
path: join(FRAMES_DIR, `frame_${padded}.png`),
|
|
170
|
+
animations: 'disabled',
|
|
171
|
+
clip: { x: 0, y: 0, width: VP_WIDTH, height: VP_HEIGHT },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if ((f + 1) % FPS === 0 || f === TOTAL_FRAMES - 1) {
|
|
175
|
+
process.stdout.write(`\r Captured ${f + 1}/${TOTAL_FRAMES} frames`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
await browser.close();
|
|
182
|
+
server.close();
|
|
183
|
+
|
|
184
|
+
const firstFrame = join(FRAMES_DIR, 'frame_0001.png');
|
|
185
|
+
const frameSizeKB = Math.round(statSync(firstFrame).size / 1024);
|
|
186
|
+
console.log(` ✓ ${TOTAL_FRAMES} frames saved to ${FRAMES_DIR}`);
|
|
187
|
+
console.log(` Frame size: ${frameSizeKB}KB each (${VP_WIDTH * 2}×${VP_HEIGHT * 2}px retina)`);
|