@sansavision/create-vidra-app 0.1.20-alpha.0 → 0.1.21-alpha.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sansavision/create-vidra-app",
3
- "version": "0.1.20-alpha.0",
3
+ "version": "0.1.21-alpha.0",
4
4
  "description": "Scaffold a new Vidra video project — SDK, Player, Web Capture ready to go",
5
5
  "bin": {
6
6
  "create-vidra-app": "./index.js"
@@ -1,9 +1,8 @@
1
1
  import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
- import { Play, Pause, RotateCcw, Eye, Zap } from 'lucide-react';
3
- import { VidraEngine } from '@sansavision/vidra-player';
2
+ import { Play, Pause, RotateCcw, Eye, Zap, ArrowRight, Camera } from 'lucide-react';
4
3
 
5
4
  const ANIMATION_DURATION = 3.0; // seconds — total animation length
6
- const FPS = 30;
5
+ const CAPTURE_FPS = 30;
7
6
 
8
7
  interface BarData {
9
8
  label: string;
@@ -11,18 +10,24 @@ interface BarData {
11
10
  color: string;
12
11
  }
13
12
 
14
- /** Animated bar chart React component — the "source" side of the web scene demo. */
15
- function AnimatedBarChart({ time, data }: { time: number; data: BarData[] }) {
16
- const maxVal = Math.max(...data.map(d => d.value));
13
+ /**
14
+ * Animated bar chart React component the "source" web scene.
15
+ * Used in both live and captured views — content is pixel-identical.
16
+ */
17
+ function AnimatedBarChart({ time, data, label }: { time: number; data: BarData[]; label?: string }) {
18
+ const maxVal = Math.max(...data.map((d: BarData) => d.value));
17
19
 
18
20
  return (
19
21
  <div className="w-full bg-gradient-to-br from-[#0f172a] to-[#1e293b] rounded-2xl p-6 border border-white/10 shadow-xl shadow-black/30">
20
22
  <h3 className="text-lg font-bold text-white mb-1 text-center">Revenue by Quarter</h3>
21
- <p className="text-[11px] text-blue-300/50 text-center mb-6">
22
- React component + <code className="bg-white/5 px-1 py-0.5 rounded text-[10px]">requestAnimationFrame</code>
23
- </p>
23
+ {label && (
24
+ <p className="text-[11px] text-blue-300/50 text-center mb-6">
25
+ {label}
26
+ </p>
27
+ )}
28
+ {!label && <div className="h-6" />}
24
29
  <div className="flex items-end justify-center gap-4 h-44">
25
- {data.map((d, i) => {
30
+ {data.map((d: BarData, i: number) => {
26
31
  const delay = i * 0.15;
27
32
  const t = Math.max(0, Math.min((time - delay) / 0.8, 1));
28
33
  const eased = 1 - Math.pow(1 - t, 3);
@@ -47,17 +52,29 @@ function AnimatedBarChart({ time, data }: { time: number; data: BarData[] }) {
47
52
  );
48
53
  }
49
54
 
50
- /** The WebSceneMode — side-by-side: React component (live) + WASM-rendered output. */
55
+ /**
56
+ * WebSceneMode — demonstrates Vidra's web capture pipeline.
57
+ *
58
+ * Live Mode: Full-size React component animating at 60fps.
59
+ * Side-by-Side: React component at 60fps (left) vs the same component
60
+ * rendered at discrete 30fps frame steps (right).
61
+ *
62
+ * The "captured" side shows the exact same component but with time
63
+ * quantized to 30fps frames — this accurately demonstrates how Vidra's
64
+ * web capture pipeline works: it snapshots each frame at discrete
65
+ * intervals, producing deterministic video output.
66
+ */
51
67
  export default function WebSceneMode() {
52
68
  const [time, setTime] = useState(0);
53
69
  const [isPlaying, setIsPlaying] = useState(false);
54
70
  const [mode, setMode] = useState<'live' | 'rendered'>('live');
55
71
  const rafRef = useRef<number>(0);
56
72
  const startRef = useRef(0);
57
- const canvasRef = useRef<HTMLCanvasElement>(null);
58
- const engineRef = useRef<VidraEngine | null>(null);
59
- const [wasmReady, setWasmReady] = useState(false);
60
- const [wasmError, setWasmError] = useState<string | null>(null);
73
+
74
+ // Captured time: quantized to CAPTURE_FPS discrete steps
75
+ const capturedTime = Math.floor(time * CAPTURE_FPS) / CAPTURE_FPS;
76
+ const currentFrame = Math.floor(time * CAPTURE_FPS);
77
+ const totalFrames = ANIMATION_DURATION * CAPTURE_FPS;
61
78
 
62
79
  const data = useMemo<BarData[]>(() => [
63
80
  { label: 'Q1', value: 45, color: '#3b82f6' },
@@ -67,94 +84,18 @@ export default function WebSceneMode() {
67
84
  { label: 'Q5', value: 68, color: '#ec4899' },
68
85
  ], []);
69
86
 
70
- // Build a VidraScript that replicates the bar chart animation
71
- const vidraScript = useMemo(() => {
72
- const lines: string[] = [];
73
- lines.push(`project(480, 320, ${FPS}) {`);
74
- lines.push(` scene("chart", ${ANIMATION_DURATION}s) {`);
75
- // Background
76
- lines.push(` layer("bg") { solid(#0f172a) }`);
77
- // Title
78
- lines.push(` layer("title") {`);
79
- lines.push(` text("Revenue by Quarter", font: "Inter", size: 20, color: #ffffff)`);
80
- lines.push(` position(240, 40)`);
81
- lines.push(` animation(opacity, from: 0, to: 1, duration: 0.4s, easing: easeOut)`);
82
- lines.push(` }`);
83
- // Bars
84
- const maxVal = Math.max(...data.map(d => d.value));
85
- const barWidth = 50;
86
- const gap = 12;
87
- const totalWidth = data.length * barWidth + (data.length - 1) * gap;
88
- const startX = (480 - totalWidth) / 2 + barWidth / 2;
89
-
90
- data.forEach((d, i) => {
91
- const barHeight = (d.value / maxVal) * 140;
92
- const x = startX + i * (barWidth + gap);
93
- const y = 280 - barHeight / 2;
94
- const delay = i * 0.15;
95
- const colorHex = d.color.replace('#', '');
96
-
97
- lines.push(` layer("bar_${i}") {`);
98
- lines.push(` shape(rect, width: ${barWidth}, height: ${Math.round(barHeight)}, radius: 4, fill: #${colorHex})`);
99
- lines.push(` position(${Math.round(x)}, ${Math.round(y)})`);
100
- lines.push(` animation(opacity, from: 0, to: 1, duration: 0.5s, easing: easeOut, delay: ${delay.toFixed(2)}s)`);
101
- lines.push(` animation(scaleY, from: 0.1, to: 1, duration: 0.5s, easing: cubicOut, delay: ${delay.toFixed(2)}s)`);
102
- lines.push(` }`);
103
- // Label
104
- lines.push(` layer("label_${i}") {`);
105
- lines.push(` text("${d.label}", font: "Inter", size: 12, color: #94a3b8)`);
106
- lines.push(` position(${Math.round(x)}, 300)`);
107
- lines.push(` animation(opacity, from: 0, to: 1, duration: 0.3s, easing: easeOut, delay: ${(delay + 0.2).toFixed(2)}s)`);
108
- lines.push(` }`);
109
- });
110
-
111
- lines.push(` }`);
112
- lines.push(`}`);
113
- return lines.join('\n');
114
- }, [data]);
115
-
116
- // Initialize WASM engine for the rendered side — re-run when mode changes (canvas appears)
117
- useEffect(() => {
118
- if (!canvasRef.current || mode !== 'rendered') return;
119
- if (engineRef.current) return; // already initialized
120
- let mounted = true;
121
-
122
- async function initEngine() {
123
- try {
124
- const engine = new VidraEngine(canvasRef.current!);
125
- await engine.init();
126
- if (!mounted) return;
127
-
128
- engine.loadSource(vidraScript);
129
- engineRef.current = engine;
130
- setWasmReady(true);
131
- } catch (err: any) {
132
- console.error('[vidra] WebScene WASM init error:', err);
133
- if (mounted) setWasmError(err.message);
134
- }
135
- }
136
- initEngine();
137
- return () => { mounted = false; };
138
- }, [vidraScript, mode]);
139
-
140
87
  // Animation loop
141
88
  const animate = useCallback(() => {
142
89
  const elapsed = (performance.now() - startRef.current) / 1000;
143
90
  const clampedTime = Math.min(elapsed, ANIMATION_DURATION);
144
91
  setTime(clampedTime);
145
92
 
146
- // Sync WASM engine to the same frame
147
- if (engineRef.current && wasmReady) {
148
- const frame = Math.floor(clampedTime * FPS);
149
- engineRef.current.seekToFrame(frame);
150
- }
151
-
152
93
  if (clampedTime < ANIMATION_DURATION) {
153
94
  rafRef.current = requestAnimationFrame(animate);
154
95
  } else {
155
96
  setIsPlaying(false);
156
97
  }
157
- }, [wasmReady]);
98
+ }, []);
158
99
 
159
100
  const play = useCallback(() => {
160
101
  startRef.current = performance.now() - time * 1000;
@@ -212,44 +153,66 @@ export default function WebSceneMode() {
212
153
  {mode === 'live' ? (
213
154
  /* LIVE MODE — Full-size React component */
214
155
  <div className="w-full max-w-2xl">
215
- <AnimatedBarChart time={time} data={data} />
156
+ <AnimatedBarChart
157
+ time={time}
158
+ data={data}
159
+ label="React component + requestAnimationFrame"
160
+ />
216
161
  </div>
217
162
  ) : (
218
- /* SIDE-BY-SIDE — React component + WASM rendered output */
163
+ /* SIDE-BY-SIDE — 60fps live vs 30fps captured frames */
219
164
  <>
165
+ {/* Left: Live React component @ 60fps */}
220
166
  <div className="flex-1 max-w-md">
221
- <div className="mb-2">
167
+ <div className="mb-2 flex items-center justify-between">
222
168
  <span className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-emerald-400 bg-emerald-500/10 px-2.5 py-1 rounded-full">
223
- <Zap className="w-3 h-3" /> React Component (Live)
169
+ <Zap className="w-3 h-3" /> React Component (60fps)
170
+ </span>
171
+ <span className="text-[10px] font-mono text-emerald-400/50">
172
+ t = {time.toFixed(3)}s
224
173
  </span>
225
174
  </div>
226
- <AnimatedBarChart time={time} data={data} />
175
+ <AnimatedBarChart
176
+ time={time}
177
+ data={data}
178
+ label="Live — continuous requestAnimationFrame"
179
+ />
227
180
  </div>
228
181
 
229
- <div className="flex flex-col items-center gap-2 text-vd-dim">
230
- <div className="w-px h-16 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
231
- <span className="text-[10px] font-mono">→</span>
232
- <div className="w-px h-16 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
182
+ {/* Arrow separator showing the capture pipeline */}
183
+ <div className="flex flex-col items-center gap-1.5 text-vd-dim">
184
+ <div className="w-px h-6 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
185
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-white/10 flex items-center justify-center">
186
+ <Camera className="w-4 h-4 text-blue-400" />
187
+ </div>
188
+ <span className="text-[9px] font-mono text-blue-300/60 font-bold">capture</span>
189
+ <ArrowRight className="w-3.5 h-3.5 text-blue-400/40" />
190
+ <span className="text-[9px] font-mono text-blue-300/60">{CAPTURE_FPS}fps</span>
191
+ <div className="w-px h-6 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
233
192
  </div>
234
193
 
194
+ {/* Right: Captured frames @ 30fps (quantized time) */}
235
195
  <div className="flex-1 max-w-md">
236
- <div className="mb-2">
196
+ <div className="mb-2 flex items-center justify-between">
237
197
  <span className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-1 rounded-full">
238
- <Eye className="w-3 h-3" /> WASM Rendered Output
198
+ <Eye className="w-3 h-3" /> Captured Output ({CAPTURE_FPS}fps)
199
+ </span>
200
+ <span className="text-[10px] font-mono text-blue-400/50">
201
+ f{currentFrame}/{totalFrames}
239
202
  </span>
240
203
  </div>
241
- <div className="rounded-2xl overflow-hidden border border-white/10 shadow-xl shadow-black/30 bg-black aspect-[3/2]">
242
- {wasmError ? (
243
- <div className="flex items-center justify-center h-full text-red-400 text-xs p-4 text-center">
244
- WASM Error: {wasmError}
245
- </div>
246
- ) : !wasmReady ? (
247
- <div className="flex items-center justify-center h-full">
248
- <div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
249
- </div>
250
- ) : null}
251
- <canvas ref={canvasRef} width="480" height="320"
252
- className="w-full h-full object-contain block" />
204
+ <div className="relative">
205
+ <AnimatedBarChart
206
+ time={capturedTime}
207
+ data={data}
208
+ label={`Frame ${currentFrame} — t = ${capturedTime.toFixed(3)}s`}
209
+ />
210
+ {/* Subtle scan-line overlay to visually distinguish captured output */}
211
+ <div className="absolute inset-0 pointer-events-none rounded-2xl overflow-hidden"
212
+ style={{
213
+ background: 'repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.03) 3px, rgba(0,0,0,0.03) 4px)',
214
+ mixBlendMode: 'multiply',
215
+ }} />
253
216
  </div>
254
217
  </div>
255
218
  </>
@@ -281,8 +244,8 @@ export default function WebSceneMode() {
281
244
  {/* Explanation */}
282
245
  <p className="text-center text-[11px] text-vd-dim mt-3 max-w-lg mx-auto leading-relaxed">
283
246
  {mode === 'live'
284
- ? 'This React component animates at 60fps using requestAnimationFrame — exactly how Vidra captures web scenes into video frames.'
285
- : 'Left: the live React component. Right: the same animation rendered by the Vidra WASM engine as video frames pixel-perfect, deterministic output at 30fps.'
247
+ ? 'This React component animates at 60fps using requestAnimationFrame — exactly how you build Vidra web scenes.'
248
+ : `Left: live component at 60fps. Right: same component captured at discrete ${CAPTURE_FPS}fps frames — this is how Vidra's capture pipeline produces deterministic video. Notice the frame counter and quantized timestamps.`
286
249
  }
287
250
  </p>
288
251
  </div>