@fulate/ui 1.0.8 → 1.0.10
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/README.md +7 -4
- package/dist/index.js +50 -29
- package/dist/text/index.d.ts +1 -1
- package/dist/text/layout.d.ts +31 -3
- package/dist/text/paint.d.ts +11 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,10 +159,13 @@ import { measureTextRun } from "@fulate/ui";
|
|
|
159
159
|
const metrics = measureTextRun(ctx, text, root.skin.text, 14, 500);
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
每次调用只执行一次 `ctx.measureText()`,返回 `font`、`width`、`lineHeight`、`drawY
|
|
163
|
-
`alphabeticBaseline
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
每次调用只执行一次 `ctx.measureText()`,返回 `font`、`width`、`lineHeight`、`drawY`、
|
|
163
|
+
`alphabeticBaseline`、`textBaseline` 和 `letterSpacing`。`drawY` 根据原生 Canvas 的
|
|
164
|
+
`fontBoundingBoxAscent`/`fontBoundingBoxDescent` 把字体行框放到行框中心;`alphabeticBaseline`
|
|
165
|
+
是同一行框内的规范 alphabetic baseline,因此不同字号的 run 可以直接对齐。空文本的 `width`
|
|
166
|
+
为 `0`,但仍测量一个空格以取得稳定字体度量。函数会把本次测量所需的文字状态写入 `ctx`,不会
|
|
167
|
+
调用 `save()` 或 `restore()`;Canvas 生命周期以及业务特有的文字顺序、对齐和绘制仍由 Shape
|
|
168
|
+
自己拥有。
|
|
166
169
|
|
|
167
170
|
### 组合型多行文本
|
|
168
171
|
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,10 @@ import { Easing, Tween } from "@tweenjs/tween.js";
|
|
|
8
8
|
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
9
9
|
var measurementListeners = /* @__PURE__ */ new Set();
|
|
10
10
|
var observesFontLoading = false;
|
|
11
|
+
var textLineBoxMetricsCache = /* @__PURE__ */ new Map();
|
|
11
12
|
function invalidateFontMeasurements() {
|
|
12
13
|
clearCache();
|
|
14
|
+
textLineBoxMetricsCache.clear();
|
|
13
15
|
for (const listener of measurementListeners) listener();
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
@@ -50,14 +52,35 @@ function measurePlainText(text, font) {
|
|
|
50
52
|
return context.measureText(text).width;
|
|
51
53
|
}
|
|
52
54
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
+
* Resolve one line box from a native Canvas measurement. The font bounding
|
|
56
|
+
* box is stable for the font state, while `alphabeticBaseline` translates the
|
|
57
|
+
* selected Canvas baseline into the same canonical alphabetic coordinate used
|
|
58
|
+
* by TextRow and composite owners.
|
|
55
59
|
*/
|
|
56
|
-
function
|
|
60
|
+
function resolveTextLinePlacement(metrics, lineHeight) {
|
|
61
|
+
const drawY = (lineHeight + metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent) / 2;
|
|
62
|
+
return {
|
|
63
|
+
drawY,
|
|
64
|
+
alphabeticBaseline: drawY - metrics.alphabeticBaseline,
|
|
65
|
+
height: lineHeight
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Measure the font line box once and reuse it for Text painting, placeholders,
|
|
70
|
+
* and composite TextBlock owners. The representative space supplies the
|
|
71
|
+
* font-level Canvas metrics without making placement depend on the content of
|
|
72
|
+
* an individual line.
|
|
73
|
+
*/
|
|
74
|
+
function measureTextLinePlacement(font, textBaseline, lineHeight) {
|
|
75
|
+
const key = `${font}\u0000${textBaseline}\u0000${lineHeight}`;
|
|
76
|
+
const cached = textLineBoxMetricsCache.get(key);
|
|
77
|
+
if (cached) return cached;
|
|
57
78
|
const context = getPlainMeasureContext();
|
|
58
79
|
context.font = font;
|
|
59
80
|
context.textBaseline = textBaseline;
|
|
60
|
-
|
|
81
|
+
const metrics = resolveTextLinePlacement(context.measureText(" "), lineHeight);
|
|
82
|
+
textLineBoxMetricsCache.set(key, metrics);
|
|
83
|
+
return metrics;
|
|
61
84
|
}
|
|
62
85
|
/** A no-wrap run can stay one Canvas string until rich layout is requested. */
|
|
63
86
|
function canUsePlainText(text, letterSpacing) {
|
|
@@ -809,40 +832,39 @@ function clearTextShadow(ctx) {
|
|
|
809
832
|
ctx.shadowOffsetX = 0;
|
|
810
833
|
ctx.shadowOffsetY = 0;
|
|
811
834
|
}
|
|
812
|
-
function getTextLineStartY(textBaseline, lineHeight) {
|
|
813
|
-
return textBaseline === "middle" ? lineHeight / 2 : textBaseline === "bottom" ? lineHeight : 0;
|
|
814
|
-
}
|
|
815
|
-
function getTextLineDrawY(textBaseline, fontSize, lineHeight) {
|
|
816
|
-
return getTextLineStartY(textBaseline, lineHeight) + (lineHeight - fontSize) / 2;
|
|
817
|
-
}
|
|
818
835
|
/**
|
|
819
836
|
* Measure one native Canvas text run with Fulate's resolved font and line-box
|
|
820
837
|
* semantics. This performs one `measureText()` call and writes the run's font,
|
|
821
838
|
* baseline, and letter spacing to the supplied context without saving or
|
|
822
|
-
* restoring it.
|
|
839
|
+
* restoring it. The font bounding box determines the centered draw position;
|
|
840
|
+
* the returned alphabetic baseline is the canonical coordinate for row
|
|
841
|
+
* alignment. Empty text measures a space for baseline data but reports zero
|
|
823
842
|
* width.
|
|
824
843
|
*/
|
|
825
844
|
function measureTextRun(ctx, text, style, fontSize = style.fontSize, fontWeight = style.fontWeight) {
|
|
826
845
|
const font = buildFontString(style, fontSize, fontWeight);
|
|
827
846
|
const lineHeight = fontSize * style.lineHeight;
|
|
828
|
-
const drawY = getTextLineDrawY(style.textBaseline, fontSize, lineHeight);
|
|
829
847
|
setTextState(ctx, font, style.letterSpacing);
|
|
830
848
|
ctx.textBaseline = style.textBaseline;
|
|
831
|
-
const
|
|
849
|
+
const measured = ctx.measureText(text || " ");
|
|
850
|
+
const lineMetrics = resolveTextLinePlacement(measured, lineHeight);
|
|
832
851
|
return {
|
|
833
852
|
font,
|
|
834
|
-
width: text ?
|
|
853
|
+
width: text ? measured.width : 0,
|
|
835
854
|
lineHeight,
|
|
836
|
-
drawY,
|
|
837
|
-
alphabeticBaseline:
|
|
855
|
+
drawY: lineMetrics.drawY,
|
|
856
|
+
alphabeticBaseline: lineMetrics.alphabeticBaseline,
|
|
857
|
+
textBaseline: style.textBaseline,
|
|
858
|
+
letterSpacing: style.letterSpacing
|
|
838
859
|
};
|
|
839
860
|
}
|
|
840
|
-
/** Measure the line box
|
|
841
|
-
function measureTextLineBoxMetrics(
|
|
861
|
+
/** Measure the line box shared by Text, Span, and composite text owners. */
|
|
862
|
+
function measureTextLineBoxMetrics(style) {
|
|
842
863
|
const lineHeight = style.fontSize * style.lineHeight;
|
|
864
|
+
const { alphabeticBaseline, height } = measureTextLinePlacement(buildFontString(style), style.textBaseline, lineHeight);
|
|
843
865
|
return {
|
|
844
|
-
alphabeticBaseline
|
|
845
|
-
height
|
|
866
|
+
alphabeticBaseline,
|
|
867
|
+
height
|
|
846
868
|
};
|
|
847
869
|
}
|
|
848
870
|
/**
|
|
@@ -856,7 +878,7 @@ function paintTextBlock(ctx, layout, style, width, height) {
|
|
|
856
878
|
if (layout.lines.length === 0) return;
|
|
857
879
|
const font = buildFontString(style);
|
|
858
880
|
const lineHeight = style.fontSize * style.lineHeight;
|
|
859
|
-
const
|
|
881
|
+
const lineMetrics = measureTextLinePlacement(font, style.textBaseline, lineHeight);
|
|
860
882
|
const blockOffset = layout.getBlockVerticalOffset(height, style.verticalAlign);
|
|
861
883
|
setTextState(ctx, font, style.letterSpacing);
|
|
862
884
|
ctx.textAlign = "left";
|
|
@@ -871,7 +893,7 @@ function paintTextBlock(ctx, layout, style, width, height) {
|
|
|
871
893
|
for (let index = 0; index < layout.lines.length; index++) {
|
|
872
894
|
const line = layout.lines[index];
|
|
873
895
|
const x = isLeftAligned ? 0 : layout.getLineX(index, width, style.textAlign);
|
|
874
|
-
const y = drawY + index * lineHeight + blockOffset;
|
|
896
|
+
const y = lineMetrics.drawY + index * lineHeight + blockOffset;
|
|
875
897
|
if (hasTextShadow) applyTextShadow(ctx, style.textShadow);
|
|
876
898
|
if (hasStroke) {
|
|
877
899
|
ctx.strokeStyle = strokeColor;
|
|
@@ -898,7 +920,6 @@ function paintTextContent(self, ctx, offsetY = 0) {
|
|
|
898
920
|
const w = self.width || 0;
|
|
899
921
|
const h = self.height || 0;
|
|
900
922
|
const lineHeightPx = style.fontSize * style.lineHeight;
|
|
901
|
-
const drawY = getTextLineDrawY(style.textBaseline, style.fontSize, lineHeightPx);
|
|
902
923
|
const ls = style.letterSpacing;
|
|
903
924
|
const layout = self.textLayout;
|
|
904
925
|
const blockOffset = layout.getBlockVerticalOffset(h, style.verticalAlign);
|
|
@@ -944,7 +965,8 @@ function paintTextContent(self, ctx, offsetY = 0) {
|
|
|
944
965
|
const phWidth = measureTextWidth(self.placeholder, font, ls);
|
|
945
966
|
const phX = style.textAlign === "center" ? (w - phWidth) / 2 : style.textAlign === "right" ? w - phWidth : 0;
|
|
946
967
|
const phBlockOffset = layout.getBlockVerticalOffset(h, style.verticalAlign, lineHeightPx);
|
|
947
|
-
|
|
968
|
+
const lineMetrics = measureTextLinePlacement(font, style.textBaseline, lineHeightPx);
|
|
969
|
+
drawTextLine(ctx, self.placeholder, phX, lineMetrics.drawY + phBlockOffset, font, ls, true, false);
|
|
948
970
|
ctx.fillStyle = fillStyle;
|
|
949
971
|
}
|
|
950
972
|
if (self.isEditing && self.caretVisible && self.textarea) {
|
|
@@ -3140,13 +3162,12 @@ var Text = class extends Rectangle {
|
|
|
3140
3162
|
return this.textBlock.layout;
|
|
3141
3163
|
}
|
|
3142
3164
|
/** @internal Shared by TextRow layout and the final Text painter. */
|
|
3143
|
-
getTextLineBoxMetrics(
|
|
3165
|
+
getTextLineBoxMetrics() {
|
|
3144
3166
|
const style = this.getResolvedTextStyle();
|
|
3145
3167
|
const cached = this.textLineBoxMetricsCache;
|
|
3146
|
-
if (cached?.
|
|
3147
|
-
const metrics = measureTextLineBoxMetrics(
|
|
3168
|
+
if (cached?.style === style) return cached.metrics;
|
|
3169
|
+
const metrics = measureTextLineBoxMetrics(style);
|
|
3148
3170
|
this.textLineBoxMetricsCache = {
|
|
3149
|
-
text,
|
|
3150
3171
|
style,
|
|
3151
3172
|
metrics
|
|
3152
3173
|
};
|
|
@@ -3155,7 +3176,7 @@ var Text = class extends Rectangle {
|
|
|
3155
3176
|
/** @internal Alphabetic baseline relative to this Text box. */
|
|
3156
3177
|
getTextBlockFirstAlphabeticBaseline() {
|
|
3157
3178
|
const style = this.getResolvedTextStyle();
|
|
3158
|
-
return this.textLayout.getBlockVerticalOffset(this.height ?? 0, style.verticalAlign) + this.getTextLineBoxMetrics(
|
|
3179
|
+
return this.textLayout.getBlockVerticalOffset(this.height ?? 0, style.verticalAlign) + this.getTextLineBoxMetrics().alphabeticBaseline;
|
|
3159
3180
|
}
|
|
3160
3181
|
/** @internal The editor frame follows the Text box, not visible overflow. */
|
|
3161
3182
|
getSelectionFrameBounds() {
|
package/dist/text/index.d.ts
CHANGED
|
@@ -118,7 +118,7 @@ export declare class Text<P extends TextProperties = TextProperties, O extends T
|
|
|
118
118
|
getFontString(style?: Required<TextStyleConfig>): string;
|
|
119
119
|
get textLayout(): import("./layout").TextLayoutSnapshot;
|
|
120
120
|
/** @internal Shared by TextRow layout and the final Text painter. */
|
|
121
|
-
getTextLineBoxMetrics(
|
|
121
|
+
getTextLineBoxMetrics(): TextLineBoxMetrics;
|
|
122
122
|
/** @internal Alphabetic baseline relative to this Text box. */
|
|
123
123
|
getTextBlockFirstAlphabeticBaseline(): number;
|
|
124
124
|
/** @internal The editor frame follows the Text box, not visible overflow. */
|
package/dist/text/layout.d.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { type LayoutLine } from "@chenglou/pretext";
|
|
2
|
+
/**
|
|
3
|
+
* The vertical placement of a Canvas text run is a font property, not a
|
|
4
|
+
* `fontSize` approximation. Keep one resolved line box per native font state
|
|
5
|
+
* so painting can reuse the measurement without measuring on every frame.
|
|
6
|
+
*/
|
|
7
|
+
export interface TextLineBoxMetrics {
|
|
8
|
+
/** Alphabetic baseline relative to the line-box top. */
|
|
9
|
+
readonly alphabeticBaseline: number;
|
|
10
|
+
/** Line-box height in logical pixels. */
|
|
11
|
+
readonly height: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TextLinePlacementMetrics extends TextLineBoxMetrics {
|
|
14
|
+
/** Canvas draw Y relative to the line-box top. */
|
|
15
|
+
readonly drawY: number;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Subscribe a non-Text owner to browser font measurement completion.
|
|
4
19
|
*
|
|
@@ -29,10 +44,23 @@ export interface TextLayoutMetrics {
|
|
|
29
44
|
readonly truncated: boolean;
|
|
30
45
|
}
|
|
31
46
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
47
|
+
* Resolve one line box from a native Canvas measurement. The font bounding
|
|
48
|
+
* box is stable for the font state, while `alphabeticBaseline` translates the
|
|
49
|
+
* selected Canvas baseline into the same canonical alphabetic coordinate used
|
|
50
|
+
* by TextRow and composite owners.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveTextLinePlacement(metrics: Pick<TextMetrics, "fontBoundingBoxAscent" | "fontBoundingBoxDescent" | "alphabeticBaseline">, lineHeight: number): {
|
|
53
|
+
drawY: number;
|
|
54
|
+
alphabeticBaseline: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Measure the font line box once and reuse it for Text painting, placeholders,
|
|
59
|
+
* and composite TextBlock owners. The representative space supplies the
|
|
60
|
+
* font-level Canvas metrics without making placement depend on the content of
|
|
61
|
+
* an individual line.
|
|
34
62
|
*/
|
|
35
|
-
export declare function
|
|
63
|
+
export declare function measureTextLinePlacement(font: string, textBaseline: CanvasTextBaseline, lineHeight: number): TextLinePlacementMetrics;
|
|
36
64
|
/**
|
|
37
65
|
* Measure one text run through the same engine used by Text and Span.
|
|
38
66
|
* `white-space: pre-wrap` keeps the caller's spaces and hard breaks intact.
|
package/dist/text/paint.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type TextLayoutSnapshot } from "./layout";
|
|
1
|
+
import { type TextLayoutSnapshot, type TextLineBoxMetrics } from "./layout";
|
|
2
2
|
import type { Text } from "./index";
|
|
3
3
|
import { type ResolvedTextStyle } from "./style";
|
|
4
4
|
import type { ShadowOption, TextStyleConfig } from "@fulate/core";
|
|
5
|
+
export type { TextLineBoxMetrics } from "./layout";
|
|
5
6
|
export declare function resolveColorFill(ctx: CanvasRenderingContext2D, color: ResolvedTextStyle["color"], w: number, h: number): string | CanvasGradient;
|
|
6
7
|
export declare function drawTextLine(ctx: CanvasRenderingContext2D, line: string, x: number, y: number, font: string, letterSpacing: number, doFill: boolean, doStroke: boolean): void;
|
|
7
8
|
export interface TextDecorationOptions {
|
|
@@ -29,21 +30,23 @@ export interface TextRunMetrics {
|
|
|
29
30
|
readonly drawY: number;
|
|
30
31
|
/** Alphabetic baseline relative to the run's line-box top. */
|
|
31
32
|
readonly alphabeticBaseline: number;
|
|
33
|
+
/** Canvas baseline used when the run was measured. */
|
|
34
|
+
readonly textBaseline: CanvasTextBaseline;
|
|
35
|
+
/** Letter spacing used when the run was measured. */
|
|
36
|
+
readonly letterSpacing: number;
|
|
32
37
|
}
|
|
33
38
|
/**
|
|
34
39
|
* Measure one native Canvas text run with Fulate's resolved font and line-box
|
|
35
40
|
* semantics. This performs one `measureText()` call and writes the run's font,
|
|
36
41
|
* baseline, and letter spacing to the supplied context without saving or
|
|
37
|
-
* restoring it.
|
|
42
|
+
* restoring it. The font bounding box determines the centered draw position;
|
|
43
|
+
* the returned alphabetic baseline is the canonical coordinate for row
|
|
44
|
+
* alignment. Empty text measures a space for baseline data but reports zero
|
|
38
45
|
* width.
|
|
39
46
|
*/
|
|
40
47
|
export declare function measureTextRun(ctx: CanvasRenderingContext2D, text: string, style: Readonly<Required<TextStyleConfig>>, fontSize?: number, fontWeight?: string | number): TextRunMetrics;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly height: number;
|
|
44
|
-
}
|
|
45
|
-
/** Measure the line box produced by the existing Text painter. */
|
|
46
|
-
export declare function measureTextLineBoxMetrics(text: string, style: ResolvedTextStyle): TextLineBoxMetrics;
|
|
48
|
+
/** Measure the line box shared by Text, Span, and composite text owners. */
|
|
49
|
+
export declare function measureTextLineBoxMetrics(style: ResolvedTextStyle): TextLineBoxMetrics;
|
|
47
50
|
/**
|
|
48
51
|
* Paint a previously synchronized text block.
|
|
49
52
|
*
|