modern-text 0.4.2 → 0.4.4

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { BoundingBox, Path2D, getPathsBoundingBox, Matrix3, parseSvg, Vector2 } from 'modern-path2d';
1
+ import { Path2D, BoundingBox, getPathsBoundingBox, Matrix3, parseSvg, Vector2 } from 'modern-path2d';
2
2
  import { fonts } from 'modern-font';
3
3
 
4
4
  function parseColor(ctx, source, box) {
@@ -146,16 +146,26 @@ class Character {
146
146
  this.content = content;
147
147
  this.index = index;
148
148
  this.parent = parent;
149
+ __publicField$3(this, "path", new Path2D());
149
150
  __publicField$3(this, "lineBox", new BoundingBox());
150
151
  __publicField$3(this, "inlineBox", new BoundingBox());
151
152
  __publicField$3(this, "glyphBox");
152
153
  __publicField$3(this, "underlinePosition", 0);
153
154
  __publicField$3(this, "underlineThickness", 0);
154
- __publicField$3(this, "yStrikeoutPosition", 0);
155
- __publicField$3(this, "yStrikeoutSize", 0);
155
+ __publicField$3(this, "strikeoutPosition", 0);
156
+ __publicField$3(this, "strikeoutSize", 0);
157
+ __publicField$3(this, "ascender", 0);
158
+ __publicField$3(this, "descender", 0);
159
+ __publicField$3(this, "typoAscender", 0);
160
+ __publicField$3(this, "typoDescender", 0);
161
+ __publicField$3(this, "typoLineGap", 0);
162
+ __publicField$3(this, "winAscent", 0);
163
+ __publicField$3(this, "winDescent", 0);
164
+ __publicField$3(this, "xHeight", 0);
165
+ __publicField$3(this, "capHeight", 0);
156
166
  __publicField$3(this, "baseline", 0);
157
167
  __publicField$3(this, "centerDiviation", 0);
158
- __publicField$3(this, "path", new Path2D());
168
+ __publicField$3(this, "fontStyle");
159
169
  }
160
170
  get center() {
161
171
  return this.glyphBox?.center;
@@ -182,25 +192,34 @@ class Character {
182
192
  if (!sfnt) {
183
193
  return this;
184
194
  }
185
- const { unitsPerEm, ascender, descender, os2, post } = sfnt;
195
+ const { hhea, os2, post, head } = sfnt;
196
+ const unitsPerEm = head.unitsPerEm;
197
+ const ascender = hhea.ascent;
198
+ const descender = hhea.descent;
186
199
  const { content, computedStyle } = this;
187
200
  const { fontSize } = computedStyle;
188
201
  const rate = unitsPerEm / fontSize;
189
202
  const advanceWidth = sfnt.getAdvanceWidth(content, fontSize);
190
203
  const advanceHeight = (ascender + Math.abs(descender)) / rate;
191
204
  const baseline = ascender / rate;
192
- const yStrikeoutPosition = (ascender - os2.yStrikeoutPosition) / rate;
193
- const yStrikeoutSize = os2.yStrikeoutSize / rate;
194
- const underlinePosition = (ascender - post.underlinePosition) / rate;
195
- const underlineThickness = post.underlineThickness / rate;
196
205
  this.inlineBox.width = advanceWidth;
197
206
  this.inlineBox.height = advanceHeight;
198
- this.underlinePosition = underlinePosition;
199
- this.underlineThickness = underlineThickness;
200
- this.yStrikeoutPosition = yStrikeoutPosition;
201
- this.yStrikeoutSize = yStrikeoutSize;
207
+ this.underlinePosition = (ascender - post.underlinePosition) / rate;
208
+ this.underlineThickness = post.underlineThickness / rate;
209
+ this.strikeoutPosition = (ascender - os2.yStrikeoutPosition) / rate;
210
+ this.strikeoutSize = os2.yStrikeoutSize / rate;
211
+ this.ascender = ascender / rate;
212
+ this.descender = descender / rate;
213
+ this.typoAscender = os2.sTypoAscender / rate;
214
+ this.typoDescender = os2.sTypoDescender / rate;
215
+ this.typoLineGap = os2.sTypoLineGap / rate;
216
+ this.winAscent = os2.usWinAscent / rate;
217
+ this.winDescent = os2.usWinDescent / rate;
218
+ this.xHeight = os2.sxHeight / rate;
219
+ this.capHeight = os2.sCapHeight / rate;
202
220
  this.baseline = baseline;
203
221
  this.centerDiviation = advanceHeight / 2 - baseline;
222
+ this.fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
204
223
  return this;
205
224
  }
206
225
  update(fonts) {
@@ -214,11 +233,12 @@ class Character {
214
233
  content,
215
234
  computedStyle: style,
216
235
  baseline,
217
- inlineBox
236
+ inlineBox,
237
+ ascender,
238
+ descender,
239
+ typoAscender,
240
+ fontStyle
218
241
  } = this;
219
- const { os2, head, ascender, descender } = sfnt;
220
- const typoAscender = os2.sTypoAscender;
221
- const fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
222
242
  const { left, top } = inlineBox;
223
243
  const needsItalic = style.fontStyle === "italic" && fontStyle !== "italic";
224
244
  let x = left;
@@ -276,7 +296,6 @@ class Character {
276
296
  }
277
297
  }
278
298
  }
279
- path.addCommands(this._decoration());
280
299
  const fontWeight = style.fontWeight ?? 400;
281
300
  if (fontWeight in fontWeightMap && ((fontWeight === 700 || fontWeight === "bold") && fontStyle !== "bold")) {
282
301
  path.bold(fontWeightMap[fontWeight] * style.fontSize * 0.05);
@@ -290,49 +309,6 @@ class Character {
290
309
  this.glyphBox = this.getGlyphBoundingBox();
291
310
  return this;
292
311
  }
293
- _decoration() {
294
- const { isVertical, underlinePosition, yStrikeoutPosition } = this;
295
- const { textDecoration, fontSize } = this.computedStyle;
296
- const { left, top, width, height } = this.inlineBox;
297
- const lineWidth = 0.1 * fontSize;
298
- let start;
299
- switch (textDecoration) {
300
- case "underline":
301
- if (isVertical) {
302
- start = left;
303
- } else {
304
- start = top + underlinePosition;
305
- }
306
- break;
307
- case "line-through":
308
- if (isVertical) {
309
- start = left + width / 2;
310
- } else {
311
- start = top + yStrikeoutPosition;
312
- }
313
- break;
314
- case "none":
315
- default:
316
- return [];
317
- }
318
- if (isVertical) {
319
- return [
320
- { type: "M", x: start, y: top },
321
- { type: "L", x: start, y: top + height },
322
- { type: "L", x: start + lineWidth, y: top + height },
323
- { type: "L", x: start + lineWidth, y: top },
324
- { type: "Z" }
325
- ];
326
- } else {
327
- return [
328
- { type: "M", x: left, y: start },
329
- { type: "L", x: left + width, y: start },
330
- { type: "L", x: left + width, y: start + lineWidth },
331
- { type: "L", x: left, y: start + lineWidth },
332
- { type: "Z" }
333
- ];
334
- }
335
- }
336
312
  _italic(path, startPoint) {
337
313
  path.skew(-0.24, 0, startPoint || {
338
314
  y: this.inlineBox.top + this.baseline,
@@ -674,7 +650,7 @@ function parseStrokeWidthScale(strokeWidth, fontSize, total) {
674
650
  return strokeWidth / total;
675
651
  }
676
652
  }
677
- function getTransformMatrix(a, b, c, isVertical) {
653
+ function getTransformMatrix(a, b, c, isVertical, type) {
678
654
  let scale;
679
655
  if (!isVertical) {
680
656
  scale = {
@@ -689,10 +665,18 @@ function getTransformMatrix(a, b, c, isVertical) {
689
665
  }
690
666
  const offset = c.center.add(
691
667
  a.center.sub(b.center).scale(scale.x, scale.y)
692
- ).sub({
693
- x: a.width / 2 * scale.x,
694
- y: a.height / 2 * scale.y
695
- });
668
+ );
669
+ if (type === "line") {
670
+ offset.sub({
671
+ x: a.width / 2 * scale.x,
672
+ y: a.height * scale.y
673
+ });
674
+ } else {
675
+ offset.sub({
676
+ x: a.width / 2 * scale.x,
677
+ y: a.height / 2 * scale.y
678
+ });
679
+ }
696
680
  const m = new Matrix3();
697
681
  m.translate(-a.left, -a.top);
698
682
  if (isVertical) {
@@ -721,14 +705,13 @@ function highlight() {
721
705
  paths,
722
706
  update: (text) => {
723
707
  paths.length = 0;
724
- const { characters } = text;
725
- let group;
726
708
  const groups = [];
709
+ let group;
727
710
  let prevStyle;
728
- characters.forEach((character) => {
729
- const { isVertical, computedStyle: style } = character;
711
+ text.forEachCharacter((character) => {
712
+ const { isVertical, computedStyle: style, inlineBox, fontSize } = character;
730
713
  if (!isNone(style.highlightImage) && character.glyphBox) {
731
- if (style.highlightSize !== "1rem" && prevStyle?.highlightImage === style.highlightImage && prevStyle?.highlightSize === style.highlightSize && prevStyle?.highlightStrokeWidth === style.highlightStrokeWidth && prevStyle?.highlightOverflow === style.highlightOverflow && group?.length && (isVertical ? group[0].inlineBox.left === character.inlineBox.left : group[0].inlineBox.top === character.inlineBox.top) && group[0].fontSize === character.fontSize) {
714
+ if (style.highlightSize !== "1rem" && prevStyle?.highlightImage === style.highlightImage && prevStyle?.highlightSize === style.highlightSize && prevStyle?.highlightStrokeWidth === style.highlightStrokeWidth && prevStyle?.highlightOverflow === style.highlightOverflow && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === fontSize) {
732
715
  group.push(character);
733
716
  } else {
734
717
  group = [];
@@ -738,15 +721,15 @@ function highlight() {
738
721
  }
739
722
  prevStyle = style;
740
723
  });
741
- groups.filter((characters2) => characters2.length).map((characters2) => {
742
- const char = characters2[0];
724
+ groups.filter((characters) => characters.length).map((characters) => {
725
+ const char = characters[0];
743
726
  return {
744
- style: char.computedStyle,
745
- baseline: char.baseline,
746
- box: BoundingBox.from(...characters2.map((c) => c.glyphBox))
727
+ char,
728
+ groupBox: BoundingBox.from(...characters.map((c) => c.glyphBox))
747
729
  };
748
730
  }).forEach((group2) => {
749
- const { style, box: groupBox, baseline } = group2;
731
+ const { char, groupBox } = group2;
732
+ const style = char.computedStyle;
750
733
  const { fontSize, writingMode } = style;
751
734
  const isVertical = writingMode.includes("vertical");
752
735
  const strokeWidthScale = parseStrokeWidthScale(style.highlightStrokeWidth, fontSize, groupBox.width);
@@ -757,12 +740,21 @@ function highlight() {
757
740
  const box = getPathsBoundingBox(svgPaths, true);
758
741
  const refBox = getPathsBoundingBox(refPaths, false);
759
742
  const unitWidth = charsPerRepeat ? fontSize * charsPerRepeat : isVertical ? groupBox.height : groupBox.width;
760
- const unitHeight = baseline * 0.8;
743
+ let unitHeight;
744
+ let type;
745
+ if (box.height / refBox.height > 0.8) {
746
+ type = "block";
747
+ unitHeight = groupBox.height;
748
+ } else {
749
+ type = "line";
750
+ unitHeight = char.inlineBox.top - groupBox.top + char.underlinePosition;
751
+ }
761
752
  const transform = getTransformMatrix(
762
753
  box,
763
754
  refBox,
764
755
  new BoundingBox(groupBox.left, groupBox.top, isVertical ? unitHeight : unitWidth, isVertical ? unitWidth : unitHeight),
765
- isVertical
756
+ isVertical,
757
+ type
766
758
  );
767
759
  const styleScale = fontSize / box.width * 2;
768
760
  const total = Math.ceil(groupBox.width / unitWidth);
@@ -919,7 +911,7 @@ function render() {
919
911
  return boxes.length ? BoundingBox.from(...boxes) : void 0;
920
912
  },
921
913
  render: (ctx, text) => {
922
- const { characters, paragraphs, glyphBox, effects, style } = text;
914
+ const { paragraphs, glyphBox, effects, style } = text;
923
915
  function fillBackground(color, box) {
924
916
  ctx.fillStyle = color;
925
917
  ctx.fillRect(box.left, box.top, box.width, box.height);
@@ -938,7 +930,7 @@ function render() {
938
930
  ctx.save();
939
931
  const [a, c, e, b, d, f] = getTransform2D(text, style2).transpose().elements;
940
932
  ctx.transform(a, b, c, d, e, f);
941
- characters.forEach((character) => {
933
+ text.forEachCharacter((character) => {
942
934
  if (character.parent.style?.backgroundColor) {
943
935
  fillBackground(character.parent.style.backgroundColor, character.inlineBox);
944
936
  }
@@ -983,6 +975,112 @@ function getTransform2D(text, style) {
983
975
  return tempM1.clone();
984
976
  }
985
977
 
978
+ function textDecoration() {
979
+ const paths = [];
980
+ return definePlugin({
981
+ name: "textDecoration",
982
+ paths,
983
+ update: (text) => {
984
+ paths.length = 0;
985
+ const groups = [];
986
+ let group;
987
+ let prevStyle;
988
+ text.forEachCharacter((character) => {
989
+ const { computedStyle: style, isVertical, inlineBox, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = character;
990
+ if (!isNone(style.textDecoration)) {
991
+ let flag = false;
992
+ if (prevStyle?.textDecoration === style.textDecoration && prevStyle?.writingMode === style.writingMode && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top)) {
993
+ switch (style.textDecoration) {
994
+ case "underline":
995
+ if (group[0].underlinePosition === underlinePosition && group[0].underlineThickness === underlineThickness) {
996
+ flag = true;
997
+ }
998
+ break;
999
+ case "line-through":
1000
+ if (group[0].strikeoutPosition === strikeoutPosition && group[0].strikeoutSize === strikeoutSize) {
1001
+ flag = true;
1002
+ }
1003
+ break;
1004
+ }
1005
+ }
1006
+ if (flag) {
1007
+ group.push(character);
1008
+ } else {
1009
+ group = [];
1010
+ group.push(character);
1011
+ groups.push(group);
1012
+ }
1013
+ prevStyle = style;
1014
+ } else {
1015
+ prevStyle = void 0;
1016
+ }
1017
+ });
1018
+ groups.forEach((group2) => {
1019
+ const { computedStyle: style, isVertical, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = group2[0];
1020
+ const { textDecoration: textDecoration2 } = style;
1021
+ const { left, top, width, height } = BoundingBox.from(...group2.map((c) => c.inlineBox));
1022
+ let strokePosition = isVertical ? left : top;
1023
+ let strokeWidth = 0;
1024
+ switch (textDecoration2) {
1025
+ case "underline":
1026
+ strokePosition += underlinePosition;
1027
+ strokeWidth = underlineThickness * 2;
1028
+ break;
1029
+ case "line-through":
1030
+ strokePosition += strikeoutPosition;
1031
+ strokeWidth = strikeoutSize * 2;
1032
+ break;
1033
+ }
1034
+ strokePosition -= strokeWidth;
1035
+ if (isVertical) {
1036
+ paths.push(new Path2D([
1037
+ { type: "M", x: strokePosition, y: top },
1038
+ { type: "L", x: strokePosition, y: top + height },
1039
+ { type: "L", x: strokePosition + strokeWidth, y: top + height },
1040
+ { type: "L", x: strokePosition + strokeWidth, y: top },
1041
+ { type: "Z" }
1042
+ ]));
1043
+ } else {
1044
+ paths.push(new Path2D([
1045
+ { type: "M", x: left, y: strokePosition },
1046
+ { type: "L", x: left + width, y: strokePosition },
1047
+ { type: "L", x: left + width, y: strokePosition + strokeWidth },
1048
+ { type: "L", x: left, y: strokePosition + strokeWidth },
1049
+ { type: "Z" }
1050
+ ]));
1051
+ }
1052
+ });
1053
+ },
1054
+ render: (ctx, text) => {
1055
+ const { effects, fontSize } = text;
1056
+ if (effects) {
1057
+ effects.forEach((style) => {
1058
+ ctx.save();
1059
+ const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
1060
+ ctx.transform(a, b, c, d, e, f);
1061
+ paths.forEach((path) => {
1062
+ drawPath({
1063
+ ctx,
1064
+ path,
1065
+ fontSize,
1066
+ ...style
1067
+ });
1068
+ });
1069
+ ctx.restore();
1070
+ });
1071
+ } else {
1072
+ paths.forEach((path) => {
1073
+ drawPath({
1074
+ ctx,
1075
+ path,
1076
+ fontSize
1077
+ });
1078
+ });
1079
+ }
1080
+ }
1081
+ });
1082
+ }
1083
+
986
1084
  var __defProp = Object.defineProperty;
987
1085
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
988
1086
  var __publicField = (obj, key, value) => {
@@ -1057,7 +1155,7 @@ class Text {
1057
1155
  this.measureDom = measureDom;
1058
1156
  this.effects = effects;
1059
1157
  this.fonts = fonts;
1060
- this.use(render()).use(highlight()).use(listStyle());
1158
+ this.use(listStyle()).use(textDecoration()).use(highlight()).use(render());
1061
1159
  }
1062
1160
  get fontSize() {
1063
1161
  return this.computedStyle.fontSize;
@@ -1072,6 +1170,16 @@ class Text {
1072
1170
  this.plugins.set(plugin.name, plugin);
1073
1171
  return this;
1074
1172
  }
1173
+ forEachCharacter(handle) {
1174
+ this.paragraphs.forEach((p, paragraphIndex) => {
1175
+ p.fragments.forEach((f, fragmentIndex) => {
1176
+ f.characters.forEach((c, characterIndex) => {
1177
+ handle(c, { paragraphIndex, fragmentIndex, characterIndex });
1178
+ });
1179
+ });
1180
+ });
1181
+ return this;
1182
+ }
1075
1183
  updateParagraphs() {
1076
1184
  this.computedStyle = { ...defaultTextStyles, ...this.style };
1077
1185
  let { content, computedStyle: style } = this;
@@ -1243,4 +1351,4 @@ function renderText(options) {
1243
1351
  return new Text(options).render(options);
1244
1352
  }
1245
1353
 
1246
- export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, highlight, isNone, listStyle, measureText, parseColor, render, renderText, setupView, uploadColor, uploadColors };
1354
+ export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, highlight, isNone, listStyle, measureText, parseColor, render, renderText, setupView, textDecoration, uploadColor, uploadColors };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modern-text",
3
3
  "type": "module",
4
- "version": "0.4.2",
4
+ "version": "0.4.4",
5
5
  "packageManager": "pnpm@9.9.0",
6
6
  "description": "Measure and render text in a way that describes the DOM.",
7
7
  "author": "wxm",