vueseq 0.1.0 → 0.2.0

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Frame Renderer
3
- *
3
+ *
4
4
  * The core rendering loop that captures each frame using Playwright.
5
5
  * For each frame:
6
6
  * 1. Seek GSAP globalTimeline to the exact time
@@ -12,96 +12,166 @@ import { chromium } from 'playwright'
12
12
  import { createVideoServer } from '../bundler/vite.js'
13
13
  import { mkdir } from 'fs/promises'
14
14
  import { join } from 'path'
15
+ import { getOptimalChromiumConfig } from './gpu.js'
16
+
17
+ // GPU configuration is now handled by the gpu.js module
18
+ // which auto-detects the best backend for the current system
19
+
20
+ /**
21
+ * Get the timeline duration from a Vue component
22
+ * @param {Object} options
23
+ * @param {string} options.input - Absolute path to the Video.vue component
24
+ * @param {number} [options.width=1920] - Video width in pixels
25
+ * @param {number} [options.height=1080] - Video height in pixels
26
+ * @returns {Promise<number|null>} Duration in seconds, or null if not detectable
27
+ */
28
+ export async function getTimelineDuration(options) {
29
+ const { input, width = 1920, height = 1080 } = options
30
+
31
+ const { url, cleanup } = await createVideoServer({ input, width, height })
32
+
33
+ // Get optimal GPU configuration for this system
34
+ const gpuConfig = await getOptimalChromiumConfig()
35
+
36
+ const launchOptions = {
37
+ headless: gpuConfig.headless,
38
+ args: gpuConfig.args,
39
+ }
40
+ if (gpuConfig.channel) {
41
+ launchOptions.channel = gpuConfig.channel
42
+ }
43
+ const browser = await chromium.launch(launchOptions)
44
+ const context = await browser.newContext({
45
+ viewport: { width, height },
46
+ deviceScaleFactor: 1,
47
+ })
48
+ const page = await context.newPage()
49
+
50
+ try {
51
+ await page.goto(url, { waitUntil: 'networkidle' })
52
+ await page.waitForFunction(() => window.__VUESEQ_READY__ === true, {
53
+ timeout: 30000,
54
+ })
55
+ // Give Vue/GSAP a moment to set up timelines
56
+ await page.waitForTimeout(100)
57
+
58
+ const duration = await page.evaluate(() =>
59
+ window.__VUESEQ_GET_DURATION__?.(),
60
+ )
61
+ return duration
62
+ } finally {
63
+ await browser.close()
64
+ await cleanup()
65
+ }
66
+ }
15
67
 
16
68
  /**
17
69
  * Render frames from a Vue component
18
70
  * @param {Object} options
19
71
  * @param {string} options.input - Absolute path to the Video.vue component
20
72
  * @param {number} [options.fps=30] - Frames per second
21
- * @param {number} options.duration - Duration in seconds
73
+ * @param {number} [options.duration] - Duration in seconds (auto-detected if not provided)
22
74
  * @param {number} [options.width=1920] - Video width in pixels
23
75
  * @param {number} [options.height=1080] - Video height in pixels
24
76
  * @param {function} [options.onProgress] - Progress callback
25
77
  * @returns {Promise<{framesDir: string, totalFrames: number, cleanup: () => Promise<void>}>}
26
78
  */
27
79
  export async function renderFrames(options) {
28
- const {
29
- input,
30
- fps = 30,
31
- duration,
32
- width = 1920,
33
- height = 1080,
34
- onProgress
35
- } = options
36
-
80
+ let {
81
+ input,
82
+ fps = 30,
83
+ duration,
84
+ width = 1920,
85
+ height = 1080,
86
+ onProgress,
87
+ } = options
88
+
89
+ // Auto-detect duration if not provided
90
+ if (!duration) {
91
+ duration = await getTimelineDuration({ input, width, height })
37
92
  if (!duration || duration <= 0) {
38
- throw new Error('Duration must be a positive number (in seconds)')
93
+ throw new Error(
94
+ 'Could not auto-detect duration. Specify -d/--duration manually.',
95
+ )
39
96
  }
40
-
41
- const totalFrames = Math.ceil(duration * fps)
42
-
43
- // Start Vite server
44
- const { url, tempDir, cleanup } = await createVideoServer({ input, width, height })
45
-
46
- const framesDir = join(tempDir, 'frames')
47
- await mkdir(framesDir, { recursive: true })
48
-
49
- // Launch headless browser
50
- const browser = await chromium.launch({
51
- headless: true
52
- })
53
-
54
- const context = await browser.newContext({
55
- viewport: { width, height },
56
- deviceScaleFactor: 1
97
+ }
98
+
99
+ const totalFrames = Math.ceil(duration * fps)
100
+
101
+ // Start Vite server
102
+ const { url, tempDir, cleanup } = await createVideoServer({
103
+ input,
104
+ width,
105
+ height,
106
+ })
107
+
108
+ const framesDir = join(tempDir, 'frames')
109
+ await mkdir(framesDir, { recursive: true })
110
+
111
+ // Launch headless browser with optimal GPU config
112
+ const gpuConfig = await getOptimalChromiumConfig()
113
+ const launchOptions2 = {
114
+ headless: gpuConfig.headless,
115
+ args: gpuConfig.args,
116
+ }
117
+ if (gpuConfig.channel) {
118
+ launchOptions2.channel = gpuConfig.channel
119
+ }
120
+ const browser = await chromium.launch(launchOptions2)
121
+
122
+ const context = await browser.newContext({
123
+ viewport: { width, height },
124
+ deviceScaleFactor: 1,
125
+ })
126
+
127
+ const page = await context.newPage()
128
+
129
+ try {
130
+ // Load the page
131
+ await page.goto(url, { waitUntil: 'networkidle' })
132
+
133
+ // Wait for VueSeq bridge to be ready
134
+ await page.waitForFunction(() => window.__VUESEQ_READY__ === true, {
135
+ timeout: 30000,
57
136
  })
58
137
 
59
- const page = await context.newPage()
60
-
61
- try {
62
- // Load the page
63
- await page.goto(url, { waitUntil: 'networkidle' })
64
-
65
- // Wait for VueSeq bridge to be ready
66
- await page.waitForFunction(
67
- () => window.__VUESEQ_READY__ === true,
68
- { timeout: 30000 }
69
- )
70
-
71
- // Give Vue a moment to mount and GSAP to set up timelines
72
- await page.waitForTimeout(100)
73
-
74
- // Render each frame
75
- for (let frame = 0; frame < totalFrames; frame++) {
76
- const timeInSeconds = frame / fps
77
-
78
- // Seek GSAP to exact time and wait for paint
79
- await page.evaluate(async (t) => {
80
- window.__VUESEQ_SEEK__(t)
81
- // Wait for next animation frame to ensure DOM is painted
82
- await new Promise(resolve => requestAnimationFrame(resolve))
83
- }, timeInSeconds)
84
-
85
- // Take screenshot
86
- const framePath = join(framesDir, `frame-${String(frame).padStart(5, '0')}.png`)
87
- await page.screenshot({
88
- path: framePath,
89
- type: 'png'
90
- })
91
-
92
- // Progress callback
93
- if (onProgress) {
94
- onProgress({
95
- frame,
96
- total: totalFrames,
97
- timeInSeconds,
98
- percent: Math.round((frame + 1) / totalFrames * 100)
99
- })
100
- }
101
- }
102
- } finally {
103
- await browser.close()
138
+ // Give Vue a moment to mount and GSAP to set up timelines
139
+ await page.waitForTimeout(100)
140
+
141
+ // Render each frame
142
+ for (let frame = 0; frame < totalFrames; frame++) {
143
+ const timeInSeconds = frame / fps
144
+
145
+ // Seek GSAP to exact time and wait for paint
146
+ await page.evaluate(async (t) => {
147
+ window.__VUESEQ_SEEK__(t)
148
+ // Wait for next animation frame to ensure DOM is painted
149
+ await new Promise((resolve) => requestAnimationFrame(resolve))
150
+ }, timeInSeconds)
151
+
152
+ // Take screenshot
153
+ const framePath = join(
154
+ framesDir,
155
+ `frame-${String(frame).padStart(5, '0')}.png`,
156
+ )
157
+ await page.screenshot({
158
+ path: framePath,
159
+ type: 'png',
160
+ })
161
+
162
+ // Progress callback
163
+ if (onProgress) {
164
+ onProgress({
165
+ frame,
166
+ total: totalFrames,
167
+ timeInSeconds,
168
+ percent: Math.round(((frame + 1) / totalFrames) * 100),
169
+ })
170
+ }
104
171
  }
172
+ } finally {
173
+ await browser.close()
174
+ }
105
175
 
106
- return { framesDir, totalFrames, cleanup }
176
+ return { framesDir, totalFrames, cleanup }
107
177
  }
@@ -35,7 +35,17 @@ window.__VUESEQ_SET_CONFIG__ = (config) => {
35
35
  window.__VUESEQ_CONFIG__ = config
36
36
  }
37
37
 
38
- // 6. Mark as ready after a microtask to ensure Vue is mounted
38
+ // 6. Expose timeline duration for auto-detection
39
+ window.__VUESEQ_GET_DURATION__ = () => {
40
+ const duration = gsap.globalTimeline.duration()
41
+ // Return null for infinite timelines (repeat: -1)
42
+ if (duration === Infinity || duration > 3600) {
43
+ return null
44
+ }
45
+ return duration
46
+ }
47
+
48
+ // 7. Mark as ready after a microtask to ensure Vue is mounted
39
49
  queueMicrotask(() => {
40
50
  window.__VUESEQ_READY__ = true
41
51
  })