mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
feat: implement keyframe support for more properties at the core (no ui yet)
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import type {
|
||||||
|
AnimationPropertyPath,
|
||||||
|
ColorAnimationChannel,
|
||||||
|
ElementAnimations,
|
||||||
|
} from "@/types/animation";
|
||||||
|
|
||||||
|
export function getColorChannelForPath({
|
||||||
|
animations,
|
||||||
|
propertyPath,
|
||||||
|
}: {
|
||||||
|
animations: ElementAnimations | undefined;
|
||||||
|
propertyPath: AnimationPropertyPath;
|
||||||
|
}): ColorAnimationChannel | undefined {
|
||||||
|
const channel = animations?.channels[propertyPath];
|
||||||
|
if (!channel || channel.valueKind !== "color") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
getElementLocalTime,
|
getElementLocalTime,
|
||||||
|
resolveColorAtTime,
|
||||||
resolveOpacityAtTime,
|
resolveOpacityAtTime,
|
||||||
resolveTransformAtTime,
|
resolveTransformAtTime,
|
||||||
resolveVolumeAtTime,
|
resolveVolumeAtTime,
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export function getNumberChannelValueAtTime({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColorValueAtTime({
|
export function getColorValueAtTime({
|
||||||
channel,
|
channel,
|
||||||
time,
|
time,
|
||||||
fallbackValue,
|
fallbackValue,
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ interface AnimationPropertyDefinition {
|
|||||||
defaultInterpolation: AnimationInterpolation;
|
defaultInterpolation: AnimationInterpolation;
|
||||||
numericRange?: NumericRange;
|
numericRange?: NumericRange;
|
||||||
supportsElement: ({ element }: { element: TimelineElement }) => boolean;
|
supportsElement: ({ element }: { element: TimelineElement }) => boolean;
|
||||||
getValue: ({ element }: { element: TimelineElement }) => number | null;
|
getValue: ({ element }: { element: TimelineElement }) => AnimationValue | null;
|
||||||
setValue: ({
|
setValue: ({
|
||||||
element,
|
element,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
element: TimelineElement;
|
element: TimelineElement;
|
||||||
value: number;
|
value: AnimationValue;
|
||||||
}) => TimelineElement;
|
}) => TimelineElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
...element,
|
...element,
|
||||||
transform: {
|
transform: {
|
||||||
...element.transform,
|
...element.transform,
|
||||||
position: { ...element.transform.position, x: value },
|
position: { ...element.transform.position, x: value as number },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: element,
|
: element,
|
||||||
@@ -62,7 +62,7 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
...element,
|
...element,
|
||||||
transform: {
|
transform: {
|
||||||
...element.transform,
|
...element.transform,
|
||||||
position: { ...element.transform.position, y: value },
|
position: { ...element.transform.position, y: value as number },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: element,
|
: element,
|
||||||
@@ -76,7 +76,10 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
isVisualElement(element) ? element.transform.scale : null,
|
isVisualElement(element) ? element.transform.scale : null,
|
||||||
setValue: ({ element, value }) =>
|
setValue: ({ element, value }) =>
|
||||||
isVisualElement(element)
|
isVisualElement(element)
|
||||||
? { ...element, transform: { ...element.transform, scale: value } }
|
? {
|
||||||
|
...element,
|
||||||
|
transform: { ...element.transform, scale: value as number },
|
||||||
|
}
|
||||||
: element,
|
: element,
|
||||||
},
|
},
|
||||||
"transform.rotate": {
|
"transform.rotate": {
|
||||||
@@ -87,7 +90,10 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
isVisualElement(element) ? element.transform.rotate : null,
|
isVisualElement(element) ? element.transform.rotate : null,
|
||||||
setValue: ({ element, value }) =>
|
setValue: ({ element, value }) =>
|
||||||
isVisualElement(element)
|
isVisualElement(element)
|
||||||
? { ...element, transform: { ...element.transform, rotate: value } }
|
? {
|
||||||
|
...element,
|
||||||
|
transform: { ...element.transform, rotate: value as number },
|
||||||
|
}
|
||||||
: element,
|
: element,
|
||||||
},
|
},
|
||||||
opacity: {
|
opacity: {
|
||||||
@@ -98,7 +104,9 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
getValue: ({ element }) =>
|
getValue: ({ element }) =>
|
||||||
isVisualElement(element) ? element.opacity : null,
|
isVisualElement(element) ? element.opacity : null,
|
||||||
setValue: ({ element, value }) =>
|
setValue: ({ element, value }) =>
|
||||||
isVisualElement(element) ? { ...element, opacity: value } : element,
|
isVisualElement(element)
|
||||||
|
? { ...element, opacity: value as number }
|
||||||
|
: element,
|
||||||
},
|
},
|
||||||
volume: {
|
volume: {
|
||||||
valueKind: "number",
|
valueKind: "number",
|
||||||
@@ -108,7 +116,33 @@ const ANIMATION_PROPERTY_REGISTRY: Record<
|
|||||||
getValue: ({ element }) =>
|
getValue: ({ element }) =>
|
||||||
element.type === "audio" ? element.volume : null,
|
element.type === "audio" ? element.volume : null,
|
||||||
setValue: ({ element, value }) =>
|
setValue: ({ element, value }) =>
|
||||||
element.type === "audio" ? { ...element, volume: value } : element,
|
element.type === "audio"
|
||||||
|
? { ...element, volume: value as number }
|
||||||
|
: element,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
valueKind: "color",
|
||||||
|
defaultInterpolation: "linear",
|
||||||
|
supportsElement: ({ element }) => element.type === "text",
|
||||||
|
getValue: ({ element }) => (element.type === "text" ? element.color : null),
|
||||||
|
setValue: ({ element, value }) =>
|
||||||
|
element.type === "text"
|
||||||
|
? { ...element, color: value as string }
|
||||||
|
: element,
|
||||||
|
},
|
||||||
|
"background.color": {
|
||||||
|
valueKind: "color",
|
||||||
|
defaultInterpolation: "linear",
|
||||||
|
supportsElement: ({ element }) => element.type === "text",
|
||||||
|
getValue: ({ element }) =>
|
||||||
|
element.type === "text" ? element.background.color : null,
|
||||||
|
setValue: ({ element, value }) =>
|
||||||
|
element.type === "text"
|
||||||
|
? {
|
||||||
|
...element,
|
||||||
|
background: { ...element.background, color: value as string },
|
||||||
|
}
|
||||||
|
: element,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -163,7 +197,7 @@ export function withElementBaseValueForProperty({
|
|||||||
value: AnimationValue;
|
value: AnimationValue;
|
||||||
}): TimelineElement {
|
}): TimelineElement {
|
||||||
const coercedValue = coerceAnimationValueForProperty({ propertyPath, value });
|
const coercedValue = coerceAnimationValueForProperty({ propertyPath, value });
|
||||||
if (coercedValue === null || typeof coercedValue !== "number") {
|
if (coercedValue === null) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
const definition = getAnimationPropertyDefinition({ propertyPath });
|
const definition = getAnimationPropertyDefinition({ propertyPath });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ElementAnimations } from "@/types/animation";
|
import type { AnimationPropertyPath, ElementAnimations } from "@/types/animation";
|
||||||
import type { Transform } from "@/types/timeline";
|
import type { Transform } from "@/types/timeline";
|
||||||
import { getNumberChannelValueAtTime } from "./interpolation";
|
import { getColorValueAtTime, getNumberChannelValueAtTime } from "./interpolation";
|
||||||
|
import { getColorChannelForPath } from "./color-channel";
|
||||||
import { getNumberChannelForPath } from "./number-channel";
|
import { getNumberChannelForPath } from "./number-channel";
|
||||||
|
|
||||||
export function getElementLocalTime({
|
export function getElementLocalTime({
|
||||||
@@ -91,6 +92,24 @@ export function resolveOpacityAtTime({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveColorAtTime({
|
||||||
|
baseColor,
|
||||||
|
animations,
|
||||||
|
propertyPath,
|
||||||
|
localTime,
|
||||||
|
}: {
|
||||||
|
baseColor: string;
|
||||||
|
animations: ElementAnimations | undefined;
|
||||||
|
propertyPath: AnimationPropertyPath;
|
||||||
|
localTime: number;
|
||||||
|
}): string {
|
||||||
|
return getColorValueAtTime({
|
||||||
|
channel: getColorChannelForPath({ animations, propertyPath }),
|
||||||
|
time: Math.max(0, localTime),
|
||||||
|
fallbackValue: baseColor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveVolumeAtTime({
|
export function resolveVolumeAtTime({
|
||||||
baseVolume,
|
baseVolume,
|
||||||
animations,
|
animations,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from "@/lib/text/layout";
|
} from "@/lib/text/layout";
|
||||||
import {
|
import {
|
||||||
getElementLocalTime,
|
getElementLocalTime,
|
||||||
|
resolveColorAtTime,
|
||||||
resolveOpacityAtTime,
|
resolveOpacityAtTime,
|
||||||
resolveTransformAtTime,
|
resolveTransformAtTime,
|
||||||
} from "@/lib/animation";
|
} from "@/lib/animation";
|
||||||
@@ -150,11 +151,24 @@ export class TextNode extends BaseNode<TextNodeParams> {
|
|||||||
const lineCount = lines.length;
|
const lineCount = lines.length;
|
||||||
const block = measureTextBlock({ lineMetrics, lineHeightPx, fallbackFontSize: scaledFontSize });
|
const block = measureTextBlock({ lineMetrics, lineHeightPx, fallbackFontSize: scaledFontSize });
|
||||||
|
|
||||||
|
const textColor = resolveColorAtTime({
|
||||||
|
baseColor: this.params.color,
|
||||||
|
animations: this.params.animations,
|
||||||
|
propertyPath: "color",
|
||||||
|
localTime,
|
||||||
|
});
|
||||||
|
const backgroundColor = resolveColorAtTime({
|
||||||
|
baseColor: this.params.background.color,
|
||||||
|
animations: this.params.animations,
|
||||||
|
propertyPath: "background.color",
|
||||||
|
localTime,
|
||||||
|
});
|
||||||
|
|
||||||
const drawContent = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) => {
|
const drawContent = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D) => {
|
||||||
ctx.font = fontString;
|
ctx.font = fontString;
|
||||||
ctx.textAlign = this.params.textAlign;
|
ctx.textAlign = this.params.textAlign;
|
||||||
ctx.textBaseline = baseline;
|
ctx.textBaseline = baseline;
|
||||||
ctx.fillStyle = this.params.color;
|
ctx.fillStyle = textColor;
|
||||||
if ("letterSpacing" in ctx) {
|
if ("letterSpacing" in ctx) {
|
||||||
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = `${letterSpacing}px`;
|
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = `${letterSpacing}px`;
|
||||||
}
|
}
|
||||||
@@ -165,7 +179,7 @@ export class TextNode extends BaseNode<TextNodeParams> {
|
|||||||
this.params.background.color !== "transparent" &&
|
this.params.background.color !== "transparent" &&
|
||||||
lineCount > 0
|
lineCount > 0
|
||||||
) {
|
) {
|
||||||
const { color, cornerRadius = 0 } = this.params.background;
|
const { cornerRadius = 0 } = this.params.background;
|
||||||
const backgroundRect = getTextBackgroundRect({
|
const backgroundRect = getTextBackgroundRect({
|
||||||
textAlign: this.params.textAlign,
|
textAlign: this.params.textAlign,
|
||||||
block,
|
block,
|
||||||
@@ -175,11 +189,11 @@ export class TextNode extends BaseNode<TextNodeParams> {
|
|||||||
if (backgroundRect) {
|
if (backgroundRect) {
|
||||||
const p = clamp({ value: cornerRadius, min: CORNER_RADIUS_MIN, max: CORNER_RADIUS_MAX }) / 100;
|
const p = clamp({ value: cornerRadius, min: CORNER_RADIUS_MIN, max: CORNER_RADIUS_MAX }) / 100;
|
||||||
const radius = Math.min(backgroundRect.width, backgroundRect.height) / 2 * p;
|
const radius = Math.min(backgroundRect.width, backgroundRect.height) / 2 * p;
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = backgroundColor;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.roundRect(backgroundRect.left, backgroundRect.top, backgroundRect.width, backgroundRect.height, radius);
|
ctx.roundRect(backgroundRect.left, backgroundRect.top, backgroundRect.width, backgroundRect.height, radius);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.fillStyle = this.params.color;
|
ctx.fillStyle = textColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export const ANIMATION_PROPERTY_PATHS = [
|
|||||||
"transform.rotate",
|
"transform.rotate",
|
||||||
"opacity",
|
"opacity",
|
||||||
"volume",
|
"volume",
|
||||||
|
"color",
|
||||||
|
"background.color",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type AnimationPropertyPath = (typeof ANIMATION_PROPERTY_PATHS)[number];
|
export type AnimationPropertyPath = (typeof ANIMATION_PROPERTY_PATHS)[number];
|
||||||
|
|||||||
Reference in New Issue
Block a user