@sansavision/create-vidra-app 0.1.20-alpha.0 → 0.1.22-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 +1 -1
- package/template/src/components/WebSceneMode.tsx +122 -125
package/package.json
CHANGED
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
import { useState, useEffect, useRef,
|
|
2
|
-
import { Play, Pause, RotateCcw, Eye, Zap } from 'lucide-react';
|
|
3
|
-
import { VidraEngine } from '@sansavision/vidra-player';
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
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;
|
|
6
|
+
|
|
7
|
+
// Color palette for randomized bars
|
|
8
|
+
const BAR_COLORS = [
|
|
9
|
+
'#3b82f6', '#22c55e', '#a855f7', '#f97316', '#ec4899',
|
|
10
|
+
'#06b6d4', '#eab308', '#ef4444', '#8b5cf6', '#14b8a6',
|
|
11
|
+
];
|
|
12
|
+
const BAR_LABELS = [
|
|
13
|
+
['Q1', 'Q2', 'Q3', 'Q4', 'Q5'],
|
|
14
|
+
['Jan', 'Feb', 'Mar', 'Apr', 'May'],
|
|
15
|
+
['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
|
|
16
|
+
['US', 'EU', 'UK', 'JP', 'AU'],
|
|
17
|
+
['Web', 'iOS', 'Droid', 'Mac', 'Win'],
|
|
18
|
+
['Alpha', 'Beta', 'Gamma', 'Delta', 'Zeta'],
|
|
19
|
+
];
|
|
20
|
+
const CHART_TITLES = [
|
|
21
|
+
'Revenue by Quarter', 'Monthly Sales', 'Weekly Traffic',
|
|
22
|
+
'Regional Revenue', 'Platform Users', 'Release Metrics',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** Generate random bar chart data */
|
|
26
|
+
function generateRandomData(): { title: string; bars: BarData[] } {
|
|
27
|
+
const labelSetIdx = Math.floor(Math.random() * BAR_LABELS.length);
|
|
28
|
+
const labels = BAR_LABELS[labelSetIdx];
|
|
29
|
+
const title = CHART_TITLES[labelSetIdx];
|
|
30
|
+
// Shuffle colors
|
|
31
|
+
const shuffled = [...BAR_COLORS].sort(() => Math.random() - 0.5);
|
|
32
|
+
return {
|
|
33
|
+
title,
|
|
34
|
+
bars: labels.map((label, i) => ({
|
|
35
|
+
label,
|
|
36
|
+
value: 20 + Math.floor(Math.random() * 80), // 20–99
|
|
37
|
+
color: shuffled[i % shuffled.length],
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
7
41
|
|
|
8
42
|
interface BarData {
|
|
9
43
|
label: string;
|
|
@@ -11,18 +45,24 @@ interface BarData {
|
|
|
11
45
|
color: string;
|
|
12
46
|
}
|
|
13
47
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Animated bar chart React component — the "source" web scene.
|
|
50
|
+
* Used in both live and captured views — content is pixel-identical.
|
|
51
|
+
*/
|
|
52
|
+
function AnimatedBarChart({ time, data, title, label }: { time: number; data: BarData[]; title: string; label?: string }) {
|
|
53
|
+
const maxVal = Math.max(...data.map((d: BarData) => d.value));
|
|
17
54
|
|
|
18
55
|
return (
|
|
19
56
|
<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
|
-
<h3 className="text-lg font-bold text-white mb-1 text-center">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
<h3 className="text-lg font-bold text-white mb-1 text-center">{title}</h3>
|
|
58
|
+
{label && (
|
|
59
|
+
<p className="text-[11px] text-blue-300/50 text-center mb-6">
|
|
60
|
+
{label}
|
|
61
|
+
</p>
|
|
62
|
+
)}
|
|
63
|
+
{!label && <div className="h-6" />}
|
|
24
64
|
<div className="flex items-end justify-center gap-4 h-44">
|
|
25
|
-
{data.map((d, i) => {
|
|
65
|
+
{data.map((d: BarData, i: number) => {
|
|
26
66
|
const delay = i * 0.15;
|
|
27
67
|
const t = Math.max(0, Math.min((time - delay) / 0.8, 1));
|
|
28
68
|
const eased = 1 - Math.pow(1 - t, 3);
|
|
@@ -47,95 +87,32 @@ function AnimatedBarChart({ time, data }: { time: number; data: BarData[] }) {
|
|
|
47
87
|
);
|
|
48
88
|
}
|
|
49
89
|
|
|
50
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* WebSceneMode — demonstrates Vidra's web capture pipeline.
|
|
92
|
+
*
|
|
93
|
+
* Live Mode: Full-size React component animating at 60fps.
|
|
94
|
+
* Side-by-Side: React component at 60fps (left) vs the same component
|
|
95
|
+
* rendered at discrete 30fps frame steps (right).
|
|
96
|
+
*
|
|
97
|
+
* The "captured" side shows the exact same component but with time
|
|
98
|
+
* quantized to 30fps frames — this accurately demonstrates how Vidra's
|
|
99
|
+
* web capture pipeline works: it snapshots each frame at discrete
|
|
100
|
+
* intervals, producing deterministic video output.
|
|
101
|
+
*/
|
|
51
102
|
export default function WebSceneMode() {
|
|
52
103
|
const [time, setTime] = useState(0);
|
|
53
104
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
54
105
|
const [mode, setMode] = useState<'live' | 'rendered'>('live');
|
|
55
106
|
const rafRef = useRef<number>(0);
|
|
56
107
|
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);
|
|
61
|
-
|
|
62
|
-
const data = useMemo<BarData[]>(() => [
|
|
63
|
-
{ label: 'Q1', value: 45, color: '#3b82f6' },
|
|
64
|
-
{ label: 'Q2', value: 72, color: '#22c55e' },
|
|
65
|
-
{ label: 'Q3', value: 58, color: '#a855f7' },
|
|
66
|
-
{ label: 'Q4', value: 91, color: '#f97316' },
|
|
67
|
-
{ label: 'Q5', value: 68, color: '#ec4899' },
|
|
68
|
-
], []);
|
|
69
|
-
|
|
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
108
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!mounted) return;
|
|
109
|
+
// Captured time: quantized to CAPTURE_FPS discrete steps
|
|
110
|
+
const capturedTime = Math.floor(time * CAPTURE_FPS) / CAPTURE_FPS;
|
|
111
|
+
const currentFrame = Math.floor(time * CAPTURE_FPS);
|
|
112
|
+
const totalFrames = ANIMATION_DURATION * CAPTURE_FPS;
|
|
127
113
|
|
|
128
|
-
|
|
129
|
-
|
|
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]);
|
|
114
|
+
// Randomized data — new values every playback to prove it's live
|
|
115
|
+
const [chartData, setChartData] = useState(() => generateRandomData());
|
|
139
116
|
|
|
140
117
|
// Animation loop
|
|
141
118
|
const animate = useCallback(() => {
|
|
@@ -143,18 +120,12 @@ export default function WebSceneMode() {
|
|
|
143
120
|
const clampedTime = Math.min(elapsed, ANIMATION_DURATION);
|
|
144
121
|
setTime(clampedTime);
|
|
145
122
|
|
|
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
123
|
if (clampedTime < ANIMATION_DURATION) {
|
|
153
124
|
rafRef.current = requestAnimationFrame(animate);
|
|
154
125
|
} else {
|
|
155
126
|
setIsPlaying(false);
|
|
156
127
|
}
|
|
157
|
-
}, [
|
|
128
|
+
}, []);
|
|
158
129
|
|
|
159
130
|
const play = useCallback(() => {
|
|
160
131
|
startRef.current = performance.now() - time * 1000;
|
|
@@ -169,6 +140,7 @@ export default function WebSceneMode() {
|
|
|
169
140
|
|
|
170
141
|
const restart = useCallback(() => {
|
|
171
142
|
cancelAnimationFrame(rafRef.current);
|
|
143
|
+
setChartData(generateRandomData()); // new random data each playback!
|
|
172
144
|
setTime(0);
|
|
173
145
|
startRef.current = performance.now();
|
|
174
146
|
setIsPlaying(true);
|
|
@@ -212,44 +184,69 @@ export default function WebSceneMode() {
|
|
|
212
184
|
{mode === 'live' ? (
|
|
213
185
|
/* LIVE MODE — Full-size React component */
|
|
214
186
|
<div className="w-full max-w-2xl">
|
|
215
|
-
<AnimatedBarChart
|
|
187
|
+
<AnimatedBarChart
|
|
188
|
+
time={time}
|
|
189
|
+
data={chartData.bars}
|
|
190
|
+
title={chartData.title}
|
|
191
|
+
label="React component + requestAnimationFrame"
|
|
192
|
+
/>
|
|
216
193
|
</div>
|
|
217
194
|
) : (
|
|
218
|
-
/* SIDE-BY-SIDE —
|
|
195
|
+
/* SIDE-BY-SIDE — 60fps live vs 30fps captured frames */
|
|
219
196
|
<>
|
|
197
|
+
{/* Left: Live React component @ 60fps */}
|
|
220
198
|
<div className="flex-1 max-w-md">
|
|
221
|
-
<div className="mb-2">
|
|
199
|
+
<div className="mb-2 flex items-center justify-between">
|
|
222
200
|
<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 (
|
|
201
|
+
<Zap className="w-3 h-3" /> React Component (60fps)
|
|
202
|
+
</span>
|
|
203
|
+
<span className="text-[10px] font-mono text-emerald-400/50">
|
|
204
|
+
t = {time.toFixed(3)}s
|
|
224
205
|
</span>
|
|
225
206
|
</div>
|
|
226
|
-
<AnimatedBarChart
|
|
207
|
+
<AnimatedBarChart
|
|
208
|
+
time={time}
|
|
209
|
+
data={chartData.bars}
|
|
210
|
+
title={chartData.title}
|
|
211
|
+
label="Live — continuous requestAnimationFrame"
|
|
212
|
+
/>
|
|
227
213
|
</div>
|
|
228
214
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
<div className="w-
|
|
215
|
+
{/* Arrow separator showing the capture pipeline */}
|
|
216
|
+
<div className="flex flex-col items-center gap-1.5 text-vd-dim">
|
|
217
|
+
<div className="w-px h-6 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
|
|
218
|
+
<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">
|
|
219
|
+
<Camera className="w-4 h-4 text-blue-400" />
|
|
220
|
+
</div>
|
|
221
|
+
<span className="text-[9px] font-mono text-blue-300/60 font-bold">capture</span>
|
|
222
|
+
<ArrowRight className="w-3.5 h-3.5 text-blue-400/40" />
|
|
223
|
+
<span className="text-[9px] font-mono text-blue-300/60">{CAPTURE_FPS}fps</span>
|
|
224
|
+
<div className="w-px h-6 bg-gradient-to-b from-transparent via-white/20 to-transparent" />
|
|
233
225
|
</div>
|
|
234
226
|
|
|
227
|
+
{/* Right: Captured frames @ 30fps (quantized time) */}
|
|
235
228
|
<div className="flex-1 max-w-md">
|
|
236
|
-
<div className="mb-2">
|
|
229
|
+
<div className="mb-2 flex items-center justify-between">
|
|
237
230
|
<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" />
|
|
231
|
+
<Eye className="w-3 h-3" /> Captured Output ({CAPTURE_FPS}fps)
|
|
232
|
+
</span>
|
|
233
|
+
<span className="text-[10px] font-mono text-blue-400/50">
|
|
234
|
+
f{currentFrame}/{totalFrames}
|
|
239
235
|
</span>
|
|
240
236
|
</div>
|
|
241
|
-
<div className="
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
237
|
+
<div className="relative">
|
|
238
|
+
<AnimatedBarChart
|
|
239
|
+
time={capturedTime}
|
|
240
|
+
data={chartData.bars}
|
|
241
|
+
title={chartData.title}
|
|
242
|
+
label={`Frame ${currentFrame} — t = ${capturedTime.toFixed(3)}s`}
|
|
243
|
+
/>
|
|
244
|
+
{/* Subtle scan-line overlay to visually distinguish captured output */}
|
|
245
|
+
<div className="absolute inset-0 pointer-events-none rounded-2xl overflow-hidden"
|
|
246
|
+
style={{
|
|
247
|
+
background: 'repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(0,0,0,0.03) 3px, rgba(0,0,0,0.03) 4px)',
|
|
248
|
+
mixBlendMode: 'multiply',
|
|
249
|
+
}} />
|
|
253
250
|
</div>
|
|
254
251
|
</div>
|
|
255
252
|
</>
|
|
@@ -281,8 +278,8 @@ export default function WebSceneMode() {
|
|
|
281
278
|
{/* Explanation */}
|
|
282
279
|
<p className="text-center text-[11px] text-vd-dim mt-3 max-w-lg mx-auto leading-relaxed">
|
|
283
280
|
{mode === 'live'
|
|
284
|
-
? 'This React component animates at 60fps using requestAnimationFrame — exactly how Vidra
|
|
285
|
-
:
|
|
281
|
+
? 'This React component animates at 60fps using requestAnimationFrame — exactly how you build Vidra web scenes.'
|
|
282
|
+
: `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
283
|
}
|
|
287
284
|
</p>
|
|
288
285
|
</div>
|