@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.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * ResourceInspector Component
3
+ *
4
+ * Shows GPU resources: textures, uniforms, buffers, and memory usage.
5
+ */
6
+
7
+ import React from "react";
8
+ import type { FrameGraph } from "@clypra/runtime/planner";
9
+
10
+ export interface ResourceInspectorProps {
11
+ frameGraph: FrameGraph;
12
+ memoryUsage?: number; // bytes
13
+ }
14
+
15
+ export const ResourceInspector: React.FC<ResourceInspectorProps> = ({ frameGraph, memoryUsage = 0 }) => {
16
+ const textures = frameGraph.resourceRequests.filter((r) => r.type === "texture");
17
+ const buffers = frameGraph.resourceRequests.filter((r) => r.type === "buffer");
18
+ const transient = frameGraph.resourceRequests.filter((r) => r.transient);
19
+ const permanent = frameGraph.resourceRequests.filter((r) => !r.transient);
20
+
21
+ const totalMemory = frameGraph.resourceRequests.reduce((sum, resource) => {
22
+ const bytesPerPixel = getBytesPerPixel(resource.format);
23
+ return sum + resource.width * resource.height * bytesPerPixel;
24
+ }, 0);
25
+
26
+ return (
27
+ <div className="resource-inspector" style={{ padding: "16px", fontFamily: "monospace" }}>
28
+ <div style={{ marginBottom: "16px", fontWeight: "bold", fontSize: "14px" }}>Resource Inspector</div>
29
+
30
+ {/* Summary */}
31
+ <div style={{ marginBottom: "16px", padding: "12px", backgroundColor: "#f5f5f5", borderRadius: "6px" }}>
32
+ <div style={{ fontSize: "12px", marginBottom: "4px" }}>
33
+ <span style={{ fontWeight: "600" }}>Total Resources:</span> {frameGraph.resourceRequests.length}
34
+ </div>
35
+ <div style={{ fontSize: "12px", marginBottom: "4px" }}>
36
+ <span style={{ fontWeight: "600" }}>Textures:</span> {textures.length} | <span style={{ fontWeight: "600" }}>Buffers:</span> {buffers.length}
37
+ </div>
38
+ <div style={{ fontSize: "12px", marginBottom: "4px" }}>
39
+ <span style={{ fontWeight: "600" }}>Transient:</span> {transient.length} | <span style={{ fontWeight: "600" }}>Permanent:</span> {permanent.length}
40
+ </div>
41
+ <div style={{ fontSize: "12px", fontWeight: "600", color: "#1976d2" }}>Memory: {formatBytes(totalMemory)}</div>
42
+ </div>
43
+
44
+ {/* Texture List */}
45
+ <div style={{ marginBottom: "16px" }}>
46
+ <div style={{ fontSize: "13px", fontWeight: "600", marginBottom: "8px" }}>Textures ({textures.length})</div>
47
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
48
+ {textures.map((resource) => (
49
+ <ResourceItem key={resource.id} resource={resource} />
50
+ ))}
51
+ </div>
52
+ </div>
53
+
54
+ {/* Buffer List */}
55
+ {buffers.length > 0 && (
56
+ <div>
57
+ <div style={{ fontSize: "13px", fontWeight: "600", marginBottom: "8px" }}>Buffers ({buffers.length})</div>
58
+ <div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
59
+ {buffers.map((resource) => (
60
+ <ResourceItem key={resource.id} resource={resource} />
61
+ ))}
62
+ </div>
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ };
68
+
69
+ interface ResourceItemProps {
70
+ resource: {
71
+ id: string;
72
+ type: "texture" | "buffer";
73
+ width: number;
74
+ height: number;
75
+ format: string;
76
+ transient: boolean;
77
+ };
78
+ }
79
+
80
+ const ResourceItem: React.FC<ResourceItemProps> = ({ resource }) => {
81
+ const bytesPerPixel = getBytesPerPixel(resource.format);
82
+ const size = resource.width * resource.height * bytesPerPixel;
83
+
84
+ return (
85
+ <div
86
+ style={{
87
+ padding: "8px",
88
+ backgroundColor: resource.transient ? "#fff9c4" : "#e3f2fd",
89
+ border: "1px solid #e0e0e0",
90
+ borderRadius: "4px",
91
+ fontSize: "11px",
92
+ }}
93
+ >
94
+ <div style={{ fontWeight: "600", marginBottom: "4px" }}>
95
+ {resource.id}
96
+ {resource.transient && <span style={{ marginLeft: "8px", fontSize: "10px", color: "#f57c00" }}>⏱ Transient</span>}
97
+ </div>
98
+ <div style={{ color: "#666" }}>
99
+ {resource.width}×{resource.height} | {resource.format} | {formatBytes(size)}
100
+ </div>
101
+ </div>
102
+ );
103
+ };
104
+
105
+ function getBytesPerPixel(format: string): number {
106
+ switch (format) {
107
+ case "rgba8":
108
+ return 4;
109
+ case "rgba16f":
110
+ return 8;
111
+ case "rgba32f":
112
+ return 16;
113
+ case "r8":
114
+ return 1;
115
+ case "depth24":
116
+ return 3;
117
+ default:
118
+ return 4;
119
+ }
120
+ }
121
+
122
+ function formatBytes(bytes: number): string {
123
+ if (bytes < 1024) return `${bytes} B`;
124
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
125
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
126
+ }
@@ -0,0 +1,2 @@
1
+ export { ResourceInspector } from "./ResourceInspector";
2
+ export type { ResourceInspectorProps } from "./ResourceInspector";
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Timeline Component
3
+ *
4
+ * Draggable playhead with frame-accurate scrubbing.
5
+ * Supports keyboard shortcuts for navigation.
6
+ */
7
+
8
+ import React, { useRef, useState, useEffect } from "react";
9
+
10
+ export interface TimelineProps {
11
+ /** Total duration in seconds */
12
+ duration: number;
13
+ /** Current playback time in seconds */
14
+ currentTime: number;
15
+ /** Callback when user seeks to a new time */
16
+ onSeek: (time: number) => void;
17
+ /** Frame rate for frame-accurate navigation */
18
+ frameRate?: number;
19
+ /** Show frame numbers instead of timecodes */
20
+ showFrames?: boolean;
21
+ /** Height of the timeline in pixels */
22
+ height?: number;
23
+ }
24
+
25
+ export function Timeline({ duration, currentTime, onSeek, frameRate = 60, showFrames = false, height = 80 }: TimelineProps) {
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+ const [isDragging, setIsDragging] = useState(false);
28
+ const [hoverTime, setHoverTime] = useState<number | null>(null);
29
+
30
+ const currentFrame = Math.round(currentTime * frameRate);
31
+ const totalFrames = Math.round(duration * frameRate);
32
+
33
+ // Format time as MM:SS:FF or frame number
34
+ const formatTime = (time: number): string => {
35
+ if (showFrames) {
36
+ return `Frame ${Math.round(time * frameRate)}`;
37
+ }
38
+
39
+ const totalSeconds = Math.floor(time);
40
+ const frames = Math.round((time - totalSeconds) * frameRate);
41
+ const minutes = Math.floor(totalSeconds / 60);
42
+ const seconds = totalSeconds % 60;
43
+
44
+ return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`;
45
+ };
46
+
47
+ // Convert mouse position to time
48
+ const getTimeFromMouseEvent = (e: React.MouseEvent | MouseEvent): number => {
49
+ if (!containerRef.current) return 0;
50
+
51
+ const rect = containerRef.current.getBoundingClientRect();
52
+ const x = e.clientX - rect.left;
53
+ const progress = Math.max(0, Math.min(1, x / rect.width));
54
+ return progress * duration;
55
+ };
56
+
57
+ // Handle mouse down to start dragging
58
+ const handleMouseDown = (e: React.MouseEvent) => {
59
+ e.preventDefault();
60
+ setIsDragging(true);
61
+ const time = getTimeFromMouseEvent(e);
62
+ onSeek(time);
63
+ };
64
+
65
+ // Handle mouse move for dragging and hover
66
+ const handleMouseMove = (e: React.MouseEvent) => {
67
+ const time = getTimeFromMouseEvent(e);
68
+ setHoverTime(time);
69
+
70
+ if (isDragging) {
71
+ onSeek(time);
72
+ }
73
+ };
74
+
75
+ const handleMouseLeave = () => {
76
+ setHoverTime(null);
77
+ };
78
+
79
+ // Handle global mouse events while dragging
80
+ useEffect(() => {
81
+ if (!isDragging) return;
82
+
83
+ const handleGlobalMouseMove = (e: MouseEvent) => {
84
+ if (!containerRef.current) return;
85
+ const time = getTimeFromMouseEvent(e);
86
+ onSeek(time);
87
+ };
88
+
89
+ const handleGlobalMouseUp = () => {
90
+ setIsDragging(false);
91
+ };
92
+
93
+ document.addEventListener("mousemove", handleGlobalMouseMove);
94
+ document.addEventListener("mouseup", handleGlobalMouseUp);
95
+
96
+ return () => {
97
+ document.removeEventListener("mousemove", handleGlobalMouseMove);
98
+ document.removeEventListener("mouseup", handleGlobalMouseUp);
99
+ };
100
+ }, [isDragging, onSeek, duration]);
101
+
102
+ // Keyboard shortcuts for frame navigation
103
+ useEffect(() => {
104
+ const handleKeyDown = (e: KeyboardEvent) => {
105
+ const frameDuration = 1 / frameRate;
106
+
107
+ switch (e.key) {
108
+ case "ArrowLeft":
109
+ e.preventDefault();
110
+ onSeek(Math.max(0, currentTime - frameDuration));
111
+ break;
112
+ case "ArrowRight":
113
+ e.preventDefault();
114
+ onSeek(Math.min(duration, currentTime + frameDuration));
115
+ break;
116
+ case "Home":
117
+ e.preventDefault();
118
+ onSeek(0);
119
+ break;
120
+ case "End":
121
+ e.preventDefault();
122
+ onSeek(duration);
123
+ break;
124
+ }
125
+ };
126
+
127
+ document.addEventListener("keydown", handleKeyDown);
128
+ return () => document.removeEventListener("keydown", handleKeyDown);
129
+ }, [currentTime, duration, frameRate, onSeek]);
130
+
131
+ const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
132
+ const hoverProgress = hoverTime !== null && duration > 0 ? (hoverTime / duration) * 100 : null;
133
+
134
+ return (
135
+ <div
136
+ style={{
137
+ width: "100%",
138
+ padding: "16px",
139
+ background: "#0f172a",
140
+ borderRadius: "8px",
141
+ border: "1px solid #334155",
142
+ }}
143
+ >
144
+ {/* Timeline header */}
145
+ <div
146
+ style={{
147
+ display: "flex",
148
+ justifyContent: "space-between",
149
+ marginBottom: "12px",
150
+ fontSize: "14px",
151
+ color: "#94a3b8",
152
+ }}
153
+ >
154
+ <span>{formatTime(currentTime)}</span>
155
+ <span>{formatTime(duration)}</span>
156
+ </div>
157
+
158
+ {/* Timeline track */}
159
+ <div
160
+ ref={containerRef}
161
+ onMouseDown={handleMouseDown}
162
+ onMouseMove={handleMouseMove}
163
+ onMouseLeave={handleMouseLeave}
164
+ style={{
165
+ position: "relative",
166
+ height: `${height}px`,
167
+ background: "#1e293b",
168
+ borderRadius: "6px",
169
+ cursor: isDragging ? "grabbing" : "grab",
170
+ overflow: "hidden",
171
+ }}
172
+ >
173
+ {/* Progress bar */}
174
+ <div
175
+ style={{
176
+ position: "absolute",
177
+ left: 0,
178
+ top: 0,
179
+ bottom: 0,
180
+ width: `${progress}%`,
181
+ background: "linear-gradient(90deg, #3b82f6 0%, #2563eb 100%)",
182
+ transition: isDragging ? "none" : "width 0.1s ease-out",
183
+ }}
184
+ />
185
+
186
+ {/* Hover indicator */}
187
+ {hoverProgress !== null && !isDragging && (
188
+ <div
189
+ style={{
190
+ position: "absolute",
191
+ left: `${hoverProgress}%`,
192
+ top: 0,
193
+ bottom: 0,
194
+ width: "2px",
195
+ background: "#f59e0b",
196
+ pointerEvents: "none",
197
+ }}
198
+ >
199
+ <div
200
+ style={{
201
+ position: "absolute",
202
+ bottom: "100%",
203
+ left: "50%",
204
+ transform: "translateX(-50%)",
205
+ padding: "4px 8px",
206
+ background: "#f59e0b",
207
+ color: "#000",
208
+ fontSize: "12px",
209
+ fontWeight: 600,
210
+ borderRadius: "4px",
211
+ marginBottom: "4px",
212
+ whiteSpace: "nowrap",
213
+ }}
214
+ >
215
+ {hoverTime !== null && formatTime(hoverTime)}
216
+ </div>
217
+ </div>
218
+ )}
219
+
220
+ {/* Playhead */}
221
+ <div
222
+ style={{
223
+ position: "absolute",
224
+ left: `${progress}%`,
225
+ top: 0,
226
+ bottom: 0,
227
+ width: "3px",
228
+ background: "#ef4444",
229
+ transform: "translateX(-50%)",
230
+ pointerEvents: "none",
231
+ }}
232
+ >
233
+ {/* Playhead handle */}
234
+ <div
235
+ style={{
236
+ position: "absolute",
237
+ top: "50%",
238
+ left: "50%",
239
+ transform: "translate(-50%, -50%)",
240
+ width: "16px",
241
+ height: "16px",
242
+ background: "#ef4444",
243
+ border: "2px solid #fff",
244
+ borderRadius: "50%",
245
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)",
246
+ }}
247
+ />
248
+ </div>
249
+
250
+ {/* Frame markers */}
251
+ <div
252
+ style={{
253
+ position: "absolute",
254
+ left: 0,
255
+ right: 0,
256
+ bottom: 0,
257
+ height: "20px",
258
+ display: "flex",
259
+ alignItems: "flex-end",
260
+ pointerEvents: "none",
261
+ }}
262
+ >
263
+ {Array.from({ length: 11 }, (_, i) => i * 10).map((percent) => (
264
+ <div
265
+ key={percent}
266
+ style={{
267
+ position: "absolute",
268
+ left: `${percent}%`,
269
+ height: percent % 20 === 0 ? "12px" : "6px",
270
+ width: "1px",
271
+ background: "#475569",
272
+ }}
273
+ />
274
+ ))}
275
+ </div>
276
+ </div>
277
+
278
+ {/* Timeline footer */}
279
+ <div
280
+ style={{
281
+ marginTop: "8px",
282
+ fontSize: "12px",
283
+ color: "#64748b",
284
+ textAlign: "center",
285
+ }}
286
+ >
287
+ {showFrames ? `Frame ${currentFrame} / ${totalFrames}` : `${frameRate} FPS • Use ← → arrows for frame navigation`}
288
+ </div>
289
+ </div>
290
+ );
291
+ }
@@ -0,0 +1,2 @@
1
+ export { Timeline } from "./Timeline";
2
+ export type { TimelineProps } from "./Timeline";