@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.
- package/dist/OpenTypeTextReveal.d.ts +58 -0
- package/dist/OpenTypeTextReveal.js +621 -0
- package/dist/TelemetryReveal.d.ts +28 -0
- package/dist/TelemetryReveal.js +185 -0
- package/dist/TextReveal.d.ts +34 -0
- package/dist/TextReveal.js +196 -0
- package/dist/esm/OpenTypeTextReveal.js +581 -0
- package/dist/esm/TelemetryReveal.js +148 -0
- package/dist/esm/TextReveal.js +159 -0
- package/dist/esm/index.js +5 -2
- package/dist/esm/presets.js +88 -0
- package/dist/esm/strokeCharacters.js +517 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.esm.js +5 -2
- package/dist/index.js +12 -5
- package/dist/presets.d.ts +10 -0
- package/dist/presets.js +91 -0
- package/dist/strokeCharacters.d.ts +33 -0
- package/dist/strokeCharacters.js +521 -0
- package/package.json +5 -1
- package/dist/MazeDemo.d.ts +0 -12
- package/dist/MazeDemo.js +0 -376
- package/dist/MazeDemoNew.d.ts +0 -12
- package/dist/MazeDemoNew.js +0 -376
- package/dist/esm/MazeDemo.js +0 -339
- package/dist/esm/MazeDemoNew.js +0 -339
- package/dist/esm/utils/mazeGenerator.js +0 -266
- package/dist/utils/mazeGenerator.d.ts +0 -56
- package/dist/utils/mazeGenerator.js +0 -271
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type ChaosMode = "none" | "fragmented";
|
|
3
|
+
type ChartPattern = "latency" | "cpu" | "noise";
|
|
4
|
+
type DotDistribution = "even" | "endpoints" | "random";
|
|
5
|
+
interface OpenTypeTextRevealProps {
|
|
6
|
+
/** The text to display */
|
|
7
|
+
text: string;
|
|
8
|
+
/** URL or path to the font file (.otf, .ttf, .woff) */
|
|
9
|
+
fontUrl: string;
|
|
10
|
+
/** Font size in pixels */
|
|
11
|
+
fontSize?: number;
|
|
12
|
+
/** Width of the SVG container */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Height of the SVG container */
|
|
15
|
+
height?: number;
|
|
16
|
+
/** Center the text in the container */
|
|
17
|
+
centerText?: boolean;
|
|
18
|
+
/** Show time-series chart intro animation (default: true when chaosMode="fragmented") */
|
|
19
|
+
showChartIntro?: boolean;
|
|
20
|
+
/** Duration the chart is visible before lines fade (default: 1.5s) */
|
|
21
|
+
chartDuration?: number;
|
|
22
|
+
/** Duration for chart lines to fade out (default: 0.5s) */
|
|
23
|
+
chartLineFadeDuration?: number;
|
|
24
|
+
/** Pause duration after lines fade, before dots move (default: 0.3s) */
|
|
25
|
+
chartPauseDuration?: number;
|
|
26
|
+
/** Duration for dots to animate from chart to target positions (default: 0.8s) */
|
|
27
|
+
chartTransitionDuration?: number;
|
|
28
|
+
/** Telemetry pattern style for the chart */
|
|
29
|
+
chartPattern?: ChartPattern;
|
|
30
|
+
/** Pixels of path length per dot (lower = more dots). Default: 30 */
|
|
31
|
+
dotsPerPathUnit?: number;
|
|
32
|
+
/** Minimum dots per contour. Default: 1 */
|
|
33
|
+
minDotsPerContour?: number;
|
|
34
|
+
/** Maximum dots per contour (0 = unlimited). Default: 0 */
|
|
35
|
+
maxDotsPerContour?: number;
|
|
36
|
+
/** How dots are distributed along each contour. Default: "even" */
|
|
37
|
+
dotDistribution?: DotDistribution;
|
|
38
|
+
chaosMode?: ChaosMode;
|
|
39
|
+
chaosDuration?: number;
|
|
40
|
+
dotsDuration?: number;
|
|
41
|
+
flowDuration?: number;
|
|
42
|
+
flowDelay?: number;
|
|
43
|
+
particlesPerPath?: number;
|
|
44
|
+
particleRadius?: number;
|
|
45
|
+
color?: string;
|
|
46
|
+
/** Colors for each word (space-separated). Falls back to `color` if not specified. */
|
|
47
|
+
wordColors?: string[];
|
|
48
|
+
particleColor?: string;
|
|
49
|
+
strokeWidth?: number;
|
|
50
|
+
opacity?: number;
|
|
51
|
+
showGlow?: boolean;
|
|
52
|
+
fadeAfterAssembly?: boolean;
|
|
53
|
+
fadeOpacity?: number;
|
|
54
|
+
loop?: boolean;
|
|
55
|
+
loopDelay?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare const OpenTypeTextReveal: React.FC<OpenTypeTextRevealProps>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.OpenTypeTextReveal = void 0;
|
|
40
|
+
const react_1 = __importStar(require("react"));
|
|
41
|
+
const opentype_js_1 = __importDefault(require("opentype.js"));
|
|
42
|
+
let globalIdCounter = 0;
|
|
43
|
+
/**
|
|
44
|
+
* Estimates path length from SVG path data including curves.
|
|
45
|
+
*/
|
|
46
|
+
function estimatePathLength(d) {
|
|
47
|
+
var _a;
|
|
48
|
+
// Simple estimation - count segments and approximate
|
|
49
|
+
const commands = d.match(/[MLHVCSQTAZ][^MLHVCSQTAZ]*/gi) || [];
|
|
50
|
+
let length = 0;
|
|
51
|
+
let lastX = 0, lastY = 0;
|
|
52
|
+
let startX = 0, startY = 0;
|
|
53
|
+
for (const cmd of commands) {
|
|
54
|
+
const type = cmd[0].toUpperCase();
|
|
55
|
+
const nums = ((_a = cmd.slice(1).match(/-?\d+\.?\d*/g)) === null || _a === void 0 ? void 0 : _a.map(Number)) || [];
|
|
56
|
+
switch (type) {
|
|
57
|
+
case 'M':
|
|
58
|
+
lastX = nums[0] || 0;
|
|
59
|
+
lastY = nums[1] || 0;
|
|
60
|
+
startX = lastX;
|
|
61
|
+
startY = lastY;
|
|
62
|
+
break;
|
|
63
|
+
case 'L':
|
|
64
|
+
if (nums.length >= 2) {
|
|
65
|
+
length += Math.sqrt((nums[0] - lastX) ** 2 + (nums[1] - lastY) ** 2);
|
|
66
|
+
lastX = nums[0];
|
|
67
|
+
lastY = nums[1];
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case 'H':
|
|
71
|
+
if (nums.length >= 1) {
|
|
72
|
+
length += Math.abs(nums[0] - lastX);
|
|
73
|
+
lastX = nums[0];
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case 'V':
|
|
77
|
+
if (nums.length >= 1) {
|
|
78
|
+
length += Math.abs(nums[0] - lastY);
|
|
79
|
+
lastY = nums[0];
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 'C':
|
|
83
|
+
// Cubic bezier - approximate with chord length * 1.5
|
|
84
|
+
if (nums.length >= 6) {
|
|
85
|
+
const dx = nums[4] - lastX;
|
|
86
|
+
const dy = nums[5] - lastY;
|
|
87
|
+
length += Math.sqrt(dx * dx + dy * dy) * 1.5;
|
|
88
|
+
lastX = nums[4];
|
|
89
|
+
lastY = nums[5];
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'Q':
|
|
93
|
+
// Quadratic bezier - approximate with chord length * 1.3
|
|
94
|
+
if (nums.length >= 4) {
|
|
95
|
+
const dx = nums[2] - lastX;
|
|
96
|
+
const dy = nums[3] - lastY;
|
|
97
|
+
length += Math.sqrt(dx * dx + dy * dy) * 1.3;
|
|
98
|
+
lastX = nums[2];
|
|
99
|
+
lastY = nums[3];
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case 'Z':
|
|
103
|
+
length += Math.sqrt((startX - lastX) ** 2 + (startY - lastY) ** 2);
|
|
104
|
+
lastX = startX;
|
|
105
|
+
lastY = startY;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return length || 100;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get start and end points from path
|
|
113
|
+
*/
|
|
114
|
+
function getPathEndpoints(d) {
|
|
115
|
+
const coords = d.match(/-?\d+\.?\d*/g);
|
|
116
|
+
if (!coords || coords.length < 2) {
|
|
117
|
+
return { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } };
|
|
118
|
+
}
|
|
119
|
+
// Start is first two coords (after M)
|
|
120
|
+
const start = { x: parseFloat(coords[0]), y: parseFloat(coords[1]) };
|
|
121
|
+
// End is last two coords before Z (or just last two)
|
|
122
|
+
const end = {
|
|
123
|
+
x: parseFloat(coords[coords.length - 2]),
|
|
124
|
+
y: parseFloat(coords[coords.length - 1])
|
|
125
|
+
};
|
|
126
|
+
return { start, end };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Seeded random for consistent chaos patterns
|
|
130
|
+
*/
|
|
131
|
+
function seededRandom(seed) {
|
|
132
|
+
return () => {
|
|
133
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
134
|
+
return seed / 0x7fffffff;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Generate time-series chart positions for dots
|
|
139
|
+
*/
|
|
140
|
+
function generateChartPositions(numDots, width, height, pattern, seed) {
|
|
141
|
+
if (numDots === 0)
|
|
142
|
+
return [];
|
|
143
|
+
const random = seededRandom(seed);
|
|
144
|
+
const positions = [];
|
|
145
|
+
// Chart area with padding
|
|
146
|
+
const padding = width * 0.1;
|
|
147
|
+
const chartWidth = width - padding * 2;
|
|
148
|
+
const chartHeight = height * 0.6;
|
|
149
|
+
const chartTop = height * 0.2;
|
|
150
|
+
// Generate y-values based on pattern
|
|
151
|
+
const yValues = [];
|
|
152
|
+
switch (pattern) {
|
|
153
|
+
case "latency": {
|
|
154
|
+
// Mostly stable baseline with occasional spikes
|
|
155
|
+
const baseline = 0.3;
|
|
156
|
+
const numSpikes = Math.max(1, Math.floor(numDots * 0.15)); // ~15% are spikes
|
|
157
|
+
const spikeIndices = new Set();
|
|
158
|
+
// Pick random spike positions
|
|
159
|
+
while (spikeIndices.size < numSpikes) {
|
|
160
|
+
spikeIndices.add(Math.floor(random() * numDots));
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < numDots; i++) {
|
|
163
|
+
if (spikeIndices.has(i)) {
|
|
164
|
+
// Spike: 60-95% height
|
|
165
|
+
yValues.push(0.6 + random() * 0.35);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Baseline with small noise: 20-40%
|
|
169
|
+
yValues.push(baseline + (random() - 0.5) * 0.2);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "cpu": {
|
|
175
|
+
// Sine wave foundation with noise - gradual rises and drops
|
|
176
|
+
const frequency = 2 + random() * 2; // 2-4 cycles across the chart
|
|
177
|
+
for (let i = 0; i < numDots; i++) {
|
|
178
|
+
const t = i / (numDots - 1 || 1);
|
|
179
|
+
const sine = Math.sin(t * Math.PI * frequency) * 0.3;
|
|
180
|
+
const noise = (random() - 0.5) * 0.15;
|
|
181
|
+
yValues.push(0.5 + sine + noise);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "noise":
|
|
186
|
+
default: {
|
|
187
|
+
// Irregular fluctuations using smoothed noise
|
|
188
|
+
let value = 0.5;
|
|
189
|
+
for (let i = 0; i < numDots; i++) {
|
|
190
|
+
// Random walk with mean reversion
|
|
191
|
+
const drift = (0.5 - value) * 0.1; // Pull toward center
|
|
192
|
+
const noise = (random() - 0.5) * 0.25;
|
|
193
|
+
value = Math.max(0.15, Math.min(0.85, value + drift + noise));
|
|
194
|
+
yValues.push(value);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Convert to positions
|
|
200
|
+
for (let i = 0; i < numDots; i++) {
|
|
201
|
+
const x = padding + (i / (numDots - 1 || 1)) * chartWidth;
|
|
202
|
+
// Invert y since SVG y increases downward
|
|
203
|
+
const y = chartTop + (1 - yValues[i]) * chartHeight;
|
|
204
|
+
positions.push({ x, y });
|
|
205
|
+
}
|
|
206
|
+
return positions;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Split compound path into individual contours
|
|
210
|
+
*/
|
|
211
|
+
function splitPathIntoContours(d) {
|
|
212
|
+
const contours = [];
|
|
213
|
+
const parts = d.split(/(?=[Mm])/);
|
|
214
|
+
for (const part of parts) {
|
|
215
|
+
const trimmed = part.trim();
|
|
216
|
+
if (trimmed) {
|
|
217
|
+
contours.push(trimmed);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return contours;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Sample points along an SVG path using browser's native getPointAtLength
|
|
224
|
+
*/
|
|
225
|
+
function samplePointsAlongPath(d, numPoints, distribution, seed) {
|
|
226
|
+
if (numPoints <= 0)
|
|
227
|
+
return [];
|
|
228
|
+
// SSR fallback: estimate points without DOM
|
|
229
|
+
if (typeof document === 'undefined') {
|
|
230
|
+
const endpoints = getPathEndpoints(d);
|
|
231
|
+
const points = [];
|
|
232
|
+
for (let i = 0; i < numPoints; i++) {
|
|
233
|
+
const t = numPoints === 1 ? 0 : i / (numPoints - 1);
|
|
234
|
+
points.push({
|
|
235
|
+
x: endpoints.start.x + (endpoints.end.x - endpoints.start.x) * t,
|
|
236
|
+
y: endpoints.start.y + (endpoints.end.y - endpoints.start.y) * t,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return points;
|
|
240
|
+
}
|
|
241
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
242
|
+
path.setAttribute('d', d);
|
|
243
|
+
const totalLength = path.getTotalLength();
|
|
244
|
+
if (totalLength === 0) {
|
|
245
|
+
const endpoints = getPathEndpoints(d);
|
|
246
|
+
return [endpoints.start];
|
|
247
|
+
}
|
|
248
|
+
// Generate normalized positions (0-1) based on distribution
|
|
249
|
+
const positions = [];
|
|
250
|
+
switch (distribution) {
|
|
251
|
+
case "even":
|
|
252
|
+
for (let i = 0; i < numPoints; i++) {
|
|
253
|
+
positions.push(numPoints === 1 ? 0 : i / (numPoints - 1));
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case "endpoints":
|
|
257
|
+
// Concentrate more points near 0 and 1
|
|
258
|
+
for (let i = 0; i < numPoints; i++) {
|
|
259
|
+
const t = numPoints === 1 ? 0 : i / (numPoints - 1);
|
|
260
|
+
// Apply curve to cluster at ends: more samples near 0 and 1
|
|
261
|
+
const weighted = t < 0.5
|
|
262
|
+
? 0.5 * Math.pow(2 * t, 0.5)
|
|
263
|
+
: 1 - 0.5 * Math.pow(2 * (1 - t), 0.5);
|
|
264
|
+
positions.push(weighted);
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
case "random": {
|
|
268
|
+
const random = seededRandom(seed);
|
|
269
|
+
for (let i = 0; i < numPoints; i++) {
|
|
270
|
+
positions.push(random());
|
|
271
|
+
}
|
|
272
|
+
positions.sort((a, b) => a - b); // Sort for visual consistency
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Convert positions to actual points
|
|
277
|
+
const points = [];
|
|
278
|
+
for (const t of positions) {
|
|
279
|
+
const point = path.getPointAtLength(t * totalLength);
|
|
280
|
+
points.push({ x: point.x, y: point.y });
|
|
281
|
+
}
|
|
282
|
+
return points;
|
|
283
|
+
}
|
|
284
|
+
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, }) => {
|
|
285
|
+
const idRef = (0, react_1.useRef)(null);
|
|
286
|
+
if (idRef.current === null) {
|
|
287
|
+
idRef.current = `otr${globalIdCounter++}`;
|
|
288
|
+
}
|
|
289
|
+
const uniqueId = idRef.current;
|
|
290
|
+
const finalParticleColor = particleColor || color;
|
|
291
|
+
const isFragmented = chaosMode === "fragmented";
|
|
292
|
+
// Chart intro is on by default when fragmented mode is active
|
|
293
|
+
const shouldShowChartIntro = showChartIntro !== null && showChartIntro !== void 0 ? showChartIntro : isFragmented;
|
|
294
|
+
// Font and path state
|
|
295
|
+
const [font, setFont] = (0, react_1.useState)(null);
|
|
296
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
297
|
+
const [cycle, setCycle] = (0, react_1.useState)(0);
|
|
298
|
+
// Load font
|
|
299
|
+
(0, react_1.useEffect)(() => {
|
|
300
|
+
let cancelled = false;
|
|
301
|
+
opentype_js_1.default.load(fontUrl)
|
|
302
|
+
.then((loadedFont) => {
|
|
303
|
+
if (!cancelled) {
|
|
304
|
+
setFont(loadedFont);
|
|
305
|
+
setError(null);
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
.catch((err) => {
|
|
309
|
+
if (!cancelled) {
|
|
310
|
+
setError(err.message || "Failed to load font");
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return () => {
|
|
314
|
+
cancelled = true;
|
|
315
|
+
};
|
|
316
|
+
}, [fontUrl]);
|
|
317
|
+
// Generate paths from font, tracking word indices
|
|
318
|
+
const { paths, textWidth, textHeight, wordCount } = (0, react_1.useMemo)(() => {
|
|
319
|
+
if (!font) {
|
|
320
|
+
return { paths: [], textWidth: 0, textHeight: fontSize, wordCount: 0 };
|
|
321
|
+
}
|
|
322
|
+
// Split text into words and generate paths per word
|
|
323
|
+
const words = text.split(/\s+/).filter(w => w.length > 0);
|
|
324
|
+
const pathInfos = [];
|
|
325
|
+
let currentX = 0;
|
|
326
|
+
let contourIndex = 0;
|
|
327
|
+
// Track overall bounds
|
|
328
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
329
|
+
words.forEach((word, wordIndex) => {
|
|
330
|
+
// Get path for this word
|
|
331
|
+
const wordPath = font.getPath(word, currentX, fontSize, fontSize);
|
|
332
|
+
const pathData = wordPath.toPathData(2);
|
|
333
|
+
const contours = splitPathIntoContours(pathData);
|
|
334
|
+
// Add contours with word index
|
|
335
|
+
contours.forEach((d) => {
|
|
336
|
+
pathInfos.push({
|
|
337
|
+
d,
|
|
338
|
+
id: `contour-${contourIndex++}`,
|
|
339
|
+
length: estimatePathLength(d),
|
|
340
|
+
endpoints: getPathEndpoints(d),
|
|
341
|
+
wordIndex,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
// Update bounds
|
|
345
|
+
const bbox = wordPath.getBoundingBox();
|
|
346
|
+
minX = Math.min(minX, bbox.x1);
|
|
347
|
+
minY = Math.min(minY, bbox.y1);
|
|
348
|
+
maxX = Math.max(maxX, bbox.x2);
|
|
349
|
+
maxY = Math.max(maxY, bbox.y2);
|
|
350
|
+
// Advance X position for next word (add space width)
|
|
351
|
+
const spaceWidth = font.getAdvanceWidth(' ', fontSize);
|
|
352
|
+
currentX = bbox.x2 + spaceWidth;
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
paths: pathInfos,
|
|
356
|
+
textWidth: maxX - minX,
|
|
357
|
+
textHeight: maxY - minY,
|
|
358
|
+
wordCount: words.length,
|
|
359
|
+
};
|
|
360
|
+
}, [font, text, fontSize]);
|
|
361
|
+
// Calculate centering offset
|
|
362
|
+
const offsetX = centerText ? (width - textWidth) / 2 : 0;
|
|
363
|
+
const offsetY = centerText ? (height - textHeight) / 2 - (fontSize * 0.2) : 0;
|
|
364
|
+
// Apply offset to paths
|
|
365
|
+
const resolvedPaths = (0, react_1.useMemo)(() => {
|
|
366
|
+
return paths.map((path) => (Object.assign(Object.assign({}, path), { d: offsetPathData(path.d, offsetX, offsetY), endpoints: {
|
|
367
|
+
start: { x: path.endpoints.start.x + offsetX, y: path.endpoints.start.y + offsetY },
|
|
368
|
+
end: { x: path.endpoints.end.x + offsetX, y: path.endpoints.end.y + offsetY },
|
|
369
|
+
} })));
|
|
370
|
+
}, [paths, offsetX, offsetY]);
|
|
371
|
+
// Generate fragment offsets
|
|
372
|
+
const fragmentOffsets = (0, react_1.useMemo)(() => {
|
|
373
|
+
const random = seededRandom(42);
|
|
374
|
+
return resolvedPaths.map(() => ({
|
|
375
|
+
x: (random() - 0.5) * 150,
|
|
376
|
+
y: (random() - 0.5) * 100,
|
|
377
|
+
}));
|
|
378
|
+
}, [resolvedPaths]);
|
|
379
|
+
// Calculate dots per contour based on path length
|
|
380
|
+
const dotsPerContour = (0, react_1.useMemo)(() => {
|
|
381
|
+
return resolvedPaths.map(path => {
|
|
382
|
+
const rawCount = Math.round(path.length / dotsPerPathUnit);
|
|
383
|
+
const bounded = maxDotsPerContour > 0
|
|
384
|
+
? Math.min(rawCount, maxDotsPerContour)
|
|
385
|
+
: rawCount;
|
|
386
|
+
return Math.max(minDotsPerContour, bounded);
|
|
387
|
+
});
|
|
388
|
+
}, [resolvedPaths, dotsPerPathUnit, minDotsPerContour, maxDotsPerContour]);
|
|
389
|
+
const totalDots = (0, react_1.useMemo)(() => {
|
|
390
|
+
return dotsPerContour.reduce((a, b) => a + b, 0);
|
|
391
|
+
}, [dotsPerContour]);
|
|
392
|
+
// Count dots per word for chart generation
|
|
393
|
+
const dotsPerWord = (0, react_1.useMemo)(() => {
|
|
394
|
+
const counts = [];
|
|
395
|
+
resolvedPaths.forEach((path, contourIndex) => {
|
|
396
|
+
const wordIdx = path.wordIndex;
|
|
397
|
+
while (counts.length <= wordIdx)
|
|
398
|
+
counts.push(0);
|
|
399
|
+
counts[wordIdx] += dotsPerContour[contourIndex];
|
|
400
|
+
});
|
|
401
|
+
return counts;
|
|
402
|
+
}, [resolvedPaths, dotsPerContour]);
|
|
403
|
+
// Generate chart positions per word - each word gets its own full-width line
|
|
404
|
+
const chartPositions = (0, react_1.useMemo)(() => {
|
|
405
|
+
if (!shouldShowChartIntro)
|
|
406
|
+
return [];
|
|
407
|
+
const allPositions = [];
|
|
408
|
+
dotsPerWord.forEach((numDots, wordIndex) => {
|
|
409
|
+
if (numDots === 0)
|
|
410
|
+
return;
|
|
411
|
+
// Each word gets its own time-series spanning the full width
|
|
412
|
+
// Use different seed per word for variation
|
|
413
|
+
const wordPositions = generateChartPositions(numDots, width, height, chartPattern, 123 + wordIndex * 1000);
|
|
414
|
+
wordPositions.forEach(pos => {
|
|
415
|
+
allPositions.push(Object.assign(Object.assign({}, pos), { wordIndex }));
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
return allPositions;
|
|
419
|
+
}, [shouldShowChartIntro, dotsPerWord, width, height, chartPattern]);
|
|
420
|
+
// Target positions for dots after chart phase - sampled along each contour
|
|
421
|
+
const dotTargetPositions = (0, react_1.useMemo)(() => {
|
|
422
|
+
// Group by word first to match chart positions order
|
|
423
|
+
const positionsByWord = [];
|
|
424
|
+
resolvedPaths.forEach((path, contourIndex) => {
|
|
425
|
+
const wordIdx = path.wordIndex;
|
|
426
|
+
while (positionsByWord.length <= wordIdx)
|
|
427
|
+
positionsByWord.push([]);
|
|
428
|
+
const numDots = dotsPerContour[contourIndex];
|
|
429
|
+
const sampledPoints = samplePointsAlongPath(path.d, numDots, dotDistribution, contourIndex * 1000);
|
|
430
|
+
sampledPoints.forEach(point => {
|
|
431
|
+
positionsByWord[wordIdx].push(Object.assign(Object.assign({}, point), { contourIndex, wordIndex: wordIdx }));
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
// Flatten in word order to match chartPositions
|
|
435
|
+
return positionsByWord.flat();
|
|
436
|
+
}, [resolvedPaths, dotsPerContour, dotDistribution]);
|
|
437
|
+
// Helper to get color for a word index
|
|
438
|
+
const getWordColor = (wordIndex) => {
|
|
439
|
+
if (wordColors && wordColors.length > 0) {
|
|
440
|
+
return wordColors[wordIndex % wordColors.length];
|
|
441
|
+
}
|
|
442
|
+
return color;
|
|
443
|
+
};
|
|
444
|
+
// Chart intro timing
|
|
445
|
+
const chartPhaseEndTime = shouldShowChartIntro
|
|
446
|
+
? chartDuration + chartLineFadeDuration + chartPauseDuration + chartTransitionDuration
|
|
447
|
+
: 0;
|
|
448
|
+
// Timing calculations (offset by chart phase if present)
|
|
449
|
+
const numPaths = resolvedPaths.length || 1;
|
|
450
|
+
const perItemDotsDuration = (dotsDuration * 0.5) / numPaths;
|
|
451
|
+
const perItemLinesDuration = (dotsDuration * 0.5) / numPaths;
|
|
452
|
+
const perItemAssemblyDuration = chaosDuration / numPaths;
|
|
453
|
+
const dotsPhaseEnd = chartPhaseEndTime + dotsDuration * 0.5;
|
|
454
|
+
const linesPhaseEnd = chartPhaseEndTime + dotsDuration;
|
|
455
|
+
// Skip assembly phase when chart intro handles the transition
|
|
456
|
+
const needsAssembly = isFragmented && !shouldShowChartIntro;
|
|
457
|
+
const assemblyEndTime = needsAssembly
|
|
458
|
+
? chartPhaseEndTime + dotsDuration + chaosDuration
|
|
459
|
+
: chartPhaseEndTime + dotsDuration;
|
|
460
|
+
const flowBeginTime = assemblyEndTime + flowDelay;
|
|
461
|
+
const totalDuration = flowBeginTime + flowDuration;
|
|
462
|
+
// Loop via remount
|
|
463
|
+
(0, react_1.useEffect)(() => {
|
|
464
|
+
if (!loop)
|
|
465
|
+
return;
|
|
466
|
+
const timeout = setTimeout(() => {
|
|
467
|
+
setCycle((c) => c + 1);
|
|
468
|
+
}, (totalDuration + loopDelay) * 1000);
|
|
469
|
+
return () => clearTimeout(timeout);
|
|
470
|
+
}, [loop, totalDuration, loopDelay, cycle]);
|
|
471
|
+
if (error) {
|
|
472
|
+
return (react_1.default.createElement("svg", { width: width, height: height },
|
|
473
|
+
react_1.default.createElement("text", { x: 10, y: 30, fill: "red", fontSize: 14 },
|
|
474
|
+
"Error: ",
|
|
475
|
+
error)));
|
|
476
|
+
}
|
|
477
|
+
if (!font) {
|
|
478
|
+
return (react_1.default.createElement("svg", { width: width, height: height },
|
|
479
|
+
react_1.default.createElement("text", { x: 10, y: 30, fill: color, fontSize: 14 }, "Loading font...")));
|
|
480
|
+
}
|
|
481
|
+
return (react_1.default.createElement("svg", { key: cycle, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg", style: { opacity } },
|
|
482
|
+
react_1.default.createElement("defs", null,
|
|
483
|
+
showGlow && (react_1.default.createElement("filter", { id: `glow-${uniqueId}`, x: "-50%", y: "-50%", width: "200%", height: "200%" },
|
|
484
|
+
react_1.default.createElement("feGaussianBlur", { stdDeviation: "2", result: "blur" }),
|
|
485
|
+
react_1.default.createElement("feMerge", null,
|
|
486
|
+
react_1.default.createElement("feMergeNode", { in: "blur" }),
|
|
487
|
+
react_1.default.createElement("feMergeNode", { in: "SourceGraphic" })))),
|
|
488
|
+
react_1.default.createElement("radialGradient", { id: `particleGradient-${uniqueId}`, cx: "50%", cy: "50%", r: "50%" },
|
|
489
|
+
react_1.default.createElement("stop", { offset: "0%", style: { stopColor: finalParticleColor, stopOpacity: 1 } }),
|
|
490
|
+
react_1.default.createElement("stop", { offset: "100%", style: { stopColor: finalParticleColor, stopOpacity: 0.3 } }))),
|
|
491
|
+
shouldShowChartIntro && chartPositions.length > 0 && (react_1.default.createElement("g", { className: "chart-intro" },
|
|
492
|
+
(() => {
|
|
493
|
+
// Group chart positions by word (chartPositions now has wordIndex)
|
|
494
|
+
const positionsByWord = new Map();
|
|
495
|
+
chartPositions.forEach((pos) => {
|
|
496
|
+
if (!positionsByWord.has(pos.wordIndex)) {
|
|
497
|
+
positionsByWord.set(pos.wordIndex, []);
|
|
498
|
+
}
|
|
499
|
+
positionsByWord.get(pos.wordIndex).push({ x: pos.x, y: pos.y });
|
|
500
|
+
});
|
|
501
|
+
return Array.from(positionsByWord.entries()).map(([wordIndex, positions]) => (react_1.default.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" },
|
|
502
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "0", to: "0.6", dur: "0.3s", begin: "0s", fill: "freeze" }),
|
|
503
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "0.6", to: "0", dur: `${chartLineFadeDuration}s`, begin: `${chartDuration}s`, fill: "freeze" }))));
|
|
504
|
+
})(),
|
|
505
|
+
chartPositions.map((chartPos, index) => {
|
|
506
|
+
const targetPos = dotTargetPositions[index];
|
|
507
|
+
if (!targetPos)
|
|
508
|
+
return null;
|
|
509
|
+
const dotRadius = strokeWidth;
|
|
510
|
+
const dotAppearDelay = (index / chartPositions.length) * 0.5; // Stagger appearance
|
|
511
|
+
const transitionBegin = chartDuration + chartLineFadeDuration + chartPauseDuration;
|
|
512
|
+
// Fade out when this dot's contour starts drawing
|
|
513
|
+
const contourDrawBegin = dotsPhaseEnd + (targetPos.contourIndex + 1) * perItemLinesDuration;
|
|
514
|
+
const dotColor = getWordColor(chartPos.wordIndex);
|
|
515
|
+
return (react_1.default.createElement("circle", { key: `chart-dot-${index}`, cx: chartPos.x, cy: chartPos.y, r: dotRadius, fill: dotColor, opacity: "0" },
|
|
516
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${dotAppearDelay}s`, fill: "freeze" }),
|
|
517
|
+
react_1.default.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" }),
|
|
518
|
+
react_1.default.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" }),
|
|
519
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${contourDrawBegin}s`, fill: "freeze" })));
|
|
520
|
+
}))),
|
|
521
|
+
react_1.default.createElement("g", { className: "opentype-paths" }, resolvedPaths.map((path, index) => {
|
|
522
|
+
const offset = fragmentOffsets[index];
|
|
523
|
+
const dotRadius = strokeWidth / 2;
|
|
524
|
+
const pathColor = getWordColor(path.wordIndex);
|
|
525
|
+
// Skip fragment offset when chart intro handles the transition
|
|
526
|
+
const useFragmentOffset = isFragmented && !shouldShowChartIntro;
|
|
527
|
+
return (react_1.default.createElement("g", { key: path.id, transform: useFragmentOffset ? `translate(${offset.x}, ${offset.y})` : undefined },
|
|
528
|
+
isFragmented && !shouldShowChartIntro && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
529
|
+
react_1.default.createElement("circle", { cx: path.endpoints.start.x, cy: path.endpoints.start.y, r: dotRadius, fill: pathColor, opacity: "0" },
|
|
530
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "0", to: "1", dur: "0.15s", begin: `${chartPhaseEndTime + index * perItemDotsDuration}s`, fill: "freeze" }),
|
|
531
|
+
react_1.default.createElement("animate", { attributeName: "opacity", from: "1", to: "0", dur: "0.2s", begin: `${dotsPhaseEnd + (index + 1) * perItemLinesDuration}s`, fill: "freeze" })))),
|
|
532
|
+
(() => {
|
|
533
|
+
// Determine animation mode
|
|
534
|
+
const shouldDrawLines = isFragmented || shouldShowChartIntro;
|
|
535
|
+
const drawDuration = shouldShowChartIntro && !isFragmented
|
|
536
|
+
? perItemLinesDuration
|
|
537
|
+
: perItemLinesDuration + chaosDuration;
|
|
538
|
+
return (react_1.default.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 },
|
|
539
|
+
shouldDrawLines && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
540
|
+
react_1.default.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" }),
|
|
541
|
+
react_1.default.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" }))),
|
|
542
|
+
!shouldDrawLines && fadeAfterAssembly && (react_1.default.createElement("animate", { attributeName: "opacity", from: "1", to: fadeOpacity, dur: "0.5s", begin: `${flowBeginTime}s`, fill: "freeze" }))));
|
|
543
|
+
})(),
|
|
544
|
+
useFragmentOffset && (react_1.default.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" }))));
|
|
545
|
+
})),
|
|
546
|
+
react_1.default.createElement("g", { className: "opentype-particles", filter: showGlow ? `url(#glow-${uniqueId})` : undefined }, resolvedPaths.flatMap((path, pathIndex) => {
|
|
547
|
+
return Array.from({ length: particlesPerPath }).map((_, particleIndex) => {
|
|
548
|
+
const particleDelay = (particleIndex / particlesPerPath) * flowDuration;
|
|
549
|
+
const beginTime = flowBeginTime + particleDelay;
|
|
550
|
+
return (react_1.default.createElement("circle", { key: `particle-${pathIndex}-${particleIndex}`, r: particleRadius, fill: `url(#particleGradient-${uniqueId})`, opacity: "0" },
|
|
551
|
+
react_1.default.createElement("animateMotion", { dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze", path: path.d }),
|
|
552
|
+
react_1.default.createElement("animate", { attributeName: "opacity", values: "0;1;1;0", keyTimes: "0;0.1;0.9;1", dur: `${flowDuration}s`, begin: `${beginTime}s`, fill: "freeze" })));
|
|
553
|
+
});
|
|
554
|
+
}))));
|
|
555
|
+
};
|
|
556
|
+
exports.OpenTypeTextReveal = OpenTypeTextReveal;
|
|
557
|
+
/**
|
|
558
|
+
* Offset all coordinates in path data
|
|
559
|
+
*/
|
|
560
|
+
function offsetPathData(d, offsetX, offsetY) {
|
|
561
|
+
let result = "";
|
|
562
|
+
let i = 0;
|
|
563
|
+
let coordIndex = 0;
|
|
564
|
+
while (i < d.length) {
|
|
565
|
+
const char = d[i];
|
|
566
|
+
// Check for command letters
|
|
567
|
+
if (/[MmLlHhVvCcSsQqTtAaZz]/.test(char)) {
|
|
568
|
+
result += char;
|
|
569
|
+
// Reset coord tracking for absolute commands, or handle relative
|
|
570
|
+
if (char === 'Z' || char === 'z') {
|
|
571
|
+
coordIndex = 0;
|
|
572
|
+
}
|
|
573
|
+
else if (char === 'H' || char === 'h') {
|
|
574
|
+
// Horizontal - only X coords
|
|
575
|
+
coordIndex = -1; // Special handling
|
|
576
|
+
}
|
|
577
|
+
else if (char === 'V' || char === 'v') {
|
|
578
|
+
// Vertical - only Y coords
|
|
579
|
+
coordIndex = -2; // Special handling
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
coordIndex = 0;
|
|
583
|
+
}
|
|
584
|
+
i++;
|
|
585
|
+
}
|
|
586
|
+
else if (/[-\d.]/.test(char)) {
|
|
587
|
+
// Parse number
|
|
588
|
+
let numStr = "";
|
|
589
|
+
while (i < d.length && /[-\d.eE]/.test(d[i])) {
|
|
590
|
+
numStr += d[i];
|
|
591
|
+
i++;
|
|
592
|
+
}
|
|
593
|
+
const num = parseFloat(numStr);
|
|
594
|
+
// Apply offset based on coordinate type
|
|
595
|
+
if (coordIndex === -1) {
|
|
596
|
+
// H command - X only
|
|
597
|
+
result += String(num + offsetX);
|
|
598
|
+
}
|
|
599
|
+
else if (coordIndex === -2) {
|
|
600
|
+
// V command - Y only
|
|
601
|
+
result += String(num + offsetY);
|
|
602
|
+
}
|
|
603
|
+
else if (coordIndex % 2 === 0) {
|
|
604
|
+
// X coordinate
|
|
605
|
+
result += String(num + offsetX);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// Y coordinate
|
|
609
|
+
result += String(num + offsetY);
|
|
610
|
+
}
|
|
611
|
+
if (coordIndex >= 0)
|
|
612
|
+
coordIndex++;
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// Whitespace or comma
|
|
616
|
+
result += char;
|
|
617
|
+
i++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
}
|