Compare commits

...
Author SHA1 Message Date
Maze WintherandCursor 02d957582b refactor: move rendering primitives into model
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 04:20:00 +02:00
Maze WintherandCursor ab982b888e refactor: extract decoration model types
Move animation, effect, mask, and param value shapes into model decorations so element data no longer depends on decoration domains.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 03:58:21 +02:00
Maze WintherandCursor 6ac2116daa refactor: extract timeline model types
Move project entity shapes into the model layer so sibling domains no longer depend on timeline for core data types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 03:49:52 +02:00
Maze Winther 79cc7d2159 chore: bun.lock 2026-05-03 01:10:27 +02:00
Maze Winther 8ab3aeba7c docs: move NOTES.md file 2026-05-02 16:38:06 +02:00
241 changed files with 1529 additions and 1539 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import { snapToStep } from "@/utils/math";
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import type { DynamicAnimationPathValue, NumericSpec } from "./types";
export function getAnimationParamValueKind({
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ScalarAnimationKey } from "@/animation/types";
import type { ScalarAnimationKey } from "@/model/decorations/animations";
const BEZIER_SOLVE_ITERATIONS = 20;
+1 -14
View File
@@ -1,18 +1,5 @@
import { converter, formatHex, formatHex8, parse } from "culori";
import type {
AnimationBindingComponent,
AnimationBindingOfKind,
AnimationBindingInstance,
AnimationBindingKind,
ColorAnimationBinding,
DiscreteAnimationBinding,
NumberAnimationBinding,
AnimationPath,
AnimationValue,
DiscreteValue,
Vector2AnimationBinding,
VectorValue,
} from "@/animation/types";
import type { AnimationBindingComponent, AnimationBindingInstance, AnimationBindingKind, AnimationBindingOfKind, AnimationPath, AnimationValue, ColorAnimationBinding, DiscreteAnimationBinding, DiscreteValue, NumberAnimationBinding, Vector2AnimationBinding, VectorValue } from "@/model/decorations/animations";
import { clamp } from "@/utils/math";
interface LinearRgba {
+1 -5
View File
@@ -2,11 +2,7 @@ import {
getDefaultLeftHandle,
getDefaultRightHandle,
} from "@/animation/bezier";
import type {
CurveHandle,
NormalizedCubicBezier,
ScalarAnimationKey,
} from "@/animation/types";
import type { CurveHandle, NormalizedCubicBezier, ScalarAnimationKey } from "@/model/decorations/animations";
const VALUE_EPSILON = 1e-6;
@@ -1,8 +1,5 @@
import type {
ElementAnimations,
EffectParamPath,
} from "@/animation/types";
import type { ParamValues } from "@/params";
import type { EffectParamPath, ElementAnimations } from "@/model/decorations/animations";
import type { ParamValues } from "@/model/decorations/param-values";
import { removeElementKeyframe } from "./keyframes";
import { resolveAnimationPathValueAtTime } from "./resolve";
+2 -8
View File
@@ -1,11 +1,5 @@
import type {
AnimationBindingInstance,
AnimationPath,
ElementAnimations,
ScalarAnimationChannel,
ScalarGraphChannel,
ScalarGraphKeyframeContext,
} from "@/animation/types";
import type { AnimationBindingInstance, AnimationPath, ElementAnimations, ScalarAnimationChannel } from "@/model/decorations/animations";
import type { ScalarGraphChannel, ScalarGraphKeyframeContext } from "@/animation/types";
export interface EditableScalarChannels {
binding: AnimationBindingInstance;
@@ -1,8 +1,5 @@
import type {
ElementAnimations,
GraphicParamPath,
} from "@/animation/types";
import type { ParamDefinition, ParamValues } from "@/params";
import type { ElementAnimations, GraphicParamPath } from "@/model/decorations/animations";
import type { ParamDefinition, ParamValues } from "@/model/decorations/param-values";
import { resolveAnimationPathValueAtTime } from "./resolve";
export const GRAPHIC_PARAM_PATH_PREFIX = "params.";
+1 -10
View File
@@ -1,13 +1,4 @@
import type {
AnimationChannel,
AnimationInterpolation,
AnimationValue,
DiscreteAnimationChannel,
DiscreteValue,
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
} from "@/animation/types";
import type { AnimationChannel, AnimationInterpolation, AnimationValue, DiscreteAnimationChannel, DiscreteValue, ScalarAnimationChannel, ScalarAnimationKey, ScalarSegmentType } from "@/model/decorations/animations";
import {
getBezierPoint,
getDefaultLeftHandle,
+2 -7
View File
@@ -1,10 +1,5 @@
import type {
AnimationBindingInstance,
AnimationChannel,
AnimationPath,
ElementAnimations,
ElementKeyframe,
} from "@/animation/types";
import type { AnimationBindingInstance, AnimationChannel, AnimationPath, ElementAnimations } from "@/model/decorations/animations";
import type { ElementKeyframe } from "@/animation/types";
import {
type AnimationComponentValue,
composeAnimationValue,
+2 -15
View File
@@ -1,18 +1,5 @@
import type {
AnimationBindingInstance,
AnimationBindingKind,
AnimationChannel,
AnimationInterpolation,
AnimationPath,
AnimationValue,
DiscreteAnimationChannel,
DiscreteAnimationKey,
ElementAnimations,
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarCurveKeyframePatch,
ScalarSegmentType,
} from "@/animation/types";
import type { AnimationBindingInstance, AnimationBindingKind, AnimationChannel, AnimationInterpolation, AnimationPath, AnimationValue, DiscreteAnimationChannel, DiscreteAnimationKey, ElementAnimations, ScalarAnimationChannel, ScalarAnimationKey, ScalarSegmentType } from "@/model/decorations/animations";
import type { ScalarCurveKeyframePatch } from "@/animation/types";
import {
cloneAnimationBinding,
createAnimationBinding,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { AnimationPath, AnimationPropertyPath } from "@/animation/types";
import type { AnimationPath, AnimationPropertyPath } from "@/model/decorations/animations";
import { ANIMATION_PROPERTY_PATHS } from "./types";
import { isEffectParamPath } from "./effect-param-channel";
import { isGraphicParamPath } from "./graphic-param-channel";
+2 -6
View File
@@ -1,9 +1,5 @@
import type {
AnimationPropertyGroup,
AnimationPropertyPath,
ElementAnimations,
} from "@/animation/types";
import { ANIMATION_PROPERTY_GROUPS } from "@/animation/types";
import type { AnimationPropertyGroup, AnimationPropertyPath, ElementAnimations } from "@/model/decorations/animations";
import { ANIMATION_PROPERTY_GROUPS } from "@/model/decorations/animations";
import { getKeyframeAtTime } from "./keyframe-query";
export interface GroupKeyframeRef {
+1 -5
View File
@@ -1,8 +1,4 @@
import type {
AnimationPath,
AnimationValueForPath,
ElementAnimations,
} from "@/animation/types";
import type { AnimationPath, AnimationValueForPath, ElementAnimations } from "@/model/decorations/animations";
import {
type AnimationComponentValue,
composeAnimationValue,
+57 -181
View File
@@ -1,186 +1,62 @@
export const ANIMATION_PROPERTY_PATHS = [
"transform.positionX",
"transform.positionY",
"transform.scaleX",
"transform.scaleY",
"transform.rotate",
"opacity",
"volume",
"color",
"background.color",
"background.paddingX",
"background.paddingY",
"background.offsetX",
"background.offsetY",
"background.cornerRadius",
] as const;
import type {
AnimationInterpolation,
AnimationPath,
AnimationValue,
CurveHandle,
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
TangentMode,
} from "@/model/decorations/animations";
export type AnimationPropertyPath = (typeof ANIMATION_PROPERTY_PATHS)[number];
export type GraphicParamPath = `params.${string}`;
export type EffectParamPath = `effects.${string}.params.${string}`;
export type AnimationPath =
| AnimationPropertyPath
| GraphicParamPath
| EffectParamPath;
export type {
AnimationBindingByKind,
AnimationBindingComponent,
AnimationBindingInstance,
AnimationBindingKind,
AnimationBindingOfKind,
AnimationChannel,
AnimationColorPropertyPath,
AnimationInterpolation,
AnimationKeyframe,
AnimationNumericPropertyPath,
AnimationPath,
AnimationPropertyGroup,
AnimationPropertyPath,
AnimationPropertyValueMap,
AnimationValue,
AnimationValueForPath,
ChannelExtrapolationMode,
ColorAnimationBinding,
ContinuousKeyframeInterpolation,
CurveHandle,
DiscreteAnimationBinding,
DiscreteAnimationChannel,
DiscreteAnimationKey,
DiscreteKeyframeInterpolation,
DiscreteValue,
DynamicAnimationPathValue,
EffectParamPath,
ElementAnimationBindingMap,
ElementAnimationChannelMap,
ElementAnimations,
GraphicParamPath,
NormalizedCubicBezier,
NumberAnimationBinding,
NumericSpec,
PrimitiveAnimationChannelKind,
ScalarAnimationChannel,
ScalarAnimationKey,
ScalarSegmentType,
TangentMode,
Vector2AnimationBinding,
VectorValue,
} from "@/model/decorations/animations";
export const ANIMATION_PROPERTY_GROUPS = {
"transform.scale": ["transform.scaleX", "transform.scaleY"],
} as const satisfies Record<string, ReadonlyArray<AnimationPropertyPath>>;
export type AnimationPropertyGroup = keyof typeof ANIMATION_PROPERTY_GROUPS;
export type VectorValue = { x: number; y: number };
export type DiscreteValue = boolean | string;
export type AnimationValue = number | string | boolean | VectorValue;
export interface AnimationPropertyValueMap {
"transform.positionX": number;
"transform.positionY": number;
"transform.scaleX": number;
"transform.scaleY": number;
"transform.rotate": number;
opacity: number;
volume: number;
color: string;
"background.color": string;
"background.paddingX": number;
"background.paddingY": number;
"background.offsetX": number;
"background.offsetY": number;
"background.cornerRadius": number;
}
export type DynamicAnimationPathValue = number | string | boolean;
export interface NumericSpec {
min?: number;
max?: number;
step?: number;
}
export type AnimationValueForPath<TPath extends AnimationPath> =
TPath extends AnimationPropertyPath
? AnimationPropertyValueMap[TPath]
: TPath extends GraphicParamPath | EffectParamPath
? DynamicAnimationPathValue
: never;
export type AnimationNumericPropertyPath = {
[K in AnimationPropertyPath]: AnimationValueForPath<K> extends number ? K : never;
}[AnimationPropertyPath];
export type AnimationColorPropertyPath = {
[K in AnimationPropertyPath]: AnimationValueForPath<K> extends string ? K : never;
}[AnimationPropertyPath];
export type ContinuousKeyframeInterpolation = "linear" | "hold" | "bezier";
export type DiscreteKeyframeInterpolation = "hold";
export type AnimationInterpolation =
| ContinuousKeyframeInterpolation
| DiscreteKeyframeInterpolation;
export type PrimitiveAnimationChannelKind = "scalar" | "discrete";
export type AnimationBindingKind = "number" | "vector2" | "color" | "discrete";
export type ScalarSegmentType = "step" | "linear" | "bezier";
export type TangentMode = "auto" | "aligned" | "broken" | "flat";
export type ChannelExtrapolationMode = "hold" | "linear";
export interface CurveHandle {
dt: number;
dv: number;
}
interface BaseAnimationKeyframe<TValue extends number | DiscreteValue> {
id: string;
time: number; // relative to element start time
value: TValue;
}
export interface ScalarAnimationKey extends BaseAnimationKeyframe<number> {
leftHandle?: CurveHandle;
rightHandle?: CurveHandle;
segmentToNext: ScalarSegmentType;
tangentMode: TangentMode;
}
export interface DiscreteAnimationKey
extends BaseAnimationKeyframe<DiscreteValue> {}
export type AnimationKeyframe = ScalarAnimationKey | DiscreteAnimationKey;
export interface ScalarAnimationChannel {
kind: "scalar";
keys: ScalarAnimationKey[];
extrapolation?: {
before: ChannelExtrapolationMode;
after: ChannelExtrapolationMode;
};
}
export interface DiscreteAnimationChannel {
kind: "discrete";
keys: DiscreteAnimationKey[];
}
export type AnimationChannel =
| ScalarAnimationChannel
| DiscreteAnimationChannel;
export type ElementAnimationChannelMap = Record<
string,
AnimationChannel | undefined
>;
export interface AnimationBindingComponent<TKey extends string = string> {
key: TKey;
channelId: string;
}
interface BaseAnimationBinding<
TKind extends AnimationBindingKind,
TComponentKey extends string,
> {
path: AnimationPath;
kind: TKind;
components: AnimationBindingComponent<TComponentKey>[];
}
export interface NumberAnimationBinding
extends BaseAnimationBinding<"number", "value"> {}
export interface Vector2AnimationBinding
extends BaseAnimationBinding<"vector2", "x" | "y"> {}
export interface ColorAnimationBinding
extends BaseAnimationBinding<"color", "r" | "g" | "b" | "a"> {
colorSpace: "srgb-linear";
}
export interface DiscreteAnimationBinding
extends BaseAnimationBinding<"discrete", "value"> {}
export type AnimationBindingInstance =
| NumberAnimationBinding
| Vector2AnimationBinding
| ColorAnimationBinding
| DiscreteAnimationBinding;
export interface AnimationBindingByKind {
number: NumberAnimationBinding;
vector2: Vector2AnimationBinding;
color: ColorAnimationBinding;
discrete: DiscreteAnimationBinding;
}
export type AnimationBindingOfKind<TKind extends AnimationBindingKind> =
AnimationBindingByKind[TKind];
export type ElementAnimationBindingMap = Record<
string,
AnimationBindingInstance | undefined
>;
export interface ElementAnimations {
bindings: ElementAnimationBindingMap;
channels: ElementAnimationChannelMap;
}
export type NormalizedCubicBezier = [number, number, number, number];
export {
ANIMATION_PROPERTY_GROUPS,
ANIMATION_PROPERTY_PATHS,
} from "@/model/decorations/animations";
export interface ScalarGraphChannelTarget {
propertyPath: AnimationPath;
+3 -3
View File
@@ -1,6 +1,6 @@
import { getKeyframeById } from "@/animation";
import type { SelectedKeyframeRef } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import { elementId, type ElementRef, type TimelineElement } from "@/model";
import { PasteKeyframesCommand } from "@/commands/timeline";
import type {
ClipboardHandler,
@@ -12,7 +12,7 @@ function resolveSingleSourceElement({
selectedKeyframes,
}: {
selectedKeyframes: SelectedKeyframeRef[];
}) {
}): ElementRef | null {
const firstKeyframe = selectedKeyframes[0];
if (!firstKeyframe) {
return null;
@@ -20,7 +20,7 @@ function resolveSingleSourceElement({
const sourceElement = {
trackId: firstKeyframe.trackId,
elementId: firstKeyframe.elementId,
elementId: elementId(firstKeyframe.elementId),
};
const isSingleSource = selectedKeyframes.every(
(keyframe) =>
+3 -12
View File
@@ -1,17 +1,8 @@
import type { EditorCore } from "@/core";
import type {
AnimationInterpolation,
AnimationPath,
AnimationValue,
ScalarCurveKeyframePatch,
SelectedKeyframeRef,
} from "@/animation/types";
import type { AnimationInterpolation, AnimationPath, AnimationValue } from "@/model/decorations/animations";
import type { ScalarCurveKeyframePatch, SelectedKeyframeRef } from "@/animation/types";
import type { Command } from "@/commands/base-command";
import type {
CreateTimelineElement,
ElementRef,
TrackType,
} from "@/timeline";
import type { CreateTimelineElement, ElementRef, TrackType } from "@/model";
export interface ElementClipboardItem {
trackId: string;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { EditorSelectionPatch } from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
export interface CommandResult {
selection?: EditorSelectionPatch;
@@ -6,7 +6,7 @@ import { storageService } from "@/services/storage/service";
import { videoCache } from "@/services/video-cache/service";
import { waveformCache } from "@/services/waveform-cache/service";
import { hasMediaId } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveMediaAssetCommand extends Command {
private savedAssets: MediaAsset[] | null = null;
+1 -1
View File
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { buildDefaultScene } from "@/timeline/scenes";
export class CreateSceneCommand extends Command {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { canDeleteScene, getFallbackSceneAfterDelete } from "@/timeline/scenes";
export class DeleteSceneCommand extends Command {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import { getFrameTime, moveBookmarkInArray } from "@/timeline/bookmarks/index";
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
export class RenameSceneCommand extends Command {
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { TScene } from "@/timeline";
import type { TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { Bookmark, TScene } from "@/timeline";
import type { Bookmark, TScene } from "@/model";
import { updateSceneInArray } from "@/timeline/scenes";
import {
getFrameTime,
@@ -6,7 +6,7 @@ import {
} from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import type { KeyframeClipboardItem } from "@/clipboard";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { SceneTracks, TimelineElement } from "@/model";
import { updateElementInSceneTracks } from "@/timeline";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import { generateUUID } from "@/utils/id";
@@ -4,7 +4,7 @@ import {
type CommandResult,
} from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { ElementRef, SceneTracks, TimelineElement } from "@/model";
import type { ElementClipboardItem } from "@/clipboard";
import { generateUUID } from "@/utils/id";
import {
@@ -16,7 +16,7 @@ import { cloneAnimations } from "@/animation";
export class PasteCommand extends Command {
private savedState: SceneTracks | null = null;
private pastedElements: { trackId: string; elementId: string }[] = [];
private pastedElements: ElementRef[] = [];
private readonly time: number;
private readonly clipboardItems: ElementClipboardItem[];
@@ -142,7 +142,7 @@ export class PasteCommand extends Command {
}
}
getPastedElements(): { trackId: string; elementId: string }[] {
getPastedElements(): ElementRef[] {
return this.pastedElements;
}
}
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import type { TimelineTrack } from "@/timeline";
import type { TimelineTrack } from "@/model";
function removeTrackElements<TTrack extends TimelineTrack>({
track,
@@ -3,7 +3,13 @@ import {
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import {
elementId,
type ElementId,
type ElementRef,
type SceneTracks,
type TimelineElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import { applyPlacement, resolveTrackPlacement } from "@/timeline/placement";
@@ -14,7 +20,7 @@ interface DuplicateElementsParams {
}
export class DuplicateElementsCommand extends Command {
private duplicatedElements: { trackId: string; elementId: string }[] = [];
private duplicatedElements: ElementRef[] = [];
private savedState: SceneTracks | null = null;
private elements: DuplicateElementsParams["elements"];
@@ -53,7 +59,7 @@ export class DuplicateElementsCommand extends Command {
continue;
}
const newId = generateUUID();
const newId = elementId(generateUUID());
newTrackElements.push(
buildDuplicateElement({
element,
@@ -107,7 +113,7 @@ export class DuplicateElementsCommand extends Command {
}
}
getDuplicatedElements(): { trackId: string; elementId: string }[] {
getDuplicatedElements(): ElementRef[] {
return this.duplicatedElements;
}
}
@@ -118,7 +124,7 @@ function buildDuplicateElement({
startTime,
}: {
element: TimelineElement;
id: string;
id: ElementId;
startTime: number;
}): TimelineElement {
return {
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
import { buildDefaultEffectInstance } from "@/effects";
function addEffectToElement({
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
function removeEffectFromElement({
element,
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
function reorderEffectsOnElement({
element,
@@ -1,7 +1,7 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { SceneTracks, VisualElement } from "@/model";
export function toggleEffectOnElement({
element,
@@ -1,8 +1,8 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import { isVisualElement, updateElementInSceneTracks } from "@/timeline";
import type { ParamValues } from "@/params";
import type { SceneTracks, VisualElement } from "@/timeline";
import type { ParamValues } from "@/model/decorations/param-values";
import type { SceneTracks, VisualElement } from "@/model";
function updateEffectParamsOnElement({
element,
@@ -1,11 +1,13 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type {
CreateTimelineElement,
SceneTracks,
TimelineElement,
TrackType,
} from "@/timeline";
import {
elementId,
type CreateTimelineElement,
type ElementId,
type SceneTracks,
type TimelineElement,
type TrackType,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { requiresMediaId } from "@/timeline/element-utils";
import type { MediaAsset } from "@/media/types";
@@ -29,13 +31,13 @@ export interface InsertElementParams {
}
export class InsertElementCommand extends Command {
private elementId: string;
private elementId: ElementId;
private savedState: SceneTracks | null = null;
private targetTrackId: string | null = null;
constructor({ element, placement }: InsertElementParams) {
super();
this.elementId = generateUUID();
this.elementId = elementId(generateUUID());
this.element = element;
this.placement = placement;
}
@@ -3,7 +3,7 @@ import { Command, type CommandResult } from "@/commands/base-command";
import { removeEffectParamKeyframe } from "@/animation/effect-param-channel";
import { updateElementInSceneTracks } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
@@ -5,8 +5,8 @@ import {
} from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import { updateElementInSceneTracks } from "@/timeline";
import type { AnimationPath, AnimationValue } from "@/animation/types";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { AnimationPath, AnimationValue } from "@/model/decorations/animations";
import type { SceneTracks, TimelineElement } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
function removeKeyframeAndPersist({
@@ -2,8 +2,8 @@ import { EditorCore } from "@/core";
import { retimeElementKeyframe } from "@/animation";
import { Command, type CommandResult } from "@/commands/base-command";
import { updateElementInSceneTracks } from "@/timeline";
import type { AnimationPath } from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { AnimationPath } from "@/model/decorations/animations";
import type { SceneTracks } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
export class RetimeKeyframeCommand extends Command {
@@ -5,11 +5,9 @@ import {
import { Command, type CommandResult } from "@/commands/base-command";
import { updateElementInSceneTracks } from "@/timeline";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import type {
AnimationPath,
ScalarCurveKeyframePatch,
} from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { AnimationPath } from "@/model/decorations/animations";
import type { ScalarCurveKeyframePatch } from "@/animation/types";
import type { SceneTracks } from "@/model";
export class UpdateScalarKeyframeCurveCommand extends Command {
private savedState: SceneTracks | null = null;
@@ -7,8 +7,8 @@ import {
import { updateElementInSceneTracks } from "@/timeline";
import { isVisualElement } from "@/timeline/element-utils";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import type { AnimationInterpolation } from "@/animation/types";
import type { SceneTracks } from "@/timeline";
import type { AnimationInterpolation } from "@/model/decorations/animations";
import type { SceneTracks } from "@/model";
export class UpsertEffectParamKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
@@ -2,13 +2,9 @@ import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { upsertPathKeyframe } from "@/animation";
import { updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { resolveAnimationTarget } from "@/timeline/animation-targets";
import type {
AnimationPath,
AnimationInterpolation,
AnimationValue,
} from "@/animation/types";
import type { AnimationInterpolation, AnimationPath, AnimationValue } from "@/model/decorations/animations";
export class UpsertKeyframeCommand extends Command {
private savedState: SceneTracks | null = null;
@@ -4,9 +4,9 @@ import {
getCustomMaskClosedStateAfterPointRemoval,
removeCustomMaskPoints,
} from "@/masks/custom-path";
import type { CustomMask } from "@/masks/types";
import type { CustomMask } from "@/model/decorations/mask";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function deletePointsFromCustomMask({
mask,
@@ -2,9 +2,9 @@ import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { insertPointOnCustomMaskSegment } from "@/masks/definitions/custom";
import type { ElementBounds } from "@/preview/element-bounds";
import type { CustomMask } from "@/masks/types";
import type { CustomMask } from "@/model/decorations/mask";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function insertPointIntoCustomMask({
mask,
@@ -1,7 +1,7 @@
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { SceneTracks, MaskableElement } from "@/timeline";
import type { MaskableElement, SceneTracks } from "@/model";
function removeMaskFromElement({
element,
@@ -1,8 +1,8 @@
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import { isMaskableElement, updateElementInSceneTracks } from "@/timeline";
import type { Mask } from "@/masks/types";
import type { SceneTracks, MaskableElement } from "@/timeline";
import type { Mask } from "@/model/decorations/mask";
import type { MaskableElement, SceneTracks } from "@/model";
export function toggleMaskInvertedOnElement({
element,
@@ -4,7 +4,12 @@ import {
type CommandResult,
} from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks, TimelineElement, TimelineTrack } from "@/timeline";
import {
elementId as toElementId,
type SceneTracks,
type TimelineElement,
type TimelineTrack,
} from "@/model";
import {
buildEmptyTrack,
validateElementTrackCompatibility,
@@ -117,7 +122,7 @@ export class MoveElementCommand extends Command {
return createElementSelectionResult(
this.moves.map(({ elementId, targetTrackId }) => ({
trackId: targetTrackId,
elementId,
elementId: toElementId(elementId),
})),
);
}
@@ -3,7 +3,12 @@ import {
createElementSelectionResult,
type CommandResult,
} from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import {
elementId,
type ElementRef,
type SceneTracks,
type TimelineElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import { isRetimableElement } from "@/timeline";
@@ -12,7 +17,7 @@ import { getSourceSpanAtClipTime } from "@/retime";
export class SplitElementsCommand extends Command {
private savedState: SceneTracks | null = null;
private rightSideElements: { trackId: string; elementId: string }[] = [];
private rightSideElements: ElementRef[] = [];
private readonly elements: { trackId: string; elementId: string }[];
private readonly splitTime: number;
private readonly retainSide: "both" | "left" | "right";
@@ -32,7 +37,7 @@ export class SplitElementsCommand extends Command {
this.retainSide = retainSide;
}
getRightSideElements(): { trackId: string; elementId: string }[] {
getRightSideElements(): ElementRef[] {
return this.rightSideElements;
}
@@ -107,7 +112,7 @@ export class SplitElementsCommand extends Command {
},
];
} else if (this.retainSide === "right") {
const newId = generateUUID();
const newId = elementId(generateUUID());
this.rightSideElements.push({
trackId: track.id,
elementId: newId,
@@ -126,7 +131,7 @@ export class SplitElementsCommand extends Command {
];
} else {
// "both" - split into two pieces
const secondElementId = generateUUID();
const secondElementId = elementId(generateUUID());
this.rightSideElements.push({
trackId: track.id,
elementId: secondElementId,
@@ -10,11 +10,12 @@ import {
resolveTrackPlacement,
} from "@/timeline/placement";
import { updateElementInSceneTracks } from "@/timeline/track-element-update";
import type {
SceneTracks,
TimelineElement,
VideoElement,
} from "@/timeline/types";
import {
elementId,
type SceneTracks,
type TimelineElement,
type VideoElement,
} from "@/model";
import { generateUUID } from "@/utils/id";
export class ToggleSourceAudioSeparationCommand extends Command {
@@ -75,7 +76,7 @@ export class ToggleSourceAudioSeparationCommand extends Command {
...buildSeparatedAudioElement({
sourceElement: videoElement,
}),
id: generateUUID(),
id: elementId(generateUUID()),
};
const placementResult = resolveTrackPlacement({
tracks: this.savedState,
@@ -1,6 +1,6 @@
import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks, TimelineElement } from "@/timeline";
import type { SceneTracks, TimelineElement } from "@/model";
import {
findTrackInSceneTracks,
updateElementInSceneTracks,
@@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks, TrackType } from "@/timeline";
import type { SceneTracks, TrackType } from "@/model";
import { generateUUID } from "@/utils/id";
import { EditorCore } from "@/core";
import {
@@ -1,6 +1,6 @@
import { Command, type CommandResult } from "@/commands/base-command";
import { EditorCore } from "@/core";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
export class RemoveTrackCommand extends Command {
private savedState: SceneTracks | null = null;
@@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import { canTrackHaveAudio, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";
@@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
import { canTrackBeHidden, findTrackInSceneTracks, updateTrackInSceneTracks } from "@/timeline";
@@ -1,5 +1,5 @@
import { Command, type CommandResult } from "@/commands/base-command";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import { EditorCore } from "@/core";
export class TracksSnapshotCommand extends Command {
@@ -45,7 +45,7 @@ import {
type MediaViewMode,
useAssetsPanelStore,
} from "@/components/editor/panels/assets/assets-panel-store";
import { MASKABLE_ELEMENT_TYPES } from "@/timeline";
import { MASKABLE_ELEMENT_TYPES } from "@/model";
import type { MediaAsset } from "@/media/types";
import { cn } from "@/utils/ui";
import {
@@ -1,6 +1,6 @@
"use client";
import type { ParamDefinition, NumberParamDefinition } from "@/params";
import type { NumberParamDefinition, ParamDefinition } from "@/model/decorations/param-values";
import {
formatNumberForDisplay,
getFractionDigitsForStep,
@@ -3,8 +3,8 @@ import {
getKeyframeAtTime,
hasKeyframesForPath,
} from "@/animation";
import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import type { AnimationPropertyPath, ElementAnimations } from "@/model/decorations/animations";
import type { TimelineElement } from "@/model";
import { upsertElementKeyframe } from "@/timeline/animation-properties";
export function useKeyframedColorProperty({
@@ -3,8 +3,8 @@ import {
getKeyframeAtTime,
hasKeyframesForPath,
} from "@/animation";
import type { AnimationPropertyPath, ElementAnimations } from "@/animation/types";
import type { TimelineElement } from "@/timeline";
import type { AnimationPropertyPath, ElementAnimations } from "@/model/decorations/animations";
import type { TimelineElement } from "@/model";
import { upsertElementKeyframe } from "@/timeline/animation-properties";
import { snapToStep } from "@/utils/math";
import { usePropertyDraft } from "./use-property-draft";
@@ -7,16 +7,14 @@ import {
hasKeyframesForPath,
upsertPathKeyframe,
} from "@/animation";
import type {
ElementAnimations,
} from "@/animation/types";
import type { ElementAnimations } from "@/model/decorations/animations";
import {
coerceAnimationParamValue,
getAnimationParamDefaultInterpolation,
getAnimationParamValueKind,
} from "@/animation/animated-params";
import type { ParamDefinition } from "@/params";
import type { TimelineElement } from "@/timeline";
import type { ParamDefinition } from "@/model/decorations/param-values";
import type { TimelineElement } from "@/model";
export interface KeyframedParamPropertyResult {
hasAnimatedKeyframes: boolean;
@@ -1,17 +1,5 @@
import type { ReactNode } from "react";
import type {
EffectElement,
GraphicElement,
ImageElement,
MaskableElement,
RetimableElement,
StickerElement,
TextElement,
VisualElement,
VideoElement,
AudioElement,
TimelineElement,
} from "@/timeline";
import type { AudioElement, EffectElement, GraphicElement, ImageElement, MaskableElement, RetimableElement, StickerElement, TextElement, TimelineElement, VideoElement, VisualElement } from "@/model";
import type { MediaAsset } from "@/media/types";
import { HugeiconsIcon } from "@hugeicons/react";
import {
+1 -1
View File
@@ -2,7 +2,7 @@ import type { EditorCore } from "@/core";
import type { Command, CommandResult } from "@/commands";
import type { EditorSelectionSnapshot } from "@/selection/editor-selection";
import { applyRippleAdjustments, computeRippleAdjustments } from "@/ripple";
import type { SceneTracks } from "@/timeline/types";
import type { SceneTracks } from "@/model";
interface CommandHistoryEntry {
command: Command;
+1 -1
View File
@@ -1,5 +1,5 @@
import type { EditorCore } from "@/core";
import type { SceneTracks, TScene } from "@/timeline";
import type { SceneTracks, TScene } from "@/model";
import { storageService } from "@/services/storage/service";
import {
getMainScene,
@@ -6,7 +6,7 @@ import type {
EditorSelectionSnapshot,
SelectedMaskPointSelection,
} from "@/selection/editor-selection";
import type { ElementRef } from "@/timeline/types";
import type { ElementRef } from "@/model";
export class SelectionManager {
private selectedElements: ElementRef[] = [];
+4 -15
View File
@@ -1,13 +1,7 @@
import type { EditorCore } from "@/core";
import type { ElementBounds } from "@/preview/element-bounds";
import type { ParamValues } from "@/params";
import type {
SceneTracks,
TrackType,
TimelineTrack,
TimelineElement,
RetimeConfig,
} from "@/timeline";
import type { ParamValues } from "@/model/decorations/param-values";
import type { RetimeConfig, SceneTracks, TimelineElement, TimelineTrack, TrackType } from "@/model";
import { calculateTotalDuration } from "@/timeline";
import { TimelineDragSource } from "@/timeline/drag-source";
import { findTrackInSceneTracks } from "@/timeline/track-element-update";
@@ -15,13 +9,8 @@ import {
canElementBeHidden,
canElementHaveAudio,
} from "@/timeline/element-utils";
import type {
AnimationPath,
AnimationInterpolation,
AnimationValue,
AnimationValueForPath,
ScalarCurveKeyframePatch,
} from "@/animation/types";
import type { AnimationInterpolation, AnimationPath, AnimationValue, AnimationValueForPath } from "@/model/decorations/animations";
import type { ScalarCurveKeyframePatch } from "@/animation/types";
import {
getElementLocalTime,
resolveAnimationPathValueAtTime,
@@ -1,9 +1,9 @@
"use client";
import { useState } from "react";
import type { ParamValues } from "@/params";
import type { Effect } from "@/effects/types";
import type { EffectElement, VisualElement } from "@/timeline";
import type { ParamValues } from "@/model/decorations/param-values";
import type { Effect } from "@/model/decorations/effect";
import type { EffectElement, VisualElement } from "@/model";
import { effectsRegistry } from "@/effects";
import { useEditor } from "@/editor/use-editor";
import { useElementPreview } from "@/timeline/hooks/use-element-preview";
+2 -1
View File
@@ -1,4 +1,5 @@
import type { EffectDefinition, EffectPass } from "@/effects/types";
import type { EffectPass } from "@/model/decorations/effect";
import type { EffectDefinition } from "@/effects/types";
export const GAUSSIAN_BLUR_SHADER = "gaussian-blur";
+4 -3
View File
@@ -1,9 +1,10 @@
import { generateUUID } from "@/utils/id";
import { buildDefaultParamValues } from "@/params/registry";
import { effectsRegistry } from "./registry";
import type { ParamValues } from "@/params";
import type { Effect, EffectDefinition, EffectPass } from "@/effects/types";
import { VISUAL_ELEMENT_TYPES } from "@/timeline";
import type { ParamValues } from "@/model/decorations/param-values";
import type { Effect, EffectPass } from "@/model/decorations/effect";
import type { EffectDefinition } from "@/effects/types";
import { VISUAL_ELEMENT_TYPES } from "@/model";
export { effectsRegistry } from "./registry";
export { registerDefaultEffects } from "./definitions";
+8 -23
View File
@@ -1,27 +1,12 @@
import type { ParamDefinition, ParamValues } from "@/params";
import type { ParamDefinition, ParamValues } from "@/model/decorations/param-values";
import type { EffectPass, EffectPassTemplate } from "@/model/decorations/effect";
export interface Effect {
id: string;
type: string;
params: ParamValues;
enabled: boolean;
}
export type EffectUniformValue = number | number[];
export interface EffectPass {
shader: string;
uniforms: Record<string, EffectUniformValue>;
}
export interface EffectPassTemplate {
shader: string;
uniforms(params: {
effectParams: ParamValues;
width: number;
height: number;
}): Record<string, EffectUniformValue>;
}
export type {
Effect,
EffectPass,
EffectPassTemplate,
EffectUniformValue,
} from "@/model/decorations/effect";
export interface EffectRendererConfig {
passes: EffectPassTemplate[];
@@ -6,8 +6,8 @@ import {
useKeyframedParamProperty,
type KeyframedParamPropertyResult,
} from "@/components/editor/panels/properties/hooks/use-keyframed-param-property";
import type { ParamDefinition, ParamValues } from "@/params";
import type { GraphicElement } from "@/timeline";
import type { ParamDefinition, ParamValues } from "@/model/decorations/param-values";
import type { GraphicElement } from "@/model";
import {
graphicsRegistry,
registerDefaultGraphics,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
@@ -1,4 +1,4 @@
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
export type GraphicStrokeAlign = "inside" | "center" | "outside";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import { applyAlignedStroke } from "../stroke";
import { STROKE_ALIGN_PARAM, type GraphicStrokeAlign } from "./shared";
import type { GraphicDefinition } from "../types";
+149 -149
View File
@@ -1,149 +1,149 @@
import { resolveGraphicParamsAtTime } from "@/animation";
import type { ElementAnimations } from "@/animation/types";
import { buildDefaultParamValues } from "@/params/registry";
import type { ParamValues } from "@/params";
import { graphicsRegistry } from "./registry";
import {
registerDefaultGraphics,
ellipseGraphicDefinition,
polygonGraphicDefinition,
rectangleGraphicDefinition,
starGraphicDefinition,
} from "./definitions";
import {
DEFAULT_GRAPHIC_SOURCE_SIZE,
type GraphicInstance,
type GraphicDefinition,
} from "./types";
const graphicPreviewUrlCache = new Map<string, string>();
const FALLBACK_CORNER_RADIUS_RATIO = 0.2;
const FALLBACK_FILL_OPACITY = 0.08;
const FALLBACK_MIN_FONT_SIZE = 12;
const FALLBACK_FONT_SIZE_RATIO = 0.15;
function buildFallbackPreviewUrl({
name,
size,
}: {
name: string;
size: number;
}): string {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" rx="${size * FALLBACK_CORNER_RADIUS_RATIO}" fill="white" fill-opacity="${FALLBACK_FILL_OPACITY}" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white" font-size="${Math.max(FALLBACK_MIN_FONT_SIZE, size * FALLBACK_FONT_SIZE_RATIO)}" font-family="sans-serif">${name}</text>
</svg>
`;
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
export function getGraphicDefinition({
definitionId,
}: {
definitionId: string;
}): GraphicDefinition {
registerDefaultGraphics();
return graphicsRegistry.get(definitionId);
}
export function buildDefaultGraphicInstance({
definitionId,
}: {
definitionId: string;
}): GraphicInstance {
const definition = getGraphicDefinition({ definitionId });
return {
definitionId,
params: buildDefaultParamValues(definition.params),
};
}
export function resolveGraphicParams(
definition: GraphicDefinition,
params?: ParamValues,
): ParamValues {
return {
...buildDefaultParamValues(definition.params),
...(params ?? {}),
};
}
export function resolveGraphicElementParamsAtTime({
element,
localTime,
}: {
element: {
definitionId: string;
params: ParamValues;
animations?: ElementAnimations;
};
localTime: number;
}): ParamValues {
const definition = getGraphicDefinition({
definitionId: element.definitionId,
});
return resolveGraphicParamsAtTime({
params: resolveGraphicParams(definition, element.params),
definitions: definition.params,
animations: element.animations,
localTime,
});
}
export function buildGraphicPreviewUrl({
definitionId,
params,
size = DEFAULT_GRAPHIC_SOURCE_SIZE,
}: {
definitionId: string;
params?: ParamValues;
size?: number;
}): string {
const definition = getGraphicDefinition({ definitionId });
const resolvedParams = resolveGraphicParams(definition, params);
const cacheKey = JSON.stringify({ definitionId, resolvedParams, size });
const cachedUrl = graphicPreviewUrlCache.get(cacheKey);
if (cachedUrl) {
return cachedUrl;
}
if (typeof document === "undefined") {
return buildFallbackPreviewUrl({ name: definition.name, size });
}
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) {
return buildFallbackPreviewUrl({ name: definition.name, size });
}
definition.render({
ctx,
params: resolvedParams,
width: size,
height: size,
});
const previewUrl = canvas.toDataURL("image/png");
graphicPreviewUrlCache.set(cacheKey, previewUrl);
return previewUrl;
}
export {
DEFAULT_GRAPHIC_SOURCE_SIZE,
ellipseGraphicDefinition,
graphicsRegistry,
polygonGraphicDefinition,
rectangleGraphicDefinition,
registerDefaultGraphics,
starGraphicDefinition,
};
export type {
GraphicDefinition,
GraphicInstance,
GraphicRenderContext,
} from "./types";
import { resolveGraphicParamsAtTime } from "@/animation";
import type { ElementAnimations } from "@/model/decorations/animations";
import { buildDefaultParamValues } from "@/params/registry";
import type { ParamValues } from "@/model/decorations/param-values";
import { graphicsRegistry } from "./registry";
import {
registerDefaultGraphics,
ellipseGraphicDefinition,
polygonGraphicDefinition,
rectangleGraphicDefinition,
starGraphicDefinition,
} from "./definitions";
import {
DEFAULT_GRAPHIC_SOURCE_SIZE,
type GraphicInstance,
type GraphicDefinition,
} from "./types";
const graphicPreviewUrlCache = new Map<string, string>();
const FALLBACK_CORNER_RADIUS_RATIO = 0.2;
const FALLBACK_FILL_OPACITY = 0.08;
const FALLBACK_MIN_FONT_SIZE = 12;
const FALLBACK_FONT_SIZE_RATIO = 0.15;
function buildFallbackPreviewUrl({
name,
size,
}: {
name: string;
size: number;
}): string {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" rx="${size * FALLBACK_CORNER_RADIUS_RATIO}" fill="white" fill-opacity="${FALLBACK_FILL_OPACITY}" />
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="white" font-size="${Math.max(FALLBACK_MIN_FONT_SIZE, size * FALLBACK_FONT_SIZE_RATIO)}" font-family="sans-serif">${name}</text>
</svg>
`;
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
export function getGraphicDefinition({
definitionId,
}: {
definitionId: string;
}): GraphicDefinition {
registerDefaultGraphics();
return graphicsRegistry.get(definitionId);
}
export function buildDefaultGraphicInstance({
definitionId,
}: {
definitionId: string;
}): GraphicInstance {
const definition = getGraphicDefinition({ definitionId });
return {
definitionId,
params: buildDefaultParamValues(definition.params),
};
}
export function resolveGraphicParams(
definition: GraphicDefinition,
params?: ParamValues,
): ParamValues {
return {
...buildDefaultParamValues(definition.params),
...(params ?? {}),
};
}
export function resolveGraphicElementParamsAtTime({
element,
localTime,
}: {
element: {
definitionId: string;
params: ParamValues;
animations?: ElementAnimations;
};
localTime: number;
}): ParamValues {
const definition = getGraphicDefinition({
definitionId: element.definitionId,
});
return resolveGraphicParamsAtTime({
params: resolveGraphicParams(definition, element.params),
definitions: definition.params,
animations: element.animations,
localTime,
});
}
export function buildGraphicPreviewUrl({
definitionId,
params,
size = DEFAULT_GRAPHIC_SOURCE_SIZE,
}: {
definitionId: string;
params?: ParamValues;
size?: number;
}): string {
const definition = getGraphicDefinition({ definitionId });
const resolvedParams = resolveGraphicParams(definition, params);
const cacheKey = JSON.stringify({ definitionId, resolvedParams, size });
const cachedUrl = graphicPreviewUrlCache.get(cacheKey);
if (cachedUrl) {
return cachedUrl;
}
if (typeof document === "undefined") {
return buildFallbackPreviewUrl({ name: definition.name, size });
}
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) {
return buildFallbackPreviewUrl({ name: definition.name, size });
}
definition.render({
ctx,
params: resolvedParams,
width: size,
height: size,
});
const previewUrl = canvas.toDataURL("image/png");
graphicPreviewUrlCache.set(cacheKey, previewUrl);
return previewUrl;
}
export {
DEFAULT_GRAPHIC_SOURCE_SIZE,
ellipseGraphicDefinition,
graphicsRegistry,
polygonGraphicDefinition,
rectangleGraphicDefinition,
registerDefaultGraphics,
starGraphicDefinition,
};
export type {
GraphicDefinition,
GraphicInstance,
GraphicRenderContext,
} from "./types";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ParamDefinition, ParamValues } from "@/params";
import type { ParamDefinition, ParamValues } from "@/model/decorations/param-values";
export const DEFAULT_GRAPHIC_SOURCE_SIZE = 512;
+1 -6
View File
@@ -15,12 +15,7 @@ import { textMaskDefinition } from "@/masks/definitions/text";
import { getMaskSnapGeometry } from "@/masks/geometry";
import { snapMaskInteraction } from "@/masks/snap";
import type { ElementBounds } from "@/preview/element-bounds";
import type {
CustomMaskParams,
RectangleMaskParams,
SplitMaskParams,
TextMaskParams,
} from "@/masks/types";
import type { CustomMaskParams, RectangleMaskParams, SplitMaskParams, TextMaskParams } from "@/model/decorations/mask";
const bounds: ElementBounds = {
cx: 200,
+3 -3
View File
@@ -1,8 +1,8 @@
"use client";
import type { MaskableElement } from "@/timeline";
import type { Mask, MaskType, TextMask } from "@/masks/types";
import type { NumberParamDefinition, SelectParamDefinition } from "@/params";
import type { MaskableElement } from "@/model";
import type { Mask, MaskType, TextMask } from "@/model/decorations/mask";
import type { NumberParamDefinition, SelectParamDefinition } from "@/model/decorations/param-values";
import { masksRegistry, buildDefaultMaskInstance } from "@/masks";
import { useEditor } from "@/editor/use-editor";
import { useElementPreview } from "@/timeline/hooks/use-element-preview";
+2 -10
View File
@@ -1,14 +1,6 @@
import type { ElementBounds } from "@/preview/element-bounds";
export interface CustomMaskPathPoint {
id: string;
x: number;
y: number;
inX: number;
inY: number;
outX: number;
outY: number;
}
import type { CustomMaskPathPoint } from "@/model/decorations/mask";
export type { CustomMaskPathPoint } from "@/model/decorations/mask";
export type CustomMaskHandlePart = "anchor" | "in" | "out";
+3 -9
View File
@@ -3,15 +3,9 @@ import {
MIN_MASK_DIMENSION,
} from "@/masks/dimensions";
import { computeFeatherUpdate } from "../param-update";
import type {
BaseMaskParams,
MaskDefaultContext,
MaskFeatures,
MaskInteractionDefinition,
MaskParamUpdateArgs,
RectangleMaskParams,
} from "@/masks/types";
import type { NumberParamDefinition, ParamDefinition } from "@/params";
import type { BaseMaskParams, RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefaultContext, MaskFeatures, MaskInteractionDefinition, MaskParamUpdateArgs } from "@/masks/types";
import type { NumberParamDefinition, ParamDefinition } from "@/model/decorations/param-values";
import {
getBoxMaskHandlePositions,
getBoxMaskOverlays,
@@ -1,8 +1,5 @@
import type {
MaskDefaultContext,
MaskDefinition,
RectangleMaskParams,
} from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefaultContext, MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+3 -9
View File
@@ -1,15 +1,9 @@
import { generateUUID } from "@/utils/id";
import type { ParamDefinition } from "@/params";
import type { ParamDefinition } from "@/model/decorations/param-values";
import { PEN_CURSOR } from "@/preview/components/cursors";
import type { ElementBounds } from "@/preview/element-bounds";
import type {
CustomMask,
CustomMaskParams,
MaskDefinition,
MaskHandlePosition,
MaskOverlay,
MaskParamUpdateArgs,
} from "@/masks/types";
import type { CustomMask, CustomMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition, MaskHandlePosition, MaskOverlay, MaskParamUpdateArgs } from "@/masks/types";
import {
buildCustomMaskPath2D,
buildCustomMaskSvgPath,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { BaseMaskParams, MaskDefinition } from "@/masks/types";
import type { BaseMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import { masksRegistry, type MaskIconProps } from "../registry";
import { cinematicBarsMaskDefinition } from "./cinematic-bars";
import { customMaskDefinition } from "./custom";
+2 -1
View File
@@ -1,4 +1,5 @@
import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+2 -5
View File
@@ -1,9 +1,6 @@
import { computeFeatherUpdate } from "../param-update";
import type {
MaskDefinition,
MaskParamUpdateArgs,
SplitMaskParams,
} from "@/masks/types";
import type { SplitMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition, MaskParamUpdateArgs } from "@/masks/types";
import { halfPlaneSign, lineEdgeIntersection } from "../utils";
import {
getLineMaskHandlePositions,
+2 -1
View File
@@ -1,4 +1,5 @@
import type { MaskDefinition, RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition } from "@/masks/types";
import {
BOX_LIKE_MASK_PARAMS,
buildBoxMaskInteraction,
+3 -7
View File
@@ -1,10 +1,6 @@
import type { ParamDefinition } from "@/params";
import type {
MaskDefinition,
MaskParamUpdateArgs,
TextMask,
TextMaskParams,
} from "@/masks/types";
import type { ParamDefinition } from "@/model/decorations/param-values";
import type { TextMask, TextMaskParams } from "@/model/decorations/mask";
import type { MaskDefinition, MaskParamUpdateArgs } from "@/masks/types";
import { DEFAULTS } from "@/timeline/defaults";
import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/text/typography";
import {
+4 -4
View File
@@ -1,7 +1,7 @@
import type { ElementBounds } from "@/preview/element-bounds";
import type { SnapLine } from "@/preview/preview-snap";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import { MIN_MASK_DIMENSION } from "@/masks/dimensions";
import type { RectangleMaskParams } from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
type CenterMaskParams = {
centerX: number;
@@ -100,10 +100,10 @@ export function toGlobalMaskSnapLines({
bounds,
canvasSize,
}: {
lines: SnapLine[];
lines: PreviewSnapLine[];
bounds: ElementBounds;
canvasSize: { width: number; height: number };
}): SnapLine[] {
}): PreviewSnapLine[] {
const centerX = bounds.cx - canvasSize.width / 2;
const centerY = bounds.cy - canvasSize.height / 2;
+2 -9
View File
@@ -1,14 +1,7 @@
import { FEATHER_HANDLE_SCALE } from "@/masks/feather";
import type { ElementBounds } from "@/preview/element-bounds";
import type {
MaskFeatures,
MaskHandlePosition,
MaskLineOverlay,
MaskOverlay,
MaskRectOverlay,
RectangleMaskParams,
MaskShapeOverlay,
} from "@/masks/types";
import type { RectangleMaskParams } from "@/model/decorations/mask";
import type { MaskFeatures, MaskHandlePosition, MaskLineOverlay, MaskOverlay, MaskRectOverlay, MaskShapeOverlay } from "@/masks/types";
const LINE_HANDLE_OFFSET_SCREEN_PX = 20;
const BOX_HANDLE_OFFSET_SCREEN_PX = 20;
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Mask, MaskDefaultContext, MaskType } from "@/masks/types";
import type { Mask, MaskType } from "@/model/decorations/mask";
import type { MaskDefaultContext } from "@/masks/types";
import { masksRegistry } from "./registry";
import { generateUUID } from "@/utils/id";
+3 -10
View File
@@ -1,14 +1,7 @@
import { MAX_FEATHER } from "@/masks/feather";
import type { ParamDefinition } from "@/params";
import type {
BaseMaskParams,
MaskDefaultContext,
MaskDefinition,
MaskInteractionResult,
MaskSnapArgs,
MaskSnapResult,
MaskType,
} from "@/masks/types";
import type { ParamDefinition } from "@/model/decorations/param-values";
import type { BaseMaskParams, MaskType } from "@/model/decorations/mask";
import type { MaskDefaultContext, MaskDefinition, MaskInteractionResult, MaskSnapArgs, MaskSnapResult } from "@/masks/types";
import type { HugeiconsIconProps } from "@hugeicons/react";
import { DefinitionRegistry } from "@/params/registry";
+3 -3
View File
@@ -6,9 +6,9 @@ import {
snapScale,
snapScaleAxes,
type ScaleEdgePreference,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import type { RectangleMaskParams, SplitMaskParams } from "@/masks/types";
import type { RectangleMaskParams, SplitMaskParams } from "@/model/decorations/mask";
import {
isRectangleMaskParams,
getMaskSnapGeometry,
@@ -20,7 +20,7 @@ type SharedMaskParams = SplitMaskParams | RectangleMaskParams;
type MaskSnapResult<TParams extends SharedMaskParams> = {
params: TParams;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
};
const CORNER_SIZE_HANDLES = new Set([
+27 -128
View File
@@ -1,135 +1,34 @@
import type { ElementBounds } from "@/preview/element-bounds";
import type { SnapLine } from "@/preview/preview-snap";
import type { ParamDefinition } from "@/params";
import type { CustomMaskPathPoint } from "@/masks/custom-path";
import type { PreviewSnapLine } from "@/preview/preview-snap";
import type { ParamDefinition } from "@/model/decorations/param-values";
import type {
BaseMaskParams,
Mask,
MaskType,
} from "@/model/decorations/mask";
export type {
BaseMaskParams,
CinematicBarsMask,
CustomMask,
CustomMaskParams,
CustomMaskPathPoint,
DiamondMask,
EllipseMask,
HeartMask,
Mask,
MaskType,
RectangleMask,
RectangleMaskParams,
SplitMask,
SplitMaskParams,
StarMask,
TextDecoration,
TextFontStyle,
TextFontWeight,
} from "@/text/primitives";
export type MaskType =
| "split"
| "cinematic-bars"
| "rectangle"
| "ellipse"
| "heart"
| "diamond"
| "star"
| "text"
| "custom";
export interface BaseMaskParams {
feather: number;
inverted: boolean;
strokeColor: string;
strokeWidth: number;
strokeAlign: "inside" | "center" | "outside";
}
export interface SplitMaskParams extends BaseMaskParams {
centerX: number;
centerY: number;
rotation: number;
}
export interface RectangleMaskParams extends BaseMaskParams {
centerX: number;
centerY: number;
width: number;
height: number;
rotation: number;
scale: number;
}
export interface TextMaskParams extends BaseMaskParams {
content: string;
fontSize: number;
fontFamily: string;
fontWeight: TextFontWeight;
fontStyle: TextFontStyle;
textDecoration: TextDecoration;
letterSpacing: number;
lineHeight: number;
centerX: number;
centerY: number;
rotation: number;
scale: number;
}
export interface CustomMaskParams extends BaseMaskParams {
path: CustomMaskPathPoint[];
closed: boolean;
centerX: number;
centerY: number;
rotation: number;
scale: number;
}
export interface SplitMask {
id: string;
type: "split";
params: SplitMaskParams;
}
export interface CinematicBarsMask {
id: string;
type: "cinematic-bars";
params: RectangleMaskParams;
}
export interface RectangleMask {
id: string;
type: "rectangle";
params: RectangleMaskParams;
}
export interface EllipseMask {
id: string;
type: "ellipse";
params: RectangleMaskParams;
}
export interface HeartMask {
id: string;
type: "heart";
params: RectangleMaskParams;
}
export interface DiamondMask {
id: string;
type: "diamond";
params: RectangleMaskParams;
}
export interface StarMask {
id: string;
type: "star";
params: RectangleMaskParams;
}
export interface TextMask {
id: string;
type: "text";
params: TextMaskParams;
}
export interface CustomMask {
id: string;
type: "custom";
params: CustomMaskParams;
}
export type Mask =
| SplitMask
| CinematicBarsMask
| RectangleMask
| EllipseMask
| HeartMask
| DiamondMask
| StarMask
| TextMask
| CustomMask;
TextMask,
TextMaskParams,
} from "@/model/decorations/mask";
export interface MaskRenderer {
buildPath?: (params: {
@@ -261,7 +160,7 @@ export interface MaskSnapResult<
TParams extends BaseMaskParams = BaseMaskParams,
> {
params: TParams;
activeLines: SnapLine[];
activeLines: PreviewSnapLine[];
}
export interface MaskInteractionResult {
+7 -6
View File
@@ -14,11 +14,12 @@ import {
} from "@/preview/element-bounds";
import {
SNAP_THRESHOLD_SCREEN_PIXELS,
type SnapLine,
type PreviewSnapLine,
} from "@/preview/preview-snap";
import type { SelectedMaskPointSelection } from "@/selection/editor-selection";
import type { Mask, MaskInteractionResult } from "@/masks/types";
import type { MaskableElement } from "@/timeline";
import type { Mask } from "@/model/decorations/mask";
import type { MaskInteractionResult } from "@/masks/types";
import type { MaskableElement } from "@/model";
import { registerCanceller } from "@/editor/cancel-interaction";
interface DragState {
@@ -94,7 +95,7 @@ function withUpdatedMaskParams<TMask extends Mask>({
export function useMaskHandles({
onSnapLinesChange,
}: {
onSnapLinesChange?: (lines: SnapLine[]) => void;
onSnapLinesChange?: (lines: PreviewSnapLine[]) => void;
}) {
const editor = useEditor();
const isShiftHeldRef = useShiftKey();
@@ -552,7 +553,7 @@ export function useMaskHandles({
screenPixels: SNAP_THRESHOLD_SCREEN_PIXELS,
});
const { params: nextParams, activeLines } = isShiftHeldRef.current
? { params: proposedParams, activeLines: [] as SnapLine[] }
? { params: proposedParams, activeLines: [] as PreviewSnapLine[] }
: (def.interaction.snap?.({
handleId: drag.handleId,
startParams: drag.startParams,
@@ -560,7 +561,7 @@ export function useMaskHandles({
bounds: selectedWithMask.bounds,
canvasSize,
snapThreshold,
}) ?? { params: proposedParams, activeLines: [] as SnapLine[] });
}) ?? { params: proposedParams, activeLines: [] as PreviewSnapLine[] });
onSnapLinesChange?.(activeLines);
+1 -7
View File
@@ -1,10 +1,4 @@
import type {
AudioElement,
VideoElement,
LibraryAudioElement,
RetimeConfig,
SceneTracks,
} from "@/timeline";
import type { AudioElement, LibraryAudioElement, RetimeConfig, SceneTracks, VideoElement } from "@/model";
import { shouldMaintainPitch } from "@/retime/rate";
import type { MediaAsset } from "@/media/types";
import { applyAudioMasteringToBuffer } from "@/media/audio-mastering";
+1 -1
View File
@@ -6,7 +6,7 @@ import {
type VideoCodec,
} from "mediabunny";
import { createTimelineAudioBuffer } from "@/media/audio";
import type { SceneTracks } from "@/timeline";
import type { SceneTracks } from "@/model";
import type { MediaAsset } from "@/media/types";
import { TICKS_PER_SECOND } from "@/wasm";
import { renderThumbnailDataUrl } from "./thumbnail";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import { getSourceTimeAtClipTime } from "@/retime";
import type { RetimeConfig } from "@/timeline";
import type { RetimeConfig } from "@/model";
const RMS_ANALYSIS_WINDOW_SECONDS = 0.02;
const DEFAULT_SOURCE_WAVEFORM_BUCKET_SIZE = 128;

Some files were not shown because too many files have changed in this diff Show More