import { useEditor } from "@/editor/use-editor"; import { clamp } from "@/utils/math"; import { NumberField } from "@/components/ui/number-field"; import { OcCheckerboardIcon } from "@/components/icons"; import { Fragment, useRef } from "react"; import { useMenuPreview } from "@/editor/use-menu-preview"; import { Section, SectionContent, SectionField, SectionHeader, SectionTitle, } from "@/components/section"; import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue, } from "@/components/ui/select"; import type { BlendMode } from "@/rendering"; import type { ElementType } from "@/timeline"; import type { ElementAnimations } from "@/animation/types"; import { HugeiconsIcon } from "@hugeicons/react"; import { RainDropIcon } from "@hugeicons/core-free-icons"; import { KeyframeToggle } from "@/components/editor/panels/properties/components/keyframe-toggle"; import { useKeyframedNumberProperty } from "@/components/editor/panels/properties/hooks/use-keyframed-number-property"; import { useElementPlayhead } from "@/components/editor/panels/properties/hooks/use-element-playhead"; import { resolveOpacityAtTime } from "@/animation/values"; import { DEFAULTS } from "@/timeline/defaults"; import { isPropertyAtDefault } from "./transform-tab"; import type { MediaTime } from "@/wasm"; type BlendingElement = { id: string; opacity: number; type: ElementType; blendMode?: BlendMode; startTime: MediaTime; duration: MediaTime; animations?: ElementAnimations; }; const BLEND_MODE_GROUPS: { value: BlendMode; label: string }[][] = [ [{ value: "normal", label: "Normal" }], [ { value: "darken", label: "Darken" }, { value: "multiply", label: "Multiply" }, { value: "color-burn", label: "Color Burn" }, ], [ { value: "lighten", label: "Lighten" }, { value: "screen", label: "Screen" }, { value: "plus-lighter", label: "Plus Lighter" }, { value: "color-dodge", label: "Color Dodge" }, ], [ { value: "overlay", label: "Overlay" }, { value: "soft-light", label: "Soft Light" }, { value: "hard-light", label: "Hard Light" }, ], [ { value: "difference", label: "Difference" }, { value: "exclusion", label: "Exclusion" }, ], [ { value: "hue", label: "Hue" }, { value: "saturation", label: "Saturation" }, { value: "color", label: "Color" }, { value: "luminosity", label: "Luminosity" }, ], ]; export function BlendingTab({ element, trackId, }: { element: BlendingElement; trackId: string; }) { const editor = useEditor(); const isPreviewActive = useEditor((e) => e.timeline.isPreviewActive()); const blendMode = element.blendMode ?? DEFAULTS.element.blendMode; const committedBlendModeRef = useRef(blendMode); if (!isPreviewActive) { committedBlendModeRef.current = blendMode; } const { onPointerLeave, onOpenChange: handleBlendModeOpenChange, markCommitted, } = useMenuPreview(); const previewBlendMode = ({ value }: { value: BlendMode }) => editor.timeline.previewElements({ updates: [ { trackId, elementId: element.id, updates: { blendMode: value } }, ], }); const commitBlendMode = (value: BlendMode) => { if (editor.timeline.isPreviewActive()) { editor.timeline.commitPreview(); } else { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, patch: { blendMode: value }, }, ], }); } markCommitted(); }; 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, step: 0.01, buildBaseUpdates: ({ value }) => ({ opacity: value }), }); return (
Blending
} > } 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: DEFAULTS.element.opacity }) } isDefault={isPropertyAtDefault({ hasAnimatedKeyframes: opacity.hasAnimatedKeyframes, isPlayheadWithinElementRange, resolvedValue: resolvedOpacity, staticValue: element.opacity, defaultValue: DEFAULTS.element.opacity, })} dragSensitivity="slow" />
); }