@sansavision/create-vidra-app 0.1.19-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,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
|
|
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
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
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,93 +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
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
if (!canvasRef.current) return;
|
|
119
|
-
let mounted = true;
|
|
120
|
-
|
|
121
|
-
async function initEngine() {
|
|
122
|
-
try {
|
|
123
|
-
const engine = new VidraEngine(canvasRef.current!);
|
|
124
|
-
await engine.init();
|
|
125
|
-
if (!mounted) return;
|
|
126
|
-
|
|
127
|
-
engine.loadSource(vidraScript);
|
|
128
|
-
engineRef.current = engine;
|
|
129
|
-
setWasmReady(true);
|
|
130
|
-
} catch (err: any) {
|
|
131
|
-
console.error('[vidra] WebScene WASM init error:', err);
|
|
132
|
-
if (mounted) setWasmError(err.message);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
initEngine();
|
|
136
|
-
return () => { mounted = false; };
|
|
137
|
-
}, [vidraScript]);
|
|
138
|
-
|
|
139
87
|
// Animation loop
|
|
140
88
|
const animate = useCallback(() => {
|
|
141
89
|
const elapsed = (performance.now() - startRef.current) / 1000;
|
|
142
90
|
const clampedTime = Math.min(elapsed, ANIMATION_DURATION);
|
|
143
91
|
setTime(clampedTime);
|
|
144
92
|
|
|
145
|
-
// Sync WASM engine to the same frame
|
|
146
|
-
if (engineRef.current && wasmReady) {
|
|
147
|
-
const frame = Math.floor(clampedTime * FPS);
|
|
148
|
-
engineRef.current.seekToFrame(frame);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
93
|
if (clampedTime < ANIMATION_DURATION) {
|
|
152
94
|
rafRef.current = requestAnimationFrame(animate);
|
|
153
95
|
} else {
|
|
154
96
|
setIsPlaying(false);
|
|
155
97
|
}
|
|
156
|
-
}, [
|
|
98
|
+
}, []);
|
|
157
99
|
|
|
158
100
|
const play = useCallback(() => {
|
|
159
101
|
startRef.current = performance.now() - time * 1000;
|
|
@@ -191,16 +133,16 @@ export default function WebSceneMode() {
|
|
|
191
133
|
<button
|
|
192
134
|
onClick={() => setMode('live')}
|
|
193
135
|
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-semibold transition-all ${mode === 'live'
|
|
194
|
-
|
|
195
|
-
|
|
136
|
+
? 'bg-emerald-500/20 text-emerald-400 ring-1 ring-emerald-500/30'
|
|
137
|
+
: 'bg-white/5 text-vd-dim hover:bg-white/10'
|
|
196
138
|
}`}>
|
|
197
139
|
<Zap className="w-3.5 h-3.5" /> Live Mode
|
|
198
140
|
</button>
|
|
199
141
|
<button
|
|
200
142
|
onClick={() => setMode('rendered')}
|
|
201
143
|
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-semibold transition-all ${mode === 'rendered'
|
|
202
|
-
|
|
203
|
-
|
|
144
|
+
? 'bg-blue-500/20 text-blue-400 ring-1 ring-blue-500/30'
|
|
145
|
+
: 'bg-white/5 text-vd-dim hover:bg-white/10'
|
|
204
146
|
}`}>
|
|
205
147
|
<Eye className="w-3.5 h-3.5" /> Side-by-Side
|
|
206
148
|
</button>
|
|
@@ -211,44 +153,66 @@ export default function WebSceneMode() {
|
|
|
211
153
|
{mode === 'live' ? (
|
|
212
154
|
/* LIVE MODE — Full-size React component */
|
|
213
155
|
<div className="w-full max-w-2xl">
|
|
214
|
-
<AnimatedBarChart
|
|
156
|
+
<AnimatedBarChart
|
|
157
|
+
time={time}
|
|
158
|
+
data={data}
|
|
159
|
+
label="React component + requestAnimationFrame"
|
|
160
|
+
/>
|
|
215
161
|
</div>
|
|
216
162
|
) : (
|
|
217
|
-
/* SIDE-BY-SIDE —
|
|
163
|
+
/* SIDE-BY-SIDE — 60fps live vs 30fps captured frames */
|
|
218
164
|
<>
|
|
165
|
+
{/* Left: Live React component @ 60fps */}
|
|
219
166
|
<div className="flex-1 max-w-md">
|
|
220
|
-
<div className="mb-2">
|
|
167
|
+
<div className="mb-2 flex items-center justify-between">
|
|
221
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">
|
|
222
|
-
<Zap className="w-3 h-3" /> React Component (
|
|
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
|
|
223
173
|
</span>
|
|
224
174
|
</div>
|
|
225
|
-
<AnimatedBarChart
|
|
175
|
+
<AnimatedBarChart
|
|
176
|
+
time={time}
|
|
177
|
+
data={data}
|
|
178
|
+
label="Live — continuous requestAnimationFrame"
|
|
179
|
+
/>
|
|
226
180
|
</div>
|
|
227
181
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<
|
|
231
|
-
<div className="w-
|
|
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" />
|
|
232
192
|
</div>
|
|
233
193
|
|
|
194
|
+
{/* Right: Captured frames @ 30fps (quantized time) */}
|
|
234
195
|
<div className="flex-1 max-w-md">
|
|
235
|
-
<div className="mb-2">
|
|
196
|
+
<div className="mb-2 flex items-center justify-between">
|
|
236
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">
|
|
237
|
-
<Eye className="w-3 h-3" />
|
|
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}
|
|
238
202
|
</span>
|
|
239
203
|
</div>
|
|
240
|
-
<div className="
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}} />
|
|
252
216
|
</div>
|
|
253
217
|
</div>
|
|
254
218
|
</>
|
|
@@ -280,8 +244,8 @@ export default function WebSceneMode() {
|
|
|
280
244
|
{/* Explanation */}
|
|
281
245
|
<p className="text-center text-[11px] text-vd-dim mt-3 max-w-lg mx-auto leading-relaxed">
|
|
282
246
|
{mode === 'live'
|
|
283
|
-
? 'This React component animates at 60fps using requestAnimationFrame — exactly how Vidra
|
|
284
|
-
:
|
|
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.`
|
|
285
249
|
}
|
|
286
250
|
</p>
|
|
287
251
|
</div>
|