@weasel-js/text 1.10.1 → 1.11.1

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/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-EFPJZYNI.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-EFPJZYNI.js';
1
+ import { DEFAULT_TAB_SIZE, resolveTextStyle, cachedLayoutRuns, graphemeCount, wrapLines, hangs, tabAdvance, widthWithTabs, layoutRuns } from './chunk-JOVZEHBJ.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-JOVZEHBJ.js';
3
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';
4
+ import { faceMetricsOf, resolveFontVariant, syntheticBoldGrowth, glyphGeneration, loadFontOutlines, faceMetricsFor, glyphOutline, outlineStatus, cssFamilyName } from '@weasel-js/font';
5
+ import { pathToD, pathFromD, mergeRegion, growRegion, transformPath, poseFrameOf, pathSignedArea, reversePath, PATH_M, PATH_L, PATH_Z } from '@weasel-js/geom';
6
+ import { MIXED, deepEqual } from '@weasel-js/registry';
6
7
  export { MIXED } from '@weasel-js/registry';
7
- import { transformPath, poseFrameOf, 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 || !DECORATION_KINDS.some((kind) => style[kind])) {
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,14 +465,66 @@ 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) {
511
+ const outlineOnly = g.resolved.source === "outline";
512
+ if (!outlineOnly) {
513
+ if (minSize === void 0) return null;
514
+ if (run.fontSize < minSize && !strokeHasInk(run.stroke)) return null;
515
+ }
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}` };
523
+ }
524
+ function faceOutlineD(g) {
469
525
  if (g.glyph?.color) return null;
470
526
  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;
527
+ if (g.resolved.source !== "outline" && g.glyph === null) return null;
476
528
  return glyphOutline(family, weight, style, g.cp);
477
529
  }
478
530
  function glyphQuad(g) {
@@ -497,10 +549,13 @@ function runFaces(runs) {
497
549
  const faces = runs.map((r) => faceMetricsOf(resolveFontVariant(r.fontFamily, r.fontWeight, r.fontStyle)));
498
550
  return (run) => faces[run];
499
551
  }
500
- function groupFor(groups, g, run, source) {
552
+ function groupFor(groups, g, run, source, last) {
501
553
  const { family, weight, style } = g.resolved.resolved;
502
554
  const { synthetic } = g.resolved;
503
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
+ }
504
559
  const stroke = source === "outline" && strokeHasInk(run.stroke) ? run.stroke : void 0;
505
560
  const key = `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${paintKey(run.fill?.paint)}|${strokeKey(stroke)}|${source}|${page}`;
506
561
  let group = groups.get(key);
@@ -519,23 +574,41 @@ function groupFor(groups, g, run, source) {
519
574
  };
520
575
  groups.set(key, group);
521
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;
522
586
  return group;
523
587
  }
524
588
  function paintRuns(laid, runs, opts = {}) {
525
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
+ };
526
601
  const decorations = [];
527
602
  const faceOf = runFaces(runs);
528
603
  const rulePaint = (run) => fillHasInk(runs[run].fill) ? paintKey(runs[run].fill.paint) : null;
529
604
  for (const line of laid.lines) {
530
605
  for (const g of line.glyphs) {
531
606
  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}`,
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,
539
612
  x: g.x,
540
613
  baselineY: g.baselineY,
541
614
  scale: g.fontSize
@@ -543,7 +616,9 @@ function paintRuns(laid, runs, opts = {}) {
543
616
  continue;
544
617
  }
545
618
  const quad = glyphQuad(g);
546
- if (quad) groupFor(groups, g, run, g.glyph?.color ? "color" : g.resolved.source).quads.push(quad);
619
+ if (!quad) continue;
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);
547
622
  }
548
623
  for (const rule of lineDecorations(line.glyphs, runs, faceOf, rulePaint)) {
549
624
  const { run, ...rect } = rule;
@@ -1007,18 +1082,6 @@ var FLAG_KEYS = /* @__PURE__ */ new Set([
1007
1082
  "strikethrough",
1008
1083
  "overline"
1009
1084
  ]);
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
1085
  function valueAt(run, key) {
1023
1086
  const value = run[key];
1024
1087
  if (FLAG_KEYS.has(key)) return value ?? false;
@@ -1277,9 +1340,6 @@ function assertOutlinable(run) {
1277
1340
  }
1278
1341
  const r = resolveFontVariant(fontFamily, fontWeight, fontStyle);
1279
1342
  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
1343
  if (r.source === "outline") return;
1284
1344
  const status = outlineStatus(family, weight, style);
1285
1345
  if (status === null) {
@@ -1374,7 +1434,7 @@ function walkOutlines(data, pose, opts) {
1374
1434
  "the text holds an emoji, which is drawn in its own colors and has no outline."
1375
1435
  );
1376
1436
  }
1377
- const d = glyphOutlineD(g, runs[g.run], 0);
1437
+ const d = faceOutlineD(g);
1378
1438
  if (d === null) {
1379
1439
  if (glyphQuad(g) !== null) {
1380
1440
  const { family, weight, style } = g.resolved.resolved;
@@ -1391,7 +1451,7 @@ function walkOutlines(data, pose, opts) {
1391
1451
  const s = g.fontSize;
1392
1452
  const shear = g.resolved.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
1393
1453
  const m = [s, 0, -shear * s, s, g.x + dx, g.baselineY + dy];
1394
- parts[paintOf[g.run]].push(transformPath(emGlyph(d), m));
1454
+ parts[paintOf[g.run]].push(transformPath(emboldenGlyph(emGlyph(d), d, g.resolved), m));
1395
1455
  }
1396
1456
  for (const r of lineDecorations(line.glyphs, runs, faceOf, (run) => String(paintOf[run]))) {
1397
1457
  rules[paintOf[r.run]].push(ruleContour(r.x0 + dx, r.y0 + dy, r.x1 + dx, r.y1 + dy));
@@ -1428,6 +1488,6 @@ async function loadTextOutlines(data, opts = {}) {
1428
1488
  await Promise.all(facesOf(runs).map((f) => loadFontOutlines(f.family, { weight: f.weight, style: f.style })));
1429
1489
  }
1430
1490
 
1431
- 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 };
1491
+ 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, textLineBoxes, textPoseLayoutInput, textToPath, textToPathsByPaint, toRuns, transformRunTexts, unboldPatch, verticalAlignOffset };
1432
1492
  //# sourceMappingURL=index.js.map
1433
1493
  //# sourceMappingURL=index.js.map