fix text align
This commit is contained in:
@@ -144,7 +144,7 @@ export const getEffectiveTextWidth = (
|
|||||||
return Math.max(1, base + SINGLE_LINE_WIDTH_SLACK_PX);
|
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 = (
|
export const getScaledTextWidth = (
|
||||||
width: number | undefined,
|
width: number | undefined,
|
||||||
scale: number,
|
scale: number,
|
||||||
@@ -152,3 +152,68 @@ export const getScaledTextWidth = (
|
|||||||
if (!width) return undefined;
|
if (!width) return undefined;
|
||||||
return Math.ceil(width * scale) + Math.ceil(SINGLE_LINE_WIDTH_SLACK_PX * scale);
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import { toCssLinearGradient } from '@/pages/editor/utils/gradient';
|
|||||||
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
import { getFontFamily } from '@/pages/editor/utils/fontFamily';
|
||||||
import {
|
import {
|
||||||
getCssFontWeight,
|
getCssFontWeight,
|
||||||
getEffectiveTextWidth,
|
getViewerTextLayout,
|
||||||
getScaledTextWidth,
|
|
||||||
usesWrappedLayout,
|
usesWrappedLayout,
|
||||||
SINGLE_LINE_WIDTH_SLACK_PX,
|
|
||||||
} from '@/pages/editor/utils/textStyle';
|
} from '@/pages/editor/utils/textStyle';
|
||||||
import '@/pages/viewer/styles/entranceAnimations.css';
|
import '@/pages/viewer/styles/entranceAnimations.css';
|
||||||
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
import { mergeEntranceAnimationStyle } from '@/pages/viewer/utils/entranceAnimationStyle';
|
||||||
@@ -131,51 +129,41 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
letterSpacing: obj.letterSpacing ?? 0,
|
letterSpacing: obj.letterSpacing ?? 0,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
};
|
};
|
||||||
const layoutWidth = wrapped
|
const textLayout = getViewerTextLayout({
|
||||||
? getEffectiveTextWidth(obj.width, obj.text, measureOpts, false)
|
x: obj.x || 0,
|
||||||
: undefined;
|
width: obj.width,
|
||||||
const textWidth = wrapped ? getScaledTextWidth(layoutWidth, scale) : undefined;
|
text: obj.text,
|
||||||
// getScaledTextWidth adds SINGLE_LINE_WIDTH_SLACK_PX to prevent unwanted CSS wrapping.
|
textAlign: obj.textAlign,
|
||||||
// Compensate by shifting textLeft left by the same amount so the right edge stays at obj.x * scale.
|
rotation,
|
||||||
const textSlackPx = textWidth !== undefined ? Math.ceil(SINGLE_LINE_WIDTH_SLACK_PX * scale) : 0;
|
scale,
|
||||||
const textLeft = wrapped
|
measureOpts,
|
||||||
? Math.round(((obj.x || 0) - (layoutWidth || obj.width || 0)) * scale) - textSlackPx
|
wrapped,
|
||||||
: Math.round((obj.x || 0) * scale);
|
});
|
||||||
const textTransform = wrapped
|
|
||||||
? rotation
|
|
||||||
? `rotate(${rotation}deg)`
|
|
||||||
: undefined
|
|
||||||
: rotation
|
|
||||||
? `rotate(${rotation}deg) translateX(-100%)`
|
|
||||||
: 'translateX(-100%)';
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
style={withEntrance(
|
style={withEntrance(
|
||||||
{
|
{
|
||||||
...textBaseStyle,
|
...textBaseStyle,
|
||||||
left: `${textLeft}px`,
|
left: `${textLayout.leftPx}px`,
|
||||||
width: wrapped ? (textWidth ? `${textWidth}px` : undefined) : 'max-content',
|
width: textLayout.widthPx !== undefined ? `${textLayout.widthPx}px` : 'max-content',
|
||||||
maxWidth: wrapped ? undefined : 'none',
|
|
||||||
fontSize: `${fontSize * scale}px`,
|
fontSize: `${fontSize * scale}px`,
|
||||||
fontFamily: getFontFamily(obj.fontFamily),
|
fontFamily: getFontFamily(obj.fontFamily),
|
||||||
fontWeight: getCssFontWeight(obj.fontWeight),
|
fontWeight: getCssFontWeight(obj.fontWeight),
|
||||||
lineHeight,
|
lineHeight,
|
||||||
textAlign: obj.textAlign ?? 'right',
|
textAlign: textLayout.textAlign,
|
||||||
direction: 'rtl',
|
direction: 'rtl',
|
||||||
color: getColorWithOpacity(obj.fill, obj.opacity),
|
color: getColorWithOpacity(obj.fill, obj.opacity),
|
||||||
whiteSpace: wrapped ? 'pre-wrap' : 'nowrap',
|
whiteSpace: wrapped ? 'pre-wrap' : 'nowrap',
|
||||||
overflowWrap: wrapped ? 'break-word' : 'normal',
|
overflowWrap: wrapped ? 'break-word' : 'normal',
|
||||||
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
||||||
boxSizing: 'content-box',
|
boxSizing: 'content-box',
|
||||||
transform: textTransform,
|
transform: textLayout.transform,
|
||||||
transformOrigin: wrapped ? 'top left' : 'top right',
|
transformOrigin: textLayout.transformOrigin,
|
||||||
},
|
},
|
||||||
obj,
|
obj,
|
||||||
index,
|
index,
|
||||||
wrapped
|
{ rotationDeg: rotation, transformOrigin: textLayout.transformOrigin },
|
||||||
? { rotationDeg: rotation, transformOrigin: 'top left' }
|
|
||||||
: { rotationDeg: rotation, transformOrigin: 'top right' },
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{obj.text || ''}
|
{obj.text || ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user