pellicule 0.0.1 → 0.0.2

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/bin/cli.js ADDED
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { parseArgs } from 'node:util'
4
+ import { resolve, extname, basename, dirname } from 'node:path'
5
+ import { existsSync, readFileSync } from 'node:fs'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { renderToMp4 } from '../src/render.js'
8
+
9
+ // Read version from package.json
10
+ const __dirname = dirname(fileURLToPath(import.meta.url))
11
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'))
12
+ const VERSION = pkg.version
13
+
14
+ // ANSI color codes (no dependencies needed)
15
+ const colors = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ red: '\x1b[31m',
20
+ yellow: '\x1b[33m',
21
+ cyan: '\x1b[36m',
22
+ white: '\x1b[37m',
23
+ pellicule: '\x1b[38;2;66;184;131m', // #42b883 - Vue green
24
+ bgPellicule: '\x1b[48;2;66;184;131m'
25
+ }
26
+
27
+ const c = {
28
+ error: (s) => `${colors.red}${s}${colors.reset}`,
29
+ warn: (s) => `${colors.yellow}${s}${colors.reset}`,
30
+ info: (s) => `${colors.cyan}${s}${colors.reset}`,
31
+ dim: (s) => `${colors.dim}${s}${colors.reset}`,
32
+ bold: (s) => `${colors.bold}${s}${colors.reset}`,
33
+ highlight: (s) => `${colors.pellicule}${s}${colors.reset}`,
34
+ brand: (s) => `${colors.bgPellicule}${colors.white}${colors.bold}${s}${colors.reset}`
35
+ }
36
+
37
+ function fail(msg, hint) {
38
+ console.error(c.error(`\nError: ${msg}\n`))
39
+ if (hint) console.error(c.dim(` ${hint}\n`))
40
+ process.exit(1)
41
+ }
42
+
43
+ const HELP = `
44
+ ${c.bold('pellicule')} ${c.dim(`v${VERSION}`)} - Render Vue components to video
45
+
46
+ ${c.bold('USAGE')}
47
+ ${c.highlight('pellicule')} ${c.dim('→ renders Video.vue to output.mp4')}
48
+ ${c.highlight('pellicule')} <input.vue> ${c.dim('→ custom input file')}
49
+ ${c.highlight('pellicule')} <input.vue> -o <file> ${c.dim('→ custom output path')}
50
+
51
+ ${c.bold('OPTIONS')}
52
+ ${c.info('-o, --output')} <file> Output file path ${c.dim('(default: ./output.mp4)')}
53
+ ${c.info('-d, --duration')} <frames> Duration in frames ${c.dim('(default: 90)')}
54
+ ${c.info('-f, --fps')} <number> Frames per second ${c.dim('(default: 30)')}
55
+ ${c.info('-w, --width')} <pixels> Video width ${c.dim('(default: 1920)')}
56
+ ${c.info('-h, --height')} <pixels> Video height ${c.dim('(default: 1080)')}
57
+ ${c.info('-r, --range')} <start:end> Frame range for partial render ${c.dim('(e.g., 100:200)')}
58
+ ${c.info('--help')} Show this help message
59
+ ${c.info('--version')} Show version number
60
+
61
+ ${c.bold('EXAMPLES')}
62
+ ${c.dim('# Zero-config (renders Video.vue → output.mp4)')}
63
+ ${c.highlight('pellicule')}
64
+
65
+ ${c.dim('# Specify input file (.vue extension is optional)')}
66
+ ${c.highlight('pellicule')} MyVideo
67
+
68
+ ${c.dim('# Custom output and duration')}
69
+ ${c.highlight('pellicule')} Video.vue -o intro.mp4 -d 150
70
+
71
+ ${c.dim('# 4K video at 60fps')}
72
+ ${c.highlight('pellicule')} Video.vue -w 3840 -h 2160 -f 60
73
+
74
+ ${c.dim('# 10 second video')}
75
+ ${c.highlight('pellicule')} Video.vue -d 300 -f 30
76
+
77
+ ${c.dim('# Render only frames 100-200 (for faster iteration)')}
78
+ ${c.highlight('pellicule')} Video.vue -d 300 -r 100:200
79
+
80
+ ${c.dim('# Render from frame 150 to 250')}
81
+ ${c.highlight('pellicule')} Video.vue -d 300 --range 150:250
82
+
83
+ ${c.bold('DURATION HELPER')}
84
+ frames = seconds * fps
85
+ ${c.dim('3 seconds at 30fps = 90 frames')}
86
+ ${c.dim('5 seconds at 30fps = 150 frames')}
87
+ ${c.dim('10 seconds at 60fps = 600 frames')}
88
+
89
+ ${c.dim('Documentation: https://docs.sailscasts.com/pellicule')}
90
+ `
91
+
92
+ function printBanner() {
93
+ console.log()
94
+ console.log(` ${c.brand(' PELLICULE ')} ${c.dim(`v${VERSION}`)}`)
95
+ console.log()
96
+ }
97
+
98
+ function formatTime(ms) {
99
+ if (ms < 1000) return `${ms}ms`
100
+ const seconds = (ms / 1000).toFixed(1)
101
+ return `${seconds}s`
102
+ }
103
+
104
+ function formatProgress(frame, total, fps) {
105
+ const percent = Math.round(((frame + 1) / total) * 100)
106
+ const barWidth = 30
107
+ const filled = Math.round((percent / 100) * barWidth)
108
+ const empty = barWidth - filled
109
+ const bar = c.highlight('█'.repeat(filled)) + c.dim('░'.repeat(empty))
110
+ return ` ${bar} ${c.bold(percent + '%')} ${c.dim(`(${frame + 1}/${total} @ ${fps.toFixed(1)} fps)`)}`
111
+ }
112
+
113
+ async function main() {
114
+ const { values, positionals } = parseArgs({
115
+ allowPositionals: true,
116
+ options: {
117
+ output: { type: 'string', short: 'o' },
118
+ duration: { type: 'string', short: 'd' },
119
+ fps: { type: 'string', short: 'f' },
120
+ width: { type: 'string', short: 'w' },
121
+ height: { type: 'string', short: 'h' },
122
+ range: { type: 'string', short: 'r' },
123
+ help: { type: 'boolean' },
124
+ version: { type: 'boolean' }
125
+ }
126
+ })
127
+
128
+ // Handle help and version
129
+ if (values.help) {
130
+ console.log(HELP)
131
+ process.exit(0)
132
+ }
133
+
134
+ if (values.version) {
135
+ console.log(VERSION)
136
+ process.exit(0)
137
+ }
138
+
139
+ // Default to Video.vue if no input provided
140
+ const input = positionals[0] || 'Video.vue'
141
+
142
+ // Try to resolve the input file, auto-appending .vue if needed
143
+ let inputPath = resolve(input)
144
+
145
+ if (!existsSync(inputPath) && !input.endsWith('.vue')) {
146
+ const withVue = resolve(input + '.vue')
147
+ if (existsSync(withVue)) {
148
+ inputPath = withVue
149
+ }
150
+ }
151
+
152
+ if (!existsSync(inputPath)) fail(`File not found: ${input}`)
153
+ if (extname(inputPath) !== '.vue') fail(`Input must be a .vue file, got: ${extname(inputPath) || '(no extension)'}`)
154
+
155
+ // Parse options with defaults
156
+ const fps = parseInt(values.fps || '30', 10)
157
+ const durationInFrames = parseInt(values.duration || '90', 10)
158
+ const width = parseInt(values.width || '1920', 10)
159
+ const height = parseInt(values.height || '1080', 10)
160
+ const output = values.output || './output.mp4'
161
+ const outputPath = resolve(output)
162
+
163
+ // Parse optional range (start:end format)
164
+ let startFrame = 0
165
+ let endFrame = durationInFrames
166
+
167
+ if (values.range) {
168
+ const rangeParts = values.range.split(':')
169
+ if (rangeParts.length !== 2) fail(`Invalid range format: ${values.range}`, 'Expected format: start:end (e.g., 100:200)')
170
+ const [startStr, endStr] = rangeParts
171
+ startFrame = parseInt(startStr, 10)
172
+ endFrame = parseInt(endStr, 10)
173
+ if (isNaN(startFrame) || startFrame < 0) fail(`Invalid start frame in range: ${startStr}`)
174
+ if (isNaN(endFrame) || endFrame <= 0) fail(`Invalid end frame in range: ${endStr}`)
175
+ }
176
+
177
+ // Validate options
178
+ if (isNaN(fps) || fps <= 0) fail(`Invalid fps value: ${values.fps}`)
179
+ if (isNaN(durationInFrames) || durationInFrames <= 0) fail(`Invalid duration value: ${values.duration}`)
180
+ if (isNaN(width) || width <= 0) fail(`Invalid width value: ${values.width}`)
181
+ if (isNaN(height) || height <= 0) fail(`Invalid height value: ${values.height}`)
182
+ if (startFrame >= endFrame) fail(`Start frame (${startFrame}) must be less than end frame (${endFrame})`)
183
+ if (endFrame > durationInFrames) fail(`End frame (${endFrame}) exceeds duration (${durationInFrames})`)
184
+
185
+ // Print banner and config
186
+ printBanner()
187
+
188
+ const isPartialRender = startFrame > 0 || endFrame < durationInFrames
189
+ const framesToRender = endFrame - startFrame
190
+ const durationSeconds = (durationInFrames / fps).toFixed(1)
191
+ const partialSeconds = (framesToRender / fps).toFixed(1)
192
+
193
+ console.log(` ${c.bold('Input')} ${c.info(basename(inputPath))}`)
194
+ console.log(` ${c.bold('Output')} ${c.info(basename(outputPath))}`)
195
+ console.log(` ${c.bold('Resolution')} ${width}x${height}`)
196
+ console.log(` ${c.bold('Duration')} ${durationInFrames} frames @ ${fps}fps ${c.dim(`(${durationSeconds}s)`)}`)
197
+ if (isPartialRender) {
198
+ console.log(` ${c.bold('Range')} ${c.highlight(`frames ${startFrame}-${endFrame - 1}`)} ${c.dim(`(${framesToRender} frames, ${partialSeconds}s)`)}`)
199
+ }
200
+ console.log()
201
+
202
+ const startTime = Date.now()
203
+
204
+ // ANSI escape codes for line control
205
+ const clearLine = '\x1b[2K' // Clear entire line
206
+ const cursorToStart = '\r' // Move cursor to start of line
207
+
208
+ try {
209
+ // Render with progress callback
210
+ await renderToMp4({
211
+ input: inputPath,
212
+ fps,
213
+ durationInFrames,
214
+ startFrame,
215
+ endFrame,
216
+ width,
217
+ height,
218
+ output: outputPath,
219
+ silent: true,
220
+ onProgress: ({ frame, total, fps: currentFps }) => {
221
+ // Clear line and print progress (stays on same line)
222
+ process.stdout.write(clearLine + cursorToStart + formatProgress(frame, total, currentFps || fps))
223
+ }
224
+ })
225
+
226
+ // Clear progress line when done
227
+ process.stdout.write(clearLine + cursorToStart)
228
+
229
+ const totalTime = Date.now() - startTime
230
+ console.log()
231
+ console.log(` ${c.highlight('Done!')} Rendered ${framesToRender} frames in ${formatTime(totalTime)}`)
232
+ console.log(` ${c.dim('Output:')} ${outputPath}`)
233
+ console.log()
234
+
235
+ } catch (error) {
236
+ // Clear progress line on error
237
+ process.stdout.write(clearLine + cursorToStart)
238
+ console.error()
239
+ console.error(c.error(` Error: ${error.message}`))
240
+ console.error()
241
+
242
+ if (error.message.includes('ffmpeg')) {
243
+ console.error(c.warn(' Hint: Make sure FFmpeg is installed and available in your PATH'))
244
+ console.error(c.dim(' Install: https://ffmpeg.org/download.html'))
245
+ console.error()
246
+ }
247
+
248
+ process.exit(1)
249
+ }
250
+ }
251
+
252
+ main().catch((error) => {
253
+ console.error(c.error(`\nUnexpected error: ${error.message}\n`))
254
+ process.exit(1)
255
+ })
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "pellicule",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Deterministic video rendering with Vue",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
+ "bin": {
8
+ "pellicule": "./bin/cli.js"
9
+ },
7
10
  "exports": {
8
11
  ".": "./src/index.js",
9
12
  "./render": "./src/render.js"
@@ -0,0 +1,34 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import { useFrame } from '../composables/frame.js'
4
+ import { provideSequence } from '../composables/sequence.js'
5
+
6
+ const props = defineProps({
7
+ /**
8
+ * The frame number where this sequence starts
9
+ */
10
+ from: {
11
+ type: Number,
12
+ required: true
13
+ },
14
+ /**
15
+ * Duration of this sequence in frames
16
+ */
17
+ durationInFrames: {
18
+ type: Number,
19
+ required: true
20
+ }
21
+ })
22
+
23
+ const globalFrame = useFrame()
24
+
25
+ // Provide sequence context to children
26
+ const { isActive } = provideSequence(props.from, props.durationInFrames)
27
+
28
+ // Compute whether to show content
29
+ const shouldRender = computed(() => isActive.value)
30
+ </script>
31
+
32
+ <template>
33
+ <slot v-if="shouldRender" />
34
+ </template>
@@ -0,0 +1,24 @@
1
+ import { inject, computed } from 'vue'
2
+ import { FRAME_KEY } from './keys.js'
3
+
4
+ /**
5
+ * Get the current frame number.
6
+ * Must be used within a Pellicule render context.
7
+ *
8
+ * @returns {import('vue').ComputedRef<number>} Current frame number
9
+ *
10
+ * @example
11
+ * const frame = useFrame()
12
+ * const opacity = computed(() => frame.value / 30) // fade in over 1 second at 30fps
13
+ */
14
+ export function useFrame() {
15
+ const frame = inject(FRAME_KEY)
16
+
17
+ if (frame === undefined) {
18
+ throw new Error(
19
+ 'useFrame() must be used within a Pellicule render context'
20
+ )
21
+ }
22
+
23
+ return computed(() => frame.value)
24
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Injection keys for Pellicule context
3
+ * Using Symbol.for() so keys are shared across module instances
4
+ */
5
+ export const FRAME_KEY = Symbol.for('pellicule-frame')
6
+ export const CONFIG_KEY = Symbol.for('pellicule-config')
7
+ export const SEQUENCE_KEY = Symbol.for('pellicule-sequence')
@@ -0,0 +1,82 @@
1
+ import { inject, computed, provide } from 'vue'
2
+ import { SEQUENCE_KEY } from './keys.js'
3
+ import { useFrame } from './frame.js'
4
+
5
+ /**
6
+ * Get sequence timing information.
7
+ * Can be used in two ways:
8
+ *
9
+ * 1. Inside a <Sequence> component - returns timing for that sequence
10
+ * 2. With explicit timing - useSequence(from, durationInFrames)
11
+ *
12
+ * @param {number} [from] - Start frame (optional if inside <Sequence>)
13
+ * @param {number} [durationInFrames] - Duration in frames (optional if inside <Sequence>)
14
+ * @returns {{ localFrame: ComputedRef<number>, progress: ComputedRef<number>, isActive: ComputedRef<boolean> }}
15
+ *
16
+ * @example
17
+ * // Inside a <Sequence> component
18
+ * const { localFrame, progress, isActive } = useSequence()
19
+ *
20
+ * @example
21
+ * // With explicit timing
22
+ * const verse1 = useSequence(0, 90)
23
+ * const verse2 = useSequence(90, 90)
24
+ */
25
+ export function useSequence(from, durationInFrames) {
26
+ const globalFrame = useFrame()
27
+
28
+ // If arguments provided, use them directly
29
+ if (from !== undefined && durationInFrames !== undefined) {
30
+ const end = from + durationInFrames
31
+
32
+ const localFrame = computed(() => Math.max(0, globalFrame.value - from))
33
+ const progress = computed(() => {
34
+ if (globalFrame.value < from) return 0
35
+ if (globalFrame.value >= end) return 1
36
+ return (globalFrame.value - from) / durationInFrames
37
+ })
38
+ const isActive = computed(() =>
39
+ globalFrame.value >= from && globalFrame.value < end
40
+ )
41
+
42
+ return { localFrame, progress, isActive }
43
+ }
44
+
45
+ // Otherwise, try to get from parent <Sequence>
46
+ const sequenceContext = inject(SEQUENCE_KEY, null)
47
+
48
+ if (!sequenceContext) {
49
+ throw new Error(
50
+ 'useSequence() must be used within a <Sequence> component or called with (from, durationInFrames) arguments'
51
+ )
52
+ }
53
+
54
+ return sequenceContext
55
+ }
56
+
57
+ /**
58
+ * Provide sequence context to children.
59
+ * Used internally by the <Sequence> component.
60
+ *
61
+ * @param {number} from - Start frame
62
+ * @param {number} durationInFrames - Duration in frames
63
+ */
64
+ export function provideSequence(from, durationInFrames) {
65
+ const globalFrame = useFrame()
66
+ const end = from + durationInFrames
67
+
68
+ const localFrame = computed(() => Math.max(0, globalFrame.value - from))
69
+ const progress = computed(() => {
70
+ if (globalFrame.value < from) return 0
71
+ if (globalFrame.value >= end) return 1
72
+ return (globalFrame.value - from) / durationInFrames
73
+ })
74
+ const isActive = computed(() =>
75
+ globalFrame.value >= from && globalFrame.value < end
76
+ )
77
+
78
+ const context = { localFrame, progress, isActive, from, durationInFrames }
79
+ provide(SEQUENCE_KEY, context)
80
+
81
+ return context
82
+ }
@@ -0,0 +1,23 @@
1
+ import { inject } from 'vue'
2
+ import { CONFIG_KEY } from './keys.js'
3
+
4
+ /**
5
+ * Get the video configuration (fps, duration, dimensions).
6
+ * Must be used within a Pellicule render context.
7
+ *
8
+ * @returns {{ fps: number, durationInFrames: number, width: number, height: number }}
9
+ *
10
+ * @example
11
+ * const { fps, durationInFrames, width, height } = useVideoConfig()
12
+ */
13
+ export function useVideoConfig() {
14
+ const config = inject(CONFIG_KEY)
15
+
16
+ if (!config) {
17
+ throw new Error(
18
+ 'useVideoConfig() must be used within a Pellicule render context'
19
+ )
20
+ }
21
+
22
+ return config
23
+ }
package/src/index.js CHANGED
@@ -6,7 +6,13 @@
6
6
  */
7
7
 
8
8
  // Composables
9
- export { useFrame, useVideoConfig, FRAME_KEY, CONFIG_KEY } from './composables.js'
9
+ export { useFrame } from './composables/frame.js'
10
+ export { useVideoConfig } from './composables/video-config.js'
11
+ export { useSequence } from './composables/sequence.js'
12
+ export { FRAME_KEY, CONFIG_KEY, SEQUENCE_KEY } from './composables/keys.js'
13
+
14
+ // Components
15
+ export { default as Sequence } from './components/Sequence.vue'
10
16
 
11
17
  // Animation utilities
12
- export { interpolate, sequence, Easing } from './math.js'
18
+ export { interpolate, sequence, Easing } from './utils/math.js'
@@ -9,7 +9,9 @@ import { join } from 'path'
9
9
  * @param {object} options
10
10
  * @param {string} options.input - Path to the .vue file
11
11
  * @param {number} options.fps - Frames per second (default: 30)
12
- * @param {number} options.durationInFrames - Total frames to render
12
+ * @param {number} options.durationInFrames - Total frames in the video (for animation calculations)
13
+ * @param {number} options.startFrame - First frame to render (default: 0)
14
+ * @param {number} options.endFrame - Last frame to render, exclusive (default: durationInFrames)
13
15
  * @param {number} options.width - Video width in pixels (default: 1920)
14
16
  * @param {number} options.height - Video height in pixels (default: 1080)
15
17
  * @param {string} options.outputDir - Directory for frame images (default: './frames')
@@ -21,6 +23,8 @@ export async function renderVideo(options) {
21
23
  input,
22
24
  fps = 30,
23
25
  durationInFrames,
26
+ startFrame = 0,
27
+ endFrame,
24
28
  width = 1920,
25
29
  height = 1080,
26
30
  outputDir = './frames',
@@ -28,6 +32,10 @@ export async function renderVideo(options) {
28
32
  silent = false
29
33
  } = options
30
34
 
35
+ // Calculate actual frame range to render
36
+ const actualEndFrame = endFrame !== undefined ? endFrame : durationInFrames
37
+ const framesToRender = actualEndFrame - startFrame
38
+
31
39
  const log = silent ? () => {} : console.log.bind(console)
32
40
 
33
41
  const startTime = Date.now()
@@ -62,10 +70,13 @@ export async function renderVideo(options) {
62
70
  }
63
71
  })
64
72
 
65
- log(`Rendering ${durationInFrames} frames at ${fps}fps (${width}x${height})`)
73
+ const rangeInfo = startFrame > 0 || actualEndFrame < durationInFrames
74
+ ? ` (frames ${startFrame}-${actualEndFrame - 1})`
75
+ : ''
76
+ log(`Rendering ${framesToRender} frames at ${fps}fps (${width}x${height})${rangeInfo}`)
66
77
 
67
78
  try {
68
- // Load page ONCE with config
79
+ // Load page ONCE with config (durationInFrames stays full for correct animation calculations)
69
80
  const pageUrl = `${url}?fps=${fps}&duration=${durationInFrames}&width=${width}&height=${height}`
70
81
  await page.goto(pageUrl, { waitUntil: 'networkidle' })
71
82
 
@@ -80,33 +91,36 @@ export async function renderVideo(options) {
80
91
 
81
92
  const renderStart = Date.now()
82
93
 
83
- // Render each frame by updating the frame ref (no page reload!)
84
- for (let frame = 0; frame < durationInFrames; frame++) {
94
+ // Render each frame in the specified range
95
+ for (let frame = startFrame; frame < actualEndFrame; frame++) {
85
96
  // Update frame number - Vue reactivity handles re-render
86
97
  await page.evaluate((f) => window.__PELLICULE_SET_FRAME__(f), frame)
87
98
 
88
- // Screenshot
89
- const framePath = join(outputDir, `frame-${String(frame).padStart(5, '0')}.png`)
99
+ // Screenshot - output frames are numbered from 0
100
+ const outputFrameNum = frame - startFrame
101
+ const framePath = join(outputDir, `frame-${String(outputFrameNum).padStart(5, '0')}.png`)
90
102
  await page.screenshot({ path: framePath })
91
103
 
92
104
  // Progress callback
93
105
  if (onProgress) {
94
106
  const elapsed = Date.now() - renderStart
95
- const currentFps = (frame + 1) / (elapsed / 1000)
96
- onProgress({ frame, total: durationInFrames, fps: currentFps })
107
+ const framesRendered = outputFrameNum + 1
108
+ const currentFps = framesRendered / (elapsed / 1000)
109
+ onProgress({ frame: outputFrameNum, total: framesToRender, fps: currentFps })
97
110
  }
98
111
 
99
112
  // Log progress every 10 frames
100
- if (frame % 10 === 0 || frame === durationInFrames - 1) {
101
- const percent = Math.round(((frame + 1) / durationInFrames) * 100)
113
+ const outputFrameIndex = frame - startFrame
114
+ if (outputFrameIndex % 10 === 0 || frame === actualEndFrame - 1) {
115
+ const percent = Math.round(((outputFrameIndex + 1) / framesToRender) * 100)
102
116
  const elapsed = Date.now() - renderStart
103
- const framesPerSec = ((frame + 1) / (elapsed / 1000)).toFixed(1)
104
- log(`Frame ${frame + 1}/${durationInFrames} (${percent}%) - ${framesPerSec} fps`)
117
+ const framesPerSec = ((outputFrameIndex + 1) / (elapsed / 1000)).toFixed(1)
118
+ log(`Frame ${outputFrameIndex + 1}/${framesToRender} (${percent}%) - ${framesPerSec} fps`)
105
119
  }
106
120
  }
107
121
 
108
122
  const renderTime = Date.now() - renderStart
109
- log(`Rendered ${durationInFrames} frames in ${renderTime}ms (${(durationInFrames / (renderTime / 1000)).toFixed(1)} fps)`)
123
+ log(`Rendered ${framesToRender} frames in ${renderTime}ms (${(framesToRender / (renderTime / 1000)).toFixed(1)} fps)`)
110
124
 
111
125
  } finally {
112
126
  await browser.close()
@@ -116,5 +130,5 @@ export async function renderVideo(options) {
116
130
  log(`Total time: ${Date.now() - startTime}ms`)
117
131
  log(`Frames saved to ${outputDir}`)
118
132
 
119
- return { framesDir: outputDir, totalFrames: durationInFrames }
133
+ return { framesDir: outputDir, totalFrames: framesToRender }
120
134
  }
@@ -1,51 +0,0 @@
1
- import { inject, computed } from 'vue'
2
-
3
- /**
4
- * Injection keys for Pellicule context
5
- * Using Symbol.for() so keys are shared across module instances
6
- */
7
- export const FRAME_KEY = Symbol.for('pellicule-frame')
8
- export const CONFIG_KEY = Symbol.for('pellicule-config')
9
-
10
- /**
11
- * Get the current frame number.
12
- * Must be used within a Pellicule render context.
13
- *
14
- * @returns {import('vue').ComputedRef<number>} Current frame number
15
- *
16
- * @example
17
- * const frame = useFrame()
18
- * const opacity = computed(() => frame.value / 30) // fade in over 1 second at 30fps
19
- */
20
- export function useFrame() {
21
- const frame = inject(FRAME_KEY)
22
-
23
- if (frame === undefined) {
24
- throw new Error(
25
- 'useFrame() must be used within a Pellicule render context'
26
- )
27
- }
28
-
29
- return computed(() => frame.value)
30
- }
31
-
32
- /**
33
- * Get the video configuration (fps, duration, dimensions).
34
- * Must be used within a Pellicule render context.
35
- *
36
- * @returns {{ fps: number, durationInFrames: number, width: number, height: number }}
37
- *
38
- * @example
39
- * const { fps, durationInFrames, width, height } = useVideoConfig()
40
- */
41
- export function useVideoConfig() {
42
- const config = inject(CONFIG_KEY)
43
-
44
- if (!config) {
45
- throw new Error(
46
- 'useVideoConfig() must be used within a Pellicule render context'
47
- )
48
- }
49
-
50
- return config
51
- }
File without changes