render-tag 0.1.7 → 0.1.9
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/README.md +75 -151
- package/lib/css-resolver.d.ts.map +1 -1
- package/lib/css-resolver.js +16 -0
- package/lib/css-resolver.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -32
- package/lib/index.js.map +1 -1
- package/lib/layout.d.ts +6 -1
- package/lib/layout.d.ts.map +1 -1
- package/lib/layout.js +189 -29
- package/lib/layout.js.map +1 -1
- package/lib/path/glyph-layout.d.ts +120 -0
- package/lib/path/glyph-layout.d.ts.map +1 -0
- package/lib/path/glyph-layout.js +337 -0
- package/lib/path/glyph-layout.js.map +1 -0
- package/lib/path/grapheme.d.ts +2 -0
- package/lib/path/grapheme.d.ts.map +1 -0
- package/lib/path/grapheme.js +39 -0
- package/lib/path/grapheme.js.map +1 -0
- package/lib/path/index.d.ts +131 -0
- package/lib/path/index.d.ts.map +1 -0
- package/lib/path/index.js +421 -0
- package/lib/path/index.js.map +1 -0
- package/lib/path/svg-path.d.ts +68 -0
- package/lib/path/svg-path.d.ts.map +1 -0
- package/lib/path/svg-path.js +393 -0
- package/lib/path/svg-path.js.map +1 -0
- package/lib/render-tag.umd.js +4 -4
- package/lib/render-tag.umd.js.map +1 -1
- package/lib/render.d.ts +18 -0
- package/lib/render.d.ts.map +1 -1
- package/lib/render.js +3 -3
- package/lib/render.js.map +1 -1
- package/lib/types.d.ts +18 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout: walks styled segments and lays each grapheme along a path.
|
|
3
|
+
* No canvas rendering happens here — that's the caller's job. Reusable
|
|
4
|
+
* for hit-testing, debugging, alternate renderers (SVG, GPU).
|
|
5
|
+
*
|
|
6
|
+
* Joining-script support: graphemes from Arabic, Hebrew, Indic, Thai, Khmer,
|
|
7
|
+
* Myanmar and related scripts within a single segment are grouped into one
|
|
8
|
+
* "shaped run" placement. The browser's fillText/measureText then shapes the
|
|
9
|
+
* run as a unit (cursive joining, reordering, conjuncts) — something
|
|
10
|
+
* per-grapheme drawing cannot do.
|
|
11
|
+
*/
|
|
12
|
+
import type { ResolvedStyle, StyledNode } from '../types.js';
|
|
13
|
+
import type { PathLike } from './svg-path.js';
|
|
14
|
+
export interface Segment {
|
|
15
|
+
text: string;
|
|
16
|
+
style: ResolvedStyle;
|
|
17
|
+
/** True when this segment should be laid out right-to-left. */
|
|
18
|
+
rtl: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface GlyphPlacement {
|
|
21
|
+
/**
|
|
22
|
+
* Renderable text unit — usually a single grapheme cluster, but joining
|
|
23
|
+
* scripts (Arabic, Indic, Thai, Khmer, Myanmar, …) emit multi-grapheme runs
|
|
24
|
+
* here so the browser can shape them correctly during fillText.
|
|
25
|
+
*/
|
|
26
|
+
char: string;
|
|
27
|
+
/** Origin of the glyph on the path (translate target before fillText). */
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
/** Tangent angle at the glyph origin, in radians. */
|
|
31
|
+
rotation: number;
|
|
32
|
+
/** Advance width of the glyph or shaped run. */
|
|
33
|
+
width: number;
|
|
34
|
+
/** Resolved style to apply when drawing this glyph. */
|
|
35
|
+
style: ResolvedStyle;
|
|
36
|
+
/** Font ascent above the baseline (px) — used for visual extent. */
|
|
37
|
+
ascent: number;
|
|
38
|
+
/** Font descent below the baseline (px). */
|
|
39
|
+
descent: number;
|
|
40
|
+
/** Distance from the start of the text along the path (px). */
|
|
41
|
+
pathOffset: number;
|
|
42
|
+
/** True when this placement is a shaped run, not a single grapheme. */
|
|
43
|
+
shaped: boolean;
|
|
44
|
+
}
|
|
45
|
+
export type AlignMode = 'left' | 'center' | 'right' | 'justify';
|
|
46
|
+
/**
|
|
47
|
+
* Where the path runs relative to the rendered text.
|
|
48
|
+
* - `alphabetic` (default) — path = text baseline; descenders drop below.
|
|
49
|
+
* - `middle` — path runs through the vertical center of the text.
|
|
50
|
+
* - `top` — path runs along the top of the text.
|
|
51
|
+
* - `bottom` — path runs along the bottom (including descenders).
|
|
52
|
+
* - `hanging` / `ideographic` — approximations of the matching CSS values.
|
|
53
|
+
*/
|
|
54
|
+
export type TextBaseline = 'alphabetic' | 'middle' | 'top' | 'bottom' | 'hanging' | 'ideographic';
|
|
55
|
+
export interface LayoutInput {
|
|
56
|
+
segments: Segment[];
|
|
57
|
+
path: PathLike;
|
|
58
|
+
ctx: CanvasRenderingContext2D;
|
|
59
|
+
align: AlignMode;
|
|
60
|
+
textBaseline: TextBaseline;
|
|
61
|
+
}
|
|
62
|
+
export interface LayoutOutput {
|
|
63
|
+
glyphs: GlyphPlacement[];
|
|
64
|
+
/** Sum of glyph widths + letterSpacing (the natural width of the rendered text). */
|
|
65
|
+
textWidth: number;
|
|
66
|
+
pathLength: number;
|
|
67
|
+
/**
|
|
68
|
+
* Max line height across all segments. Resolved CSS line-height in px when
|
|
69
|
+
* set; otherwise the segment's font size. Useful as the ribbon thickness
|
|
70
|
+
* when drawing a background polygon around curved text.
|
|
71
|
+
*/
|
|
72
|
+
lineHeight: number;
|
|
73
|
+
/**
|
|
74
|
+
* Visible bounding box of the rendered text in the layout's coordinate
|
|
75
|
+
* system. Computed as the union of each glyph's cell, where the cell is
|
|
76
|
+
* width × per-glyph line-height (CSS line-height in px when set, else
|
|
77
|
+
* font size) and rotated by the glyph's tangent. lineHeight is
|
|
78
|
+
* distributed above/below the baseline by the font's ascent/descent
|
|
79
|
+
* ratio. Per-glyph (not max-across) so mixed-size curve text doesn't
|
|
80
|
+
* inflate.
|
|
81
|
+
*
|
|
82
|
+
* Consumers (e.g. Polotno) use this to keep an element's width/height in
|
|
83
|
+
* sync with the rendered model. The library does not consume `bounds`
|
|
84
|
+
* itself — it's purely exposed for callers.
|
|
85
|
+
*/
|
|
86
|
+
bounds: {
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
};
|
|
92
|
+
/** The textBaseline mode used for this layout (echoes the input). */
|
|
93
|
+
textBaseline: TextBaseline;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns the local-y of the alphabetic baseline given a textBaseline
|
|
97
|
+
* choice and a glyph's ascent/descent. All baseline-relative computations
|
|
98
|
+
* (decoration positions, background polygons, bounds cells) ADD this offset
|
|
99
|
+
* to their local-y so the path line through (0,0) corresponds to the
|
|
100
|
+
* requested baseline anchor.
|
|
101
|
+
*/
|
|
102
|
+
export declare function baselineLocalY(tb: TextBaseline, ascent: number, descent: number): number;
|
|
103
|
+
/**
|
|
104
|
+
* Flatten a styled tree into a flat sequence of styled text segments.
|
|
105
|
+
* Walks in document order; concatenates text under nested inline elements.
|
|
106
|
+
* Each `#text` node contributes one segment with its resolved style.
|
|
107
|
+
*/
|
|
108
|
+
export declare function flattenSegments(root: StyledNode): Segment[];
|
|
109
|
+
/**
|
|
110
|
+
* Lay out graphemes along a path. Pure: does not call ctx.fillText.
|
|
111
|
+
*
|
|
112
|
+
* Algorithm:
|
|
113
|
+
* 1. Per segment, split into graphemes and shaped runs, measure each.
|
|
114
|
+
* 2. Compute total natural width.
|
|
115
|
+
* 3. Pick a starting offset along the path based on `align`.
|
|
116
|
+
* 4. For each placement: get p0 / p1 from the path, rotation = atan2(p1-p0).
|
|
117
|
+
* If a placement would overshoot, only allow it within kerning slack.
|
|
118
|
+
*/
|
|
119
|
+
export declare function layoutGlyphsOnPath(input: LayoutInput): LayoutOutput;
|
|
120
|
+
//# sourceMappingURL=glyph-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glyph-layout.d.ts","sourceRoot":"","sources":["../../src/path/glyph-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG7D,OAAO,KAAK,EAAE,QAAQ,EAAS,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;IACrB,+DAA+D;IAC/D,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,KAAK,EAAE,aAAa,CAAC;IACrB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GACpB,YAAY,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAE3E,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,wBAAwB,CAAC;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,qEAAqE;IACrE,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAChD,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,CAY3D;AAwID;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CA8GnE"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout: walks styled segments and lays each grapheme along a path.
|
|
3
|
+
* No canvas rendering happens here — that's the caller's job. Reusable
|
|
4
|
+
* for hit-testing, debugging, alternate renderers (SVG, GPU).
|
|
5
|
+
*
|
|
6
|
+
* Joining-script support: graphemes from Arabic, Hebrew, Indic, Thai, Khmer,
|
|
7
|
+
* Myanmar and related scripts within a single segment are grouped into one
|
|
8
|
+
* "shaped run" placement. The browser's fillText/measureText then shapes the
|
|
9
|
+
* run as a unit (cursive joining, reordering, conjuncts) — something
|
|
10
|
+
* per-grapheme drawing cannot do.
|
|
11
|
+
*/
|
|
12
|
+
import { applyFont, getFontMetrics } from '../layout.js';
|
|
13
|
+
import { stringToArray } from './grapheme.js';
|
|
14
|
+
/**
|
|
15
|
+
* Returns the local-y of the alphabetic baseline given a textBaseline
|
|
16
|
+
* choice and a glyph's ascent/descent. All baseline-relative computations
|
|
17
|
+
* (decoration positions, background polygons, bounds cells) ADD this offset
|
|
18
|
+
* to their local-y so the path line through (0,0) corresponds to the
|
|
19
|
+
* requested baseline anchor.
|
|
20
|
+
*/
|
|
21
|
+
export function baselineLocalY(tb, ascent, descent) {
|
|
22
|
+
switch (tb) {
|
|
23
|
+
case 'alphabetic': return 0;
|
|
24
|
+
case 'middle': return (ascent - descent) / 2;
|
|
25
|
+
case 'top': return ascent;
|
|
26
|
+
case 'bottom': return -descent;
|
|
27
|
+
case 'hanging': return ascent * 0.8;
|
|
28
|
+
case 'ideographic': return -descent * 0.5;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Flatten a styled tree into a flat sequence of styled text segments.
|
|
33
|
+
* Walks in document order; concatenates text under nested inline elements.
|
|
34
|
+
* Each `#text` node contributes one segment with its resolved style.
|
|
35
|
+
*/
|
|
36
|
+
export function flattenSegments(root) {
|
|
37
|
+
const out = [];
|
|
38
|
+
function walk(node, inheritedRtl) {
|
|
39
|
+
const rtl = node.style.direction === 'rtl' || inheritedRtl;
|
|
40
|
+
if (node.tagName === '#text' && node.textContent) {
|
|
41
|
+
out.push({ text: node.textContent, style: node.style, rtl });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const child of node.children)
|
|
45
|
+
walk(child, rtl);
|
|
46
|
+
}
|
|
47
|
+
walk(root, false);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
// Unicode ranges where graphemes need shape-aware rendering. The browser's
|
|
51
|
+
// fillText handles these correctly only when given the whole run at once,
|
|
52
|
+
// not one grapheme at a time:
|
|
53
|
+
// - Hebrew (no joining, but RTL+BiDi)
|
|
54
|
+
// - Arabic + presentation forms (cursive joining)
|
|
55
|
+
// - N'Ko, Mandaic, Syriac, Thaana (joining/RTL)
|
|
56
|
+
// - Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada,
|
|
57
|
+
// Malayalam, Sinhala (Indic reordering + conjuncts)
|
|
58
|
+
// - Thai, Lao (combining marks + word break)
|
|
59
|
+
// - Tibetan (stacking)
|
|
60
|
+
// - Myanmar (reordering + stacking)
|
|
61
|
+
// - Khmer (reordering + subscript consonants)
|
|
62
|
+
// IMPORTANT: written with `\u` escapes only. Mixing literal RTL characters
|
|
63
|
+
// confuses the regex parser at parse time — e.g. the precomposed `יִ` is
|
|
64
|
+
// actually two code points (U+05D9 + U+05B4) and a literal range starting
|
|
65
|
+
// at the second code point engulfs CJK / Hangul / Hiragana / Katakana,
|
|
66
|
+
// causing `needsShaping('中')` to return true.
|
|
67
|
+
const SHAPING_RE = new RegExp('[' +
|
|
68
|
+
'\\u0590-\\u05FF' + // Hebrew
|
|
69
|
+
'\\u0600-\\u06FF' + // Arabic
|
|
70
|
+
'\\u0700-\\u074F' + // Syriac
|
|
71
|
+
'\\u0750-\\u077F' + // Arabic Supplement
|
|
72
|
+
'\\u0780-\\u07BF' + // Thaana
|
|
73
|
+
'\\u07C0-\\u07FF' + // NKo
|
|
74
|
+
'\\u0800-\\u083F' + // Samaritan
|
|
75
|
+
'\\u0840-\\u085F' + // Mandaic
|
|
76
|
+
'\\u0860-\\u086F' + // Syriac Supplement
|
|
77
|
+
'\\u08A0-\\u08FF' + // Arabic Extended-A
|
|
78
|
+
'\\u0900-\\u097F' + // Devanagari
|
|
79
|
+
'\\u0980-\\u09FF' + // Bengali
|
|
80
|
+
'\\u0A00-\\u0A7F' + // Gurmukhi
|
|
81
|
+
'\\u0A80-\\u0AFF' + // Gujarati
|
|
82
|
+
'\\u0B00-\\u0B7F' + // Oriya
|
|
83
|
+
'\\u0B80-\\u0BFF' + // Tamil
|
|
84
|
+
'\\u0C00-\\u0C7F' + // Telugu
|
|
85
|
+
'\\u0C80-\\u0CFF' + // Kannada
|
|
86
|
+
'\\u0D00-\\u0D7F' + // Malayalam
|
|
87
|
+
'\\u0D80-\\u0DFF' + // Sinhala
|
|
88
|
+
'\\u0E00-\\u0E7F' + // Thai
|
|
89
|
+
'\\u0E80-\\u0EFF' + // Lao
|
|
90
|
+
'\\u0F00-\\u0FFF' + // Tibetan
|
|
91
|
+
'\\u1000-\\u109F' + // Myanmar
|
|
92
|
+
'\\u1780-\\u17FF' + // Khmer
|
|
93
|
+
'\\u1800-\\u18AF' + // Mongolian
|
|
94
|
+
'\\uFB1D-\\uFB4F' + // Hebrew Presentation Forms
|
|
95
|
+
'\\uFB50-\\uFDFF' + // Arabic Presentation Forms-A
|
|
96
|
+
'\\uFE70-\\uFEFF' + // Arabic Presentation Forms-B
|
|
97
|
+
']');
|
|
98
|
+
function needsShaping(s) {
|
|
99
|
+
return SHAPING_RE.test(s);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Split a single styled segment into PreGlyphs.
|
|
103
|
+
*
|
|
104
|
+
* Non-joining graphemes (Latin, CJK, …) emit one PreGlyph per grapheme so the
|
|
105
|
+
* curve can drive per-glyph rotation. Joining-script graphemes are grouped
|
|
106
|
+
* into runs (split at whitespace + style boundaries) so the browser can shape
|
|
107
|
+
* them correctly when we later call fillText on the run as a whole.
|
|
108
|
+
*
|
|
109
|
+
* For RTL segments, the RUN ORDER is reversed (not the graphemes inside a
|
|
110
|
+
* shaped run) — that way Arabic words still shape correctly while flowing in
|
|
111
|
+
* visual right-to-left order along an LTR path walk.
|
|
112
|
+
*/
|
|
113
|
+
function preGlyphsForSegment(ctx, seg) {
|
|
114
|
+
const graphemes = stringToArray(seg.text);
|
|
115
|
+
if (graphemes.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
applyFont(ctx, seg.style);
|
|
118
|
+
// Always assign — when the current segment's letterSpacing is 0/unset,
|
|
119
|
+
// we still need to reset the previous segment's value.
|
|
120
|
+
ctx.letterSpacing = `${seg.style.letterSpacing || 0}px`;
|
|
121
|
+
const { ascent, descent } = getFontMetrics(ctx, seg.style);
|
|
122
|
+
// Group graphemes into (shaped run | single non-shaped grapheme).
|
|
123
|
+
// Boundaries: shape-status change, ASCII whitespace.
|
|
124
|
+
const runs = [];
|
|
125
|
+
let currentShapedRun = '';
|
|
126
|
+
for (const g of graphemes) {
|
|
127
|
+
const isSpace = g === ' ';
|
|
128
|
+
if (needsShaping(g) && !isSpace) {
|
|
129
|
+
currentShapedRun += g;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (currentShapedRun) {
|
|
133
|
+
runs.push({ text: currentShapedRun, shaped: true, isSpace: false });
|
|
134
|
+
currentShapedRun = '';
|
|
135
|
+
}
|
|
136
|
+
runs.push({ text: g, shaped: false, isSpace });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (currentShapedRun) {
|
|
140
|
+
runs.push({ text: currentShapedRun, shaped: true, isSpace: false });
|
|
141
|
+
}
|
|
142
|
+
// RTL: reverse run order. Don't reverse graphemes inside a shaped run —
|
|
143
|
+
// the browser will lay them out right-to-left during fillText.
|
|
144
|
+
if (seg.rtl)
|
|
145
|
+
runs.reverse();
|
|
146
|
+
// Measure each run with the current ctx font/letterSpacing.
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const r of runs) {
|
|
149
|
+
const width = ctx.measureText(r.text).width;
|
|
150
|
+
out.push({
|
|
151
|
+
text: r.text,
|
|
152
|
+
width,
|
|
153
|
+
style: seg.style,
|
|
154
|
+
isSpace: r.isSpace,
|
|
155
|
+
ascent,
|
|
156
|
+
descent,
|
|
157
|
+
shaped: r.shaped,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Lay out graphemes along a path. Pure: does not call ctx.fillText.
|
|
164
|
+
*
|
|
165
|
+
* Algorithm:
|
|
166
|
+
* 1. Per segment, split into graphemes and shaped runs, measure each.
|
|
167
|
+
* 2. Compute total natural width.
|
|
168
|
+
* 3. Pick a starting offset along the path based on `align`.
|
|
169
|
+
* 4. For each placement: get p0 / p1 from the path, rotation = atan2(p1-p0).
|
|
170
|
+
* If a placement would overshoot, only allow it within kerning slack.
|
|
171
|
+
*/
|
|
172
|
+
export function layoutGlyphsOnPath(input) {
|
|
173
|
+
const { segments, path, ctx, align, textBaseline } = input;
|
|
174
|
+
// 1. Pre-measure all placements (one per grapheme or shaped run).
|
|
175
|
+
// Caller's ctx state is mutated here (font, fontKerning, letterSpacing).
|
|
176
|
+
// The outer drawTextOnPath/drawTextOnPathLayout calls ctx.save before this
|
|
177
|
+
// and ctx.restore after, so the leak doesn't reach the caller.
|
|
178
|
+
const preGlyphs = [];
|
|
179
|
+
let measuredWholeWidth = 0;
|
|
180
|
+
let maxLineHeight = 0;
|
|
181
|
+
for (const seg of segments) {
|
|
182
|
+
if (!seg.text)
|
|
183
|
+
continue;
|
|
184
|
+
const lh = seg.style.lineHeight > 0 ? seg.style.lineHeight : seg.style.fontSize;
|
|
185
|
+
if (lh > maxLineHeight)
|
|
186
|
+
maxLineHeight = lh;
|
|
187
|
+
const segGlyphs = preGlyphsForSegment(ctx, seg);
|
|
188
|
+
if (segGlyphs.length === 0)
|
|
189
|
+
continue;
|
|
190
|
+
preGlyphs.push(...segGlyphs);
|
|
191
|
+
// Whole-segment width — kerning makes this < sum of per-glyph widths.
|
|
192
|
+
measuredWholeWidth += ctx.measureText(seg.text).width;
|
|
193
|
+
}
|
|
194
|
+
// 2. Sum natural width (sum of placement widths + letterSpacing per item).
|
|
195
|
+
let textWidth = 0;
|
|
196
|
+
for (const g of preGlyphs) {
|
|
197
|
+
textWidth += g.width + (g.style.letterSpacing || 0);
|
|
198
|
+
}
|
|
199
|
+
// Trim the last letterSpacing — it shouldn't trail.
|
|
200
|
+
if (preGlyphs.length > 0) {
|
|
201
|
+
textWidth -= preGlyphs[preGlyphs.length - 1].style.letterSpacing || 0;
|
|
202
|
+
}
|
|
203
|
+
// Kerning slack: how much the per-glyph sum can exceed the measured
|
|
204
|
+
// whole-string width. When the path is sized to match the visual text,
|
|
205
|
+
// we use this to allow up to N px of overshoot at the path's tail end.
|
|
206
|
+
const kerningSlack = Math.max(0, textWidth - measuredWholeWidth);
|
|
207
|
+
// 3. Starting offset + per-space extra (justify).
|
|
208
|
+
const pathLength = path.length;
|
|
209
|
+
let startOffset = 0;
|
|
210
|
+
let extraPerSpace = 0;
|
|
211
|
+
if (align === 'center') {
|
|
212
|
+
startOffset = Math.max(0, (pathLength - textWidth) / 2);
|
|
213
|
+
}
|
|
214
|
+
else if (align === 'right') {
|
|
215
|
+
startOffset = Math.max(0, pathLength - textWidth);
|
|
216
|
+
}
|
|
217
|
+
else if (align === 'justify') {
|
|
218
|
+
const spaceCount = preGlyphs.filter(g => g.isSpace).length;
|
|
219
|
+
if (spaceCount > 0 && pathLength > textWidth) {
|
|
220
|
+
extraPerSpace = (pathLength - textWidth) / spaceCount;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// 4. Walk the path.
|
|
224
|
+
// We track two cumulative offsets:
|
|
225
|
+
// - `offset` is the path arc-length (used for path.getPointAtLength), and
|
|
226
|
+
// advances by `effectiveWidth` (incl. justify extraPerSpace).
|
|
227
|
+
// - `naturalOffset` is the glyph's position in NATURAL text-space
|
|
228
|
+
// [0, textWidth]. Used as `pathOffset` for gradient slicing — must NOT
|
|
229
|
+
// include extraPerSpace, otherwise late justified glyphs map past the
|
|
230
|
+
// gradient's last stop.
|
|
231
|
+
const glyphs = [];
|
|
232
|
+
let offset = startOffset;
|
|
233
|
+
let naturalOffset = 0;
|
|
234
|
+
for (let i = 0; i < preGlyphs.length; i++) {
|
|
235
|
+
const g = preGlyphs[i];
|
|
236
|
+
const effectiveWidth = g.width + (g.isSpace ? extraPerSpace : 0);
|
|
237
|
+
const p0 = path.getPointAtLength(offset);
|
|
238
|
+
if (!p0)
|
|
239
|
+
break;
|
|
240
|
+
let endLen = offset + effectiveWidth;
|
|
241
|
+
let p1;
|
|
242
|
+
if (endLen > pathLength) {
|
|
243
|
+
// Clamp to pathLength only if the overshoot is within the kerning slack —
|
|
244
|
+
// keeps the last glyph from getting dropped to a sub-px rounding miss.
|
|
245
|
+
if (endLen - pathLength <= kerningSlack + 0.5) {
|
|
246
|
+
p1 = path.getPointAtLength(pathLength);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
p1 = path.getPointAtLength(endLen);
|
|
254
|
+
}
|
|
255
|
+
if (!p1)
|
|
256
|
+
break;
|
|
257
|
+
const rotation = Math.atan2(p1.y - p0.y, p1.x - p0.x);
|
|
258
|
+
glyphs.push({
|
|
259
|
+
char: g.text,
|
|
260
|
+
x: p0.x,
|
|
261
|
+
y: p0.y,
|
|
262
|
+
rotation,
|
|
263
|
+
width: g.width,
|
|
264
|
+
style: g.style,
|
|
265
|
+
ascent: g.ascent,
|
|
266
|
+
descent: g.descent,
|
|
267
|
+
pathOffset: naturalOffset,
|
|
268
|
+
shaped: g.shaped,
|
|
269
|
+
});
|
|
270
|
+
offset = endLen + (g.style.letterSpacing || 0);
|
|
271
|
+
naturalOffset += g.width + (g.style.letterSpacing || 0);
|
|
272
|
+
}
|
|
273
|
+
const bounds = computeBounds(glyphs, textBaseline);
|
|
274
|
+
return {
|
|
275
|
+
glyphs,
|
|
276
|
+
textWidth,
|
|
277
|
+
pathLength,
|
|
278
|
+
lineHeight: maxLineHeight,
|
|
279
|
+
bounds,
|
|
280
|
+
textBaseline,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Bounding box of the rendered text: union of each glyph's
|
|
285
|
+
* `width × per-glyph line-height` cell, rotated by the glyph's tangent.
|
|
286
|
+
*
|
|
287
|
+
* The cell height is the per-glyph line-height (CSS line-height when set,
|
|
288
|
+
* else font-size — Polotno's chosen metric). It is distributed
|
|
289
|
+
* ASYMMETRICALLY above/below the baseline using the font's natural
|
|
290
|
+
* ascent/descent ratio, so cap-height + ascender area is covered and the
|
|
291
|
+
* cell doesn't waste pixels below an empty descender. For a typical font
|
|
292
|
+
* with ascent ~25 / descent ~7 / lineHeight 32, the cell extends ~25px
|
|
293
|
+
* above and ~7px below the baseline, matching the painted glyph extent.
|
|
294
|
+
*
|
|
295
|
+
* Empty layouts return `{ x: 0, y: 0, width: 0, height: 0 }`.
|
|
296
|
+
*/
|
|
297
|
+
function computeBounds(glyphs, textBaseline) {
|
|
298
|
+
if (glyphs.length === 0)
|
|
299
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
300
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
301
|
+
for (const g of glyphs) {
|
|
302
|
+
const lh = g.style.lineHeight > 0 ? g.style.lineHeight : g.style.fontSize;
|
|
303
|
+
// Distribute the lineHeight above/below the baseline by the font's
|
|
304
|
+
// natural ascent/descent ratio (CSS line-box half-leading semantics).
|
|
305
|
+
const fontHeight = g.ascent + g.descent;
|
|
306
|
+
const ascentShare = fontHeight > 0 ? g.ascent / fontHeight : 0.75;
|
|
307
|
+
const topFromBaseline = lh * ascentShare; // above baseline
|
|
308
|
+
const bottomFromBaseline = lh * (1 - ascentShare); // below baseline
|
|
309
|
+
// Shift by the local-y of the baseline so the cell stays correct under
|
|
310
|
+
// any textBaseline (e.g. 'middle' places the cell vertically centred
|
|
311
|
+
// on the path; 'top' moves the entire cell down).
|
|
312
|
+
const baseY = baselineLocalY(textBaseline, g.ascent, g.descent);
|
|
313
|
+
const top = baseY - topFromBaseline;
|
|
314
|
+
const bottom = baseY + bottomFromBaseline;
|
|
315
|
+
const c = Math.cos(g.rotation);
|
|
316
|
+
const s = Math.sin(g.rotation);
|
|
317
|
+
// Local cell: x in [0, width], y in [top, bottom]. Rotate + translate.
|
|
318
|
+
const corners = [
|
|
319
|
+
{ x: g.x + (-s) * top, y: g.y + c * top },
|
|
320
|
+
{ x: g.x + c * g.width + (-s) * top, y: g.y + s * g.width + c * top },
|
|
321
|
+
{ x: g.x + c * g.width + (-s) * bottom, y: g.y + s * g.width + c * bottom },
|
|
322
|
+
{ x: g.x + (-s) * bottom, y: g.y + c * bottom },
|
|
323
|
+
];
|
|
324
|
+
for (const p of corners) {
|
|
325
|
+
if (p.x < minX)
|
|
326
|
+
minX = p.x;
|
|
327
|
+
if (p.y < minY)
|
|
328
|
+
minY = p.y;
|
|
329
|
+
if (p.x > maxX)
|
|
330
|
+
maxX = p.x;
|
|
331
|
+
if (p.y > maxY)
|
|
332
|
+
maxY = p.y;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=glyph-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glyph-layout.js","sourceRoot":"","sources":["../../src/path/glyph-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAsF9C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,EAAgB,EAAE,MAAc,EAAE,OAAe;IAEjD,QAAQ,EAAE,EAAE,CAAC;QACX,KAAK,YAAY,CAAC,CAAE,OAAO,CAAC,CAAC;QAC7B,KAAK,QAAQ,CAAC,CAAM,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAClD,KAAK,KAAK,CAAC,CAAS,OAAO,MAAM,CAAC;QAClC,KAAK,QAAQ,CAAC,CAAM,OAAO,CAAC,OAAO,CAAC;QACpC,KAAK,SAAS,CAAC,CAAK,OAAO,MAAM,GAAG,GAAG,CAAC;QACxC,KAAK,aAAa,CAAC,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAgB;IAC9C,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,SAAS,IAAI,CAAC,IAAgB,EAAE,YAAqB;QACnD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,KAAK,KAAK,IAAI,YAAY,CAAC;QAC3D,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACjD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAClB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,2EAA2E;AAC3E,0EAA0E;AAC1E,8BAA8B;AAC9B,uCAAuC;AACvC,mDAAmD;AACnD,iDAAiD;AACjD,6EAA6E;AAC7E,uDAAuD;AACvD,8CAA8C;AAC9C,wBAAwB;AACxB,qCAAqC;AACrC,+CAA+C;AAC/C,2EAA2E;AAC3E,yEAAyE;AACzE,0EAA0E;AAC1E,uEAAuE;AACvE,8CAA8C;AAC9C,MAAM,UAAU,GAAG,IAAI,MAAM,CAC3B,GAAG;IACD,iBAAiB,GAAc,SAAS;IACxC,iBAAiB,GAAc,SAAS;IACxC,iBAAiB,GAAc,SAAS;IACxC,iBAAiB,GAAc,oBAAoB;IACnD,iBAAiB,GAAc,SAAS;IACxC,iBAAiB,GAAc,MAAM;IACrC,iBAAiB,GAAc,YAAY;IAC3C,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,oBAAoB;IACnD,iBAAiB,GAAc,oBAAoB;IACnD,iBAAiB,GAAc,aAAa;IAC5C,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,WAAW;IAC1C,iBAAiB,GAAc,WAAW;IAC1C,iBAAiB,GAAc,QAAQ;IACvC,iBAAiB,GAAc,QAAQ;IACvC,iBAAiB,GAAc,SAAS;IACxC,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,YAAY;IAC3C,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,OAAO;IACtC,iBAAiB,GAAc,MAAM;IACrC,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,UAAU;IACzC,iBAAiB,GAAc,QAAQ;IACvC,iBAAiB,GAAc,YAAY;IAC3C,iBAAiB,GAAc,4BAA4B;IAC3D,iBAAiB,GAAc,8BAA8B;IAC7D,iBAAiB,GAAc,8BAA8B;IAC/D,GAAG,CACJ,CAAC;AAEF,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC5B,CAAC;AAeD;;;;;;;;;;;GAWG;AACH,SAAS,mBAAmB,CAC1B,GAA6B,EAC7B,GAAY;IAEZ,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,uEAAuE;IACvE,uDAAuD;IACvD,GAAG,CAAC,aAAa,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,IAAW,CAAC;IAC/D,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IAE3D,kEAAkE;IAClE,qDAAqD;IACrD,MAAM,IAAI,GAA0D,EAAE,CAAC;IACvE,IAAI,gBAAgB,GAAG,EAAE,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC;QAC1B,IAAI,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChC,gBAAgB,IAAI,CAAC,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,IAAI,gBAAgB,EAAE,CAAC;gBACrB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;gBACpE,gBAAgB,GAAG,EAAE,CAAC;YACxB,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,IAAI,gBAAgB,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,wEAAwE;IACxE,+DAA+D;IAC/D,IAAI,GAAG,CAAC,GAAG;QAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAE5B,4DAA4D;IAC5D,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;QAC5C,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK;YACL,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,MAAM;YACN,OAAO;YACP,MAAM,EAAE,CAAC,CAAC,MAAM;SACjB,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAkB;IACnD,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,GAAG,KAAK,CAAC;IAE3D,kEAAkE;IAClE,yEAAyE;IACzE,2EAA2E;IAC3E,+DAA+D;IAC/D,MAAM,SAAS,GAAe,EAAE,CAAC;IACjC,IAAI,kBAAkB,GAAG,CAAC,CAAC;IAC3B,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,IAAI;YAAE,SAAS;QACxB,MAAM,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;QAChF,IAAI,EAAE,GAAG,aAAa;YAAE,aAAa,GAAG,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACrC,SAAS,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QAC7B,sEAAsE;QACtE,kBAAkB,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC;IACxD,CAAC;IAED,2EAA2E;IAC3E,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,SAAS,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,oDAAoD;IACpD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,SAAS,IAAI,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC;IACxE,CAAC;IAED,oEAAoE;IACpE,uEAAuE;IACvE,uEAAuE;IACvE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,kBAAkB,CAAC,CAAC;IAEjE,kDAAkD;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/B,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvB,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,CAAC;SAAM,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QAC7B,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC;IACpD,CAAC;SAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAC3D,IAAI,UAAU,GAAG,CAAC,IAAI,UAAU,GAAG,SAAS,EAAE,CAAC;YAC7C,aAAa,GAAG,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,UAAU,CAAC;QACxD,CAAC;IACH,CAAC;IAED,oBAAoB;IACpB,mCAAmC;IACnC,2EAA2E;IAC3E,iEAAiE;IACjE,mEAAmE;IACnE,0EAA0E;IAC1E,yEAAyE;IACzE,2BAA2B;IAC3B,MAAM,MAAM,GAAqB,EAAE,CAAC;IACpC,IAAI,MAAM,GAAG,WAAW,CAAC;IACzB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QACvB,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjE,MAAM,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACzC,IAAI,CAAC,EAAE;YAAE,MAAM;QAEf,IAAI,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;QACrC,IAAI,EAAgB,CAAC;QACrB,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;YACxB,0EAA0E;YAC1E,uEAAuE;YACvE,IAAI,MAAM,GAAG,UAAU,IAAI,YAAY,GAAG,GAAG,EAAE,CAAC;gBAC9C,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;YACzC,CAAC;iBAAM,CAAC;gBACN,MAAM;YACR,CAAC;QACH,CAAC;aAAM,CAAC;YACN,EAAE,GAAG,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;QACD,IAAI,CAAC,EAAE;YAAE,MAAM;QAEf,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,CAAC,EAAE,EAAE,CAAC,CAAC;YACP,QAAQ;YACR,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,UAAU,EAAE,aAAa;YACzB,MAAM,EAAE,CAAC,CAAC,MAAM;SACjB,CAAC,CAAC;QAEH,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC;QAC/C,aAAa,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnD,OAAO;QACL,MAAM;QACN,SAAS;QACT,UAAU;QACV,UAAU,EAAE,aAAa;QACzB,MAAM;QACN,YAAY;KACb,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,aAAa,CACpB,MAAwB,EACxB,YAA0B;IAE1B,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACpE,IAAI,IAAI,GAAG,QAAQ,EAAE,IAAI,GAAG,QAAQ,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC;IACzE,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;QAC1E,mEAAmE;QACnE,sEAAsE;QACtE,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC;QACxC,MAAM,WAAW,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;QAClE,MAAM,eAAe,GAAG,EAAE,GAAG,WAAW,CAAC,CAAQ,iBAAiB;QAClE,MAAM,kBAAkB,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,iBAAiB;QACpE,uEAAuE;QACvE,qEAAqE;QACrE,kDAAkD;QAClD,MAAM,KAAK,GAAG,cAAc,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,KAAK,GAAG,eAAe,CAAC;QACpC,MAAM,MAAM,GAAG,KAAK,GAAG,kBAAkB,CAAC;QAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC/B,uEAAuE;QACvE,MAAM,OAAO,GAAG;YACd,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE;YACzC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,GAAG,EAAE;YACrE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,EAAE;YAC3E,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SAChD,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI;gBAAE,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grapheme.d.ts","sourceRoot":"","sources":["../../src/path/grapheme.ts"],"names":[],"mappings":"AAwBA,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAUjD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a string into user-perceived characters (grapheme clusters).
|
|
3
|
+
* Uses Intl.Segmenter when available (handles ZWJ emoji sequences, regional
|
|
4
|
+
* indicators, combining marks). Falls back to a surrogate-pair-aware split
|
|
5
|
+
* for older environments.
|
|
6
|
+
*
|
|
7
|
+
* Equivalent in spirit to konva's stringToArray helper.
|
|
8
|
+
*/
|
|
9
|
+
let _segmenter;
|
|
10
|
+
function getSegmenter() {
|
|
11
|
+
if (_segmenter !== undefined)
|
|
12
|
+
return _segmenter;
|
|
13
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
14
|
+
try {
|
|
15
|
+
_segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
_segmenter = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
_segmenter = null;
|
|
23
|
+
}
|
|
24
|
+
return _segmenter;
|
|
25
|
+
}
|
|
26
|
+
export function stringToArray(s) {
|
|
27
|
+
if (!s)
|
|
28
|
+
return [];
|
|
29
|
+
const seg = getSegmenter();
|
|
30
|
+
if (seg) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const piece of seg.segment(s))
|
|
33
|
+
out.push(piece.segment);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
// Fallback: handle UTF-16 surrogate pairs but not full grapheme clusters.
|
|
37
|
+
return Array.from(s);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=grapheme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grapheme.js","sourceRoot":"","sources":["../../src/path/grapheme.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,IAAI,UAA6C,CAAC;AAElD,SAAS,YAAY;IACnB,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,IAAI,OAAO,IAAI,KAAK,WAAW,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QACxE,IAAI,CAAC;YACH,UAAU,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;SAAM,CAAC;QACN,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAS;IACrC,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5D,OAAO,GAAG,CAAC;IACb,CAAC;IACD,0EAA0E;IAC1E,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-tag/path — draw rich text along an SVG path on a 2D canvas.
|
|
3
|
+
*
|
|
4
|
+
* Reuses render-tag's HTML+CSS pipeline (parseHTML, resolveStylesFromCSS) and
|
|
5
|
+
* font primitives (applyFont) so styled spans, fonts, and colors come straight
|
|
6
|
+
* from the input HTML — no separate option surface.
|
|
7
|
+
*
|
|
8
|
+
* import { drawTextOnPath } from 'render-tag/path';
|
|
9
|
+
* drawTextOnPath({
|
|
10
|
+
* html: '<b>Hello</b> <span style="color:red">world</span>',
|
|
11
|
+
* path: 'M0,50 Q200,0 400,50',
|
|
12
|
+
* ctx,
|
|
13
|
+
* align: 'center',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* For finer control:
|
|
17
|
+
* const result = layoutTextOnPath({ html, path, align });
|
|
18
|
+
* drawTextOnPathLayout({ layout: result, ctx });
|
|
19
|
+
*
|
|
20
|
+
* Supported styles (matches the main renderer):
|
|
21
|
+
* font-family / size / weight / style / kerning
|
|
22
|
+
* color, -webkit-text-fill-color, -webkit-text-stroke (width + color), paint-order
|
|
23
|
+
* background-color (span backgrounds drawn as curve-following polygons)
|
|
24
|
+
* text-shadow (rotates with glyphs; multi-shadow supported)
|
|
25
|
+
* text-decoration: underline / line-through / overline (solid/dotted/dashed/double/wavy)
|
|
26
|
+
* text-decoration-color, text-decoration-style
|
|
27
|
+
* background-clip:text + background-image:linear-gradient (gradient flows along the path)
|
|
28
|
+
* letter-spacing, direction: rtl, dir="rtl"
|
|
29
|
+
* Arabic / Hebrew / Indic / Thai / Khmer / Myanmar — shaped runs are
|
|
30
|
+
* rendered as a unit so cursive joining and reordering work correctly.
|
|
31
|
+
*/
|
|
32
|
+
import { type PathLike } from './svg-path.js';
|
|
33
|
+
import { type AlignMode, type GlyphPlacement, type TextBaseline } from './glyph-layout.js';
|
|
34
|
+
export type { PathLike, GlyphPlacement, AlignMode, TextBaseline };
|
|
35
|
+
export interface LayoutTextOnPathConfig {
|
|
36
|
+
/** Rich-text HTML (same dialect as render-tag's main API). */
|
|
37
|
+
html: string;
|
|
38
|
+
/** SVG path 'd' attribute string, or a PathLike implementation. */
|
|
39
|
+
path: string | PathLike;
|
|
40
|
+
/** Alignment of text along the path (default 'left'). */
|
|
41
|
+
align?: AlignMode;
|
|
42
|
+
/**
|
|
43
|
+
* Where the path runs relative to the rendered text. Default
|
|
44
|
+
* `'alphabetic'` (path = baseline, descenders drop below). Use `'middle'`
|
|
45
|
+
* for design-tool style "text centered on path" rendering. The choice
|
|
46
|
+
* also affects `bounds`.
|
|
47
|
+
*/
|
|
48
|
+
textBaseline?: TextBaseline;
|
|
49
|
+
/**
|
|
50
|
+
* Optional measurement context. If omitted, an offscreen canvas is created.
|
|
51
|
+
* Pass one to share font measurement caches with other render-tag calls.
|
|
52
|
+
*/
|
|
53
|
+
ctx?: CanvasRenderingContext2D;
|
|
54
|
+
}
|
|
55
|
+
export interface TextOnPathLayout {
|
|
56
|
+
/** Per-glyph (or per-shaped-run) placement records. */
|
|
57
|
+
glyphs: GlyphPlacement[];
|
|
58
|
+
/** Sum of per-glyph advances (the natural width of the rendered text). */
|
|
59
|
+
textWidth: number;
|
|
60
|
+
/** Total arc length of the path. */
|
|
61
|
+
pathLength: number;
|
|
62
|
+
/**
|
|
63
|
+
* Max line height across all segments. Useful as the thickness for a
|
|
64
|
+
* background ribbon polygon around curved text.
|
|
65
|
+
*/
|
|
66
|
+
lineHeight: number;
|
|
67
|
+
/**
|
|
68
|
+
* Visible bounding box of the rendered text in the layout's coordinate
|
|
69
|
+
* system (same shape as `DOMRect`). Computed as the union of each glyph's
|
|
70
|
+
* `width × per-glyph line-height` cell, rotated by the glyph's tangent.
|
|
71
|
+
*
|
|
72
|
+
* Consumer-facing: render-tag does not consume `bounds` itself. Use it to
|
|
73
|
+
* keep a parent element's width/height in sync with the rendered curved
|
|
74
|
+
* text without re-walking the glyphs.
|
|
75
|
+
*/
|
|
76
|
+
bounds: {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
};
|
|
82
|
+
/** The textBaseline mode the layout was computed with. */
|
|
83
|
+
textBaseline: TextBaseline;
|
|
84
|
+
}
|
|
85
|
+
export interface DrawTextOnPathLayoutConfig {
|
|
86
|
+
/** Result from layoutTextOnPath(). */
|
|
87
|
+
layout: TextOnPathLayout;
|
|
88
|
+
/** Destination 2D context. Saved+restored around the whole batch. */
|
|
89
|
+
ctx: CanvasRenderingContext2D;
|
|
90
|
+
}
|
|
91
|
+
export interface DrawTextOnPathConfig {
|
|
92
|
+
/** Rich-text HTML (same dialect as render-tag's main API). */
|
|
93
|
+
html: string;
|
|
94
|
+
/** SVG path 'd' attribute string, or a PathLike implementation. */
|
|
95
|
+
path: string | PathLike;
|
|
96
|
+
/** Destination 2D context. */
|
|
97
|
+
ctx: CanvasRenderingContext2D;
|
|
98
|
+
/** Alignment of text along the path (default 'left'). */
|
|
99
|
+
align?: AlignMode;
|
|
100
|
+
/** Where the path runs relative to the text (default 'alphabetic'). */
|
|
101
|
+
textBaseline?: TextBaseline;
|
|
102
|
+
}
|
|
103
|
+
export type DrawTextOnPathResult = TextOnPathLayout;
|
|
104
|
+
/**
|
|
105
|
+
* Compute glyph placements for rich text along a path, without drawing.
|
|
106
|
+
* Useful for inspection, hit-testing, or rendering the same layout multiple times.
|
|
107
|
+
* The HTML's CSS resolves against an infinite-width container, so wrapping
|
|
108
|
+
* does not happen — all text flows along the path as one logical line.
|
|
109
|
+
*
|
|
110
|
+
* The caller's ctx (when passed) has its `font`, `fontKerning`, and
|
|
111
|
+
* `letterSpacing` state saved+restored around the measurement work; nothing
|
|
112
|
+
* leaks to the caller.
|
|
113
|
+
*/
|
|
114
|
+
export declare function layoutTextOnPath(config: LayoutTextOnPathConfig): TextOnPathLayout;
|
|
115
|
+
/**
|
|
116
|
+
* Draw a pre-computed layout onto a canvas context.
|
|
117
|
+
*
|
|
118
|
+
* Rendering passes (matching CSS painting order):
|
|
119
|
+
* 1. Span backgrounds (background-color, curve-following polygons)
|
|
120
|
+
* 2. Text shadows (per glyph, multi-shadow supported, rotates with glyph)
|
|
121
|
+
* 3. Glyph fill + stroke (paint-order aware; gradient text via slicing)
|
|
122
|
+
* 4. Text decoration (underline / line-through / overline)
|
|
123
|
+
*
|
|
124
|
+
* ctx state is saved+restored around the entire batch — nothing leaks.
|
|
125
|
+
*/
|
|
126
|
+
export declare function drawTextOnPathLayout(config: DrawTextOnPathLayoutConfig): void;
|
|
127
|
+
/**
|
|
128
|
+
* Convenience: compute layout and draw in a single call.
|
|
129
|
+
*/
|
|
130
|
+
export declare function drawTextOnPath(config: DrawTextOnPathConfig): DrawTextOnPathResult;
|
|
131
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/path/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAWH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC;AAElE,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;OAGG;IACH,GAAG,CAAC,EAAE,wBAAwB,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,0DAA0D;IAC1D,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B;IACzC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,CAAC;IACzB,qEAAqE;IACrE,GAAG,EAAE,wBAAwB,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,8BAA8B;IAC9B,GAAG,EAAE,wBAAwB,CAAC;IAC9B,yDAAyD;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AAEpD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,gBAAgB,CAkBjF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,IAAI,CAa7E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAUjF"}
|