@weasel-js/text 1.11.0 → 1.12.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.
- package/dist/{chunk-HI4VEOYP.js → chunk-ADRQIIHS.js} +31 -31
- package/dist/chunk-ADRQIIHS.js.map +1 -0
- package/dist/index.d.ts +100 -30
- package/dist/index.js +215 -117
- package/dist/index.js.map +1 -1
- package/dist/{test-seams-BypGSlZj.d.ts → test-seams-B0BDm36M.d.ts} +11 -8
- package/dist/test-seams.d.ts +1 -1
- package/dist/test-seams.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-HI4VEOYP.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextPaint } from './test-seams-
|
|
2
|
-
export { B as BidiAnalysis, d as BidiReordering, e as BidiResolver, D as DEFAULT_TEXT_STYLE, F as FontVariantCaps,
|
|
3
|
-
import { FaceRuleMetrics, FaceMetrics, FontStyle, BmFontChar } from '@weasel-js/font';
|
|
1
|
+
import { S as StyledRun, T as TextStyle, L as LaidOutRuns, R as ResolvedTextStyle, a as ResolvedRun, b as LayoutRunsOpts, c as TextPaint } from './test-seams-B0BDm36M.js';
|
|
2
|
+
export { B as BidiAnalysis, d as BidiReordering, e as BidiResolver, D as DEFAULT_TEXT_FILL, f as DEFAULT_TEXT_STYLE, F as FontVariantCaps, g as LAYOUT_CACHE_STRUCTURAL_LIMIT, h as LAYOUT_CACHE_VARIANT_LIMIT, i as LaidOutCell, j as LaidOutLineBox, M as MARKDOWN_RUN_GRAMMAR, P as PlacedGlyph, k as RunFlag, l as RunGrammar, m as RunMarker, n as RunSourceMap, o as SCRIPT_METRICS, p as SMALL_CAPS_SCALE, q as ScriptPreset, r as SmallCapsText, s as TextAlign, t as TextDirection, u as TextTransform, v as TransformedRunText, w as cachedLayoutRuns, x as fontString, y as isBoldWeight, z as isSmallCapsLetter, A as layoutRuns, C as markdownToRuns, E as numericWeight, G as paragraphDirection, H as registerBidi, I as resolveAlign, J as resolveRunFace, K as resolveRuns, N as resolveTextStyle, O as runsToMarkdown, Q as runsToPlainText, U as scriptMetrics, V as scriptMetricsFor, W as smallCapsScale, X as smallCapsScaleFor, Y as smallCapsText, Z as toRuns, _ as transformRunTexts } from './test-seams-B0BDm36M.js';
|
|
3
|
+
import { FaceRuleMetrics, FaceMetrics, FontStyle, BmFontChar, ResolveResult } from '@weasel-js/font';
|
|
4
4
|
import { Fill, Stroke } from '@weasel-js/paint';
|
|
5
|
-
import { Rect,
|
|
5
|
+
import { Rect, PolygonPath, PathInWorldPose } from '@weasel-js/geom';
|
|
6
6
|
import { Mixed } from '@weasel-js/registry';
|
|
7
7
|
export { MIXED, Mixed } from '@weasel-js/registry';
|
|
8
8
|
|
|
@@ -162,9 +162,10 @@ interface LaidOutQuad {
|
|
|
162
162
|
* One glyph the renderer should tessellate rather than sample.
|
|
163
163
|
*
|
|
164
164
|
* `d` is em-space SVG path data — 1 unit is 1 em, y grows down, origin on the
|
|
165
|
-
* baseline at the pen. Placing it is therefore a uniform scale by `scale`
|
|
165
|
+
* baseline at the pen — with any synthetic bold already grown into it. Placing it is therefore a uniform scale by `scale`
|
|
166
166
|
* followed by a translate to `(x, baselineY)`. `key` identifies the cached
|
|
167
|
-
* tessellation — face identity plus codepoint
|
|
167
|
+
* tessellation — face identity plus codepoint, plus the growth of a synthetic
|
|
168
|
+
* bold.
|
|
168
169
|
*/
|
|
169
170
|
interface LaidOutOutlineGlyph {
|
|
170
171
|
d: string;
|
|
@@ -189,7 +190,8 @@ interface LaidOutGroup {
|
|
|
189
190
|
/** Resolved variant — matches the registered atlas and the texture-cache key. */
|
|
190
191
|
weight: number;
|
|
191
192
|
style: FontStyle;
|
|
192
|
-
/** Gap between the request and the resolved match, for the renderer to fake.
|
|
193
|
+
/** Gap between the request and the resolved match, for the renderer to fake.
|
|
194
|
+
* An `'outline'` group's bold is already grown into its glyphs. */
|
|
193
195
|
synthetic: {
|
|
194
196
|
bold: boolean;
|
|
195
197
|
italic: boolean;
|
|
@@ -209,7 +211,7 @@ interface LaidOutGroup {
|
|
|
209
211
|
/** Outline painted over the glyphs. Only ever set on an `'outline'` group —
|
|
210
212
|
* the SDF tiers have no geometry to stroke, which is why a stroke pulls its
|
|
211
213
|
* run onto the outline tier at any size. A run the tier cannot serve at all
|
|
212
|
-
* (no registered outlines
|
|
214
|
+
* (no registered outlines) stays on the atlas, unstroked. */
|
|
213
215
|
stroke?: Stroke;
|
|
214
216
|
/** Textured glyph quads. Always empty for an `'outline'` group. */
|
|
215
217
|
quads: LaidOutQuad[];
|
|
@@ -659,6 +661,62 @@ declare function setFlagOverRange(runs: readonly StyledRun[], style: TextStyle,
|
|
|
659
661
|
*/
|
|
660
662
|
declare const SYNTHETIC_ITALIC_RADIANS = 0.2094;
|
|
661
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Synthetic bold as geometry. The renderer fakes a missing bold face by
|
|
666
|
+
* dropping the distance-field threshold, which moves every edge of the glyph
|
|
667
|
+
* outward by `syntheticBoldGrowth`; outlining it is the glyph's own outline
|
|
668
|
+
* grown by that distance.
|
|
669
|
+
*/
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* How far synthetic bold grows a glyph served as `resolved`, in em per side,
|
|
673
|
+
* and how its corners turn; `null` when the renderer draws it unshifted.
|
|
674
|
+
* An MSDF atlas keeps sharp corners, so its shifted edge meets in a miter; a
|
|
675
|
+
* canvas bake is a true distance field, whose shifted edge rounds them.
|
|
676
|
+
*/
|
|
677
|
+
declare function syntheticBoldOffset(resolved: ResolveResult): {
|
|
678
|
+
growth: number;
|
|
679
|
+
join: 'miter' | 'round';
|
|
680
|
+
} | null;
|
|
681
|
+
/**
|
|
682
|
+
* {@link emboldenGlyph} for the renderer's outline tier, which takes outline
|
|
683
|
+
* data: `d` grown as the field draws it, and a tag naming the growth for the
|
|
684
|
+
* glyph's tessellation key. `null` when the renderer draws `resolved` unshifted.
|
|
685
|
+
*/
|
|
686
|
+
declare function emboldenGlyphD(d: string, resolved: ResolveResult): {
|
|
687
|
+
d: string;
|
|
688
|
+
tag: string;
|
|
689
|
+
} | null;
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* The per-glyph half of text-as-geometry, shared by `textToPath` and
|
|
693
|
+
* `textClusters`: checking a run's face can give outlines, finding a placed
|
|
694
|
+
* glyph's outline, and setting it where the renderer's outline tier puts it.
|
|
695
|
+
*/
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Why a text node has no outline geometry to give:
|
|
699
|
+
*
|
|
700
|
+
* - `'no-outlines'` — the face that serves it has no outlines registered
|
|
701
|
+
* (`registerFontOutlines`), so there is no geometry to read.
|
|
702
|
+
* - `'outlines-loading'` — registered but not parsed yet. `loadTextOutlines`
|
|
703
|
+
* waits for it; the call has already started the load.
|
|
704
|
+
* - `'outlines-failed'` — registered, and the bytes did not load or parse.
|
|
705
|
+
* - `'color-glyph'` — the text holds an emoji, which is a picture in its own
|
|
706
|
+
* colors with no outline. Nothing a caller registers changes that.
|
|
707
|
+
*/
|
|
708
|
+
type TextOutlinesFailure = 'no-outlines' | 'color-glyph' | 'outlines-loading' | 'outlines-failed';
|
|
709
|
+
/** Thrown by `textToPath`, `textToPathsByPaint` and `textClusters` when some
|
|
710
|
+
* run of the text has no outline geometry. Names the run's requested face and
|
|
711
|
+
* the function that was asked. */
|
|
712
|
+
declare class TextOutlinesError extends Error {
|
|
713
|
+
readonly reason: TextOutlinesFailure;
|
|
714
|
+
readonly family: string;
|
|
715
|
+
readonly weight: number;
|
|
716
|
+
readonly style: FontStyle;
|
|
717
|
+
constructor(reason: TextOutlinesFailure, family: string, weight: number, style: FontStyle, detail: string, caller?: string);
|
|
718
|
+
}
|
|
719
|
+
|
|
662
720
|
/**
|
|
663
721
|
* Text as path geometry: the glyph outlines a text node draws, placed in
|
|
664
722
|
* world space and composed into one `Path`. The glyphs are the ones
|
|
@@ -699,27 +757,6 @@ interface TextToPathOptions {
|
|
|
699
757
|
* against, as for `textCommandFromPose`. Default 1. */
|
|
700
758
|
scale?: number;
|
|
701
759
|
}
|
|
702
|
-
/**
|
|
703
|
-
* Why a text node has no outline geometry to give:
|
|
704
|
-
*
|
|
705
|
-
* - `'no-outlines'` — the face that serves it has no outlines registered
|
|
706
|
-
* (`registerFontOutlines`), so there is no geometry to read.
|
|
707
|
-
* - `'outlines-loading'` — registered but not parsed yet. `loadTextOutlines`
|
|
708
|
-
* waits for it; the call has already started the load.
|
|
709
|
-
* - `'outlines-failed'` — registered, and the bytes did not load or parse.
|
|
710
|
-
* - `'color-glyph'` — the text holds an emoji, which is a picture in its own
|
|
711
|
-
* colors with no outline. Nothing a caller registers changes that.
|
|
712
|
-
*/
|
|
713
|
-
type TextOutlinesFailure = 'no-outlines' | 'color-glyph' | 'outlines-loading' | 'outlines-failed';
|
|
714
|
-
/** Thrown by {@link textToPath} when some run of the text has no outline
|
|
715
|
-
* geometry. Names the run's requested face. */
|
|
716
|
-
declare class TextOutlinesError extends Error {
|
|
717
|
-
readonly reason: TextOutlinesFailure;
|
|
718
|
-
readonly family: string;
|
|
719
|
-
readonly weight: number;
|
|
720
|
-
readonly style: FontStyle;
|
|
721
|
-
constructor(reason: TextOutlinesFailure, family: string, weight: number, style: FontStyle, detail: string);
|
|
722
|
-
}
|
|
723
760
|
/**
|
|
724
761
|
* The outline geometry of a text node, in world space: every glyph and every
|
|
725
762
|
* underline, strikethrough and overline rule it draws, as one `'nonzero'`
|
|
@@ -752,4 +789,37 @@ declare function textToPathsByPaint(data: TextOutlineSource, pose: PathInWorldPo
|
|
|
752
789
|
*/
|
|
753
790
|
declare function loadTextOutlines(data: TextOutlineSource, opts?: TextToPathOptions): Promise<void>;
|
|
754
791
|
|
|
755
|
-
|
|
792
|
+
/** One grapheme cluster of a text line: its slot and its outlines. */
|
|
793
|
+
interface TextCluster {
|
|
794
|
+
/** Its slot's leading edge, from the line's left, in visual order. */
|
|
795
|
+
x: number;
|
|
796
|
+
/** The slot's width: its cells' advances, kerning and tracking included. */
|
|
797
|
+
advance: number;
|
|
798
|
+
/** Outlines with the slot's left edge at x = 0 and the baseline at y = 0, y down, one per paint; empty for a
|
|
799
|
+
* space or a color glyph. */
|
|
800
|
+
paths: TextPaintPath[];
|
|
801
|
+
/** Height of the x-height's middle above the baseline. */
|
|
802
|
+
middle: number;
|
|
803
|
+
}
|
|
804
|
+
/** Options for {@link textClusters}. */
|
|
805
|
+
interface TextClustersOptions extends TextToPathOptions {
|
|
806
|
+
/** The node's box width, which a `wrap` style wraps at and `align` aligns
|
|
807
|
+
* in, as the node's own layout does. Default `Infinity`: one line per
|
|
808
|
+
* paragraph. */
|
|
809
|
+
width?: number;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* The first laid line of `data` as its grapheme clusters, in visual order,
|
|
813
|
+
* laid at the node's box origin with no pose applied; the lines after it are
|
|
814
|
+
* dropped. The slots tile the
|
|
815
|
+
* line: each cluster's `advance` runs to the next one's `x`, so the kerning
|
|
816
|
+
* between two clusters belongs to the left one. Decoration rules are left out.
|
|
817
|
+
*
|
|
818
|
+
* Throws `TextOutlinesError` as `textToPath` does, except that a color
|
|
819
|
+
* glyph, or one drawn by a fallback face with no outlines, keeps its slot
|
|
820
|
+
* with no `paths`. Only a code point that inks its slot is drawn, so a color
|
|
821
|
+
* cluster's leading code points stay blank too.
|
|
822
|
+
*/
|
|
823
|
+
declare function textClusters(data: TextOutlineSource, opts?: TextClustersOptions): TextCluster[];
|
|
824
|
+
|
|
825
|
+
export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_DECORATION_METRICS, type DecorationKind, type FaceMetricsFn, type FitTextPoseOptions, type FlagKey, type LabelOptions, type LaidOutDecoration, type LaidOutGroup, type LaidOutOutlineGlyph, type LaidOutQuad, LaidOutRuns, type LayoutLine, type LayoutResult, LayoutRunsOpts, type MarkdownFontOptions, type MeasureFn, type MeasureTextBoundsOpts, type MeasuredSpan, type MeasuredText, NO_BREAK, type PaintRunsOpts, type PaintedRuns, type PointInTextPoseOpts, type PositionedRun, type RangeStyle, ResolvedRun, ResolvedTextStyle, type RunStylePatch, SYNTHETIC_ITALIC_RADIANS, type SetFlagResult, type StyleKey, StyledRun, type TextCluster, type TextClustersOptions, type TextLineBoxesOpts, type TextNodeSource, type TextOutlineSource, TextOutlinesError, type TextOutlinesFailure, TextPaint, type TextPaintPath, type TextPose, type TextPoseLayout, type TextPoseLayoutInput, type TextRenderer, TextStyle, type TextToPathOptions, type TextVerticalAlign, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, emboldenGlyphD, fitTextPose, isHardLineBreak, isTextData, layoutMarkdown, layoutTextPose, lineBreakOpportunities, loadTextOutlines, measureText, measureTextBounds, measuredWidth, nodeHasFlag, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, runsCarryStyling, setFlagOverRange, styleAtRange, supersededKey, syntheticBoldOffset, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, unboldPatch, verticalAlignOffset };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs
|
|
2
|
-
export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-
|
|
1
|
+
import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, layoutRuns, tabAdvance, widthWithTabs } from './chunk-ADRQIIHS.js';
|
|
2
|
+
export { BREAK_ALLOWED, BREAK_MANDATORY, DEFAULT_TEXT_FILL, DEFAULT_TEXT_STYLE, LAYOUT_CACHE_STRUCTURAL_LIMIT, LAYOUT_CACHE_VARIANT_LIMIT, NO_BREAK, cachedLayoutRuns, fontString, isHardLineBreak, layoutRuns, lineBreakOpportunities, paragraphDirection, registerBidi, resolveAlign, resolveTextStyle } from './chunk-ADRQIIHS.js';
|
|
3
3
|
import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
|
|
4
|
-
import { faceMetricsOf, resolveFontVariant, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName
|
|
4
|
+
import { faceMetricsOf, resolveFontVariant, syntheticBoldGrowth, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
|
|
5
|
+
import { pathToD, pathFromD, mergeRegion, growRegion, poseFrameOf, transformPath, PATH_M, PATH_L, PATH_Z, pathSignedArea, reversePath } from '@weasel-js/geom';
|
|
5
6
|
import { MIXED, deepEqual } from '@weasel-js/registry';
|
|
6
7
|
export { MIXED } from '@weasel-js/registry';
|
|
7
|
-
import { transformPath, poseFrameOf, growRegion, pathFromD, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
|
|
8
8
|
|
|
9
9
|
// src/runs.ts
|
|
10
10
|
function toRuns(input) {
|
|
@@ -413,8 +413,8 @@ function lineDecorations(glyphs, runs, faceOf, paintOf) {
|
|
|
413
413
|
};
|
|
414
414
|
for (const g of glyphs) {
|
|
415
415
|
const style = runs[g.run];
|
|
416
|
-
const paint = paintOf(g.run);
|
|
417
|
-
if (paint === null
|
|
416
|
+
const paint = style.underline || style.strikethrough || style.overline ? paintOf(g.run) : null;
|
|
417
|
+
if (paint === null) {
|
|
418
418
|
close();
|
|
419
419
|
continue;
|
|
420
420
|
}
|
|
@@ -465,13 +465,61 @@ function layoutTextPose(pose, scale = 1) {
|
|
|
465
465
|
y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
|
|
466
466
|
};
|
|
467
467
|
}
|
|
468
|
+
var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
|
|
469
|
+
var CACHE_LIMIT = 4096;
|
|
470
|
+
var cache = /* @__PURE__ */ new Map();
|
|
471
|
+
var dCache = /* @__PURE__ */ new Map();
|
|
472
|
+
function syntheticBoldOffset(resolved2) {
|
|
473
|
+
if (!resolved2.synthetic.bold) return null;
|
|
474
|
+
const { source } = resolved2;
|
|
475
|
+
if (source !== "atlas" && source !== "canvas") return null;
|
|
476
|
+
const { family, weight, style } = resolved2.resolved;
|
|
477
|
+
const growth = syntheticBoldGrowth(source, family, weight, style);
|
|
478
|
+
if (growth === void 0 || !(growth > 0)) return null;
|
|
479
|
+
return { growth, join: source === "atlas" ? "miter" : "round" };
|
|
480
|
+
}
|
|
481
|
+
function emboldenGlyph(em, d, resolved2) {
|
|
482
|
+
const offset = syntheticBoldOffset(resolved2);
|
|
483
|
+
if (offset === null) return em;
|
|
484
|
+
const key = `${offset.growth}|${offset.join}|${d}`;
|
|
485
|
+
let grown = cache.get(key);
|
|
486
|
+
if (grown) return grown;
|
|
487
|
+
grown = mergeRegion(
|
|
488
|
+
growRegion(em, offset.growth, { join: offset.join, tolerance: SYNTHETIC_BOLD_TOLERANCE }),
|
|
489
|
+
{ tolerance: SYNTHETIC_BOLD_TOLERANCE }
|
|
490
|
+
);
|
|
491
|
+
if (cache.size >= CACHE_LIMIT) cache.clear();
|
|
492
|
+
cache.set(key, grown);
|
|
493
|
+
return grown;
|
|
494
|
+
}
|
|
495
|
+
function emboldenGlyphD(d, resolved2) {
|
|
496
|
+
const offset = syntheticBoldOffset(resolved2);
|
|
497
|
+
if (offset === null) return null;
|
|
498
|
+
const tag = `bold:${offset.growth}:${offset.join}`;
|
|
499
|
+
const key = `${tag}|${d}`;
|
|
500
|
+
let grown = dCache.get(key);
|
|
501
|
+
if (grown === void 0) {
|
|
502
|
+
grown = pathToD(emboldenGlyph(pathFromD(d), d, resolved2));
|
|
503
|
+
if (dCache.size >= CACHE_LIMIT) dCache.clear();
|
|
504
|
+
dCache.set(key, grown);
|
|
505
|
+
}
|
|
506
|
+
return { d: grown, tag };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/layout/paintRuns.ts
|
|
468
510
|
function glyphOutlineD(g, run, minSize) {
|
|
469
|
-
|
|
511
|
+
const outlineOnly = g.resolved.source === "outline";
|
|
512
|
+
if (!outlineOnly) {
|
|
470
513
|
if (minSize === void 0) return null;
|
|
471
514
|
if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
|
|
472
|
-
if (g.resolved.synthetic.bold) return null;
|
|
473
515
|
}
|
|
474
|
-
|
|
516
|
+
const d = faceOutlineD(g);
|
|
517
|
+
if (d === null) return null;
|
|
518
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
519
|
+
const key = `${family}|${weight}|${style}|${g.cp}`;
|
|
520
|
+
if (outlineOnly || !g.resolved.synthetic.bold) return { d, key };
|
|
521
|
+
const bold = emboldenGlyphD(d, g.resolved);
|
|
522
|
+
return bold && { d: bold.d, key: `${key}|${bold.tag}` };
|
|
475
523
|
}
|
|
476
524
|
function faceOutlineD(g) {
|
|
477
525
|
if (g.glyph?.color) return null;
|
|
@@ -501,10 +549,13 @@ function runFaces(runs) {
|
|
|
501
549
|
const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
|
|
502
550
|
return (run) => faces[run];
|
|
503
551
|
}
|
|
504
|
-
function groupFor(groups, g, run, source) {
|
|
552
|
+
function groupFor(groups, g, run, source, last) {
|
|
505
553
|
const { family, weight, style } = g.resolved.resolved;
|
|
506
554
|
const { synthetic } = g.resolved;
|
|
507
555
|
const page = source === "outline" ? 0 : g.glyph.page;
|
|
556
|
+
if (last.group !== null && last.run === g.run && last.source === source && last.page === page && last.family === family && last.weight === weight && last.style === style && last.bold === synthetic.bold && last.italic === synthetic.italic) {
|
|
557
|
+
return last.group;
|
|
558
|
+
}
|
|
508
559
|
const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
|
|
509
560
|
const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
|
|
510
561
|
let group = groups.get(key);
|
|
@@ -523,23 +574,41 @@ function groupFor(groups, g, run, source) {
|
|
|
523
574
|
};
|
|
524
575
|
groups.set(key, group);
|
|
525
576
|
}
|
|
577
|
+
last.group = group;
|
|
578
|
+
last.run = g.run;
|
|
579
|
+
last.family = family;
|
|
580
|
+
last.weight = weight;
|
|
581
|
+
last.style = style;
|
|
582
|
+
last.bold = synthetic.bold;
|
|
583
|
+
last.italic = synthetic.italic;
|
|
584
|
+
last.source = source;
|
|
585
|
+
last.page = page;
|
|
526
586
|
return group;
|
|
527
587
|
}
|
|
528
588
|
function paintRuns(laid, runs, opts = {}) {
|
|
529
589
|
const groups = /* @__PURE__ */ new Map();
|
|
590
|
+
const last = {
|
|
591
|
+
group: null,
|
|
592
|
+
run: -1,
|
|
593
|
+
family: "",
|
|
594
|
+
weight: 0,
|
|
595
|
+
style: "normal",
|
|
596
|
+
bold: false,
|
|
597
|
+
italic: false,
|
|
598
|
+
source: "atlas",
|
|
599
|
+
page: 0
|
|
600
|
+
};
|
|
530
601
|
const decorations = [];
|
|
531
602
|
const faceOf = runFaces(runs);
|
|
532
603
|
const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
|
|
533
604
|
for (const line of laid.lines) {
|
|
534
605
|
for (const g of line.glyphs) {
|
|
535
606
|
const run = runs[g.run];
|
|
536
|
-
const
|
|
537
|
-
if (!fillHasInk(run.fill) && !(
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
d,
|
|
542
|
-
key: `${family}|${weight}|${style}|${g.cp}`,
|
|
607
|
+
const outline = glyphOutlineD(g, run, opts.outlineMinSize);
|
|
608
|
+
if (!fillHasInk(run.fill) && !(outline !== null && strokeHasInk(run.stroke))) continue;
|
|
609
|
+
if (outline !== null) {
|
|
610
|
+
groupFor(groups, g, run, "outline", last).glyphs.push({
|
|
611
|
+
...outline,
|
|
543
612
|
x: g.x,
|
|
544
613
|
baselineY: g.baselineY,
|
|
545
614
|
scale: g.fontSize
|
|
@@ -548,8 +617,8 @@ function paintRuns(laid, runs, opts = {}) {
|
|
|
548
617
|
}
|
|
549
618
|
const quad = glyphQuad(g);
|
|
550
619
|
if (!quad) continue;
|
|
551
|
-
if (g.glyph?.color) groupFor(groups, g, run, "color").quads.push({ ...quad, colorGlyph: g.glyph });
|
|
552
|
-
else groupFor(groups, g, run, g.resolved.source).quads.push(quad);
|
|
620
|
+
if (g.glyph?.color) groupFor(groups, g, run, "color", last).quads.push({ ...quad, colorGlyph: g.glyph });
|
|
621
|
+
else groupFor(groups, g, run, g.resolved.source, last).quads.push(quad);
|
|
553
622
|
}
|
|
554
623
|
for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
|
|
555
624
|
const { run, ...rect } = rule;
|
|
@@ -1233,41 +1302,13 @@ function effectiveRangeStyle(range, style, paint) {
|
|
|
1233
1302
|
|
|
1234
1303
|
// src/syntheticItalic.ts
|
|
1235
1304
|
var SYNTHETIC_ITALIC_RADIANS = 0.2094;
|
|
1236
|
-
var SYNTHETIC_BOLD_TOLERANCE = 5e-4;
|
|
1237
|
-
var CACHE_LIMIT = 4096;
|
|
1238
|
-
var cache = /* @__PURE__ */ new Map();
|
|
1239
|
-
function syntheticBoldOffset(resolved2) {
|
|
1240
|
-
if (!resolved2.synthetic.bold) return null;
|
|
1241
|
-
const { source } = resolved2;
|
|
1242
|
-
if (source !== "atlas" && source !== "canvas") return null;
|
|
1243
|
-
const { family, weight, style } = resolved2.resolved;
|
|
1244
|
-
const growth = syntheticBoldGrowth(source, family, weight, style);
|
|
1245
|
-
if (growth === void 0 || !(growth > 0)) return null;
|
|
1246
|
-
return { growth, join: source === "atlas" ? "miter" : "round" };
|
|
1247
|
-
}
|
|
1248
|
-
function emboldenGlyph(em, d, resolved2) {
|
|
1249
|
-
const offset = syntheticBoldOffset(resolved2);
|
|
1250
|
-
if (offset === null) return em;
|
|
1251
|
-
const key = `${offset.growth}|${offset.join}|${d}`;
|
|
1252
|
-
let grown = cache.get(key);
|
|
1253
|
-
if (grown) return grown;
|
|
1254
|
-
grown = growRegion(em, offset.growth, {
|
|
1255
|
-
join: offset.join,
|
|
1256
|
-
tolerance: SYNTHETIC_BOLD_TOLERANCE
|
|
1257
|
-
});
|
|
1258
|
-
if (cache.size >= CACHE_LIMIT) cache.clear();
|
|
1259
|
-
cache.set(key, grown);
|
|
1260
|
-
return grown;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// src/textToPath.ts
|
|
1264
1305
|
var TextOutlinesError = class extends Error {
|
|
1265
1306
|
reason;
|
|
1266
1307
|
family;
|
|
1267
1308
|
weight;
|
|
1268
1309
|
style;
|
|
1269
|
-
constructor(reason, family, weight, style, detail) {
|
|
1270
|
-
super(
|
|
1310
|
+
constructor(reason, family, weight, style, detail, caller = "textToPath") {
|
|
1311
|
+
super(`${caller}: "${family}" ${weight}/${style} \u2014 ${detail}`);
|
|
1271
1312
|
this.name = "TextOutlinesError";
|
|
1272
1313
|
this.reason = reason;
|
|
1273
1314
|
this.family = family;
|
|
@@ -1275,22 +1316,10 @@ var TextOutlinesError = class extends Error {
|
|
|
1275
1316
|
this.style = style;
|
|
1276
1317
|
}
|
|
1277
1318
|
};
|
|
1278
|
-
function
|
|
1279
|
-
const out = /* @__PURE__ */ new Map();
|
|
1280
|
-
const add = (family, weight, style) => {
|
|
1281
|
-
out.set(`${family}|${weight}|${style}`, { family, weight, style });
|
|
1282
|
-
};
|
|
1283
|
-
for (const r of runs) {
|
|
1284
|
-
add(r.fontFamily, r.fontWeight, r.fontStyle);
|
|
1285
|
-
const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
|
|
1286
|
-
add(family, weight, style);
|
|
1287
|
-
}
|
|
1288
|
-
return [...out.values()];
|
|
1289
|
-
}
|
|
1290
|
-
function assertOutlinable(run) {
|
|
1319
|
+
function assertOutlinable(run, caller) {
|
|
1291
1320
|
const { fontFamily, fontWeight, fontStyle } = run;
|
|
1292
1321
|
const fail = (reason, detail) => {
|
|
1293
|
-
throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail);
|
|
1322
|
+
throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail, caller);
|
|
1294
1323
|
};
|
|
1295
1324
|
const requested = outlineStatus(fontFamily, fontWeight, fontStyle);
|
|
1296
1325
|
if (requested === "idle" || requested === "loading") {
|
|
@@ -1312,6 +1341,21 @@ function assertOutlinable(run) {
|
|
|
1312
1341
|
fail("outlines-failed", `the outlines registered for ${family} ${weight}/${style} failed to load.`);
|
|
1313
1342
|
}
|
|
1314
1343
|
}
|
|
1344
|
+
function paintsOf(runs) {
|
|
1345
|
+
const paints = [];
|
|
1346
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
1347
|
+
const paintOf = runs.map((run) => {
|
|
1348
|
+
const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
|
|
1349
|
+
let i = indexOf.get(key);
|
|
1350
|
+
if (i === void 0) {
|
|
1351
|
+
i = paints.length;
|
|
1352
|
+
indexOf.set(key, i);
|
|
1353
|
+
paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
|
|
1354
|
+
}
|
|
1355
|
+
return i;
|
|
1356
|
+
});
|
|
1357
|
+
return { paints, paintOf };
|
|
1358
|
+
}
|
|
1315
1359
|
var EM_CACHE_LIMIT = 4096;
|
|
1316
1360
|
var emCache = /* @__PURE__ */ new Map();
|
|
1317
1361
|
function emGlyph(d) {
|
|
@@ -1323,6 +1367,37 @@ function emGlyph(d) {
|
|
|
1323
1367
|
emCache.set(d, p);
|
|
1324
1368
|
return p;
|
|
1325
1369
|
}
|
|
1370
|
+
function placedGlyphOutline(g, dx, dy, caller, blankFallback = false) {
|
|
1371
|
+
const d = faceOutlineD(g);
|
|
1372
|
+
if (d === null) {
|
|
1373
|
+
if (!blankFallback && glyphQuad(g) !== null) {
|
|
1374
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
1375
|
+
throw new TextOutlinesError(
|
|
1376
|
+
"no-outlines",
|
|
1377
|
+
family,
|
|
1378
|
+
weight,
|
|
1379
|
+
style,
|
|
1380
|
+
"a fallback face serving some of the text's characters has no outlines registered.",
|
|
1381
|
+
caller
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
const s = g.fontSize;
|
|
1387
|
+
const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
|
|
1388
|
+
const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
|
|
1389
|
+
return transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m);
|
|
1390
|
+
}
|
|
1391
|
+
function colorGlyphError(run, caller) {
|
|
1392
|
+
return new TextOutlinesError(
|
|
1393
|
+
"color-glyph",
|
|
1394
|
+
run.fontFamily,
|
|
1395
|
+
run.fontWeight,
|
|
1396
|
+
run.fontStyle,
|
|
1397
|
+
"the text holds an emoji, which is drawn in its own colors and has no outline.",
|
|
1398
|
+
caller
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1326
1401
|
function concat(parts) {
|
|
1327
1402
|
let nc = 0, nk = 0;
|
|
1328
1403
|
for (const p of parts) {
|
|
@@ -1340,20 +1415,26 @@ function concat(parts) {
|
|
|
1340
1415
|
}
|
|
1341
1416
|
return { kind: "polygon", commands, coords, fillRule: "nonzero" };
|
|
1342
1417
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1418
|
+
|
|
1419
|
+
// src/textToPath.ts
|
|
1420
|
+
function facesOf(runs) {
|
|
1421
|
+
const out = /* @__PURE__ */ new Map();
|
|
1422
|
+
const add = (family, weight, style) => {
|
|
1423
|
+
out.set(`${family}|${weight}|${style}`, { family, weight, style });
|
|
1349
1424
|
};
|
|
1425
|
+
for (const r of runs) {
|
|
1426
|
+
add(r.fontFamily, r.fontWeight, r.fontStyle);
|
|
1427
|
+
const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
|
|
1428
|
+
add(family, weight, style);
|
|
1429
|
+
}
|
|
1430
|
+
return [...out.values()];
|
|
1350
1431
|
}
|
|
1351
|
-
function
|
|
1432
|
+
function outlineLayoutInput(data, box, opts, caller) {
|
|
1352
1433
|
const textPose = {
|
|
1353
|
-
x:
|
|
1354
|
-
y:
|
|
1355
|
-
width:
|
|
1356
|
-
height:
|
|
1434
|
+
x: box.x,
|
|
1435
|
+
y: box.y,
|
|
1436
|
+
width: box.width,
|
|
1437
|
+
height: box.height,
|
|
1357
1438
|
text: data.text,
|
|
1358
1439
|
runs: data.runs,
|
|
1359
1440
|
style: data.style,
|
|
@@ -1361,20 +1442,21 @@ function walkOutlines(data, pose, opts) {
|
|
|
1361
1442
|
fill: data.fill,
|
|
1362
1443
|
stroke: data.stroke
|
|
1363
1444
|
};
|
|
1364
|
-
const
|
|
1365
|
-
for (const run of runs) assertOutlinable(run);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
});
|
|
1445
|
+
const input = textPoseLayoutInput(textPose, opts.scale ?? 1);
|
|
1446
|
+
for (const run of input.runs) assertOutlinable(run, caller);
|
|
1447
|
+
return input;
|
|
1448
|
+
}
|
|
1449
|
+
function ruleContour(x0, y0, x1, y1) {
|
|
1450
|
+
return {
|
|
1451
|
+
kind: "polygon",
|
|
1452
|
+
commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
|
|
1453
|
+
coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
|
|
1454
|
+
fillRule: "nonzero"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function walkOutlines(data, pose, opts) {
|
|
1458
|
+
const { runs, opts: layoutOpts } = outlineLayoutInput(data, pose, opts, "textToPath");
|
|
1459
|
+
const { paints, paintOf } = paintsOf(runs);
|
|
1378
1460
|
const laid = layoutRuns(runs, layoutOpts);
|
|
1379
1461
|
const dx = pose.x;
|
|
1380
1462
|
const dy = pose.y + verticalAlignOffset(data.verticalAlign, pose.height, laid.bounds.height);
|
|
@@ -1383,34 +1465,9 @@ function walkOutlines(data, pose, opts) {
|
|
|
1383
1465
|
const faceOf = runFaces(runs);
|
|
1384
1466
|
for (const line of laid.lines) {
|
|
1385
1467
|
for (const g of line.glyphs) {
|
|
1386
|
-
if (g.glyph?.color)
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
"color-glyph",
|
|
1390
|
-
fontFamily,
|
|
1391
|
-
fontWeight,
|
|
1392
|
-
fontStyle,
|
|
1393
|
-
"the text holds an emoji, which is drawn in its own colors and has no outline."
|
|
1394
|
-
);
|
|
1395
|
-
}
|
|
1396
|
-
const d = faceOutlineD(g);
|
|
1397
|
-
if (d === null) {
|
|
1398
|
-
if (glyphQuad(g) !== null) {
|
|
1399
|
-
const { family, weight, style } = g.resolved.resolved;
|
|
1400
|
-
throw new TextOutlinesError(
|
|
1401
|
-
"no-outlines",
|
|
1402
|
-
family,
|
|
1403
|
-
weight,
|
|
1404
|
-
style,
|
|
1405
|
-
"a fallback face serving some of the text's characters has no outlines registered."
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
const s = g.fontSize;
|
|
1411
|
-
const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
|
|
1412
|
-
const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
|
|
1413
|
-
parts[paintOf[g.run]].push(transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m));
|
|
1468
|
+
if (g.glyph?.color) throw colorGlyphError(runs[g.run], "textToPath");
|
|
1469
|
+
const glyph = placedGlyphOutline(g, dx, dy, "textToPath");
|
|
1470
|
+
if (glyph) parts[paintOf[g.run]].push(glyph);
|
|
1414
1471
|
}
|
|
1415
1472
|
for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
|
|
1416
1473
|
rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
|
|
@@ -1446,7 +1503,48 @@ async function loadTextOutlines(data, opts = {}) {
|
|
|
1446
1503
|
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1447
1504
|
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1448
1505
|
}
|
|
1506
|
+
var MIDDLE_FALLBACK = 0.25;
|
|
1507
|
+
function textClusters(data, opts = {}) {
|
|
1508
|
+
const box = { x: 0, y: 0, width: opts.width ?? Infinity, height: 0 };
|
|
1509
|
+
const { runs, opts: layoutOpts } = outlineLayoutInput(data, box, opts, "textClusters");
|
|
1510
|
+
const line = layoutRuns(runs, layoutOpts).lines[0];
|
|
1511
|
+
if (!line || line.cells.length === 0) return [];
|
|
1512
|
+
const { paints, paintOf } = paintsOf(runs);
|
|
1513
|
+
const glyphOf = new Array(line.cells.length);
|
|
1514
|
+
for (const g of line.glyphs) glyphOf[g.cell] = g;
|
|
1515
|
+
const groups = [];
|
|
1516
|
+
let open = [];
|
|
1517
|
+
line.cells.forEach((cell, i) => {
|
|
1518
|
+
open.push(glyphOf[i]);
|
|
1519
|
+
if (cell.clusterEnd || i === line.cells.length - 1) {
|
|
1520
|
+
groups.push(open);
|
|
1521
|
+
open = [];
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
const slots = groups.map((glyphs) => ({ glyphs, left: Math.min(...glyphs.map((g) => g.x)) })).sort((a, b) => a.left - b.left);
|
|
1525
|
+
return slots.map(({ glyphs, left }, i) => {
|
|
1526
|
+
const right = i + 1 < slots.length ? slots[i + 1].left : line.x1;
|
|
1527
|
+
const parts = paints.map(() => []);
|
|
1528
|
+
for (const g of glyphs) {
|
|
1529
|
+
if (g.glyph?.color || !line.cells[g.cell].drawsInk) continue;
|
|
1530
|
+
const outline = placedGlyphOutline(g, -left, -line.baselineY, "textClusters", true);
|
|
1531
|
+
if (outline) parts[paintOf[g.run]].push(outline);
|
|
1532
|
+
}
|
|
1533
|
+
const paths = [];
|
|
1534
|
+
parts.forEach((p, k) => {
|
|
1535
|
+
if (p.length > 0) paths.push({ ...paints[k], path: concat(p) });
|
|
1536
|
+
});
|
|
1537
|
+
const first = glyphs[0];
|
|
1538
|
+
const xHeight = faceMetricsOf(first.resolved)?.xHeight;
|
|
1539
|
+
return {
|
|
1540
|
+
x: left - line.x0,
|
|
1541
|
+
advance: right - left,
|
|
1542
|
+
paths,
|
|
1543
|
+
middle: (xHeight === void 0 ? MIDDLE_FALLBACK : xHeight / 2) * first.fontSize
|
|
1544
|
+
};
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1449
1547
|
|
|
1450
|
-
export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
|
|
1548
|
+
export { DEFAULT_DECORATION_METRICS, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, SYNTHETIC_ITALIC_RADIANS, TextOutlinesError, applyStyleToRange, cachedPaintRuns, caretIndexAt, createMarkdownRenderer, decorationMetrics, defaultLabelTextRenderer, effectiveRangeStyle, emboldenGlyphD, fitTextPose, isBoldWeight, isSmallCapsLetter, isTextData, layoutMarkdown, layoutTextPose, loadTextOutlines, markdownToRuns, measureText, measureTextBounds, measuredWidth, nodeHasFlag, numericWeight, paintRuns, patchRangeStyle, pointInTextPose, rangeWeight, renderLabel, resolveRunFace, resolveRuns, runsCarryStyling, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, setFlagOverRange, smallCapsScale, smallCapsScaleFor, smallCapsText, styleAtRange, supersededKey, syntheticBoldOffset, textClusters, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
|
|
1451
1549
|
//# sourceMappingURL=index.js.map
|
|
1452
1550
|
//# sourceMappingURL=index.js.map
|