summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts24
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts95
-rw-r--r--apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts4
-rw-r--r--apps/web/src/components/editor/panels/properties/section.tsx2
-rw-r--r--apps/web/src/components/editor/panels/properties/sections/blending.tsx72
-rw-r--r--apps/web/src/components/editor/panels/properties/text-properties.tsx493
6 files changed, 454 insertions, 236 deletions
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts
new file mode 100644
index 00000000..46e6cb6d
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-element-playhead.ts
@@ -0,0 +1,24 @@
+import { useEditor } from "@/hooks/use-editor";
+import { getElementLocalTime } from "@/lib/animation";
+import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants";
+
+export function useElementPlayhead({
+ startTime,
+ duration,
+}: {
+ startTime: number;
+ duration: number;
+}) {
+ const editor = useEditor();
+ const playheadTime = editor.playback.getCurrentTime();
+ const localTime = getElementLocalTime({
+ timelineTime: playheadTime,
+ elementStartTime: startTime,
+ elementDuration: duration,
+ });
+ const isPlayheadWithinElementRange =
+ playheadTime >= startTime - TIME_EPSILON_SECONDS &&
+ playheadTime <= startTime + duration + TIME_EPSILON_SECONDS;
+
+ return { localTime, isPlayheadWithinElementRange };
+}
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts
new file mode 100644
index 00000000..dbd19e4c
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-color-property.ts
@@ -0,0 +1,95 @@
+import { useEditor } from "@/hooks/use-editor";
+import {
+ getKeyframeAtTime,
+ hasKeyframesForPath,
+ upsertElementKeyframe,
+} from "@/lib/animation";
+import type { AnimationPropertyPath, ElementAnimations } from "@/types/animation";
+import type { TimelineElement } from "@/types/timeline";
+
+export function useKeyframedColorProperty({
+ trackId,
+ elementId,
+ animations,
+ propertyPath,
+ localTime,
+ isPlayheadWithinElementRange,
+ resolvedColor,
+ buildBaseUpdates,
+}: {
+ trackId: string;
+ elementId: string;
+ animations: ElementAnimations | undefined;
+ propertyPath: AnimationPropertyPath;
+ localTime: number;
+ isPlayheadWithinElementRange: boolean;
+ resolvedColor: string;
+ buildBaseUpdates: ({ value }: { value: string }) => Partial<TimelineElement>;
+}) {
+ const editor = useEditor();
+
+ const hasAnimatedKeyframes = hasKeyframesForPath({ animations, propertyPath });
+ const keyframeAtTime = isPlayheadWithinElementRange
+ ? getKeyframeAtTime({ animations, propertyPath, time: localTime })
+ : null;
+ const keyframeIdAtTime = keyframeAtTime?.id ?? null;
+ const isKeyframedAtTime = keyframeAtTime !== null;
+ const shouldUseAnimatedChannel =
+ hasAnimatedKeyframes && isPlayheadWithinElementRange;
+
+ const onChange = ({ color }: { color: string }) => {
+ if (shouldUseAnimatedChannel) {
+ editor.timeline.previewElements({
+ updates: [
+ {
+ trackId,
+ elementId,
+ updates: {
+ animations: upsertElementKeyframe({
+ animations,
+ propertyPath,
+ time: localTime,
+ value: color,
+ }),
+ },
+ },
+ ],
+ });
+ return;
+ }
+
+ editor.timeline.previewElements({
+ updates: [{ trackId, elementId, updates: buildBaseUpdates({ value: color }) }],
+ });
+ };
+
+ const onChangeEnd = () => editor.timeline.commitPreview();
+
+ const toggleKeyframe = () => {
+ if (!isPlayheadWithinElementRange) {
+ return;
+ }
+
+ if (keyframeIdAtTime) {
+ editor.timeline.removeKeyframes({
+ keyframes: [{ trackId, elementId, propertyPath, keyframeId: keyframeIdAtTime }],
+ });
+ return;
+ }
+
+ editor.timeline.upsertKeyframes({
+ keyframes: [
+ { trackId, elementId, propertyPath, time: localTime, value: resolvedColor },
+ ],
+ });
+ };
+
+ return {
+ isKeyframedAtTime,
+ hasAnimatedKeyframes,
+ keyframeIdAtTime,
+ onChange,
+ onChangeEnd,
+ toggleKeyframe,
+ };
+}
diff --git a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
index cd62ca75..11aa049c 100644
--- a/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
+++ b/apps/web/src/components/editor/panels/properties/hooks/use-keyframed-number-property.ts
@@ -5,7 +5,7 @@ import {
upsertElementKeyframe,
} from "@/lib/animation";
import type { AnimationPropertyPath, ElementAnimations } from "@/types/animation";
-import type { ElementUpdatePatch } from "@/types/timeline";
+import type { TimelineElement } from "@/types/timeline";
import { usePropertyDraft } from "./use-property-draft";
export function useKeyframedNumberProperty({
@@ -29,7 +29,7 @@ export function useKeyframedNumberProperty({
displayValue: string;
parse: (input: string) => number | null;
valueAtPlayhead: number;
- buildBaseUpdates: ({ value }: { value: number }) => ElementUpdatePatch;
+ buildBaseUpdates: ({ value }: { value: number }) => Partial<TimelineElement>;
}) {
const editor = useEditor();
diff --git a/apps/web/src/components/editor/panels/properties/section.tsx b/apps/web/src/components/editor/panels/properties/section.tsx
index 4428b59c..5989108b 100644
--- a/apps/web/src/components/editor/panels/properties/section.tsx
+++ b/apps/web/src/components/editor/panels/properties/section.tsx
@@ -223,7 +223,7 @@ export function SectionField({
}) {
return (
<div className={cn("flex flex-col gap-2", className)}>
- <div className="flex items-center gap-1.5">
+ <div className="flex h-4 items-center gap-1.5">
{beforeLabel}
<Label>{label}</Label>
</div>
diff --git a/apps/web/src/components/editor/panels/properties/sections/blending.tsx b/apps/web/src/components/editor/panels/properties/sections/blending.tsx
index 66fe820f..a50e84e7 100644
--- a/apps/web/src/components/editor/panels/properties/sections/blending.tsx
+++ b/apps/web/src/components/editor/panels/properties/sections/blending.tsx
@@ -1,5 +1,4 @@
import { useEditor } from "@/hooks/use-editor";
-import { usePropertyDraft } from "../hooks/use-property-draft";
import { clamp } from "@/utils/math";
import { NumberField } from "@/components/ui/number-field";
import {
@@ -19,14 +18,23 @@ import {
} from "@/components/ui/select";
import type { BlendMode } from "@/types/rendering";
import type { ElementType } from "@/types/timeline";
+import type { ElementAnimations } from "@/types/animation";
import { HugeiconsIcon } from "@hugeicons/react";
import { RainDropIcon } from "@hugeicons/core-free-icons";
+import { KeyframeToggle } from "../keyframe-toggle";
+import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property";
+import { useElementPlayhead } from "../hooks/use-element-playhead";
+import { resolveOpacityAtTime } from "@/lib/animation";
+import { isPropertyAtDefault } from "./transform";
type BlendingElement = {
id: string;
opacity: number;
type: ElementType;
blendMode?: BlendMode;
+ startTime: number;
+ duration: number;
+ animations?: ElementAnimations;
};
const BLEND_MODE_GROUPS = [
@@ -105,20 +113,31 @@ export function BlendingSection({
}
};
- const opacity = usePropertyDraft({
- displayValue: Math.round(element.opacity * 100).toString(),
+ const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
+ startTime: element.startTime,
+ duration: element.duration,
+ });
+ const resolvedOpacity = resolveOpacityAtTime({
+ baseOpacity: element.opacity,
+ animations: element.animations,
+ localTime,
+ });
+
+ const opacity = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "opacity",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedOpacity * 100).toString(),
parse: (input) => {
const parsed = parseFloat(input);
if (Number.isNaN(parsed)) return null;
return clamp({ value: parsed, min: 0, max: 100 }) / 100;
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- { trackId, elementId: element.id, updates: { opacity: value } },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedOpacity,
+ buildBaseUpdates: ({ value }) => ({ opacity: value }),
});
return (
@@ -126,7 +145,18 @@ export function BlendingSection({
<SectionHeader><SectionTitle>Blending</SectionTitle></SectionHeader>
<SectionContent>
<div className="flex items-start gap-2">
- <SectionField label="Opacity" className="w-1/2">
+ <SectionField
+ label="Opacity"
+ className="w-1/2"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={opacity.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle opacity keyframe"
+ onToggle={opacity.toggleKeyframe}
+ />
+ }
+ >
<NumberField
className="w-full"
icon={
@@ -140,18 +170,14 @@ export function BlendingSection({
onBlur={opacity.onBlur}
onScrub={opacity.scrubTo}
onScrubEnd={opacity.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { opacity: DEFAULT_OPACITY },
- },
- ],
- })
- }
- isDefault={element.opacity === DEFAULT_OPACITY}
+ onReset={() => opacity.commitValue({ value: DEFAULT_OPACITY })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: opacity.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedOpacity,
+ staticValue: element.opacity,
+ defaultValue: DEFAULT_OPACITY,
+ })}
dragSensitivity="slow"
/>
</SectionField>
diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx
index 73f3f1a3..ae6de420 100644
--- a/apps/web/src/components/editor/panels/properties/text-properties.tsx
+++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx
@@ -28,7 +28,13 @@ import {
MIN_FONT_SIZE,
} from "@/constants/text-constants";
import { usePropertyDraft } from "./hooks/use-property-draft";
+import { useKeyframedColorProperty } from "./hooks/use-keyframed-color-property";
+import { useKeyframedNumberProperty } from "./hooks/use-keyframed-number-property";
+import { useElementPlayhead } from "./hooks/use-element-playhead";
import { TransformSection, BlendingSection } from "./sections";
+import { KeyframeToggle } from "./keyframe-toggle";
+import { isPropertyAtDefault } from "./sections/transform";
+import { resolveColorAtTime, resolveNumberAtTime } from "@/lib/animation";
import { HugeiconsIcon } from "@hugeicons/react";
import {
TextFontIcon,
@@ -80,7 +86,9 @@ function ContentSection({
return (
<Section collapsible sectionKey="text:content" showTopBorder={false}>
- <SectionHeader><SectionTitle>Content</SectionTitle></SectionHeader>
+ <SectionHeader>
+ <SectionTitle>Content</SectionTitle>
+ </SectionHeader>
<SectionContent>
<Textarea
placeholder="Name"
@@ -103,6 +111,27 @@ function TypographySection({
trackId: string;
}) {
const editor = useEditor();
+ const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
+ startTime: element.startTime,
+ duration: element.duration,
+ });
+ const resolvedTextColor = resolveColorAtTime({
+ baseColor: element.color,
+ animations: element.animations,
+ propertyPath: "color",
+ localTime,
+ });
+
+ const textColor = useKeyframedColorProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "color",
+ localTime,
+ isPlayheadWithinElementRange,
+ resolvedColor: resolvedTextColor,
+ buildBaseUpdates: ({ value }) => ({ color: value }),
+ });
const fontSize = usePropertyDraft({
displayValue: element.fontSize.toString(),
@@ -122,7 +151,9 @@ function TypographySection({
return (
<Section collapsible sectionKey="text:typography">
- <SectionHeader><SectionTitle>Typography</SectionTitle></SectionHeader>
+ <SectionHeader>
+ <SectionTitle>Typography</SectionTitle>
+ </SectionHeader>
<SectionContent>
<SectionFields>
<SectionField label="Font">
@@ -166,23 +197,23 @@ function TypographySection({
icon={<HugeiconsIcon icon={TextFontIcon} />}
/>
</SectionField>
- <SectionField label="Color">
+ <SectionField
+ label="Color"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={textColor.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle text color keyframe"
+ onToggle={textColor.toggleKeyframe}
+ />
+ }
+ >
<ColorPicker
value={uppercase({
- string: (element.color || "FFFFFF").replace("#", ""),
+ string: resolvedTextColor.replace("#", ""),
})}
- onChange={(color) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { color: `#${color}` },
- },
- ],
- })
- }
- onChangeEnd={() => editor.timeline.commitPreview()}
+ onChange={(color) => textColor.onChange({ color: `#${color}` })}
+ onChangeEnd={textColor.onChangeEnd}
/>
</SectionField>
</SectionFields>
@@ -236,7 +267,9 @@ function SpacingSection({
return (
<Section collapsible sectionKey="text:spacing" showBottomBorder={false}>
- <SectionHeader><SectionTitle>Spacing</SectionTitle></SectionHeader>
+ <SectionHeader>
+ <SectionTitle>Spacing</SectionTitle>
+ </SectionHeader>
<SectionContent>
<div className="flex items-start gap-2">
<SectionField label="Letter spacing" className="w-1/2">
@@ -306,117 +339,152 @@ function BackgroundSection({
}) {
const editor = useEditor();
const lastSelectedColor = useRef(DEFAULT_COLOR);
+ const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
+ startTime: element.startTime,
+ duration: element.duration,
+ });
+ const resolvedBgColor = resolveColorAtTime({
+ baseColor: element.background.color,
+ animations: element.animations,
+ propertyPath: "background.color",
+ localTime,
+ });
- const cornerRadius = usePropertyDraft({
- displayValue: Math.round(
- clamp({
- value: element.background.cornerRadius ?? 0,
- min: CORNER_RADIUS_MIN,
- max: CORNER_RADIUS_MAX,
- }),
- ).toString(),
+ const bgColor = useKeyframedColorProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.color",
+ localTime,
+ isPlayheadWithinElementRange,
+ resolvedColor: resolvedBgColor,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...element.background, color: value },
+ }),
+ });
+
+ const bg = element.background;
+
+ const resolvedPaddingX = resolveNumberAtTime({
+ baseValue: bg.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ animations: element.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+ });
+ const resolvedPaddingY = resolveNumberAtTime({
+ baseValue: bg.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY,
+ animations: element.animations,
+ propertyPath: "background.paddingY",
+ localTime,
+ });
+ const resolvedOffsetX = resolveNumberAtTime({
+ baseValue: bg.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX,
+ animations: element.animations,
+ propertyPath: "background.offsetX",
+ localTime,
+ });
+ const resolvedOffsetY = resolveNumberAtTime({
+ baseValue: bg.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY,
+ animations: element.animations,
+ propertyPath: "background.offsetY",
+ localTime,
+ });
+ const resolvedCornerRadius = resolveNumberAtTime({
+ baseValue: bg.cornerRadius ?? CORNER_RADIUS_MIN,
+ animations: element.animations,
+ propertyPath: "background.cornerRadius",
+ localTime,
+ });
+
+ const paddingX = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.paddingX",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedPaddingX).toString(),
parse: (input) => {
const parsed = parseFloat(input);
- if (Number.isNaN(parsed)) return null;
- return clamp({
- value: Math.round(parsed),
- min: CORNER_RADIUS_MIN,
- max: CORNER_RADIUS_MAX,
- });
+ return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: { ...element.background, cornerRadius: value },
- },
- },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedPaddingX,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...bg, paddingX: value },
+ }),
});
- const paddingX = usePropertyDraft({
- displayValue: Math.round(
- element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- ).toString(),
+ const paddingY = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.paddingY",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedPaddingY).toString(),
parse: (input) => {
const parsed = parseFloat(input);
return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { background: { ...element.background, paddingX: value } },
- },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedPaddingY,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...bg, paddingY: value },
+ }),
});
- const paddingY = usePropertyDraft({
- displayValue: Math.round(
- element.background.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY,
- ).toString(),
+ const offsetX = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.offsetX",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedOffsetX).toString(),
parse: (input) => {
const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
+ return Number.isNaN(parsed) ? null : Math.round(parsed);
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { background: { ...element.background, paddingY: value } },
- },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedOffsetX,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...bg, offsetX: value },
+ }),
});
- const offsetX = usePropertyDraft({
- displayValue: Math.round(element.background.offsetX ?? 0).toString(),
+ const offsetY = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.offsetY",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedOffsetY).toString(),
parse: (input) => {
const parsed = parseFloat(input);
return Number.isNaN(parsed) ? null : Math.round(parsed);
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { background: { ...element.background, offsetX: value } },
- },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedOffsetY,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...bg, offsetY: value },
+ }),
});
- const offsetY = usePropertyDraft({
- displayValue: Math.round(element.background.offsetY ?? 0).toString(),
+ const cornerRadius = useKeyframedNumberProperty({
+ trackId,
+ elementId: element.id,
+ animations: element.animations,
+ propertyPath: "background.cornerRadius",
+ localTime,
+ isPlayheadWithinElementRange,
+ displayValue: Math.round(resolvedCornerRadius).toString(),
parse: (input) => {
const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.round(parsed);
+ if (Number.isNaN(parsed)) return null;
+ return clamp({ value: Math.round(parsed), min: CORNER_RADIUS_MIN, max: CORNER_RADIUS_MAX });
},
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { background: { ...element.background, offsetY: value } },
- },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
+ valueAtPlayhead: resolvedCornerRadius,
+ buildBaseUpdates: ({ value }) => ({
+ background: { ...bg, cornerRadius: value },
+ }),
});
const toggleBackgroundEnabled = () => {
@@ -472,36 +540,47 @@ function BackgroundSection({
)}
>
<SectionFields>
- <SectionField label="Color">
+ <SectionField
+ label="Color"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={bgColor.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle background color keyframe"
+ onToggle={bgColor.toggleKeyframe}
+ />
+ }
+ >
<ColorPicker
value={
!element.background.enabled ||
element.background.color === "transparent"
? lastSelectedColor.current.replace("#", "")
- : element.background.color.replace("#", "")
+ : resolvedBgColor.replace("#", "")
}
onChange={(color) => {
const hexColor = `#${color}`;
if (color !== "transparent") {
lastSelectedColor.current = hexColor;
}
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: { ...element.background, color: hexColor },
- },
- },
- ],
- });
+ bgColor.onChange({ color: hexColor });
}}
- onChangeEnd={() => editor.timeline.commitPreview()}
+ onChangeEnd={bgColor.onChangeEnd}
/>
</SectionField>
<div className="flex items-start gap-2">
- <SectionField label="Width" className="w-1/2">
+ <SectionField
+ label="Width"
+ className="w-1/2"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={paddingX.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle background width keyframe"
+ onToggle={paddingX.toggleKeyframe}
+ />
+ }
+ >
<NumberField
icon="W"
value={paddingX.displayValue}
@@ -511,30 +590,28 @@ function BackgroundSection({
onBlur={paddingX.onBlur}
onScrub={paddingX.scrubTo}
onScrubEnd={paddingX.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: {
- ...element.background,
- paddingX: DEFAULT_TEXT_BACKGROUND.paddingX,
- },
- },
- },
- ],
- })
- }
- isDefault={
- (element.background.paddingX ??
- DEFAULT_TEXT_BACKGROUND.paddingX) ===
- DEFAULT_TEXT_BACKGROUND.paddingX
- }
+ onReset={() => paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedPaddingX,
+ staticValue: bg.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
+ defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX,
+ })}
/>
</SectionField>
- <SectionField label="Height" className="w-1/2">
+ <SectionField
+ label="Height"
+ className="w-1/2"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={paddingY.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle background height keyframe"
+ onToggle={paddingY.toggleKeyframe}
+ />
+ }
+ >
<NumberField
icon="H"
value={paddingY.displayValue}
@@ -544,32 +621,30 @@ function BackgroundSection({
onBlur={paddingY.onBlur}
onScrub={paddingY.scrubTo}
onScrubEnd={paddingY.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: {
- ...element.background,
- paddingY: DEFAULT_TEXT_BACKGROUND.paddingY,
- },
- },
- },
- ],
- })
- }
- isDefault={
- (element.background.paddingY ??
- DEFAULT_TEXT_BACKGROUND.paddingY) ===
- DEFAULT_TEXT_BACKGROUND.paddingY
- }
+ onReset={() => paddingY.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingY })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: paddingY.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedPaddingY,
+ staticValue: bg.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY,
+ defaultValue: DEFAULT_TEXT_BACKGROUND.paddingY,
+ })}
/>
</SectionField>
</div>
<div className="flex items-start gap-2">
- <SectionField label="X-offset" className="w-1/2">
+ <SectionField
+ label="X-offset"
+ className="w-1/2"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={offsetX.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle x-offset keyframe"
+ onToggle={offsetX.toggleKeyframe}
+ />
+ }
+ >
<NumberField
icon="X"
value={offsetX.displayValue}
@@ -578,23 +653,28 @@ function BackgroundSection({
onBlur={offsetX.onBlur}
onScrub={offsetX.scrubTo}
onScrubEnd={offsetX.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: { ...element.background, offsetX: DEFAULT_TEXT_BACKGROUND.offsetX },
- },
- },
- ],
- })
- }
- isDefault={(element.background.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX) === DEFAULT_TEXT_BACKGROUND.offsetX}
+ onReset={() => offsetX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.offsetX })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: offsetX.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedOffsetX,
+ staticValue: bg.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX,
+ defaultValue: DEFAULT_TEXT_BACKGROUND.offsetX,
+ })}
/>
</SectionField>
- <SectionField label="Y-offset" className="w-1/2">
+ <SectionField
+ label="Y-offset"
+ className="w-1/2"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={offsetY.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle y-offset keyframe"
+ onToggle={offsetY.toggleKeyframe}
+ />
+ }
+ >
<NumberField
icon="Y"
value={offsetY.displayValue}
@@ -603,24 +683,28 @@ function BackgroundSection({
onBlur={offsetY.onBlur}
onScrub={offsetY.scrubTo}
onScrubEnd={offsetY.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: { ...element.background, offsetY: DEFAULT_TEXT_BACKGROUND.offsetY },
- },
- },
- ],
- })
- }
- isDefault={(element.background.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY) === DEFAULT_TEXT_BACKGROUND.offsetY}
+ onReset={() => offsetY.commitValue({ value: DEFAULT_TEXT_BACKGROUND.offsetY })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: offsetY.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedOffsetY,
+ staticValue: bg.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY,
+ defaultValue: DEFAULT_TEXT_BACKGROUND.offsetY,
+ })}
/>
</SectionField>
</div>
- <SectionField label="Corner radius">
+ <SectionField
+ label="Corner radius"
+ beforeLabel={
+ <KeyframeToggle
+ isActive={cornerRadius.isKeyframedAtTime}
+ isDisabled={!isPlayheadWithinElementRange}
+ title="Toggle corner radius keyframe"
+ onToggle={cornerRadius.toggleKeyframe}
+ />
+ }
+ >
<NumberField
icon="R"
value={cornerRadius.displayValue}
@@ -631,25 +715,14 @@ function BackgroundSection({
onBlur={cornerRadius.onBlur}
onScrub={cornerRadius.scrubTo}
onScrubEnd={cornerRadius.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: {
- ...element.background,
- cornerRadius: CORNER_RADIUS_MIN,
- },
- },
- },
- ],
- })
- }
- isDefault={
- (element.background.cornerRadius ?? 0) === CORNER_RADIUS_MIN
- }
+ onReset={() => cornerRadius.commitValue({ value: CORNER_RADIUS_MIN })}
+ isDefault={isPropertyAtDefault({
+ hasAnimatedKeyframes: cornerRadius.hasAnimatedKeyframes,
+ isPlayheadWithinElementRange,
+ resolvedValue: resolvedCornerRadius,
+ staticValue: bg.cornerRadius ?? CORNER_RADIUS_MIN,
+ defaultValue: CORNER_RADIUS_MIN,
+ })}
/>
</SectionField>
</SectionFields>