diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts index c044ff5f..29b290d9 100644 --- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts +++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-param-property.ts @@ -83,7 +83,7 @@ export function useKeyframedParamProperty({ defaultInterpolation: getParamDefaultInterpolation({ param, }), - coerceValue: (nextValue) => + coerceValue: ({ value: nextValue }) => coerceAnimationValueForParam({ param, value: nextValue, diff --git a/apps/web/src/lib/animation/bezier.ts b/apps/web/src/lib/animation/bezier.ts index 4dce027e..5a9758ed 100644 --- a/apps/web/src/lib/animation/bezier.ts +++ b/apps/web/src/lib/animation/bezier.ts @@ -1,5 +1,7 @@ import type { ScalarAnimationKey } from "@/lib/animation/types"; +const BEZIER_SOLVE_ITERATIONS = 20; + export function getBezierPoint({ progress, p0, @@ -68,7 +70,7 @@ export function solveBezierProgressForTime({ const leftHandle = rightKey.leftHandle ?? getDefaultLeftHandle({ leftKey, rightKey }); - for (let iteration = 0; iteration < 20; iteration++) { + for (let iteration = 0; iteration < BEZIER_SOLVE_ITERATIONS; iteration++) { const mid = (lower + upper) / 2; const estimate = getBezierPoint({ progress: mid, diff --git a/apps/web/src/lib/animation/binding-values.ts b/apps/web/src/lib/animation/binding-values.ts index 6f7ce995..344fd776 100644 --- a/apps/web/src/lib/animation/binding-values.ts +++ b/apps/web/src/lib/animation/binding-values.ts @@ -13,6 +13,7 @@ import type { Vector2AnimationBinding, VectorValue, } from "@/lib/animation/types"; +import { clamp } from "@/utils/math"; interface LinearRgba { r: number; @@ -25,10 +26,6 @@ export type AnimationComponentValue = number | DiscreteValue; const toRgb = converter("rgb"); -function clamp01({ value }: { value: number }): number { - return Math.max(0, Math.min(1, value)); -} - function srgbToLinear({ value }: { value: number }): number { return value <= 0.04045 ? value / 12.92 @@ -36,7 +33,7 @@ function srgbToLinear({ value }: { value: number }): number { } function linearToSrgb({ value }: { value: number }): number { - const clamped = clamp01({ value }); + const clamped = clamp({ value, min: 0, max: 1 }); return clamped <= 0.0031308 ? clamped * 12.92 : 1.055 * Math.pow(clamped, 1 / 2.4) - 0.055; @@ -234,7 +231,7 @@ export function parseColorToLinearRgba({ r: srgbToLinear({ value: rgb.r ?? 0 }), g: srgbToLinear({ value: rgb.g ?? 0 }), b: srgbToLinear({ value: rgb.b ?? 0 }), - a: clamp01({ value: rgb.alpha ?? 1 }), + a: clamp({ value: rgb.alpha ?? 1, min: 0, max: 1 }), }; } @@ -248,7 +245,7 @@ export function formatLinearRgba({ r: linearToSrgb({ value: color.r }), g: linearToSrgb({ value: color.g }), b: linearToSrgb({ value: color.b }), - alpha: clamp01({ value: color.a }), + alpha: clamp({ value: color.a, min: 0, max: 1 }), } as const; return rgb.alpha < 1 ? formatHex8(rgb) : formatHex(rgb); } diff --git a/apps/web/src/lib/animation/interpolation.ts b/apps/web/src/lib/animation/interpolation.ts index 9609441c..67456aa0 100644 --- a/apps/web/src/lib/animation/interpolation.ts +++ b/apps/web/src/lib/animation/interpolation.ts @@ -9,6 +9,7 @@ import type { ScalarSegmentType, } from "@/lib/animation/types"; import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants"; +import { clamp } from "@/utils/math"; import { getBezierPoint, getDefaultLeftHandle, @@ -41,10 +42,6 @@ function isWithinTimePair({ ); } -function clamp01({ value }: { value: number }): number { - return Math.max(0, Math.min(1, value)); -} - function lerpNumber({ leftValue, rightValue, @@ -287,8 +284,10 @@ export function getScalarChannelValueAtTime({ return rightKey.value; } - const progress = clamp01({ + const progress = clamp({ value: (time - leftKey.time) / span, + min: 0, + max: 1, }); if (leftKey.segmentToNext === "linear") { return lerpNumber({ diff --git a/apps/web/src/lib/animation/keyframes.ts b/apps/web/src/lib/animation/keyframes.ts index 7eaa5ed6..d72f9e16 100644 --- a/apps/web/src/lib/animation/keyframes.ts +++ b/apps/web/src/lib/animation/keyframes.ts @@ -440,9 +440,9 @@ export function upsertPathKeyframe({ keyframeId?: string; kind: AnimationBindingKind; defaultInterpolation: AnimationInterpolation; - coerceValue: (value: AnimationValue) => AnimationValue | null; + coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null; }): ElementAnimations | undefined { - const coercedValue = coerceValue(value); + const coercedValue = coerceValue({ value }); if (coercedValue === null) { return animations; } @@ -547,7 +547,7 @@ export function upsertElementKeyframe({ keyframeId, kind: propertyDefinition.kind, defaultInterpolation: propertyDefinition.defaultInterpolation, - coerceValue: (nextValue) => + coerceValue: ({ value: nextValue }) => coerceAnimationValueForProperty({ propertyPath, value: nextValue, diff --git a/apps/web/src/lib/animation/target-resolver.ts b/apps/web/src/lib/animation/target-resolver.ts index 06c9e140..64bb8709 100644 --- a/apps/web/src/lib/animation/target-resolver.ts +++ b/apps/web/src/lib/animation/target-resolver.ts @@ -32,9 +32,9 @@ export interface AnimationPathDescriptor { kind: AnimationBindingKind; defaultInterpolation: AnimationInterpolation; numericRanges?: Partial>; - coerceValue(value: AnimationValue): AnimationValue | null; - getBaseValue(): AnimationValue | null; - setBaseValue(value: AnimationValue): TimelineElement; + coerceValue: ({ value }: { value: AnimationValue }) => AnimationValue | null; + getBaseValue: () => AnimationValue | null; + setBaseValue: ({ value }: { value: AnimationValue }) => TimelineElement; } export function getParamValueKind({ @@ -135,9 +135,9 @@ function buildGraphicParamDescriptor({ kind: getParamValueKind({ param }), defaultInterpolation: getParamDefaultInterpolation({ param }), numericRanges: getParamNumericRange({ param }), - coerceValue: (value) => coerceAnimationValueForParam({ param, value }), + coerceValue: ({ value }) => coerceAnimationValueForParam({ param, value }), getBaseValue: () => element.params[param.key] ?? param.default, - setBaseValue: (value) => { + setBaseValue: ({ value }) => { const coercedValue = coerceAnimationValueForParam({ param, value }); if (coercedValue === null) { return element; @@ -183,9 +183,9 @@ function buildEffectParamDescriptor({ kind: getParamValueKind({ param }), defaultInterpolation: getParamDefaultInterpolation({ param }), numericRanges: getParamNumericRange({ param }), - coerceValue: (value) => coerceAnimationValueForParam({ param, value }), + coerceValue: ({ value }) => coerceAnimationValueForParam({ param, value }), getBaseValue: () => effect.params[param.key] ?? param.default, - setBaseValue: (value) => { + setBaseValue: ({ value }) => { const coercedValue = coerceAnimationValueForParam({ param, value }); if (coercedValue === null) { return element; @@ -239,7 +239,7 @@ export function resolveAnimationTarget({ kind: propertyDefinition.kind, defaultInterpolation: propertyDefinition.defaultInterpolation, numericRanges: propertyDefinition.numericRanges, - coerceValue: (value) => + coerceValue: ({ value }) => coerceAnimationValueForProperty({ propertyPath: path, value, @@ -249,7 +249,7 @@ export function resolveAnimationTarget({ element, propertyPath: path, }), - setBaseValue: (value) => { + setBaseValue: ({ value }) => { const coercedValue = propertyDefinition.coerceValue({ value }); if (coercedValue === null) { return element; diff --git a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts index 71b2ac21..35aa0860 100644 --- a/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts +++ b/apps/web/src/lib/commands/timeline/element/keyframes/remove-keyframe.ts @@ -79,7 +79,7 @@ function removeKeyframeAndPersist({ const shouldPersistToBase = isChannelNowEmpty && valueBefore !== null; const baseElement = shouldPersistToBase - ? target.setBaseValue(valueBefore) + ? target.setBaseValue({ value: valueBefore }) : element; return { ...baseElement, animations: nextAnimations }; diff --git a/apps/web/src/services/storage/migrations/transformers/v21-to-v22.ts b/apps/web/src/services/storage/migrations/transformers/v21-to-v22.ts index 15fd9edf..5212caf6 100644 --- a/apps/web/src/services/storage/migrations/transformers/v21-to-v22.ts +++ b/apps/web/src/services/storage/migrations/transformers/v21-to-v22.ts @@ -1,10 +1,44 @@ -import { parseColorToLinearRgba } from "@/lib/animation/binding-values"; +import { converter, parse } from "culori"; import type { MigrationResult, ProjectRecord } from "./types"; import { getProjectId, isRecord } from "./utils"; const COLOR_COMPONENT_KEYS = ["r", "g", "b", "a"] as const; type LegacyInterpolation = "linear" | "hold"; +const toRgb = converter("rgb"); + +interface LinearRgba { + r: number; + g: number; + b: number; + a: number; +} + +function srgbToLinear({ value }: { value: number }): number { + return value <= 0.04045 + ? value / 12.92 + : Math.pow((value + 0.055) / 1.055, 2.4); +} + +function parseColorToLinearRgba({ + color, +}: { + color: string; +}): LinearRgba | null { + const parsed = parse(color); + const rgb = parsed ? toRgb(parsed) : null; + if (!rgb) { + return null; + } + + return { + r: srgbToLinear({ value: rgb.r ?? 0 }), + g: srgbToLinear({ value: rgb.g ?? 0 }), + b: srgbToLinear({ value: rgb.b ?? 0 }), + a: Math.max(0, Math.min(1, rgb.alpha ?? 1)), + }; +} + interface LegacyScalarKeyframe { id: string; time: number;