@principal-ai/logo-component 0.1.4 → 0.1.6

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,431 @@
1
+ import React, { useMemo, useRef, useState, useEffect } from "react";
2
+ import opentype from "opentype.js";
3
+ let globalIdCounter = 0;
4
+ /**
5
+ * Estimates path length from SVG path data including curves.
6
+ */
7
+ function estimatePathLength(d) {
8
+ var _a;
9
+ // Simple estimation - count segments and approximate
10
+ const commands = d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/gi) || [];
11
+ let length = 0;
12
+ let lastX = 0, lastY = 0;
13
+ let startX = 0, startY = 0;
14
+ for (const cmd of commands) {
15
+ const type = cmd[0].toUpperCase();
16
+ const nums = ((_a = cmd.slice(1).match(/-?\d+\.?\d*/g)) === null || _a === void 0 ? void 0 : _a.map(Number)) || [];
17
+ switch (type) {
18
+ case 'M':
19
+ lastX = nums[0] || 0;
20
+ lastY = nums[1] || 0;
21
+ startX = lastX;
22
+ startY = lastY;
23
+ break;
24
+ case 'L':
25
+ if (nums.length >= 2) {
26
+ length += Math.sqrt((nums[0] - lastX) ** 2 + (nums[1] - lastY) ** 2);
27
+ lastX = nums[0];
28
+ lastY = nums[1];
29
+ }
30
+ break;
31
+ case 'H':
32
+ if (nums.length >= 1) {
33
+ length += Math.abs(nums[0] - lastX);
34
+ lastX = nums[0];
35
+ }
36
+ break;
37
+ case 'V':
38
+ if (nums.length >= 1) {
39
+ length += Math.abs(nums[0] - lastY);
40
+ lastY = nums[0];
41
+ }
42
+ break;
43
+ case 'C':
44
+ // Cubic bezier - approximate with chord length * 1.5
45
+ if (nums.length >= 6) {
46
+ const dx = nums[4] - lastX;
47
+ const dy = nums[5] - lastY;
48
+ length += Math.sqrt(dx * dx + dy * dy) * 1.5;
49
+ lastX = nums[4];
50
+ lastY = nums[5];
51
+ }
52
+ break;
53
+ case 'Q':
54
+ // Quadratic bezier - approximate with chord length * 1.3
55
+ if (nums.length >= 4) {
56
+ const dx = nums[2] - lastX;
57
+ const dy = nums[3] - lastY;
58
+ length += Math.sqrt(dx * dx + dy * dy) * 1.3;
59
+ lastX = nums[2];
60
+ lastY = nums[3];
61
+ }
62
+ break;
63
+ case 'Z':
64
+ length += Math.sqrt((startX - lastX) ** 2 + (startY - lastY) ** 2);
65
+ lastX = startX;
66
+ lastY = startY;
67
+ break;
68
+ }
69
+ }
70
+ return length || 100;
71
+ }
72
+ /**
73
+ * Get start and end points from path
74
+ */
75
+ function getPathEndpoints(d) {
76
+ const coords = d.match(/-?\d+\.?\d*/g);
77
+ if (!coords || coords.length < 2) {
78
+ return { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } };
79
+ }
80
+ // Start is first two coords (after M)
81
+ const start = { x: parseFloat(coords[0]), y: parseFloat(coords[1]) };
82
+ // End is last two coords before Z (or just last two)
83
+ const end = {
84
+ x: parseFloat(coords[coords.length - 2]),
85
+ y: parseFloat(coords[coords.length - 1])
86
+ };
87
+ return { start, end };
88
+ }
89
+ /**
90
+ * Seeded random for consistent chaos patterns
91
+ */
92
+ function seededRandom(seed) {
93
+ return () => {
94
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
95
+ return seed / 0x7fffffff;
96
+ };
97
+ }
98
+ /**
99
+ * Generate time-series chart positions for dots
100
+ */
101
+ function generateChartPositions(numDots, width, height, pattern, seed) {
102
+ if (numDots === 0)
103
+ return [];
104
+ const random = seededRandom(seed);
105
+ const positions = [];
106
+ // Chart area with padding
107
+ const padding = width * 0.1;
108
+ const chartWidth = width - padding * 2;
109
+ const chartHeight = height * 0.6;
110
+ const chartTop = height * 0.2;
111
+ // Generate y-values based on pattern
112
+ const yValues = [];
113
+ switch (pattern) {
114
+ case "latency": {
115
+ // Mostly stable baseline with occasional spikes
116
+ const baseline = 0.3;
117
+ const numSpikes = Math.max(1, Math.floor(numDots * 0.15)); // ~15% are spikes
118
+ const spikeIndices = new Set();
119
+ // Pick random spike positions
120
+ while (spikeIndices.size < numSpikes) {
121
+ spikeIndices.add(Math.floor(random() * numDots));
122
+ }
123
+ for (let i = 0; i < numDots; i++) {
124
+ if (spikeIndices.has(i)) {
125
+ // Spike: 60-95% height
126
+ yValues.push(0.6 + random() * 0.35);
127
+ }
128
+ else {
129
+ // Baseline with small noise: 20-40%
130
+ yValues.push(baseline + (random() - 0.5) * 0.2);
131
+ }
132
+ }
133
+ break;
134
+ }
135
+ case "cpu": {
136
+ // Sine wave foundation with noise - gradual rises and drops
137
+ const frequency = 2 + random() * 2; // 2-4 cycles across the chart
138
+ for (let i = 0; i < numDots; i++) {
139
+ const t = i / (numDots - 1 || 1);
140
+ const sine = Math.sin(t * Math.PI * frequency) * 0.3;
141
+ const noise = (random() - 0.5) * 0.15;
142
+ yValues.push(0.5 + sine + noise);
143
+ }
144
+ break;
145
+ }
146
+ case "noise":
147
+ default: {
148
+ // Irregular fluctuations using smoothed noise
149
+ let value = 0.5;
150
+ for (let i = 0; i < numDots; i++) {
151
+ // Random walk with mean reversion
152
+ const drift = (0.5 - value) * 0.1; // Pull toward center
153
+ const noise = (random() - 0.5) * 0.25;
154
+ value = Math.max(0.15, Math.min(0.85, value + drift + noise));
155
+ yValues.push(value);
156
+ }
157
+ break;
158
+ }
159
+ }
160
+ // Convert to positions
161
+ for (let i = 0; i < numDots; i++) {
162
+ const x = padding + (i / (numDots - 1 || 1)) * chartWidth;
163
+ // Invert y since SVG y increases downward
164
+ const y = chartTop + (1 - yValues[i]) * chartHeight;
165
+ positions.push({ x, y });
166
+ }
167
+ return positions;
168
+ }
169
+ /**
170
+ * Split compound path into individual contours
171
+ */
172
+ function splitPathIntoContours(d) {
173
+ const contours = [];
174
+ const parts = d.split(/(?=[Mm])/);
175
+ for (const part of parts) {
176
+ const trimmed = part.trim();
177
+ if (trimmed) {
178
+ contours.push(trimmed);
179
+ }
180
+ }
181
+ return contours;
182
+ }
183
+ export const OpenTypeTextReveal = ({ text, fontUrl, fontSize = 72, width = 600, height = 150, centerText = true, showChartIntro, chartDuration = 1.5, chartLineFadeDuration = 0.5, chartPauseDuration = 0.3, chartTransitionDuration = 0.8, chartPattern = "latency", chaosMode = "none", chaosDuration = 2, dotsDuration = 1.5, flowDuration = 2, flowDelay = 0.3, particlesPerPath = 1, particleRadius = 2, color = "currentColor", particleColor, strokeWidth = 1.5, opacity = 0.9, showGlow = true, fadeAfterAssembly = true, fadeOpacity = 0.5, loop = true, loopDelay = 1, }) => {
184
+ const idRef = useRef(null);
185
+ if (idRef.current === null) {
186
+ idRef.current = `otr${globalIdCounter++}`;
187
+ }
188
+ const uniqueId = idRef.current;
189
+ const finalParticleColor = particleColor || color;
190
+ const isFragmented = chaosMode === "fragmented";
191
+ // Chart intro is on by default when fragmented mode is active
192
+ const shouldShowChartIntro = showChartIntro !== null && showChartIntro !== void 0 ? showChartIntro : isFragmented;
193
+ // Font and path state
194
+ const [font, setFont] = useState(null);
195
+ const [error, setError] = useState(null);
196
+ const [cycle, setCycle] = useState(0);
197
+ // Load font
198
+ useEffect(() => {
199
+ let cancelled = false;
200
+ opentype.load(fontUrl)
201
+ .then((loadedFont) => {
202
+ if (!cancelled) {
203
+ setFont(loadedFont);
204
+ setError(null);
205
+ }
206
+ })
207
+ .catch((err) => {
208
+ if (!cancelled) {
209
+ setError(err.message || "Failed to load font");
210
+ }
211
+ });
212
+ return () => {
213
+ cancelled = true;
214
+ };
215
+ }, [fontUrl]);
216
+ // Generate paths from font
217
+ const { paths, textWidth, textHeight } = useMemo(() => {
218
+ if (!font) {
219
+ return { paths: [], textWidth: 0, textHeight: fontSize };
220
+ }
221
+ // Get the path for the text
222
+ const textPath = font.getPath(text, 0, fontSize, fontSize);
223
+ const pathData = textPath.toPathData(2); // 2 decimal places
224
+ // Split into individual contours (each letter shape)
225
+ const contours = splitPathIntoContours(pathData);
226
+ const pathInfos = contours.map((d, index) => ({
227
+ d,
228
+ id: `contour-${index}`,
229
+ length: estimatePathLength(d),
230
+ endpoints: getPathEndpoints(d),
231
+ }));
232
+ // Calculate bounds
233
+ const bbox = textPath.getBoundingBox();
234
+ return {
235
+ paths: pathInfos,
236
+ textWidth: bbox.x2 - bbox.x1,
237
+ textHeight: bbox.y2 - bbox.y1,
238
+ };
239
+ }, [font, text, fontSize]);
240
+ // Calculate centering offset
241
+ const offsetX = centerText ? (width - textWidth) / 2 : 0;
242
+ const offsetY = centerText ? (height - textHeight) / 2 - (fontSize * 0.2) : 0;
243
+ // Apply offset to paths
244
+ const resolvedPaths = useMemo(() => {
245
+ return paths.map((path) => (Object.assign(Object.assign({}, path), { d: offsetPathData(path.d, offsetX, offsetY), endpoints: {
246
+ start: { x: path.endpoints.start.x + offsetX, y: path.endpoints.start.y + offsetY },
247
+ end: { x: path.endpoints.end.x + offsetX, y: path.endpoints.end.y + offsetY },
248
+ } })));
249
+ }, [paths, offsetX, offsetY]);
250
+ // Generate fragment offsets
251
+ const fragmentOffsets = useMemo(() => {
252
+ const random = seededRandom(42);
253
+ return resolvedPaths.map(() => ({
254
+ x: (random() - 0.5) * 150,
255
+ y: (random() - 0.5) * 100,
256
+ }));
257
+ }, [resolvedPaths]);
258
+ // Generate chart positions for the intro animation
259
+ const chartPositions = useMemo(() => {
260
+ if (!shouldShowChartIntro)
261
+ return [];
262
+ return generateChartPositions(resolvedPaths.length, width, height, chartPattern, 123);
263
+ }, [shouldShowChartIntro, resolvedPaths.length, width, height, chartPattern]);
264
+ // Target positions for dots after chart phase
265
+ // Always go to final letter positions (path start points)
266
+ const dotTargetPositions = useMemo(() => {
267
+ return resolvedPaths.map((path) => path.endpoints.start);
268
+ }, [resolvedPaths]);
269
+ // Chart intro timing
270
+ const chartPhaseEndTime = shouldShowChartIntro
271
+ ? chartDuration + chartLineFadeDuration + chartPauseDuration + chartTransitionDuration
272
+ : 0;
273
+ // Timing calculations (offset by chart phase if present)
274
+ const numPaths = resolvedPaths.length || 1;
275
+ const perItemDotsDuration = (dotsDuration * 0.5) / numPaths;
276
+ const perItemLinesDuration = (dotsDuration * 0.5) / numPaths;
277
+ const perItemAssemblyDuration = chaosDuration / numPaths;
278
+ const dotsPhaseEnd = chartPhaseEndTime + dotsDuration * 0.5;
279
+ const linesPhaseEnd = chartPhaseEndTime + dotsDuration;
280
+ // Skip assembly phase when chart intro handles the transition
281
+ const needsAssembly = isFragmented && !shouldShowChartIntro;
282
+ const assemblyEndTime = needsAssembly
283
+ ? chartPhaseEndTime + dotsDuration + chaosDuration
284
+ : chartPhaseEndTime + dotsDuration;
285
+ const flowBeginTime = assemblyEndTime + flowDelay;
286
+ const totalDuration = flowBeginTime + flowDuration;
287
+ // Loop via remount
288
+ useEffect(() => {
289
+ if (!loop)
290
+ return;
291
+ const timeout = setTimeout(() => {
292
+ setCycle((c) => c + 1);
293
+ }, (totalDuration + loopDelay) * 1000);
294
+ return () => clearTimeout(timeout);
295
+ }, [loop, totalDuration, loopDelay, cycle]);
296
+ if (error) {
297
+ return (React.createElement("svg", { width: width, height: height },
298
+ React.createElement("text", { x: 10, y: 30, fill: "red", fontSize: 14 },
299
+ "Error: ",
300
+ error)));
301
+ }
302
+ if (!font) {
303
+ return (React.createElement("svg", { width: width, height: height },
304
+ React.createElement("text", { x: 10, y: 30, fill: color, fontSize: 14 }, "Loading font...")));
305
+ }
306
+ return (React.createElement("svg", { key: cycle, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", style: { opacity } },
307
+ React.createElement("defs", null,
308
+ showGlow && (React.createElement("filter", { id: `glow-${uniqueId}`, x: "-50%", y: "-50%", width: "200%", height: "200%" },
309
+ React.createElement("feGaussianBlur", { stdDeviation: "2", result: "blur" }),
310
+ React.createElement("feMerge", null,
311
+ React.createElement("feMergeNode", { in: "blur" }),
312
+ React.createElement("feMergeNode", { in: "SourceGraphic" })))),
313
+ React.createElement("radialGradient", { id: `particleGradient-${uniqueId}`, cx: "50%", cy: "50%", r: "50%" },
314
+ React.createElement("stop", { offset: "0%", style: { stopColor: finalParticleColor, stopOpacity: 1 } }),
315
+ React.createElement("stop", { offset: "100%", style: { stopColor: finalParticleColor, stopOpacity: 0.3 } }))),
316
+ shouldShowChartIntro && chartPositions.length > 0 && (React.createElement("g", { className: "chart-intro" },
317
+ React.createElement("polyline", { points: chartPositions.map(p => `${p.x},${p.y}`).join(' '), fill: "none", stroke: color, strokeWidth: strokeWidth * 0.75, strokeLinecap: "round", strokeLinejoin: "round", opacity: "0" },
318
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "0.6", dur: "0.3s", begin: "0s", fill: "freeze" }),
319
+ React.createElement("animate", { attributeName: "opacity", from: "0.6", to: "0", dur: `${chartLineFadeDuration}s`, begin: `${chartDuration}s`, fill: "freeze" })),
320
+ chartPositions.map((chartPos, index) => {
321
+ const targetPos = dotTargetPositions[index];
322
+ if (!targetPos)
323
+ return null;
324
+ const dotRadius = strokeWidth;
325
+ const dotAppearDelay = (index / chartPositions.length) * 0.5; // Stagger appearance
326
+ const transitionBegin = chartDuration + chartLineFadeDuration + chartPauseDuration;
327
+ return (React.createElement("circle", { key: `chart-dot-${index}`, cx: chartPos.x, cy: chartPos.y, r: dotRadius, fill: color, opacity: "0" },
328
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${dotAppearDelay}s`, fill: "freeze" }),
329
+ React.createElement("animate", { attributeName: "cx", from: chartPos.x, to: targetPos.x, dur: `${chartTransitionDuration}s`, begin: `${transitionBegin}s`, fill: "freeze", calcMode: "spline", keySplines: "0.4 0 0.2 1", keyTimes: "0;1" }),
330
+ React.createElement("animate", { attributeName: "cy", from: chartPos.y, to: targetPos.y, dur: `${chartTransitionDuration}s`, begin: `${transitionBegin}s`, fill: "freeze", calcMode: "spline", keySplines: "0.4 0 0.2 1", keyTimes: "0;1" }),
331
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })));
332
+ }))),
333
+ React.createElement("g", { className: "opentype-paths" }, resolvedPaths.map((path, index) => {
334
+ const offset = fragmentOffsets[index];
335
+ const dotRadius = strokeWidth / 2;
336
+ // Skip fragment offset when chart intro handles the transition
337
+ const useFragmentOffset = isFragmented && !shouldShowChartIntro;
338
+ return (React.createElement("g", { key: path.id, transform: useFragmentOffset ? `translate(${offset.x}, ${offset.y})` : undefined },
339
+ isFragmented && !shouldShowChartIntro && (React.createElement(React.Fragment, null,
340
+ React.createElement("circle", { cx: path.endpoints.start.x, cy: path.endpoints.start.y, r: dotRadius, fill: color, opacity: "0" },
341
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${chartPhaseEndTime + index * perItemDotsDuration}s`, fill: "freeze" }),
342
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })))),
343
+ (() => {
344
+ // Determine animation mode
345
+ const shouldDrawLines = isFragmented || shouldShowChartIntro;
346
+ const drawDuration = shouldShowChartIntro && !isFragmented
347
+ ? perItemLinesDuration
348
+ : perItemLinesDuration + chaosDuration;
349
+ return (React.createElement("path", { d: path.d, fill: "none", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", opacity: shouldDrawLines ? 0 : 1, strokeDasharray: shouldDrawLines ? path.length : undefined, strokeDashoffset: shouldDrawLines ? path.length : undefined },
350
+ shouldDrawLines && (React.createElement(React.Fragment, null,
351
+ React.createElement("animate", { attributeName: "opacity", values: `0;1;1;${fadeAfterAssembly ? fadeOpacity : 1}`, keyTimes: "0;0.1;0.9;1", dur: `${drawDuration}s`, begin: `${dotsPhaseEnd + index * perItemLinesDuration}s`, fill: "freeze" }),
352
+ React.createElement("animate", { attributeName: "stroke-dashoffset", from: path.length, to: "0", dur: `${perItemLinesDuration}s`, begin: `${dotsPhaseEnd + index * perItemLinesDuration}s`, fill: "freeze", calcMode: "spline", keySplines: "0.4 0 0.2 1", keyTimes: "0;1" }))),
353
+ !shouldDrawLines && fadeAfterAssembly && (React.createElement("animate", { attributeName: "opacity", from: "1", to: fadeOpacity, dur: "0.5s", begin: `${flowBeginTime}s`, fill: "freeze" }))));
354
+ })(),
355
+ useFragmentOffset && (React.createElement("animateTransform", { attributeName: "transform", type: "translate", from: `${offset.x} ${offset.y}`, to: "0 0", dur: `${perItemAssemblyDuration}s`, begin: `${linesPhaseEnd + index * perItemAssemblyDuration}s`, fill: "freeze", calcMode: "spline", keySplines: "0.33 0 0.2 1", keyTimes: "0;1" }))));
356
+ })),
357
+ React.createElement("g", { className: "opentype-particles", filter: showGlow ? `url(#glow-${uniqueId})` : undefined }, resolvedPaths.flatMap((path, pathIndex) => {
358
+ return Array.from({ length: particlesPerPath }).map((_, particleIndex) => {
359
+ const particleDelay = (particleIndex / particlesPerPath) * flowDuration;
360
+ const beginTime = flowBeginTime + particleDelay;
361
+ return (React.createElement("circle", { key: `particle-${pathIndex}-${particleIndex}`, r: particleRadius, fill: `url(#particleGradient-${uniqueId})`, opacity: "0" },
362
+ React.createElement("animateMotion", { dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze", path: path.d }),
363
+ React.createElement("animate", { attributeName: "opacity", values: "0;1;1;0", keyTimes: "0;0.1;0.9;1", dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze" })));
364
+ });
365
+ }))));
366
+ };
367
+ /**
368
+ * Offset all coordinates in path data
369
+ */
370
+ function offsetPathData(d, offsetX, offsetY) {
371
+ let result = "";
372
+ let i = 0;
373
+ let coordIndex = 0;
374
+ while (i < d.length) {
375
+ const char = d[i];
376
+ // Check for command letters
377
+ if (/[MmLlHhVvCcSsQqTtAaZz]/.test(char)) {
378
+ result += char;
379
+ // Reset coord tracking for absolute commands, or handle relative
380
+ if (char === 'Z' || char === 'z') {
381
+ coordIndex = 0;
382
+ }
383
+ else if (char === 'H' || char === 'h') {
384
+ // Horizontal - only X coords
385
+ coordIndex = -1; // Special handling
386
+ }
387
+ else if (char === 'V' || char === 'v') {
388
+ // Vertical - only Y coords
389
+ coordIndex = -2; // Special handling
390
+ }
391
+ else {
392
+ coordIndex = 0;
393
+ }
394
+ i++;
395
+ }
396
+ else if (/[-\d.]/.test(char)) {
397
+ // Parse number
398
+ let numStr = "";
399
+ while (i < d.length && /[-\d.eE]/.test(d[i])) {
400
+ numStr += d[i];
401
+ i++;
402
+ }
403
+ const num = parseFloat(numStr);
404
+ // Apply offset based on coordinate type
405
+ if (coordIndex === -1) {
406
+ // H command - X only
407
+ result += String(num + offsetX);
408
+ }
409
+ else if (coordIndex === -2) {
410
+ // V command - Y only
411
+ result += String(num + offsetY);
412
+ }
413
+ else if (coordIndex % 2 === 0) {
414
+ // X coordinate
415
+ result += String(num + offsetX);
416
+ }
417
+ else {
418
+ // Y coordinate
419
+ result += String(num + offsetY);
420
+ }
421
+ if (coordIndex >= 0)
422
+ coordIndex++;
423
+ }
424
+ else {
425
+ // Whitespace or comma
426
+ result += char;
427
+ i++;
428
+ }
429
+ }
430
+ return result;
431
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ export const SquareLogo = ({ width = 150, height = 150, colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#96CEB4', '#FFEAA7', '#DDA15E', '#BC6C25', '#E76F51'], opacity = 0.9, }) => {
3
+ const rectangleStyle = {
4
+ transition: 'all 0.3s ease',
5
+ };
6
+ return (React.createElement("svg", { width: width, height: height, viewBox: "0 0 100 100", xmlns: "http://www.w3.org/2000/svg", style: { opacity } },
7
+ React.createElement("defs", null,
8
+ React.createElement("filter", { id: "glow" },
9
+ React.createElement("feGaussianBlur", { stdDeviation: "1.5", result: "coloredBlur" }),
10
+ React.createElement("feMerge", null,
11
+ React.createElement("feMergeNode", { in: "coloredBlur" }),
12
+ React.createElement("feMergeNode", { in: "SourceGraphic" })))),
13
+ React.createElement("rect", { x: "10", y: "10", width: "10", height: "15", fill: colors[1], style: rectangleStyle, filter: "url(#glow)" },
14
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite" })),
15
+ React.createElement("rect", { x: "20", y: "10", width: "15", height: "80", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
16
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "0.3s" })),
17
+ React.createElement("rect", { x: "35", y: "10", width: "30", height: "12", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
18
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "0.6s" })),
19
+ React.createElement("rect", { x: "65", y: "10", width: "25", height: "12", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
20
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "0.9s" })),
21
+ React.createElement("rect", { x: "35", y: "22", width: "15", height: "18", fill: colors[3], style: rectangleStyle, filter: "url(#glow)" },
22
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "1.2s" })),
23
+ React.createElement("rect", { x: "50", y: "22", width: "15", height: "18", fill: colors[4], style: rectangleStyle, filter: "url(#glow)" },
24
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "1.5s" })),
25
+ React.createElement("rect", { x: "65", y: "22", width: "12", height: "18", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
26
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "1.8s" })),
27
+ React.createElement("rect", { x: "77", y: "22", width: "13", height: "28", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
28
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "2.1s" })),
29
+ React.createElement("rect", { x: "77", y: "50", width: "13", height: "40", fill: colors[5], style: rectangleStyle, filter: "url(#glow)" },
30
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "2.1s" })),
31
+ React.createElement("rect", { x: "35", y: "40", width: "55", height: "10", fill: colors[0], style: rectangleStyle, filter: "url(#glow)" },
32
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "2.4s" })),
33
+ React.createElement("rect", { x: "10", y: "25", width: "10", height: "35", fill: colors[6], style: rectangleStyle, filter: "url(#glow)" },
34
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "2.7s" })),
35
+ React.createElement("rect", { x: "35", y: "50", width: "20", height: "20", fill: colors[7], style: rectangleStyle, filter: "url(#glow)" },
36
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "0.3s" })),
37
+ React.createElement("rect", { x: "55", y: "50", width: "22", height: "20", fill: colors[8], style: rectangleStyle, filter: "url(#glow)" },
38
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "0.6s" })),
39
+ React.createElement("rect", { x: "10", y: "60", width: "10", height: "30", fill: colors[1], style: rectangleStyle, filter: "url(#glow)" },
40
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "0.9s" })),
41
+ React.createElement("rect", { x: "35", y: "70", width: "25", height: "20", fill: colors[2], style: rectangleStyle, filter: "url(#glow)" },
42
+ React.createElement("animate", { attributeName: "opacity", values: "1;0.8;1", dur: "3s", repeatCount: "indefinite", begin: "1.2s" })),
43
+ React.createElement("rect", { x: "60", y: "70", width: "17", height: "20", fill: colors[3], style: rectangleStyle, filter: "url(#glow)" },
44
+ React.createElement("animate", { attributeName: "opacity", values: "0.8;1;0.8", dur: "3s", repeatCount: "indefinite", begin: "1.5s" })),
45
+ React.createElement("line", { x1: "20", y1: "10", x2: "20", y2: "90", stroke: "rgba(255, 255, 255, 0.15)", strokeWidth: "0.5" }),
46
+ React.createElement("line", { x1: "35", y1: "10", x2: "35", y2: "90", stroke: "rgba(255, 255, 255, 0.15)", strokeWidth: "0.5" }),
47
+ React.createElement("line", { x1: "10", y1: "22", x2: "90", y2: "22", stroke: "rgba(255, 255, 255, 0.15)", strokeWidth: "0.5" }),
48
+ React.createElement("line", { x1: "10", y1: "40", x2: "90", y2: "40", stroke: "rgba(255, 255, 255, 0.15)", strokeWidth: "0.5" }),
49
+ React.createElement("line", { x1: "10", y1: "50", x2: "90", y2: "50", stroke: "rgba(255, 255, 255, 0.15)", strokeWidth: "0.5" })));
50
+ };
@@ -0,0 +1,148 @@
1
+ import React, { useMemo, useRef, useState, useEffect } from "react";
2
+ import { TELEMETRY_PRESETS } from "./presets";
3
+ let globalIdCounter = 0;
4
+ /**
5
+ * Estimates path length from simple SVG path data (M/L commands).
6
+ */
7
+ function estimatePathLength(d) {
8
+ const coords = d.match(/-?\d+\.?\d*/g);
9
+ if (!coords || coords.length < 4)
10
+ return 100;
11
+ let totalLength = 0;
12
+ for (let i = 2; i < coords.length; i += 2) {
13
+ const x1 = parseFloat(coords[i - 2]);
14
+ const y1 = parseFloat(coords[i - 1]);
15
+ const x2 = parseFloat(coords[i]);
16
+ const y2 = parseFloat(coords[i + 1]);
17
+ totalLength += Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
18
+ }
19
+ return totalLength || 100;
20
+ }
21
+ /**
22
+ * Parse start and end points from path data (M x1,y1 L x2,y2)
23
+ */
24
+ function getPathEndpoints(d) {
25
+ const coords = d.match(/-?\d+\.?\d*/g);
26
+ if (!coords || coords.length < 4) {
27
+ return { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } };
28
+ }
29
+ return {
30
+ start: { x: parseFloat(coords[0]), y: parseFloat(coords[1]) },
31
+ end: { x: parseFloat(coords[coords.length - 2]), y: parseFloat(coords[coords.length - 1]) },
32
+ };
33
+ }
34
+ /**
35
+ * Seeded random for consistent chaos patterns
36
+ */
37
+ function seededRandom(seed) {
38
+ return () => {
39
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
40
+ return seed / 0x7fffffff;
41
+ };
42
+ }
43
+ export const TelemetryReveal = ({ width = 200, height = 200, paths: customPaths, preset = "network", viewBox, chaosMode = "none", chaosDuration = 3, dotsDuration = 2, flowDuration = 2, flowDelay = 0.3, particlesPerPath = 2, particleRadius = 3, color = "currentColor", particleColor, strokeWidth = 2, opacity = 0.9, showGlow = true, fadeAfterAssembly = true, fadeOpacity = 0.5, loop = true, loopDelay = 1, }) => {
44
+ const idRef = useRef(null);
45
+ if (idRef.current === null) {
46
+ idRef.current = `tr${globalIdCounter++}`;
47
+ }
48
+ const uniqueId = idRef.current;
49
+ const finalParticleColor = particleColor || color;
50
+ const isFragmented = chaosMode === "fragmented";
51
+ // Loop via remount
52
+ const [cycle, setCycle] = useState(0);
53
+ // Resolve paths from custom or preset
54
+ const resolvedPaths = useMemo(() => {
55
+ if (customPaths)
56
+ return customPaths;
57
+ const presetConfig = TELEMETRY_PRESETS[preset];
58
+ return (presetConfig === null || presetConfig === void 0 ? void 0 : presetConfig.paths) || [];
59
+ }, [customPaths, preset]);
60
+ // Resolve viewBox
61
+ const resolvedViewBox = useMemo(() => {
62
+ var _a;
63
+ if (viewBox)
64
+ return viewBox;
65
+ if (!customPaths && ((_a = TELEMETRY_PRESETS[preset]) === null || _a === void 0 ? void 0 : _a.viewBox)) {
66
+ return TELEMETRY_PRESETS[preset].viewBox;
67
+ }
68
+ return "0 0 200 200";
69
+ }, [viewBox, customPaths, preset]);
70
+ // Calculate path lengths and endpoints
71
+ const pathData = useMemo(() => {
72
+ return resolvedPaths.map((p) => ({
73
+ length: estimatePathLength(p.d),
74
+ endpoints: getPathEndpoints(p.d),
75
+ }));
76
+ }, [resolvedPaths]);
77
+ // Generate offsets for fragmented mode
78
+ const fragmentOffsets = useMemo(() => {
79
+ const random = seededRandom(42);
80
+ return resolvedPaths.map(() => ({
81
+ x: (random() - 0.5) * 80,
82
+ y: (random() - 0.5) * 80,
83
+ }));
84
+ }, [resolvedPaths]);
85
+ // Timing calculations
86
+ // In fragmented mode: dots appear, extend to lines, then assemble
87
+ // Phase 1: Dots appear one at a time (dotsDuration / 2)
88
+ // Phase 2: Lines draw one at a time (dotsDuration / 2)
89
+ // Phase 3: Lines move to final position one at a time (chaosDuration)
90
+ // Phase 4: Particles flow
91
+ const numPaths = resolvedPaths.length || 1;
92
+ const perItemDotsDuration = (dotsDuration * 0.5) / numPaths;
93
+ const perItemLinesDuration = (dotsDuration * 0.5) / numPaths;
94
+ const perItemAssemblyDuration = chaosDuration / numPaths;
95
+ const dotsPhaseEnd = dotsDuration * 0.5;
96
+ const linesPhaseEnd = dotsDuration;
97
+ const assemblyEndTime = isFragmented ? dotsDuration + chaosDuration : 0;
98
+ const flowBeginTime = assemblyEndTime + flowDelay;
99
+ // Total animation duration (last particle finishes at flowBeginTime + flowDuration)
100
+ const totalDuration = flowBeginTime + flowDuration;
101
+ // Remount-based loop
102
+ useEffect(() => {
103
+ if (!loop)
104
+ return;
105
+ const timeout = setTimeout(() => {
106
+ setCycle((c) => c + 1);
107
+ }, (totalDuration + loopDelay) * 1000);
108
+ return () => clearTimeout(timeout);
109
+ }, [loop, totalDuration, loopDelay, cycle]);
110
+ return (React.createElement("svg", { key: cycle, width: width, height: height, viewBox: resolvedViewBox, xmlns: "http://www.w3.org/2000/svg", style: { opacity } },
111
+ React.createElement("defs", null,
112
+ showGlow && (React.createElement("filter", { id: `glow-${uniqueId}`, x: "-50%", y: "-50%", width: "200%", height: "200%" },
113
+ React.createElement("feGaussianBlur", { stdDeviation: "2", result: "blur" }),
114
+ React.createElement("feMerge", null,
115
+ React.createElement("feMergeNode", { in: "blur" }),
116
+ React.createElement("feMergeNode", { in: "SourceGraphic" })))),
117
+ React.createElement("radialGradient", { id: `particleGradient-${uniqueId}`, cx: "50%", cy: "50%", r: "50%" },
118
+ React.createElement("stop", { offset: "0%", style: { stopColor: finalParticleColor, stopOpacity: 1 } }),
119
+ React.createElement("stop", { offset: "100%", style: { stopColor: finalParticleColor, stopOpacity: 0.3 } }))),
120
+ React.createElement("g", { className: "telemetry-paths" }, resolvedPaths.map((path, index) => {
121
+ const offset = fragmentOffsets[index];
122
+ const { length: pathLength, endpoints } = pathData[index] || { length: 100, endpoints: { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } } };
123
+ const dotRadius = strokeWidth / 2;
124
+ return (React.createElement("g", { key: path.id || `path-${index}`, transform: isFragmented ? `translate(${offset.x}, ${offset.y})` : undefined },
125
+ isFragmented && (React.createElement(React.Fragment, null,
126
+ React.createElement("circle", { cx: endpoints.start.x, cy: endpoints.start.y, r: dotRadius, fill: color, opacity: "0" },
127
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${index * perItemDotsDuration}s`, fill: "freeze" }),
128
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })),
129
+ React.createElement("circle", { cx: endpoints.end.x, cy: endpoints.end.y, r: dotRadius, fill: color, opacity: "0" },
130
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${index * perItemDotsDuration}s`, fill: "freeze" }),
131
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })))),
132
+ React.createElement("path", { d: path.d, fill: "none", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", opacity: isFragmented ? 0 : 1, strokeDasharray: isFragmented ? pathLength : undefined, strokeDashoffset: isFragmented ? pathLength : undefined },
133
+ isFragmented && (React.createElement(React.Fragment, null,
134
+ React.createElement("animate", { attributeName: "opacity", values: `0;1;1;${fadeAfterAssembly ? fadeOpacity : 1}`, keyTimes: "0;0.1;0.9;1", dur: `${perItemLinesDuration + chaosDuration}s`, begin: `${dotsPhaseEnd + index * perItemLinesDuration}s`, fill: "freeze" }),
135
+ React.createElement("animate", { attributeName: "stroke-dashoffset", from: pathLength, to: "0", dur: `${perItemLinesDuration}s`, begin: `${dotsPhaseEnd + index * perItemLinesDuration}s`, fill: "freeze", calcMode: "spline", keySplines: "0.4 0 0.2 1", keyTimes: "0;1" }))),
136
+ !isFragmented && fadeAfterAssembly && (React.createElement("animate", { attributeName: "opacity", from: "1", to: fadeOpacity, dur: "0.5s", begin: `${flowBeginTime}s`, fill: "freeze" }))),
137
+ isFragmented && (React.createElement("animateTransform", { attributeName: "transform", type: "translate", from: `${offset.x} ${offset.y}`, to: "0 0", dur: `${perItemAssemblyDuration}s`, begin: `${linesPhaseEnd + index * perItemAssemblyDuration}s`, fill: "freeze", calcMode: "spline", keySplines: "0.33 0 0.2 1", keyTimes: "0;1" }))));
138
+ })),
139
+ React.createElement("g", { className: "telemetry-particles", filter: showGlow ? `url(#glow-${uniqueId})` : undefined }, resolvedPaths.flatMap((path, pathIndex) => {
140
+ return Array.from({ length: particlesPerPath }).map((_, particleIndex) => {
141
+ const particleDelay = (particleIndex / particlesPerPath) * flowDuration;
142
+ const beginTime = flowBeginTime + particleDelay;
143
+ return (React.createElement("circle", { key: `particle-${pathIndex}-${particleIndex}`, r: particleRadius, fill: `url(#particleGradient-${uniqueId})`, opacity: "0" },
144
+ React.createElement("animateMotion", { dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze", path: path.d }),
145
+ React.createElement("animate", { attributeName: "opacity", values: "0;1;1;0", keyTimes: "0;0.1;0.9;1", dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze" })));
146
+ });
147
+ }))));
148
+ };