zrender-nightly 5.7.0-dev.20250620 → 5.7.0-dev.20250622

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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/build/prepublish.js +20 -0
  3. package/dist/zrender.js +563 -277
  4. package/dist/zrender.js.map +1 -1
  5. package/dist/zrender.min.js +1 -1
  6. package/lib/Element.d.ts +4 -0
  7. package/lib/Element.js +34 -16
  8. package/lib/Handler.js +1 -1
  9. package/lib/Storage.js +20 -20
  10. package/lib/canvas/graphic.js +1 -1
  11. package/lib/contain/text.d.ts +14 -2
  12. package/lib/contain/text.js +65 -15
  13. package/lib/core/BoundingRect.d.ts +25 -3
  14. package/lib/core/BoundingRect.js +182 -76
  15. package/lib/core/OrientedBoundingRect.d.ts +2 -2
  16. package/lib/core/OrientedBoundingRect.js +50 -34
  17. package/lib/core/PathProxy.d.ts +1 -0
  18. package/lib/core/PathProxy.js +16 -1
  19. package/lib/core/dom.d.ts +1 -0
  20. package/lib/core/dom.js +17 -0
  21. package/lib/core/env.js +15 -10
  22. package/lib/core/types.d.ts +1 -0
  23. package/lib/core/util.d.ts +1 -0
  24. package/lib/core/util.js +2 -1
  25. package/lib/graphic/Displayable.js +1 -1
  26. package/lib/graphic/Text.d.ts +4 -2
  27. package/lib/graphic/Text.js +23 -14
  28. package/lib/graphic/helper/parseText.d.ts +13 -4
  29. package/lib/graphic/helper/parseText.js +110 -54
  30. package/lib/svg-legacy/helper/ClippathManager.js +6 -6
  31. package/lib/tool/color.d.ts +3 -1
  32. package/lib/tool/color.js +6 -6
  33. package/lib/tool/parseSVG.js +11 -0
  34. package/lib/tool/path.js +7 -4
  35. package/lib/zrender.d.ts +1 -1
  36. package/lib/zrender.js +1 -1
  37. package/package.json +3 -2
  38. package/src/Element.ts +69 -16
  39. package/src/Handler.ts +1 -1
  40. package/src/Storage.ts +25 -24
  41. package/src/canvas/graphic.ts +1 -1
  42. package/src/canvas/helper.ts +1 -1
  43. package/src/contain/text.ts +103 -19
  44. package/src/core/BoundingRect.ts +308 -87
  45. package/src/core/OrientedBoundingRect.ts +86 -46
  46. package/src/core/PathProxy.ts +17 -1
  47. package/src/core/Transformable.ts +2 -0
  48. package/src/core/dom.ts +24 -0
  49. package/src/core/env.ts +31 -24
  50. package/src/core/matrix.ts +2 -1
  51. package/src/core/types.ts +2 -0
  52. package/src/core/util.ts +4 -2
  53. package/src/graphic/Displayable.ts +1 -3
  54. package/src/graphic/Group.ts +2 -0
  55. package/src/graphic/Text.ts +68 -21
  56. package/src/graphic/helper/parseText.ts +211 -83
  57. package/src/svg-legacy/helper/ClippathManager.ts +5 -5
  58. package/src/tool/color.ts +15 -11
  59. package/src/tool/parseSVG.ts +12 -1
  60. package/src/tool/path.ts +9 -4
  61. package/src/zrender.ts +1 -1
package/src/Element.ts CHANGED
@@ -31,6 +31,7 @@ import Point from './core/Point';
31
31
  import { LIGHT_LABEL_COLOR, DARK_LABEL_COLOR } from './config';
32
32
  import { parse, stringify } from './tool/color';
33
33
  import { REDRAW_BIT } from './graphic/constants';
34
+ import { invert } from './core/matrix';
34
35
 
35
36
  export interface ElementAnimateConfig {
36
37
  duration?: number
@@ -81,7 +82,11 @@ export interface ElementTextConfig {
81
82
 
82
83
  /**
83
84
  * Rect that text will be positioned.
84
- * Default to be the rect of element.
85
+ * Default to be the boundingRect of the host element.
86
+ * The coords of `layoutRect` is based on the target element, but not global.
87
+ *
88
+ * [NOTICE]: boundingRect includes `lineWidth`, which is inconsistent with
89
+ * the general element placement principle, where `lineWidth` is not counted.
85
90
  */
86
91
  layoutRect?: RectLike
87
92
 
@@ -109,6 +114,10 @@ export interface ElementTextConfig {
109
114
 
110
115
  /**
111
116
  * If use local user space. Which will apply host's transform
117
+ *
118
+ * [NOTICE]: If the host element may rotate to non-parallel to screen x/y,
119
+ * need to use `local:true`, otherwise the transformed layout rect may not be expected.
120
+ *
112
121
  * @default false
113
122
  */
114
123
  local?: boolean
@@ -166,6 +175,16 @@ export interface ElementTextConfig {
166
175
  * In case position is not using builtin `inside` hints.
167
176
  */
168
177
  inside?: boolean
178
+
179
+ /**
180
+ * Auto calculate overflow area by `textConfig.layoutRect` (if any) or `host.boundingRect`.
181
+ * It makes sense only if label is inside. It ensure the text does not overflow the host.
182
+ * Useful in `text.style.overflow` and `text.style.lineOverflow`.
183
+ *
184
+ * If `textConfig.rotation` or `text.rotation exists`, it works correctly only when the rotated text is parallel
185
+ * to its host (i.e. 0, PI/2, PI, PI*3/2, 2*PI, ...). Do not supported other cases until a real scenario arises.
186
+ */
187
+ autoOverflowArea?: boolean
169
188
  }
170
189
  export interface ElementTextGuideLineConfig {
171
190
  /**
@@ -238,6 +257,7 @@ export interface ElementProps extends Partial<ElementEventHandlerProps>, Partial
238
257
  draggable?: boolean | 'horizontal' | 'vertical'
239
258
 
240
259
  silent?: boolean
260
+ ignoreHostSilent?: boolean
241
261
 
242
262
  ignoreClip?: boolean
243
263
  globalScaleRatio?: number
@@ -277,8 +297,9 @@ export type ElementCalculateTextPosition = (
277
297
  rect: RectLike
278
298
  ) => TextPositionCalculationResult;
279
299
 
280
- let tmpTextPosCalcRes = {} as TextPositionCalculationResult;
281
- let tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
300
+ const tmpTextPosCalcRes = {} as TextPositionCalculationResult;
301
+ const tmpBoundingRect = new BoundingRect(0, 0, 0, 0);
302
+ const tmpInnerTextTrans: number[] = [];
282
303
 
283
304
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
284
305
  interface Element<Props extends ElementProps = ElementProps> extends Transformable,
@@ -313,6 +334,14 @@ class Element<Props extends ElementProps = ElementProps> {
313
334
  */
314
335
  silent: boolean
315
336
 
337
+ /**
338
+ * When this element has `__hostTarget` (e.g., this is a `textContent`), whether
339
+ * its silent is controlled by that host silent. They may need separate silent
340
+ * settings. e.g., the host do not have `fill` but only `stroke`, or their mouse
341
+ * events serve for different features.
342
+ */
343
+ ignoreHostSilent: boolean
344
+
316
345
  /**
317
346
  * 是否是 Group
318
347
  */
@@ -368,6 +397,8 @@ class Element<Props extends ElementProps = ElementProps> {
368
397
  */
369
398
  __inHover: boolean
370
399
 
400
+ __clipPaths?: Path[]
401
+
371
402
  /**
372
403
  * path to clip the elements and its children, if it is a group.
373
404
  * @see http://www.w3.org/TR/2dcontext/#clipping-region
@@ -511,9 +542,12 @@ class Element<Props extends ElementProps = ElementProps> {
511
542
  // Reset x/y/rotation
512
543
  innerTransformable.copyTransform(textEl);
513
544
 
514
- // Force set attached text's position if `position` is in config.
515
- if (textConfig.position != null) {
516
- let layoutRect = tmpBoundingRect;
545
+ const hasPosition = textConfig.position != null;
546
+ const autoOverflowArea = textConfig.autoOverflowArea;
547
+
548
+ let layoutRect: BoundingRect;
549
+ if (autoOverflowArea || hasPosition) {
550
+ layoutRect = tmpBoundingRect;
517
551
  if (textConfig.layoutRect) {
518
552
  layoutRect.copy(textConfig.layoutRect);
519
553
  }
@@ -523,7 +557,10 @@ class Element<Props extends ElementProps = ElementProps> {
523
557
  if (!isLocal) {
524
558
  layoutRect.applyTransform(this.transform);
525
559
  }
560
+ }
526
561
 
562
+ // Force set attached text's position if `position` is in config.
563
+ if (hasPosition) {
527
564
  if (this.calculateTextPosition) {
528
565
  this.calculateTextPosition(tmpTextPosCalcRes, textConfig, layoutRect);
529
566
  }
@@ -578,11 +615,27 @@ class Element<Props extends ElementProps = ElementProps> {
578
615
  }
579
616
  }
580
617
 
618
+ const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});
619
+
620
+ if (autoOverflowArea) {
621
+ const overflowRect = innerTextDefaultStyle.overflowRect =
622
+ innerTextDefaultStyle.overflowRect || new BoundingRect(0, 0, 0, 0);
623
+ innerTransformable.getLocalTransform(tmpInnerTextTrans);
624
+ invert(tmpInnerTextTrans, tmpInnerTextTrans);
625
+ BoundingRect.copy(overflowRect, layoutRect);
626
+ // If transform to a non-orthogonal state (e.g. rotate PI/3), the result of this "apply"
627
+ // is not expected. But we don't need to address it until a real scenario arises.
628
+ overflowRect.applyTransform(tmpInnerTextTrans);
629
+ }
630
+ else {
631
+ innerTextDefaultStyle.overflowRect = null;
632
+ }
633
+ // [CAUTION] Do not change `innerTransformable` below.
634
+
581
635
  // Calculate text color
582
636
  const isInside = textConfig.inside == null // Force to be inside or not.
583
637
  ? (typeof textConfig.position === 'string' && textConfig.position.indexOf('inside') >= 0)
584
638
  : textConfig.inside;
585
- const innerTextDefaultStyle = this._innerTextDefaultStyle || (this._innerTextDefaultStyle = {});
586
639
 
587
640
  let textFill;
588
641
  let textStroke;
@@ -1021,16 +1074,16 @@ class Element<Props extends ElementProps = ElementProps> {
1021
1074
  * Return if el.silent or any ancestor element has silent true.
1022
1075
  */
1023
1076
  isSilent() {
1024
- let isSilent = this.silent;
1025
- let ancestor = this.parent;
1026
- while (!isSilent && ancestor) {
1027
- if (ancestor.silent) {
1028
- isSilent = true;
1029
- break;
1077
+ // Follow the logic of `Handler.ts`#`isHover`.
1078
+ let el: Element = this;
1079
+ while (el) {
1080
+ if (el.silent) {
1081
+ return true;
1030
1082
  }
1031
- ancestor = ancestor.parent;
1083
+ const hostEl = el.__hostTarget;
1084
+ el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
1032
1085
  }
1033
- return isSilent;
1086
+ return false;
1034
1087
  }
1035
1088
 
1036
1089
  /**
@@ -1637,6 +1690,7 @@ class Element<Props extends ElementProps = ElementProps> {
1637
1690
 
1638
1691
  elProto.ignore =
1639
1692
  elProto.silent =
1693
+ elProto.ignoreHostSilent =
1640
1694
  elProto.isGroup =
1641
1695
  elProto.draggable =
1642
1696
  elProto.dragging =
@@ -2026,5 +2080,4 @@ function animateToShallow<T>(
2026
2080
  }
2027
2081
  }
2028
2082
 
2029
-
2030
2083
  export default Element;
package/src/Handler.ts CHANGED
@@ -504,7 +504,7 @@ function isHover(displayable: Displayable, x: number, y: number) {
504
504
  // Consider when el is textContent, also need to be silent
505
505
  // if any of its host el and its ancestors is silent.
506
506
  const hostEl = el.__hostTarget;
507
- el = hostEl ? hostEl : el.parent;
507
+ el = hostEl ? (el.ignoreHostSilent ? null : hostEl) : el.parent;
508
508
  }
509
509
  return isSilent ? SILENT : true;
510
510
  }
package/src/Storage.ts CHANGED
@@ -8,6 +8,7 @@ import timsort from './core/timsort';
8
8
  import Displayable from './graphic/Displayable';
9
9
  import Path from './graphic/Path';
10
10
  import { REDRAW_BIT } from './graphic/constants';
11
+ import { NullUndefined } from './core/types';
11
12
 
12
13
  let invalidZErrorLogged = false;
13
14
  function logInvalidZError() {
@@ -83,7 +84,7 @@ export default class Storage {
83
84
 
84
85
  private _updateAndAddDisplayable(
85
86
  el: Element,
86
- clipPaths: Path[],
87
+ parentClipPaths: Path[] | NullUndefined,
87
88
  includeIgnore?: boolean
88
89
  ) {
89
90
  if (el.ignore && !includeIgnore) {
@@ -95,18 +96,21 @@ export default class Storage {
95
96
  el.afterUpdate();
96
97
 
97
98
  const userSetClipPath = el.getClipPath();
98
-
99
- if (el.ignoreClip) {
100
- clipPaths = null;
101
- }
102
- else if (userSetClipPath) {
103
-
104
- // FIXME 效率影响
105
- if (clipPaths) {
106
- clipPaths = clipPaths.slice();
99
+ const parentHasClipPaths = parentClipPaths && parentClipPaths.length;
100
+ let clipPathIdx = 0;
101
+ let thisClipPaths = el.__clipPaths;
102
+
103
+ if (!el.ignoreClip
104
+ && (parentHasClipPaths || userSetClipPath)
105
+ ) { // has clipPath in this pass
106
+ if (!thisClipPaths) {
107
+ thisClipPaths = el.__clipPaths = [];
107
108
  }
108
- else {
109
- clipPaths = [];
109
+ if (parentHasClipPaths) {
110
+ // PENDING: performance?
111
+ for (let idx = 0; idx < parentClipPaths.length; idx++) {
112
+ thisClipPaths[clipPathIdx++] = parentClipPaths[idx];
113
+ }
110
114
  }
111
115
 
112
116
  let currentClipPath = userSetClipPath;
@@ -118,13 +122,17 @@ export default class Storage {
118
122
  currentClipPath.parent = parentClipPath as Group;
119
123
  currentClipPath.updateTransform();
120
124
 
121
- clipPaths.push(currentClipPath);
125
+ thisClipPaths[clipPathIdx++] = currentClipPath;
122
126
 
123
127
  parentClipPath = currentClipPath;
124
128
  currentClipPath = currentClipPath.getClipPath();
125
129
  }
126
130
  }
127
131
 
132
+ if (thisClipPaths) { // Remove other old clipPath in array.
133
+ thisClipPaths.length = clipPathIdx;
134
+ }
135
+
128
136
  // ZRText and Group and combining morphing Path may use children
129
137
  if ((el as GroupLike).childrenRef) {
130
138
  const children = (el as GroupLike).childrenRef();
@@ -137,7 +145,7 @@ export default class Storage {
137
145
  child.__dirty |= REDRAW_BIT;
138
146
  }
139
147
 
140
- this._updateAndAddDisplayable(child, clipPaths, includeIgnore);
148
+ this._updateAndAddDisplayable(child, thisClipPaths, includeIgnore);
141
149
  }
142
150
 
143
151
  // Mark group clean here
@@ -146,13 +154,6 @@ export default class Storage {
146
154
  }
147
155
  else {
148
156
  const disp = el as Displayable;
149
- // Element is displayable
150
- if (clipPaths && clipPaths.length) {
151
- disp.__clipPaths = clipPaths;
152
- }
153
- else if (disp.__clipPaths && disp.__clipPaths.length > 0) {
154
- disp.__clipPaths = [];
155
- }
156
157
 
157
158
  // Avoid invalid z, z2, zlevel cause sorting error.
158
159
  if (isNaN(disp.z)) {
@@ -174,18 +175,18 @@ export default class Storage {
174
175
  // Add decal
175
176
  const decalEl = (el as Path).getDecalElement && (el as Path).getDecalElement();
176
177
  if (decalEl) {
177
- this._updateAndAddDisplayable(decalEl, clipPaths, includeIgnore);
178
+ this._updateAndAddDisplayable(decalEl, thisClipPaths, includeIgnore);
178
179
  }
179
180
 
180
181
  // Add attached text element and guide line.
181
182
  const textGuide = el.getTextGuideLine();
182
183
  if (textGuide) {
183
- this._updateAndAddDisplayable(textGuide, clipPaths, includeIgnore);
184
+ this._updateAndAddDisplayable(textGuide, thisClipPaths, includeIgnore);
184
185
  }
185
186
 
186
187
  const textEl = el.getTextContent();
187
188
  if (textEl) {
188
- this._updateAndAddDisplayable(textEl, clipPaths, includeIgnore);
189
+ this._updateAndAddDisplayable(textEl, thisClipPaths, includeIgnore);
189
190
  }
190
191
  }
191
192
 
@@ -152,7 +152,7 @@ function brushPath(ctx: CanvasRenderingContext2D, el: Path, style: PathStyleProp
152
152
  strokePattern = (dirtyFlag || !el.__canvasStrokePattern)
153
153
  ? createCanvasPattern(ctx, stroke as ImagePatternObject, el)
154
154
  : el.__canvasStrokePattern;
155
- el.__canvasStrokePattern = fillPattern;
155
+ el.__canvasStrokePattern = strokePattern;
156
156
  }
157
157
  // Use the gradient or pattern
158
158
  if (hasFillGradient) {
@@ -82,8 +82,8 @@ export function getCanvasGradient(this: void, ctx: CanvasRenderingContext2D, obj
82
82
  return canvasGradient;
83
83
  }
84
84
 
85
+ // [CAVEAT] Assume the clipPaths array is never modified during a batch of `isClipPathChanged` calling.
85
86
  export function isClipPathChanged(clipPaths: Path[], prevClipPaths: Path[]): boolean {
86
- // displayable.__clipPaths can only be `null`/`undefined` or an non-empty array.
87
87
  if (clipPaths === prevClipPaths || (!clipPaths && !prevClipPaths)) {
88
88
  return false;
89
89
  }
@@ -1,25 +1,110 @@
1
1
  import BoundingRect, { RectLike } from '../core/BoundingRect';
2
- import { Dictionary, TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types';
2
+ import { TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types';
3
3
  import LRU from '../core/LRU';
4
4
  import { DEFAULT_FONT, platformApi } from '../core/platform';
5
5
 
6
- let textWidthCache: Dictionary<LRU<number>> = {};
7
-
6
+ /**
7
+ * @deprecated But keep for possible outside usage.
8
+ * Use `ensureFontMeasureInfo` + `measureWidth` instead.
9
+ */
8
10
  export function getWidth(text: string, font: string): number {
11
+ return measureWidth(ensureFontMeasureInfo(font), text);
12
+ }
13
+
14
+ export interface FontMeasureInfo {
15
+ font: string;
16
+ strWidthCache: LRU<number>;
17
+ // Key: char code, index: 0~127 (include 127)
18
+ asciiWidthMap: number[] | null | undefined;
19
+ asciiWidthMapTried: boolean;
20
+ // Default width char width used both in non-ascii and line height.
21
+ stWideCharWidth: number;
22
+ // Default asc char width
23
+ asciiCharWidth: number;
24
+ }
25
+
26
+ export function ensureFontMeasureInfo(font: string): FontMeasureInfo {
27
+ if (!_fontMeasureInfoCache) {
28
+ _fontMeasureInfoCache = new LRU(100);
29
+ }
9
30
  font = font || DEFAULT_FONT;
10
- let cacheOfFont = textWidthCache[font];
11
- if (!cacheOfFont) {
12
- cacheOfFont = textWidthCache[font] = new LRU(500);
31
+ let measureInfo = _fontMeasureInfoCache.get(font);
32
+ if (!measureInfo) {
33
+ measureInfo = {
34
+ font: font,
35
+ strWidthCache: new LRU(500),
36
+ asciiWidthMap: null, // Init lazily for performance.
37
+ asciiWidthMapTried: false,
38
+ // FIXME
39
+ // Other languages?
40
+ // FIXME
41
+ // Consider proportional font?
42
+ stWideCharWidth: platformApi.measureText('国', font).width,
43
+ asciiCharWidth: platformApi.measureText('a', font).width,
44
+ };
45
+ _fontMeasureInfoCache.put(font, measureInfo);
13
46
  }
14
- let width = cacheOfFont.get(text);
15
- if (width == null) {
16
- width = platformApi.measureText(text, font).width;
17
- cacheOfFont.put(text, width);
47
+ return measureInfo;
48
+ }
49
+ let _fontMeasureInfoCache: LRU<FontMeasureInfo>;
50
+
51
+ /**
52
+ * For getting more precise result in truncate.
53
+ * non-monospace font vary in char width.
54
+ * But if it is time consuming in some platform, return null/undefined.
55
+ * @return Key: char code, index: 0~127 (include 127)
56
+ */
57
+ function tryCreateASCIIWidthMap(font: string): FontMeasureInfo['asciiWidthMap'] {
58
+ // PENDING: is it necessary? Re-examine it if bad case reported.
59
+ if (_getASCIIWidthMapLongCount >= GET_ASCII_WIDTH_LONG_COUNT_MAX) {
60
+ return;
18
61
  }
62
+ font = font || DEFAULT_FONT;
63
+ const asciiWidthMap = [];
64
+ const start = +(new Date());
65
+ // 0~31 and 127 may also have width, and may vary in some fonts.
66
+ for (let code = 0; code <= 127; code++) {
67
+ asciiWidthMap[code] = platformApi.measureText(String.fromCharCode(code), font).width;
68
+ }
69
+ const cost = +(new Date()) - start;
70
+ if (cost > 16) {
71
+ _getASCIIWidthMapLongCount = GET_ASCII_WIDTH_LONG_COUNT_MAX;
72
+ }
73
+ else if (cost > 2) {
74
+ _getASCIIWidthMapLongCount++;
75
+ }
76
+ return asciiWidthMap;
77
+ }
78
+ let _getASCIIWidthMapLongCount: number = 0;
79
+ const GET_ASCII_WIDTH_LONG_COUNT_MAX = 5;
19
80
 
81
+ /**
82
+ * Hot path, performance sensitive.
83
+ */
84
+ export function measureCharWidth(fontMeasureInfo: FontMeasureInfo, charCode: number): number {
85
+ if (!fontMeasureInfo.asciiWidthMapTried) {
86
+ fontMeasureInfo.asciiWidthMap = tryCreateASCIIWidthMap(fontMeasureInfo.font);
87
+ fontMeasureInfo.asciiWidthMapTried = true;
88
+ }
89
+ return (0 <= charCode && charCode <= 127)
90
+ ? (fontMeasureInfo.asciiWidthMap != null
91
+ ? fontMeasureInfo.asciiWidthMap[charCode]
92
+ : fontMeasureInfo.asciiCharWidth
93
+ )
94
+ : fontMeasureInfo.stWideCharWidth;
95
+ }
96
+
97
+ export function measureWidth(fontMeasureInfo: FontMeasureInfo, text: string): number {
98
+ const strWidthCache = fontMeasureInfo.strWidthCache;
99
+ let width = strWidthCache.get(text);
100
+ if (width == null) {
101
+ width = platformApi.measureText(text, fontMeasureInfo.font).width;
102
+ strWidthCache.put(text, width);
103
+ }
20
104
  return width;
21
105
  }
22
106
 
107
+
23
108
  /**
24
109
  *
25
110
  * Get bounding rect for inner usage(TSpan)
@@ -31,7 +116,7 @@ export function innerGetBoundingRect(
31
116
  textAlign?: TextAlign,
32
117
  textBaseline?: TextVerticalAlign
33
118
  ): BoundingRect {
34
- const width = getWidth(text, font);
119
+ const width = measureWidth(ensureFontMeasureInfo(font), text);
35
120
  const height = getLineHeight(font);
36
121
 
37
122
  const x = adjustTextX(0, width, textAlign);
@@ -68,31 +153,30 @@ export function getBoundingRect(
68
153
  }
69
154
  }
70
155
 
71
- export function adjustTextX(x: number, width: number, textAlign: TextAlign): number {
156
+ export function adjustTextX(x: number, width: number, textAlign: TextAlign, inverse?: boolean): number {
72
157
  // TODO Right to left language
73
158
  if (textAlign === 'right') {
74
- x -= width;
159
+ !inverse ? (x -= width) : (x += width);
75
160
  }
76
161
  else if (textAlign === 'center') {
77
- x -= width / 2;
162
+ !inverse ? (x -= width / 2) : (x += width / 2);
78
163
  }
79
164
  return x;
80
165
  }
81
166
 
82
- export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign): number {
167
+ export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign, inverse?: boolean): number {
83
168
  if (verticalAlign === 'middle') {
84
- y -= height / 2;
169
+ !inverse ? (y -= height / 2) : (y += height / 2);
85
170
  }
86
171
  else if (verticalAlign === 'bottom') {
87
- y -= height;
172
+ !inverse ? (y -= height) : (y += height);
88
173
  }
89
174
  return y;
90
175
  }
91
176
 
92
-
93
177
  export function getLineHeight(font?: string): number {
94
178
  // FIXME A rough approach.
95
- return getWidth('国', font);
179
+ return ensureFontMeasureInfo(font).stWideCharWidth;
96
180
  }
97
181
 
98
182
  export function measureText(text: string, font?: string): {