@weasel-js/text 1.9.4 → 1.10.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/README.md +11 -9
- package/dist/{chunk-VZNK3C6F.js → chunk-Z6YRJ33J.js} +358 -695
- package/dist/chunk-Z6YRJ33J.js.map +1 -0
- package/dist/index.d.ts +511 -62
- package/dist/index.js +781 -21
- package/dist/index.js.map +1 -1
- package/dist/{test-seams-Czz9ua7X.d.ts → test-seams-CuLNcDew.d.ts} +191 -351
- package/dist/test-seams.d.ts +1 -1
- package/dist/test-seams.js +1 -1
- package/package.json +6 -5
- package/dist/chunk-VZNK3C6F.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs,
|
|
2
|
-
export { BREAK_ALLOWED, BREAK_MANDATORY,
|
|
3
|
-
import { resolveScreenLength } from '@weasel-js/paint';
|
|
4
|
-
import { faceMetricsOf, resolveFontVariant, faceMetricsFor, cssFamilyName } from '@weasel-js/font';
|
|
1
|
+
import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-Z6YRJ33J.js';
|
|
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-Z6YRJ33J.js';
|
|
3
|
+
import { resolveScreenLength, fillHasInk, strokeHasInk, paintKey, strokeKey } from '@weasel-js/paint';
|
|
4
|
+
import { faceMetricsOf, resolveFontVariant, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
|
|
5
|
+
import { MIXED, isPlainObject } from '@weasel-js/registry';
|
|
6
|
+
export { MIXED } from '@weasel-js/registry';
|
|
7
|
+
import { transformPath, poseFrameOf, pathFromD, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
|
|
5
8
|
|
|
6
9
|
// src/runs.ts
|
|
7
10
|
function toRuns(input) {
|
|
@@ -368,6 +371,66 @@ function resolveRuns(runs, style, viewScale = 1) {
|
|
|
368
371
|
return out;
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
// src/layout/decorationMetrics.ts
|
|
375
|
+
var DECORATION_KINDS = ["underline", "strikethrough", "overline"];
|
|
376
|
+
var DEFAULT_DECORATION_METRICS = Object.freeze({
|
|
377
|
+
underline: Object.freeze({ offset: 0.1, thickness: 0.05 }),
|
|
378
|
+
strikethrough: Object.freeze({ offset: -0.3, thickness: 0.05 }),
|
|
379
|
+
overline: Object.freeze({ offset: -0.9, thickness: 0.05 })
|
|
380
|
+
});
|
|
381
|
+
var resolved = /* @__PURE__ */ new WeakMap();
|
|
382
|
+
function decorationMetrics(face) {
|
|
383
|
+
if (!face) return DEFAULT_DECORATION_METRICS;
|
|
384
|
+
let out = resolved.get(face);
|
|
385
|
+
if (!out) {
|
|
386
|
+
const underline = face.underline ?? DEFAULT_DECORATION_METRICS.underline;
|
|
387
|
+
out = Object.freeze({
|
|
388
|
+
underline,
|
|
389
|
+
strikethrough: face.strikethrough ?? DEFAULT_DECORATION_METRICS.strikethrough,
|
|
390
|
+
overline: Object.freeze({ offset: DEFAULT_DECORATION_METRICS.overline.offset, thickness: underline.thickness })
|
|
391
|
+
});
|
|
392
|
+
resolved.set(face, out);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
function decorationRule(kind, baselineY, fontSize, face) {
|
|
397
|
+
const m = decorationMetrics(face)[kind];
|
|
398
|
+
const y0 = baselineY + fontSize * m.offset;
|
|
399
|
+
return { y0, y1: y0 + fontSize * m.thickness };
|
|
400
|
+
}
|
|
401
|
+
function lineDecorations(glyphs, runs, faceOf, paintOf) {
|
|
402
|
+
const rules = [];
|
|
403
|
+
let open = null;
|
|
404
|
+
const close = () => {
|
|
405
|
+
const s = open;
|
|
406
|
+
open = null;
|
|
407
|
+
if (!s || s.x1 <= s.x0) return;
|
|
408
|
+
for (const kind of DECORATION_KINDS) {
|
|
409
|
+
if (!s.style[kind]) continue;
|
|
410
|
+
const { y0, y1 } = decorationRule(kind, s.baselineY, s.style.fontSize, s.face);
|
|
411
|
+
rules.push({ kind, x0: s.x0, y0, x1: s.x1, y1, run: s.run });
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
for (const g of glyphs) {
|
|
415
|
+
const style = runs[g.run];
|
|
416
|
+
const paint = paintOf(g.run);
|
|
417
|
+
if (paint === null || !DECORATION_KINDS.some((kind) => style[kind])) {
|
|
418
|
+
close();
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const face = faceOf(g.run);
|
|
422
|
+
const joins = open !== null && DECORATION_KINDS.every((kind) => open.style[kind] === style[kind]) && open.style.fontSize === style.fontSize && open.face === face && open.baselineY === g.baselineY && open.paint === paint;
|
|
423
|
+
if (joins) {
|
|
424
|
+
open.x1 = g.x + g.step;
|
|
425
|
+
} else {
|
|
426
|
+
close();
|
|
427
|
+
open = { run: g.run, style, face, paint, baselineY: g.baselineY, x0: g.x, x1: g.x + g.step };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
close();
|
|
431
|
+
return rules;
|
|
432
|
+
}
|
|
433
|
+
|
|
371
434
|
// src/measure/verticalAlign.ts
|
|
372
435
|
function verticalAlignOffset(align, boxHeight, textHeight) {
|
|
373
436
|
if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
|
|
@@ -402,6 +465,135 @@ function layoutTextPose(pose, scale = 1) {
|
|
|
402
465
|
y: pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height)
|
|
403
466
|
};
|
|
404
467
|
}
|
|
468
|
+
function glyphOutlineD(g, run, minSize) {
|
|
469
|
+
if (g.glyph?.color) return null;
|
|
470
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
471
|
+
if (g.resolved.source === "outline") return glyphOutline(family, weight, style, g.cp);
|
|
472
|
+
if (g.glyph === null) return null;
|
|
473
|
+
if (minSize === void 0) return null;
|
|
474
|
+
if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
|
|
475
|
+
if (g.resolved.synthetic.bold) return null;
|
|
476
|
+
return glyphOutline(family, weight, style, g.cp);
|
|
477
|
+
}
|
|
478
|
+
function glyphQuad(g) {
|
|
479
|
+
const { font, glyph } = g;
|
|
480
|
+
if (font === null || glyph === null || glyph.width === 0 || glyph.page < 0) return null;
|
|
481
|
+
const scale = g.fontSize / g.metrics.size;
|
|
482
|
+
const x0 = g.x + glyph.xoffset * scale;
|
|
483
|
+
const y0 = g.baselineY + (glyph.yoffset - g.metrics.base) * scale;
|
|
484
|
+
return {
|
|
485
|
+
x0,
|
|
486
|
+
y0,
|
|
487
|
+
x1: x0 + glyph.width * scale,
|
|
488
|
+
y1: y0 + glyph.height * scale,
|
|
489
|
+
u0: glyph.x / font.common.scaleW,
|
|
490
|
+
v0: glyph.y / font.common.scaleH,
|
|
491
|
+
u1: (glyph.x + glyph.width) / font.common.scaleW,
|
|
492
|
+
v1: (glyph.y + glyph.height) / font.common.scaleH,
|
|
493
|
+
baselineY: g.baselineY
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function runFaces(runs) {
|
|
497
|
+
const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
|
|
498
|
+
return (run) => faces[run];
|
|
499
|
+
}
|
|
500
|
+
function groupFor(groups, g, run, source) {
|
|
501
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
502
|
+
const { synthetic } = g.resolved;
|
|
503
|
+
const page = source === "outline" ? 0 : g.glyph.page;
|
|
504
|
+
const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
|
|
505
|
+
const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
|
|
506
|
+
let group = groups.get(key);
|
|
507
|
+
if (!group) {
|
|
508
|
+
group = {
|
|
509
|
+
family,
|
|
510
|
+
weight,
|
|
511
|
+
style,
|
|
512
|
+
synthetic: { ...synthetic },
|
|
513
|
+
source,
|
|
514
|
+
page,
|
|
515
|
+
fill: run.fill,
|
|
516
|
+
...stroke ? { stroke } : {},
|
|
517
|
+
quads: [],
|
|
518
|
+
glyphs: []
|
|
519
|
+
};
|
|
520
|
+
groups.set(key, group);
|
|
521
|
+
}
|
|
522
|
+
return group;
|
|
523
|
+
}
|
|
524
|
+
function paintRuns(laid, runs, opts = {}) {
|
|
525
|
+
const groups = /* @__PURE__ */ new Map();
|
|
526
|
+
const decorations = [];
|
|
527
|
+
const faceOf = runFaces(runs);
|
|
528
|
+
const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
|
|
529
|
+
for (const line of laid.lines) {
|
|
530
|
+
for (const g of line.glyphs) {
|
|
531
|
+
const run = runs[g.run];
|
|
532
|
+
const d = glyphOutlineD(g, run, opts.outlineMinSize);
|
|
533
|
+
if (!fillHasInk(run.fill) && !(d !== null && strokeHasInk(run.stroke))) continue;
|
|
534
|
+
if (d !== null) {
|
|
535
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
536
|
+
groupFor(groups, g, run, "outline").glyphs.push({
|
|
537
|
+
d,
|
|
538
|
+
key: `${family}|${weight}|${style}|${g.cp}`,
|
|
539
|
+
x: g.x,
|
|
540
|
+
baselineY: g.baselineY,
|
|
541
|
+
scale: g.fontSize
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const quad = glyphQuad(g);
|
|
546
|
+
if (quad) groupFor(groups, g, run, g.glyph?.color ? "color" : g.resolved.source).quads.push(quad);
|
|
547
|
+
}
|
|
548
|
+
for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
|
|
549
|
+
const { run, ...rect } = rule;
|
|
550
|
+
decorations.push({ ...rect, fill: runs[run].fill });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return { groups: [...groups.values()], decorations };
|
|
554
|
+
}
|
|
555
|
+
var PAINT_CACHE_VARIANT_LIMIT = 32;
|
|
556
|
+
var own = /* @__PURE__ */ new WeakMap();
|
|
557
|
+
var shared = /* @__PURE__ */ new WeakMap();
|
|
558
|
+
function outlineBucket(runs, min) {
|
|
559
|
+
if (min === void 0) return -1;
|
|
560
|
+
const sizes = /* @__PURE__ */ new Set();
|
|
561
|
+
for (const r of runs) sizes.add(r.fontSize);
|
|
562
|
+
let n = 0;
|
|
563
|
+
for (const size of sizes) if (size >= min) n++;
|
|
564
|
+
return n;
|
|
565
|
+
}
|
|
566
|
+
function variantKey(runs, opts) {
|
|
567
|
+
let out = `${outlineBucket(runs, opts.outlineMinSize)}`;
|
|
568
|
+
for (const r of runs) {
|
|
569
|
+
out += `|${r.underline ? 1 : 0}${r.strikethrough ? 1 : 0}${r.overline ? 1 : 0}${paintKey(r.fill?.paint)}|${strokeKey(r.stroke)}`;
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function cachedPaintRuns(laid, runs, opts = {}) {
|
|
574
|
+
const generation = glyphGeneration();
|
|
575
|
+
const key = variantKey(runs, opts);
|
|
576
|
+
const fresh = (p) => p !== void 0 && p.generation === generation && p.key === key;
|
|
577
|
+
let byLayout = own.get(runs);
|
|
578
|
+
if (byLayout === void 0) {
|
|
579
|
+
byLayout = /* @__PURE__ */ new WeakMap();
|
|
580
|
+
own.set(runs, byLayout);
|
|
581
|
+
}
|
|
582
|
+
const mine = byLayout.get(laid);
|
|
583
|
+
if (fresh(mine)) return mine.painted;
|
|
584
|
+
let byKey = shared.get(laid);
|
|
585
|
+
if (byKey === void 0) {
|
|
586
|
+
byKey = /* @__PURE__ */ new Map();
|
|
587
|
+
shared.set(laid, byKey);
|
|
588
|
+
}
|
|
589
|
+
let painting = byKey.get(key);
|
|
590
|
+
byKey.delete(key);
|
|
591
|
+
if (!fresh(painting)) painting = { generation, key, painted: paintRuns(laid, runs, opts) };
|
|
592
|
+
if (byKey.size >= PAINT_CACHE_VARIANT_LIMIT) byKey.delete(byKey.keys().next().value);
|
|
593
|
+
byKey.set(key, painting);
|
|
594
|
+
byLayout.set(laid, painting);
|
|
595
|
+
return painting.painted;
|
|
596
|
+
}
|
|
405
597
|
|
|
406
598
|
// src/measure/measureText.ts
|
|
407
599
|
function measuredWidth(ctx, text, style) {
|
|
@@ -417,27 +609,40 @@ function measureText(ctx, text, maxWidth, style) {
|
|
|
417
609
|
u += ch.length;
|
|
418
610
|
}
|
|
419
611
|
at.push(u);
|
|
420
|
-
const
|
|
421
|
-
const
|
|
612
|
+
const flatWidth = (start, end) => measuredWidth(ctx, text.slice(at[start], at[end]), style);
|
|
613
|
+
const tabs = [];
|
|
614
|
+
cps.forEach((cp, k) => {
|
|
615
|
+
if (cp === 9) tabs.push(k);
|
|
616
|
+
});
|
|
617
|
+
const space = tabs.length === 0 ? 0 : ctx.measureText(" ").width;
|
|
618
|
+
const tab = { stop: style.tabSize * (space + style.letterSpacing), min: space / 2 };
|
|
619
|
+
const widthOf = (start, end, piece) => widthWithTabs(start, end, tabs, () => tab, flatWidth, piece);
|
|
620
|
+
const wrapped = wrapLines(cps, maxWidth, (start, end) => widthOf(start, end));
|
|
621
|
+
const lines = [];
|
|
622
|
+
const spans = [];
|
|
623
|
+
for (const l of wrapped) {
|
|
422
624
|
let end = l.end;
|
|
423
625
|
while (end > l.start && hangs(cps[end - 1])) end--;
|
|
424
|
-
|
|
425
|
-
|
|
626
|
+
lines.push(text.slice(at[l.start], at[end]));
|
|
627
|
+
const row = [];
|
|
628
|
+
widthOf(l.start, end, (from, to, x) => row.push({ text: text.slice(at[from], at[to]), x }));
|
|
629
|
+
spans.push(row);
|
|
630
|
+
}
|
|
426
631
|
const lineStarts = wrapped.map((l) => at[l.start]);
|
|
427
632
|
const height = lines.length * style.fontSize * style.lineHeight;
|
|
428
|
-
return { lines, lineStarts, height };
|
|
633
|
+
return { lines, spans, lineStarts, height };
|
|
429
634
|
}
|
|
430
635
|
|
|
431
636
|
// src/measure/measureTextBounds.ts
|
|
432
637
|
function measureTextBounds(text, style, opts) {
|
|
433
|
-
const
|
|
434
|
-
const runs = resolveRuns([{ text }],
|
|
638
|
+
const resolved2 = resolveTextStyle(style, void 0, opts?.scale);
|
|
639
|
+
const runs = resolveRuns([{ text }], resolved2, opts?.scale);
|
|
435
640
|
const { bounds } = cachedLayoutRuns(runs, {
|
|
436
641
|
maxWidth: opts?.maxWidth ?? Infinity,
|
|
437
|
-
lineHeight: opts?.lineHeight ??
|
|
438
|
-
align:
|
|
439
|
-
direction:
|
|
440
|
-
tabSize:
|
|
642
|
+
lineHeight: opts?.lineHeight ?? resolved2.lineHeight,
|
|
643
|
+
align: resolved2.align,
|
|
644
|
+
direction: resolved2.direction,
|
|
645
|
+
tabSize: resolved2.tabSize
|
|
441
646
|
});
|
|
442
647
|
return { width: bounds.width, height: bounds.height };
|
|
443
648
|
}
|
|
@@ -458,6 +663,115 @@ function textLineBoxes(pose, opts = {}) {
|
|
|
458
663
|
}
|
|
459
664
|
return out;
|
|
460
665
|
}
|
|
666
|
+
|
|
667
|
+
// src/hitTest.ts
|
|
668
|
+
function caretCells(cells) {
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const c of cells) {
|
|
671
|
+
const prev = out[out.length - 1];
|
|
672
|
+
if (prev && (!prev.clusterEnd || prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd)) {
|
|
673
|
+
const x0 = Math.min(prev.x, c.x);
|
|
674
|
+
const x1 = Math.max(prev.x + prev.advance, c.x + c.advance);
|
|
675
|
+
out[out.length - 1] = {
|
|
676
|
+
...prev,
|
|
677
|
+
x: x0,
|
|
678
|
+
advance: x1 - x0,
|
|
679
|
+
srcIndex: Math.min(prev.srcIndex, c.srcIndex),
|
|
680
|
+
srcEnd: Math.max(prev.srcEnd, c.srcEnd),
|
|
681
|
+
clusterEnd: c.clusterEnd
|
|
682
|
+
};
|
|
683
|
+
} else {
|
|
684
|
+
out.push(c);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return out;
|
|
688
|
+
}
|
|
689
|
+
function pointInTextPose(x, y, pose, opts = {}) {
|
|
690
|
+
const p = opts.padding ?? 0;
|
|
691
|
+
return x >= pose.x - p && x <= pose.x + pose.width + p && y >= pose.y - p && y <= pose.y + pose.height + p;
|
|
692
|
+
}
|
|
693
|
+
function caretIndexAt(x, y, pose) {
|
|
694
|
+
const { laid, x: dx, y: dy } = layoutTextPose(pose);
|
|
695
|
+
const lines = laid.lines;
|
|
696
|
+
if (lines.length === 0) return 0;
|
|
697
|
+
const last = lines[lines.length - 1];
|
|
698
|
+
if (y < dy + lines[0].y0) return lines[0].cells[0]?.srcIndex ?? lines[0].srcEnd;
|
|
699
|
+
if (y >= dy + last.y1) return last.srcEnd;
|
|
700
|
+
let line = last;
|
|
701
|
+
for (const candidate of lines) {
|
|
702
|
+
if (y < dy + candidate.y1) {
|
|
703
|
+
line = candidate;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (line.cells.length === 0) return line.srcEnd;
|
|
708
|
+
const cells = caretCells(line.cells);
|
|
709
|
+
const visual = cells.map((_, i) => i).sort((a, b) => cells[a].x - cells[b].x);
|
|
710
|
+
for (const i of visual) {
|
|
711
|
+
const c = cells[i];
|
|
712
|
+
if (x >= dx + c.x + c.advance / 2) continue;
|
|
713
|
+
return c.level % 2 === 1 ? c.srcEnd : c.srcIndex;
|
|
714
|
+
}
|
|
715
|
+
const trailing = cells[visual[visual.length - 1]];
|
|
716
|
+
return trailing.level % 2 === 1 ? trailing.srcIndex : trailing.srcEnd;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/fitTextPose.ts
|
|
720
|
+
function fitTextPose(pose, opts = {}) {
|
|
721
|
+
const axis = opts.axis ?? "height";
|
|
722
|
+
const { padX, padY } = resolvePadding(opts.padding);
|
|
723
|
+
const { bounds } = layoutTextPose(axis === "height" ? { ...pose, width: pose.width - padX * 2 } : { ...pose, style: { ...pose.style, wrap: false } }).laid;
|
|
724
|
+
if (axis === "height") return { ...pose, height: bounds.height + padY * 2 };
|
|
725
|
+
return {
|
|
726
|
+
...pose,
|
|
727
|
+
width: bounds.width + padX * 2,
|
|
728
|
+
height: bounds.height + padY * 2
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function resolvePadding(p) {
|
|
732
|
+
if (p == null) return { padX: 0, padY: 0 };
|
|
733
|
+
if (typeof p === "number") return { padX: p, padY: p };
|
|
734
|
+
return { padX: p.x ?? 0, padY: p.y ?? 0 };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/renderLabel.ts
|
|
738
|
+
function renderLabel(ctx, text, x, y, options = {}) {
|
|
739
|
+
const {
|
|
740
|
+
align = "center",
|
|
741
|
+
fontSize = 13,
|
|
742
|
+
renderText,
|
|
743
|
+
width,
|
|
744
|
+
height,
|
|
745
|
+
padX = 4,
|
|
746
|
+
padY = 1,
|
|
747
|
+
cornerRadius = 3
|
|
748
|
+
} = options;
|
|
749
|
+
ctx.save();
|
|
750
|
+
ctx.font = `${fontSize}px sans-serif`;
|
|
751
|
+
ctx.textBaseline = "top";
|
|
752
|
+
const contentW = width ?? ctx.measureText(text).width;
|
|
753
|
+
const contentH = height ?? fontSize;
|
|
754
|
+
const w = contentW + padX * 2;
|
|
755
|
+
const h = contentH + padY * 2;
|
|
756
|
+
const rx = align === "center" ? x - w / 2 : x - padX;
|
|
757
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
758
|
+
ctx.beginPath();
|
|
759
|
+
ctx.roundRect(rx, y - padY, w, h, cornerRadius);
|
|
760
|
+
ctx.fill();
|
|
761
|
+
ctx.textAlign = align === "center" ? "center" : "left";
|
|
762
|
+
const tx = align === "center" ? x : x;
|
|
763
|
+
(renderText ?? defaultLabelTextRenderer)(ctx, text, tx, y);
|
|
764
|
+
ctx.restore();
|
|
765
|
+
}
|
|
766
|
+
var defaultLabelTextRenderer = (ctx, text, x, y) => {
|
|
767
|
+
ctx.fillStyle = "#FFFFFF";
|
|
768
|
+
ctx.fillText(text, x, y);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/isTextData.ts
|
|
772
|
+
function isTextData(data) {
|
|
773
|
+
return typeof data === "object" && data !== null && typeof data.text === "string";
|
|
774
|
+
}
|
|
461
775
|
function runMetrics(run, fontSize, face) {
|
|
462
776
|
const script = run.script ? scriptMetrics(face)[run.script] : void 0;
|
|
463
777
|
const scale = run.fontScale ?? script?.size ?? 1;
|
|
@@ -469,7 +783,7 @@ function runMetrics(run, fontSize, face) {
|
|
|
469
783
|
y: shiftEm === 0 ? 0 : -shiftEm * fontSize
|
|
470
784
|
};
|
|
471
785
|
}
|
|
472
|
-
function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf) {
|
|
786
|
+
function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.3, faceOf, tabSize = DEFAULT_TAB_SIZE) {
|
|
473
787
|
if (runs.length === 0) return { lines: [], width: 0, height: 0 };
|
|
474
788
|
const shown = transformRunTexts(runs.map((r) => r.text), runs.map((r) => r.textTransform ?? "none"));
|
|
475
789
|
const segs = [];
|
|
@@ -520,7 +834,7 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
|
|
|
520
834
|
const out = [];
|
|
521
835
|
for (let k = from; k < to; k++) {
|
|
522
836
|
const last = out[out.length - 1];
|
|
523
|
-
if (last && last.run === runOf[k] && last.end === startOf[k]) {
|
|
837
|
+
if (last && last.run === runOf[k] && last.end === startOf[k] && cps[k] !== 9 && cps[k - 1] !== 9) {
|
|
524
838
|
out[out.length - 1] = { ...last, end: endOf[k] };
|
|
525
839
|
} else {
|
|
526
840
|
out.push({ run: runOf[k], start: startOf[k], end: endOf[k] });
|
|
@@ -535,14 +849,25 @@ function layoutMarkdown(runs, maxWidth, fontSize, measure, lineHeightFactor = 1.
|
|
|
535
849
|
const positioned = [];
|
|
536
850
|
for (const p of line) {
|
|
537
851
|
const { run, face, size, y, runSize } = placed[p.run];
|
|
538
|
-
const
|
|
852
|
+
const tab = run.text[p.start] === " " ? stopOf(p.run) : null;
|
|
853
|
+
const width2 = tab ? tabAdvance(x, tab.stop, tab.min) : pieceWidth(p);
|
|
539
854
|
positioned.push({ ...run, text: run.text.slice(p.start, p.end), x, width: width2, y, size, ...face ? { face } : {} });
|
|
540
855
|
x += width2;
|
|
541
856
|
maxSize = Math.max(maxSize, runSize);
|
|
542
857
|
}
|
|
543
858
|
lines.push({ runs: positioned, width: x, height: (maxSize > 0 ? maxSize : fontSize) * lineHeightFactor });
|
|
544
859
|
}
|
|
545
|
-
const
|
|
860
|
+
const stopOf = (run) => {
|
|
861
|
+
const { run: r, size } = placed[run];
|
|
862
|
+
const space = measure(" ", size, r.bold ?? false, r.italic ?? false);
|
|
863
|
+
return { stop: tabSize * space, min: space / 2 };
|
|
864
|
+
};
|
|
865
|
+
const tabs = [];
|
|
866
|
+
cps.forEach((cp, k) => {
|
|
867
|
+
if (cp === 9) tabs.push(k);
|
|
868
|
+
});
|
|
869
|
+
const flatWidth = (start, end) => piecesOf(start, end).reduce((w, p) => w + pieceWidth(p), 0);
|
|
870
|
+
const widthOf = (start, end) => widthWithTabs(start, end, tabs, (k) => stopOf(runOf[k]), flatWidth);
|
|
546
871
|
for (const line of wrapLines(cps, maxWidth, widthOf)) {
|
|
547
872
|
let end = line.end;
|
|
548
873
|
if (line.wrapped) while (end > line.start && hangs(cps[end - 1])) end--;
|
|
@@ -599,7 +924,7 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
|
|
|
599
924
|
const family = fontOpts.family ?? "sans-serif";
|
|
600
925
|
const weight = numericWeight(fontOpts.weight ?? 400);
|
|
601
926
|
const faceOf = (bold, italic) => faceMetricsFor(family, bold ? 700 : weight, italic ? "italic" : "normal");
|
|
602
|
-
const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf);
|
|
927
|
+
const layout = layoutMarkdown(parsed, maxWidth, fontSize, measure, fontOpts.lineHeight, faceOf, fontOpts.tabSize);
|
|
603
928
|
function walk(c, x, y, drawRun) {
|
|
604
929
|
const align = c.textAlign;
|
|
605
930
|
const left = blockLeft(c, x, layout.width);
|
|
@@ -657,7 +982,442 @@ function createMarkdownRenderer(ctx, text, fontSize, maxWidth = Infinity, fontOp
|
|
|
657
982
|
};
|
|
658
983
|
return { renderer, strokeRenderer, width: layout.width, height: layout.height };
|
|
659
984
|
}
|
|
985
|
+
var STYLE_KEYS = [
|
|
986
|
+
"bold",
|
|
987
|
+
"fontWeight",
|
|
988
|
+
"italic",
|
|
989
|
+
"underline",
|
|
990
|
+
"strikethrough",
|
|
991
|
+
"overline",
|
|
992
|
+
"fontFamily",
|
|
993
|
+
"fontSize",
|
|
994
|
+
"letterSpacing",
|
|
995
|
+
"fill",
|
|
996
|
+
"stroke",
|
|
997
|
+
"script",
|
|
998
|
+
"baselineShift",
|
|
999
|
+
"fontScale",
|
|
1000
|
+
"textTransform",
|
|
1001
|
+
"fontVariantCaps"
|
|
1002
|
+
];
|
|
1003
|
+
var FLAG_KEYS = /* @__PURE__ */ new Set([
|
|
1004
|
+
"bold",
|
|
1005
|
+
"italic",
|
|
1006
|
+
"underline",
|
|
1007
|
+
"strikethrough",
|
|
1008
|
+
"overline"
|
|
1009
|
+
]);
|
|
1010
|
+
function deepEqual(a, b) {
|
|
1011
|
+
if (Object.is(a, b)) return true;
|
|
1012
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1013
|
+
return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
1014
|
+
}
|
|
1015
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1016
|
+
const keys = Object.keys(a);
|
|
1017
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
1018
|
+
return keys.every((k) => k in b && deepEqual(a[k], b[k]));
|
|
1019
|
+
}
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
function valueAt(run, key) {
|
|
1023
|
+
const value = run[key];
|
|
1024
|
+
if (FLAG_KEYS.has(key)) return value ?? false;
|
|
1025
|
+
return value;
|
|
1026
|
+
}
|
|
1027
|
+
var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["fill", "stroke"]);
|
|
1028
|
+
function sameValue(key, a, b) {
|
|
1029
|
+
return STRUCTURAL_KEYS.has(key) ? deepEqual(a, b) : Object.is(a, b);
|
|
1030
|
+
}
|
|
1031
|
+
function sameStyle(a, b) {
|
|
1032
|
+
return STYLE_KEYS.every((key) => sameValue(key, valueAt(a, key), valueAt(b, key)));
|
|
1033
|
+
}
|
|
1034
|
+
function totalLength(runs) {
|
|
1035
|
+
let n = 0;
|
|
1036
|
+
for (const run of runs) n += run.text.length;
|
|
1037
|
+
return n;
|
|
1038
|
+
}
|
|
1039
|
+
function styleAtRange(runs, start, end) {
|
|
1040
|
+
const lo = Math.max(0, start);
|
|
1041
|
+
const hi = end;
|
|
1042
|
+
if (!(lo < hi)) return {};
|
|
1043
|
+
const first = /* @__PURE__ */ new Map();
|
|
1044
|
+
const mixed = /* @__PURE__ */ new Set();
|
|
1045
|
+
let seen = false;
|
|
1046
|
+
let pos = 0;
|
|
1047
|
+
for (const run of runs) {
|
|
1048
|
+
const runEnd = pos + run.text.length;
|
|
1049
|
+
const a = Math.max(pos, lo);
|
|
1050
|
+
const b = Math.min(runEnd, hi);
|
|
1051
|
+
pos = runEnd;
|
|
1052
|
+
if (!(a < b)) continue;
|
|
1053
|
+
for (const key of STYLE_KEYS) {
|
|
1054
|
+
const value = valueAt(run, key);
|
|
1055
|
+
if (!seen) first.set(key, value);
|
|
1056
|
+
else if (!mixed.has(key) && !sameValue(key, first.get(key), value)) {
|
|
1057
|
+
mixed.add(key);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
seen = true;
|
|
1061
|
+
}
|
|
1062
|
+
if (!seen) return {};
|
|
1063
|
+
const style = {};
|
|
1064
|
+
for (const key of STYLE_KEYS) {
|
|
1065
|
+
if (mixed.has(key)) style[key] = MIXED;
|
|
1066
|
+
else {
|
|
1067
|
+
const value = first.get(key);
|
|
1068
|
+
if (value !== void 0) style[key] = value;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return style;
|
|
1072
|
+
}
|
|
1073
|
+
function runsCarryStyling(runs) {
|
|
1074
|
+
return runs.some(
|
|
1075
|
+
(run) => STYLE_KEYS.some((key) => {
|
|
1076
|
+
const value = run[key];
|
|
1077
|
+
return value !== void 0 && value !== false;
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
function supersededKey(patch) {
|
|
1082
|
+
if ("bold" in patch && !("fontWeight" in patch)) return "fontWeight";
|
|
1083
|
+
if (patch.fontWeight !== void 0 && !("bold" in patch)) return "bold";
|
|
1084
|
+
return void 0;
|
|
1085
|
+
}
|
|
1086
|
+
function patchRangeStyle(range, patch) {
|
|
1087
|
+
const out = { ...range };
|
|
1088
|
+
const superseded = supersededKey(patch);
|
|
1089
|
+
if (superseded !== void 0) delete out[superseded];
|
|
1090
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1091
|
+
if (value !== void 0) out[key] = value;
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
function patchRun(run, patch) {
|
|
1096
|
+
const next = { ...run };
|
|
1097
|
+
const superseded = supersededKey(patch);
|
|
1098
|
+
if (superseded !== void 0) delete next[superseded];
|
|
1099
|
+
for (const key of STYLE_KEYS) {
|
|
1100
|
+
if (!(key in patch)) continue;
|
|
1101
|
+
const value = patch[key];
|
|
1102
|
+
if (value === void 0 || value === false) delete next[key];
|
|
1103
|
+
else next[key] = value;
|
|
1104
|
+
}
|
|
1105
|
+
return next;
|
|
1106
|
+
}
|
|
1107
|
+
function coalesceRuns(runs) {
|
|
1108
|
+
const out = [];
|
|
1109
|
+
for (const run of runs) {
|
|
1110
|
+
if (run.text.length === 0) continue;
|
|
1111
|
+
const prev = out[out.length - 1];
|
|
1112
|
+
if (prev && sameStyle(prev, run)) prev.text += run.text;
|
|
1113
|
+
else out.push({ ...run });
|
|
1114
|
+
}
|
|
1115
|
+
return out;
|
|
1116
|
+
}
|
|
1117
|
+
function applyStyleToRange(runs, start, end, patch) {
|
|
1118
|
+
const lo = Math.max(0, start);
|
|
1119
|
+
const hi = Math.min(totalLength(runs), end);
|
|
1120
|
+
if (!(lo < hi)) return coalesceRuns(runs);
|
|
1121
|
+
const out = [];
|
|
1122
|
+
let pos = 0;
|
|
1123
|
+
for (const run of runs) {
|
|
1124
|
+
const runEnd = pos + run.text.length;
|
|
1125
|
+
const a = Math.max(pos, lo);
|
|
1126
|
+
const b = Math.min(runEnd, hi);
|
|
1127
|
+
if (a < b) {
|
|
1128
|
+
if (pos < a) out.push({ ...run, text: run.text.slice(0, a - pos) });
|
|
1129
|
+
out.push(patchRun({ ...run, text: run.text.slice(a - pos, b - pos) }, patch));
|
|
1130
|
+
if (b < runEnd) out.push({ ...run, text: run.text.slice(b - pos) });
|
|
1131
|
+
} else {
|
|
1132
|
+
out.push({ ...run });
|
|
1133
|
+
}
|
|
1134
|
+
pos = runEnd;
|
|
1135
|
+
}
|
|
1136
|
+
return coalesceRuns(out);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/runs/flagRange.ts
|
|
1140
|
+
function nodeHasFlag(style, key) {
|
|
1141
|
+
switch (key) {
|
|
1142
|
+
case "bold":
|
|
1143
|
+
return style.fontWeight !== void 0 && isBoldWeight(style.fontWeight);
|
|
1144
|
+
case "italic":
|
|
1145
|
+
return style.fontStyle === "italic";
|
|
1146
|
+
case "underline":
|
|
1147
|
+
return style.underline === true;
|
|
1148
|
+
case "strikethrough":
|
|
1149
|
+
return style.strikethrough === true;
|
|
1150
|
+
case "overline":
|
|
1151
|
+
return style.overline === true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function unboldPatch(style) {
|
|
1155
|
+
return nodeHasFlag(style, "bold") ? { bold: false, fontWeight: 400 } : { bold: false };
|
|
1156
|
+
}
|
|
1157
|
+
function clearNodeFlag(style, key) {
|
|
1158
|
+
const next = { ...style };
|
|
1159
|
+
switch (key) {
|
|
1160
|
+
case "italic":
|
|
1161
|
+
next.fontStyle = "normal";
|
|
1162
|
+
break;
|
|
1163
|
+
case "underline":
|
|
1164
|
+
delete next.underline;
|
|
1165
|
+
break;
|
|
1166
|
+
case "strikethrough":
|
|
1167
|
+
delete next.strikethrough;
|
|
1168
|
+
break;
|
|
1169
|
+
case "overline":
|
|
1170
|
+
delete next.overline;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
return next;
|
|
1174
|
+
}
|
|
1175
|
+
function setFlagOverRange(runs, style, start, end, key, value) {
|
|
1176
|
+
if (value || !nodeHasFlag(style, key)) {
|
|
1177
|
+
return { runs: applyStyleToRange(runs, start, end, { [key]: value }), style };
|
|
1178
|
+
}
|
|
1179
|
+
if (key === "bold") {
|
|
1180
|
+
return { runs: applyStyleToRange(runs, start, end, unboldPatch(style)), style };
|
|
1181
|
+
}
|
|
1182
|
+
const total = runs.reduce((n, r) => n + r.text.length, 0);
|
|
1183
|
+
const lo = Math.max(0, start);
|
|
1184
|
+
const hi = Math.min(total, end);
|
|
1185
|
+
let next = applyStyleToRange(runs, 0, total, { [key]: true });
|
|
1186
|
+
if (lo < hi) next = applyStyleToRange(next, lo, hi, { [key]: false });
|
|
1187
|
+
return { runs: next, style: clearNodeFlag(style, key) };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/runs/effectiveRangeStyle.ts
|
|
1191
|
+
var FLAGS = ["italic", "underline", "strikethrough", "overline"];
|
|
1192
|
+
function rangeWeight(range, style) {
|
|
1193
|
+
const node = numericWeight(style?.fontWeight ?? 400);
|
|
1194
|
+
const own2 = range?.fontWeight;
|
|
1195
|
+
if (own2 === MIXED) return { fontWeight: MIXED, bold: MIXED };
|
|
1196
|
+
if (own2 !== void 0) return { fontWeight: own2, bold: isBoldWeight(own2) };
|
|
1197
|
+
const flag = range?.bold;
|
|
1198
|
+
if (flag === MIXED) {
|
|
1199
|
+
if (node === 700) return { fontWeight: 700, bold: true };
|
|
1200
|
+
return { fontWeight: MIXED, bold: isBoldWeight(node) ? true : MIXED };
|
|
1201
|
+
}
|
|
1202
|
+
const weight = flag === true ? 700 : node;
|
|
1203
|
+
return { fontWeight: weight, bold: isBoldWeight(weight) };
|
|
1204
|
+
}
|
|
1205
|
+
var OVERRIDES = [
|
|
1206
|
+
"fontFamily",
|
|
1207
|
+
"fontSize",
|
|
1208
|
+
"letterSpacing",
|
|
1209
|
+
"fill",
|
|
1210
|
+
"script",
|
|
1211
|
+
"baselineShift",
|
|
1212
|
+
"fontScale",
|
|
1213
|
+
"textTransform",
|
|
1214
|
+
"fontVariantCaps"
|
|
1215
|
+
];
|
|
1216
|
+
function effectiveRangeStyle(range, style, paint) {
|
|
1217
|
+
const resolved2 = resolveTextStyle(style, paint);
|
|
1218
|
+
const node = style ?? {};
|
|
1219
|
+
const out = {
|
|
1220
|
+
fontFamily: resolved2.fontFamily,
|
|
1221
|
+
fontSize: resolved2.fontSize,
|
|
1222
|
+
letterSpacing: resolved2.letterSpacing,
|
|
1223
|
+
textTransform: resolved2.textTransform,
|
|
1224
|
+
fontVariantCaps: resolved2.fontVariantCaps
|
|
1225
|
+
};
|
|
1226
|
+
if (resolved2.fill !== null) out.fill = resolved2.fill;
|
|
1227
|
+
Object.assign(out, rangeWeight(range, style));
|
|
1228
|
+
for (const key of FLAGS) {
|
|
1229
|
+
const on = nodeHasFlag(node, key);
|
|
1230
|
+
const run = range?.[key];
|
|
1231
|
+
out[key] = on || run === void 0 ? on : run;
|
|
1232
|
+
}
|
|
1233
|
+
if (range === null) return out;
|
|
1234
|
+
for (const key of OVERRIDES) {
|
|
1235
|
+
if (range[key] !== void 0) out[key] = range[key];
|
|
1236
|
+
}
|
|
1237
|
+
return out;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/syntheticItalic.ts
|
|
1241
|
+
var SYNTHETIC_ITALIC_RADIANS = 0.2094;
|
|
1242
|
+
var TextOutlinesError = class extends Error {
|
|
1243
|
+
reason;
|
|
1244
|
+
family;
|
|
1245
|
+
weight;
|
|
1246
|
+
style;
|
|
1247
|
+
constructor(reason, family, weight, style, detail) {
|
|
1248
|
+
super(`textToPath: "${family}" ${weight}/${style} \u2014 ${detail}`);
|
|
1249
|
+
this.name = "TextOutlinesError";
|
|
1250
|
+
this.reason = reason;
|
|
1251
|
+
this.family = family;
|
|
1252
|
+
this.weight = weight;
|
|
1253
|
+
this.style = style;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
function facesOf(runs) {
|
|
1257
|
+
const out = /* @__PURE__ */ new Map();
|
|
1258
|
+
const add = (family, weight, style) => {
|
|
1259
|
+
out.set(`${family}|${weight}|${style}`, { family, weight, style });
|
|
1260
|
+
};
|
|
1261
|
+
for (const r of runs) {
|
|
1262
|
+
add(r.fontFamily, r.fontWeight, r.fontStyle);
|
|
1263
|
+
const { family, weight, style } = resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle).resolved;
|
|
1264
|
+
add(family, weight, style);
|
|
1265
|
+
}
|
|
1266
|
+
return [...out.values()];
|
|
1267
|
+
}
|
|
1268
|
+
function assertOutlinable(run) {
|
|
1269
|
+
const { fontFamily, fontWeight, fontStyle } = run;
|
|
1270
|
+
const fail = (reason, detail) => {
|
|
1271
|
+
throw new TextOutlinesError(reason, fontFamily, fontWeight, fontStyle, detail);
|
|
1272
|
+
};
|
|
1273
|
+
const requested = outlineStatus(fontFamily, fontWeight, fontStyle);
|
|
1274
|
+
if (requested === "idle" || requested === "loading") {
|
|
1275
|
+
void loadFontOutlines(fontFamily, { weight: fontWeight, style: fontStyle });
|
|
1276
|
+
fail("outlines-loading", "its outlines have not loaded yet; await loadTextOutlines first.");
|
|
1277
|
+
}
|
|
1278
|
+
const r = resolveFontVariant(fontFamily, fontWeight, fontStyle);
|
|
1279
|
+
const { family, weight, style } = r.resolved;
|
|
1280
|
+
if (r.synthetic.bold) {
|
|
1281
|
+
fail("synthetic-bold", `drawn as a faux bold of ${family} ${weight}/${style}, which has no geometry to embolden. Register outlines for the real bold face.`);
|
|
1282
|
+
}
|
|
1283
|
+
if (r.source === "outline") return;
|
|
1284
|
+
const status = outlineStatus(family, weight, style);
|
|
1285
|
+
if (status === null) {
|
|
1286
|
+
fail("no-outlines", `served by ${family} ${weight}/${style}, which has no outlines registered. Call registerFontOutlines for it.`);
|
|
1287
|
+
}
|
|
1288
|
+
if (status === "idle" || status === "loading") {
|
|
1289
|
+
void loadFontOutlines(family, { weight, style });
|
|
1290
|
+
fail("outlines-loading", `the outlines of ${family} ${weight}/${style} have not loaded yet; await loadTextOutlines first.`);
|
|
1291
|
+
}
|
|
1292
|
+
if (status === "failed") {
|
|
1293
|
+
fail("outlines-failed", `the outlines registered for ${family} ${weight}/${style} failed to load.`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
var EM_CACHE_LIMIT = 4096;
|
|
1297
|
+
var emCache = /* @__PURE__ */ new Map();
|
|
1298
|
+
function emGlyph(d) {
|
|
1299
|
+
let p = emCache.get(d);
|
|
1300
|
+
if (p) return p;
|
|
1301
|
+
p = pathFromD(d);
|
|
1302
|
+
if (pathSignedArea(p) < 0) p = reversePath(p);
|
|
1303
|
+
if (emCache.size >= EM_CACHE_LIMIT) emCache.clear();
|
|
1304
|
+
emCache.set(d, p);
|
|
1305
|
+
return p;
|
|
1306
|
+
}
|
|
1307
|
+
function concat(parts) {
|
|
1308
|
+
let nc = 0, nk = 0;
|
|
1309
|
+
for (const p of parts) {
|
|
1310
|
+
nc += p.commands.length;
|
|
1311
|
+
nk += p.coords.length;
|
|
1312
|
+
}
|
|
1313
|
+
const commands = new Uint8Array(nc);
|
|
1314
|
+
const coords = new Float32Array(nk);
|
|
1315
|
+
let ci = 0, ki = 0;
|
|
1316
|
+
for (const p of parts) {
|
|
1317
|
+
commands.set(p.commands, ci);
|
|
1318
|
+
ci += p.commands.length;
|
|
1319
|
+
coords.set(p.coords, ki);
|
|
1320
|
+
ki += p.coords.length;
|
|
1321
|
+
}
|
|
1322
|
+
return { kind: "polygon", commands, coords, fillRule: "nonzero" };
|
|
1323
|
+
}
|
|
1324
|
+
function ruleContour(x0, y0, x1, y1) {
|
|
1325
|
+
return {
|
|
1326
|
+
kind: "polygon",
|
|
1327
|
+
commands: new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]),
|
|
1328
|
+
coords: new Float32Array([x0, y0, x1, y0, x1, y1, x0, y1]),
|
|
1329
|
+
fillRule: "nonzero"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
function walkOutlines(data, pose, opts) {
|
|
1333
|
+
const textPose = {
|
|
1334
|
+
x: pose.x,
|
|
1335
|
+
y: pose.y,
|
|
1336
|
+
width: pose.width,
|
|
1337
|
+
height: pose.height,
|
|
1338
|
+
text: data.text,
|
|
1339
|
+
runs: data.runs,
|
|
1340
|
+
style: data.style,
|
|
1341
|
+
verticalAlign: data.verticalAlign,
|
|
1342
|
+
fill: data.fill,
|
|
1343
|
+
stroke: data.stroke
|
|
1344
|
+
};
|
|
1345
|
+
const { runs, opts: layoutOpts } = textPoseLayoutInput(textPose, opts.scale ?? 1);
|
|
1346
|
+
for (const run of runs) assertOutlinable(run);
|
|
1347
|
+
const paints = [];
|
|
1348
|
+
const indexOf = /* @__PURE__ */ new Map();
|
|
1349
|
+
const paintOf = runs.map((run) => {
|
|
1350
|
+
const key = `${paintKey(run.fill?.paint)}|${strokeKey(run.stroke)}`;
|
|
1351
|
+
let i = indexOf.get(key);
|
|
1352
|
+
if (i === void 0) {
|
|
1353
|
+
i = paints.length;
|
|
1354
|
+
indexOf.set(key, i);
|
|
1355
|
+
paints.push(run.stroke !== void 0 ? { fill: run.fill, stroke: run.stroke } : { fill: run.fill });
|
|
1356
|
+
}
|
|
1357
|
+
return i;
|
|
1358
|
+
});
|
|
1359
|
+
const laid = layoutRuns(runs, layoutOpts);
|
|
1360
|
+
const dx = pose.x;
|
|
1361
|
+
const dy = pose.y + verticalAlignOffset(data.verticalAlign, pose.height, laid.bounds.height);
|
|
1362
|
+
const parts = paints.map(() => []);
|
|
1363
|
+
const rules = paints.map(() => []);
|
|
1364
|
+
const faceOf = runFaces(runs);
|
|
1365
|
+
for (const line of laid.lines) {
|
|
1366
|
+
for (const g of line.glyphs) {
|
|
1367
|
+
const d = glyphOutlineD(g, runs[g.run], 0);
|
|
1368
|
+
if (d === null) {
|
|
1369
|
+
if (glyphQuad(g) !== null) {
|
|
1370
|
+
const { family, weight, style } = g.resolved.resolved;
|
|
1371
|
+
throw new TextOutlinesError(
|
|
1372
|
+
"no-outlines",
|
|
1373
|
+
family,
|
|
1374
|
+
weight,
|
|
1375
|
+
style,
|
|
1376
|
+
"a fallback face serving some of the text's characters has no outlines registered."
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
const s = g.fontSize;
|
|
1382
|
+
const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
|
|
1383
|
+
const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
|
|
1384
|
+
parts[paintOf[g.run]].push(transformPath(emGlyph(d), m));
|
|
1385
|
+
}
|
|
1386
|
+
for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
|
|
1387
|
+
rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const frame = poseFrameOf(pose);
|
|
1391
|
+
const paths = parts.map((glyphs, i) => {
|
|
1392
|
+
const path = concat([...glyphs, ...rules[i]]);
|
|
1393
|
+
if (!frame || path.commands.length === 0) return path;
|
|
1394
|
+
return transformPath(path, frame);
|
|
1395
|
+
});
|
|
1396
|
+
return { paints, paths };
|
|
1397
|
+
}
|
|
1398
|
+
function textToPath(data, pose, opts = {}) {
|
|
1399
|
+
return concat(walkOutlines(data, pose, opts).paths);
|
|
1400
|
+
}
|
|
1401
|
+
function textToPathsByPaint(data, pose, opts = {}) {
|
|
1402
|
+
const { paints, paths } = walkOutlines(data, pose, opts);
|
|
1403
|
+
const out = [];
|
|
1404
|
+
paints.forEach((paint, i) => {
|
|
1405
|
+
if (paths[i].commands.length > 0) out.push({ ...paint, path: paths[i] });
|
|
1406
|
+
});
|
|
1407
|
+
return out;
|
|
1408
|
+
}
|
|
1409
|
+
async function loadTextOutlines(data, opts = {}) {
|
|
1410
|
+
const textPose = {
|
|
1411
|
+
width: 0,
|
|
1412
|
+
text: data.text,
|
|
1413
|
+
runs: data.runs,
|
|
1414
|
+
style: data.style
|
|
1415
|
+
};
|
|
1416
|
+
const { runs } = textPoseLayoutInput(textPose, opts.scale ?? 1);
|
|
1417
|
+
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1418
|
+
await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
|
|
1419
|
+
}
|
|
660
1420
|
|
|
661
|
-
export { MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, SMALL_CAPS_SCALE, createMarkdownRenderer, isBoldWeight, isSmallCapsLetter, layoutMarkdown, layoutTextPose, markdownToRuns, measureText, measureTextBounds, measuredWidth, numericWeight, resolveRunFace, resolveRuns, runsToMarkdown, runsToPlainText, scriptMetrics, scriptMetricsFor, smallCapsScale, smallCapsScaleFor, smallCapsText, textLineBoxes, textPoseLayoutInput, toRuns, transformRunTexts, verticalAlignOffset };
|
|
1421
|
+
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 };
|
|
662
1422
|
//# sourceMappingURL=index.js.map
|
|
663
1423
|
//# sourceMappingURL=index.js.map
|