modern-text 0.4.2 → 0.4.3

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,
@@ -721,14 +697,13 @@ function highlight() {
721
697
  paths,
722
698
  update: (text) => {
723
699
  paths.length = 0;
724
- const { characters } = text;
725
- let group;
726
700
  const groups = [];
701
+ let group;
727
702
  let prevStyle;
728
- characters.forEach((character) => {
729
- const { isVertical, computedStyle: style } = character;
703
+ text.forEachCharacter((character) => {
704
+ const { isVertical, computedStyle: style, inlineBox, fontSize } = character;
730
705
  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) {
706
+ 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
707
  group.push(character);
733
708
  } else {
734
709
  group = [];
@@ -738,15 +713,15 @@ function highlight() {
738
713
  }
739
714
  prevStyle = style;
740
715
  });
741
- groups.filter((characters2) => characters2.length).map((characters2) => {
742
- const char = characters2[0];
716
+ groups.filter((characters) => characters.length).map((characters) => {
717
+ const char = characters[0];
743
718
  return {
744
719
  style: char.computedStyle,
745
- baseline: char.baseline,
746
- box: BoundingBox.from(...characters2.map((c) => c.glyphBox))
720
+ unitHeight: char.typoAscender + char.typoDescender,
721
+ box: BoundingBox.from(...characters.map((c) => c.glyphBox))
747
722
  };
748
723
  }).forEach((group2) => {
749
- const { style, box: groupBox, baseline } = group2;
724
+ const { style, box: groupBox, unitHeight } = group2;
750
725
  const { fontSize, writingMode } = style;
751
726
  const isVertical = writingMode.includes("vertical");
752
727
  const strokeWidthScale = parseStrokeWidthScale(style.highlightStrokeWidth, fontSize, groupBox.width);
@@ -757,7 +732,6 @@ function highlight() {
757
732
  const box = getPathsBoundingBox(svgPaths, true);
758
733
  const refBox = getPathsBoundingBox(refPaths, false);
759
734
  const unitWidth = charsPerRepeat ? fontSize * charsPerRepeat : isVertical ? groupBox.height : groupBox.width;
760
- const unitHeight = baseline * 0.8;
761
735
  const transform = getTransformMatrix(
762
736
  box,
763
737
  refBox,
@@ -919,7 +893,7 @@ function render() {
919
893
  return boxes.length ? BoundingBox.from(...boxes) : void 0;
920
894
  },
921
895
  render: (ctx, text) => {
922
- const { characters, paragraphs, glyphBox, effects, style } = text;
896
+ const { paragraphs, glyphBox, effects, style } = text;
923
897
  function fillBackground(color, box) {
924
898
  ctx.fillStyle = color;
925
899
  ctx.fillRect(box.left, box.top, box.width, box.height);
@@ -938,7 +912,7 @@ function render() {
938
912
  ctx.save();
939
913
  const [a, c, e, b, d, f] = getTransform2D(text, style2).transpose().elements;
940
914
  ctx.transform(a, b, c, d, e, f);
941
- characters.forEach((character) => {
915
+ text.forEachCharacter((character) => {
942
916
  if (character.parent.style?.backgroundColor) {
943
917
  fillBackground(character.parent.style.backgroundColor, character.inlineBox);
944
918
  }
@@ -983,6 +957,112 @@ function getTransform2D(text, style) {
983
957
  return tempM1.clone();
984
958
  }
985
959
 
960
+ function textDecoration() {
961
+ const paths = [];
962
+ return definePlugin({
963
+ name: "textDecoration",
964
+ paths,
965
+ update: (text) => {
966
+ paths.length = 0;
967
+ const groups = [];
968
+ let group;
969
+ let prevStyle;
970
+ text.forEachCharacter((character) => {
971
+ const { computedStyle: style, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = character;
972
+ if (!isNone(style.textDecoration)) {
973
+ let flag = false;
974
+ if (prevStyle?.textDecoration === style.textDecoration && prevStyle?.writingMode === style.writingMode) {
975
+ switch (style.textDecoration) {
976
+ case "underline":
977
+ if (group[0].underlinePosition === underlinePosition && group[0].underlineThickness === underlineThickness) {
978
+ flag = true;
979
+ }
980
+ break;
981
+ case "line-through":
982
+ if (group[0].strikeoutPosition === strikeoutPosition && group[0].strikeoutSize === strikeoutSize) {
983
+ flag = true;
984
+ }
985
+ break;
986
+ }
987
+ }
988
+ if (flag) {
989
+ group.push(character);
990
+ } else {
991
+ group = [];
992
+ group.push(character);
993
+ groups.push(group);
994
+ }
995
+ prevStyle = style;
996
+ } else {
997
+ prevStyle = void 0;
998
+ }
999
+ });
1000
+ groups.forEach((group2) => {
1001
+ const { computedStyle: style, isVertical, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = group2[0];
1002
+ const { textDecoration: textDecoration2 } = style;
1003
+ const { left, top, width, height } = BoundingBox.from(...group2.map((c) => c.inlineBox));
1004
+ let strokePosition = isVertical ? left : top;
1005
+ let strokeWidth = 0;
1006
+ switch (textDecoration2) {
1007
+ case "underline":
1008
+ strokePosition += underlinePosition;
1009
+ strokeWidth = underlineThickness * 2;
1010
+ break;
1011
+ case "line-through":
1012
+ strokePosition += strikeoutPosition;
1013
+ strokeWidth = strikeoutSize * 2;
1014
+ break;
1015
+ }
1016
+ strokePosition -= strokeWidth;
1017
+ if (isVertical) {
1018
+ paths.push(new Path2D([
1019
+ { type: "M", x: strokePosition, y: top },
1020
+ { type: "L", x: strokePosition, y: top + height },
1021
+ { type: "L", x: strokePosition + strokeWidth, y: top + height },
1022
+ { type: "L", x: strokePosition + strokeWidth, y: top },
1023
+ { type: "Z" }
1024
+ ]));
1025
+ } else {
1026
+ paths.push(new Path2D([
1027
+ { type: "M", x: left, y: strokePosition },
1028
+ { type: "L", x: left + width, y: strokePosition },
1029
+ { type: "L", x: left + width, y: strokePosition + strokeWidth },
1030
+ { type: "L", x: left, y: strokePosition + strokeWidth },
1031
+ { type: "Z" }
1032
+ ]));
1033
+ }
1034
+ });
1035
+ },
1036
+ render: (ctx, text) => {
1037
+ const { effects, fontSize } = text;
1038
+ if (effects) {
1039
+ effects.forEach((style) => {
1040
+ ctx.save();
1041
+ const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
1042
+ ctx.transform(a, b, c, d, e, f);
1043
+ paths.forEach((path) => {
1044
+ drawPath({
1045
+ ctx,
1046
+ path,
1047
+ fontSize,
1048
+ ...style
1049
+ });
1050
+ });
1051
+ ctx.restore();
1052
+ });
1053
+ } else {
1054
+ paths.forEach((path) => {
1055
+ drawPath({
1056
+ ctx,
1057
+ path,
1058
+ fontSize
1059
+ });
1060
+ });
1061
+ }
1062
+ }
1063
+ });
1064
+ }
1065
+
986
1066
  var __defProp = Object.defineProperty;
987
1067
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
988
1068
  var __publicField = (obj, key, value) => {
@@ -1057,7 +1137,7 @@ class Text {
1057
1137
  this.measureDom = measureDom;
1058
1138
  this.effects = effects;
1059
1139
  this.fonts = fonts;
1060
- this.use(render()).use(highlight()).use(listStyle());
1140
+ this.use(listStyle()).use(textDecoration()).use(highlight()).use(render());
1061
1141
  }
1062
1142
  get fontSize() {
1063
1143
  return this.computedStyle.fontSize;
@@ -1072,6 +1152,16 @@ class Text {
1072
1152
  this.plugins.set(plugin.name, plugin);
1073
1153
  return this;
1074
1154
  }
1155
+ forEachCharacter(handle) {
1156
+ this.paragraphs.forEach((p, paragraphIndex) => {
1157
+ p.fragments.forEach((f, fragmentIndex) => {
1158
+ f.characters.forEach((c, characterIndex) => {
1159
+ handle(c, { paragraphIndex, fragmentIndex, characterIndex });
1160
+ });
1161
+ });
1162
+ });
1163
+ return this;
1164
+ }
1075
1165
  updateParagraphs() {
1076
1166
  this.computedStyle = { ...defaultTextStyles, ...this.style };
1077
1167
  let { content, computedStyle: style } = this;
@@ -1243,4 +1333,4 @@ function renderText(options) {
1243
1333
  return new Text(options).render(options);
1244
1334
  }
1245
1335
 
1246
- export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, highlight, isNone, listStyle, measureText, parseColor, render, renderText, setupView, uploadColor, uploadColors };
1336
+ 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.3",
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",