pa_font 0.3.1 → 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
 
@@ -119,7 +119,6 @@ console.log(metrics.width, metrics.bbox);
119
119
  - `overflowWrap`: `normal | break-word | anywhere`
120
120
  - `wordBreak`: `normal | break-all | keep-all`
121
121
  - `overflow`: `visible | hidden`
122
- - `overflowY`: `visible | hidden | scroll`
123
122
  - `textOverflow`: `clip | ellipsis`
124
123
  - `fontFamily`, `fontWeight`, `fontStyle`, `font`
125
124
  - `engine`: `pretext | native`
@@ -136,9 +135,15 @@ console.log(metrics.width, metrics.bbox);
136
135
  빈 줄 하나 이상은 새 문단으로 처리됩니다.
137
136
 
138
137
  - `gap`: 문단 사이 간격. `lineHeight` 배수이며 기본값은 `0.5`
138
+ - `anchor`: 기준점을 옮깁니다. `anchor: 0.5`면 CSS의 `translate(-50%, -50%)`처럼 동작
139
139
  - `whiteSpace: "normal"`이면 문단 안 단일 줄바꿈은 공백으로 정리
140
140
  - `whiteSpace: "pre-wrap"`이면 문단 안 단일 줄바꿈 유지
141
- - `overflowY: "scroll"`을 쓰려면 `height`가 필요
141
+
142
+ `anchor`는 다음 형식을 지원합니다.
143
+
144
+ - `0.5` -> `{ x: 0.5, y: 0.5 }`
145
+ - `[0.5, 0]`
146
+ - `{ x: 0.5, y: 0.5 }`
142
147
 
143
148
  ## paParagraph API
144
149
 
@@ -168,6 +173,19 @@ const mobile = paragraph.relayout({ width: 280 });
168
173
  엄격한 `overflowWrap: "normal"`이 필요한 경우만 native로 내려갑니다.
169
174
  실제로 사용된 엔진은 `paragraph.layoutEngine`에서 확인할 수 있습니다.
170
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
+
171
189
  ### `paragraph.drawText(ctx, options?)`
172
190
 
173
191
  현재 문단 레이아웃을 canvas 텍스트로 그립니다.
@@ -184,28 +202,6 @@ paragraph.drawText(ctx, {
184
202
  - `strokeStyle`
185
203
  - `fill`
186
204
  - `stroke`
187
- - `scrollTop`
188
-
189
- 스크롤 가능한 viewport를 만들고 싶다면:
190
-
191
- ```js
192
- const paragraph = font.paragraph(text, {
193
- width: 320,
194
- height: 220,
195
- overflowY: "scroll",
196
- lineHeight: 1.5,
197
- gap: 0.5,
198
- });
199
-
200
- paragraph.drawText(ctx, {
201
- fillStyle: "#111",
202
- scrollTop: 48,
203
- });
204
- ```
205
-
206
- 전체 콘텐츠 높이는 `paragraph.metrics.height`,
207
- 현재 viewport 높이는 `paragraph.metrics.viewportHeight`,
208
- 최대 스크롤 값은 `paragraph.metrics.maxScrollTop`에서 확인할 수 있습니다.
209
205
 
210
206
  ### `paragraph.toShape(options?)`
211
207
 
package/dist/paFont.cjs CHANGED
@@ -3321,30 +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
- viewportHeight: finalLayoutBox.contentBox.h,
3336
- maxScrollTop: Math.max(0, textHeight - finalLayoutBox.contentBox.h),
3337
3335
  lineCount: lines.length,
3338
- bbox: textBBox,
3339
- contentBox: { ...finalLayoutBox.contentBox },
3340
- paddingBox: { ...finalLayoutBox.paddingBox },
3341
- marginBox: { ...finalLayoutBox.marginBox },
3342
- 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 }
3343
3341
  },
3344
3342
  prepared: cachedPrepared,
3345
3343
  preparedWhiteSpace: cachedPreparedWhiteSpace,
3346
3344
  layoutEngine: layoutState.layoutEngine,
3347
- layoutBox: finalLayoutBox,
3345
+ layoutBox: anchoredLayout.layoutBox,
3348
3346
  containerWidth: layoutBox.containerWidth,
3349
3347
  containerHeight: layoutBox.containerHeight
3350
3348
  };
@@ -3360,12 +3358,11 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
3360
3358
  const width = normalizeDimension(options.width);
3361
3359
  const height = normalizeDimension(options.height);
3362
3360
  const gap = normalizeGap(options.gap);
3363
- const overflow = normalizeEnum(options.overflow, ["visible", "hidden"], "visible");
3364
- const overflowY = resolveOverflowY(options.overflowY, overflow);
3361
+ const anchor = normalizeAnchor(options.anchor);
3365
3362
  if (options.width != null && width == null) throw new TypeError("font.paragraph() option \"width\" must be a positive number.");
3366
3363
  if (options.height != null && height == null) throw new TypeError("font.paragraph() option \"height\" must be a positive number.");
3367
3364
  if (options.gap != null && gap == null) throw new TypeError("font.paragraph() option \"gap\" must be a non-negative number.");
3368
- if (overflowY === "scroll" && height == null) throw new TypeError("font.paragraph() option \"height\" is required when \"overflowY\" is \"scroll\".");
3365
+ if (options.anchor != null && anchor == null) throw new TypeError("font.paragraph() option \"anchor\" must be a number, [x, y], or { x, y }.");
3369
3366
  const font = resolveCanvasFont(fontInstance, textOptions.size, options);
3370
3367
  return {
3371
3368
  ...textOptions,
@@ -3381,6 +3378,10 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
3381
3378
  width,
3382
3379
  height,
3383
3380
  gap: gap ?? DEFAULT_PARAGRAPH_GAP,
3381
+ anchor: anchor ?? {
3382
+ x: 0,
3383
+ y: 0
3384
+ },
3384
3385
  lineHeight: resolveLineHeight(options.lineHeight, textOptions.size),
3385
3386
  align: normalizeEnum(options.align, [
3386
3387
  "left",
@@ -3403,8 +3404,7 @@ function normalizeParagraphOptions(fontInstance, options = {}) {
3403
3404
  "break-all",
3404
3405
  "keep-all"
3405
3406
  ], wrapDefaults.wordBreak),
3406
- overflow,
3407
- overflowY,
3407
+ overflow: normalizeEnum(options.overflow, ["visible", "hidden"], "visible"),
3408
3408
  textOverflow: normalizeNullableEnum(options.textOverflow, ["clip", "ellipsis"], null),
3409
3409
  maxLines: normalizeMaxLines(options.maxLines),
3410
3410
  ellipsis: normalizeEllipsis(options.ellipsis),
@@ -3593,7 +3593,7 @@ function applyOverflowClamping(lines, options, layoutBox, measureWidth) {
3593
3593
  const contentWidth = layoutBox.contentWidth;
3594
3594
  const result = lines.map((line) => ({ ...line }));
3595
3595
  let lineLimit = options.maxLines;
3596
- if (shouldClampLinesToHeight(options) && layoutBox.clipContentHeight != null) {
3596
+ if (options.overflow === "hidden" && layoutBox.clipContentHeight != null) {
3597
3597
  const visibleLineCount = countVisibleLinesForHeight(result, options, layoutBox.clipContentHeight);
3598
3598
  lineLimit = lineLimit == null ? visibleLineCount : Math.min(lineLimit, visibleLineCount);
3599
3599
  }
@@ -3818,10 +3818,48 @@ function finalizeLayoutBox(layoutBox, options, textHeight) {
3818
3818
  x: layoutBox.contentX,
3819
3819
  y: layoutBox.contentY,
3820
3820
  w: layoutBox.contentWidth,
3821
- h: shouldClipToContentHeight(options) ? contentHeight : textHeight
3821
+ h: options.overflow === "hidden" ? contentHeight : textHeight
3822
3822
  }
3823
3823
  };
3824
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
+ }
3825
3863
  function resolveContainerDimension(explicit, fallback) {
3826
3864
  if (Number.isFinite(explicit) && explicit > 0) return explicit;
3827
3865
  if (Number.isFinite(fallback) && fallback > 0) return fallback;
@@ -3834,19 +3872,6 @@ function normalizeNullableEnum(value, supported, fallback) {
3834
3872
  if (value == null) return fallback;
3835
3873
  return typeof value === "string" && supported.includes(value) ? value : fallback;
3836
3874
  }
3837
- function resolveOverflowY(value, overflow) {
3838
- return normalizeEnum(value, [
3839
- "visible",
3840
- "hidden",
3841
- "scroll"
3842
- ], overflow === "hidden" ? "hidden" : "visible");
3843
- }
3844
- function shouldClampLinesToHeight(options) {
3845
- return options.overflowY === "hidden";
3846
- }
3847
- function shouldClipToContentHeight(options) {
3848
- return options.overflow === "hidden" || options.overflowY === "hidden" || options.overflowY === "scroll";
3849
- }
3850
3875
  function normalizeEllipsis(value) {
3851
3876
  if (value === false) return false;
3852
3877
  if (typeof value === "string") return value;
@@ -3863,6 +3888,40 @@ function normalizeDimension(value) {
3863
3888
  function normalizeGap(value) {
3864
3889
  return Number.isFinite(value) && value >= 0 ? Number(value) : null;
3865
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
+ }
3866
3925
  function normalizeSpacing(value) {
3867
3926
  if (value == null) return zeroSpacing();
3868
3927
  if (Number.isFinite(value)) {
@@ -4283,7 +4342,6 @@ var paParagraph = class paParagraph {
4283
4342
  if (!ctx || typeof ctx.fillText !== "function") throw new TypeError("drawText() expects a CanvasRenderingContext2D.");
4284
4343
  const fill = options.fill !== false;
4285
4344
  const stroke = options.stroke === true;
4286
- const scrollTop = clampScrollTop(options.scrollTop, this.metrics.maxScrollTop);
4287
4345
  if (!fill && !stroke) return;
4288
4346
  this._syncLayoutWithContext(ctx);
4289
4347
  ctx.save();
@@ -4292,13 +4350,12 @@ var paParagraph = class paParagraph {
4292
4350
  ctx.textBaseline = "alphabetic";
4293
4351
  if (options.fillStyle != null) ctx.fillStyle = options.fillStyle;
4294
4352
  if (options.strokeStyle != null) ctx.strokeStyle = options.strokeStyle;
4295
- if (shouldClipParagraph(this.options) && this._state.layoutBox?.clipBox) {
4353
+ if (this.options.overflow === "hidden" && this._state.layoutBox?.clipBox) {
4296
4354
  const clipBox = this._state.layoutBox.clipBox;
4297
4355
  ctx.beginPath();
4298
4356
  ctx.rect(clipBox.x, clipBox.y, clipBox.w, clipBox.h);
4299
4357
  ctx.clip();
4300
4358
  }
4301
- if (scrollTop > 0) ctx.translate(0, -scrollTop);
4302
4359
  this._state.lines.forEach((line) => {
4303
4360
  line.fragments.forEach((fragment) => {
4304
4361
  if (fill) ctx.fillText(fragment.text, fragment.x, line.baseline);
@@ -4349,8 +4406,6 @@ var paParagraph = class paParagraph {
4349
4406
  this.metrics = {
4350
4407
  ...state.metrics,
4351
4408
  bbox: { ...state.metrics.bbox },
4352
- viewportHeight: state.metrics.viewportHeight,
4353
- maxScrollTop: state.metrics.maxScrollTop,
4354
4409
  contentBox: state.metrics.contentBox ? { ...state.metrics.contentBox } : void 0,
4355
4410
  paddingBox: state.metrics.paddingBox ? { ...state.metrics.paddingBox } : void 0,
4356
4411
  marginBox: state.metrics.marginBox ? { ...state.metrics.marginBox } : void 0,
@@ -4445,13 +4500,6 @@ function normalizeParagraphPointOptions(options = {}) {
4445
4500
  layout: options.layout ?? "current"
4446
4501
  };
4447
4502
  }
4448
- function shouldClipParagraph(options) {
4449
- return options.overflow === "hidden" || options.overflowY === "hidden" || options.overflowY === "scroll";
4450
- }
4451
- function clampScrollTop(value, maxScrollTop) {
4452
- if (!Number.isFinite(value) || value <= 0) return 0;
4453
- return Math.min(Number(value), maxScrollTop ?? 0);
4454
- }
4455
4503
  //#endregion
4456
4504
  //#region src/paFont/paFont.js
4457
4505
  var browserFontRegistrationId = 0;