@clypra/ui 1.0.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/.releaserc.json +16 -0
- package/package.json +47 -0
- package/src/components/GraphInspector/GraphInspector.tsx +91 -0
- package/src/components/GraphInspector/index.ts +2 -0
- package/src/components/PassInspector/PassInspector.tsx +128 -0
- package/src/components/PassInspector/index.ts +2 -0
- package/src/components/PerformanceMonitor/PerformanceMonitor.tsx +129 -0
- package/src/components/PerformanceMonitor/index.ts +2 -0
- package/src/components/PresetManager/PresetManager.tsx +420 -0
- package/src/components/PresetManager/index.ts +2 -0
- package/src/components/PreviewCanvas/PreviewCanvas.tsx +168 -0
- package/src/components/PreviewCanvas/index.ts +2 -0
- package/src/components/ResourceInspector/ResourceInspector.tsx +126 -0
- package/src/components/ResourceInspector/index.ts +2 -0
- package/src/components/Timeline/Timeline.tsx +291 -0
- package/src/components/Timeline/index.ts +2 -0
- package/src/components/ValidationPanel/ValidationPanel.tsx +428 -0
- package/src/components/ValidationPanel/index.ts +2 -0
- package/src/index.ts +35 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PresetManager Component
|
|
3
|
+
*
|
|
4
|
+
* Create, load, save, and manage effect presets.
|
|
5
|
+
* Supports preset library, export/import JSON.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState } from "react";
|
|
9
|
+
|
|
10
|
+
export interface Preset {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
effectId: string;
|
|
15
|
+
parameters: Record<string, any>;
|
|
16
|
+
version: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
tags?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PresetManagerProps {
|
|
23
|
+
/** Current effect being edited */
|
|
24
|
+
effect: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
};
|
|
29
|
+
/** Current parameter values */
|
|
30
|
+
parameters: Record<string, any>;
|
|
31
|
+
/** Available presets for this effect */
|
|
32
|
+
presets?: Preset[];
|
|
33
|
+
/** Callback when preset is loaded */
|
|
34
|
+
onLoadPreset?: (preset: Preset) => void;
|
|
35
|
+
/** Callback when preset is saved */
|
|
36
|
+
onSavePreset?: (preset: Preset) => void;
|
|
37
|
+
/** Callback when preset is deleted */
|
|
38
|
+
onDeletePreset?: (presetId: string) => void;
|
|
39
|
+
/** Callback when preset is exported */
|
|
40
|
+
onExportPreset?: (preset: Preset) => void;
|
|
41
|
+
/** Callback when preset is imported */
|
|
42
|
+
onImportPreset?: (preset: Preset) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function PresetManager({ effect, parameters, presets = [], onLoadPreset, onSavePreset, onDeletePreset, onExportPreset, onImportPreset }: PresetManagerProps) {
|
|
46
|
+
const [selectedPreset, setSelectedPreset] = useState<Preset | null>(null);
|
|
47
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
48
|
+
const [newPresetName, setNewPresetName] = useState("");
|
|
49
|
+
const [newPresetDescription, setNewPresetDescription] = useState("");
|
|
50
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
51
|
+
const [filterTag, setFilterTag] = useState<string | null>(null);
|
|
52
|
+
|
|
53
|
+
// Get all unique tags from presets
|
|
54
|
+
const allTags = Array.from(new Set(presets.flatMap((p) => p.tags || []))).sort();
|
|
55
|
+
|
|
56
|
+
// Filter presets by search query and tag
|
|
57
|
+
const filteredPresets = presets.filter((preset) => {
|
|
58
|
+
const matchesSearch = searchQuery === "" || preset.name.toLowerCase().includes(searchQuery.toLowerCase()) || preset.description?.toLowerCase().includes(searchQuery.toLowerCase());
|
|
59
|
+
|
|
60
|
+
const matchesTag = filterTag === null || (preset.tags && preset.tags.includes(filterTag));
|
|
61
|
+
|
|
62
|
+
return matchesSearch && matchesTag;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const handleCreatePreset = () => {
|
|
66
|
+
if (!newPresetName.trim()) return;
|
|
67
|
+
|
|
68
|
+
const newPreset: Preset = {
|
|
69
|
+
id: `preset-${Date.now()}`,
|
|
70
|
+
name: newPresetName,
|
|
71
|
+
description: newPresetDescription,
|
|
72
|
+
effectId: effect.id,
|
|
73
|
+
parameters: { ...parameters },
|
|
74
|
+
version: effect.version,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
updatedAt: new Date().toISOString(),
|
|
77
|
+
tags: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
onSavePreset?.(newPreset);
|
|
81
|
+
setIsCreating(false);
|
|
82
|
+
setNewPresetName("");
|
|
83
|
+
setNewPresetDescription("");
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleLoadPreset = (preset: Preset) => {
|
|
87
|
+
setSelectedPreset(preset);
|
|
88
|
+
onLoadPreset?.(preset);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleExportJSON = (preset: Preset) => {
|
|
92
|
+
const json = JSON.stringify(preset, null, 2);
|
|
93
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
94
|
+
const url = URL.createObjectURL(blob);
|
|
95
|
+
const a = document.createElement("a");
|
|
96
|
+
a.href = url;
|
|
97
|
+
a.download = `${preset.name.replace(/\s+/g, "-").toLowerCase()}.json`;
|
|
98
|
+
a.click();
|
|
99
|
+
URL.revokeObjectURL(url);
|
|
100
|
+
onExportPreset?.(preset);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleImportJSON = () => {
|
|
104
|
+
const input = document.createElement("input");
|
|
105
|
+
input.type = "file";
|
|
106
|
+
input.accept = ".json";
|
|
107
|
+
input.onchange = async (e) => {
|
|
108
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
109
|
+
if (!file) return;
|
|
110
|
+
|
|
111
|
+
const text = await file.text();
|
|
112
|
+
try {
|
|
113
|
+
const preset = JSON.parse(text) as Preset;
|
|
114
|
+
onImportPreset?.(preset);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Failed to import preset:", error);
|
|
117
|
+
alert("Invalid preset file");
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
input.click();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
padding: "16px",
|
|
127
|
+
background: "#0f172a",
|
|
128
|
+
borderRadius: "8px",
|
|
129
|
+
border: "1px solid #334155",
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{/* Header */}
|
|
133
|
+
<div
|
|
134
|
+
style={{
|
|
135
|
+
display: "flex",
|
|
136
|
+
justifyContent: "space-between",
|
|
137
|
+
alignItems: "center",
|
|
138
|
+
marginBottom: "16px",
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<h3 style={{ margin: 0, color: "#f1f5f9", fontSize: "16px" }}>Preset Manager</h3>
|
|
142
|
+
<div style={{ display: "flex", gap: "8px" }}>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => setIsCreating(true)}
|
|
145
|
+
style={{
|
|
146
|
+
padding: "6px 12px",
|
|
147
|
+
background: "#3b82f6",
|
|
148
|
+
color: "white",
|
|
149
|
+
border: "none",
|
|
150
|
+
borderRadius: "6px",
|
|
151
|
+
cursor: "pointer",
|
|
152
|
+
fontSize: "14px",
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
+ New Preset
|
|
156
|
+
</button>
|
|
157
|
+
<button
|
|
158
|
+
onClick={handleImportJSON}
|
|
159
|
+
style={{
|
|
160
|
+
padding: "6px 12px",
|
|
161
|
+
background: "#475569",
|
|
162
|
+
color: "white",
|
|
163
|
+
border: "none",
|
|
164
|
+
borderRadius: "6px",
|
|
165
|
+
cursor: "pointer",
|
|
166
|
+
fontSize: "14px",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
Import
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Effect info */}
|
|
175
|
+
<div
|
|
176
|
+
style={{
|
|
177
|
+
marginBottom: "16px",
|
|
178
|
+
padding: "12px",
|
|
179
|
+
background: "#1e293b",
|
|
180
|
+
borderRadius: "6px",
|
|
181
|
+
fontSize: "14px",
|
|
182
|
+
color: "#94a3b8",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<div>
|
|
186
|
+
<strong style={{ color: "#f1f5f9" }}>Effect:</strong> {effect.name}
|
|
187
|
+
</div>
|
|
188
|
+
<div>
|
|
189
|
+
<strong style={{ color: "#f1f5f9" }}>Version:</strong> {effect.version}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Create preset form */}
|
|
194
|
+
{isCreating && (
|
|
195
|
+
<div
|
|
196
|
+
style={{
|
|
197
|
+
marginBottom: "16px",
|
|
198
|
+
padding: "12px",
|
|
199
|
+
background: "#1e293b",
|
|
200
|
+
borderRadius: "6px",
|
|
201
|
+
border: "2px solid #3b82f6",
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
placeholder="Preset name"
|
|
207
|
+
value={newPresetName}
|
|
208
|
+
onChange={(e) => setNewPresetName(e.target.value)}
|
|
209
|
+
style={{
|
|
210
|
+
width: "100%",
|
|
211
|
+
padding: "8px",
|
|
212
|
+
marginBottom: "8px",
|
|
213
|
+
background: "#0f172a",
|
|
214
|
+
border: "1px solid #334155",
|
|
215
|
+
borderRadius: "4px",
|
|
216
|
+
color: "#f1f5f9",
|
|
217
|
+
fontSize: "14px",
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
<textarea
|
|
221
|
+
placeholder="Description (optional)"
|
|
222
|
+
value={newPresetDescription}
|
|
223
|
+
onChange={(e) => setNewPresetDescription(e.target.value)}
|
|
224
|
+
rows={3}
|
|
225
|
+
style={{
|
|
226
|
+
width: "100%",
|
|
227
|
+
padding: "8px",
|
|
228
|
+
marginBottom: "8px",
|
|
229
|
+
background: "#0f172a",
|
|
230
|
+
border: "1px solid #334155",
|
|
231
|
+
borderRadius: "4px",
|
|
232
|
+
color: "#f1f5f9",
|
|
233
|
+
fontSize: "14px",
|
|
234
|
+
resize: "vertical",
|
|
235
|
+
}}
|
|
236
|
+
/>
|
|
237
|
+
<div style={{ display: "flex", gap: "8px" }}>
|
|
238
|
+
<button
|
|
239
|
+
onClick={handleCreatePreset}
|
|
240
|
+
disabled={!newPresetName.trim()}
|
|
241
|
+
style={{
|
|
242
|
+
flex: 1,
|
|
243
|
+
padding: "8px",
|
|
244
|
+
background: newPresetName.trim() ? "#10b981" : "#334155",
|
|
245
|
+
color: "white",
|
|
246
|
+
border: "none",
|
|
247
|
+
borderRadius: "6px",
|
|
248
|
+
cursor: newPresetName.trim() ? "pointer" : "not-allowed",
|
|
249
|
+
fontSize: "14px",
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
Save Preset
|
|
253
|
+
</button>
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => {
|
|
256
|
+
setIsCreating(false);
|
|
257
|
+
setNewPresetName("");
|
|
258
|
+
setNewPresetDescription("");
|
|
259
|
+
}}
|
|
260
|
+
style={{
|
|
261
|
+
flex: 1,
|
|
262
|
+
padding: "8px",
|
|
263
|
+
background: "#475569",
|
|
264
|
+
color: "white",
|
|
265
|
+
border: "none",
|
|
266
|
+
borderRadius: "6px",
|
|
267
|
+
cursor: "pointer",
|
|
268
|
+
fontSize: "14px",
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
Cancel
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Search and filter */}
|
|
278
|
+
<div style={{ marginBottom: "16px" }}>
|
|
279
|
+
<input
|
|
280
|
+
type="text"
|
|
281
|
+
placeholder="Search presets..."
|
|
282
|
+
value={searchQuery}
|
|
283
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
284
|
+
style={{
|
|
285
|
+
width: "100%",
|
|
286
|
+
padding: "8px",
|
|
287
|
+
marginBottom: "8px",
|
|
288
|
+
background: "#1e293b",
|
|
289
|
+
border: "1px solid #334155",
|
|
290
|
+
borderRadius: "6px",
|
|
291
|
+
color: "#f1f5f9",
|
|
292
|
+
fontSize: "14px",
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
{allTags.length > 0 && (
|
|
296
|
+
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
|
|
297
|
+
<button
|
|
298
|
+
onClick={() => setFilterTag(null)}
|
|
299
|
+
style={{
|
|
300
|
+
padding: "4px 10px",
|
|
301
|
+
background: filterTag === null ? "#3b82f6" : "#334155",
|
|
302
|
+
color: "white",
|
|
303
|
+
border: "none",
|
|
304
|
+
borderRadius: "4px",
|
|
305
|
+
cursor: "pointer",
|
|
306
|
+
fontSize: "12px",
|
|
307
|
+
}}
|
|
308
|
+
>
|
|
309
|
+
All
|
|
310
|
+
</button>
|
|
311
|
+
{allTags.map((tag) => (
|
|
312
|
+
<button
|
|
313
|
+
key={tag}
|
|
314
|
+
onClick={() => setFilterTag(tag)}
|
|
315
|
+
style={{
|
|
316
|
+
padding: "4px 10px",
|
|
317
|
+
background: filterTag === tag ? "#3b82f6" : "#334155",
|
|
318
|
+
color: "white",
|
|
319
|
+
border: "none",
|
|
320
|
+
borderRadius: "4px",
|
|
321
|
+
cursor: "pointer",
|
|
322
|
+
fontSize: "12px",
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
{tag}
|
|
326
|
+
</button>
|
|
327
|
+
))}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* Preset library */}
|
|
333
|
+
<div style={{ maxHeight: "400px", overflowY: "auto" }}>
|
|
334
|
+
{filteredPresets.length === 0 ? (
|
|
335
|
+
<div
|
|
336
|
+
style={{
|
|
337
|
+
padding: "32px",
|
|
338
|
+
textAlign: "center",
|
|
339
|
+
color: "#64748b",
|
|
340
|
+
fontSize: "14px",
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
{presets.length === 0 ? "No presets yet. Create your first preset!" : "No presets match your search"}
|
|
344
|
+
</div>
|
|
345
|
+
) : (
|
|
346
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
|
347
|
+
{filteredPresets.map((preset) => (
|
|
348
|
+
<div
|
|
349
|
+
key={preset.id}
|
|
350
|
+
style={{
|
|
351
|
+
padding: "12px",
|
|
352
|
+
background: selectedPreset?.id === preset.id ? "#1e40af" : "#1e293b",
|
|
353
|
+
border: selectedPreset?.id === preset.id ? "2px solid #3b82f6" : "1px solid #334155",
|
|
354
|
+
borderRadius: "6px",
|
|
355
|
+
cursor: "pointer",
|
|
356
|
+
}}
|
|
357
|
+
onClick={() => handleLoadPreset(preset)}
|
|
358
|
+
>
|
|
359
|
+
<div
|
|
360
|
+
style={{
|
|
361
|
+
display: "flex",
|
|
362
|
+
justifyContent: "space-between",
|
|
363
|
+
alignItems: "start",
|
|
364
|
+
marginBottom: "6px",
|
|
365
|
+
}}
|
|
366
|
+
>
|
|
367
|
+
<div style={{ flex: 1 }}>
|
|
368
|
+
<div
|
|
369
|
+
style={{
|
|
370
|
+
fontWeight: 600,
|
|
371
|
+
color: "#f1f5f9",
|
|
372
|
+
marginBottom: "4px",
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
{preset.name}
|
|
376
|
+
</div>
|
|
377
|
+
{preset.description && <div style={{ fontSize: "13px", color: "#94a3b8" }}>{preset.description}</div>}
|
|
378
|
+
</div>
|
|
379
|
+
<div style={{ display: "flex", gap: "6px" }} onClick={(e) => e.stopPropagation()}>
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => handleExportJSON(preset)}
|
|
382
|
+
title="Export as JSON"
|
|
383
|
+
style={{
|
|
384
|
+
padding: "4px 8px",
|
|
385
|
+
background: "#475569",
|
|
386
|
+
color: "white",
|
|
387
|
+
border: "none",
|
|
388
|
+
borderRadius: "4px",
|
|
389
|
+
cursor: "pointer",
|
|
390
|
+
fontSize: "12px",
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
↓
|
|
394
|
+
</button>
|
|
395
|
+
<button
|
|
396
|
+
onClick={() => onDeletePreset?.(preset.id)}
|
|
397
|
+
title="Delete preset"
|
|
398
|
+
style={{
|
|
399
|
+
padding: "4px 8px",
|
|
400
|
+
background: "#ef4444",
|
|
401
|
+
color: "white",
|
|
402
|
+
border: "none",
|
|
403
|
+
borderRadius: "4px",
|
|
404
|
+
cursor: "pointer",
|
|
405
|
+
fontSize: "12px",
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
×
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
<div style={{ fontSize: "12px", color: "#64748b" }}>Updated: {new Date(preset.updatedAt).toLocaleDateString()}</div>
|
|
413
|
+
</div>
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PreviewCanvas Component
|
|
3
|
+
*
|
|
4
|
+
* Renders effect with current parameters.
|
|
5
|
+
* Supports pause/play/scrub and before/after comparison.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export interface PreviewCanvasProps {
|
|
11
|
+
/** Effect definition to render */
|
|
12
|
+
effect: any; // TODO: Type from @clypra/engine
|
|
13
|
+
/** Media inputs for the effect */
|
|
14
|
+
inputs: Record<string, any>;
|
|
15
|
+
/** Current playback time in seconds */
|
|
16
|
+
currentTime: number;
|
|
17
|
+
/** Canvas width */
|
|
18
|
+
width?: number;
|
|
19
|
+
/** Canvas height */
|
|
20
|
+
height?: number;
|
|
21
|
+
/** Show before/after comparison */
|
|
22
|
+
showComparison?: boolean;
|
|
23
|
+
/** Playback state */
|
|
24
|
+
playing?: boolean;
|
|
25
|
+
/** Callback when playback state changes */
|
|
26
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
27
|
+
/** Callback when time changes */
|
|
28
|
+
onTimeChange?: (time: number) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function PreviewCanvas({ effect, inputs, currentTime, width = 1920, height = 1080, showComparison = false, playing = false, onPlayingChange, onTimeChange }: PreviewCanvasProps) {
|
|
32
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
33
|
+
const animationFrameRef = useRef<number>();
|
|
34
|
+
const lastTimeRef = useRef<number>(0);
|
|
35
|
+
|
|
36
|
+
// Handle playback animation
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!playing) {
|
|
39
|
+
if (animationFrameRef.current) {
|
|
40
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const animate = (timestamp: number) => {
|
|
46
|
+
const deltaTime = (timestamp - lastTimeRef.current) / 1000;
|
|
47
|
+
lastTimeRef.current = timestamp;
|
|
48
|
+
|
|
49
|
+
if (onTimeChange && deltaTime > 0) {
|
|
50
|
+
onTimeChange(currentTime + deltaTime);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
lastTimeRef.current = performance.now();
|
|
57
|
+
animationFrameRef.current = requestAnimationFrame(animate);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
if (animationFrameRef.current) {
|
|
61
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}, [playing, currentTime, onTimeChange]);
|
|
65
|
+
|
|
66
|
+
// Render effect frame
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const canvas = canvasRef.current;
|
|
69
|
+
if (!canvas) return;
|
|
70
|
+
|
|
71
|
+
const ctx = canvas.getContext("2d");
|
|
72
|
+
if (!ctx) return;
|
|
73
|
+
|
|
74
|
+
// TODO: Integrate with PixiRenderer from @clypra/runtime
|
|
75
|
+
// For now, show placeholder
|
|
76
|
+
ctx.fillStyle = "#1a1a1a";
|
|
77
|
+
ctx.fillRect(0, 0, width, height);
|
|
78
|
+
|
|
79
|
+
ctx.fillStyle = "#3b82f6";
|
|
80
|
+
ctx.font = "24px sans-serif";
|
|
81
|
+
ctx.textAlign = "center";
|
|
82
|
+
ctx.textBaseline = "middle";
|
|
83
|
+
ctx.fillText(`Preview Canvas - ${effect?.name || "No Effect"}`, width / 2, height / 2 - 30);
|
|
84
|
+
|
|
85
|
+
ctx.fillStyle = "#94a3b8";
|
|
86
|
+
ctx.font = "16px monospace";
|
|
87
|
+
ctx.fillText(`Time: ${currentTime.toFixed(2)}s`, width / 2, height / 2 + 10);
|
|
88
|
+
ctx.fillText(`Playing: ${playing}`, width / 2, height / 2 + 40);
|
|
89
|
+
|
|
90
|
+
if (showComparison) {
|
|
91
|
+
// Draw comparison split
|
|
92
|
+
ctx.strokeStyle = "#f59e0b";
|
|
93
|
+
ctx.lineWidth = 3;
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(width / 2, 0);
|
|
96
|
+
ctx.lineTo(width / 2, height);
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
|
|
99
|
+
ctx.fillStyle = "#f59e0b";
|
|
100
|
+
ctx.font = "14px sans-serif";
|
|
101
|
+
ctx.fillText("Before", width / 4, 30);
|
|
102
|
+
ctx.fillText("After", (width * 3) / 4, 30);
|
|
103
|
+
}
|
|
104
|
+
}, [effect, inputs, currentTime, width, height, showComparison, playing]);
|
|
105
|
+
|
|
106
|
+
const handleCanvasClick = () => {
|
|
107
|
+
if (onPlayingChange) {
|
|
108
|
+
onPlayingChange(!playing);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="preview-canvas-container">
|
|
114
|
+
<canvas
|
|
115
|
+
ref={canvasRef}
|
|
116
|
+
width={width}
|
|
117
|
+
height={height}
|
|
118
|
+
onClick={handleCanvasClick}
|
|
119
|
+
style={{
|
|
120
|
+
width: "100%",
|
|
121
|
+
height: "auto",
|
|
122
|
+
cursor: "pointer",
|
|
123
|
+
border: "1px solid #334155",
|
|
124
|
+
borderRadius: "8px",
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
marginTop: "8px",
|
|
130
|
+
display: "flex",
|
|
131
|
+
gap: "12px",
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
fontSize: "14px",
|
|
134
|
+
color: "#94a3b8",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => onPlayingChange?.(!playing)}
|
|
139
|
+
style={{
|
|
140
|
+
padding: "6px 16px",
|
|
141
|
+
background: playing ? "#ef4444" : "#3b82f6",
|
|
142
|
+
color: "white",
|
|
143
|
+
border: "none",
|
|
144
|
+
borderRadius: "6px",
|
|
145
|
+
cursor: "pointer",
|
|
146
|
+
fontWeight: 500,
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{playing ? "⏸ Pause" : "▶ Play"}
|
|
150
|
+
</button>
|
|
151
|
+
<label>
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
checked={showComparison}
|
|
155
|
+
onChange={(e) => {
|
|
156
|
+
// Handle comparison toggle - prop would need to be passed
|
|
157
|
+
}}
|
|
158
|
+
style={{ marginRight: "6px" }}
|
|
159
|
+
/>
|
|
160
|
+
Before/After
|
|
161
|
+
</label>
|
|
162
|
+
<span>
|
|
163
|
+
Resolution: {width}×{height}
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|