@principal-ai/logo-component 0.1.5 → 0.1.7

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,581 @@
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
+ /**
184
+ * Sample points along an SVG path using browser's native getPointAtLength
185
+ */
186
+ function samplePointsAlongPath(d, numPoints, distribution, seed) {
187
+ if (numPoints <= 0)
188
+ return [];
189
+ // SSR fallback: estimate points without DOM
190
+ if (typeof document === 'undefined') {
191
+ const endpoints = getPathEndpoints(d);
192
+ const points = [];
193
+ for (let i = 0; i < numPoints; i++) {
194
+ const t = numPoints === 1 ? 0 : i / (numPoints - 1);
195
+ points.push({
196
+ x: endpoints.start.x + (endpoints.end.x - endpoints.start.x) * t,
197
+ y: endpoints.start.y + (endpoints.end.y - endpoints.start.y) * t,
198
+ });
199
+ }
200
+ return points;
201
+ }
202
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
203
+ path.setAttribute('d', d);
204
+ const totalLength = path.getTotalLength();
205
+ if (totalLength === 0) {
206
+ const endpoints = getPathEndpoints(d);
207
+ return [endpoints.start];
208
+ }
209
+ // Generate normalized positions (0-1) based on distribution
210
+ const positions = [];
211
+ switch (distribution) {
212
+ case "even":
213
+ for (let i = 0; i < numPoints; i++) {
214
+ positions.push(numPoints === 1 ? 0 : i / (numPoints - 1));
215
+ }
216
+ break;
217
+ case "endpoints":
218
+ // Concentrate more points near 0 and 1
219
+ for (let i = 0; i < numPoints; i++) {
220
+ const t = numPoints === 1 ? 0 : i / (numPoints - 1);
221
+ // Apply curve to cluster at ends: more samples near 0 and 1
222
+ const weighted = t < 0.5
223
+ ? 0.5 * Math.pow(2 * t, 0.5)
224
+ : 1 - 0.5 * Math.pow(2 * (1 - t), 0.5);
225
+ positions.push(weighted);
226
+ }
227
+ break;
228
+ case "random": {
229
+ const random = seededRandom(seed);
230
+ for (let i = 0; i < numPoints; i++) {
231
+ positions.push(random());
232
+ }
233
+ positions.sort((a, b) => a - b); // Sort for visual consistency
234
+ break;
235
+ }
236
+ }
237
+ // Convert positions to actual points
238
+ const points = [];
239
+ for (const t of positions) {
240
+ const point = path.getPointAtLength(t * totalLength);
241
+ points.push({ x: point.x, y: point.y });
242
+ }
243
+ return points;
244
+ }
245
+ 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", dotsPerPathUnit = 30, minDotsPerContour = 1, maxDotsPerContour = 0, dotDistribution = "even", chaosMode = "none", chaosDuration = 2, dotsDuration = 1.5, flowDuration = 2, flowDelay = 0.3, particlesPerPath = 1, particleRadius = 2, color = "currentColor", wordColors, particleColor, strokeWidth = 1.5, opacity = 0.9, showGlow = true, fadeAfterAssembly = true, fadeOpacity = 0.5, loop = true, loopDelay = 1, }) => {
246
+ const idRef = useRef(null);
247
+ if (idRef.current === null) {
248
+ idRef.current = `otr${globalIdCounter++}`;
249
+ }
250
+ const uniqueId = idRef.current;
251
+ const finalParticleColor = particleColor || color;
252
+ const isFragmented = chaosMode === "fragmented";
253
+ // Chart intro is on by default when fragmented mode is active
254
+ const shouldShowChartIntro = showChartIntro !== null && showChartIntro !== void 0 ? showChartIntro : isFragmented;
255
+ // Font and path state
256
+ const [font, setFont] = useState(null);
257
+ const [error, setError] = useState(null);
258
+ const [cycle, setCycle] = useState(0);
259
+ // Load font
260
+ useEffect(() => {
261
+ let cancelled = false;
262
+ opentype.load(fontUrl)
263
+ .then((loadedFont) => {
264
+ if (!cancelled) {
265
+ setFont(loadedFont);
266
+ setError(null);
267
+ }
268
+ })
269
+ .catch((err) => {
270
+ if (!cancelled) {
271
+ setError(err.message || "Failed to load font");
272
+ }
273
+ });
274
+ return () => {
275
+ cancelled = true;
276
+ };
277
+ }, [fontUrl]);
278
+ // Generate paths from font, tracking word indices
279
+ const { paths, textWidth, textHeight, wordCount } = useMemo(() => {
280
+ if (!font) {
281
+ return { paths: [], textWidth: 0, textHeight: fontSize, wordCount: 0 };
282
+ }
283
+ // Split text into words and generate paths per word
284
+ const words = text.split(/\s+/).filter(w => w.length > 0);
285
+ const pathInfos = [];
286
+ let currentX = 0;
287
+ let contourIndex = 0;
288
+ // Track overall bounds
289
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
290
+ words.forEach((word, wordIndex) => {
291
+ // Get path for this word
292
+ const wordPath = font.getPath(word, currentX, fontSize, fontSize);
293
+ const pathData = wordPath.toPathData(2);
294
+ const contours = splitPathIntoContours(pathData);
295
+ // Add contours with word index
296
+ contours.forEach((d) => {
297
+ pathInfos.push({
298
+ d,
299
+ id: `contour-${contourIndex++}`,
300
+ length: estimatePathLength(d),
301
+ endpoints: getPathEndpoints(d),
302
+ wordIndex,
303
+ });
304
+ });
305
+ // Update bounds
306
+ const bbox = wordPath.getBoundingBox();
307
+ minX = Math.min(minX, bbox.x1);
308
+ minY = Math.min(minY, bbox.y1);
309
+ maxX = Math.max(maxX, bbox.x2);
310
+ maxY = Math.max(maxY, bbox.y2);
311
+ // Advance X position for next word (add space width)
312
+ const spaceWidth = font.getAdvanceWidth(' ', fontSize);
313
+ currentX = bbox.x2 + spaceWidth;
314
+ });
315
+ return {
316
+ paths: pathInfos,
317
+ textWidth: maxX - minX,
318
+ textHeight: maxY - minY,
319
+ wordCount: words.length,
320
+ };
321
+ }, [font, text, fontSize]);
322
+ // Calculate centering offset
323
+ const offsetX = centerText ? (width - textWidth) / 2 : 0;
324
+ const offsetY = centerText ? (height - textHeight) / 2 - (fontSize * 0.2) : 0;
325
+ // Apply offset to paths
326
+ const resolvedPaths = useMemo(() => {
327
+ return paths.map((path) => (Object.assign(Object.assign({}, path), { d: offsetPathData(path.d, offsetX, offsetY), endpoints: {
328
+ start: { x: path.endpoints.start.x + offsetX, y: path.endpoints.start.y + offsetY },
329
+ end: { x: path.endpoints.end.x + offsetX, y: path.endpoints.end.y + offsetY },
330
+ } })));
331
+ }, [paths, offsetX, offsetY]);
332
+ // Generate fragment offsets
333
+ const fragmentOffsets = useMemo(() => {
334
+ const random = seededRandom(42);
335
+ return resolvedPaths.map(() => ({
336
+ x: (random() - 0.5) * 150,
337
+ y: (random() - 0.5) * 100,
338
+ }));
339
+ }, [resolvedPaths]);
340
+ // Calculate dots per contour based on path length
341
+ const dotsPerContour = useMemo(() => {
342
+ return resolvedPaths.map(path => {
343
+ const rawCount = Math.round(path.length / dotsPerPathUnit);
344
+ const bounded = maxDotsPerContour > 0
345
+ ? Math.min(rawCount, maxDotsPerContour)
346
+ : rawCount;
347
+ return Math.max(minDotsPerContour, bounded);
348
+ });
349
+ }, [resolvedPaths, dotsPerPathUnit, minDotsPerContour, maxDotsPerContour]);
350
+ const totalDots = useMemo(() => {
351
+ return dotsPerContour.reduce((a, b) => a + b, 0);
352
+ }, [dotsPerContour]);
353
+ // Count dots per word for chart generation
354
+ const dotsPerWord = useMemo(() => {
355
+ const counts = [];
356
+ resolvedPaths.forEach((path, contourIndex) => {
357
+ const wordIdx = path.wordIndex;
358
+ while (counts.length <= wordIdx)
359
+ counts.push(0);
360
+ counts[wordIdx] += dotsPerContour[contourIndex];
361
+ });
362
+ return counts;
363
+ }, [resolvedPaths, dotsPerContour]);
364
+ // Generate chart positions per word - each word gets its own full-width line
365
+ const chartPositions = useMemo(() => {
366
+ if (!shouldShowChartIntro)
367
+ return [];
368
+ const allPositions = [];
369
+ dotsPerWord.forEach((numDots, wordIndex) => {
370
+ if (numDots === 0)
371
+ return;
372
+ // Each word gets its own time-series spanning the full width
373
+ // Use different seed per word for variation
374
+ const wordPositions = generateChartPositions(numDots, width, height, chartPattern, 123 + wordIndex * 1000);
375
+ wordPositions.forEach(pos => {
376
+ allPositions.push(Object.assign(Object.assign({}, pos), { wordIndex }));
377
+ });
378
+ });
379
+ return allPositions;
380
+ }, [shouldShowChartIntro, dotsPerWord, width, height, chartPattern]);
381
+ // Target positions for dots after chart phase - sampled along each contour
382
+ const dotTargetPositions = useMemo(() => {
383
+ // Group by word first to match chart positions order
384
+ const positionsByWord = [];
385
+ resolvedPaths.forEach((path, contourIndex) => {
386
+ const wordIdx = path.wordIndex;
387
+ while (positionsByWord.length <= wordIdx)
388
+ positionsByWord.push([]);
389
+ const numDots = dotsPerContour[contourIndex];
390
+ const sampledPoints = samplePointsAlongPath(path.d, numDots, dotDistribution, contourIndex * 1000);
391
+ sampledPoints.forEach(point => {
392
+ positionsByWord[wordIdx].push(Object.assign(Object.assign({}, point), { contourIndex, wordIndex: wordIdx }));
393
+ });
394
+ });
395
+ // Flatten in word order to match chartPositions
396
+ return positionsByWord.flat();
397
+ }, [resolvedPaths, dotsPerContour, dotDistribution]);
398
+ // Helper to get color for a word index
399
+ const getWordColor = (wordIndex) => {
400
+ if (wordColors && wordColors.length > 0) {
401
+ return wordColors[wordIndex % wordColors.length];
402
+ }
403
+ return color;
404
+ };
405
+ // Chart intro timing
406
+ const chartPhaseEndTime = shouldShowChartIntro
407
+ ? chartDuration + chartLineFadeDuration + chartPauseDuration + chartTransitionDuration
408
+ : 0;
409
+ // Timing calculations (offset by chart phase if present)
410
+ const numPaths = resolvedPaths.length || 1;
411
+ const perItemDotsDuration = (dotsDuration * 0.5) / numPaths;
412
+ const perItemLinesDuration = (dotsDuration * 0.5) / numPaths;
413
+ const perItemAssemblyDuration = chaosDuration / numPaths;
414
+ const dotsPhaseEnd = chartPhaseEndTime + dotsDuration * 0.5;
415
+ const linesPhaseEnd = chartPhaseEndTime + dotsDuration;
416
+ // Skip assembly phase when chart intro handles the transition
417
+ const needsAssembly = isFragmented && !shouldShowChartIntro;
418
+ const assemblyEndTime = needsAssembly
419
+ ? chartPhaseEndTime + dotsDuration + chaosDuration
420
+ : chartPhaseEndTime + dotsDuration;
421
+ const flowBeginTime = assemblyEndTime + flowDelay;
422
+ const totalDuration = flowBeginTime + flowDuration;
423
+ // Loop via remount
424
+ useEffect(() => {
425
+ if (!loop)
426
+ return;
427
+ const timeout = setTimeout(() => {
428
+ setCycle((c) => c + 1);
429
+ }, (totalDuration + loopDelay) * 1000);
430
+ return () => clearTimeout(timeout);
431
+ }, [loop, totalDuration, loopDelay, cycle]);
432
+ if (error) {
433
+ return (React.createElement("svg", { width: width, height: height },
434
+ React.createElement("text", { x: 10, y: 30, fill: "red", fontSize: 14 },
435
+ "Error: ",
436
+ error)));
437
+ }
438
+ if (!font) {
439
+ return (React.createElement("svg", { width: width, height: height },
440
+ React.createElement("text", { x: 10, y: 30, fill: color, fontSize: 14 }, "Loading font...")));
441
+ }
442
+ return (React.createElement("svg", { key: cycle, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", style: { opacity } },
443
+ React.createElement("defs", null,
444
+ showGlow && (React.createElement("filter", { id: `glow-${uniqueId}`, x: "-50%", y: "-50%", width: "200%", height: "200%" },
445
+ React.createElement("feGaussianBlur", { stdDeviation: "2", result: "blur" }),
446
+ React.createElement("feMerge", null,
447
+ React.createElement("feMergeNode", { in: "blur" }),
448
+ React.createElement("feMergeNode", { in: "SourceGraphic" })))),
449
+ React.createElement("radialGradient", { id: `particleGradient-${uniqueId}`, cx: "50%", cy: "50%", r: "50%" },
450
+ React.createElement("stop", { offset: "0%", style: { stopColor: finalParticleColor, stopOpacity: 1 } }),
451
+ React.createElement("stop", { offset: "100%", style: { stopColor: finalParticleColor, stopOpacity: 0.3 } }))),
452
+ shouldShowChartIntro && chartPositions.length > 0 && (React.createElement("g", { className: "chart-intro" },
453
+ (() => {
454
+ // Group chart positions by word (chartPositions now has wordIndex)
455
+ const positionsByWord = new Map();
456
+ chartPositions.forEach((pos) => {
457
+ if (!positionsByWord.has(pos.wordIndex)) {
458
+ positionsByWord.set(pos.wordIndex, []);
459
+ }
460
+ positionsByWord.get(pos.wordIndex).push({ x: pos.x, y: pos.y });
461
+ });
462
+ return Array.from(positionsByWord.entries()).map(([wordIndex, positions]) => (React.createElement("polyline", { key: `chart-line-${wordIndex}`, points: positions.map(p => `${p.x},${p.y}`).join(' '), fill: "none", stroke: getWordColor(wordIndex), strokeWidth: strokeWidth * 0.75, strokeLinecap: "round", strokeLinejoin: "round", opacity: "0" },
463
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "0.6", dur: "0.3s", begin: "0s", fill: "freeze" }),
464
+ React.createElement("animate", { attributeName: "opacity", from: "0.6", to: "0", dur: `${chartLineFadeDuration}s`, begin: `${chartDuration}s`, fill: "freeze" }))));
465
+ })(),
466
+ chartPositions.map((chartPos, index) => {
467
+ const targetPos = dotTargetPositions[index];
468
+ if (!targetPos)
469
+ return null;
470
+ const dotRadius = strokeWidth;
471
+ const dotAppearDelay = (index / chartPositions.length) * 0.5; // Stagger appearance
472
+ const transitionBegin = chartDuration + chartLineFadeDuration + chartPauseDuration;
473
+ // Fade out when this dot's contour starts drawing
474
+ const contourDrawBegin = dotsPhaseEnd + (targetPos.contourIndex + 1) * perItemLinesDuration;
475
+ const dotColor = getWordColor(chartPos.wordIndex);
476
+ return (React.createElement("circle", { key: `chart-dot-${index}`, cx: chartPos.x, cy: chartPos.y, r: dotRadius, fill: dotColor, opacity: "0" },
477
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${dotAppearDelay}s`, fill: "freeze" }),
478
+ 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" }),
479
+ 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" }),
480
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${contourDrawBegin}s`, fill: "freeze" })));
481
+ }))),
482
+ React.createElement("g", { className: "opentype-paths" }, resolvedPaths.map((path, index) => {
483
+ const offset = fragmentOffsets[index];
484
+ const dotRadius = strokeWidth / 2;
485
+ const pathColor = getWordColor(path.wordIndex);
486
+ // Skip fragment offset when chart intro handles the transition
487
+ const useFragmentOffset = isFragmented && !shouldShowChartIntro;
488
+ return (React.createElement("g", { key: path.id, transform: useFragmentOffset ? `translate(${offset.x}, ${offset.y})` : undefined },
489
+ isFragmented && !shouldShowChartIntro && (React.createElement(React.Fragment, null,
490
+ React.createElement("circle", { cx: path.endpoints.start.x, cy: path.endpoints.start.y, r: dotRadius, fill: pathColor, opacity: "0" },
491
+ React.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${chartPhaseEndTime + index * perItemDotsDuration}s`, fill: "freeze" }),
492
+ React.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })))),
493
+ (() => {
494
+ // Determine animation mode
495
+ const shouldDrawLines = isFragmented || shouldShowChartIntro;
496
+ const drawDuration = shouldShowChartIntro && !isFragmented
497
+ ? perItemLinesDuration
498
+ : perItemLinesDuration + chaosDuration;
499
+ return (React.createElement("path", { d: path.d, fill: "none", stroke: pathColor, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", opacity: shouldDrawLines ? 0 : 1, strokeDasharray: shouldDrawLines ? path.length : undefined, strokeDashoffset: shouldDrawLines ? path.length : undefined },
500
+ shouldDrawLines && (React.createElement(React.Fragment, null,
501
+ 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" }),
502
+ 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" }))),
503
+ !shouldDrawLines && fadeAfterAssembly && (React.createElement("animate", { attributeName: "opacity", from: "1", to: fadeOpacity, dur: "0.5s", begin: `${flowBeginTime}s`, fill: "freeze" }))));
504
+ })(),
505
+ 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" }))));
506
+ })),
507
+ React.createElement("g", { className: "opentype-particles", filter: showGlow ? `url(#glow-${uniqueId})` : undefined }, resolvedPaths.flatMap((path, pathIndex) => {
508
+ return Array.from({ length: particlesPerPath }).map((_, particleIndex) => {
509
+ const particleDelay = (particleIndex / particlesPerPath) * flowDuration;
510
+ const beginTime = flowBeginTime + particleDelay;
511
+ return (React.createElement("circle", { key: `particle-${pathIndex}-${particleIndex}`, r: particleRadius, fill: `url(#particleGradient-${uniqueId})`, opacity: "0" },
512
+ React.createElement("animateMotion", { dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze", path: path.d }),
513
+ 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" })));
514
+ });
515
+ }))));
516
+ };
517
+ /**
518
+ * Offset all coordinates in path data
519
+ */
520
+ function offsetPathData(d, offsetX, offsetY) {
521
+ let result = "";
522
+ let i = 0;
523
+ let coordIndex = 0;
524
+ while (i < d.length) {
525
+ const char = d[i];
526
+ // Check for command letters
527
+ if (/[MmLlHhVvCcSsQqTtAaZz]/.test(char)) {
528
+ result += char;
529
+ // Reset coord tracking for absolute commands, or handle relative
530
+ if (char === 'Z' || char === 'z') {
531
+ coordIndex = 0;
532
+ }
533
+ else if (char === 'H' || char === 'h') {
534
+ // Horizontal - only X coords
535
+ coordIndex = -1; // Special handling
536
+ }
537
+ else if (char === 'V' || char === 'v') {
538
+ // Vertical - only Y coords
539
+ coordIndex = -2; // Special handling
540
+ }
541
+ else {
542
+ coordIndex = 0;
543
+ }
544
+ i++;
545
+ }
546
+ else if (/[-\d.]/.test(char)) {
547
+ // Parse number
548
+ let numStr = "";
549
+ while (i < d.length && /[-\d.eE]/.test(d[i])) {
550
+ numStr += d[i];
551
+ i++;
552
+ }
553
+ const num = parseFloat(numStr);
554
+ // Apply offset based on coordinate type
555
+ if (coordIndex === -1) {
556
+ // H command - X only
557
+ result += String(num + offsetX);
558
+ }
559
+ else if (coordIndex === -2) {
560
+ // V command - Y only
561
+ result += String(num + offsetY);
562
+ }
563
+ else if (coordIndex % 2 === 0) {
564
+ // X coordinate
565
+ result += String(num + offsetX);
566
+ }
567
+ else {
568
+ // Y coordinate
569
+ result += String(num + offsetY);
570
+ }
571
+ if (coordIndex >= 0)
572
+ coordIndex++;
573
+ }
574
+ else {
575
+ // Whitespace or comma
576
+ result += char;
577
+ i++;
578
+ }
579
+ }
580
+ return result;
581
+ }