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 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 finalLayoutBox = finalizeLayoutBox(layoutBox, normalized, textHeight);
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: finalLayoutBox.contentBox.x,
3332
- y: finalLayoutBox.contentBox.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: { ...finalLayoutBox.contentBox },
3338
- paddingBox: { ...finalLayoutBox.paddingBox },
3339
- marginBox: { ...finalLayoutBox.marginBox },
3340
- clipBox: { ...finalLayoutBox.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: finalLayoutBox,
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)) {