mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
fix a ton of issues + improvement
This commit is contained in:
@@ -2,9 +2,11 @@ import type { TimelineTrack, TimelineElement } from "@/types/timeline";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import { isMainTrack } from "@/lib/timeline";
|
||||
import {
|
||||
DEFAULT_TEXT_ELEMENT,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
FONT_SIZE_SCALE_REFERENCE,
|
||||
} from "@/constants/text-constants";
|
||||
import { getTextVisualRect, measureTextBlock } from "@/lib/text/layout";
|
||||
|
||||
export interface ElementBounds {
|
||||
cx: number;
|
||||
@@ -121,27 +123,36 @@ export function getElementBounds({
|
||||
|
||||
const lines = element.content.split("\n");
|
||||
const lineMetrics = lines.map((line) => ctx.measureText(line));
|
||||
|
||||
let top = Number.POSITIVE_INFINITY;
|
||||
let bottom = Number.NEGATIVE_INFINITY;
|
||||
let maxWidth = 0;
|
||||
|
||||
for (let i = 0; i < lineMetrics.length; i++) {
|
||||
const metrics = lineMetrics[i];
|
||||
const y = i * lineHeightPx;
|
||||
top = Math.min(
|
||||
top,
|
||||
y - (metrics.actualBoundingBoxAscent ?? scaledFontSize * 0.8),
|
||||
);
|
||||
bottom = Math.max(
|
||||
bottom,
|
||||
y + (metrics.actualBoundingBoxDescent ?? scaledFontSize * 0.2),
|
||||
);
|
||||
maxWidth = Math.max(maxWidth, metrics.width);
|
||||
}
|
||||
|
||||
measuredWidth = maxWidth;
|
||||
measuredHeight = bottom - top;
|
||||
const block = measureTextBlock({
|
||||
lineMetrics,
|
||||
lineHeightPx,
|
||||
fallbackFontSize: scaledFontSize,
|
||||
});
|
||||
const fontSizeRatio = element.fontSize / DEFAULT_TEXT_ELEMENT.fontSize;
|
||||
const visualRect = getTextVisualRect({
|
||||
textAlign: element.textAlign,
|
||||
block,
|
||||
background: element.background,
|
||||
fontSizeRatio,
|
||||
});
|
||||
measuredWidth = visualRect.width;
|
||||
measuredHeight = visualRect.height;
|
||||
const localCenterX = visualRect.left + visualRect.width / 2;
|
||||
const localCenterY = visualRect.top + visualRect.height / 2;
|
||||
const scaledCenterX = localCenterX * element.transform.scale;
|
||||
const scaledCenterY = localCenterY * element.transform.scale;
|
||||
const rotationRad = (element.transform.rotate * Math.PI) / 180;
|
||||
const cos = Math.cos(rotationRad);
|
||||
const sin = Math.sin(rotationRad);
|
||||
const rotatedCenterX = scaledCenterX * cos - scaledCenterY * sin;
|
||||
const rotatedCenterY = scaledCenterX * sin + scaledCenterY * cos;
|
||||
return {
|
||||
cx: canvasWidth / 2 + element.transform.position.x + rotatedCenterX,
|
||||
cy: canvasHeight / 2 + element.transform.position.y + rotatedCenterY,
|
||||
width: measuredWidth * element.transform.scale,
|
||||
height: measuredHeight * element.transform.scale,
|
||||
rotation: element.transform.rotate,
|
||||
};
|
||||
}
|
||||
|
||||
const width = measuredWidth * element.transform.scale;
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { DEFAULT_TEXT_BACKGROUND } from "@/constants/text-constants";
|
||||
import type { TextElement } from "@/types/timeline";
|
||||
|
||||
type TextRect = {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export interface TextBlockMeasurement {
|
||||
visualCenterOffset: number;
|
||||
height: number;
|
||||
maxWidth: number;
|
||||
}
|
||||
|
||||
export function getMetricAscent({
|
||||
metrics,
|
||||
fallbackFontSize,
|
||||
}: {
|
||||
metrics: TextMetrics;
|
||||
fallbackFontSize: number;
|
||||
}): number {
|
||||
return metrics.actualBoundingBoxAscent ?? fallbackFontSize * 0.8;
|
||||
}
|
||||
|
||||
export function getMetricDescent({
|
||||
metrics,
|
||||
fallbackFontSize,
|
||||
}: {
|
||||
metrics: TextMetrics;
|
||||
fallbackFontSize: number;
|
||||
}): number {
|
||||
return metrics.actualBoundingBoxDescent ?? fallbackFontSize * 0.2;
|
||||
}
|
||||
|
||||
export function measureTextBlock({
|
||||
lineMetrics,
|
||||
lineHeightPx,
|
||||
fallbackFontSize,
|
||||
}: {
|
||||
lineMetrics: TextMetrics[];
|
||||
lineHeightPx: number;
|
||||
fallbackFontSize: number;
|
||||
}): TextBlockMeasurement {
|
||||
let top = Number.POSITIVE_INFINITY;
|
||||
let bottom = Number.NEGATIVE_INFINITY;
|
||||
let maxWidth = 0;
|
||||
|
||||
for (let index = 0; index < lineMetrics.length; index++) {
|
||||
const metrics = lineMetrics[index];
|
||||
const lineY = index * lineHeightPx;
|
||||
top = Math.min(
|
||||
top,
|
||||
lineY - getMetricAscent({ metrics, fallbackFontSize }),
|
||||
);
|
||||
bottom = Math.max(
|
||||
bottom,
|
||||
lineY + getMetricDescent({ metrics, fallbackFontSize }),
|
||||
);
|
||||
maxWidth = Math.max(maxWidth, metrics.width);
|
||||
}
|
||||
|
||||
const height = bottom - top;
|
||||
const visualCenterOffset = (top + bottom) / 2;
|
||||
|
||||
return { visualCenterOffset, height, maxWidth };
|
||||
}
|
||||
|
||||
function getTextRect({
|
||||
textAlign,
|
||||
block,
|
||||
}: {
|
||||
textAlign: TextElement["textAlign"];
|
||||
block: TextBlockMeasurement;
|
||||
}): TextRect {
|
||||
const left =
|
||||
textAlign === "left" ? 0 : textAlign === "right" ? -block.maxWidth : -block.maxWidth / 2;
|
||||
|
||||
return {
|
||||
left,
|
||||
top: -block.height / 2,
|
||||
width: block.maxWidth,
|
||||
height: block.height,
|
||||
};
|
||||
}
|
||||
|
||||
function isTextBackgroundVisible({
|
||||
background,
|
||||
}: {
|
||||
background: TextElement["background"];
|
||||
}): boolean {
|
||||
return Boolean(background.color) && background.color !== "transparent";
|
||||
}
|
||||
|
||||
export function getTextBackgroundRect({
|
||||
textAlign,
|
||||
block,
|
||||
background,
|
||||
fontSizeRatio = 1,
|
||||
}: {
|
||||
textAlign: TextElement["textAlign"];
|
||||
block: TextBlockMeasurement;
|
||||
background: TextElement["background"];
|
||||
fontSizeRatio?: number;
|
||||
}): TextRect | null {
|
||||
if (!isTextBackgroundVisible({ background })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const textRect = getTextRect({ textAlign, block });
|
||||
const paddingX =
|
||||
(background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) * fontSizeRatio;
|
||||
const paddingY =
|
||||
(background.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY) * fontSizeRatio;
|
||||
const offsetX = background.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX;
|
||||
const offsetY = background.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY;
|
||||
|
||||
return {
|
||||
left: textRect.left - paddingX + offsetX,
|
||||
top: textRect.top - paddingY + offsetY,
|
||||
width: textRect.width + paddingX * 2,
|
||||
height: textRect.height + paddingY * 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTextVisualRect({
|
||||
textAlign,
|
||||
block,
|
||||
background,
|
||||
fontSizeRatio = 1,
|
||||
}: {
|
||||
textAlign: TextElement["textAlign"];
|
||||
block: TextBlockMeasurement;
|
||||
background: TextElement["background"];
|
||||
fontSizeRatio?: number;
|
||||
}): TextRect {
|
||||
const textRect = getTextRect({ textAlign, block });
|
||||
const backgroundRect = getTextBackgroundRect({
|
||||
textAlign,
|
||||
block,
|
||||
background,
|
||||
fontSizeRatio,
|
||||
});
|
||||
|
||||
if (!backgroundRect) {
|
||||
return textRect;
|
||||
}
|
||||
|
||||
const left = Math.min(textRect.left, backgroundRect.left);
|
||||
const top = Math.min(textRect.top, backgroundRect.top);
|
||||
const right = Math.max(
|
||||
textRect.left + textRect.width,
|
||||
backgroundRect.left + backgroundRect.width,
|
||||
);
|
||||
const bottom = Math.max(
|
||||
textRect.top + textRect.height,
|
||||
backgroundRect.top + backgroundRect.height,
|
||||
);
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
width: right - left,
|
||||
height: bottom - top,
|
||||
};
|
||||
}
|
||||
@@ -154,7 +154,14 @@ export function buildTextElement({
|
||||
: DEFAULT_TEXT_ELEMENT.fontSize,
|
||||
fontFamily: t.fontFamily ?? DEFAULT_TEXT_ELEMENT.fontFamily,
|
||||
color: t.color ?? DEFAULT_TEXT_ELEMENT.color,
|
||||
backgroundColor: t.backgroundColor ?? DEFAULT_TEXT_ELEMENT.backgroundColor,
|
||||
background: {
|
||||
color: t.background?.color ?? DEFAULT_TEXT_ELEMENT.background.color,
|
||||
cornerRadius: t.background?.cornerRadius,
|
||||
paddingX: t.background?.paddingX,
|
||||
paddingY: t.background?.paddingY,
|
||||
offsetX: t.background?.offsetX,
|
||||
offsetY: t.background?.offsetY,
|
||||
},
|
||||
textAlign: t.textAlign ?? DEFAULT_TEXT_ELEMENT.textAlign,
|
||||
fontWeight: t.fontWeight ?? DEFAULT_TEXT_ELEMENT.fontWeight,
|
||||
fontStyle: t.fontStyle ?? DEFAULT_TEXT_ELEMENT.fontStyle,
|
||||
|
||||
Reference in New Issue
Block a user