@clipkit/runtime 1.0.0

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.
Files changed (219) hide show
  1. package/LICENSE +54 -0
  2. package/README.md +98 -0
  3. package/dist/animation/easings.d.ts +9 -0
  4. package/dist/animation/easings.d.ts.map +1 -0
  5. package/dist/animation/easings.js +230 -0
  6. package/dist/animation/easings.js.map +1 -0
  7. package/dist/animation/expr.d.ts +44 -0
  8. package/dist/animation/expr.d.ts.map +1 -0
  9. package/dist/animation/expr.js +236 -0
  10. package/dist/animation/expr.js.map +1 -0
  11. package/dist/animation/keyframes.d.ts +23 -0
  12. package/dist/animation/keyframes.d.ts.map +1 -0
  13. package/dist/animation/keyframes.js +117 -0
  14. package/dist/animation/keyframes.js.map +1 -0
  15. package/dist/animation/motion-path.d.ts +18 -0
  16. package/dist/animation/motion-path.d.ts.map +1 -0
  17. package/dist/animation/motion-path.js +269 -0
  18. package/dist/animation/motion-path.js.map +1 -0
  19. package/dist/animation/noise1d.d.ts +5 -0
  20. package/dist/animation/noise1d.d.ts.map +1 -0
  21. package/dist/animation/noise1d.js +27 -0
  22. package/dist/animation/noise1d.js.map +1 -0
  23. package/dist/animation/presets.d.ts +60 -0
  24. package/dist/animation/presets.d.ts.map +1 -0
  25. package/dist/animation/presets.js +221 -0
  26. package/dist/animation/presets.js.map +1 -0
  27. package/dist/assets/cache.d.ts +18 -0
  28. package/dist/assets/cache.d.ts.map +1 -0
  29. package/dist/assets/cache.js +56 -0
  30. package/dist/assets/cache.js.map +1 -0
  31. package/dist/assets/fonts.d.ts +20 -0
  32. package/dist/assets/fonts.d.ts.map +1 -0
  33. package/dist/assets/fonts.js +127 -0
  34. package/dist/assets/fonts.js.map +1 -0
  35. package/dist/assets/loader.d.ts +18 -0
  36. package/dist/assets/loader.d.ts.map +1 -0
  37. package/dist/assets/loader.js +87 -0
  38. package/dist/assets/loader.js.map +1 -0
  39. package/dist/assets/lut.d.ts +5 -0
  40. package/dist/assets/lut.d.ts.map +1 -0
  41. package/dist/assets/lut.js +77 -0
  42. package/dist/assets/lut.js.map +1 -0
  43. package/dist/assets/media-time.d.ts +31 -0
  44. package/dist/assets/media-time.d.ts.map +1 -0
  45. package/dist/assets/media-time.js +65 -0
  46. package/dist/assets/media-time.js.map +1 -0
  47. package/dist/assets/mp4-frame-source.d.ts +44 -0
  48. package/dist/assets/mp4-frame-source.d.ts.map +1 -0
  49. package/dist/assets/mp4-frame-source.js +387 -0
  50. package/dist/assets/mp4-frame-source.js.map +1 -0
  51. package/dist/audio/encoder.d.ts +31 -0
  52. package/dist/audio/encoder.d.ts.map +1 -0
  53. package/dist/audio/encoder.js +96 -0
  54. package/dist/audio/encoder.js.map +1 -0
  55. package/dist/audio/fades.d.ts +16 -0
  56. package/dist/audio/fades.d.ts.map +1 -0
  57. package/dist/audio/fades.js +43 -0
  58. package/dist/audio/fades.js.map +1 -0
  59. package/dist/audio/limiter.d.ts +8 -0
  60. package/dist/audio/limiter.d.ts.map +1 -0
  61. package/dist/audio/limiter.js +39 -0
  62. package/dist/audio/limiter.js.map +1 -0
  63. package/dist/audio/loader.d.ts +6 -0
  64. package/dist/audio/loader.d.ts.map +1 -0
  65. package/dist/audio/loader.js +42 -0
  66. package/dist/audio/loader.js.map +1 -0
  67. package/dist/audio/mixer.d.ts +17 -0
  68. package/dist/audio/mixer.d.ts.map +1 -0
  69. package/dist/audio/mixer.js +204 -0
  70. package/dist/audio/mixer.js.map +1 -0
  71. package/dist/audio/varispeed.d.ts +24 -0
  72. package/dist/audio/varispeed.d.ts.map +1 -0
  73. package/dist/audio/varispeed.js +114 -0
  74. package/dist/audio/varispeed.js.map +1 -0
  75. package/dist/audio/wav.d.ts +6 -0
  76. package/dist/audio/wav.d.ts.map +1 -0
  77. package/dist/audio/wav.js +62 -0
  78. package/dist/audio/wav.js.map +1 -0
  79. package/dist/backend/backend.d.ts +579 -0
  80. package/dist/backend/backend.d.ts.map +1 -0
  81. package/dist/backend/backend.js +17 -0
  82. package/dist/backend/backend.js.map +1 -0
  83. package/dist/backend/webgl-backend.d.ts +97 -0
  84. package/dist/backend/webgl-backend.d.ts.map +1 -0
  85. package/dist/backend/webgl-backend.js +2142 -0
  86. package/dist/backend/webgl-backend.js.map +1 -0
  87. package/dist/backend/webgpu-backend.d.ts +121 -0
  88. package/dist/backend/webgpu-backend.d.ts.map +1 -0
  89. package/dist/backend/webgpu-backend.js +2481 -0
  90. package/dist/backend/webgpu-backend.js.map +1 -0
  91. package/dist/compositor/bitfont.d.ts +8 -0
  92. package/dist/compositor/bitfont.d.ts.map +1 -0
  93. package/dist/compositor/bitfont.js +52 -0
  94. package/dist/compositor/bitfont.js.map +1 -0
  95. package/dist/compositor/camera.d.ts +5 -0
  96. package/dist/compositor/camera.d.ts.map +1 -0
  97. package/dist/compositor/camera.js +114 -0
  98. package/dist/compositor/camera.js.map +1 -0
  99. package/dist/compositor/color.d.ts +26 -0
  100. package/dist/compositor/color.d.ts.map +1 -0
  101. package/dist/compositor/color.js +189 -0
  102. package/dist/compositor/color.js.map +1 -0
  103. package/dist/compositor/element-renderers/caption.d.ts +4 -0
  104. package/dist/compositor/element-renderers/caption.d.ts.map +1 -0
  105. package/dist/compositor/element-renderers/caption.js +376 -0
  106. package/dist/compositor/element-renderers/caption.js.map +1 -0
  107. package/dist/compositor/element-renderers/group.d.ts +12 -0
  108. package/dist/compositor/element-renderers/group.d.ts.map +1 -0
  109. package/dist/compositor/element-renderers/group.js +259 -0
  110. package/dist/compositor/element-renderers/group.js.map +1 -0
  111. package/dist/compositor/element-renderers/image.d.ts +4 -0
  112. package/dist/compositor/element-renderers/image.d.ts.map +1 -0
  113. package/dist/compositor/element-renderers/image.js +97 -0
  114. package/dist/compositor/element-renderers/image.js.map +1 -0
  115. package/dist/compositor/element-renderers/lit.d.ts +6 -0
  116. package/dist/compositor/element-renderers/lit.d.ts.map +1 -0
  117. package/dist/compositor/element-renderers/lit.js +82 -0
  118. package/dist/compositor/element-renderers/lit.js.map +1 -0
  119. package/dist/compositor/element-renderers/particles.d.ts +4 -0
  120. package/dist/compositor/element-renderers/particles.d.ts.map +1 -0
  121. package/dist/compositor/element-renderers/particles.js +212 -0
  122. package/dist/compositor/element-renderers/particles.js.map +1 -0
  123. package/dist/compositor/element-renderers/shape.d.ts +4 -0
  124. package/dist/compositor/element-renderers/shape.d.ts.map +1 -0
  125. package/dist/compositor/element-renderers/shape.js +171 -0
  126. package/dist/compositor/element-renderers/shape.js.map +1 -0
  127. package/dist/compositor/element-renderers/svg.d.ts +4 -0
  128. package/dist/compositor/element-renderers/svg.d.ts.map +1 -0
  129. package/dist/compositor/element-renderers/svg.js +210 -0
  130. package/dist/compositor/element-renderers/svg.js.map +1 -0
  131. package/dist/compositor/element-renderers/text.d.ts +25 -0
  132. package/dist/compositor/element-renderers/text.d.ts.map +1 -0
  133. package/dist/compositor/element-renderers/text.js +1358 -0
  134. package/dist/compositor/element-renderers/text.js.map +1 -0
  135. package/dist/compositor/element-renderers/video.d.ts +12 -0
  136. package/dist/compositor/element-renderers/video.d.ts.map +1 -0
  137. package/dist/compositor/element-renderers/video.js +109 -0
  138. package/dist/compositor/element-renderers/video.js.map +1 -0
  139. package/dist/compositor/fit.d.ts +18 -0
  140. package/dist/compositor/fit.d.ts.map +1 -0
  141. package/dist/compositor/fit.js +106 -0
  142. package/dist/compositor/fit.js.map +1 -0
  143. package/dist/compositor/lighting.d.ts +63 -0
  144. package/dist/compositor/lighting.d.ts.map +1 -0
  145. package/dist/compositor/lighting.js +141 -0
  146. package/dist/compositor/lighting.js.map +1 -0
  147. package/dist/compositor/mat4.d.ts +88 -0
  148. package/dist/compositor/mat4.d.ts.map +1 -0
  149. package/dist/compositor/mat4.js +245 -0
  150. package/dist/compositor/mat4.js.map +1 -0
  151. package/dist/compositor/project.d.ts +24 -0
  152. package/dist/compositor/project.d.ts.map +1 -0
  153. package/dist/compositor/project.js +105 -0
  154. package/dist/compositor/project.js.map +1 -0
  155. package/dist/compositor/render-context.d.ts +194 -0
  156. package/dist/compositor/render-context.d.ts.map +1 -0
  157. package/dist/compositor/render-context.js +10 -0
  158. package/dist/compositor/render-context.js.map +1 -0
  159. package/dist/compositor/resolve.d.ts +80 -0
  160. package/dist/compositor/resolve.d.ts.map +1 -0
  161. package/dist/compositor/resolve.js +276 -0
  162. package/dist/compositor/resolve.js.map +1 -0
  163. package/dist/compositor/scene.d.ts +10 -0
  164. package/dist/compositor/scene.d.ts.map +1 -0
  165. package/dist/compositor/scene.js +658 -0
  166. package/dist/compositor/scene.js.map +1 -0
  167. package/dist/compositor/transform.d.ts +73 -0
  168. package/dist/compositor/transform.d.ts.map +1 -0
  169. package/dist/compositor/transform.js +229 -0
  170. package/dist/compositor/transform.js.map +1 -0
  171. package/dist/compositor/unit.d.ts +27 -0
  172. package/dist/compositor/unit.d.ts.map +1 -0
  173. package/dist/compositor/unit.js +74 -0
  174. package/dist/compositor/unit.js.map +1 -0
  175. package/dist/encoder/exporter.d.ts +95 -0
  176. package/dist/encoder/exporter.d.ts.map +1 -0
  177. package/dist/encoder/exporter.js +341 -0
  178. package/dist/encoder/exporter.js.map +1 -0
  179. package/dist/encoder/index.d.ts +3 -0
  180. package/dist/encoder/index.d.ts.map +1 -0
  181. package/dist/encoder/index.js +2 -0
  182. package/dist/encoder/index.js.map +1 -0
  183. package/dist/index.d.ts +12 -0
  184. package/dist/index.d.ts.map +1 -0
  185. package/dist/index.js +27 -0
  186. package/dist/index.js.map +1 -0
  187. package/dist/logger.d.ts +13 -0
  188. package/dist/logger.d.ts.map +1 -0
  189. package/dist/logger.js +32 -0
  190. package/dist/logger.js.map +1 -0
  191. package/dist/runtime.d.ts +216 -0
  192. package/dist/runtime.d.ts.map +1 -0
  193. package/dist/runtime.js +1012 -0
  194. package/dist/runtime.js.map +1 -0
  195. package/dist/svg/morph.d.ts +6 -0
  196. package/dist/svg/morph.d.ts.map +1 -0
  197. package/dist/svg/morph.js +62 -0
  198. package/dist/svg/morph.js.map +1 -0
  199. package/dist/svg/svg-renderer.d.ts +18 -0
  200. package/dist/svg/svg-renderer.d.ts.map +1 -0
  201. package/dist/svg/svg-renderer.js +142 -0
  202. package/dist/svg/svg-renderer.js.map +1 -0
  203. package/dist/text/caption-chunk.d.ts +17 -0
  204. package/dist/text/caption-chunk.d.ts.map +1 -0
  205. package/dist/text/caption-chunk.js +76 -0
  206. package/dist/text/caption-chunk.js.map +1 -0
  207. package/dist/text/font-atlas.d.ts +63 -0
  208. package/dist/text/font-atlas.d.ts.map +1 -0
  209. package/dist/text/font-atlas.js +225 -0
  210. package/dist/text/font-atlas.js.map +1 -0
  211. package/dist/text/measure.d.ts +38 -0
  212. package/dist/text/measure.d.ts.map +1 -0
  213. package/dist/text/measure.js +164 -0
  214. package/dist/text/measure.js.map +1 -0
  215. package/dist/text/text-animation.d.ts +52 -0
  216. package/dist/text/text-animation.d.ts.map +1 -0
  217. package/dist/text/text-animation.js +133 -0
  218. package/dist/text/text-animation.js.map +1 -0
  219. package/package.json +47 -0
@@ -0,0 +1,225 @@
1
+ // Canvas-2D-based font atlas generator (v2, clean rewrite).
2
+ //
3
+ // Renders the ASCII printable charset into a single texture once per
4
+ // {family, size, weight} key. Characters are laid out in a grid with
5
+ // padding to avoid bleeding between cells at linear filtering.
6
+ //
7
+ // Fixes over the v0 implementation:
8
+ // - Uses `actualBoundingBox{Ascent,Descent}` correctly (clamping to a
9
+ // positive minimum so the negative-ascent edge case can't break layout).
10
+ // - Pre-loads the requested font via the FontFace API BEFORE measuring,
11
+ // so we measure the actual font, not the browser fallback.
12
+ // - Documents that the caller MUST await loadFont() before requesting
13
+ // an atlas. The atlas itself is sync once fonts are ready.
14
+ import { getLogger } from '../logger.js';
15
+ // The atlas charset. ASCII printable plus the Unicode punctuation +
16
+ // symbols that actually appear in modern Western text — smart quotes,
17
+ // dashes, bullets, currency, etc. Without these, paintviz-style content
18
+ // (curly quotes around dialogue, em-dashes in citations, · separators)
19
+ // drops glyphs at render time.
20
+ //
21
+ // Longer-term: scan the Source for actually-used characters and build a
22
+ // per-source atlas, so non-Latin content "just works" without growing
23
+ // this string further.
24
+ const ASCII_PRINTABLE = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' +
25
+ // Latin-1 punctuation + symbols
26
+ ' ¢£¥¦§©«®°±·»¿×÷' +
27
+ // General Punctuation: en/em dash, smart quotes, ellipsis, bullets
28
+ '–—‘’“”„†‡•…‰′″›‹' +
29
+ // Common symbols: euro, trademark, registered, arrows
30
+ '€™←↑→↓';
31
+ export function atlasKey(k) {
32
+ return `${k.family}|${k.size}|${k.weight}`;
33
+ }
34
+ /**
35
+ * Generate a font atlas synchronously. Caller must have already awaited
36
+ * font loading (loadFont + document.fonts.ready) — this function does
37
+ * NOT load fonts.
38
+ */
39
+ export function generateFontAtlas(key, backend) {
40
+ const { family, size, weight } = key;
41
+ const canvas = typeof OffscreenCanvas !== 'undefined'
42
+ ? new OffscreenCanvas(1, 1)
43
+ : document.createElement('canvas');
44
+ const ctx = canvas.getContext('2d', { willReadFrequently: false });
45
+ if (!ctx)
46
+ throw new Error('Failed to acquire 2D context for font atlas');
47
+ const fontSpec = `${weight} ${size}px ${family}`;
48
+ ctx.font = fontSpec;
49
+ ctx.textBaseline = 'alphabetic';
50
+ ctx.textAlign = 'left';
51
+ // Measure baseline metrics from a reference string with ascenders + descenders.
52
+ // Clamp to positive minimums: browsers occasionally return negative ascents
53
+ // for fallback fonts, which used to break v0 layout.
54
+ //
55
+ // Two metric sets with different jobs:
56
+ // - INK bounds (actualBoundingBox*, maxed over the charset below)
57
+ // size the atlas cells — a cell must contain every drawn pixel.
58
+ // - DESIGN metrics (fontBoundingBox*) drive line LAYOUT. CSS
59
+ // half-leading centers the font's design block in the line box;
60
+ // centering the ink block instead shifts baselines by a few px
61
+ // in a per-font direction (Inter reads high, DejaVu low).
62
+ const refMetrics = ctx.measureText('Mg');
63
+ let ascent = Math.max(size * 0.6, refMetrics.actualBoundingBoxAscent || size * 0.8);
64
+ let descent = Math.max(size * 0.15, refMetrics.actualBoundingBoxDescent || size * 0.2);
65
+ let fontAscent = Math.max(size * 0.6, refMetrics.fontBoundingBoxAscent || refMetrics.actualBoundingBoxAscent || size * 0.8);
66
+ let fontDescent = Math.max(size * 0.1, refMetrics.fontBoundingBoxDescent || refMetrics.actualBoundingBoxDescent || size * 0.2);
67
+ // fontBoundingBox APPROXIMATES the metrics Chrome's line layout
68
+ // uses, but disagrees per font (sohne-var lays out 3px high, Inter
69
+ // 2px low through it). When a document is available, measure the
70
+ // REAL baseline with a zero-size inline-block strut — its baseline
71
+ // sits on the text baseline, so offsetTop+offsetHeight inside a
72
+ // line-height:1 block IS Chrome's within-line-box baseline. Only
73
+ // the half-leading combo (ascent − descent)/2 affects our layout,
74
+ // so anchoring ascent = baseline, descent = size − baseline
75
+ // reproduces DOM positions exactly at any line_height.
76
+ if (typeof document !== 'undefined' && document.body) {
77
+ const probe = document.createElement('div');
78
+ probe.style.cssText =
79
+ `position:absolute;visibility:hidden;left:-9999px;top:0;` +
80
+ `white-space:nowrap;font:${fontSpec};line-height:${size}px;`;
81
+ probe.textContent = 'Hg';
82
+ const strut = document.createElement('span');
83
+ strut.style.cssText = 'display:inline-block;width:0;height:0;';
84
+ probe.appendChild(strut);
85
+ document.body.appendChild(probe);
86
+ const baseline = strut.offsetTop + strut.offsetHeight;
87
+ probe.remove();
88
+ if (baseline > 0 && baseline < size * 2) {
89
+ fontAscent = baseline;
90
+ fontDescent = size - baseline;
91
+ }
92
+ }
93
+ const metrics = new Map();
94
+ let maxCellWidth = 0;
95
+ for (const ch of ASCII_PRINTABLE) {
96
+ const m = ctx.measureText(ch);
97
+ const advance = m.width;
98
+ const aLeft = m.actualBoundingBoxLeft || 0;
99
+ const aRight = m.actualBoundingBoxRight || advance;
100
+ const aAscent = m.actualBoundingBoxAscent || ascent;
101
+ const aDescent = m.actualBoundingBoxDescent || descent;
102
+ const width = Math.ceil(Math.max(advance, aLeft + aRight));
103
+ metrics.set(ch, { width, advance, aLeft, aRight, aAscent, aDescent });
104
+ if (width > maxCellWidth)
105
+ maxCellWidth = width;
106
+ if (aAscent > ascent)
107
+ ascent = aAscent;
108
+ if (aDescent > descent)
109
+ descent = aDescent;
110
+ }
111
+ // Generous padding inside each cell. With ≥ 4 pixels of empty space around
112
+ // every glyph, bilinear filtering at the cell edge samples only padding
113
+ // pixels (transparent) — never neighboring glyphs. Combined with the
114
+ // half-texel UV inset on the read side, this eliminates glyph bleed.
115
+ const PADDING = 4;
116
+ const cellWidth = Math.ceil(maxCellWidth + 2 * PADDING);
117
+ const cellHeight = Math.ceil(ascent + descent + 2 * PADDING);
118
+ // Layout in a square-ish grid.
119
+ const cols = Math.ceil(Math.sqrt(ASCII_PRINTABLE.length));
120
+ const rows = Math.ceil(ASCII_PRINTABLE.length / cols);
121
+ const atlasWidth = cols * cellWidth;
122
+ const atlasHeight = rows * cellHeight;
123
+ // Cap at backend's max texture size — bail loudly if we exceed.
124
+ const maxDim = backend.capabilities.maxTextureSize;
125
+ if (atlasWidth > maxDim || atlasHeight > maxDim) {
126
+ throw new Error(`Font atlas for ${family} ${size}px ${weight} (${atlasWidth}×${atlasHeight}) ` +
127
+ `exceeds backend max texture dimension ${maxDim}.`);
128
+ }
129
+ canvas.width = atlasWidth;
130
+ canvas.height = atlasHeight;
131
+ // Re-set font on the resized canvas (browsers clear context state on resize).
132
+ ctx.font = fontSpec;
133
+ ctx.textBaseline = 'alphabetic';
134
+ ctx.textAlign = 'left';
135
+ ctx.fillStyle = '#ffffff';
136
+ ctx.clearRect(0, 0, atlasWidth, atlasHeight);
137
+ // Render each glyph and record its metrics.
138
+ // Antialiasing margin (in pixels) baked into each glyph's atlas bounds.
139
+ // This is what the renderer samples at the quad edge — guaranteed to be
140
+ // either transparent or a low-alpha AA fringe, never another glyph's ink.
141
+ const AA_MARGIN = 2;
142
+ const glyphs = new Map();
143
+ for (let i = 0; i < ASCII_PRINTABLE.length; i++) {
144
+ const ch = ASCII_PRINTABLE[i];
145
+ const col = i % cols;
146
+ const row = Math.floor(i / cols);
147
+ const cellX = col * cellWidth;
148
+ const cellY = row * cellHeight;
149
+ const baselineY = cellY + PADDING + ascent;
150
+ const penX = cellX + PADDING;
151
+ // Render glyph at the baseline. textBaseline: 'alphabetic' means y is baseline.
152
+ ctx.fillText(ch, penX, baselineY);
153
+ const m = metrics.get(ch);
154
+ // Compute the glyph's tight bounding box from its actual measured bounds,
155
+ // plus AA margin, then clamp to the cell so a runaway measurement can't
156
+ // pick up neighboring cells.
157
+ let glyphLeft = Math.floor(penX - m.aLeft) - AA_MARGIN;
158
+ let glyphRight = Math.ceil(penX + m.aRight) + AA_MARGIN;
159
+ let glyphTop = Math.floor(baselineY - m.aAscent) - AA_MARGIN;
160
+ let glyphBottom = Math.ceil(baselineY + m.aDescent) + AA_MARGIN;
161
+ glyphLeft = Math.max(cellX, glyphLeft);
162
+ glyphRight = Math.min(cellX + cellWidth, glyphRight);
163
+ glyphTop = Math.max(cellY, glyphTop);
164
+ glyphBottom = Math.min(cellY + cellHeight, glyphBottom);
165
+ const glyphW = Math.max(0, glyphRight - glyphLeft);
166
+ const glyphH = Math.max(0, glyphBottom - glyphTop);
167
+ glyphs.set(ch, {
168
+ x: glyphLeft,
169
+ y: glyphTop,
170
+ width: glyphW,
171
+ height: glyphH,
172
+ advance: m.advance,
173
+ offsetX: glyphLeft - penX,
174
+ offsetY: glyphTop - baselineY,
175
+ });
176
+ }
177
+ getLogger().debug(`Generated font atlas: ${family} ${size}px ${weight} (${atlasWidth}×${atlasHeight}, ${ASCII_PRINTABLE.length} glyphs)`);
178
+ const texture = backend.createTexture(canvas);
179
+ // Dedicated 1×1 measuring context for lazy pair-kerning — the atlas
180
+ // canvas itself would otherwise be retained at full size by the
181
+ // closure after its texture upload.
182
+ const kernCanvas = typeof OffscreenCanvas !== 'undefined'
183
+ ? new OffscreenCanvas(1, 1)
184
+ : document.createElement('canvas');
185
+ const kernCtx = kernCanvas.getContext('2d');
186
+ kernCtx.font = fontSpec;
187
+ const kernCache = new Map();
188
+ const kern = (a, b) => {
189
+ if (a === '' || b === '')
190
+ return 0;
191
+ const key = a + b;
192
+ let v = kernCache.get(key);
193
+ if (v === undefined) {
194
+ const ma = metrics.get(a);
195
+ const mb = metrics.get(b);
196
+ if (!ma || !mb) {
197
+ v = 0;
198
+ }
199
+ else {
200
+ if (kernCtx.font !== fontSpec)
201
+ kernCtx.font = fontSpec;
202
+ v = kernCtx.measureText(key).width - ma.advance - mb.advance;
203
+ }
204
+ kernCache.set(key, v);
205
+ }
206
+ return v;
207
+ };
208
+ return {
209
+ family,
210
+ size,
211
+ weight,
212
+ texture,
213
+ width: atlasWidth,
214
+ height: atlasHeight,
215
+ glyphs,
216
+ // Exported ascent/descent are the DESIGN metrics used for line
217
+ // layout. Cell geometry above keeps using the ink-bound values;
218
+ // glyph offsets are baseline-relative, so the two never mix.
219
+ ascent: fontAscent,
220
+ descent: fontDescent,
221
+ lineHeight: cellHeight,
222
+ kern,
223
+ };
224
+ }
225
+ //# sourceMappingURL=font-atlas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-atlas.js","sourceRoot":"","sources":["../../src/text/font-atlas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Ensure a font-family has a generic fallback so an unregistered or
3
+ * unavailable family falls back to SANS-SERIF (matching the browser/CSS
4
+ * convention) instead of the Canvas 2D default, which is serif. No-op if
5
+ * the value already carries a fallback chain or is itself a generic
6
+ * family. Applied at every text render so `font_family: "Inter"` doesn't
7
+ * silently render as Times when Inter isn't registered.
8
+ */
9
+ export declare function withFontFallback(family: string): string;
10
+ /**
11
+ * Measure the rendered width (in pixels) of a string at the given font.
12
+ * Returns 0 if measurement isn't possible (non-browser environment).
13
+ */
14
+ export declare function measureTextWidth(text: string, fontFamily: string, fontWeight: string | number, fontSize: number): number;
15
+ /**
16
+ * Compute the font size (in pixels) that makes `text` fit exactly within
17
+ * `maxWidth` at the given family and weight. Clamps to a sane range.
18
+ *
19
+ * Strategy: measure at a fixed reference size, then scale linearly. Text
20
+ * width is approximately linear in font size for a single line, so a
21
+ * single measurement is enough.
22
+ */
23
+ export declare function autoFitFontSize(text: string, fontFamily: string, fontWeight: string | number, maxWidth: number, fallback?: number, minSize?: number, maxSize?: number): number;
24
+ /**
25
+ * Two-dimensional auto-fit: the largest font size at which `text`,
26
+ * greedy-word-wrapped to `boxWidth`, fits within `boxHeight`
27
+ * (`lineCount × size × lineHeightRatio`). Used by
28
+ * `font_size: "auto"` when the element has BOTH width and height —
29
+ * the text wraps and grows to fill the box.
30
+ *
31
+ * Binary search over [minSize, maxSize]; the wrap simulation uses the
32
+ * same kerning-free per-char metric the renderer draws with, so the
33
+ * renderer's own wrap at the returned size agrees with the search.
34
+ * Returns minSize when even that overflows (text then overflows the
35
+ * box, CSS-style).
36
+ */
37
+ export declare function autoFitFontSizeBox(text: string, fontFamily: string, fontWeight: string | number, boxWidth: number, boxHeight: number, lineHeightRatio: number, minSize?: number, maxSize?: number): number;
38
+ //# sourceMappingURL=measure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../src/text/measure.ts"],"names":[],"mappings":"AAaA;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKvD;AAoBD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAAG,MAAM,EAC3B,QAAQ,EAAE,MAAM,GACf,MAAM,CASR;AA2CD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAAG,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAAW,EACrB,OAAO,GAAE,MAAsB,EAC/B,OAAO,GAAE,MAAsB,GAC9B,MAAM,CAMR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAAG,MAAM,EAC3B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,MAAsB,EAC/B,OAAO,GAAE,MAAsB,GAC9B,MAAM,CAyCR"}
@@ -0,0 +1,164 @@
1
+ // Text measurement utilities. Used for `font_size: "auto"` to compute the
2
+ // pixel size that makes a given string fit within a width constraint.
3
+ //
4
+ // Measurement uses a single shared Canvas 2D context. measureText is fast
5
+ // and doesn't depend on any GPU resources, so we can call it freely during
6
+ // preflight without affecting the render loop.
7
+ // Generic CSS families that already imply a fallback target.
8
+ const GENERIC_FAMILIES = new Set([
9
+ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy',
10
+ 'system-ui', 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded',
11
+ ]);
12
+ /**
13
+ * Ensure a font-family has a generic fallback so an unregistered or
14
+ * unavailable family falls back to SANS-SERIF (matching the browser/CSS
15
+ * convention) instead of the Canvas 2D default, which is serif. No-op if
16
+ * the value already carries a fallback chain or is itself a generic
17
+ * family. Applied at every text render so `font_family: "Inter"` doesn't
18
+ * silently render as Times when Inter isn't registered.
19
+ */
20
+ export function withFontFallback(family) {
21
+ const f = family.trim();
22
+ if (f.includes(','))
23
+ return f;
24
+ if (GENERIC_FAMILIES.has(f.toLowerCase()))
25
+ return f;
26
+ return `${f}, sans-serif`;
27
+ }
28
+ let sharedCtx = null;
29
+ function getCtx() {
30
+ if (sharedCtx)
31
+ return sharedCtx;
32
+ // Workers have no document but DO have OffscreenCanvas — real
33
+ // measurement beats the character-count estimate everywhere.
34
+ if (typeof OffscreenCanvas !== 'undefined') {
35
+ sharedCtx = new OffscreenCanvas(1, 1).getContext('2d');
36
+ return sharedCtx;
37
+ }
38
+ if (typeof document === 'undefined')
39
+ return null;
40
+ const canvas = document.createElement('canvas');
41
+ canvas.width = 1;
42
+ canvas.height = 1;
43
+ sharedCtx = canvas.getContext('2d');
44
+ return sharedCtx;
45
+ }
46
+ /**
47
+ * Measure the rendered width (in pixels) of a string at the given font.
48
+ * Returns 0 if measurement isn't possible (non-browser environment).
49
+ */
50
+ export function measureTextWidth(text, fontFamily, fontWeight, fontSize) {
51
+ const ctx = getCtx();
52
+ if (!ctx) {
53
+ // Rough estimate — every character ≈ 0.5 of font size in width. Better
54
+ // than nothing for non-browser callers.
55
+ return text.length * fontSize * 0.5;
56
+ }
57
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
58
+ return ctx.measureText(text).width;
59
+ }
60
+ const REFERENCE_FONT_SIZE = 100;
61
+ const MIN_FONT_SIZE = 8;
62
+ const MAX_FONT_SIZE = 1000;
63
+ /**
64
+ * Per-character width at the reference size — the SUM of these matches
65
+ * the renderer's kerning-free atlas advances, unlike a whole-string
66
+ * measureText (which applies kerning and reads a few px narrower).
67
+ * Auto-fit MUST size against the same metric the renderer draws with,
68
+ * or "exactly fits" overflows the box by the kerning delta.
69
+ */
70
+ function measureCharWidth(ch, fontFamily, fontWeight) {
71
+ const ctx = getCtx();
72
+ if (!ctx)
73
+ return REFERENCE_FONT_SIZE * 0.5;
74
+ ctx.font = `${fontWeight} ${REFERENCE_FONT_SIZE}px ${fontFamily}`;
75
+ return ctx.measureText(ch).width;
76
+ }
77
+ /** Kerning-free reference width of a string (sum of per-char widths). */
78
+ function refWidthOf(text, fontFamily, fontWeight, cache) {
79
+ let w = 0;
80
+ for (const ch of text) {
81
+ let cw = cache.get(ch);
82
+ if (cw === undefined) {
83
+ cw = measureCharWidth(ch, fontFamily, fontWeight);
84
+ cache.set(ch, cw);
85
+ }
86
+ w += cw;
87
+ }
88
+ return w;
89
+ }
90
+ /**
91
+ * Compute the font size (in pixels) that makes `text` fit exactly within
92
+ * `maxWidth` at the given family and weight. Clamps to a sane range.
93
+ *
94
+ * Strategy: measure at a fixed reference size, then scale linearly. Text
95
+ * width is approximately linear in font size for a single line, so a
96
+ * single measurement is enough.
97
+ */
98
+ export function autoFitFontSize(text, fontFamily, fontWeight, maxWidth, fallback = 48, minSize = MIN_FONT_SIZE, maxSize = MAX_FONT_SIZE) {
99
+ if (!text || maxWidth <= 0)
100
+ return fallback;
101
+ const refWidth = refWidthOf(text, fontFamily, fontWeight, new Map());
102
+ if (refWidth <= 0)
103
+ return fallback;
104
+ const scaled = REFERENCE_FONT_SIZE * (maxWidth / refWidth);
105
+ return Math.max(minSize, Math.min(scaled, maxSize));
106
+ }
107
+ /**
108
+ * Two-dimensional auto-fit: the largest font size at which `text`,
109
+ * greedy-word-wrapped to `boxWidth`, fits within `boxHeight`
110
+ * (`lineCount × size × lineHeightRatio`). Used by
111
+ * `font_size: "auto"` when the element has BOTH width and height —
112
+ * the text wraps and grows to fill the box.
113
+ *
114
+ * Binary search over [minSize, maxSize]; the wrap simulation uses the
115
+ * same kerning-free per-char metric the renderer draws with, so the
116
+ * renderer's own wrap at the returned size agrees with the search.
117
+ * Returns minSize when even that overflows (text then overflows the
118
+ * box, CSS-style).
119
+ */
120
+ export function autoFitFontSizeBox(text, fontFamily, fontWeight, boxWidth, boxHeight, lineHeightRatio, minSize = MIN_FONT_SIZE, maxSize = MAX_FONT_SIZE) {
121
+ if (!text || boxWidth <= 0 || boxHeight <= 0)
122
+ return minSize;
123
+ const cache = new Map();
124
+ const spaceRef = refWidthOf(' ', fontFamily, fontWeight, cache);
125
+ const lineWords = text.split('\n').map((line) => line.split(' ').map((word) => refWidthOf(word, fontFamily, fontWeight, cache)));
126
+ const fits = (size) => {
127
+ const scale = size / REFERENCE_FONT_SIZE;
128
+ let lineCount = 0;
129
+ for (const words of lineWords) {
130
+ let current = 0;
131
+ let started = false;
132
+ for (const refW of words) {
133
+ const w = refW * scale;
134
+ if (w > boxWidth)
135
+ return false; // a single word can't fit
136
+ const candidate = started ? current + spaceRef * scale + w : w;
137
+ if (started && candidate > boxWidth) {
138
+ lineCount += 1;
139
+ current = w;
140
+ }
141
+ else {
142
+ current = candidate;
143
+ started = true;
144
+ }
145
+ }
146
+ lineCount += 1;
147
+ }
148
+ return lineCount * size * lineHeightRatio <= boxHeight;
149
+ };
150
+ if (!fits(minSize))
151
+ return minSize;
152
+ let lo = minSize;
153
+ let hi = maxSize;
154
+ for (let i = 0; i < 24; i++) {
155
+ const mid = (lo + hi) / 2;
156
+ if (fits(mid))
157
+ lo = mid;
158
+ else
159
+ hi = mid;
160
+ }
161
+ // Round down a notch so float dust never pushes the wrap over.
162
+ return Math.floor(lo * 10) / 10;
163
+ }
164
+ //# sourceMappingURL=measure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure.js","sourceRoot":"","sources":["../../src/text/measure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { AnimationType, BaseElement, Easing } from '@clipkit/protocol';
2
+ export declare function isTextAnimationType(t: AnimationType): boolean;
3
+ /** [xRot, yRot, zRot] in degrees, applied about the unit's center. */
4
+ export type UnitRotation = [number, number, number];
5
+ export interface UnitEffect {
6
+ /** 0..1 multiplier folded into the glyph tint. */
7
+ opacity: number;
8
+ /** Element-local pixel offsets (pre-scale/skew/rotation). */
9
+ dx: number;
10
+ dy: number;
11
+ /**
12
+ * text-flip rotations (CKP/1.0, §6.5), accumulated per split
13
+ * granularity — each applies about the center of ITS unit (letter
14
+ * rotations about the glyph cell, word rotations about the word's
15
+ * bounding box). Word composes OUTSIDE letter.
16
+ */
17
+ flips?: {
18
+ letter?: UnitRotation;
19
+ word?: UnitRotation;
20
+ };
21
+ }
22
+ export interface CompiledTextAnim {
23
+ type: AnimationType;
24
+ split: 'letter' | 'word';
25
+ stagger: number;
26
+ /** Per-unit animation duration, seconds. */
27
+ duration: number;
28
+ /** Animation start, seconds relative to element start. */
29
+ startTime: number;
30
+ distance: number;
31
+ direction: 'left' | 'right' | 'up' | 'down';
32
+ frequency: number;
33
+ easing: Easing | undefined;
34
+ /** text-flip: rotation axis + starting angle in degrees. */
35
+ axis: 'x' | 'y' | 'z';
36
+ angle: number;
37
+ }
38
+ /** True when any compiled animation carries per-unit 3D rotation —
39
+ * the text renderer's signal to take the matrix path (§6.5). */
40
+ export declare function hasUnitRotations(compiled: CompiledTextAnim[]): boolean;
41
+ /**
42
+ * Compile the element's text-* animations. Returns null when there are
43
+ * none — the renderer's fast path.
44
+ */
45
+ export declare function compileTextAnimations(element: BaseElement): CompiledTextAnim[] | null;
46
+ /**
47
+ * Effect for one glyph at the element-local time. `letterIndex` counts
48
+ * drawn glyphs (whitespace excluded); `wordIndex` counts whitespace-
49
+ * separated runs.
50
+ */
51
+ export declare function evaluateUnitEffect(compiled: CompiledTextAnim[], localTime: number, letterIndex: number, wordIndex: number): UnitEffect;
52
+ //# sourceMappingURL=text-animation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-animation.d.ts","sourceRoot":"","sources":["../../src/text/text-animation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAa,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAYvF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAE7D;AAED,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,IAAI,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,4DAA4D;IAC5D,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAID;gEACgE;AAChE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAEtE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAwBrF;AAMD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAAE,EAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,UAAU,CA+DZ"}
@@ -0,0 +1,133 @@
1
+ // Per-unit (letter / word) text animation evaluation.
2
+ //
3
+ // The text renderer draws one quad per glyph, so per-unit animation is
4
+ // just per-glyph math at draw time: each glyph carries a letter index
5
+ // and a word index; for each active text-* animation we compute the
6
+ // unit's eased progress (offset by `stagger × unitIndex`) and fold the
7
+ // result into an opacity multiplier + position offset applied in
8
+ // element-local space (before scale/skew/rotation, so kinetic type
9
+ // composes with every other transform).
10
+ //
11
+ // Pure functions — no caching, no state. Costs a few multiplies per
12
+ // glyph per frame.
13
+ import { applyEasing } from '../animation/easings.js';
14
+ const TEXT_ANIMATION_TYPES = new Set([
15
+ 'text-appear',
16
+ 'text-slide',
17
+ 'text-fly',
18
+ 'text-typewriter',
19
+ 'text-wave',
20
+ 'text-flip',
21
+ ]);
22
+ export function isTextAnimationType(t) {
23
+ return TEXT_ANIMATION_TYPES.has(t);
24
+ }
25
+ const NO_EFFECT = { opacity: 1, dx: 0, dy: 0 };
26
+ /** True when any compiled animation carries per-unit 3D rotation —
27
+ * the text renderer's signal to take the matrix path (§6.5). */
28
+ export function hasUnitRotations(compiled) {
29
+ return compiled.some((a) => a.type === 'text-flip');
30
+ }
31
+ /**
32
+ * Compile the element's text-* animations. Returns null when there are
33
+ * none — the renderer's fast path.
34
+ */
35
+ export function compileTextAnimations(element) {
36
+ const anims = element.animations;
37
+ if (!anims || anims.length === 0)
38
+ return null;
39
+ let out = null;
40
+ for (const a of anims) {
41
+ if (!TEXT_ANIMATION_TYPES.has(a.type))
42
+ continue;
43
+ // v1: entrance-only — 'end' anchoring unsupported (documented).
44
+ if (a.time === 'end')
45
+ continue;
46
+ const split = a.split ?? defaultSplit(a.type);
47
+ (out ??= []).push({
48
+ type: a.type,
49
+ split,
50
+ stagger: a.stagger ?? (split === 'word' ? 0.09 : 0.035),
51
+ duration: a.duration ?? 0.5,
52
+ startTime: typeof a.time === 'number' ? a.time : 0,
53
+ distance: a.distance ?? (a.type === 'text-fly' ? 140 : a.type === 'text-wave' ? 12 : 40),
54
+ direction: a.direction ?? 'up',
55
+ frequency: a.frequency ?? 1.5,
56
+ easing: a.easing,
57
+ axis: a.axis ?? 'x',
58
+ angle: a.rotation ?? 90,
59
+ });
60
+ }
61
+ return out;
62
+ }
63
+ function defaultSplit(t) {
64
+ return t === 'text-typewriter' || t === 'text-wave' || t === 'text-flip' ? 'letter' : 'word';
65
+ }
66
+ /**
67
+ * Effect for one glyph at the element-local time. `letterIndex` counts
68
+ * drawn glyphs (whitespace excluded); `wordIndex` counts whitespace-
69
+ * separated runs.
70
+ */
71
+ export function evaluateUnitEffect(compiled, localTime, letterIndex, wordIndex) {
72
+ let opacity = 1;
73
+ let dx = 0;
74
+ let dy = 0;
75
+ let flips;
76
+ for (const anim of compiled) {
77
+ const unit = anim.split === 'word' ? wordIndex : letterIndex;
78
+ if (anim.type === 'text-wave') {
79
+ // Ambient bob: phase marches across units; no stagger gating.
80
+ const t = localTime - anim.startTime;
81
+ if (t < 0)
82
+ continue;
83
+ dy += Math.sin(2 * Math.PI * anim.frequency * t - unit * 0.6) * anim.distance;
84
+ continue;
85
+ }
86
+ const unitStart = anim.startTime + unit * anim.stagger;
87
+ if (anim.type === 'text-typewriter') {
88
+ if (localTime < unitStart)
89
+ opacity = 0;
90
+ continue;
91
+ }
92
+ const p = anim.duration > 0
93
+ ? Math.max(0, Math.min(1, (localTime - unitStart) / anim.duration))
94
+ : localTime >= unitStart
95
+ ? 1
96
+ : 0;
97
+ const eased = applyEasing(anim.easing ?? (anim.type === 'text-fly' ? 'ease-out-back' : 'ease-out-cubic'), p);
98
+ // All entrance types fade in over the unit's window.
99
+ opacity *= Math.max(0, Math.min(1, eased));
100
+ if (anim.type === 'text-flip') {
101
+ // Rotate from `angle` to rest about the unit's own center.
102
+ const remaining = anim.angle * (1 - eased);
103
+ if (remaining !== 0) {
104
+ flips ??= {};
105
+ const slot = (flips[anim.split] ??= [0, 0, 0]);
106
+ slot[anim.axis === 'x' ? 0 : anim.axis === 'y' ? 1 : 2] += remaining;
107
+ }
108
+ continue;
109
+ }
110
+ if (anim.type === 'text-slide' || anim.type === 'text-fly') {
111
+ // Start displaced OPPOSITE the motion direction, settle at rest.
112
+ const remaining = (1 - eased) * anim.distance;
113
+ switch (anim.direction) {
114
+ case 'up':
115
+ dy += remaining;
116
+ break;
117
+ case 'down':
118
+ dy -= remaining;
119
+ break;
120
+ case 'left':
121
+ dx += remaining;
122
+ break;
123
+ case 'right':
124
+ dx -= remaining;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ if (opacity === 1 && dx === 0 && dy === 0 && !flips)
130
+ return NO_EFFECT;
131
+ return { opacity, dx, dy, flips };
132
+ }
133
+ //# sourceMappingURL=text-animation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-animation.js","sourceRoot":"","sources":["../../src/text/text-animation.ts"],"names":[],"mappings":"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"}