@shotstack/shotstack-canvas 1.0.2 → 1.0.4
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/entry.node.cjs +1548 -0
- package/dist/entry.node.cjs.map +1 -0
- package/dist/entry.node.d.cts +229 -0
- package/dist/entry.node.d.ts +229 -0
- package/dist/entry.node.js +1511 -0
- package/dist/entry.node.js.map +1 -0
- package/dist/entry.web.d.ts +207 -0
- package/dist/entry.web.js +1285 -0
- package/dist/entry.web.js.map +1 -0
- package/package.json +20 -7
- package/.eslintrc.cjs +0 -7
- package/.prettierrc +0 -1
- package/pnpm-workspace.yaml +0 -3
- package/scripts/vendor-harfbuzz.js +0 -64
- package/src/config/canvas-constants.ts +0 -30
- package/src/core/animations.ts +0 -570
- package/src/core/colors.ts +0 -11
- package/src/core/decoration.ts +0 -9
- package/src/core/drawops.ts +0 -206
- package/src/core/font-registry.ts +0 -77
- package/src/core/gradients.ts +0 -12
- package/src/core/layout.ts +0 -184
- package/src/core/utils.ts +0 -3
- package/src/core/video-generator.ts +0 -157
- package/src/env/entry.node.ts +0 -167
- package/src/env/entry.web.ts +0 -146
- package/src/index.ts +0 -1
- package/src/io/node.ts +0 -45
- package/src/io/web.ts +0 -5
- package/src/painters/node.ts +0 -290
- package/src/painters/web.ts +0 -224
- package/src/schema/asset-schema.ts +0 -166
- package/src/types.ts +0 -36
- package/src/wasm/hb-loader.ts +0 -31
- package/tsconfig.base.json +0 -22
- package/tsconfig.json +0 -16
- package/tsup.config.ts +0 -52
package/src/core/animations.ts
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
import { DrawOp, Glyph, ShapedLine } from "../types";
|
|
2
|
-
|
|
3
|
-
export type AnimationInput = {
|
|
4
|
-
preset?: "typewriter" | "fadeIn" | "slideIn" | "shift" | "ascend" | "movingLetters";
|
|
5
|
-
speed: number;
|
|
6
|
-
duration?: number;
|
|
7
|
-
style?: "character" | "word";
|
|
8
|
-
direction?: "left" | "right" | "up" | "down";
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const DECORATION_DONE_THRESHOLD = 0.999;
|
|
12
|
-
|
|
13
|
-
export function applyAnimation(
|
|
14
|
-
ops: DrawOp[],
|
|
15
|
-
lines: ShapedLine[],
|
|
16
|
-
p: { t: number; fontSize: number; anim?: AnimationInput }
|
|
17
|
-
): DrawOp[] {
|
|
18
|
-
if (!p.anim || !p.anim.preset) return ops;
|
|
19
|
-
|
|
20
|
-
const { preset } = p.anim;
|
|
21
|
-
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
22
|
-
const duration = p.anim.duration ?? Math.max(0.3, totalGlyphs / 30 / p.anim.speed);
|
|
23
|
-
const progress = Math.max(0, Math.min(1, p.t / duration));
|
|
24
|
-
|
|
25
|
-
switch (preset) {
|
|
26
|
-
case "typewriter":
|
|
27
|
-
return applyTypewriterAnimation(
|
|
28
|
-
ops,
|
|
29
|
-
lines,
|
|
30
|
-
progress,
|
|
31
|
-
p.anim.style,
|
|
32
|
-
p.fontSize,
|
|
33
|
-
p.t,
|
|
34
|
-
duration
|
|
35
|
-
);
|
|
36
|
-
case "fadeIn":
|
|
37
|
-
return applyFadeInAnimation(ops, progress);
|
|
38
|
-
case "slideIn":
|
|
39
|
-
return applySlideInAnimation(ops, progress, p.anim.direction ?? "left", p.fontSize);
|
|
40
|
-
case "shift":
|
|
41
|
-
return applyShiftAnimation(
|
|
42
|
-
ops,
|
|
43
|
-
lines,
|
|
44
|
-
progress,
|
|
45
|
-
p.anim.direction ?? "left",
|
|
46
|
-
p.fontSize,
|
|
47
|
-
p.anim.style,
|
|
48
|
-
duration
|
|
49
|
-
);
|
|
50
|
-
case "ascend":
|
|
51
|
-
return applyAscendAnimation(
|
|
52
|
-
ops,
|
|
53
|
-
lines,
|
|
54
|
-
progress,
|
|
55
|
-
p.anim.direction ?? "up",
|
|
56
|
-
p.fontSize,
|
|
57
|
-
duration
|
|
58
|
-
);
|
|
59
|
-
case "movingLetters":
|
|
60
|
-
return applyMovingLettersAnimation(ops, progress, p.anim.direction ?? "up", p.fontSize);
|
|
61
|
-
default:
|
|
62
|
-
return ops;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---- helpers
|
|
67
|
-
const isShadowFill = (op: DrawOp) => op.op === "FillPath" && (op as any).isShadow === true;
|
|
68
|
-
const isGlyphFill = (op: DrawOp) => op.op === "FillPath" && !(op as any).isShadow === true;
|
|
69
|
-
|
|
70
|
-
// Try to derive a text color from the first fill we find
|
|
71
|
-
function getTextColorFromOps(ops: DrawOp[]): string {
|
|
72
|
-
for (const op of ops) {
|
|
73
|
-
if (op.op === "FillPath") {
|
|
74
|
-
const fill: any = (op as any).fill;
|
|
75
|
-
if (fill?.kind === "solid") return fill.color;
|
|
76
|
-
if (
|
|
77
|
-
(fill?.kind === "linear" || fill?.kind === "radial") &&
|
|
78
|
-
Array.isArray(fill.stops) &&
|
|
79
|
-
fill.stops.length
|
|
80
|
-
) {
|
|
81
|
-
return fill.stops[fill.stops.length - 1].color || "#000000";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return "#000000";
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---------- TYPEWRITER ----------
|
|
89
|
-
function applyTypewriterAnimation(
|
|
90
|
-
ops: DrawOp[],
|
|
91
|
-
lines: ShapedLine[],
|
|
92
|
-
progress: number,
|
|
93
|
-
style: "character" | "word" | undefined,
|
|
94
|
-
fontSize: number,
|
|
95
|
-
time: number,
|
|
96
|
-
duration: number
|
|
97
|
-
): DrawOp[] {
|
|
98
|
-
const byWord = style === "word";
|
|
99
|
-
|
|
100
|
-
if (byWord) {
|
|
101
|
-
const wordSegments = getWordSegments(lines);
|
|
102
|
-
const totalWords = wordSegments.length;
|
|
103
|
-
const visibleWords = Math.floor(progress * totalWords);
|
|
104
|
-
if (visibleWords === 0) return ops.filter((x) => x.op === "BeginFrame");
|
|
105
|
-
|
|
106
|
-
let totalVisibleGlyphs = 0;
|
|
107
|
-
for (let i = 0; i < Math.min(visibleWords, wordSegments.length); i++) {
|
|
108
|
-
totalVisibleGlyphs += wordSegments[i].glyphCount;
|
|
109
|
-
}
|
|
110
|
-
const visibleOpsRaw = sliceGlyphOps(ops, totalVisibleGlyphs);
|
|
111
|
-
|
|
112
|
-
const visibleOps =
|
|
113
|
-
progress >= DECORATION_DONE_THRESHOLD
|
|
114
|
-
? visibleOpsRaw
|
|
115
|
-
: visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
116
|
-
|
|
117
|
-
if (progress < 1 && totalVisibleGlyphs > 0) {
|
|
118
|
-
return addTypewriterCursor(visibleOps, totalVisibleGlyphs, fontSize, time);
|
|
119
|
-
}
|
|
120
|
-
return visibleOps;
|
|
121
|
-
} else {
|
|
122
|
-
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
123
|
-
const visibleGlyphs = Math.floor(progress * totalGlyphs);
|
|
124
|
-
if (visibleGlyphs === 0) return ops.filter((x) => x.op === "BeginFrame");
|
|
125
|
-
|
|
126
|
-
const visibleOpsRaw = sliceGlyphOps(ops, visibleGlyphs);
|
|
127
|
-
|
|
128
|
-
const visibleOps =
|
|
129
|
-
progress >= DECORATION_DONE_THRESHOLD
|
|
130
|
-
? visibleOpsRaw
|
|
131
|
-
: visibleOpsRaw.filter((o) => o.op !== "DecorationLine");
|
|
132
|
-
|
|
133
|
-
if (progress < 1 && visibleGlyphs > 0) {
|
|
134
|
-
return addTypewriterCursor(visibleOps, visibleGlyphs, fontSize, time);
|
|
135
|
-
}
|
|
136
|
-
return visibleOps;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---------- ASCEND ----------
|
|
141
|
-
function applyAscendAnimation(
|
|
142
|
-
ops: DrawOp[],
|
|
143
|
-
lines: ShapedLine[],
|
|
144
|
-
progress: number,
|
|
145
|
-
direction: "left" | "right" | "up" | "down",
|
|
146
|
-
fontSize: number,
|
|
147
|
-
duration: number
|
|
148
|
-
): DrawOp[] {
|
|
149
|
-
const wordSegments = getWordSegments(lines);
|
|
150
|
-
const totalWords = wordSegments.length;
|
|
151
|
-
if (totalWords === 0) return ops;
|
|
152
|
-
|
|
153
|
-
const result: DrawOp[] = [];
|
|
154
|
-
let glyphIndex = 0;
|
|
155
|
-
|
|
156
|
-
for (const op of ops) {
|
|
157
|
-
if (op.op === "BeginFrame") {
|
|
158
|
-
result.push(op);
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
for (const op of ops) {
|
|
164
|
-
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
165
|
-
// which word owns this glyph
|
|
166
|
-
let wordIndex = -1,
|
|
167
|
-
acc = 0;
|
|
168
|
-
for (let i = 0; i < wordSegments.length; i++) {
|
|
169
|
-
const gcount = wordSegments[i].glyphCount;
|
|
170
|
-
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
171
|
-
wordIndex = i;
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
acc += gcount;
|
|
175
|
-
}
|
|
176
|
-
if (wordIndex >= 0) {
|
|
177
|
-
const startF = (wordIndex / Math.max(1, totalWords)) * (duration / duration);
|
|
178
|
-
const endF = Math.min(1, startF + 0.3);
|
|
179
|
-
if (progress >= endF) {
|
|
180
|
-
result.push(op);
|
|
181
|
-
} else if (progress > startF) {
|
|
182
|
-
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
183
|
-
const ease = easeOutCubic(Math.min(1, local));
|
|
184
|
-
const startOffset = direction === "up" ? fontSize * 0.4 : -fontSize * 0.4;
|
|
185
|
-
const animated: any = { ...op, y: op.y + startOffset * (1 - ease) };
|
|
186
|
-
if (op.op === "FillPath") {
|
|
187
|
-
if (animated.fill.kind === "solid")
|
|
188
|
-
animated.fill = { ...animated.fill, opacity: animated.fill.opacity * ease };
|
|
189
|
-
else animated.fill = { ...animated.fill, opacity: (animated.fill.opacity ?? 1) * ease };
|
|
190
|
-
} else {
|
|
191
|
-
animated.opacity = animated.opacity * ease;
|
|
192
|
-
}
|
|
193
|
-
result.push(animated);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
if (isGlyphFill(op)) glyphIndex++;
|
|
197
|
-
} else if (op.op === "DecorationLine") {
|
|
198
|
-
if (progress >= DECORATION_DONE_THRESHOLD) {
|
|
199
|
-
result.push(op);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ---------- SHIFT ----------
|
|
207
|
-
function applyShiftAnimation(
|
|
208
|
-
ops: DrawOp[],
|
|
209
|
-
lines: ShapedLine[],
|
|
210
|
-
progress: number,
|
|
211
|
-
direction: "left" | "right" | "up" | "down",
|
|
212
|
-
fontSize: number,
|
|
213
|
-
style: "character" | "word" | undefined,
|
|
214
|
-
duration: number
|
|
215
|
-
): DrawOp[] {
|
|
216
|
-
const byWord = style === "word";
|
|
217
|
-
const startOffsets = {
|
|
218
|
-
left: { x: fontSize * 0.6, y: 0 },
|
|
219
|
-
right: { x: -fontSize * 0.6, y: 0 },
|
|
220
|
-
up: { x: 0, y: fontSize * 0.6 },
|
|
221
|
-
down: { x: 0, y: -fontSize * 0.6 },
|
|
222
|
-
};
|
|
223
|
-
const offset = startOffsets[direction];
|
|
224
|
-
|
|
225
|
-
const wordSegments = byWord ? getWordSegments(lines) : [];
|
|
226
|
-
const totalGlyphs = lines.reduce((s, l) => s + l.glyphs.length, 0);
|
|
227
|
-
const totalUnits = byWord ? wordSegments.length : totalGlyphs;
|
|
228
|
-
if (totalUnits === 0) return ops;
|
|
229
|
-
|
|
230
|
-
const result: DrawOp[] = [];
|
|
231
|
-
for (const op of ops) {
|
|
232
|
-
if (op.op === "BeginFrame") {
|
|
233
|
-
result.push(op);
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const windowDuration = 0.3; // each unit animates for 0.3s of total duration
|
|
239
|
-
const overlapFactor = 0.7; // overlapping reveal
|
|
240
|
-
const staggerDelay = (duration * overlapFactor) / Math.max(1, totalUnits - 1);
|
|
241
|
-
|
|
242
|
-
const windowFor = (unitIdx: number) => {
|
|
243
|
-
const startTime = unitIdx * staggerDelay;
|
|
244
|
-
const startF = startTime / duration;
|
|
245
|
-
const endF = Math.min(1, (startTime + windowDuration) / duration);
|
|
246
|
-
return { startF, endF };
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
let glyphIndex = 0;
|
|
250
|
-
for (const op of ops) {
|
|
251
|
-
if (op.op !== "FillPath" && op.op !== "StrokePath") {
|
|
252
|
-
if (op.op === "DecorationLine" && progress > 0.8) result.push(op);
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
let unitIndex: number;
|
|
257
|
-
if (!byWord) {
|
|
258
|
-
unitIndex = glyphIndex;
|
|
259
|
-
} else {
|
|
260
|
-
let wordIndex = -1,
|
|
261
|
-
acc = 0;
|
|
262
|
-
for (let i = 0; i < wordSegments.length; i++) {
|
|
263
|
-
const gcount = wordSegments[i].glyphCount;
|
|
264
|
-
if (glyphIndex >= acc && glyphIndex < acc + gcount) {
|
|
265
|
-
wordIndex = i;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
acc += gcount;
|
|
269
|
-
}
|
|
270
|
-
unitIndex = Math.max(0, wordIndex);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const { startF, endF } = windowFor(unitIndex);
|
|
274
|
-
|
|
275
|
-
if (progress <= startF) {
|
|
276
|
-
const animated: any = { ...op, x: op.x + offset.x, y: op.y + offset.y };
|
|
277
|
-
if (op.op === "FillPath") {
|
|
278
|
-
if (animated.fill.kind === "solid") animated.fill = { ...animated.fill, opacity: 0 };
|
|
279
|
-
else animated.fill = { ...animated.fill, opacity: 0 };
|
|
280
|
-
} else {
|
|
281
|
-
animated.opacity = 0;
|
|
282
|
-
}
|
|
283
|
-
result.push(animated);
|
|
284
|
-
} else if (progress >= endF) {
|
|
285
|
-
result.push(op);
|
|
286
|
-
} else {
|
|
287
|
-
const local = (progress - startF) / Math.max(1e-6, endF - startF);
|
|
288
|
-
const ease = easeOutCubic(Math.min(1, local));
|
|
289
|
-
const dx = offset.x * (1 - ease);
|
|
290
|
-
const dy = offset.y * (1 - ease);
|
|
291
|
-
const animated: any = { ...op, x: op.x + dx, y: op.y + dy };
|
|
292
|
-
|
|
293
|
-
if (op.op === "FillPath") {
|
|
294
|
-
const targetOpacity =
|
|
295
|
-
animated.fill.kind === "solid" ? animated.fill.opacity : animated.fill.opacity ?? 1;
|
|
296
|
-
if (animated.fill.kind === "solid")
|
|
297
|
-
animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
298
|
-
else animated.fill = { ...animated.fill, opacity: targetOpacity * ease };
|
|
299
|
-
} else {
|
|
300
|
-
animated.opacity = animated.opacity * ease;
|
|
301
|
-
}
|
|
302
|
-
result.push(animated);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (isGlyphFill(op)) glyphIndex++;
|
|
306
|
-
}
|
|
307
|
-
return result;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ---------- FADE / SLIDE / WAVE ----------
|
|
311
|
-
function applyFadeInAnimation(ops: DrawOp[], progress: number): DrawOp[] {
|
|
312
|
-
const alpha = easeOutQuad(progress);
|
|
313
|
-
const scale = 0.95 + 0.05 * alpha;
|
|
314
|
-
return scaleAndFade(ops, alpha, scale);
|
|
315
|
-
}
|
|
316
|
-
function applySlideInAnimation(
|
|
317
|
-
ops: DrawOp[],
|
|
318
|
-
progress: number,
|
|
319
|
-
direction: "left" | "right" | "up" | "down",
|
|
320
|
-
fontSize: number
|
|
321
|
-
): DrawOp[] {
|
|
322
|
-
const easeProgress = easeOutCubic(progress);
|
|
323
|
-
const shift = shiftFor(1 - easeProgress, direction, fontSize * 2);
|
|
324
|
-
const alpha = easeOutQuad(progress);
|
|
325
|
-
return translateGlyphOps(ops, shift.dx, shift.dy, alpha);
|
|
326
|
-
}
|
|
327
|
-
function applyMovingLettersAnimation(
|
|
328
|
-
ops: DrawOp[],
|
|
329
|
-
progress: number,
|
|
330
|
-
direction: "left" | "right" | "up" | "down",
|
|
331
|
-
fontSize: number
|
|
332
|
-
): DrawOp[] {
|
|
333
|
-
const amp = fontSize * 0.3;
|
|
334
|
-
return waveTransform(ops, direction, amp, progress);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ---------- word segmentation / slicing ----------
|
|
338
|
-
function getWordSegments(lines: ShapedLine[]) {
|
|
339
|
-
const segments: any = [];
|
|
340
|
-
let totalGlyphIndex = 0;
|
|
341
|
-
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
342
|
-
const line = lines[lineIndex];
|
|
343
|
-
const words = segmentLineBySpaces(line);
|
|
344
|
-
for (const word of words) {
|
|
345
|
-
if (word.length > 0)
|
|
346
|
-
segments.push({ startGlyph: totalGlyphIndex, glyphCount: word.length, lineIndex });
|
|
347
|
-
totalGlyphIndex += word.length;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return segments;
|
|
351
|
-
}
|
|
352
|
-
function segmentLineBySpaces(line: ShapedLine): Glyph[][] {
|
|
353
|
-
const words: Glyph[][] = [];
|
|
354
|
-
let current: Glyph[] = [];
|
|
355
|
-
for (const g of line.glyphs) {
|
|
356
|
-
const isSpace = g.char === " " || g.char === "\t" || g.char === "\n";
|
|
357
|
-
if (isSpace) {
|
|
358
|
-
if (current.length) {
|
|
359
|
-
words.push([...current]);
|
|
360
|
-
current = [];
|
|
361
|
-
}
|
|
362
|
-
} else {
|
|
363
|
-
current.push(g);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (current.length) words.push(current);
|
|
367
|
-
return words;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Include BOTH stroke+fill for the first `maxGlyphs` glyphs; nothing from later glyphs.
|
|
371
|
-
* Stroke is only included if its corresponding fill will be shown (so it never appears early). */
|
|
372
|
-
function sliceGlyphOps(ops: DrawOp[], maxGlyphs: number): DrawOp[] {
|
|
373
|
-
const result: DrawOp[] = [];
|
|
374
|
-
let glyphCount = 0;
|
|
375
|
-
let foundGlyphs = false;
|
|
376
|
-
|
|
377
|
-
for (const op of ops) {
|
|
378
|
-
if (op.op === "BeginFrame") {
|
|
379
|
-
result.push(op);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
384
|
-
if (glyphCount < maxGlyphs) {
|
|
385
|
-
result.push(op);
|
|
386
|
-
foundGlyphs = true;
|
|
387
|
-
}
|
|
388
|
-
glyphCount++; // count only real glyph fills
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (op.op === "StrokePath") {
|
|
393
|
-
// Include stroke only if the upcoming fill is within range
|
|
394
|
-
if (glyphCount < maxGlyphs) result.push(op);
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (op.op === "FillPath" && isShadowFill(op)) {
|
|
399
|
-
if (glyphCount < maxGlyphs) result.push(op);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (op.op === "DecorationLine" && foundGlyphs) {
|
|
404
|
-
result.push(op);
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return result;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function addTypewriterCursor(
|
|
413
|
-
ops: DrawOp[],
|
|
414
|
-
glyphCount: number,
|
|
415
|
-
fontSize: number,
|
|
416
|
-
time: number
|
|
417
|
-
): DrawOp[] {
|
|
418
|
-
const blinkRate = 2;
|
|
419
|
-
const cursorVisible = Math.floor(time * blinkRate * 2) % 2 === 0;
|
|
420
|
-
if (!cursorVisible || glyphCount === 0) return ops;
|
|
421
|
-
|
|
422
|
-
let last: DrawOp | null = null;
|
|
423
|
-
let count = 0;
|
|
424
|
-
for (const op of ops) {
|
|
425
|
-
if (op.op === "FillPath" && !isShadowFill(op)) {
|
|
426
|
-
count++;
|
|
427
|
-
if (count === glyphCount) {
|
|
428
|
-
last = op;
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (last && last.op === "FillPath") {
|
|
434
|
-
const color = getTextColorFromOps(ops);
|
|
435
|
-
const cursorX = last.x + fontSize * 0.5;
|
|
436
|
-
const cursorY = last.y;
|
|
437
|
-
|
|
438
|
-
const cursorOp: DrawOp = {
|
|
439
|
-
op: "DecorationLine",
|
|
440
|
-
from: { x: cursorX, y: cursorY - fontSize * 0.7 },
|
|
441
|
-
to: { x: cursorX, y: cursorY + fontSize * 0.1 },
|
|
442
|
-
width: Math.max(2, fontSize / 25),
|
|
443
|
-
color,
|
|
444
|
-
opacity: 1,
|
|
445
|
-
};
|
|
446
|
-
return [...ops, cursorOp];
|
|
447
|
-
}
|
|
448
|
-
return ops;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ---------- transforms ----------
|
|
452
|
-
function scaleAndFade(ops: DrawOp[], alpha: number, scale: number): DrawOp[] {
|
|
453
|
-
let cx = 0,
|
|
454
|
-
cy = 0,
|
|
455
|
-
n = 0;
|
|
456
|
-
ops.forEach((op) => {
|
|
457
|
-
if (op.op === "FillPath") {
|
|
458
|
-
cx += op.x;
|
|
459
|
-
cy += op.y;
|
|
460
|
-
n++;
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
if (n > 0) {
|
|
464
|
-
cx /= n;
|
|
465
|
-
cy /= n;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return ops.map((op) => {
|
|
469
|
-
if (op.op === "FillPath") {
|
|
470
|
-
const out: any = { ...op };
|
|
471
|
-
if (out.fill.kind === "solid") out.fill = { ...out.fill, opacity: out.fill.opacity * alpha };
|
|
472
|
-
else out.fill = { ...out.fill, opacity: (out.fill.opacity ?? 1) * alpha };
|
|
473
|
-
if (scale !== 1 && n > 0) {
|
|
474
|
-
const dx = op.x - cx,
|
|
475
|
-
dy = op.y - cy;
|
|
476
|
-
out.x = cx + dx * scale;
|
|
477
|
-
out.y = cy + dy * scale;
|
|
478
|
-
}
|
|
479
|
-
return out;
|
|
480
|
-
}
|
|
481
|
-
if (op.op === "StrokePath") {
|
|
482
|
-
const out: any = { ...op, opacity: op.opacity * alpha };
|
|
483
|
-
if (scale !== 1 && n > 0) {
|
|
484
|
-
const dx = op.x - cx,
|
|
485
|
-
dy = op.y - cy;
|
|
486
|
-
out.x = cx + dx * scale;
|
|
487
|
-
out.y = cy + dy * scale;
|
|
488
|
-
}
|
|
489
|
-
return out;
|
|
490
|
-
}
|
|
491
|
-
if (op.op === "DecorationLine") return { ...op, opacity: op.opacity * alpha };
|
|
492
|
-
return op;
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function translateGlyphOps(ops: DrawOp[], dx: number, dy: number, alpha: number = 1): DrawOp[] {
|
|
497
|
-
return ops.map((op) => {
|
|
498
|
-
if (op.op === "FillPath") {
|
|
499
|
-
const out: any = { ...op, x: op.x + dx, y: op.y + dy };
|
|
500
|
-
if (alpha < 1) {
|
|
501
|
-
if (out.fill.kind === "solid")
|
|
502
|
-
out.fill = { ...out.fill, opacity: out.fill.opacity * alpha };
|
|
503
|
-
else out.fill = { ...out.fill, opacity: (out.fill.opacity ?? 1) * alpha };
|
|
504
|
-
}
|
|
505
|
-
return out;
|
|
506
|
-
}
|
|
507
|
-
if (op.op === "StrokePath")
|
|
508
|
-
return { ...op, x: op.x + dx, y: op.y + dy, opacity: op.opacity * alpha };
|
|
509
|
-
if (op.op === "DecorationLine") {
|
|
510
|
-
return {
|
|
511
|
-
...op,
|
|
512
|
-
from: { x: op.from.x + dx, y: op.from.y + dy },
|
|
513
|
-
to: { x: op.to.x + dx, y: op.to.y + dy },
|
|
514
|
-
opacity: op.opacity * alpha,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
return op;
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function waveTransform(
|
|
522
|
-
ops: DrawOp[],
|
|
523
|
-
dir: "left" | "right" | "up" | "down",
|
|
524
|
-
amp: number,
|
|
525
|
-
p: number
|
|
526
|
-
): DrawOp[] {
|
|
527
|
-
let glyphIndex = 0;
|
|
528
|
-
return ops.map((op) => {
|
|
529
|
-
if (op.op === "FillPath" || op.op === "StrokePath") {
|
|
530
|
-
const phase = Math.sin((glyphIndex / 5) * Math.PI + p * Math.PI * 4);
|
|
531
|
-
const dx = dir === "left" || dir === "right" ? phase * amp * (dir === "left" ? -1 : 1) : 0;
|
|
532
|
-
const dy = dir === "up" || dir === "down" ? phase * amp * (dir === "up" ? -1 : 1) : 0;
|
|
533
|
-
const waveAlpha = Math.min(1, p * 2);
|
|
534
|
-
if (op.op === "FillPath") {
|
|
535
|
-
if (!isShadowFill(op)) glyphIndex++;
|
|
536
|
-
const out: any = { ...op, x: op.x + dx, y: op.y + dy };
|
|
537
|
-
if (out.fill.kind === "solid")
|
|
538
|
-
out.fill = { ...out.fill, opacity: out.fill.opacity * waveAlpha };
|
|
539
|
-
else out.fill = { ...out.fill, opacity: (out.fill.opacity ?? 1) * waveAlpha };
|
|
540
|
-
return out;
|
|
541
|
-
}
|
|
542
|
-
return { ...op, x: op.x + dx, y: op.y + dy, opacity: op.opacity * waveAlpha };
|
|
543
|
-
}
|
|
544
|
-
return op;
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// ---------- misc ----------
|
|
549
|
-
function shiftFor(progress: number, dir: "left" | "right" | "up" | "down", dist: number) {
|
|
550
|
-
const d = progress * dist;
|
|
551
|
-
switch (dir) {
|
|
552
|
-
case "left":
|
|
553
|
-
return { dx: -d, dy: 0 };
|
|
554
|
-
case "right":
|
|
555
|
-
return { dx: d, dy: 0 };
|
|
556
|
-
case "up":
|
|
557
|
-
return { dx: 0, dy: -d };
|
|
558
|
-
case "down":
|
|
559
|
-
return { dx: 0, dy: d };
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
function easeOutQuad(t: number) {
|
|
563
|
-
return t * (2 - t);
|
|
564
|
-
}
|
|
565
|
-
function easeOutCubic(t: number) {
|
|
566
|
-
return 1 - Math.pow(1 - t, 3);
|
|
567
|
-
}
|
|
568
|
-
function easeInOutQuad(t: number) {
|
|
569
|
-
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
570
|
-
}
|
package/src/core/colors.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { RGBA } from "../types";
|
|
2
|
-
|
|
3
|
-
export function parseHex6(hex: string, alpha = 1): RGBA {
|
|
4
|
-
const m = /^#?([a-f0-9]{6})$/i.exec(hex);
|
|
5
|
-
if (!m) throw new Error(`Invalid color ${hex}`);
|
|
6
|
-
const n = parseInt(m[1], 16);
|
|
7
|
-
const r = (n >> 16) & 0xff;
|
|
8
|
-
const g = (n >> 8) & 0xff;
|
|
9
|
-
const b = n & 0xff;
|
|
10
|
-
return { r, g, b, a: alpha };
|
|
11
|
-
}
|
package/src/core/decoration.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export function decorationGeometry(
|
|
2
|
-
kind: "underline" | "line-through",
|
|
3
|
-
p: { baselineY: number; fontSize: number; lineWidth: number; xStart: number }
|
|
4
|
-
) {
|
|
5
|
-
const thickness = Math.max(1, Math.round(p.fontSize * 0.05));
|
|
6
|
-
let y = p.baselineY + Math.round(p.fontSize * 0.1);
|
|
7
|
-
if (kind === "line-through") y = p.baselineY - Math.round(p.fontSize * 0.3);
|
|
8
|
-
return { x1: p.xStart, x2: p.xStart + p.lineWidth, y, width: thickness };
|
|
9
|
-
}
|