439 lines
9.3 KiB
TypeScript
439 lines
9.3 KiB
TypeScript
import type {
|
|
ParamDefinition,
|
|
ParamValue,
|
|
ParamValues,
|
|
} from "@/params";
|
|
import { MIN_TRANSFORM_SCALE } from "@/animation/transform";
|
|
import type { BlendMode } from "@/rendering";
|
|
import type {
|
|
ElementType,
|
|
TimelineElement,
|
|
} from "@/timeline";
|
|
import { DEFAULTS } from "@/timeline/defaults";
|
|
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/timeline/audio-constants";
|
|
import {
|
|
CORNER_RADIUS_MAX,
|
|
CORNER_RADIUS_MIN,
|
|
} from "@/text/background";
|
|
|
|
export type ElementParamDefinition<TKey extends string = string> =
|
|
ParamDefinition<TKey> & {
|
|
read?: ({ element }: { element: TimelineElement }) => ParamValue | null;
|
|
write?: ({
|
|
element,
|
|
value,
|
|
}: {
|
|
element: TimelineElement;
|
|
value: ParamValue;
|
|
}) => TimelineElement;
|
|
};
|
|
|
|
export function buildDefaultParamValues(
|
|
params: readonly ParamDefinition[],
|
|
): ParamValues {
|
|
const values: ParamValues = {};
|
|
for (const param of params) {
|
|
values[param.key] = param.default;
|
|
}
|
|
return values;
|
|
}
|
|
|
|
export class DefinitionRegistry<TKey extends string, TDefinition> {
|
|
private definitions = new Map<TKey, TDefinition>();
|
|
private entityName: string;
|
|
|
|
constructor(entityName: string) {
|
|
this.entityName = entityName;
|
|
}
|
|
|
|
register({
|
|
key,
|
|
definition,
|
|
}: {
|
|
key: TKey;
|
|
definition: TDefinition;
|
|
}): void {
|
|
this.definitions.set(key, definition);
|
|
}
|
|
|
|
has(key: TKey): boolean {
|
|
return this.definitions.has(key);
|
|
}
|
|
|
|
get(key: TKey): TDefinition {
|
|
const def = this.definitions.get(key);
|
|
if (!def) {
|
|
throw new Error(`Unknown ${this.entityName}: ${key}`);
|
|
}
|
|
return def;
|
|
}
|
|
|
|
getAll(): TDefinition[] {
|
|
return Array.from(this.definitions.values());
|
|
}
|
|
}
|
|
|
|
const BLEND_MODE_OPTIONS: Array<{ value: BlendMode; label: string }> = [
|
|
{ value: "normal", label: "Normal" },
|
|
{ value: "darken", label: "Darken" },
|
|
{ value: "multiply", label: "Multiply" },
|
|
{ value: "color-burn", label: "Color Burn" },
|
|
{ value: "lighten", label: "Lighten" },
|
|
{ value: "screen", label: "Screen" },
|
|
{ value: "plus-lighter", label: "Plus Lighter" },
|
|
{ value: "color-dodge", label: "Color Dodge" },
|
|
{ value: "overlay", label: "Overlay" },
|
|
{ value: "soft-light", label: "Soft Light" },
|
|
{ value: "hard-light", label: "Hard Light" },
|
|
{ value: "difference", label: "Difference" },
|
|
{ value: "exclusion", label: "Exclusion" },
|
|
{ value: "hue", label: "Hue" },
|
|
{ value: "saturation", label: "Saturation" },
|
|
{ value: "color", label: "Color" },
|
|
{ value: "luminosity", label: "Luminosity" },
|
|
];
|
|
|
|
const visualElementParams: ElementParamDefinition[] = [
|
|
{
|
|
key: "transform.positionX",
|
|
label: "Position X",
|
|
type: "number",
|
|
default: DEFAULTS.element.transform.position.x,
|
|
min: -100_000,
|
|
step: 1,
|
|
},
|
|
{
|
|
key: "transform.positionY",
|
|
label: "Position Y",
|
|
type: "number",
|
|
default: DEFAULTS.element.transform.position.y,
|
|
min: -100_000,
|
|
step: 1,
|
|
},
|
|
{
|
|
key: "transform.scaleX",
|
|
label: "Scale X",
|
|
type: "number",
|
|
default: DEFAULTS.element.transform.scaleX,
|
|
min: MIN_TRANSFORM_SCALE,
|
|
step: 0.01,
|
|
},
|
|
{
|
|
key: "transform.scaleY",
|
|
label: "Scale Y",
|
|
type: "number",
|
|
default: DEFAULTS.element.transform.scaleY,
|
|
min: MIN_TRANSFORM_SCALE,
|
|
step: 0.01,
|
|
},
|
|
{
|
|
key: "transform.rotate",
|
|
label: "Rotate",
|
|
type: "number",
|
|
default: DEFAULTS.element.transform.rotate,
|
|
min: -360,
|
|
max: 360,
|
|
step: 1,
|
|
},
|
|
{
|
|
key: "opacity",
|
|
label: "Opacity",
|
|
type: "number",
|
|
default: DEFAULTS.element.opacity,
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.01,
|
|
},
|
|
{
|
|
key: "blendMode",
|
|
label: "Blend Mode",
|
|
type: "select",
|
|
default: DEFAULTS.element.blendMode,
|
|
keyframable: false,
|
|
options: BLEND_MODE_OPTIONS,
|
|
},
|
|
];
|
|
|
|
const audioElementParams: ElementParamDefinition[] = [
|
|
{
|
|
key: "volume",
|
|
label: "Volume",
|
|
type: "number",
|
|
default: DEFAULTS.element.volume,
|
|
min: VOLUME_DB_MIN,
|
|
max: VOLUME_DB_MAX,
|
|
step: 0.01,
|
|
},
|
|
{
|
|
key: "muted",
|
|
label: "Muted",
|
|
type: "boolean",
|
|
default: false,
|
|
keyframable: false,
|
|
},
|
|
];
|
|
|
|
const textElementParams: ElementParamDefinition[] = [
|
|
{
|
|
key: "content",
|
|
label: "Content",
|
|
type: "text",
|
|
default: "Default text",
|
|
keyframable: false,
|
|
},
|
|
{
|
|
key: "fontFamily",
|
|
label: "Font Family",
|
|
type: "font",
|
|
default: "Arial",
|
|
keyframable: false,
|
|
},
|
|
{
|
|
key: "fontSize",
|
|
label: "Font Size",
|
|
type: "number",
|
|
default: 15,
|
|
min: 1,
|
|
step: 1,
|
|
},
|
|
{
|
|
key: "color",
|
|
label: "Color",
|
|
type: "color",
|
|
default: "#ffffff",
|
|
},
|
|
{
|
|
key: "textAlign",
|
|
label: "Text Align",
|
|
type: "select",
|
|
default: "center",
|
|
keyframable: false,
|
|
options: [
|
|
{ value: "left", label: "Left" },
|
|
{ value: "center", label: "Center" },
|
|
{ value: "right", label: "Right" },
|
|
],
|
|
},
|
|
{
|
|
key: "fontWeight",
|
|
label: "Font Weight",
|
|
type: "select",
|
|
default: "normal",
|
|
keyframable: false,
|
|
options: [
|
|
{ value: "normal", label: "Normal" },
|
|
{ value: "bold", label: "Bold" },
|
|
],
|
|
},
|
|
{
|
|
key: "fontStyle",
|
|
label: "Font Style",
|
|
type: "select",
|
|
default: "normal",
|
|
keyframable: false,
|
|
options: [
|
|
{ value: "normal", label: "Normal" },
|
|
{ value: "italic", label: "Italic" },
|
|
],
|
|
},
|
|
{
|
|
key: "textDecoration",
|
|
label: "Text Decoration",
|
|
type: "select",
|
|
default: "none",
|
|
keyframable: false,
|
|
options: [
|
|
{ value: "none", label: "None" },
|
|
{ value: "underline", label: "Underline" },
|
|
{ value: "line-through", label: "Line Through" },
|
|
],
|
|
},
|
|
{
|
|
key: "letterSpacing",
|
|
label: "Letter Spacing",
|
|
type: "number",
|
|
default: DEFAULTS.text.letterSpacing,
|
|
min: -100,
|
|
step: 0.1,
|
|
},
|
|
{
|
|
key: "lineHeight",
|
|
label: "Line Height",
|
|
type: "number",
|
|
default: DEFAULTS.text.lineHeight,
|
|
min: 0.1,
|
|
step: 0.1,
|
|
},
|
|
{
|
|
key: "background.enabled",
|
|
label: "Background Enabled",
|
|
type: "boolean",
|
|
default: DEFAULTS.text.background.enabled,
|
|
keyframable: false,
|
|
},
|
|
{
|
|
key: "background.color",
|
|
label: "Background Color",
|
|
type: "color",
|
|
default: DEFAULTS.text.background.color,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
{
|
|
key: "background.cornerRadius",
|
|
label: "Background Radius",
|
|
type: "number",
|
|
default: DEFAULTS.text.background.cornerRadius,
|
|
min: CORNER_RADIUS_MIN,
|
|
max: CORNER_RADIUS_MAX,
|
|
step: 1,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
{
|
|
key: "background.paddingX",
|
|
label: "Background Padding X",
|
|
type: "number",
|
|
default: DEFAULTS.text.background.paddingX,
|
|
min: 0,
|
|
step: 1,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
{
|
|
key: "background.paddingY",
|
|
label: "Background Padding Y",
|
|
type: "number",
|
|
default: DEFAULTS.text.background.paddingY,
|
|
min: 0,
|
|
step: 1,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
{
|
|
key: "background.offsetX",
|
|
label: "Background Offset X",
|
|
type: "number",
|
|
default: DEFAULTS.text.background.offsetX,
|
|
min: -100_000,
|
|
step: 1,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
{
|
|
key: "background.offsetY",
|
|
label: "Background Offset Y",
|
|
type: "number",
|
|
default: DEFAULTS.text.background.offsetY,
|
|
min: -100_000,
|
|
step: 1,
|
|
dependencies: [{ param: "background.enabled", equals: true }],
|
|
},
|
|
];
|
|
|
|
export const elementParamRegistry = new DefinitionRegistry<
|
|
ElementType,
|
|
readonly ElementParamDefinition[]
|
|
>("element params");
|
|
|
|
elementParamRegistry.register({
|
|
key: "video",
|
|
definition: [...visualElementParams, ...audioElementParams],
|
|
});
|
|
elementParamRegistry.register({ key: "image", definition: visualElementParams });
|
|
elementParamRegistry.register({
|
|
key: "text",
|
|
definition: [...textElementParams, ...visualElementParams],
|
|
});
|
|
elementParamRegistry.register({
|
|
key: "sticker",
|
|
definition: visualElementParams,
|
|
});
|
|
elementParamRegistry.register({
|
|
key: "graphic",
|
|
definition: visualElementParams,
|
|
});
|
|
elementParamRegistry.register({ key: "audio", definition: audioElementParams });
|
|
elementParamRegistry.register({ key: "effect", definition: [] });
|
|
|
|
export function getElementParams({
|
|
element,
|
|
}: {
|
|
element: TimelineElement;
|
|
}): readonly ElementParamDefinition[] {
|
|
return elementParamRegistry.has(element.type)
|
|
? elementParamRegistry.get(element.type)
|
|
: [];
|
|
}
|
|
|
|
export function getBuiltInElementParams({
|
|
type,
|
|
}: {
|
|
type: ElementType;
|
|
}): readonly ElementParamDefinition[] {
|
|
return elementParamRegistry.has(type) ? elementParamRegistry.get(type) : [];
|
|
}
|
|
|
|
export function getElementParam({
|
|
element,
|
|
key,
|
|
}: {
|
|
element: TimelineElement;
|
|
key: string;
|
|
}): ElementParamDefinition | null {
|
|
return (
|
|
getElementParams({ element }).find((param) => param.key === key) ?? null
|
|
);
|
|
}
|
|
|
|
export function readElementParamValue({
|
|
element,
|
|
param,
|
|
}: {
|
|
element: TimelineElement;
|
|
param: ElementParamDefinition;
|
|
}): ParamValue | null {
|
|
if (param.read) {
|
|
return param.read({ element });
|
|
}
|
|
if ("params" in element) {
|
|
return element.params[param.key] ?? param.default;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function writeElementParamValue({
|
|
element,
|
|
param,
|
|
value,
|
|
}: {
|
|
element: TimelineElement;
|
|
param: ElementParamDefinition;
|
|
value: ParamValue;
|
|
}): TimelineElement {
|
|
if (param.write) {
|
|
return param.write({ element, value });
|
|
}
|
|
if ("params" in element) {
|
|
return {
|
|
...element,
|
|
params: {
|
|
...element.params,
|
|
[param.key]: value,
|
|
},
|
|
};
|
|
}
|
|
return element;
|
|
}
|
|
|
|
export function buildElementParamValues({
|
|
element,
|
|
}: {
|
|
element: TimelineElement;
|
|
}): ParamValues {
|
|
const values: ParamValues = {};
|
|
for (const param of getElementParams({ element })) {
|
|
const value = readElementParamValue({ element, param });
|
|
if (value !== null) {
|
|
values[param.key] = value;
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|