diff --git a/src/pages/editor/utils/textStyle.ts b/src/pages/editor/utils/textStyle.ts index 10f6225..1fd6db6 100644 --- a/src/pages/editor/utils/textStyle.ts +++ b/src/pages/editor/utils/textStyle.ts @@ -144,7 +144,7 @@ export const getEffectiveTextWidth = ( return Math.max(1, base + SINGLE_LINE_WIDTH_SLACK_PX); }; -/** Scaled width for viewer text (wrapped / multi-line layout only). */ +/** Scaled width for viewer text boxes (includes slack to avoid tighter CSS wrapping than Konva). */ export const getScaledTextWidth = ( width: number | undefined, scale: number, @@ -152,3 +152,68 @@ export const getScaledTextWidth = ( if (!width) return undefined; return Math.ceil(width * scale) + Math.ceil(SINGLE_LINE_WIDTH_SLACK_PX * scale); }; + +export type TextAlignValue = "left" | "center" | "right"; + +export type ViewerTextLayout = { + leftPx: number; + widthPx: number | undefined; + textAlign: TextAlignValue; + transform: string | undefined; + transformOrigin: "top right"; +}; + +/** + * Viewer HTML layout matching Konva TextShape: (x, y) is the top-right anchor of the text box. + * Text alignment is applied inside a fixed-width box that extends left from the anchor. + */ +export function getViewerTextLayout(args: { + x: number; + width?: number; + text?: string; + textAlign?: TextAlignValue; + rotation?: number; + scale: number; + measureOpts: TextMeasureOptions; + wrapped: boolean; +}): ViewerTextLayout { + const { + x, + width: storedWidth, + text, + textAlign = "right", + rotation = 0, + scale, + measureOpts, + wrapped, + } = args; + + const layoutWidth = getEffectiveTextWidth( + storedWidth, + text, + measureOpts, + !wrapped, + ); + const widthPx = getScaledTextWidth(layoutWidth, scale); + const slackPx = + widthPx !== undefined ? Math.ceil(SINGLE_LINE_WIDTH_SLACK_PX * scale) : 0; + const rightPx = x * scale; + const boxWidthUnscaled = layoutWidth ?? storedWidth ?? 0; + + let leftPx: number; + if (widthPx !== undefined && boxWidthUnscaled > 0) { + leftPx = Math.round(rightPx - boxWidthUnscaled * scale) - slackPx; + } else { + leftPx = Math.round(rightPx); + } + + const rotationTransform = rotation ? `rotate(${rotation}deg)` : undefined; + + return { + leftPx, + widthPx, + textAlign, + transform: rotationTransform, + transformOrigin: "top right", + }; +} diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx index 5490a33..d93a6f3 100644 --- a/src/pages/viewer/components/BookPage.tsx +++ b/src/pages/viewer/components/BookPage.tsx @@ -5,10 +5,8 @@ import { toCssLinearGradient } from '@/pages/editor/utils/gradient'; import { getFontFamily } from '@/pages/editor/utils/fontFamily'; import { getCssFontWeight, - getEffectiveTextWidth, - getScaledTextWidth, + getViewerTextLayout, usesWrappedLayout, - SINGLE_LINE_WIDTH_SLACK_PX, } from '@/pages/editor/utils/textStyle'; import '@/pages/viewer/styles/entranceAnimations.css'; import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle'; @@ -131,51 +129,41 @@ const BookPage = forwardRef( letterSpacing: obj.letterSpacing ?? 0, lineHeight, }; - const layoutWidth = wrapped - ? getEffectiveTextWidth(obj.width, obj.text, measureOpts, false) - : undefined; - const textWidth = wrapped ? getScaledTextWidth(layoutWidth, scale) : undefined; - // getScaledTextWidth adds SINGLE_LINE_WIDTH_SLACK_PX to prevent unwanted CSS wrapping. - // Compensate by shifting textLeft left by the same amount so the right edge stays at obj.x * scale. - const textSlackPx = textWidth !== undefined ? Math.ceil(SINGLE_LINE_WIDTH_SLACK_PX * scale) : 0; - const textLeft = wrapped - ? Math.round(((obj.x || 0) - (layoutWidth || obj.width || 0)) * scale) - textSlackPx - : Math.round((obj.x || 0) * scale); - const textTransform = wrapped - ? rotation - ? `rotate(${rotation}deg)` - : undefined - : rotation - ? `rotate(${rotation}deg) translateX(-100%)` - : 'translateX(-100%)'; + const textLayout = getViewerTextLayout({ + x: obj.x || 0, + width: obj.width, + text: obj.text, + textAlign: obj.textAlign, + rotation, + scale, + measureOpts, + wrapped, + }); return (
{obj.text || ''}