Signature
An animated SVG signature effect that draws out text as if hand-written.
Preview supplied by Arlan Marat; live previews were recorded from Arlan's Vault when no published video file was available. Preview: platform recorded · rights cleared.
Wild type is an animated canvas study published directly in the Vault gallery.
VSource LandVaultWhy it stands out
Wild type is an animated canvas study published directly in the Vault gallery.
Build this: a single design word in heavy white oblique lowercase on black that pop-typewrites in — each letter appears at FULL SIZE but to the RIGHT of its slot (later letters further out) and the line eases left into place — then goes wild: the word inflates ~8x, churns full-bleed through two zoom pulses, drapes over an arch and collapses exactly home; breathes; then UN-TYPES: the same measured typewriter tracks played backwards a little faster, so the last letter peels away first and the line drifts out the way it drifted in. That reversed exit IS the loop seam — no cut, no hold-then-jump — and the next loop types the next word from a list, each word carrying its own accent color. Two inks: white, and the word's accent that only shows as a REGISTRATION SLIP while the word moves — draw the same warped path a fraction of a tick earlier in the accent first, then the white on top, and the color peeks out exactly where the mesh is moving and snaps under the white at rest; the black ground glows faintly toward that accent with the warp. THE CORE TRICK IS ONE GLOBAL WARP MESH: a coarse 9x4 grid of control points over the rest word box, Catmull-Rom interpolated, one row of control positions per tick, in NORMALIZED word coordinates (X spans -1..1 across the word) — which is what makes it word-agnostic. Per-letter transforms cannot represent the wild phase (each letter magnifies more toward its outer edge, stems become wedges, baselines curve) and a global polynomial cannot either (at peak zoom the visible window spans a sliver of the word box, where any low-order field is nearly affine); local control points put the freedom where the window is. TRACE THE WORD AT RUNTIME: rasterize each letter alone at its advance on an offscreen canvas (with a synthetic oblique shear), marching-squares the alpha at iso 0.5 with interpolated edges — stitch segments by BOTH endpoints, they come out in arbitrary direction — Douglas-Peucker it, drop micro-loops by area (antialiasing specks warp into stray chips), find counters by containment, normalize into the word box; then any word rides the same fitted motion, and the typewriter maps letters onto the four measured tracks by relative position (first, last, nearest between). Warp the CONTOUR POINTS and refill a Path2D, never pixels; precompute each point's Catmull-Rom weights once per word so a frame is two 36-term dot products per point. Play the typewriter phases in discrete ticks at 30fps (the pops read wrong smoothed) but the WARP continuously between rows (held 8x moves strobe on a crisp canvas); once magnified draw rings as curves through their points, EXCEPT at sharp corners, which stay straight (a spline through a corner overshoots into a bump outside the letter); mirror the mesh in X on alternate loops. Finish: a low-alpha noise tile screened over the frame, ink alpha thinned slightly where a ring's perimeter stretches most, an afterimage of the previous frame scaled by mesh velocity during the pulses. During the wild phase only, the pointer bends the mesh: a spring-damped Gaussian nudge on the control points nearest it, relaxing to zero afterwards. Canvas 2D, framework-free; waits for the real font before tracing; the hold is a single paint; pauses offscreen / tab-hidden; reduced motion draws the first word at rest.
The complete, self-contained implementation follows, one file per block. It is framework-agnostic core logic — wire it into your own component and mount it on an element.
### wild-type/params.ts
```ts
export const FPS = 30;
export const BLANK_TICKS = 2;
export const TYPE_TICKS = 23;
export const WARP_TICKS = 37;
export const HOLD_TICKS = 27;
export const OUT_TICKS = 15;
export const OUT_SPEED = 1.6;
export const LOOP_TICKS = BLANK_TICKS + TYPE_TICKS + WARP_TICKS + HOLD_TICKS + OUT_TICKS;
export const REF_CUT_TICK = 6 + (3 - BLANK_TICKS);
export const REF_HALF_WIDTH = 167.5;
export const MESH_XS = [-1.2, -0.9, -0.6, -0.3, 0, 0.3, 0.6, 0.9, 1.2];
export const MESH_YS = [-0.6, -0.2, 0.2, 0.6];
export const WORDS = ["design", "type", "grid", "color", "motion", "shape", "pixel", "layout"];
export const ACCENTS = [
"#4f6bff",
"#ff3d7f",
"#2ee06a",
"#ff8a1f",
"#b46bff",
"#28d8f0",
"#ffd84a",
"#ff4a3d",
];
export const FONT_CSS = "var(--font-gstaad), 'Helvetica Neue', sans-serif";
export const FONT_WEIGHT = 700;
export const OBLIQUE = 0.2;
export const TRACE_EM = 320;
export const TRACE_TOL = 0.45;
export const TRACE_MIN_AREA = 6;
export const CORNER_DEG = 48;
export const REST_WIDTH_FRAC = 0.34;
export const BG = "#000000";
export const INK = "#ffffff";
export const BG_GLOW = 0.16;
export const GRAIN_ALPHA = 0.06;
export const GRAIN_TILE = 160;
export const THIN = 0.08;
export const THIN_MAX = 0.14;
export const LAG_TICKS = 1.6;
export const AFTER_K = 0.9;
export const AFTER_MAX = 0.28;
export const PULL_SIGMA = 0.55;
export const PULL_GAIN = 0.42;
export const PULL_K = 0.16;
export const PULL_DAMP = 0.78;
export const MIRROR_ALTERNATE = true;
```
### wild-type/motion.ts
```ts
export const TYPE_START = 9;
export const TYPE_END = 31;
export const TYPE_POSE: Record<string, (number[] | null)[]> = {
w: [[41.04, 0.29, 0.0049, 0.9924, 0.0032], [41.01, 0.25, 0.0051, 0.9924, 0.0026], [24.19, 0.23, 0.0036, 0.9929, 0.0038], [24.19, 0.24, 0.0031, 0.993, 0.003], [19.13, 0.33, 0.005, 0.9904, 0.006], [15.36, 0.28, 0.0037, 0.9909, 0.0013], [9.9, 0.19, 0.0048, 0.991, 0.0036], [9.9, 0.19, 0.0048, 0.991, 0.0036], [7.85, 0.26, 0.0065, 0.9911, 0.0047], [6.2, 0.36, 0.0031, 0.992, 0.0032], [4.87, 0.25, 0.0024, 0.9914, -0.0007], [3.73, 0.19, 0.0046, 0.9934, 0.0035], [2.87, 0.3, 0.0037, 0.9906, 0.002], [2.2, 0.39, 0.0037, 0.992, 0.0065], [1.62, 0.35, 0.0045, 0.9905, 0.0036], [1.19, 0.25, 0.0069, 0.992, 0.0073], [0.76, 0.24, 0.0066, 0.9924, 0.0087], [0.73, 0.26, 0.0068, 0.9926, 0.0067], [0.73, 0.26, 0.0068, 0.9926, 0.0067], [0.73, 0.26, 0.0068, 0.9926, 0.0067], [0.73, 0.26, 0.0068, 0.9926, 0.0067], [0.72, 0.25, 0.0066, 0.9921, 0.0102], [0.05, 0.19, 0.0005, 0.998, 0.0026]],
i: [null, null, null, null, [30.15, -0.14, 0.0099, 0.997, 0.0072], [23.23, -0.14, 0.0068, 0.9894, 0.002], [14.28, -0.1, 0.0298, 0.9948, 0.0286], [14.26, -0.04, 0.0332, 0.9945, 0.0302], [11.12, -0.19, 0.0113, 0.9924, 0.0073], [8.44, -0.13, 0.0062, 1.0028, 0.0036], [6.44, -0.13, 0.0062, 1.0028, 0.0036], [5.14, -0.16, 0.0127, 0.9976, 0.0086], [3.84, -0.2, 0.0152, 1.007, 0.0173], [2.77, -0.26, 0.0211, 1.0137, 0.0195], [2.08, -0.17, 0.0129, 0.9928, 0.0097], [1.12, -0.3, 0.0226, 0.996, 0.0186], [0.62, -0.95, 0.0151, 1.001, 0.0121], [0.61, -1.04, 0.0094, 1.0148, 0.0084], [0.62, -0.99, 0.0099, 1.0154, 0.0093], [0.54, -0.16, 0.0126, 1.0081, 0.0116], [0.54, -0.16, 0.0126, 1.0081, 0.0116], [0.54, -0.16, 0.0126, 1.0081, 0.0116], [0.09, 0.06, 0.0173, 1.0083, 0.0158]],
l: [null, null, null, null, null, null, [21.63, -0.11, 0.0032, 0.9965, 0.0049], [21.63, -0.11, 0.0032, 0.9965, 0.0049], [16.63, -0.11, 0.0032, 0.9965, 0.0049], [12.66, -0.21, 0.0038, 0.9978, 0.0035], [9.37, 0.25, 0, 1.0121, 0], [6.63, -0.11, 0.0032, 0.9965, 0.0049], [4.63, -0.11, 0.0032, 0.9965, 0.0049], [3.37, 0.25, 0, 1.0121, 0], [2.13, 0, 0, 1.0121, 0], [1.37, 0.25, 0, 1.0121, 0], [0.69, -0.15, 0.0017, 1.003, 0.0011], [0.67, -0.16, 0.0024, 1.0029, 0.0011], [0.66, -0.17, 0.0025, 1.003, 0.001], [0.66, -0.17, 0.0025, 1.003, 0.001], [0.66, -0.17, 0.0025, 1.003, 0.001], [0.6, -0.36, 0.0046, 1.0056, 0.0031], [-0.43, 0, 0.0051, 1.0019, 0.0041]],
d: [null, null, null, null, null, null, [44.22, 1.09, 0.0607, 1.0024, -0.081], [44.22, 1.09, 0.0607, 1.0024, -0.081], [32.32, 1.2, 0.0693, 0.9986, -0.0683], [24.45, 0.71, 0.0632, 1.0011, -0.0763], [18.69, 1.28, 0.0576, 0.992, -0.0667], [11.89, 0.77, 0.0164, 0.995, -0.0267], [7.65, 0.59, 0.0112, 0.9918, -0.005], [4.93, 0.4, 0.0066, 0.9931, 0.0015], [3.2, 0.46, 0.0037, 0.9907, 0], [1.78, 0.45, 0.0041, 0.9908, 0.0075], [0.62, 0.59, 0.0002, 0.99, 0.0048], [0.35, 0.58, -0.0004, 0.99, -0.0005], [0.35, 0.58, -0.0004, 0.99, -0.0005], [0.35, 0.58, -0.0004, 0.99, -0.0005], [0.35, 0.58, -0.0004, 0.99, -0.0005], [0.35, 0.58, -0.0004, 0.99, -0.0005], [0, 0.03, 0.0023, 0.9992, 0.0017]],
};
export const WARP_START = 32;
export const WARP_END = 68;
export const MESH: number[][] = [
[-1.2307, -0.9143, -0.7025, -0.1938, 0.0035, 0.3171, 0.6225, 0.9593, 1.0613, -1.2295, -0.9191, -0.6056, -0.2914, 0.0013, 0.31, 0.6194, 0.922, 1.2406, -1.2462, -0.9167, -0.6042, -0.3032, 0.0001, 0.3032, 0.6118, 0.9114, 1.2398, -1.2322, -0.9183, -0.6096, -0.3045, -0.0014, 0.3014, 0.6134, 0.9083, 1.2237, -0.6337, -0.6281, -0.6385, -0.6067, -0.6227, -0.6203, -0.5813, -0.5896, -0.5809, -0.1708, -0.2264, -0.2138, -0.2098, -0.2013, -0.2002, -0.1941, -0.1791, -0.179, 0.1575, 0.191, 0.197, 0.1956, 0.175, 0.1953, 0.2087, 0.2113, 0.2074, 0.6004, 0.6064, 0.6011, 0.6026, 0.6053, 0.6431, 0.6142, 0.6209, 0.6293],
[-1.4155, -1.1201, -0.6189, -0.2946, 0.0187, 0.3683, 0.7268, 1.1417, 1.5341, -1.4201, -1.0006, -0.624, -0.302, 0.0157, 0.3431, 0.6804, 1.0245, 1.4065, -1.3042, -0.9699, -0.62, -0.3027, -0.0045, 0.3085, 0.6325, 0.9652, 1.306, -1.4209, -0.9605, -0.6067, -0.3198, 0.0081, 0.2983, 0.606, 0.9254, 1.1717, -0.64, -0.4294, -0.4781, -0.6116, -0.6487, -0.6522, -0.6497, -0.5658, -0.4765, -0.3302, -0.3139, -0.2658, -0.2201, -0.2046, -0.1932, -0.1533, -0.1249, -0.0163, 0.3898, 0.2033, 0.184, 0.2055, 0.2067, 0.24, 0.2542, 0.3056, 0.4243, 0.3179, 0.534, 0.59, 0.591, 0.5896, 0.5696, 0.6493, 0.6631, 0.5565],
[-1.8609, -1.2403, -0.7409, -0.3474, 0.07, 0.5321, 0.9141, 1.5304, 2.1436, -1.8303, -1.1762, -0.6754, -0.3155, 0.0429, 0.3923, 0.814, 1.2473, 1.7522, -1.635, -1.0997, -0.6561, -0.2907, 0.0047, 0.3269, 0.6694, 1.087, 1.5286, -1.5552, -1.0544, -0.6552, -0.365, -0.0297, 0.2998, 0.6243, 0.9208, 1.2552, -1.1441, -1.0888, -0.7163, -0.4679, -0.7662, -0.7268, -0.7746, -0.4916, -0.366, -0.6762, -0.5044, -0.3718, -0.2487, -0.2082, -0.1628, -0.0742, 0.0072, 0.0964, -0.0842, 0.092, 0.1471, 0.1689, 0.1695, 0.2342, 0.3436, 0.4718, 0.6061, 0.7367, 0.6355, 0.5988, 0.6246, 0.6516, 0.6567, 0.7433, 0.8105, 0.7957],
[-2.6519, -1.6365, -1.0162, -0.3097, 0.2269, 0.8921, 1.4574, 2.3811, 3.3544, -2.9828, -1.5649, -0.7555, -0.3152, 0.0854, 0.5526, 1.0548, 1.7639, 2.7935, -2.4435, -1.411, -0.7633, -0.3268, 0.027, 0.3577, 0.7899, 1.32, 1.9305, -2.1584, -1.3576, -0.7464, -0.3847, -0.0676, 0.2665, 0.5885, 1.0395, 1.7355, -1.6107, -1.2999, -1.1679, -1.2267, -0.9584, -0.8987, -0.7861, -0.3704, 0.1522, -1.6172, -0.9157, -0.5492, -0.3184, -0.2089, -0.1559, 0.0432, 0.3121, 0.6658, -0.2929, -0.0376, 0.1027, 0.1241, 0.1925, 0.2999, 0.5591, 0.8464, 1.2091, 0.6868, 0.6402, 0.5964, 0.5909, 0.6602, 0.727, 0.9262, 1.2281, 1.6177],
[-4.1971, -2.0343, -0.2744, 0.5824, 0.7845, 2.7444, 3.9295, 6.3695, 10.1805, -4.2142, -2.5342, -0.8784, -0.3283, 0.4105, 1.2373, 2.3619, 4.4262, 7.9567, -5.896, -2.6425, -1.1922, -0.5699, 0.0115, 0.5708, 1.3185, 2.6352, 5.5687, -5.0562, -2.668, -1.4415, -0.517, -0.1391, 0.1112, 0.7927, 2.5922, 5.0481, -3.0759, -1.989, -1.5602, -1.7876, -1.4091, -1.3015, -1.4187, 0.6531, 2.7246, -2.4216, -1.9115, -0.8942, -0.4303, -0.6595, -0.3331, 0.4171, 1.9156, 4.4372, -1.3967, -0.433, -0.0596, 0.1053, 0.3178, 0.6269, 1.2526, 2.5876, 5.4843, 0.2889, 0.4976, 0.5117, 0.4672, 0.5371, 0.8912, 2.2841, 4.0773, 6.853],
[-4.5657, -2.1943, -0.2706, 0.0694, 0.9844, 3.1832, 3.9733, 7.0551, 11.2304, -4.599, -2.7036, -0.8867, -0.427, 0.4142, 1.3927, 2.6527, 4.8691, 8.7298, -6.3843, -2.8991, -1.304, -0.6185, -0.0082, 0.6134, 1.4742, 2.9037, 6.1119, -5.5913, -2.8968, -1.6177, -0.5139, -0.1109, 0.1166, 0.7768, 2.6962, 5.4552, -3.5058, -2.2385, -1.7248, -2.2458, -1.6371, -1.4098, -0.9015, 0.7408, 3.2349, -2.7816, -1.9546, -0.9649, -0.4055, -0.7082, -0.3469, 0.4952, 2.2432, 5.0828, -1.8215, -0.5515, -0.091, 0.0723, 0.284, 0.6831, 1.4495, 2.9491, 6.0899, 0.3019, 0.4776, 0.5282, 0.4243, 0.6071, 0.9707, 2.5485, 4.45, 7.6943],
[-4.7341, -2.2863, -0.3451, -0.2124, 0.9926, 3.4027, 4.1351, 7.349, 11.6855, -4.7723, -2.7811, -0.8986, -0.4666, 0.4351, 1.4585, 2.7728, 5.0967, 9.0718, -6.7366, -3.0127, -1.3506, -0.6404, -0.0018, 0.6438, 1.5325, 2.955, 6.3561, -5.821, -3.0154, -1.7074, -0.4971, -0.1393, 0.0968, 0.7507, 2.9906, 5.6482, -3.6775, -2.4458, -1.8173, -2.6322, -1.7228, -1.5042, -0.8388, 0.8029, 3.4213, -2.9233, -1.9276, -0.9978, -0.4119, -0.731, -0.3499, 0.5254, 2.3911, 5.3337, -1.9161, -0.5961, -0.1046, 0.0588, 0.2843, 0.6951, 1.5176, 3.0696, 6.3698, 0.2835, 0.4997, 0.5308, 0.4374, 0.6443, 1.0122, 2.6548, 4.7449, 8.0376],
[-4.8208, -2.2255, -0.3786, -0.2825, 1.0309, 3.5179, 4.2216, 7.5017, 11.9211, -4.8621, -2.8383, -0.9032, -0.478, 0.4477, 1.4928, 2.8209, 5.1132, 9.2465, -6.8748, -3.069, -1.3915, -0.6049, -0.0016, 0.6562, 1.5611, 3.0085, 6.5706, -5.9429, -3.0791, -1.7438, -0.5238, -0.1651, 0.0688, 0.7592, 3.0395, 5.7426, -3.7692, -2.504, -1.8482, -2.7199, -1.7976, -1.5498, -0.7272, 0.743, 3.53, -2.9993, -1.8265, -1.0133, -0.4178, -0.7197, -0.3524, 0.5374, 2.3574, 5.4743, -1.9612, -0.6258, -0.0948, 0.0123, 0.2655, 0.7119, 1.5545, 3.1435, 6.5236, 0.1554, 0.481, 0.5368, 0.4103, 0.6977, 1.044, 2.6981, 4.8541, 8.2242],
[-4.8601, -2.2424, -0.5292, -0.1149, 1.0099, 3.5824, 4.2624, 7.5709, 12.0273, -4.8031, -2.7061, -0.8941, -0.4979, 0.453, 1.5114, 2.8465, 4.9074, 9.3246, -6.9362, -3.1008, -1.4044, -0.5926, -0.0021, 0.6612, 1.5692, 2.8987, 6.6719, -5.959, -3.1169, -1.7531, -0.5454, -0.1638, 0.0637, 0.766, 3.0603, 5.7834, -3.8112, -2.53, -1.8651, -2.7208, -1.8677, -1.5827, -0.7323, 0.763, 3.5832, -3.0182, -1.9471, -1.0292, -0.4134, -0.7067, -0.3523, 0.548, 2.4138, 5.5415, -1.9894, -0.6355, -0.0968, -0.0004, 0.2531, 0.7208, 1.5647, 3.0659, 6.6304, 0.1485, 0.4877, 0.5112, 0.4434, 0.6801, 1.059, 2.7257, 4.9055, 8.3117],
[-4.8846, -2.2543, -0.5969, -0.1092, 1.0482, 3.6015, 4.1921, 7.5056, 12.0838, -4.8269, -2.7518, -0.9045, -0.4832, 0.4384, 1.5178, 2.8555, 4.9398, 9.3698, -6.9852, -3.1156, -1.4112, -0.5941, -0.0023, 0.6643, 1.5769, 2.9363, 6.8056, -5.9864, -3.1311, -1.7531, -0.54, -0.1745, 0.0643, 0.7747, 2.8606, 5.814, -3.8283, -2.5426, -2.1456, -2.8604, -1.8861, -1.5882, -0.8822, 0.7601, 3.5907, -3.0316, -1.9568, -1.0292, -0.4268, -0.6935, -0.353, 0.5426, 2.4198, 5.5594, -2.029, -0.639, -0.0988, 0.0016, 0.2534, 0.7214, 1.5687, 3.091, 6.6547, 0.1501, 0.4803, 0.4961, 0.4434, 0.6809, 1.0615, 2.7854, 4.8239, 8.3441],
[-4.89, -2.2559, -0.4559, -0.1069, 1.0795, 3.5976, 4.2167, 7.519, 12.1034, -4.8327, -2.8147, -0.9237, -0.479, 0.4178, 1.5214, 2.8583, 5.0866, 9.3834, -6.9955, -3.111, -1.4189, -0.5845, -0.0017, 0.6657, 1.5794, 2.9482, 6.864, -5.9963, -3.1862, -1.7438, -0.5565, -0.1745, 0.0604, 0.795, 2.8633, 5.8196, -3.8448, -2.5553, -2.1906, -2.9563, -1.8995, -1.6034, -0.8639, 0.7594, 3.5979, -3.0468, -1.9487, -1.0403, -0.4346, -0.6682, -0.3535, 0.5306, 2.2624, 5.5683, -2.0438, -0.6489, -0.1067, -0.0056, 0.2751, 0.7203, 1.5736, 3.0285, 6.5639, 0.1398, 0.5364, 0.4941, 0.4621, 0.6663, 1.0766, 2.7831, 4.8274, 8.3557],
[-5.0614, -2.4453, -0.492, -0.0471, 1.0937, 3.5422, 4.755, 7.7261, 12.4566, -4.9982, -3.2017, -1.005, -0.5323, 0.4028, 1.5555, 2.9064, 5.1325, 9.6696, -7.2772, -3.1905, -1.4712, -0.6705, -0.0116, 0.6733, 1.6176, 3.0622, 7.1837, -6.1778, -3.4175, -1.7021, -0.5892, -0.1622, 0.1066, 0.8213, 2.8596, 6.0191, -3.9385, -2.6263, -2.0609, -3.0493, -1.9828, -1.6098, -0.9875, 0.6303, 3.6251, -3.118, -2.4745, -1.0994, -0.4658, -0.6188, -0.382, 0.5211, 2.1893, 5.6663, -2.1898, -0.645, -0.1044, 0.0911, 0.3146, 0.7309, 1.5747, 3.1107, 6.6958, 0.1657, 0.5585, 0.5533, 0.4679, 0.6959, 1.0847, 2.7858, 4.8814, 8.5523],
[-5.6201, -2.9235, -1.2384, -0.575, 1.123, 3.3867, 5.2205, 8.5308, 13.4997, -6.6066, -3.0979, -1.4015, -0.3733, 0.4165, 1.6061, 3.0973, 5.7128, 10.4592, -7.7993, -3.6631, -1.5617, -0.7115, -0.0096, 0.7388, 1.7133, 3.5617, 7.1826, -6.675, -3.3243, -1.7732, -0.712, -0.2264, 0.1373, 0.913, 3.0144, 6.7978, -4.0678, -2.6246, -2.8699, -2.7953, -1.8383, -1.6281, -1.3398, 0.9489, 3.4928, -2.58, -2.3756, -1.3561, -0.7137, -0.7919, -0.4443, 0.5558, 2.4309, 5.613, -1.2054, -0.5636, -0.09, 0.1237, 0.3182, 0.7235, 1.5925, 3.687, 7.262, 0.5702, 0.8104, 0.9567, 0.7543, 0.8261, 1.1795, 2.6846, 5.2548, 9.0202],
[-8.9038, -5.4271, -2.5698, -0.4669, 1.2347, 3.0707, 4.8446, 7.743, 11.6651, -8.3756, -5.2557, -2.4598, -0.8978, 0.2997, 1.6733, 3.2375, 5.6431, 9.1857, -7.6819, -4.5204, -2.0427, -0.8582, 0.0182, 0.8997, 2.0897, 4.23, 7.2744, -6.6226, -3.8526, -1.9788, -0.6777, -0.3674, 0.2541, 1.3061, 2.928, 5.9057, -4.8615, -3.5527, -2.2801, -2.0539, -2.5585, -1.8319, -1.2527, -0.1254, 2.0888, -2.7024, -2.8788, -1.9596, -1.0671, -0.5083, -0.4852, 0.4314, 1.9641, 4.3471, -0.7396, -0.1895, 0.0899, 0.3057, 0.32, 0.7094, 1.6753, 3.2475, 6.1475, 0.7765, 1.5091, 1.4908, 1.521, 1.2078, 1.4264, 2.3651, 4.118, 7.0752],
[-15.9966, -9.1348, -4.3308, -1.498, 0.59, 2.1092, 3.7274, 6.1496, 9.7955, -16.0755, -9.449, -4.7799, -1.7644, 0.4073, 1.965, 3.9266, 6.9221, 11.176, -15.0045, -8.7963, -4.7447, -2.1149, -0.2986, 1.5238, 2.787, 6.7046, 11.3788, -12.8391, -7.6237, -3.4006, -1.2488, -0.5446, 0.9934, 2.93, 5.9244, 10.3481, -0.6685, -1.6573, -2.7349, -3.7897, -4.4452, -5.3139, -5.8846, -5.7108, -5.8565, 2.0408, 0.7156, -0.2522, -1.3223, -2.0466, -2.23, -1.0772, -1.807, -0.9414, 5.188, 3.3185, 2.2377, 1.5827, 0.7018, 0.9426, 0.2196, 2.296, 3.6865, 8.5496, 6.2936, 4.5876, 3.3948, 2.5487, 2.3518, 3.5726, 5.5203, 7.8036],
[-15.0242, -9.3072, -5.1322, -2.2946, 0.2168, 1.8177, 4.4225, 7.8555, 12.217, -14.1553, -8.8343, -4.9945, -2.1544, 0.2654, 1.4033, 3.5279, 7.0881, 11.2342, -13.3144, -8.2098, -4.8024, -2.3401, -0.5091, 1.6536, 3.8146, 6.6668, 10.9983, -12.9486, -7.9627, -4.0419, -2.0096, -0.4543, 1.4795, 3.8292, 6.7982, 11.0621, -2.7239, -2.5821, -2.9681, -4.0472, -5.3184, -5.8178, -7.6213, -8.4002, -8.8112, -0.5295, -0.4556, -0.9419, -1.2526, -2.2111, -1.8235, -1.7703, -2.0813, -1.5393, 2.5365, 2.4429, 2.0598, 1.8408, 1.0349, 0.9957, 1.1199, 2.2384, 4.0813, 6.0312, 5.1946, 4.6077, 3.9676, 2.7889, 3.0076, 3.6459, 5.1523, 7.6077],
[-15.8128, -9.8521, -5.5149, -2.5154, 0.2511, 1.461, 4.0237, 7.7871, 12.3007, -14.8113, -9.3552, -5.2973, -2.2699, 0.3122, 1.3151, 3.6014, 7.327, 11.5959, -13.8007, -8.5018, -5.5448, -2.7599, -0.6732, 1.7177, 3.9755, 7.0629, 11.5968, -13.2663, -7.9772, -4.0842, -2.0952, -0.4351, 1.7422, 4.1294, 7.3467, 11.8179, -2.2076, -2.3108, -3.0111, -4.3046, -5.4913, -5.8321, -7.507, -9.1081, -9.7448, 0.0404, -0.1131, -0.3805, -1.0966, -2.3727, -2.0655, -2.103, -2.3358, -2.1257, 3.1978, 2.717, 2.7255, 2.0475, 1.2199, 0.9124, 0.8307, 2.0138, 3.7404, 6.8226, 5.8243, 5.1521, 4.0938, 2.8009, 3.3045, 3.8564, 4.9252, 7.4115],
[-16.3787, -10.24, -5.7876, -2.727, 0.1371, 1.3348, 3.9368, 7.7774, 12.4131, -15.2955, -9.6779, -5.5192, -2.4047, 0.2304, 1.2787, 3.6093, 7.6083, 11.8612, -14.1821, -8.7339, -5.9872, -2.8335, -0.7027, 1.7536, 4.0778, 7.2857, 11.9962, -13.5486, -8.1224, -4.1609, -2.1099, -0.4212, 1.8335, 4.3055, 7.6446, 12.3079, -1.8815, -2.124, -2.9447, -4.346, -5.6098, -5.7106, -7.8107, -9.5303, -10.2895, 0.4117, 0.1288, -0.2734, -0.9885, -2.3953, -2.2716, -2.1565, -2.4926, -2.4237, 3.6415, 3.025, 2.9411, 2.22, 1.2716, 0.9141, 0.8803, 1.9446, 3.6198, 7.3637, 6.2143, 5.5327, 4.2629, 2.871, 3.396, 3.9101, 4.9376, 7.3969],
[-16.3665, -10.2084, -6.0037, -2.8897, 0.3133, 1.313, 3.828, 7.7506, 12.3744, -15.2761, -10.0071, -5.7674, -2.3434, 0.2573, 1.2558, 3.6493, 7.4807, 11.8503, -14.1522, -8.8146, -5.6535, -2.9634, -0.7392, 1.7453, 4.051, 7.2774, 12.0064, -13.5063, -8.0824, -4.3615, -2.1503, -0.4966, 1.9012, 4.3452, 7.6603, 12.3311, -1.8757, -2.1225, -3.1186, -4.4292, -5.4206, -5.4075, -7.4698, -9.7616, -10.3781, 0.4119, 0.0218, -0.3602, -1.0966, -2.4007, -2.2862, -2.2209, -2.6765, -2.5163, 3.6354, 3.0318, 2.7245, 2.2044, 1.2212, 0.8207, 0.9478, 1.9571, 3.5223, 7.3527, 6.1663, 5.2509, 4.3341, 3.5169, 3.5711, 3.6954, 4.8995, 7.2955],
[-16.4774, -10.282, -6.0537, -2.8838, 0.3165, 1.2934, 3.8278, 7.769, 12.4198, -15.3747, -10.0741, -6.0253, -2.2, 0.2435, 1.2525, 3.6769, 7.5148, 11.9118, -14.236, -8.767, -6.0041, -2.9917, -0.7463, 1.7527, 4.1269, 7.3216, 12.0835, -13.577, -8.1226, -4.606, -2.1584, -0.4965, 1.9162, 4.3755, 7.714, 12.4195, -1.8283, -2.0916, -3.1041, -4.4305, -5.3998, -5.314, -7.8062, -9.8217, -10.4533, 0.4708, 0.0654, -0.3493, -1.2287, -2.3721, -2.2884, -2.2565, -2.6923, -2.5418, 3.7114, 3.2911, 2.9342, 2.2442, 1.2245, 0.8365, 0.9207, 1.9703, 3.5336, 7.45, 6.2431, 5.4129, 4.385, 3.5178, 3.5825, 3.7016, 4.9299, 7.3292],
[-16.5149, -10.3051, -6.0668, -2.8892, 0.3907, 1.2979, 3.8467, 7.7894, 12.4511, -15.4102, -10.0972, -6.0309, -2.2046, 0.2429, 1.2462, 3.6762, 7.5331, 11.9402, -14.2695, -8.6878, -6.0774, -2.9988, -0.748, 1.7568, 4.0965, 7.3384, 12.111, -13.6098, -8.1425, -4.6176, -2.1641, -0.5232, 1.9202, 4.3851, 7.7311, 12.4469, -1.8353, -2.0979, -3.1118, -4.4406, -5.2114, -5.3083, -7.9227, -9.842, -10.474, 0.4693, 0.0642, -0.6506, -1.2232, -2.3768, -2.2926, -2.2381, -2.6961, -2.5443, 3.7177, 3.2976, 2.9564, 2.2496, 1.228, 0.8356, 0.9005, 1.9773, 3.5452, 7.4651, 6.2565, 5.4252, 4.3955, 3.5266, 3.592, 3.7119, 4.9438, 7.3497],
[-16.4617, -10.2697, -6.0435, -2.8499, 0.4457, 1.2923, 3.8422, 7.7739, 12.4223, -15.3605, -10.0628, -6.0081, -2.1687, 0.2478, 1.2523, 3.6553, 7.5331, 11.9114, -14.2238, -8.674, -6.1711, -2.9853, -0.7158, 1.7569, 4.1148, 7.3222, 12.0807, -13.5665, -8.2148, -4.6, -2.1534, -0.533, 1.9113, 4.3771, 7.7132, 12.415, -1.8347, -2.0954, -3.1057, -4.43, -5.1481, -5.1785, -8.0009, -9.814, -10.4434, 0.4635, 0.0605, -0.5015, -1.2468, -2.3716, -2.2876, -2.2328, -2.6888, -2.5366, 3.7027, 3.2848, 3.0771, 2.2488, 1.2146, 0.8568, 0.8899, 1.9711, 3.5353, 7.4394, 6.2353, 5.407, 4.3807, 3.4746, 3.5722, 3.7002, 4.9291, 7.3289],
[-16.3012, -10.1664, -5.9785, -2.8294, 0.5023, 1.291, 3.7356, 7.7164, 12.3222, -15.2122, -9.9632, -5.9957, -2.1622, 0.2539, 1.257, 3.6296, 7.4719, 11.8096, -14.0886, -8.474, -6.2101, -2.9531, -0.7038, 1.7463, 4.0823, 7.2591, 11.9722, -13.4406, -8.1375, -4.5554, -2.1307, -0.5246, 1.872, 4.3239, 7.644, 12.3002, -1.8096, -2.0628, -3.0602, -4.4096, -5.1383, -5.2847, -7.9018, -9.6949, -10.3146, 0.4682, 0.0733, -0.5302, -1.2134, -2.3277, -2.2476, -2.2033, -2.6359, -2.4815, 3.6783, 3.2438, 3.0212, 2.2222, 1.2324, 0.866, 0.8897, 1.9806, 3.5343, 7.3809, 6.1924, 5.6148, 4.36, 3.2597, 3.4124, 3.6912, 4.9116, 7.293],
[-15.3807, -9.547, -5.6385, -2.7198, 0.3667, 1.2712, 3.7046, 7.5375, 11.9076, -14.3835, -9.3588, -5.4699, -2.1068, 0.2668, 1.3, 3.6663, 7.3207, 11.2977, -13.3682, -8.5575, -5.3806, -2.8022, -0.596, 1.7074, 3.7623, 6.9464, 11.3573, -12.8118, -7.681, -3.9572, -1.8567, -0.4622, 1.8174, 4.2485, 7.2255, 11.6092, -2.0556, -2.1705, -3.1714, -4.5023, -5.3052, -5.0836, -6.8832, -9.2752, -9.6729, 0.1224, -0.0038, -0.6138, -1.1103, -2.2223, -2.0457, -2.0964, -2.3886, -2.25, 3.1855, 2.3552, 1.9327, 1.8585, 1.0377, 0.9324, 0.3404, 2.0216, 3.4596, 6.7083, 5.5722, 5.0971, 4.0614, 2.7353, 3.202, 3.2282, 4.6976, 7.0306],
[-15.1269, -9.2602, -4.8847, -1.9134, 0.1517, 1.22, 3.3383, 6.2409, 9.9093, -14.2483, -8.5918, -5.9791, -2.4631, 0.1506, 1.4036, 3.3163, 6.8345, 10.4263, -12.5432, -8.3122, -4.6717, -1.6588, -0.2754, 1.6035, 3.3289, 6.1661, 10.0118, -10.2243, -6.1252, -2.4085, -1.03, -0.4505, 1.5105, 3.1609, 5.078, 8.7026, 0.6767, -0.588, -1.7998, -2.2906, -2.538, -3.838, -4.3576, -4.984, -5.1472, 2.6973, 1.2365, 0.0735, -0.8201, -1.4593, -1.5529, -1.737, -2.1769, -0.8607, 4.9449, 2.8125, 1.8728, 1.1885, 0.9814, 0.8047, 0.7415, 1.6766, 2.9222, 7.4128, 4.9936, 2.414, 2.034, 2.1622, 2.2212, 2.9253, 4.2288, 6.0952],
[-11.432, -6.866, -3.6629, -1.3354, 0.4108, 1.2526, 2.9292, 5.1757, 7.997, -9.6185, -5.9543, -3.5277, -1.0929, 0.2584, 1.3297, 3.4693, 5.9073, 7.8768, -7.566, -4.3339, -1.9719, -0.5755, -0.0247, 1.3154, 2.4622, 4.1499, 7.4091, -4.8663, -3.0386, -1.1625, 0.1457, 0.4282, 0.9995, 2.0062, 3.6663, 6.6829, 1.3361, -0.07, -1.4826, -2.3175, -2.715, -2.9147, -3.2634, -3.6212, -3.6381, 2.9656, 1.3194, -0.2417, -0.8205, -1.3849, -1.3941, -1.4941, -1.4243, -0.3279, 4.3962, 2.2943, 1.1153, 0.3646, -0.0856, 0.1666, 0.5734, 1.0733, 2.377, 5.7247, 3.2703, 1.8968, 1.3193, 1.1095, 1.0117, 2.2326, 3.7911, 5.0049],
[-9.3962, -5.8912, -2.9277, -0.6161, 0.3111, 1.3824, 3.0114, 4.8878, 6.6712, -7.7065, -5.1082, -2.6677, -0.8403, 0.273, 1.2294, 2.2054, 3.8527, 6.4464, -6.2048, -3.6022, -1.5562, -0.3046, 0.0781, 1.2043, 2.1113, 3.4418, 5.7944, -3.9055, -2.3074, -0.7694, 0.1462, 0.5048, 0.9856, 2.0536, 3.1241, 5.0993, 0.5922, -0.4934, -1.7862, -2.5312, -2.7154, -2.6464, -3.1237, -3.6976, -3.6621, 2.2744, 0.4883, -0.4365, -0.906, -1.3485, -1.4647, -1.3526, -1.0683, -0.7949, 2.9738, 1.8937, 0.8139, 0.0898, -0.2144, -0.0829, 0.5324, 1.3885, 1.9292, 5.1242, 2.9112, 1.5119, 1.002, 0.7999, 0.8354, 2.0545, 3.4038, 4.1181],
[-8.6912, -5.5148, -2.7249, -0.4102, 0.3229, 1.4307, 3.0824, 4.3148, 6.1542, -7.1515, -4.1817, -2.2937, -0.7381, 0.2825, 1.1974, 2.0818, 3.6122, 6.0115, -5.844, -3.3192, -1.3964, -0.2047, 0.1768, 1.1877, 2.0108, 3.2171, 5.417, -3.7958, -2.0458, -0.5605, 0.204, 0.449, 0.9194, 1.9677, 2.673, 4.6974, 0.4717, -0.2172, -1.8133, -2.7256, -2.7497, -2.6207, -3.0815, -3.5065, -3.5131, 2.0197, 0.2997, -0.5223, -0.8975, -1.3354, -1.451, -1.3938, -1.1867, -0.9383, 2.7474, 1.6831, 0.652, -0.0888, -0.3503, -0.1956, 0.4052, 1.1948, 1.6742, 4.8733, 2.894, 1.3558, 0.8266, 0.7952, 0.7376, 1.8872, 3.0391, 3.9201],
[-8.3456, -5.286, -2.725, -0.5229, 0.2954, 1.403, 2.8648, 4.0807, 5.953, -6.86, -4.6918, -2.1943, -0.6964, 0.2944, 1.2128, 2.024, 3.5908, 5.8189, -5.5993, -3.237, -1.2876, -0.1584, 0.1978, 1.172, 1.9539, 3.0872, 5.1495, -3.423, -1.9614, -0.5183, 0.1616, 0.4656, 0.8868, 1.9423, 2.6274, 4.5592, 0.4149, -0.253, -2.0946, -2.6516, -2.7148, -2.6, -2.9022, -3.4352, -3.4441, 1.9045, 0.2682, -0.5513, -0.9068, -1.3381, -1.4507, -1.3967, -1.1492, -0.963, 2.6039, 1.65, 0.5791, -0.1163, -0.3678, -0.226, 0.3208, 1.097, 1.531, 4.6495, 2.7621, 1.2561, 0.7342, 0.7046, 0.7264, 1.8248, 3.0637, 3.7199],
[-8.2414, -5.2183, -3.0062, -0.6145, 0.3232, 1.3892, 2.8337, 4.1345, 5.8853, -6.7701, -4.6179, -2.1838, -0.6828, 0.2957, 1.2034, 2.0053, 3.5625, 5.7973, -5.4147, -3.1904, -1.2645, -0.1475, 0.2181, 1.1653, 1.9392, 3.0609, 5.1002, -3.3677, -1.9065, -0.5028, 0.1283, 0.4288, 0.8951, 1.9305, 2.635, 4.5207, 0.4111, -0.2548, -1.8219, -2.6186, -2.7185, -2.5781, -2.9195, -3.4176, -3.4298, 1.8807, 0.2633, -0.5493, -0.9076, -1.3377, -1.4506, -1.3988, -1.157, -0.9411, 2.5698, 1.6355, 0.5643, -0.1371, -0.3574, -0.2721, 0.299, 1.0702, 1.4894, 4.5877, 2.7194, 1.2441, 0.712, 0.6691, 0.7105, 1.7857, 3.0251, 3.6382],
[-8.2352, -5.1981, -3.0194, -0.6133, 0.3209, 1.3892, 2.8907, 4.1327, 5.8822, -6.7651, -4.6302, -2.1756, -0.6837, 0.2963, 1.2033, 2.0047, 3.6148, 5.7941, -5.4106, -3.1877, -1.259, -0.1468, 0.2251, 1.1652, 1.9385, 3.0754, 5.1973, -3.3492, -1.9048, -0.5019, 0.1618, 0.4041, 0.8951, 1.9297, 2.6586, 4.518, 0.4092, -0.306, -1.7238, -2.4678, -2.7236, -2.5652, -2.8373, -3.416, -3.428, 1.8779, 0.2618, -0.5421, -0.888, -1.3377, -1.449, -1.3987, -1.1319, -1.041, 2.5166, 1.6252, 0.5668, -0.1325, -0.3651, -0.2742, 0.298, 1.0598, 1.3878, 4.5834, 2.7165, 1.2422, 0.6775, 0.6918, 0.7301, 1.8077, 3.0373, 3.7351],
[-8.148, -5.1449, -3.0085, -0.6217, 0.3259, 1.3831, 2.8789, 4.0634, 5.8102, -6.6905, -4.5729, -2.1552, -0.68, 0.2885, 1.1936, 1.9665, 3.589, 5.7441, -5.3491, -3.1519, -1.2421, -0.1473, 0.2202, 1.1489, 1.9166, 3.051, 5.1268, -3.4054, -1.8454, -0.5237, 0.1598, 0.4244, 0.8733, 1.9231, 2.6348, 4.4758, 0.4296, -0.2844, -1.7416, -2.3087, -2.7188, -2.5533, -2.8346, -3.3648, -3.3964, 1.9795, 0.3962, -0.5243, -0.8447, -1.3167, -1.4037, -1.3886, -1.1291, -1.0347, 2.5085, 1.6009, 0.5708, -0.1194, -0.3423, -0.2839, 0.2941, 1.0783, 1.3693, 4.7488, 2.6738, 1.2174, 0.6603, 0.6319, 0.7378, 1.7956, 2.9985, 3.6929],
[-7.8464, -4.9565, -3.0603, -0.6315, 0.3165, 1.2967, 2.5852, 3.8051, 5.586, -6.4444, -4.9313, -2.0966, -0.654, 0.2681, 1.1431, 1.8868, 3.428, 5.5617, -5.0978, -3.0223, -1.1623, -0.1995, 0.1771, 1.0997, 1.8343, 2.9796, 4.7431, -3.1841, -1.7924, -0.5761, 0.1317, 0.3984, 0.8423, 1.8692, 2.5287, 4.2999, 0.4277, -0.2584, -1.6281, -2.2851, -2.5809, -2.4149, -2.9602, -3.22, -3.2499, 1.9196, 0.4306, -0.4475, -0.7798, -1.2502, -1.3587, -1.319, -1.0848, -0.9774, 2.429, 1.4642, 0.5771, -0.0713, -0.2837, -0.2342, 0.3002, 1.1051, 1.3017, 4.5853, 2.564, 1.1629, 0.6681, 0.6598, 0.6768, 1.77, 2.9028, 3.5715],
[-4.3587, -2.995, -1.7972, -0.8793, 0.2456, 0.9591, 1.8499, 2.7136, 4.1629, -4.0536, -2.5804, -1.3427, -0.4631, 0.1694, 0.7868, 1.4947, 2.5937, 4.0416, -3.6128, -1.7966, -0.8723, -0.318, 0.1604, 0.7759, 1.3309, 2.3191, 3.455, -2.0043, -1.2334, -0.659, -0.1373, 0.1269, 0.6646, 1.3751, 1.9383, 2.5607, -0.4199, -1.3593, -1.5247, -1.7641, -1.9229, -1.689, -2.1978, -2.7941, -2.3269, 1.0741, 0.2338, -0.1654, -0.3243, -0.8341, -0.931, -0.818, -0.5336, -0.3096, 0.0949, 0.6995, 0.4555, 0.1698, -0.4198, -0.1837, 0.2871, 1.1934, 2.5799, 1.523, 1.3326, 0.8165, 0.7008, 1.0674, 0.8754, 1.3947, 2.249, 3.0888],
[-2.7493, -1.7814, -1.0262, -0.3813, 0.0381, 0.4063, 0.8568, 1.6684, 2.583, -2.4428, -1.6837, -0.9941, -0.401, 0.029, 0.4156, 0.7642, 1.4581, 2.3895, -2.0327, -1.2882, -0.7508, -0.3439, 0.0067, 0.3854, 0.7534, 1.2614, 2.1397, -1.8494, -1.0103, -0.5804, -0.2029, 0.0806, 0.3899, 0.7462, 1.1302, 1.7299, -0.2941, -0.5919, -0.5891, -0.6212, -0.8724, -0.8501, -1.0492, -1.0993, -1.0625, 0.1128, -0.0408, -0.139, -0.2153, -0.3379, -0.3205, -0.2802, -0.3804, -1.2691, 0.2768, 0.4234, 0.3843, 0.2087, 0.048, 0.1568, 0.2986, 0.4919, 0.7991, 0.9854, 0.7899, 0.6785, 0.5639, 0.695, 0.6501, 0.8085, 1.0653, 1.4993],
[-1.6225, -1.0725, -0.581, -0.2545, -0.0154, 0.3189, 0.6725, 1.229, 1.6745, -1.6252, -1.1905, -0.7617, -0.3427, 0.0203, 0.3431, 0.6835, 1.1027, 1.719, -1.1581, -1.0117, -0.6678, -0.327, -0.0215, 0.3201, 0.6498, 1.0198, 1.4824, -1.533, -0.9697, -0.5964, -0.2805, 0.0373, 0.3865, 0.6045, 0.9899, 1.4444, -0.3901, -0.4893, -0.5032, -0.6513, -0.6869, -0.7053, -0.8394, -0.8021, -0.8496, -0.0893, -0.1452, -0.173, -0.2057, -0.2482, -0.2352, -0.2197, -0.2023, -0.1705, 0.4475, 0.3238, 0.2758, 0.2045, 0.1417, 0.195, 0.2451, 0.3097, 0.3551, 0.7431, 0.6113, 0.6068, 0.616, 0.6393, 0.6263, 0.6554, 0.7659, 0.9173],
[-1.2717, -0.9412, -0.5987, -0.2549, -0.0026, 0.3015, 0.633, 1.0149, 1.2889, -1.2863, -0.9618, -0.6379, -0.3072, 0.0014, 0.3087, 0.6195, 0.9347, 1.2531, -1.2013, -0.9214, -0.6176, -0.3102, -0.0058, 0.3042, 0.6193, 0.9239, 1.3076, -1.2822, -0.936, -0.61, -0.2398, 0.0074, 0.3112, 0.6054, 0.915, 1.1585, -0.6014, -0.5784, -0.5524, -0.6035, -0.6282, -0.6204, -0.6633, -0.6105, -0.4492, -0.1899, -0.1988, -0.1931, -0.2076, -0.2108, -0.2112, -0.2046, -0.22, -0.3636, 0.2015, 0.2325, 0.2236, 0.1985, 0.1773, 0.2052, 0.2137, 0.2273, 0.3691, 0.5543, 0.5639, 0.6199, 0.5415, 0.6116, 0.5912, 0.6132, 0.6194, 0.4322],
];
```
### wild-type/trace.ts
```ts
export interface Ring {
pts: Float64Array;
hole: boolean;
}
export interface TracedGlyph {
name: string;
rings: Ring[];
cx: number;
cy: number;
x0: number;
x1: number;
}
export interface TracedWord {
word: string;
glyphs: TracedGlyph[];
baseline: number;
}
type Seg = [number, number, number, number];
function area(p: number[]): number {
let a = 0;
const n = p.length / 2;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
a += p[i * 2] * p[j * 2 + 1] - p[j * 2] * p[i * 2 + 1];
}
return a / 2;
}
function march(a: Float32Array, w: number, h: number, minArea: number): number[][] {
const iso = 0.5;
const segs: Seg[] = [];
const at = (x: number, y: number) => a[y * w + x];
const cross = (x0: number, y0: number, x1: number, y1: number): [number, number] => {
const v0 = at(x0, y0);
const v1 = at(x1, y1);
const t = v1 === v0 ? 0.5 : (iso - v0) / (v1 - v0);
return [x0 + (x1 - x0) * t, y0 + (y1 - y0) * t];
};
for (let y = 0; y < h - 1; y++) {
for (let x = 0; x < w - 1; x++) {
const tl = at(x, y) >= iso ? 1 : 0;
const tr = at(x + 1, y) >= iso ? 1 : 0;
const br = at(x + 1, y + 1) >= iso ? 1 : 0;
const bl = at(x, y + 1) >= iso ? 1 : 0;
const c = tl | (tr << 1) | (br << 2) | (bl << 3);
if (c === 0 || c === 15) continue;
const e = (k: number): [number, number] =>
k === 0
? cross(x, y, x + 1, y)
: k === 1
? cross(x + 1, y, x + 1, y + 1)
: k === 2
? cross(x, y + 1, x + 1, y + 1)
: cross(x, y, x, y + 1);
let pairs: number[][];
switch (c) {
case 1: case 14: pairs = [[3, 0]]; break;
case 2: case 13: pairs = [[0, 1]]; break;
case 3: case 12: pairs = [[3, 1]]; break;
case 4: case 11: pairs = [[1, 2]]; break;
case 6: case 9: pairs = [[0, 2]]; break;
case 7: case 8: pairs = [[3, 2]]; break;
case 5: case 10: {
const mid = (at(x, y) + at(x + 1, y) + at(x + 1, y + 1) + at(x, y + 1)) / 4;
const inside = mid >= iso;
pairs = (c === 5) === inside ? [[3, 0], [1, 2]] : [[0, 1], [2, 3]];
break;
}
default: pairs = [];
}
for (const [p, q] of pairs) {
const A = e(p);
const B = e(q);
segs.push([A[0], A[1], B[0], B[1]]);
}
}
}
const key = (x: number, y: number) => `${Math.round(x * 64)},${Math.round(y * 64)}`;
const touching = new Map<string, number[]>();
const add = (k: string, i: number) => (touching.get(k) ?? touching.set(k, []).get(k)!).push(i);
for (let i = 0; i < segs.length; i++) {
add(key(segs[i][0], segs[i][1]), i);
add(key(segs[i][2], segs[i][3]), i);
}
const used = new Uint8Array(segs.length);
const loops: number[][] = [];
for (let i = 0; i < segs.length; i++) {
if (used[i]) continue;
used[i] = 1;
const loop: number[] = [segs[i][0], segs[i][1]];
const startKey = key(segs[i][0], segs[i][1]);
let cx = segs[i][2];
let cy = segs[i][3];
for (let guard = 0; guard < segs.length; guard++) {
loop.push(cx, cy);
const k = key(cx, cy);
if (k === startKey) break;
const cand = touching.get(k);
let next = -1;
if (cand) for (const j of cand) if (!used[j]) { next = j; break; }
if (next < 0) break;
used[next] = 1;
const sg = segs[next];
if (key(sg[0], sg[1]) === k) { cx = sg[2]; cy = sg[3]; }
else { cx = sg[0]; cy = sg[1]; }
}
if (loop.length >= 4 && key(loop[loop.length - 2], loop[loop.length - 1]) === startKey) loop.length -= 2;
if (loop.length >= 6 && Math.abs(area(loop)) >= minArea) loops.push(loop);
}
return loops;
}
function simplify(pts: number[], tol: number): number[] {
const n = pts.length / 2;
if (n < 4) return pts;
const keep = new Uint8Array(n);
keep[0] = 1;
keep[n - 1] = 1;
const stack: [number, number][] = [[0, n - 1]];
while (stack.length) {
const [a, b] = stack.pop()!;
const ax = pts[a * 2], ay = pts[a * 2 + 1], bx = pts[b * 2], by = pts[b * 2 + 1];
const dx = bx - ax, dy = by - ay;
const len = Math.hypot(dx, dy) || 1e-9;
let best = -1, bd = tol;
for (let i = a + 1; i < b; i++) {
const px = pts[i * 2] - ax, py = pts[i * 2 + 1] - ay;
const d = Math.abs(px * dy - py * dx) / len;
if (d > bd) { bd = d; best = i; }
}
if (best > 0) { keep[best] = 1; stack.push([a, best], [best, b]); }
}
const out: number[] = [];
for (let i = 0; i < n; i++) if (keep[i]) out.push(pts[i * 2], pts[i * 2 + 1]);
return out;
}
function inside(px: number, py: number, ring: number[]): boolean {
let hit = false;
const n = ring.length / 2;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = ring[i * 2], yi = ring[i * 2 + 1], xj = ring[j * 2], yj = ring[j * 2 + 1];
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) hit = !hit;
}
return hit;
}
export function traceWord(
word: string,
family: string,
weight: number,
oblique: number,
em: number,
tol: number,
minArea = 0,
): TracedWord | null {
const measure = document.createElement("canvas").getContext("2d");
if (!measure) return null;
const font = `${weight} ${em}px ${family}`;
measure.font = font;
const advances: number[] = [];
let total = 0;
for (const ch of word) {
advances.push(total);
total += measure.measureText(ch).width;
}
const pad = Math.ceil(em * 0.5);
const W = Math.ceil(total + pad * 2 + em * oblique);
const H = Math.ceil(em * 1.7);
const baselineY = Math.round(em * 1.15);
const c = document.createElement("canvas");
c.width = W;
c.height = H;
const ctx = c.getContext("2d", { willReadFrequently: true });
if (!ctx) return null;
const raw: { name: string; rings: number[][] }[] = [];
let minx = Infinity, maxx = -Infinity, miny = Infinity, maxy = -Infinity;
const chars = [...word];
for (let k = 0; k < chars.length; k++) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, W, H);
ctx.setTransform(1, 0, -oblique, 1, oblique * baselineY, 0);
ctx.font = font;
ctx.fillStyle = "#000";
ctx.textBaseline = "alphabetic";
ctx.fillText(chars[k], pad + advances[k], baselineY);
const img = ctx.getImageData(0, 0, W, H).data;
const a = new Float32Array(W * H);
for (let i = 0; i < W * H; i++) a[i] = img[i * 4 + 3] / 255;
const loops = march(a, W, H, minArea).map((l) => simplify(l, tol));
for (const l of loops) {
for (let i = 0; i < l.length; i += 2) {
if (l[i] < minx) minx = l[i];
if (l[i] > maxx) maxx = l[i];
if (l[i + 1] < miny) miny = l[i + 1];
if (l[i + 1] > maxy) maxy = l[i + 1];
}
}
raw.push({ name: chars[k], rings: loops });
}
if (!isFinite(minx)) return null;
const cx0 = (minx + maxx) / 2;
const cy0 = (miny + maxy) / 2;
const half = (maxx - minx) / 2 || 1;
const nx = (x: number) => (x - cx0) / half;
const ny = (y: number) => (y - cy0) / half;
const glyphs: TracedGlyph[] = raw.map((g) => {
const rings: Ring[] = g.rings.map((r, i) => {
const hole = g.rings.some((o, j) => j !== i && inside(r[0], r[1], o));
const pts = new Float64Array(r.length);
for (let p = 0; p < r.length; p += 2) {
pts[p] = nx(r[p]);
pts[p + 1] = ny(r[p + 1]);
}
return { pts, hole };
});
let sx = 0, sy = 0, n = 0, x0 = Infinity, x1 = -Infinity;
for (const r of rings) {
if (r.hole) continue;
for (let p = 0; p < r.pts.length; p += 2) {
sx += r.pts[p];
sy += r.pts[p + 1];
n++;
if (r.pts[p] < x0) x0 = r.pts[p];
if (r.pts[p] > x1) x1 = r.pts[p];
}
}
return { name: g.name, rings, cx: n ? sx / n : 0, cy: n ? sy / n : 0, x0, x1 };
});
return { word, glyphs, baseline: ny(baselineY) };
}
```
### wild-type/engine.ts
```ts
import { MESH, TYPE_POSE, TYPE_START } from "./motion";
import { traceWord, type TracedGlyph, type TracedWord } from "./trace";
import {
ACCENTS, AFTER_K, AFTER_MAX, BG, BG_GLOW, BLANK_TICKS, CORNER_DEG,
FONT_WEIGHT, FPS, GRAIN_ALPHA, GRAIN_TILE, HOLD_TICKS, INK, LAG_TICKS,
LOOP_TICKS, MESH_XS, MESH_YS, MIRROR_ALTERNATE, OBLIQUE, OUT_SPEED,
PULL_DAMP, PULL_GAIN, PULL_K, PULL_SIGMA, REF_CUT_TICK,
REF_HALF_WIDTH, REST_WIDTH_FRAC, THIN, THIN_MAX, TRACE_EM, TRACE_MIN_AREA,
TRACE_TOL, TYPE_TICKS, WARP_TICKS, WORDS,
} from "./params";
const GX = MESH_XS.length;
const GY = MESH_YS.length;
const NCP = GX * GY;
const CORNER_COS = Math.cos((CORNER_DEG * Math.PI) / 180);
function crTaps(t: number, knots: number[]): { idx: number[]; w: number[] } {
const n = knots.length;
const h = knots[1] - knots[0];
const j = Math.min(n - 2, Math.max(0, Math.floor((t - knots[0]) / h)));
const u = (t - knots[j]) / h;
const u2 = u * u;
const u3 = u2 * u;
return {
idx: [j - 1, j, j + 1, j + 2],
w: [
0.5 * (-u3 + 2 * u2 - u),
0.5 * (3 * u3 - 5 * u2 + 2),
0.5 * (-3 * u3 + 4 * u2 + u),
0.5 * (u3 - u2),
],
};
}
function foldTap(i: number, n: number): { idx: number[]; w: number[] } {
if (i < 0) return { idx: [0, 1], w: [2, -1] };
if (i > n - 1) return { idx: [n - 1, n - 2], w: [2, -1] };
return { idx: [i], w: [1] };
}
interface PreRing {
hole: boolean;
rest: Float64Array;
basis: Float64Array;
restPerimeter: number;
corner: Uint8Array;
}
interface PreGlyph {
glyph: TracedGlyph;
rings: PreRing[];
track: string;
}
interface Prepared {
word: TracedWord;
glyphs: PreGlyph[];
}
const TRACKS = ["w", "i", "l", "d"];
function trackFor(k: number, n: number): string {
if (n <= 1) return "w";
return TRACKS[Math.min(3, Math.round((k / (n - 1)) * 3))];
}
function perimeter(p: ArrayLike<number>): number {
let s = 0;
const n = p.length / 2;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
s += Math.hypot(p[j * 2] - p[i * 2], p[j * 2 + 1] - p[i * 2 + 1]);
}
return s;
}
function corners(p: ArrayLike<number>): Uint8Array {
const n = p.length / 2;
const out = new Uint8Array(n);
for (let i = 0; i < n; i++) {
const a = (i - 1 + n) % n;
const b = (i + 1) % n;
const ux = p[i * 2] - p[a * 2], uy = p[i * 2 + 1] - p[a * 2 + 1];
const vx = p[b * 2] - p[i * 2], vy = p[b * 2 + 1] - p[i * 2 + 1];
const lu = Math.hypot(ux, uy) || 1e-9;
const lv = Math.hypot(vx, vy) || 1e-9;
const cos = (ux * vx + uy * vy) / (lu * lv);
if (cos < CORNER_COS) out[i] = 1;
}
return out;
}
export class WildType {
private ctx: CanvasRenderingContext2D | null;
private raf = 0;
private t0 = 0;
private running = false;
private dpr = 1;
private cw = 0;
private ch = 0;
private lastSig = "";
private family: string;
private prepared = new Map<string, Prepared>();
private wordIdx = 0;
private loopNo = 0;
private lastLoopTick = 0;
private ptr = { x: 0, y: 0, active: false };
private pullOff = new Float64Array(NCP * 2);
private pullVel = new Float64Array(NCP * 2);
private restCtrl = new Float64Array(NCP * 2);
private scratchCtrl = new Float64Array(NCP * 2);
private lagCtrl = new Float64Array(NCP * 2);
private inkA: HTMLCanvasElement | null = null;
private inkB: HTMLCanvasElement | null = null;
private grain: CanvasPattern | null = null;
private warped: Float64Array = new Float64Array(0);
readonly ok: boolean;
constructor(private canvas: HTMLCanvasElement, family?: string) {
this.ctx = canvas.getContext("2d");
this.ok = !!this.ctx;
this.family = family ?? "sans-serif";
for (let r = 0; r < GY; r++)
for (let c = 0; c < GX; c++) {
this.restCtrl[r * GX + c] = MESH_XS[c];
this.restCtrl[NCP + r * GX + c] = MESH_YS[r];
}
if (this.ok) {
this.buildGrain();
this.resize();
}
}
private buildGrain() {
const g = document.createElement("canvas");
g.width = GRAIN_TILE;
g.height = GRAIN_TILE;
const gc = g.getContext("2d");
if (!gc) return;
const img = gc.createImageData(GRAIN_TILE, GRAIN_TILE);
for (let i = 0; i < img.data.length; i += 4) {
const v = Math.random() * 255;
img.data[i] = img.data[i + 1] = img.data[i + 2] = v;
img.data[i + 3] = 255;
}
gc.putImageData(img, 0, 0);
this.grain = this.ctx?.createPattern(g, "repeat") ?? null;
}
private prepare(word: string): Prepared | null {
const hit = this.prepared.get(word);
if (hit) return hit;
const traced = traceWord(word, this.family, FONT_WEIGHT, OBLIQUE, TRACE_EM, TRACE_TOL, TRACE_MIN_AREA);
if (!traced) return null;
const glyphs: PreGlyph[] = traced.glyphs.map((glyph, k) => ({
glyph,
track: trackFor(k, traced.glyphs.length),
rings: glyph.rings.map((r) => {
const n = r.pts.length / 2;
const basis = new Float64Array(n * NCP);
for (let i = 0; i < n; i++) {
const tx = crTaps(r.pts[i * 2], MESH_XS);
const ty = crTaps(r.pts[i * 2 + 1], MESH_YS);
for (let a = 0; a < 4; a++) {
const fx = foldTap(tx.idx[a], GX);
for (let b = 0; b < 4; b++) {
const fy = foldTap(ty.idx[b], GY);
const w = tx.w[a] * ty.w[b];
for (let p = 0; p < fx.idx.length; p++)
for (let q = 0; q < fy.idx.length; q++)
basis[i * NCP + fy.idx[q] * GX + fx.idx[p]] += w * fx.w[p] * fy.w[q];
}
}
}
return {
hole: r.hole,
rest: r.pts,
basis,
restPerimeter: perimeter(r.pts),
corner: corners(r.pts),
};
}),
}));
const out = { word: traced, glyphs };
this.prepared.set(word, out);
return out;
}
resize() {
const c = this.canvas;
const r = c.getBoundingClientRect();
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
this.cw = r.width;
this.ch = r.height;
c.width = Math.round(r.width * this.dpr);
c.height = Math.round(r.height * this.dpr);
for (const key of ["inkA", "inkB"] as const) {
const l = this[key] ?? document.createElement("canvas");
l.width = c.width;
l.height = c.height;
this[key] = l;
}
this.lastSig = "";
if (!this.running) this.renderStill();
}
setPointer(x: number | null, y: number | null) {
if (x == null || y == null) {
this.ptr.active = false;
return;
}
const half = (this.cw * REST_WIDTH_FRAC) / 2;
this.ptr.x = (x - this.cw / 2) / half;
this.ptr.y = (y - this.ch / 2) / half;
this.ptr.active = true;
}
start() {
if (this.running || !this.ok) return;
this.running = true;
this.t0 = performance.now();
this.lastSig = "";
const tick = (now: number) => {
if (!this.running) return;
const tau = (((now - this.t0) / 1000) * FPS) % LOOP_TICKS;
if (tau < this.lastLoopTick) {
this.loopNo++;
this.wordIdx = (this.wordIdx + 1) % WORDS.length;
}
this.lastLoopTick = tau;
this.draw(tau);
this.raf = requestAnimationFrame(tick);
};
this.raf = requestAnimationFrame(tick);
}
stop() {
this.running = false;
cancelAnimationFrame(this.raf);
}
destroy() {
this.stop();
this.ctx = null;
}
renderStill() {
this.lastSig = "";
this.wordIdx = 0;
this.draw(BLANK_TICKS + TYPE_TICKS + WARP_TICKS);
}
private sampleMesh(u: number, mirror: boolean, out: Float64Array) {
const last = MESH.length - 1;
const uu = Math.min(Math.max(u, 0), last);
const j = Math.min(last - 1, Math.floor(uu));
const f = uu - j;
const r0 = MESH[Math.max(0, j - 1)];
const r1 = MESH[j];
const r2 = MESH[Math.min(last, j + 1)];
const r3 = MESH[Math.min(last, j + 2)];
const f2 = f * f;
const f3 = f2 * f;
const w0 = 0.5 * (-f3 + 2 * f2 - f);
const w1 = 0.5 * (3 * f3 - 5 * f2 + 2);
const w2 = 0.5 * (-3 * f3 + 4 * f2 + f);
const w3 = 0.5 * (f3 - f2);
for (let i = 0; i < NCP * 2; i++) out[i] = w0 * r0[i] + w1 * r1[i] + w2 * r2[i] + w3 * r3[i];
if (mirror) {
const tmp = Float64Array.from(out);
for (let r = 0; r < GY; r++)
for (let c = 0; c < GX; c++) {
const src = r * GX + (GX - 1 - c);
const dst = r * GX + c;
out[dst] = -tmp[src];
out[NCP + dst] = tmp[NCP + src];
}
}
}
private stepPull(active: boolean): boolean {
let moving = false;
for (let j = 0; j < NCP; j++) {
const rx = this.restCtrl[j];
const ry = this.restCtrl[NCP + j];
let tx = 0;
let ty = 0;
if (active) {
const dx = this.ptr.x - rx;
const dy = this.ptr.y - ry;
const g = Math.exp(-(dx * dx + dy * dy) / (2 * PULL_SIGMA * PULL_SIGMA));
tx = dx * g * PULL_GAIN;
ty = dy * g * PULL_GAIN;
}
for (const [k, t] of [[j, tx], [NCP + j, ty]] as const) {
this.pullVel[k] += (t - this.pullOff[k]) * PULL_K;
this.pullVel[k] *= PULL_DAMP;
this.pullOff[k] += this.pullVel[k];
if (Math.abs(this.pullVel[k]) > 1e-4 || Math.abs(this.pullOff[k]) > 1e-4) moving = true;
}
}
return moving;
}
private draw(tau: number) {
const ctx = this.ctx;
if (!ctx || !this.inkA || !this.inkB) return;
const t = Math.floor(tau);
const typeEnd = BLANK_TICKS + TYPE_TICKS;
const warpEnd = typeEnd + WARP_TICKS;
const holdEnd = warpEnd + HOLD_TICKS;
const inWarp = tau >= typeEnd && tau < warpEnd;
const typingIn = t >= BLANK_TICKS && t < typeEnd;
const typingOut = t >= holdEnd;
const mirror = MIRROR_ALTERNATE && this.loopNo % 2 === 1;
const settling = this.stepPull(inWarp && this.ptr.active);
let sig: string;
if (t < BLANK_TICKS) sig = `b${t}`;
else if (typingIn) sig = `y${t}`;
else if (inWarp) sig = "warp";
else if (typingOut) sig = `o${t}`;
else sig = settling ? "settle" : "rest";
if (sig !== "warp" && sig !== "settle" && sig === this.lastSig) return;
this.lastSig = sig;
const word = WORDS[this.wordIdx];
const accent = ACCENTS[this.wordIdx % ACCENTS.length];
const prep = this.prepare(word);
const W = this.canvas.width;
const H = this.canvas.height;
const half = ((this.cw * REST_WIDTH_FRAC) / 2) * this.dpr;
const ox = W / 2;
const oy = H / 2;
let stretch = 0;
let vel = 0;
let ctrl: Float64Array | null = null;
let lag: Float64Array | null = null;
if (inWarp) {
const u = tau - typeEnd;
this.sampleMesh(u, mirror, this.scratchCtrl);
this.sampleMesh(u - LAG_TICKS, mirror, this.lagCtrl);
ctrl = this.scratchCtrl;
lag = this.lagCtrl;
for (let j = 0; j < NCP; j++) {
ctrl[j] += this.pullOff[j];
ctrl[NCP + j] += this.pullOff[NCP + j];
lag[j] += this.pullOff[j];
lag[NCP + j] += this.pullOff[NCP + j];
stretch += Math.hypot(ctrl[j] - this.restCtrl[j], ctrl[NCP + j] - this.restCtrl[NCP + j]);
vel += Math.hypot(ctrl[j] - lag[j], ctrl[NCP + j] - lag[NCP + j]) / LAG_TICKS;
}
stretch /= NCP;
vel /= NCP;
} else if (settling) {
ctrl = this.scratchCtrl;
for (let j = 0; j < NCP * 2; j++) ctrl[j] = this.restCtrl[j] + this.pullOff[j];
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
ctx.fillStyle = mix(BG, accent, Math.min(1, stretch / 2.2) * BG_GLOW);
ctx.fillRect(0, 0, W, H);
if (!prep || t < BLANK_TICKS) {
this.finish(ctx, W, H);
return;
}
const cur = this.inkA;
const prev = this.inkB;
const ic = cur.getContext("2d");
if (!ic) return;
ic.setTransform(1, 0, 0, 1, 0, 0);
ic.clearRect(0, 0, W, H);
ic.globalAlpha = 1;
const poseFor = (track: string): number[] | null => {
const tr = TYPE_POSE[track];
if (!tr) return null;
if (typingIn) return tr[t + REF_CUT_TICK - TYPE_START] ?? null;
if (typingOut) {
const e = t - holdEnd;
const idx = TYPE_TICKS - 1 - Math.floor(e * OUT_SPEED);
return idx >= 0 ? (tr[idx] ?? null) : null;
}
return null;
};
const typing = typingIn || typingOut;
if (lag) {
ic.fillStyle = accent;
for (const g of prep.glyphs) {
const p = new Path2D();
for (const r of g.rings) this.addRing(p, r, lag, null, half, ox, oy, stretch);
ic.fill(p, "evenodd");
}
}
ic.fillStyle = INK;
for (const g of prep.glyphs) {
let pose: number[] | null = null;
if (typing) {
pose = poseFor(g.track);
if (!pose) continue;
}
const p = new Path2D();
let s = 0;
for (const r of g.rings) s += this.addRing(p, r, ctrl, pose, half, ox, oy, stretch, g.glyph);
const st = g.rings.length ? s / g.rings.length : 1;
ic.globalAlpha = 1 - Math.min(THIN_MAX, Math.max(0, (st - 1) * THIN));
ic.fill(p, "evenodd");
}
ic.globalAlpha = 1;
if (inWarp) {
const a = Math.min(AFTER_MAX, vel * AFTER_K);
if (a > 0.02) {
ctx.globalAlpha = a;
ctx.drawImage(prev, 0, 0);
}
}
ctx.globalAlpha = 1;
ctx.drawImage(cur, 0, 0);
this.inkA = prev;
this.inkB = cur;
this.finish(ctx, W, H);
}
private finish(ctx: CanvasRenderingContext2D, W: number, H: number) {
if (!this.grain) return;
ctx.globalCompositeOperation = "screen";
ctx.globalAlpha = GRAIN_ALPHA;
ctx.fillStyle = this.grain;
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
private addRing(
path: Path2D,
ring: PreRing,
ctrl: Float64Array | null,
pose: number[] | null,
half: number,
ox: number,
oy: number,
stretch: number,
glyph?: TracedGlyph,
): number {
const { rest, basis, corner } = ring;
const n = rest.length / 2;
if (this.warped.length < n * 2) this.warped = new Float64Array(n * 2);
const out = this.warped;
let dx = 0, dy = 0, co = 1, si = 0, sx = 1, sh = 0, cx = 0, cy = 0;
if (pose && glyph) {
dx = pose[0] / REF_HALF_WIDTH;
dy = pose[1] / REF_HALF_WIDTH;
co = Math.cos(pose[2]);
si = Math.sin(pose[2]);
sx = pose[3];
sh = pose[4];
cx = glyph.cx;
cy = glyph.cy;
}
for (let i = 0; i < n; i++) {
let x: number;
let y: number;
if (ctrl) {
const o = i * NCP;
let ax = 0, ay = 0;
for (let j = 0; j < NCP; j++) {
const b = basis[o + j];
if (b === 0) continue;
ax += ctrl[j] * b;
ay += ctrl[NCP + j] * b;
}
x = ax;
y = ay;
} else if (pose) {
const u = (rest[i * 2] - cx) * sx + (rest[i * 2 + 1] - cy) * sh;
const v = rest[i * 2 + 1] - cy;
x = co * u - si * v + cx + dx;
y = si * u + co * v + cy + dy;
} else {
x = rest[i * 2];
y = rest[i * 2 + 1];
}
out[i * 2] = ox + x * half;
out[i * 2 + 1] = oy + y * half;
}
if (ctrl && stretch > 0.35) {
for (let i = 0; i < n; i++) {
const p0 = (i - 1 + n) % n, p1 = i, p2 = (i + 1) % n, p3 = (i + 2) % n;
const c1 = corner[p1], c2 = corner[p2];
const b1x = c1 ? out[p1 * 2] : out[p1 * 2] + (out[p2 * 2] - out[p0 * 2]) / 6;
const b1y = c1 ? out[p1 * 2 + 1] : out[p1 * 2 + 1] + (out[p2 * 2 + 1] - out[p0 * 2 + 1]) / 6;
const b2x = c2 ? out[p2 * 2] : out[p2 * 2] - (out[p3 * 2] - out[p1 * 2]) / 6;
const b2y = c2 ? out[p2 * 2 + 1] : out[p2 * 2 + 1] - (out[p3 * 2 + 1] - out[p1 * 2 + 1]) / 6;
if (i === 0) path.moveTo(out[0], out[1]);
path.bezierCurveTo(b1x, b1y, b2x, b2y, out[p2 * 2], out[p2 * 2 + 1]);
}
} else {
for (let i = 0; i < n; i++) {
if (i === 0) path.moveTo(out[0], out[1]);
else path.lineTo(out[i * 2], out[i * 2 + 1]);
}
path.closePath();
}
return ctrl ? perimeter(out.subarray(0, n * 2)) / (ring.restPerimeter * half) : 1;
}
}
function mix(a: string, b: string, t: number): string {
if (t <= 0) return a;
const pa = hex(a), pb = hex(b);
const c = pa.map((v, i) => Math.round(v + (pb[i] - v) * t));
return `rgb(${c[0]},${c[1]},${c[2]})`;
}
function hex(h: string): number[] {
const s = h.replace("#", "");
return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
}
```
### wild-type/WildTypeCard.tsx
```ts
"use client";
import { useEffect, useRef } from "react";
import { WildType } from "./engine";
import { BG, FONT_CSS, FONT_WEIGHT } from "./params";
import { onTransitionChange } from "../../lib/view-transition";
export function WildTypeCard({
bare = false,
viewTransitionName,
}: {
bare?: boolean;
viewTransitionName?: string;
} = {}) {
void bare;
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let engine: WildType | null = null;
let onScreen = false;
let hidden = false;
let inTransition = false;
let started = false;
const sync = () => {
if (!engine || reduced) return;
if (onScreen && !hidden && !inTransition) engine.start();
else engine.stop();
};
const resolveFamily = () => {
const probe = document.createElement("span");
probe.style.cssText = "position:absolute;visibility:hidden";
probe.style.fontFamily = FONT_CSS;
probe.textContent = "Ag";
document.body.appendChild(probe);
const fam = getComputedStyle(probe)
.fontFamily.split(",")[0]
.replace(/["']/g, "")
.trim();
probe.remove();
return fam;
};
const startEngine = (family?: string) => {
if (started || !canvasRef.current) return;
started = true;
engine = new WildType(canvas, family ? `"${family}", sans-serif` : undefined);
if (!engine.ok) return;
if (reduced) engine.renderStill();
else sync();
};
const hasFontApi = "fonts" in document && !!document.fonts;
const raf = requestAnimationFrame(() => {
if (!canvasRef.current) return;
const fam = hasFontApi ? resolveFamily() : "";
if (hasFontApi && fam) {
const to = window.setTimeout(() => startEngine(fam), 350);
const go = () => {
window.clearTimeout(to);
startEngine(fam);
};
document.fonts.load(`${FONT_WEIGHT} 1em "${fam}"`).then(go, go);
} else {
startEngine();
}
});
const io = new IntersectionObserver(
(es) => {
onScreen = es[0]?.isIntersecting ?? false;
sync();
},
{ threshold: 0.2 },
);
io.observe(canvas);
const onVis = () => {
hidden = document.hidden;
sync();
};
document.addEventListener("visibilitychange", onVis);
const offTransition = onTransitionChange((active) => {
inTransition = active;
sync();
});
const onMove = (e: PointerEvent) => {
const r = canvas.getBoundingClientRect();
engine?.setPointer(e.clientX - r.left, e.clientY - r.top);
};
const onLeave = () => engine?.setPointer(null, null);
canvas.addEventListener("pointermove", onMove);
canvas.addEventListener("pointerleave", onLeave);
canvas.addEventListener("pointercancel", onLeave);
let rt = 0;
const onResize = () => {
window.clearTimeout(rt);
rt = window.setTimeout(() => engine?.resize(), 120);
};
window.addEventListener("resize", onResize);
return () => {
cancelAnimationFrame(raf);
io.disconnect();
document.removeEventListener("visibilitychange", onVis);
offTransition();
canvas.removeEventListener("pointermove", onMove);
canvas.removeEventListener("pointerleave", onLeave);
canvas.removeEventListener("pointercancel", onLeave);
window.removeEventListener("resize", onResize);
window.clearTimeout(rt);
engine?.destroy();
};
}, []);
return (
<div
data-canvas-card
role="img"
aria-label="A single design word in heavy white oblique lowercase on black. It types itself in letter by letter, then inflates to fill the card as giant warped letterforms with a colored ink slipping out from under the white, swings over an arch, shrinks back to rest, and un-types itself letter by letter. Each loop brings the next word in its own color."
style={{ background: BG, ...(viewTransitionName ? { viewTransitionName } : null) }}
className="relative mx-auto aspect-[1344/620] w-full select-none overflow-hidden rounded-[12px] border border-[var(--border-line)]"
>
<canvas ref={canvasRef} className="h-full w-full" />
</div>
);
}
```Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Vault