pa_font 0.3.2 → 0.3.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/USAGE.md +21 -1
- package/dist/paFont.cjs +88 -10
- package/dist/paFont.cjs.map +1 -1
- package/dist/paFont.js +88 -10
- package/dist/paFont.js.map +1 -1
- package/paFont.d.ts +8 -0
- package/package.json +1 -1
package/USAGE.md
CHANGED
|
@@ -111,7 +111,7 @@ console.log(metrics.width, metrics.bbox);
|
|
|
111
111
|
- `margin`, `padding`: `24`, `"24 32"`, `[24, 32]`, `{ x: 32, y: 24 }`, `{ top, right, bottom, left }`
|
|
112
112
|
- `width`: 생략하면 `drawText(ctx)` 시 현재 canvas 폭 기준으로 자동 계산
|
|
113
113
|
- `height`: `overflow: "hidden"`과 같이 쓰면 clip/clamp
|
|
114
|
-
- `x`, `y`, `size`, `lineHeight`, `gap`, `align`
|
|
114
|
+
- `x`, `y`, `size`, `lineHeight`, `gap`, `anchor`, `align`
|
|
115
115
|
|
|
116
116
|
세부 제어 옵션:
|
|
117
117
|
|
|
@@ -135,9 +135,16 @@ console.log(metrics.width, metrics.bbox);
|
|
|
135
135
|
빈 줄 하나 이상은 새 문단으로 처리됩니다.
|
|
136
136
|
|
|
137
137
|
- `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
|
|
138
|
+
- `anchor`: 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
|
|
138
139
|
- `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
|
|
139
140
|
- `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
|
|
140
141
|
|
|
142
|
+
`anchor`는 다음 형식을 지원합니다.
|
|
143
|
+
|
|
144
|
+
- `0.5` -> `{ x: 0.5, y: 0.5 }`
|
|
145
|
+
- `[0.5, 0]`
|
|
146
|
+
- `{ x: 0.5, y: 0.5 }`
|
|
147
|
+
|
|
141
148
|
## paParagraph API
|
|
142
149
|
|
|
143
150
|
`font.paragraph()`는 `paParagraph`를 반환합니다.
|
|
@@ -166,6 +173,19 @@ const mobile = paragraph.relayout({ width: 280 });
|
|
|
166
173
|
엄격한 `overflowWrap: "normal"`이 필요한 경우만 native로 내려갑니다.
|
|
167
174
|
실제로 사용된 엔진은 `paragraph.layoutEngine`에서 확인할 수 있습니다.
|
|
168
175
|
|
|
176
|
+
가운데를 기준점으로 두고 싶다면:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const paragraph = font.paragraph("asdf", {
|
|
180
|
+
x: window.innerWidth * 0.5,
|
|
181
|
+
y: window.innerHeight * 0.5,
|
|
182
|
+
width: window.innerWidth,
|
|
183
|
+
size: window.innerHeight * 0.5,
|
|
184
|
+
align: "center",
|
|
185
|
+
anchor: 0.5,
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
169
189
|
### `paragraph.drawText(ctx, options?)`
|
|
170
190
|
|
|
171
191
|
현재 문단 레이아웃을 canvas 텍스트로 그립니다.
|
package/dist/paFont.cjs
CHANGED
|
@@ -3321,28 +3321,28 @@ function layoutParagraph(fontInstance, text, options = {}, state = {}) {
|
|
|
3321
3321
|
const textBBox = combineRects(lines.map((line) => line.bbox)) ?? emptyRect();
|
|
3322
3322
|
const textWidth = lines.reduce((max, line) => Math.max(max, line.width), 0);
|
|
3323
3323
|
const textHeight = resolvePositionedTextHeight(lines, layoutBox.contentY);
|
|
3324
|
-
const
|
|
3324
|
+
const anchoredLayout = applyParagraphAnchor(lines, textBBox, finalizeLayoutBox(layoutBox, normalized, textHeight), normalized.anchor);
|
|
3325
3325
|
const cachedPrepared = pretextState?.prepared ?? retainedPreparedState.prepared ?? null;
|
|
3326
3326
|
const cachedPreparedWhiteSpace = pretextState?.preparedWhiteSpace ?? retainedPreparedState.preparedWhiteSpace ?? null;
|
|
3327
3327
|
return {
|
|
3328
3328
|
options: normalized,
|
|
3329
|
-
lines,
|
|
3329
|
+
lines: anchoredLayout.lines,
|
|
3330
3330
|
metrics: {
|
|
3331
|
-
x:
|
|
3332
|
-
y:
|
|
3331
|
+
x: anchoredLayout.layoutBox.contentBox.x,
|
|
3332
|
+
y: anchoredLayout.layoutBox.contentBox.y,
|
|
3333
3333
|
width: textWidth,
|
|
3334
3334
|
height: textHeight,
|
|
3335
3335
|
lineCount: lines.length,
|
|
3336
|
-
bbox: textBBox,
|
|
3337
|
-
contentBox: { ...
|
|
3338
|
-
paddingBox: { ...
|
|
3339
|
-
marginBox: { ...
|
|
3340
|
-
clipBox: { ...
|
|
3336
|
+
bbox: anchoredLayout.textBBox,
|
|
3337
|
+
contentBox: { ...anchoredLayout.layoutBox.contentBox },
|
|
3338
|
+
paddingBox: { ...anchoredLayout.layoutBox.paddingBox },
|
|
3339
|
+
marginBox: { ...anchoredLayout.layoutBox.marginBox },
|
|
3340
|
+
clipBox: { ...anchoredLayout.layoutBox.clipBox }
|
|
3341
3341
|
},
|
|
3342
3342
|
prepared: cachedPrepared,
|
|
3343
3343
|
preparedWhiteSpace: cachedPreparedWhiteSpace,
|
|
3344
3344
|
layoutEngine: layoutState.layoutEngine,
|
|
3345
|
-
layoutBox:
|
|
3345
|
+
layoutBox: anchoredLayout.layoutBox,
|
|
3346
3346
|
containerWidth: layoutBox.containerWidth,
|
|
3347
3347
|
containerHeight: layoutBox.containerHeight
|
|
3348
3348
|
};
|
|
@@ -3358,9 +3358,11 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3358
3358
|
const width = normalizeDimension(options.width);
|
|
3359
3359
|
const height = normalizeDimension(options.height);
|
|
3360
3360
|
const gap = normalizeGap(options.gap);
|
|
3361
|
+
const anchor = normalizeAnchor(options.anchor);
|
|
3361
3362
|
if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
|
|
3362
3363
|
if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
|
|
3363
3364
|
if (options.gap != null && gap == null) throw new TypeError("font.paragraph() option \"gap\" must be a non-negative number.");
|
|
3365
|
+
if (options.anchor != null && anchor == null) throw new TypeError("font.paragraph() option \"anchor\" must be a number, [x, y], or { x, y }.");
|
|
3364
3366
|
const font = resolveCanvasFont(fontInstance, textOptions.size, options);
|
|
3365
3367
|
return {
|
|
3366
3368
|
...textOptions,
|
|
@@ -3376,6 +3378,10 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
|
|
|
3376
3378
|
width,
|
|
3377
3379
|
height,
|
|
3378
3380
|
gap: gap ?? DEFAULT_PARAGRAPH_GAP,
|
|
3381
|
+
anchor: anchor ?? {
|
|
3382
|
+
x: 0,
|
|
3383
|
+
y: 0
|
|
3384
|
+
},
|
|
3379
3385
|
lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
|
|
3380
3386
|
align: normalizeEnum(options.align, [
|
|
3381
3387
|
"left",
|
|
@@ -3816,6 +3822,44 @@ function finalizeLayoutBox(layoutBox, options, textHeight) {
|
|
|
3816
3822
|
}
|
|
3817
3823
|
};
|
|
3818
3824
|
}
|
|
3825
|
+
function applyParagraphAnchor(lines, textBBox, layoutBox, anchor) {
|
|
3826
|
+
if (anchor == null || Math.abs(anchor.x) <= JUSTIFY_EPSILON && Math.abs(anchor.y) <= JUSTIFY_EPSILON) return {
|
|
3827
|
+
lines,
|
|
3828
|
+
textBBox,
|
|
3829
|
+
layoutBox
|
|
3830
|
+
};
|
|
3831
|
+
const tx = -layoutBox.contentBox.w * anchor.x;
|
|
3832
|
+
const ty = -layoutBox.contentBox.h * anchor.y;
|
|
3833
|
+
return {
|
|
3834
|
+
lines: translatePositionedLines(lines, tx, ty),
|
|
3835
|
+
textBBox: translateRect(textBBox, tx, ty),
|
|
3836
|
+
layoutBox: translateLayoutBox(layoutBox, tx, ty)
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
function translatePositionedLines(lines, tx, ty) {
|
|
3840
|
+
return lines.map((line) => ({
|
|
3841
|
+
...line,
|
|
3842
|
+
x: line.x + tx,
|
|
3843
|
+
y: line.y + ty,
|
|
3844
|
+
baseline: line.baseline + ty,
|
|
3845
|
+
bbox: translateRect(line.bbox, tx, ty),
|
|
3846
|
+
fragments: line.fragments.map((fragment) => ({
|
|
3847
|
+
...fragment,
|
|
3848
|
+
x: fragment.x + tx
|
|
3849
|
+
}))
|
|
3850
|
+
}));
|
|
3851
|
+
}
|
|
3852
|
+
function translateLayoutBox(layoutBox, tx, ty) {
|
|
3853
|
+
return {
|
|
3854
|
+
...layoutBox,
|
|
3855
|
+
contentX: layoutBox.contentX + tx,
|
|
3856
|
+
contentY: layoutBox.contentY + ty,
|
|
3857
|
+
contentBox: translateRect(layoutBox.contentBox, tx, ty),
|
|
3858
|
+
paddingBox: translateRect(layoutBox.paddingBox, tx, ty),
|
|
3859
|
+
marginBox: translateRect(layoutBox.marginBox, tx, ty),
|
|
3860
|
+
clipBox: translateRect(layoutBox.clipBox, tx, ty)
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3819
3863
|
function resolveContainerDimension(explicit, fallback) {
|
|
3820
3864
|
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
3821
3865
|
if (Number.isFinite(fallback) && fallback > 0) return fallback;
|
|
@@ -3844,6 +3888,40 @@ function normalizeDimension(value) {
|
|
|
3844
3888
|
function normalizeGap(value) {
|
|
3845
3889
|
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
|
3846
3890
|
}
|
|
3891
|
+
function normalizeAnchor(value) {
|
|
3892
|
+
if (value == null) return null;
|
|
3893
|
+
if (Number.isFinite(value)) {
|
|
3894
|
+
const next = Number(value);
|
|
3895
|
+
return {
|
|
3896
|
+
x: next,
|
|
3897
|
+
y: next
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
if (Array.isArray(value)) {
|
|
3901
|
+
if (value.length === 1 && Number.isFinite(value[0])) {
|
|
3902
|
+
const next = Number(value[0]);
|
|
3903
|
+
return {
|
|
3904
|
+
x: next,
|
|
3905
|
+
y: next
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
if (value.length >= 2 && Number.isFinite(value[0]) && Number.isFinite(value[1])) return {
|
|
3909
|
+
x: Number(value[0]),
|
|
3910
|
+
y: Number(value[1])
|
|
3911
|
+
};
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
if (typeof value === "object") {
|
|
3915
|
+
const hasX = Number.isFinite(value.x);
|
|
3916
|
+
const hasY = Number.isFinite(value.y);
|
|
3917
|
+
if (!hasX && !hasY) return null;
|
|
3918
|
+
return {
|
|
3919
|
+
x: hasX ? Number(value.x) : 0,
|
|
3920
|
+
y: hasY ? Number(value.y) : 0
|
|
3921
|
+
};
|
|
3922
|
+
}
|
|
3923
|
+
return null;
|
|
3924
|
+
}
|
|
3847
3925
|
function normalizeSpacing(value) {
|
|
3848
3926
|
if (value == null) return zeroSpacing();
|
|
3849
3927
|
if (Number.isFinite(value)) {
|