chat-layout 1.2.0-1 → 1.2.0-2

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/index.mjs CHANGED
@@ -2386,63 +2386,65 @@ function measureRichFragmentShift(ctx, font) {
2386
2386
  }
2387
2387
  function materializeRichFragments(ctx, spans, defaultColor, atoms) {
2388
2388
  const fragments = [];
2389
- let pendingGapBefore = 0;
2390
- for (const atom of atoms) {
2391
- const occupiedWidth = atom.width + atom.extraWidthAfter;
2392
- if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) {
2393
- pendingGapBefore += occupiedWidth;
2394
- continue;
2395
- }
2396
- const span = spans[atom.itemIndex];
2397
- const font = span?.font ?? atom.font;
2398
- const color = span?.color ?? defaultColor;
2399
- const previous = fragments[fragments.length - 1];
2400
- if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
2401
- previous.text += atom.text;
2402
- previous.occupiedWidth += occupiedWidth;
2403
- continue;
2404
- }
2405
- fragments.push({
2406
- itemIndex: atom.itemIndex,
2407
- text: atom.text,
2408
- font,
2409
- color,
2410
- gapBefore: pendingGapBefore,
2411
- occupiedWidth,
2412
- shift: measureRichFragmentShift(ctx, font)
2413
- });
2414
- pendingGapBefore = 0;
2415
- }
2389
+ let pendingGap = {
2390
+ gapBefore: 0,
2391
+ gapAtomCount: 0,
2392
+ gapSpaceCount: 0
2393
+ };
2394
+ for (const atom of atoms) pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
2416
2395
  return fragments;
2417
2396
  }
2418
- function appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore) {
2397
+ function appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap) {
2419
2398
  const occupiedWidth = atom.width + atom.extraWidthAfter;
2420
- if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) return pendingGapBefore + occupiedWidth;
2399
+ if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) return {
2400
+ gapBefore: pendingGap.gapBefore + occupiedWidth,
2401
+ gapAtomCount: pendingGap.gapAtomCount + 1,
2402
+ gapSpaceCount: pendingGap.gapSpaceCount + 1
2403
+ };
2421
2404
  const span = spans[atom.itemIndex];
2422
2405
  const font = span?.font ?? atom.font;
2423
2406
  const color = span?.color ?? defaultColor;
2424
2407
  const previous = fragments[fragments.length - 1];
2425
- if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGapBefore === 0) {
2408
+ const spaceCount = atom.kind === "space" ? 1 : 0;
2409
+ if (previous != null && previous.itemIndex === atom.itemIndex && previous.font === font && pendingGap.gapBefore === 0) {
2426
2410
  previous.text += atom.text;
2427
2411
  previous.occupiedWidth += occupiedWidth;
2428
- return 0;
2412
+ previous.atomCount += 1;
2413
+ previous.spaceCount += spaceCount;
2414
+ return {
2415
+ gapBefore: 0,
2416
+ gapAtomCount: 0,
2417
+ gapSpaceCount: 0
2418
+ };
2429
2419
  }
2430
2420
  fragments.push({
2431
2421
  itemIndex: atom.itemIndex,
2432
2422
  text: atom.text,
2433
2423
  font,
2434
2424
  color,
2435
- gapBefore: pendingGapBefore,
2425
+ gapBefore: pendingGap.gapBefore,
2426
+ gapAtomCount: pendingGap.gapAtomCount,
2427
+ gapSpaceCount: pendingGap.gapSpaceCount,
2436
2428
  occupiedWidth,
2429
+ atomCount: 1,
2430
+ spaceCount,
2437
2431
  shift: measureRichFragmentShift(ctx, font)
2438
2432
  });
2439
- return 0;
2433
+ return {
2434
+ gapBefore: 0,
2435
+ gapAtomCount: 0,
2436
+ gapSpaceCount: 0
2437
+ };
2440
2438
  }
2441
2439
  function materializeRichFragmentsInRange(ctx, spans, defaultColor, prepared, start, end) {
2442
2440
  const fragments = [];
2443
- let pendingGapBefore = 0;
2441
+ let pendingGap = {
2442
+ gapBefore: 0,
2443
+ gapAtomCount: 0,
2444
+ gapSpaceCount: 0
2445
+ };
2444
2446
  forEachAtomInRange(prepared, start, end, (atom) => {
2445
- pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
2447
+ pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
2446
2448
  });
2447
2449
  return fragments;
2448
2450
  }
@@ -2488,7 +2490,11 @@ function createRichEllipsisFragment(ctx, font, color) {
2488
2490
  font,
2489
2491
  color,
2490
2492
  gapBefore: 0,
2493
+ gapAtomCount: 0,
2494
+ gapSpaceCount: 0,
2491
2495
  occupiedWidth: measureEllipsisWidth(ctx),
2496
+ atomCount: 1,
2497
+ spaceCount: 0,
2492
2498
  shift: measureFontShift(ctx)
2493
2499
  }));
2494
2500
  }
@@ -2574,13 +2580,17 @@ function layoutRichEndEllipsisFromCursor(ctx, spans, defaultFont, defaultColor,
2574
2580
  const { prefixCount, width } = selection;
2575
2581
  const fragments = [];
2576
2582
  let atomIndex = 0;
2577
- let pendingGapBefore = 0;
2583
+ let pendingGap = {
2584
+ gapBefore: 0,
2585
+ gapAtomCount: 0,
2586
+ gapSpaceCount: 0
2587
+ };
2578
2588
  let lastVisibleAtom;
2579
2589
  let lastAtom;
2580
2590
  forEachAtomFromCursorToEnd(prepared, start, (atom) => {
2581
2591
  lastAtom = atom;
2582
2592
  if (atomIndex < prefixCount) {
2583
- pendingGapBefore = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGapBefore);
2593
+ pendingGap = appendRichFragment(ctx, spans, defaultColor, fragments, atom, pendingGap);
2584
2594
  lastVisibleAtom = atom;
2585
2595
  }
2586
2596
  atomIndex += 1;
@@ -2749,6 +2759,154 @@ function layoutRichTextWithOverflow(ctx, spans, maxWidth, defaultFont, defaultCo
2749
2759
  };
2750
2760
  }
2751
2761
  //#endregion
2762
+ //#region src/text/justify.ts
2763
+ let _justifySupported;
2764
+ function isJustifySupported(ctx) {
2765
+ if (_justifySupported !== void 0) return _justifySupported;
2766
+ _justifySupported = typeof ctx.wordSpacing === "string" && typeof ctx.letterSpacing === "string";
2767
+ return _justifySupported;
2768
+ }
2769
+ function resolveJustifyMode(justify) {
2770
+ if (justify === true) return "inter-word";
2771
+ if (justify === "inter-word" || justify === "inter-character") return justify;
2772
+ return null;
2773
+ }
2774
+ const HYBRID_WORD_SHARE_CANDIDATES = [
2775
+ .15,
2776
+ .2,
2777
+ .25,
2778
+ .3,
2779
+ .35,
2780
+ .4,
2781
+ .45,
2782
+ .5,
2783
+ .55,
2784
+ .6,
2785
+ .65,
2786
+ .7,
2787
+ .75,
2788
+ .8,
2789
+ .85,
2790
+ 1,
2791
+ 0
2792
+ ];
2793
+ const PUNCTUATION_OR_SYMBOL_PATTERN = /^[\p{P}\p{S}]$/u;
2794
+ const JUSTIFY_SCORE_EPSILON = 1e-9;
2795
+ function analyzeLineForJustify(prepared, line) {
2796
+ let wordGapCount = 0;
2797
+ let wordCount = 0;
2798
+ let renderAtomCount = 0;
2799
+ let spaceCount = 0;
2800
+ let nonSpaceCount = 0;
2801
+ let cjkCount = 0;
2802
+ let latinLikeCount = 0;
2803
+ let punctuationCount = 0;
2804
+ let nonSpaceWidth = 0;
2805
+ let insideWord = false;
2806
+ forEachAtomInRange(prepared, line.start, line.end, (atom) => {
2807
+ if (atom.kind === "space" && !atom.preservesLineEnd && atom.atomicGroupId == null) wordGapCount++;
2808
+ renderAtomCount++;
2809
+ if (atom.kind === "space") {
2810
+ spaceCount++;
2811
+ insideWord = false;
2812
+ return;
2813
+ }
2814
+ nonSpaceCount++;
2815
+ nonSpaceWidth += atom.width + atom.extraWidthAfter;
2816
+ if (!insideWord) {
2817
+ wordCount++;
2818
+ insideWord = true;
2819
+ }
2820
+ if (isCJK(atom.text)) {
2821
+ cjkCount++;
2822
+ return;
2823
+ }
2824
+ if (PUNCTUATION_OR_SYMBOL_PATTERN.test(atom.text)) {
2825
+ punctuationCount++;
2826
+ return;
2827
+ }
2828
+ latinLikeCount++;
2829
+ });
2830
+ return {
2831
+ wordGapCount,
2832
+ wordCount,
2833
+ renderAtomCount,
2834
+ letterGapCount: Math.max(renderAtomCount - 1, 0),
2835
+ spaceCount,
2836
+ nonSpaceCount,
2837
+ cjkCount,
2838
+ latinLikeCount,
2839
+ punctuationCount,
2840
+ lineWidth: line.width,
2841
+ nonSpaceWidth
2842
+ };
2843
+ }
2844
+ function getAverageWordWidth(info) {
2845
+ return info.wordCount > 0 ? info.nonSpaceWidth / info.wordCount : info.lineWidth;
2846
+ }
2847
+ function getAverageCharWidth(info) {
2848
+ return info.renderAtomCount > 0 ? info.lineWidth / info.renderAtomCount : info.lineWidth;
2849
+ }
2850
+ function resolvePerGapSpacing(totalSpace, gapCount) {
2851
+ if (totalSpace === 0) return 0;
2852
+ if (gapCount <= 0) return null;
2853
+ return totalSpace / gapCount;
2854
+ }
2855
+ function exceedsThreshold(perGap, averageWidth, threshold) {
2856
+ if (!Number.isFinite(threshold)) return false;
2857
+ return perGap > threshold * averageWidth;
2858
+ }
2859
+ function createJustifySpacing(wordSpacingPx, letterSpacingPx) {
2860
+ return {
2861
+ wordSpacing: `${wordSpacingPx}px`,
2862
+ letterSpacing: `${letterSpacingPx}px`,
2863
+ wordSpacingPx,
2864
+ letterSpacingPx
2865
+ };
2866
+ }
2867
+ function computeJustifySpacing(lineWidth, maxWidth, info, mode, threshold = Number.POSITIVE_INFINITY) {
2868
+ const extraSpace = maxWidth - lineWidth;
2869
+ if (extraSpace <= 0 || mode == null) return null;
2870
+ if (mode === "inter-word" && info.wordGapCount > 0) {
2871
+ const perGap = extraSpace / info.wordGapCount;
2872
+ if (exceedsThreshold(perGap, Math.max(getAverageWordWidth(info), Number.EPSILON), threshold)) return null;
2873
+ return createJustifySpacing(perGap, 0);
2874
+ }
2875
+ if (mode !== "inter-character" || info.renderAtomCount === 0) return null;
2876
+ const avgCharWidth = Math.max(getAverageCharWidth(info), Number.EPSILON);
2877
+ if (info.wordGapCount === 0) {
2878
+ const perGap = resolvePerGapSpacing(extraSpace, info.letterGapCount);
2879
+ if (perGap == null) return null;
2880
+ if (exceedsThreshold(perGap, avgCharWidth, threshold)) return null;
2881
+ return createJustifySpacing(0, perGap);
2882
+ }
2883
+ const avgWordWidth = Math.max(getAverageWordWidth(info), Number.EPSILON);
2884
+ const nonSpaceCount = Math.max(info.nonSpaceCount, 1);
2885
+ const cjkRatio = info.cjkCount / nonSpaceCount;
2886
+ const latinLikeRatio = info.latinLikeCount / nonSpaceCount;
2887
+ const punctuationRatio = info.punctuationCount / nonSpaceCount;
2888
+ const wordPenalty = 1 + cjkRatio;
2889
+ const letterPenalty = 1 + latinLikeRatio + .5 * punctuationRatio;
2890
+ let bestCandidate = null;
2891
+ for (const wordShare of HYBRID_WORD_SHARE_CANDIDATES) {
2892
+ const wordExtraSpace = extraSpace * wordShare;
2893
+ const letterExtraSpace = extraSpace - wordExtraSpace;
2894
+ const wordSpacingPx = resolvePerGapSpacing(wordExtraSpace, info.wordGapCount);
2895
+ const letterSpacingPx = resolvePerGapSpacing(letterExtraSpace, info.letterGapCount);
2896
+ if (wordSpacingPx == null || letterSpacingPx == null) continue;
2897
+ if (exceedsThreshold(wordSpacingPx, avgWordWidth, threshold) || exceedsThreshold(letterSpacingPx, avgCharWidth, threshold)) continue;
2898
+ const wordRatio = wordSpacingPx / avgWordWidth;
2899
+ const letterRatio = letterSpacingPx / avgCharWidth;
2900
+ const score = wordPenalty * wordRatio ** 2 + letterPenalty * letterRatio ** 2;
2901
+ if (bestCandidate == null || score < bestCandidate.score - JUSTIFY_SCORE_EPSILON || Math.abs(score - bestCandidate.score) <= JUSTIFY_SCORE_EPSILON && wordShare > bestCandidate.wordShare) bestCandidate = {
2902
+ spacing: createJustifySpacing(wordSpacingPx, letterSpacingPx),
2903
+ score,
2904
+ wordShare
2905
+ };
2906
+ }
2907
+ return bestCandidate?.spacing ?? null;
2908
+ }
2909
+ //#endregion
2752
2910
  //#region src/nodes/text.ts
2753
2911
  function resolvePhysicalTextAlign(options) {
2754
2912
  if (options.physicalAlign != null) return options.physicalAlign;
@@ -2763,6 +2921,26 @@ function normalizeTextMaxWidth(maxWidth) {
2763
2921
  if (maxWidth == null) return;
2764
2922
  return Math.max(0, maxWidth);
2765
2923
  }
2924
+ const DEFAULT_TEXT_SPACING = {
2925
+ wordSpacing: "0px",
2926
+ letterSpacing: "0px"
2927
+ };
2928
+ function supportsTextSpacing(g) {
2929
+ return typeof g.wordSpacing === "string" && typeof g.letterSpacing === "string";
2930
+ }
2931
+ function withTextSpacing(g, spacing, cb) {
2932
+ if (!supportsTextSpacing(g)) return cb();
2933
+ const savedWordSpacing = g.wordSpacing;
2934
+ const savedLetterSpacing = g.letterSpacing;
2935
+ try {
2936
+ g.wordSpacing = spacing.wordSpacing;
2937
+ g.letterSpacing = spacing.letterSpacing;
2938
+ return cb();
2939
+ } finally {
2940
+ g.wordSpacing = savedWordSpacing;
2941
+ g.letterSpacing = savedLetterSpacing;
2942
+ }
2943
+ }
2766
2944
  function getTextLayoutContext(ctx) {
2767
2945
  return ctx;
2768
2946
  }
@@ -2866,7 +3044,9 @@ function drawRichLine(ctx, line, fallbackColor, x, y, lineHeight) {
2866
3044
  g.font = fragment.font;
2867
3045
  g.fillStyle = ctx.resolveDynValue(fragment.color ?? fallbackColor);
2868
3046
  g.textAlign = "left";
2869
- g.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
3047
+ withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3048
+ g.fillText(fragment.text, cursorX, y + (lineHeight + fragment.shift) / 2);
3049
+ });
2870
3050
  });
2871
3051
  cursorX += fragment.occupiedWidth;
2872
3052
  }
@@ -2945,23 +3125,83 @@ var MultilineText = class {
2945
3125
  const spans = this.text;
2946
3126
  const { width, lines } = getRichMultiLineDrawLayout(this, ctx, spans, this.options);
2947
3127
  const align = resolvePhysicalTextAlign(this.options);
2948
- const startX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
2949
- for (const line of lines) {
2950
- let cursorX = startX;
2951
- for (let fi = 0; fi < line.fragments.length; fi++) {
2952
- const frag = line.fragments[fi];
2953
- cursorX += frag.gapBefore;
2954
- ctx.with((g) => {
2955
- g.font = frag.font;
2956
- g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
2957
- if (align === "right") g.textAlign = "right";
2958
- else if (align === "center") g.textAlign = "center";
2959
- else g.textAlign = "left";
2960
- g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
2961
- });
2962
- cursorX += frag.occupiedWidth;
3128
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
3129
+ const mode = resolveJustifyMode(this.options.justify);
3130
+ const canJustify = mode != null && maxWidth != null && maxWidth > 0 && isJustifySupported(ctx.graphics);
3131
+ const threshold = this.options.justifyGapThreshold ?? 2;
3132
+ if (canJustify) {
3133
+ const prepared = readPreparedInlineLayout(getRichPreparedKey(spans, this.options.font, this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal"), createRichSourceItems(spans, this.options.font), this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal");
3134
+ let lineIndex = 0;
3135
+ const totalLines = lines.length;
3136
+ walkPreparedLineRanges(prepared, maxWidth, (lineRange) => {
3137
+ if (lineIndex >= totalLines) return false;
3138
+ const line = lines[lineIndex];
3139
+ const isLastLine = lineIndex === totalLines - 1;
3140
+ if (!(isLastLine && shouldUseMultilineOverflowLayout(this.options) && this.options.overflow === "ellipsis") && (!isLastLine || this.options.justifyLastLine === true)) {
3141
+ const info = analyzeLineForJustify(prepared, lineRange);
3142
+ const spacing = computeJustifySpacing(lineRange.width, maxWidth, info, mode, threshold);
3143
+ if (spacing != null) {
3144
+ let cursorX = x;
3145
+ for (let fi = 0; fi < line.fragments.length; fi++) {
3146
+ const frag = line.fragments[fi];
3147
+ const leadingLetterGapCount = fi > 0 ? frag.gapAtomCount + 1 : 0;
3148
+ const internalLetterGapCount = Math.max(frag.atomCount - 1, 0);
3149
+ cursorX += frag.gapBefore + leadingLetterGapCount * spacing.letterSpacingPx + frag.gapSpaceCount * spacing.wordSpacingPx;
3150
+ ctx.with((g) => {
3151
+ g.font = frag.font;
3152
+ g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
3153
+ g.textAlign = "left";
3154
+ withTextSpacing(g, spacing, () => {
3155
+ g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
3156
+ });
3157
+ });
3158
+ cursorX += frag.occupiedWidth + internalLetterGapCount * spacing.letterSpacingPx + frag.spaceCount * spacing.wordSpacingPx;
3159
+ }
3160
+ y += this.options.lineHeight;
3161
+ lineIndex++;
3162
+ return;
3163
+ }
3164
+ }
3165
+ let cursorX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
3166
+ for (let fi = 0; fi < line.fragments.length; fi++) {
3167
+ const frag = line.fragments[fi];
3168
+ cursorX += frag.gapBefore;
3169
+ ctx.with((g) => {
3170
+ g.font = frag.font;
3171
+ g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
3172
+ if (align === "right") g.textAlign = "right";
3173
+ else if (align === "center") g.textAlign = "center";
3174
+ else g.textAlign = "left";
3175
+ withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3176
+ g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
3177
+ });
3178
+ });
3179
+ cursorX += frag.occupiedWidth;
3180
+ }
3181
+ y += this.options.lineHeight;
3182
+ lineIndex++;
3183
+ });
3184
+ } else {
3185
+ const startX = align === "right" ? x + width : align === "center" ? x + width / 2 : x;
3186
+ for (const line of lines) {
3187
+ let cursorX = startX;
3188
+ for (let fi = 0; fi < line.fragments.length; fi++) {
3189
+ const frag = line.fragments[fi];
3190
+ cursorX += frag.gapBefore;
3191
+ ctx.with((g) => {
3192
+ g.font = frag.font;
3193
+ g.fillStyle = ctx.resolveDynValue(frag.color ?? this.options.color);
3194
+ if (align === "right") g.textAlign = "right";
3195
+ else if (align === "center") g.textAlign = "center";
3196
+ else g.textAlign = "left";
3197
+ withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3198
+ g.fillText(frag.text, cursorX, y + (this.options.lineHeight + frag.shift) / 2);
3199
+ });
3200
+ });
3201
+ cursorX += frag.occupiedWidth;
3202
+ }
3203
+ y += this.options.lineHeight;
2963
3204
  }
2964
- y += this.options.lineHeight;
2965
3205
  }
2966
3206
  return false;
2967
3207
  }
@@ -2969,30 +3209,76 @@ var MultilineText = class {
2969
3209
  g.font = this.options.font;
2970
3210
  g.fillStyle = ctx.resolveDynValue(this.options.color);
2971
3211
  const { width, lines } = getMultiLineDrawLayout(this, ctx, this.text, this.options);
2972
- switch (resolvePhysicalTextAlign(this.options)) {
2973
- case "left":
2974
- for (const { text, shift } of lines) {
2975
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2976
- y += this.options.lineHeight;
3212
+ const maxWidth = normalizeTextMaxWidth(ctx.constraints?.maxWidth);
3213
+ const mode = resolveJustifyMode(this.options.justify);
3214
+ const canJustify = mode != null && maxWidth != null && maxWidth > 0 && isJustifySupported(g);
3215
+ const threshold = this.options.justifyGapThreshold ?? 2;
3216
+ if (canJustify) {
3217
+ const prepared = readPreparedText(this.text, this.options.font, this.options.whiteSpace ?? "normal", this.options.wordBreak ?? "normal");
3218
+ let lineIndex = 0;
3219
+ const totalLines = lines.length;
3220
+ walkPreparedLineRanges(prepared, maxWidth, (lineRange) => {
3221
+ if (lineIndex >= totalLines) return false;
3222
+ const layout = lines[lineIndex];
3223
+ const isLastLine = lineIndex === totalLines - 1;
3224
+ if (!(isLastLine && shouldUseMultilineOverflowLayout(this.options) && this.options.overflow === "ellipsis") && (!isLastLine || this.options.justifyLastLine === true)) {
3225
+ const info = analyzeLineForJustify(prepared, lineRange);
3226
+ const spacing = computeJustifySpacing(lineRange.width, maxWidth, info, mode, threshold);
3227
+ if (spacing != null) {
3228
+ withTextSpacing(g, spacing, () => {
3229
+ g.textAlign = "left";
3230
+ g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
3231
+ });
3232
+ y += this.options.lineHeight;
3233
+ lineIndex++;
3234
+ return;
3235
+ }
2977
3236
  }
2978
- break;
2979
- case "right":
2980
- x += width;
2981
- g.textAlign = "right";
2982
- for (const { text, shift } of lines) {
2983
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2984
- y += this.options.lineHeight;
2985
- }
2986
- break;
2987
- case "center":
2988
- x += width / 2;
2989
- g.textAlign = "center";
2990
- for (const { text, shift } of lines) {
2991
- g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
2992
- y += this.options.lineHeight;
2993
- }
2994
- break;
2995
- }
3237
+ withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3238
+ switch (resolvePhysicalTextAlign(this.options)) {
3239
+ case "left":
3240
+ g.textAlign = "left";
3241
+ g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
3242
+ break;
3243
+ case "right":
3244
+ g.textAlign = "right";
3245
+ g.fillText(layout.text, x + width, y + (this.options.lineHeight + layout.shift) / 2);
3246
+ break;
3247
+ case "center":
3248
+ g.textAlign = "center";
3249
+ g.fillText(layout.text, x + width / 2, y + (this.options.lineHeight + layout.shift) / 2);
3250
+ break;
3251
+ }
3252
+ });
3253
+ y += this.options.lineHeight;
3254
+ lineIndex++;
3255
+ });
3256
+ } else withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3257
+ switch (resolvePhysicalTextAlign(this.options)) {
3258
+ case "left":
3259
+ for (const { text, shift } of lines) {
3260
+ g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
3261
+ y += this.options.lineHeight;
3262
+ }
3263
+ break;
3264
+ case "right":
3265
+ x += width;
3266
+ g.textAlign = "right";
3267
+ for (const { text, shift } of lines) {
3268
+ g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
3269
+ y += this.options.lineHeight;
3270
+ }
3271
+ break;
3272
+ case "center":
3273
+ x += width / 2;
3274
+ g.textAlign = "center";
3275
+ for (const { text, shift } of lines) {
3276
+ g.fillText(text, x, y + (this.options.lineHeight + shift) / 2);
3277
+ y += this.options.lineHeight;
3278
+ }
3279
+ break;
3280
+ }
3281
+ });
2996
3282
  return false;
2997
3283
  });
2998
3284
  }
@@ -3055,7 +3341,9 @@ var Text = class {
3055
3341
  g.font = this.options.font;
3056
3342
  g.fillStyle = ctx.resolveDynValue(this.options.color);
3057
3343
  const layout = getSingleLineLayout(this, ctx, text, this.options);
3058
- g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
3344
+ withTextSpacing(g, DEFAULT_TEXT_SPACING, () => {
3345
+ g.fillText(layout.text, x, y + (this.options.lineHeight + layout.shift) / 2);
3346
+ });
3059
3347
  return false;
3060
3348
  });
3061
3349
  }