diff options
Diffstat (limited to 'apps/web/src/components/editor/panels/properties/sections')
3 files changed, 0 insertions, 606 deletions
diff --git a/apps/web/src/components/editor/panels/properties/sections/blending.tsx b/apps/web/src/components/editor/panels/properties/sections/blending.tsx deleted file mode 100644 index a50e84e7..00000000 --- a/apps/web/src/components/editor/panels/properties/sections/blending.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useEditor } from "@/hooks/use-editor"; -import { clamp } from "@/utils/math"; -import { NumberField } from "@/components/ui/number-field"; -import { - DEFAULT_BLEND_MODE, - DEFAULT_OPACITY, -} from "@/constants/timeline-constants"; -import { OcCheckerboardIcon } from "@opencut/ui/icons"; -import { Fragment, useRef } from "react"; -import { Section, SectionContent, SectionField, SectionHeader, SectionTitle } from "../section"; -import { - Select, - SelectContent, - SelectItem, - SelectSeparator, - SelectTrigger, - SelectValue, -} 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 = [ - [{ value: "normal", label: "Normal" }], - [ - { value: "darken", label: "Darken" }, - { value: "multiply", label: "Multiply" }, - { value: "color-burn", label: "Color Burn" }, - ], - [ - { value: "lighten", label: "Lighten" }, - { value: "screen", label: "Screen" }, - { value: "plus-lighter", label: "Plus Lighter" }, - { value: "color-dodge", label: "Color Dodge" }, - ], - [ - { value: "overlay", label: "Overlay" }, - { value: "soft-light", label: "Soft Light" }, - { value: "hard-light", label: "Hard Light" }, - ], - [ - { value: "difference", label: "Difference" }, - { value: "exclusion", label: "Exclusion" }, - ], - [ - { value: "hue", label: "Hue" }, - { value: "saturation", label: "Saturation" }, - { value: "color", label: "Color" }, - { value: "luminosity", label: "Luminosity" }, - ], -]; - -export function BlendingSection({ - element, - trackId, -}: { - element: BlendingElement; - trackId: string; -}) { - const editor = useEditor(); - const blendMode = element.blendMode ?? DEFAULT_BLEND_MODE; - const didSelectRef = useRef(false); - const committedBlendModeRef = useRef(blendMode); - if (!editor.timeline.isPreviewActive()) { - committedBlendModeRef.current = blendMode; - } - - const previewBlendMode = ({ value }: { value: BlendMode }) => - editor.timeline.previewElements({ - updates: [ - { trackId, elementId: element.id, updates: { blendMode: value } }, - ], - }); - - const commitBlendMode = (value: string) => { - if (editor.timeline.isPreviewActive()) { - editor.timeline.commitPreview(); - } else { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { blendMode: value as BlendMode }, - }, - ], - }); - } - didSelectRef.current = true; - }; - - const handleBlendModeOpenChange = (isOpen: boolean) => { - if (!isOpen) { - if (!didSelectRef.current) editor.timeline.discardPreview(); - didSelectRef.current = false; - } - }; - - 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; - }, - valueAtPlayhead: resolvedOpacity, - buildBaseUpdates: ({ value }) => ({ opacity: value }), - }); - - return ( - <Section collapsible sectionKey={`${element.type}:blending`}> - <SectionHeader><SectionTitle>Blending</SectionTitle></SectionHeader> - <SectionContent> - <div className="flex items-start gap-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={ - <OcCheckerboardIcon className="size-3.5 text-muted-foreground" /> - } - value={opacity.displayValue} - min={0} - max={100} - onFocus={opacity.onFocus} - onChange={opacity.onChange} - onBlur={opacity.onBlur} - onScrub={opacity.scrubTo} - onScrubEnd={opacity.commitScrub} - onReset={() => opacity.commitValue({ value: DEFAULT_OPACITY })} - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: opacity.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedOpacity, - staticValue: element.opacity, - defaultValue: DEFAULT_OPACITY, - })} - dragSensitivity="slow" - /> - </SectionField> - <SectionField label="Blend mode" className="w-1/2"> - <Select - value={committedBlendModeRef.current} - onOpenChange={handleBlendModeOpenChange} - onValueChange={commitBlendMode} - > - <SelectTrigger - icon={<HugeiconsIcon icon={RainDropIcon} />} - className="w-full" - > - <SelectValue placeholder="Select blend mode" /> - </SelectTrigger> - <SelectContent className="w-36"> - {BLEND_MODE_GROUPS.map((group, groupIndex) => ( - <Fragment key={group[0]?.value ?? `group-${groupIndex}`}> - {group.map((option) => ( - <SelectItem - key={option.value} - value={option.value} - onPointerEnter={() => - previewBlendMode({ value: option.value as BlendMode }) - } - > - {option.label} - </SelectItem> - ))} - {groupIndex < BLEND_MODE_GROUPS.length - 1 ? ( - <SelectSeparator /> - ) : null} - </Fragment> - ))} - </SelectContent> - </Select> - </SectionField> - </div> - </SectionContent> - </Section> - ); -} diff --git a/apps/web/src/components/editor/panels/properties/sections/index.tsx b/apps/web/src/components/editor/panels/properties/sections/index.tsx deleted file mode 100644 index 8be58f39..00000000 --- a/apps/web/src/components/editor/panels/properties/sections/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./transform"; -export * from "./blending"; diff --git a/apps/web/src/components/editor/panels/properties/sections/transform.tsx b/apps/web/src/components/editor/panels/properties/sections/transform.tsx deleted file mode 100644 index 6aaaa1fe..00000000 --- a/apps/web/src/components/editor/panels/properties/sections/transform.tsx +++ /dev/null @@ -1,382 +0,0 @@ -import { NumberField } from "@/components/ui/number-field"; -import { useEditor } from "@/hooks/use-editor"; -import { clamp, isNearlyEqual } from "@/utils/math"; -import type { AnimationPropertyPath } from "@/types/animation"; -import type { VisualElement } from "@/types/timeline"; -import { - Section, - SectionContent, - SectionField, - SectionFields, - SectionHeader, - SectionTitle, -} from "../section"; -import { Button } from "@/components/ui/button"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { - ArrowExpandIcon, - Link05Icon, - RotateClockwiseIcon, -} from "@hugeicons/core-free-icons"; -import { useState } from "react"; -import { DEFAULT_TRANSFORM } from "@/constants/timeline-constants"; -import { TIME_EPSILON_SECONDS } from "@/constants/animation-constants"; -import { getElementLocalTime, resolveTransformAtTime } from "@/lib/animation"; -import { KeyframeToggle } from "../keyframe-toggle"; -import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; - -export function parseNumericInput({ input }: { input: string }): number | null { - const parsed = parseFloat(input); - return Number.isNaN(parsed) ? null : parsed; -} - -export function isPropertyAtDefault({ - hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue, - staticValue, - defaultValue, -}: { - hasAnimatedKeyframes: boolean; - isPlayheadWithinElementRange: boolean; - resolvedValue: number; - staticValue: number; - defaultValue: number; -}): boolean { - if (hasAnimatedKeyframes && isPlayheadWithinElementRange) { - return isNearlyEqual({ - leftValue: resolvedValue, - rightValue: defaultValue, - }); - } - - return staticValue === defaultValue; -} - -export function TransformSection({ - element, - trackId, - showTopBorder = true, -}: { - element: VisualElement; - trackId: string; - showTopBorder?: boolean; -}) { - const editor = useEditor(); - const [isScaleLocked, setIsScaleLocked] = useState(false); - const playheadTime = editor.playback.getCurrentTime(); - const localTime = getElementLocalTime({ - timelineTime: playheadTime, - elementStartTime: element.startTime, - elementDuration: element.duration, - }); - const resolvedTransform = resolveTransformAtTime({ - baseTransform: element.transform, - animations: element.animations, - localTime, - }); - const isPlayheadWithinElementRange = - playheadTime >= element.startTime - TIME_EPSILON_SECONDS && - playheadTime <= element.startTime + element.duration + TIME_EPSILON_SECONDS; - - const positionX = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "transform.position.x", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedTransform.position.x).toString(), - parse: (input) => parseNumericInput({ input }), - valueAtPlayhead: resolvedTransform.position.x, - buildBaseUpdates: ({ value }) => ({ - transform: { - ...element.transform, - position: { - ...element.transform.position, - x: value, - }, - }, - }), - }); - - const positionY = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "transform.position.y", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedTransform.position.y).toString(), - parse: (input) => parseNumericInput({ input }), - valueAtPlayhead: resolvedTransform.position.y, - buildBaseUpdates: ({ value }) => ({ - transform: { - ...element.transform, - position: { - ...element.transform.position, - y: value, - }, - }, - }), - }); - - const scale = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "transform.scale", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedTransform.scale * 100).toString(), - parse: (input) => { - const parsed = parseNumericInput({ input }); - if (parsed === null) return null; - return Math.max(parsed, 1) / 100; - }, - valueAtPlayhead: resolvedTransform.scale, - buildBaseUpdates: ({ value }) => ({ - transform: { - ...element.transform, - scale: value, - }, - }), - }); - const scaleFieldProps = { - className: "flex-1", - value: scale.displayValue, - onFocus: scale.onFocus, - onChange: scale.onChange, - onBlur: scale.onBlur, - dragSensitivity: "slow" as const, - onScrub: scale.scrubTo, - onScrubEnd: scale.commitScrub, - onReset: () => scale.commitValue({ value: DEFAULT_TRANSFORM.scale }), - isDefault: isPropertyAtDefault({ - hasAnimatedKeyframes: scale.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedTransform.scale, - staticValue: element.transform.scale, - defaultValue: DEFAULT_TRANSFORM.scale, - }), - }; - - const rotation = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "transform.rotate", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedTransform.rotate).toString(), - parse: (input) => { - const parsed = parseNumericInput({ input }); - if (parsed === null) return null; - return clamp({ value: parsed, min: -360, max: 360 }); - }, - valueAtPlayhead: resolvedTransform.rotate, - buildBaseUpdates: ({ value }) => ({ - transform: { - ...element.transform, - rotate: value, - }, - }), - }); - - const hasPositionKeyframe = - positionX.isKeyframedAtTime || positionY.isKeyframedAtTime; - - const togglePositionKeyframe = () => { - if (!isPlayheadWithinElementRange) { - return; - } - - if (positionX.keyframeIdAtTime || positionY.keyframeIdAtTime) { - const keyframesToRemove: Array<{ - trackId: string; - elementId: string; - propertyPath: AnimationPropertyPath; - keyframeId: string; - }> = []; - if (positionX.keyframeIdAtTime) { - keyframesToRemove.push({ - trackId, - elementId: element.id, - propertyPath: "transform.position.x" as const, - keyframeId: positionX.keyframeIdAtTime, - }); - } - if (positionY.keyframeIdAtTime) { - keyframesToRemove.push({ - trackId, - elementId: element.id, - propertyPath: "transform.position.y" as const, - keyframeId: positionY.keyframeIdAtTime, - }); - } - - editor.timeline.removeKeyframes({ - keyframes: keyframesToRemove, - }); - return; - } - - editor.timeline.upsertKeyframes({ - keyframes: [ - { - trackId, - elementId: element.id, - propertyPath: "transform.position.x", - time: localTime, - value: resolvedTransform.position.x, - }, - { - trackId, - elementId: element.id, - propertyPath: "transform.position.y", - time: localTime, - value: resolvedTransform.position.y, - }, - ], - }); - }; - - return ( - <Section - collapsible - sectionKey={`${element.type}:transform`} - showTopBorder={showTopBorder} - > - <SectionHeader><SectionTitle>Transform</SectionTitle></SectionHeader> - <SectionContent> - <SectionFields> - <SectionField - label="Scale" - beforeLabel={ - <KeyframeToggle - isActive={scale.isKeyframedAtTime} - isDisabled={!isPlayheadWithinElementRange} - title="Toggle scale keyframe" - onToggle={scale.toggleKeyframe} - /> - } - > - <div className="flex items-center gap-2"> - {isScaleLocked ? ( - <> - <NumberField icon="W" {...scaleFieldProps} /> - <NumberField icon="H" {...scaleFieldProps} /> - </> - ) : ( - <NumberField - icon={<HugeiconsIcon icon={ArrowExpandIcon} />} - {...scaleFieldProps} - className="flex-1" - /> - )} - <Button - type="button" - variant={isScaleLocked ? "secondary" : "ghost"} - size="icon" - aria-pressed={isScaleLocked} - onClick={() => setIsScaleLocked((isLocked) => !isLocked)} - > - <HugeiconsIcon icon={Link05Icon} /> - </Button> - </div> - </SectionField> - <SectionField - label="Position" - beforeLabel={ - <KeyframeToggle - isActive={hasPositionKeyframe} - isDisabled={!isPlayheadWithinElementRange} - title="Toggle position keyframe" - onToggle={togglePositionKeyframe} - /> - } - > - <div className="flex items-center gap-2"> - <NumberField - icon="X" - className="flex-1" - value={positionX.displayValue} - onFocus={positionX.onFocus} - onChange={positionX.onChange} - onBlur={positionX.onBlur} - onScrub={positionX.scrubTo} - onScrubEnd={positionX.commitScrub} - onReset={() => - positionX.commitValue({ value: DEFAULT_TRANSFORM.position.x }) - } - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: positionX.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedTransform.position.x, - staticValue: element.transform.position.x, - defaultValue: DEFAULT_TRANSFORM.position.x, - })} - /> - <NumberField - icon="Y" - className="flex-1" - value={positionY.displayValue} - onFocus={positionY.onFocus} - onChange={positionY.onChange} - onBlur={positionY.onBlur} - onScrub={positionY.scrubTo} - onScrubEnd={positionY.commitScrub} - onReset={() => - positionY.commitValue({ value: DEFAULT_TRANSFORM.position.y }) - } - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: positionY.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedTransform.position.y, - staticValue: element.transform.position.y, - defaultValue: DEFAULT_TRANSFORM.position.y, - })} - /> - </div> - </SectionField> - - <SectionField - label="Rotation" - beforeLabel={ - <KeyframeToggle - isActive={rotation.isKeyframedAtTime} - isDisabled={!isPlayheadWithinElementRange} - title="Toggle rotation keyframe" - onToggle={rotation.toggleKeyframe} - /> - } - > - <div className="flex items-center gap-2"> - <NumberField - icon={<HugeiconsIcon icon={RotateClockwiseIcon} />} - className="flex-none" - value={rotation.displayValue} - onFocus={rotation.onFocus} - onChange={rotation.onChange} - onBlur={rotation.onBlur} - dragSensitivity="slow" - onScrub={rotation.scrubTo} - onScrubEnd={rotation.commitScrub} - onReset={() => - rotation.commitValue({ value: DEFAULT_TRANSFORM.rotate }) - } - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: rotation.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedTransform.rotate, - staticValue: element.transform.rotate, - defaultValue: DEFAULT_TRANSFORM.rotate, - })} - /> - </div> - </SectionField> - </SectionFields> - </SectionContent> - </Section> - ); -} |
