From 3e89d2998588f8e35c524e523ba2ee853f0f6475 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 20 Apr 2026 11:31:17 +0200 Subject: refactor: restructure files to their domains, new preview overlay system, and dep graph --- .../editor/panels/properties/tabs/masks-tab.tsx | 826 --------------------- 1 file changed, 826 deletions(-) delete mode 100644 apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx (limited to 'apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx') diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx deleted file mode 100644 index 090fce64..00000000 --- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx +++ /dev/null @@ -1,826 +0,0 @@ -"use client"; - -import type { MaskableElement } from "@/lib/timeline"; -import type { Mask, MaskType, TextMask } from "@/lib/masks/types"; -import type { - NumberParamDefinition, - SelectParamDefinition, -} from "@/lib/params"; -import { masksRegistry, buildDefaultMaskInstance } from "@/lib/masks"; -import { useEditor } from "@/hooks/use-editor"; -import { useElementPreview } from "@/hooks/use-element-preview"; -import { useMenuPreview } from "@/hooks/use-menu-preview"; -import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { - ArrowExpandIcon, - Delete02Icon, - FeatherIcon, - PlusSignIcon, - RotateClockwiseIcon, - TextFontIcon, -} from "@hugeicons/core-free-icons"; -import { useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { ColorPicker } from "@/components/ui/color-picker"; -import { FontPicker } from "@/components/ui/font-picker"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { NumberField } from "@/components/ui/number-field"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { - clamp, - formatNumberForDisplay, - getFractionDigitsForStep, - snapToStep, -} from "@/utils/math"; -import { - Section, - SectionContent, - SectionField, - SectionFields, - SectionHeader, - SectionTitle, -} from "@/components/section"; -import { usePropertyDraft } from "../hooks/use-property-draft"; -import { - OcMirrorIcon, - OcShapesIcon, - OcTextHeightIcon, - OcTextWidthIcon, -} from "@/components/icons"; -import { cn } from "@/utils/ui"; - -type MasksTabProps = { - element: MaskableElement; - trackId: string; -}; - -type MaskItemProps = { - trackId: string; - elementId: string; - mask: Mask; - previewParam: (key: string) => (value: number | string | boolean) => void; - onCommit: () => void; -}; - -type EmptyViewProps = { - onAddMask: () => void; -}; - -type PreviewParamHandler = ( - key: string, -) => (value: number | string | boolean) => void; - -type RegisteredMaskDefinition = ReturnType<(typeof masksRegistry)["get"]>; - -function isTextMask(mask: Mask): mask is TextMask { - return mask.type === "text"; -} - -export function MasksTab({ element, trackId }: MasksTabProps) { - const editor = useEditor(); - const { renderElement, previewUpdates, commit } = - useElementPreview({ - trackId, - elementId: element.id, - fallback: element, - }); - const maskDefs = masksRegistry.getAll(); - const tracks = useEditor( - (e) => e.timeline.getPreviewTracks() ?? e.scenes.getActiveScene().tracks, - ); - const currentTime = useEditor((e) => e.playback.getCurrentTime()); - const mediaAssets = useEditor((e) => e.media.getAssets()); - const canvasSize = useEditor( - (e) => e.project.getActive().settings.canvasSize, - ); - const masks = element.masks ?? []; - const renderMasks = renderElement.masks ?? masks; - const hasMask = masks.length > 0; - const { onPointerLeave, onOpenChange, markCommitted } = useMenuPreview(); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const elementBounds = useMemo(() => { - const clampedTime = Math.min( - Math.max(currentTime, element.startTime), - element.startTime + element.duration - 1, - ); - - return ( - getVisibleElementsWithBounds({ - tracks, - currentTime: clampedTime, - canvasSize, - mediaAssets, - }).find( - (item) => item.trackId === trackId && item.elementId === element.id, - )?.bounds ?? null - ); - }, [ - canvasSize, - currentTime, - element.duration, - element.id, - element.startTime, - mediaAssets, - trackId, - tracks, - ]); - - const handleDropdownOpenChange = (open: boolean) => { - if (hasMask && open) return; - setIsDropdownOpen(open); - onOpenChange(open); - }; - - const previewMask = ({ maskType }: { maskType: MaskType }) => { - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - masks: [ - buildDefaultMaskInstance({ - maskType, - elementSize: elementBounds - ? { - width: elementBounds.width, - height: elementBounds.height, - } - : undefined, - }), - ], - } as Partial, - }, - ], - }); - }; - - const commitMask = ({ maskType }: { maskType: MaskType }) => { - if (editor.timeline.isPreviewActive()) { - editor.timeline.commitPreview(); - } else { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - patch: { - masks: [ - buildDefaultMaskInstance({ - maskType, - elementSize: elementBounds - ? { - width: elementBounds.width, - height: elementBounds.height, - } - : undefined, - }), - ], - } as Partial, - }, - ], - }); - } - markCommitted(); - setIsDropdownOpen(false); - }; - - const previewMaskParam = - ({ index, key }: { index: number; key: string }) => - (value: number | string | boolean) => { - if (!renderMasks[index]) { - return; - } - - const updatedMasks = renderMasks.map((existingMask, maskIndex) => - maskIndex !== index - ? existingMask - : { - ...existingMask, - params: { - ...existingMask.params, - [key]: value, - }, - }, - ); - - previewUpdates({ masks: updatedMasks } as Partial); - }; - - return ( -
-
- Masks - - {hasMask ? ( - - - - - - - - Only one mask is supported right now. If you need more, - duplicate the clip and apply a different mask to each copy. - - - ) : ( - - - - )} - - {maskDefs.map((definition) => ( - - previewMask({ maskType: definition.type }) - } - onClick={() => commitMask({ maskType: definition.type })} - > - - {definition.name} - - ))} - - -
- - {masks.length === 0 ? ( - setIsDropdownOpen(true)} /> - ) : ( - masks.map((mask, index) => ( - - previewMaskParam({ index, key: paramKey }) - } - onCommit={commit} - /> - )) - )} -
- ); -} - -function MaskItem({ - trackId, - elementId, - mask, - previewParam, - onCommit, -}: MaskItemProps) { - const editor = useEditor(); - const definition = masksRegistry.get(mask.type); - - return ( -
- - - - - } - > -
- - - {definition.name} - -
-
- - - -
- ); -} - -function MaskParamsFields({ - mask, - definition, - previewParam, - onCommit, -}: { - mask: Mask; - definition: RegisteredMaskDefinition; - previewParam: PreviewParamHandler; - onCommit: () => void; -}) { - const featherParam = getNumberParamDefinition({ - definition, - key: "feather", - }); - const strokeWidthParam = getNumberParamDefinition({ - definition, - key: "strokeWidth", - }); - const previewNumberParam = (key: string) => (value: number) => - previewParam(key)(value); - const previewStrokeColor = previewParam("strokeColor"); - const strokeAlignParam = definition.params.find( - (param): param is SelectParamDefinition => - param.key === "strokeAlign" && param.type === "select", - ); - - return ( - - {isTextMask(mask) ? ( - - ) : null} - {definition.features.hasPosition && - "centerX" in mask.params && - "centerY" in mask.params && ( - -
- - -
-
- )} - - {definition.features.sizeMode === "width-height" && - "width" in mask.params && - "height" in mask.params && ( - -
- - -
-
- )} - - {definition.features.sizeMode === "height-only" && - "height" in mask.params && ( - - - - )} - - {definition.features.sizeMode === "width-only" && - "width" in mask.params && ( - - - - )} - - {definition.features.sizeMode === "uniform" && "scale" in mask.params && ( - - : "S" - } - param={getNumberParamDefinition({ - definition, - key: "scale", - })} - value={getMaskNumber({ - params: mask.params, - key: "scale", - })} - onPreview={previewNumberParam("scale")} - onCommit={onCommit} - /> - - )} - - {definition.features.hasRotation && "rotation" in mask.params && ( - - } - param={getNumberParamDefinition({ - definition, - key: "rotation", - })} - value={getMaskNumber({ - params: mask.params, - key: "rotation", - })} - onPreview={previewNumberParam("rotation")} - onCommit={onCommit} - /> - - )} - - - } - param={featherParam} - value={getMaskNumber({ - params: mask.params, - key: "feather", - })} - onPreview={previewNumberParam("feather")} - onCommit={onCommit} - /> - - - -
-
- - previewStrokeColor(`#${color}`)} - onChangeEnd={(color) => { - previewStrokeColor(`#${color}`); - onCommit(); - }} - /> -
- {strokeAlignParam ? ( - - ) : null} -
-
-
- ); -} - -const LETTER_SPACING_PARAM: NumberParamDefinition = { - key: "letterSpacing", - label: "Letter spacing", - type: "number", - default: 0, - min: -100, - max: 500, - step: 1, -}; - -const LINE_HEIGHT_PARAM: NumberParamDefinition = { - key: "lineHeight", - label: "Line height", - type: "number", - default: 1.2, - min: 0.1, - max: 10, - step: 0.1, -}; - -function TextMaskFields({ - mask, - previewParam, - onCommit, - fontSizeParam, -}: { - mask: TextMask; - previewParam: PreviewParamHandler; - onCommit: () => void; - fontSizeParam: NumberParamDefinition; -}) { - const content = usePropertyDraft({ - displayValue: mask.params.content, - parse: (input) => input, - onPreview: (value) => previewParam("content")(value), - onCommit, - }); - - const previewNumberParam = (key: string) => (value: number) => - previewParam(key)(value); - - return ( - <> - -