From 9a3d9c715b032723034a83dddc516301e2d53894 Mon Sep 17 00:00:00 2001 From: xo-o Date: Sat, 25 Apr 2026 17:15:38 -0500 Subject: [PATCH] adjust color --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/app/globals.css | 1 + .../color-adjusment/adjusment-basic.tsx | 501 ++++++++++ .../color-adjusment/adjusment-curves.tsx | 864 ++++++++++++++++++ .../color-adjusment/adjusment-hsl.tsx | 291 ++++++ .../color-adjusment/index.tsx | 3 + .../floating-controls/color-adjustment.tsx | 54 ++ .../floating-controls/floating-control.tsx | 5 + .../properties-panel/caption-properties.tsx | 8 +- .../properties-panel/image-properties.tsx | 24 +- .../properties-panel/video-properties.tsx | 24 +- 12 files changed, 1778 insertions(+), 9 deletions(-) create mode 100644 src/components/editor/floating-controls/color-adjusment/adjusment-basic.tsx create mode 100644 src/components/editor/floating-controls/color-adjusment/adjusment-curves.tsx create mode 100644 src/components/editor/floating-controls/color-adjusment/adjusment-hsl.tsx create mode 100644 src/components/editor/floating-controls/color-adjusment/index.tsx create mode 100644 src/components/editor/floating-controls/color-adjustment.tsx diff --git a/package.json b/package.json index 681084d..e35deb3 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "motion": "^12.23.26", "next": "16.0.7", "next-themes": "^0.4.6", - "openvideo": "^0.2.15", + "openvideo": "^0.2.17", "opfs-tools": "^0.7.2", "pg": "^8.20.0", "pixi.js": "^8.14.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb0aec8..954cea7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,8 +123,8 @@ importers: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) openvideo: - specifier: ^0.2.15 - version: 0.2.15(@types/react@19.2.8)(react@19.2.0)(yoga-layout@3.2.1) + specifier: ^0.2.17 + version: 0.2.17(@types/react@19.2.8)(react@19.2.0)(yoga-layout@3.2.1) opfs-tools: specifier: ^0.7.2 version: 0.7.4 @@ -4828,8 +4828,8 @@ packages: resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} engines: {node: '>=0.10'} - openvideo@0.2.15: - resolution: {integrity: sha512-oknQwt3I8IMxhxrZQpbZs7RNWR74T4SVCyeBJKW9XMJDJON1YHZ0Uo5jvp68ibGDpRPb9hi/vqYNMlF4QROPIw==} + openvideo@0.2.17: + resolution: {integrity: sha512-uT4ny3EKHOphmNZSzJupvaqUVYHRwr9bAKYwyJ16oM6471fBaylWEEPXzcj5AlZl/81rX/zcD+rXMJY073BdtA==} opfs-tools@0.7.4: resolution: {integrity: sha512-DJgQnXyPcCj3q8pKa8SjP4HfNBPffj8kO6W1nc2zQydTqu5G/AxOuIjmWnRxZ84nE1EOpzZzhj8MYCZP1jRR9A==} @@ -11513,7 +11513,7 @@ snapshots: opentracing@0.14.7: {} - openvideo@0.2.15(@types/react@19.2.8)(react@19.2.0)(yoga-layout@3.2.1): + openvideo@0.2.17(@types/react@19.2.8)(react@19.2.0)(yoga-layout@3.2.1): dependencies: '@pixi/layout': 3.2.0(@types/react@19.2.8)(pixi.js@8.16.0)(react@19.2.0)(yoga-layout@3.2.1) gl-transitions: 1.43.0 diff --git a/src/app/globals.css b/src/app/globals.css index 9e3d873..64b7049 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -47,6 +47,7 @@ } :root { + --radius: 0.65rem; --background: oklch(1 0 0); --foreground: oklch(0 0 0); --card: oklch(1 0 0); diff --git a/src/components/editor/floating-controls/color-adjusment/adjusment-basic.tsx b/src/components/editor/floating-controls/color-adjusment/adjusment-basic.tsx new file mode 100644 index 0000000..27afcc7 --- /dev/null +++ b/src/components/editor/floating-controls/color-adjusment/adjusment-basic.tsx @@ -0,0 +1,501 @@ +import React, { useState, useMemo, useCallback, useEffect } from "react"; +import { Slider } from "@/components/ui/slider"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import useLayoutStore from "../../store/use-layout-store"; +import { useStudioStore } from "@/stores/studio-store"; + +type ColorAdjustment = "saturation" | "temperature" | "hue"; +type LightAdjustment = "brightness" | "contrast" | "shine" | "highlight" | "shadow"; +type EffectsAdjustment = "sharpness" | "vignette" | "fade" | "grain"; + +interface ColorValue { + saturation: number; + temperature: number; + hue: number; +} + +interface LightValue { + brightness: number; + contrast: number; + shine: number; + highlight: number; + shadow: number; +} + +interface EffectsValue { + sharpness: number; + vignette: number; + fade: number; + grain: number; +} + +const AdjusmentBasic = () => { + const { floatingControlData } = useLayoutStore(); + const { studio } = useStudioStore(); + const { clipId } = floatingControlData || {}; + const clip = studio?.getClipById(clipId) as any; + + const [colorValue, setColorValue] = useState({ + saturation: clip?.colorAdjustment?.basic?.saturation ?? 0, + temperature: clip?.colorAdjustment?.basic?.temperature ?? 0, + hue: clip?.colorAdjustment?.basic?.hue ?? 0, + }); + + const [lightValue, setLightValue] = useState({ + brightness: clip?.colorAdjustment?.basic?.brightness ?? 0, + contrast: clip?.colorAdjustment?.basic?.contrast ?? 0, + shine: clip?.colorAdjustment?.basic?.shine ?? 0, + highlight: clip?.colorAdjustment?.basic?.highlight ?? 0, + shadow: clip?.colorAdjustment?.basic?.shadow ?? 0, + }); + + const [effectsValue, setEffectsValue] = useState({ + sharpness: clip?.colorAdjustment?.basic?.sharpness ?? 0, + vignette: clip?.colorAdjustment?.basic?.vignette ?? 0, + fade: clip?.colorAdjustment?.basic?.fade ?? 0, + grain: clip?.colorAdjustment?.basic?.grain ?? 0, + }); + + // Update state when clip changes + useEffect(() => { + if (clip?.colorAdjustment?.basic) { + setColorValue({ + saturation: clip.colorAdjustment.basic.saturation ?? 0, + temperature: clip.colorAdjustment.basic.temperature ?? 0, + hue: clip.colorAdjustment.basic.hue ?? 0, + }); + setLightValue({ + brightness: clip.colorAdjustment.basic.brightness ?? 0, + contrast: clip.colorAdjustment.basic.contrast ?? 0, + shine: clip.colorAdjustment.basic.shine ?? 0, + highlight: clip.colorAdjustment.basic.highlight ?? 0, + shadow: clip.colorAdjustment.basic.shadow ?? 0, + }); + setEffectsValue({ + sharpness: clip.colorAdjustment.basic.sharpness ?? 0, + vignette: clip.colorAdjustment.basic.vignette ?? 0, + fade: clip.colorAdjustment.basic.fade ?? 0, + grain: clip.colorAdjustment.basic.grain ?? 0, + }); + } + }, [clip?.colorAdjustment?.basic]); + + // Update clip when values change + const updateClip = useCallback( + (updates: Partial) => { + if (!clip) return; + + const newColorValue = { ...colorValue, ...updates }; + setColorValue(newColorValue); + + clip.update({ + colorAdjustment: { + enabled: true, + type: "basic", + basic: { + ...(clip.colorAdjustment?.basic ?? {}), + ...newColorValue, + ...lightValue, + ...effectsValue, + }, + hsl: clip.colorAdjustment?.hsl, + curves: clip.colorAdjustment?.curves, + }, + }); + }, + [clip, colorValue, lightValue, effectsValue], + ); + + const updateLightClip = useCallback( + (updates: Partial) => { + if (!clip) return; + + const newLightValue = { ...lightValue, ...updates }; + setLightValue(newLightValue); + + clip.update({ + colorAdjustment: { + enabled: true, + type: "basic", + basic: { + ...(clip.colorAdjustment?.basic ?? {}), + ...colorValue, + ...newLightValue, + ...effectsValue, + }, + hsl: clip.colorAdjustment?.hsl, + curves: clip.colorAdjustment?.curves, + }, + }); + }, + [clip, lightValue, colorValue, effectsValue], + ); + + const updateEffectsClip = useCallback( + (updates: Partial) => { + if (!clip) return; + + const newEffectsValue = { ...effectsValue, ...updates }; + setEffectsValue(newEffectsValue); + + clip.update({ + colorAdjustment: { + enabled: true, + type: "basic", + basic: { + ...(clip.colorAdjustment?.basic ?? {}), + ...colorValue, + ...lightValue, + ...newEffectsValue, + }, + hsl: clip.colorAdjustment?.hsl, + curves: clip.colorAdjustment?.curves, + }, + }); + }, + [clip, effectsValue, colorValue, lightValue], + ); + + const min = -100; + const max = 100; + + const effectsMinMax = { + sharpness: { min: 0, max: 100 }, + vignette: { min: -100, max: 100 }, + fade: { min: 0, max: 100 }, + grain: { min: 0, max: 100 }, + }; + + const getRangeStyle = useCallback( + (value: number, customMin?: number, customMax?: number) => { + const effectiveMin = customMin ?? min; + const effectiveMax = customMax ?? max; + const effectiveRange = effectiveMax - effectiveMin; + const effectiveCenterPercent = Math.abs(effectiveMin) / effectiveRange; + + if (value >= 0) { + const width = (value / effectiveMax) * (1 - effectiveCenterPercent) * 100; + return { + left: `${effectiveCenterPercent * 100}%`, + width: `${width}%`, + }; + } else { + const width = (Math.abs(value) / Math.abs(effectiveMin)) * effectiveCenterPercent * 100; + return { + left: `${(effectiveCenterPercent - width / 100) * 100}%`, + width: `${width}%`, + }; + } + }, + [min, max], + ); + + const colorSliderStyles = useMemo( + () => ({ + saturation: getRangeStyle(colorValue.saturation), + temperature: getRangeStyle(colorValue.temperature), + hue: getRangeStyle(colorValue.hue), + }), + [colorValue, getRangeStyle], + ); + + const lightSliderStyles = useMemo( + () => ({ + brightness: getRangeStyle(lightValue.brightness), + contrast: getRangeStyle(lightValue.contrast), + shine: getRangeStyle(lightValue.shine), + highlight: getRangeStyle(lightValue.highlight), + shadow: getRangeStyle(lightValue.shadow), + }), + [lightValue, getRangeStyle], + ); + + const effectsSliderStyles = useMemo( + () => ({ + vignette: getRangeStyle( + effectsValue.vignette, + effectsMinMax.vignette.min, + effectsMinMax.vignette.max, + ), + }), + [effectsValue.vignette, getRangeStyle], + ); + + const handleColorSliderChange = useCallback( + (type: ColorAdjustment, value: number[]) => { + updateClip({ [type]: value[0] }); + }, + [updateClip], + ); + + const handleColorInputChange = useCallback( + (type: ColorAdjustment, value: string) => { + const numValue = Number(value); + if (!isNaN(numValue)) { + updateClip({ [type]: Math.min(max, Math.max(min, numValue)) }); + } + }, + [updateClip], + ); + + const handleLightSliderChange = useCallback( + (type: LightAdjustment, value: number[]) => { + updateLightClip({ [type]: value[0] }); + }, + [updateLightClip], + ); + + const handleLightInputChange = useCallback( + (type: LightAdjustment, value: string) => { + const numValue = Number(value); + if (!isNaN(numValue)) { + updateLightClip({ [type]: Math.min(max, Math.max(min, numValue)) }); + } + }, + [min, max, updateLightClip], + ); + + const handleEffectsSliderChange = useCallback( + (type: EffectsAdjustment, value: number[]) => { + updateEffectsClip({ [type]: value[0] }); + }, + [updateEffectsClip], + ); + + const handleEffectsInputChange = useCallback( + (type: EffectsAdjustment, value: string) => { + const numValue = Number(value); + if (!isNaN(numValue)) { + const { min: effectMin, max: effectMax } = effectsMinMax[type]; + updateEffectsClip({ + [type]: Math.min(effectMax, Math.max(effectMin, numValue)), + }); + } + }, + [updateEffectsClip], + ); + + const colorControls: Array<{ + id: ColorAdjustment; + label: string; + value: number; + }> = [ + { id: "saturation", label: "Saturation", value: colorValue.saturation }, + { id: "temperature", label: "Temperature", value: colorValue.temperature }, + { id: "hue", label: "Hue", value: colorValue.hue }, + ]; + + const lightControls: Array<{ + id: LightAdjustment; + label: string; + value: number; + }> = [ + { id: "brightness", label: "Brightness", value: lightValue.brightness }, + { id: "contrast", label: "Contrast", value: lightValue.contrast }, + { id: "shine", label: "Shine", value: lightValue.shine }, + { id: "highlight", label: "Highlight", value: lightValue.highlight }, + { id: "shadow", label: "Shadow", value: lightValue.shadow }, + ]; + + const effectsControls: Array<{ + id: EffectsAdjustment; + label: string; + value: number; + min: number; + max: number; + useCustomStyle?: boolean; + }> = [ + { + id: "sharpness", + label: "Sharpness", + value: effectsValue.sharpness, + min: 0, + max: 100, + useCustomStyle: false, + }, + { + id: "vignette", + label: "Vignette", + value: effectsValue.vignette, + min: -100, + max: 100, + useCustomStyle: true, + }, + { + id: "fade", + label: "Fade", + value: effectsValue.fade, + min: 0, + max: 100, + useCustomStyle: false, + }, + { + id: "grain", + label: "Grain", + value: effectsValue.grain, + min: 0, + max: 100, + useCustomStyle: false, + }, + ]; + + return ( + +
+ {/* COLOR SECTION */} +
+ + + {colorControls.map(({ id, label, value }) => ( +
+ {label} + +
+
+ + + handleColorSliderChange(id, newValue)} + min={min} + max={max} + step={1} + className={`slider-color-${id} w-full`} + /> +
+ + handleColorInputChange(id, e.target.value)} + min={min} + max={max} + step={1} + className="w-20 text-[10px]" + /> +
+
+ ))} +
+ + + + {/* LIGHTNESS SECTION */} +
+ + + {lightControls.map(({ id, label, value }) => ( +
+ {label} + +
+
+ + + handleLightSliderChange(id, newValue)} + min={min} + max={max} + step={1} + className={`slider-light-${id} w-full`} + /> +
+ + handleLightInputChange(id, e.target.value)} + min={min} + max={max} + step={1} + className="w-20 text-[10px]" + /> +
+
+ ))} +
+ + + + {/* EFFECTS SECTION */} +
+ + + {effectsControls.map( + ({ id, label, value, min: effectMin, max: effectMax, useCustomStyle }) => ( +
+ {label} + +
+
+ {useCustomStyle ? ( + <> + + handleEffectsSliderChange(id, newValue)} + min={effectMin} + max={effectMax} + step={1} + className={`slider-effects-${id} w-full`} + /> + + ) : ( + handleEffectsSliderChange(id, newValue)} + min={effectMin} + max={effectMax} + step={1} + className="w-full" + /> + )} +
+ + handleEffectsInputChange(id, e.target.value)} + min={effectMin} + max={effectMax} + step={1} + className="w-20 text-[10px]" + /> +
+
+ ), + )} +
+
+
+ ); +}; + +export default AdjusmentBasic; diff --git a/src/components/editor/floating-controls/color-adjusment/adjusment-curves.tsx b/src/components/editor/floating-controls/color-adjusment/adjusment-curves.tsx new file mode 100644 index 0000000..b5e1bef --- /dev/null +++ b/src/components/editor/floating-controls/color-adjusment/adjusment-curves.tsx @@ -0,0 +1,864 @@ +import React, { useState, useCallback, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { RotateCcw } from "lucide-react"; +import useLayoutStore from "../../store/use-layout-store"; +import { useStudioStore } from "@/stores/studio-store"; + +interface Point { + x: number; + y: number; +} + +interface Curve { + id: string; + name: string; + color: string; + points: Point[]; +} + +interface CurvePreset { + name: string; + curves: Partial>; +} + +const CURVE_PRESETS: Record = { + linear: { + name: "Linear", + curves: { + rgb: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + red: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + green: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + blue: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + }, + lighten: { + name: "Lighten", + curves: { + rgb: [ + { x: 0, y: 0 }, + { x: 0.45, y: 0.7 }, + { x: 1, y: 1 }, + ], + red: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.65 }, + { x: 1, y: 1 }, + ], + green: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.72 }, + { x: 1, y: 1 }, + ], + blue: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.75 }, + { x: 1, y: 1 }, + ], + }, + }, + darken: { + name: "Darken", + curves: { + rgb: [ + { x: 0, y: 0 }, + { x: 0.45, y: 0.3 }, + { x: 1, y: 1 }, + ], + red: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.28 }, + { x: 1, y: 1 }, + ], + green: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.33 }, + { x: 1, y: 1 }, + ], + blue: [ + { x: 0, y: 0 }, + { x: 0.5, y: 0.25 }, + { x: 1, y: 1 }, + ], + }, + }, + fade: { + name: "Fade", + curves: { + rgb: [ + { x: 0, y: 0.2 }, + { x: 0.5, y: 0.5 }, + { x: 1, y: 0.8 }, + ], + red: [ + { x: 0, y: 0.18 }, + { x: 0.5, y: 0.45 }, + { x: 1, y: 0.78 }, + ], + green: [ + { x: 0, y: 0.22 }, + { x: 0.5, y: 0.52 }, + { x: 1, y: 0.82 }, + ], + blue: [ + { x: 0, y: 0.24 }, + { x: 0.5, y: 0.55 }, + { x: 1, y: 0.85 }, + ], + }, + }, + contrast: { + name: "Contrast", + curves: { + rgb: [ + { x: 0, y: 0 }, + { x: 0.25, y: 0.1 }, + { x: 0.5, y: 0.55 }, + { x: 0.75, y: 0.9 }, + { x: 1, y: 1 }, + ], + red: [ + { x: 0, y: 0 }, + { x: 0.3, y: 0.08 }, + { x: 0.55, y: 0.52 }, + { x: 0.8, y: 0.92 }, + { x: 1, y: 1 }, + ], + green: [ + { x: 0, y: 0 }, + { x: 0.25, y: 0.12 }, + { x: 0.5, y: 0.55 }, + { x: 0.75, y: 0.88 }, + { x: 1, y: 1 }, + ], + blue: [ + { x: 0, y: 0 }, + { x: 0.28, y: 0.1 }, + { x: 0.52, y: 0.54 }, + { x: 0.78, y: 0.9 }, + { x: 1, y: 1 }, + ], + }, + }, + coolToWarm: { + name: "Cool to Warm", + curves: { + rgb: [ + { x: 0, y: 0.3 }, + { x: 0.3, y: 0.4 }, + { x: 0.5, y: 0.5 }, + { x: 0.7, y: 0.6 }, + { x: 1, y: 0.7 }, + ], + red: [ + { x: 0, y: 0.35 }, + { x: 0.5, y: 0.52 }, + { x: 1, y: 0.75 }, + ], + green: [ + { x: 0, y: 0.28 }, + { x: 0.5, y: 0.5 }, + { x: 1, y: 0.68 }, + ], + blue: [ + { x: 0, y: 0.25 }, + { x: 0.5, y: 0.48 }, + { x: 1, y: 0.7 }, + ], + }, + }, + warmToCool: { + name: "Warm to Cool", + curves: { + rgb: [ + { x: 0, y: 0.7 }, + { x: 0.3, y: 0.6 }, + { x: 0.5, y: 0.5 }, + { x: 0.7, y: 0.4 }, + { x: 1, y: 0.3 }, + ], + red: [ + { x: 0, y: 0.75 }, + { x: 0.5, y: 0.55 }, + { x: 1, y: 0.35 }, + ], + green: [ + { x: 0, y: 0.68 }, + { x: 0.5, y: 0.5 }, + { x: 1, y: 0.32 }, + ], + blue: [ + { x: 0, y: 0.7 }, + { x: 0.5, y: 0.45 }, + { x: 1, y: 0.28 }, + ], + }, + }, + vintage: { + name: "Vintage", + curves: { + rgb: [ + { x: 0, y: 0.1 }, + { x: 0.2, y: 0.25 }, + { x: 0.5, y: 0.5 }, + { x: 0.8, y: 0.75 }, + { x: 1, y: 0.9 }, + ], + red: [ + { x: 0, y: 0.08 }, + { x: 0.2, y: 0.22 }, + { x: 0.5, y: 0.5 }, + { x: 0.8, y: 0.72 }, + { x: 1, y: 0.88 }, + ], + green: [ + { x: 0, y: 0.12 }, + { x: 0.2, y: 0.28 }, + { x: 0.5, y: 0.52 }, + { x: 0.8, y: 0.78 }, + { x: 1, y: 0.92 }, + ], + blue: [ + { x: 0, y: 0.14 }, + { x: 0.2, y: 0.3 }, + { x: 0.5, y: 0.55 }, + { x: 0.8, y: 0.8 }, + { x: 1, y: 0.95 }, + ], + }, + }, +}; + +const AdjusmentCurves = () => { + const { floatingControlData } = useLayoutStore(); + const { studio } = useStudioStore(); + const { clipId } = floatingControlData || {}; + const clip = studio?.getClipById(clipId) as any; + + const canvasRef = useRef(null); + const [selectedCurve, setSelectedCurve] = useState("rgb"); + const [draggingPoint, setDraggingPoint] = useState<{ + curveId: string; + pointIndex: number; + } | null>(null); + + const PADDING = 10; + const CANVAS_SIZE = 200; + const INNER_SIZE = CANVAS_SIZE - PADDING * 2; + + const [curves, setCurves] = useState([ + { + id: "rgb", + name: "RGB", + color: "#ffffff", + points: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + { + id: "red", + name: "Red", + color: "#ef4444", + points: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + { + id: "green", + name: "Green", + color: "#22c55e", + points: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + { + id: "blue", + name: "Blue", + color: "#3b82f6", + points: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }, + ]); + + useEffect(() => { + if (!clip?.colorAdjustment?.curves) return; + setCurves((prev) => + prev.map((curve) => { + const incoming = + clip.colorAdjustment.curves[curve.id as "rgb" | "red" | "green" | "blue"] ?? curve.points; + if (JSON.stringify(incoming) === JSON.stringify(curve.points)) { + return curve; + } + return { + ...curve, + points: incoming, + }; + }), + ); + }, [clip?.colorAdjustment?.curves]); + + useEffect(() => { + if (!clip) return; + const nextCurves = curves.reduce( + (acc, curve) => { + acc[curve.id as "rgb" | "red" | "green" | "blue"] = curve.points; + return acc; + }, + {} as Record<"rgb" | "red" | "green" | "blue", Point[]>, + ); + if (JSON.stringify(nextCurves) === JSON.stringify(clip?.colorAdjustment?.curves)) return; + clip.update({ + colorAdjustment: { + enabled: true, + type: "curves", + basic: clip?.colorAdjustment?.basic, + hsl: clip?.colorAdjustment?.hsl, + curves: nextCurves, + }, + }); + }, [clip, curves]); + + const toCanvasCoords = useCallback( + (x: number, y: number) => { + return { + x: PADDING + x * INNER_SIZE, + y: PADDING + (1 - y) * INNER_SIZE, + }; + }, + [INNER_SIZE], + ); + + const toNormalizedCoords = useCallback( + (canvasX: number, canvasY: number) => { + const x = Math.min(1, Math.max(0, (canvasX - PADDING) / INNER_SIZE)); + const y = Math.min(1, Math.max(0, 1 - (canvasY - PADDING) / INNER_SIZE)); + return { x, y }; + }, + [INNER_SIZE], + ); + + const applyPreset = useCallback((presetKey: string) => { + const preset = CURVE_PRESETS[presetKey]; + if (!preset) return; + + setCurves((prev) => + prev.map((curve) => { + const presetPoints = preset.curves[curve.id as keyof typeof preset.curves]; + if (presetPoints) { + return { + ...curve, + points: [...presetPoints], + }; + } + return curve; + }), + ); + }, []); + + const getCurvePoints = useCallback((points: Point[]) => { + if (points.length < 2) return []; + + const sortedPoints = [...points].sort((a, b) => a.x - b.x); + + let finalPoints = [...sortedPoints]; + if (finalPoints[0].x > 0) finalPoints.unshift({ x: 0, y: 0 }); + if (finalPoints[finalPoints.length - 1].x < 1) finalPoints.push({ x: 1, y: 1 }); + + return finalPoints; + }, []); + + const interpolateCurve = useCallback((points: Point[], x: number) => { + if (points.length < 2) return x; + + const sortedPoints = [...points].sort((a, b) => a.x - b.x); + + if (sortedPoints.length === 2) { + const [p1, p2] = sortedPoints; + if (p2.x === p1.x) return p1.y; + const t = (x - p1.x) / (p2.x - p1.x); + return p1.y + t * (p2.y - p1.y); + } + + const extended = [...sortedPoints]; + if (extended[0].x > 0) extended.unshift({ x: 0, y: 0 }); + if (extended[extended.length - 1].x < 1) extended.push({ x: 1, y: 1 }); + + for (let i = 0; i < extended.length - 1; i++) { + const p0 = i === 0 ? extended[i] : extended[i - 1]; + const p1 = extended[i]; + const p2 = extended[i + 1]; + const p3 = i + 2 < extended.length ? extended[i + 2] : p2; + + if (x >= p1.x && x <= p2.x) { + const t = (x - p1.x) / (p2.x - p1.x); + const t2 = t * t; + const t3 = t2 * t; + + const m1 = ((p2.y - p0.y) / (p2.x - p0.x || 1)) * 0.5; + const m2 = ((p3.y - p1.y) / (p3.x - p1.x || 1)) * 0.5; + + const h00 = 2 * t3 - 3 * t2 + 1; + const h10 = t3 - 2 * t2 + t; + const h01 = -2 * t3 + 3 * t2; + const h11 = t3 - t2; + + return h00 * p1.y + h10 * m1 * (p2.x - p1.x) + h01 * p2.y + h11 * m2 * (p2.x - p1.x); + } + } + + return x; + }, []); + + const drawCurvePath = useCallback( + (ctx: CanvasRenderingContext2D, points: Point[]) => { + const finalPoints = getCurvePoints(points); + if (finalPoints.length < 2) return; + + const firstCoords = toCanvasCoords(finalPoints[0].x, finalPoints[0].y); + ctx.moveTo(firstCoords.x, firstCoords.y); + + if (finalPoints.length === 2) { + const second = toCanvasCoords(finalPoints[1].x, finalPoints[1].y); + ctx.lineTo(second.x, second.y); + return; + } + + for (let i = 0; i < finalPoints.length - 1; i++) { + const p0 = i === 0 ? finalPoints[i] : finalPoints[i - 1]; + const p1 = finalPoints[i]; + const p2 = finalPoints[i + 1]; + const p3 = i + 2 < finalPoints.length ? finalPoints[i + 2] : p2; + + const cp1 = { + x: p1.x + (p2.x - p0.x) / 6, + y: p1.y + (p2.y - p0.y) / 6, + }; + const cp2 = { + x: p2.x - (p3.x - p1.x) / 6, + y: p2.y - (p3.y - p1.y) / 6, + }; + + const cp1Coords = toCanvasCoords(cp1.x, cp1.y); + const cp2Coords = toCanvasCoords(cp2.x, cp2.y); + const p2Coords = toCanvasCoords(p2.x, p2.y); + + ctx.bezierCurveTo( + cp1Coords.x, + cp1Coords.y, + cp2Coords.x, + cp2Coords.y, + p2Coords.x, + p2Coords.y, + ); + } + }, + [getCurvePoints, toCanvasCoords], + ); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + const width = canvas.width; + const height = canvas.height; + + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = "#0a0a0a"; + ctx.fillRect(0, 0, width, height); + + ctx.fillStyle = "#111111"; + ctx.fillRect(PADDING, PADDING, INNER_SIZE, INNER_SIZE); + + ctx.strokeStyle = "#2a2a2a"; + ctx.lineWidth = 1; + + const gridSteps = 8; + for (let i = 0; i <= gridSteps; i++) { + const pos = PADDING + (i / gridSteps) * INNER_SIZE; + + ctx.beginPath(); + ctx.moveTo(pos, PADDING); + ctx.lineTo(pos, PADDING + INNER_SIZE); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(PADDING, pos); + ctx.lineTo(PADDING + INNER_SIZE, pos); + ctx.stroke(); + } + + ctx.strokeStyle = "#555555"; + ctx.lineWidth = 2; + ctx.strokeRect(PADDING, PADDING, INNER_SIZE, INNER_SIZE); + + ctx.beginPath(); + ctx.moveTo(PADDING, PADDING + INNER_SIZE); + ctx.lineTo(PADDING + INNER_SIZE, PADDING); + ctx.strokeStyle = "#333333"; + ctx.lineWidth = 1; + ctx.stroke(); + + curves.forEach((curve) => { + const points = getCurvePoints(curve.points); + if (points.length < 2) return; + + ctx.beginPath(); + ctx.strokeStyle = curve.color; + ctx.lineWidth = curve.id === selectedCurve ? 3 : 1.5; + + drawCurvePath(ctx, curve.points); + ctx.stroke(); + + if (curve.id === selectedCurve) { + points.forEach((point) => { + const canvasCoords = toCanvasCoords(point.x, point.y); + + ctx.beginPath(); + ctx.fillStyle = curve.color; + ctx.arc(canvasCoords.x, canvasCoords.y, 6, 0, 2 * Math.PI); + ctx.fill(); + ctx.strokeStyle = "#ffffff"; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.beginPath(); + ctx.fillStyle = "#ffffff"; + ctx.arc(canvasCoords.x, canvasCoords.y, 2, 0, 2 * Math.PI); + ctx.fill(); + }); + } + }); + }, [ + curves, + selectedCurve, + getCurvePoints, + interpolateCurve, + toCanvasCoords, + INNER_SIZE, + PADDING, + ]); + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const mouseX = (e.clientX - rect.left) * scaleX; + const mouseY = (e.clientY - rect.top) * scaleY; + + if ( + mouseX < PADDING || + mouseX > PADDING + INNER_SIZE || + mouseY < PADDING || + mouseY > PADDING + INNER_SIZE + ) { + return; + } + + const currentCurve = curves.find((c) => c.id === selectedCurve); + if (!currentCurve) return; + + const points = getCurvePoints(currentCurve.points); + + for (let i = 0; i < points.length; i++) { + const point = points[i]; + const canvasCoords = toCanvasCoords(point.x, point.y); + + const distance = Math.hypot(mouseX - canvasCoords.x, mouseY - canvasCoords.y); + if (distance < 10) { + setDraggingPoint({ curveId: selectedCurve, pointIndex: i }); + return; + } + } + + const { x, y } = toNormalizedCoords(mouseX, mouseY); + + if (x > 0.05 && x < 0.95 && y > 0.05 && y < 0.95) { + const existsNear = currentCurve.points.some((p) => Math.abs(p.x - x) < 0.03); + if (!existsNear) { + setCurves((prev) => + prev.map((curve) => { + if (curve.id === selectedCurve) { + const newPoints = [...curve.points, { x, y }]; + newPoints.sort((a, b) => a.x - b.x); + return { ...curve, points: newPoints }; + } + return curve; + }), + ); + } + } + }, + [ + selectedCurve, + curves, + getCurvePoints, + toCanvasCoords, + toNormalizedCoords, + INNER_SIZE, + PADDING, + ], + ); + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + if (!draggingPoint) return; + + const canvas = canvasRef.current; + if (!canvas) return; + + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const mouseX = (e.clientX - rect.left) * scaleX; + const mouseY = (e.clientY - rect.top) * scaleY; + + if ( + mouseX < PADDING || + mouseX > PADDING + INNER_SIZE || + mouseY < PADDING || + mouseY > PADDING + INNER_SIZE + ) { + return; + } + + let { x, y } = toNormalizedCoords(mouseX, mouseY); + + x = Math.min(0.95, Math.max(0.05, x)); + y = Math.min(0.95, Math.max(0.05, y)); + + setCurves((prev) => + prev.map((curve) => { + if (curve.id === draggingPoint.curveId) { + const newPoints = [...curve.points]; + const points = getCurvePoints(newPoints); + + let originalIndex = -1; + for (let i = 0; i < newPoints.length; i++) { + if ( + Math.abs(newPoints[i].x - points[draggingPoint.pointIndex].x) < 0.001 && + Math.abs(newPoints[i].y - points[draggingPoint.pointIndex].y) < 0.001 + ) { + originalIndex = i; + break; + } + } + + if ( + originalIndex !== -1 && + newPoints[originalIndex].x !== 0 && + newPoints[originalIndex].x !== 1 + ) { + newPoints[originalIndex] = { x, y }; + newPoints.sort((a, b) => a.x - b.x); + return { ...curve, points: newPoints }; + } + } + return curve; + }), + ); + }, + [draggingPoint, getCurvePoints, toNormalizedCoords, INNER_SIZE, PADDING], + ); + + const handleMouseUp = useCallback(() => { + setDraggingPoint(null); + }, []); + + const resetCurve = useCallback(() => { + setCurves((prev) => + prev.map((curve) => { + if (curve.id === selectedCurve) { + return { + ...curve, + points: [ + { x: 0, y: 0 }, + { x: 1, y: 1 }, + ], + }; + } + return curve; + }), + ); + }, [selectedCurve]); + + const deleteSelectedPoint = useCallback(() => { + const currentCurve = curves.find((c) => c.id === selectedCurve); + if (!currentCurve) return; + + if (currentCurve.points.length <= 2) return; + + setCurves((prev) => + prev.map((curve) => { + if (curve.id === selectedCurve) { + let pointToDelete = -1; + let maxDistanceFromExtremes = -1; + + curve.points.forEach((point, idx) => { + const distFromExtremes = Math.min(point.x, 1 - point.x); + if (distFromExtremes > maxDistanceFromExtremes && point.x > 0.05 && point.x < 0.95) { + maxDistanceFromExtremes = distFromExtremes; + pointToDelete = idx; + } + }); + + if (pointToDelete !== -1) { + const newPoints = curve.points.filter((_, idx) => idx !== pointToDelete); + return { ...curve, points: newPoints }; + } + } + return curve; + }), + ); + }, [selectedCurve, curves]); + + return ( + +
+ {/* Presets Section */} +
+
+ +
+
+ {Object.entries(CURVE_PRESETS).map(([key, preset]) => ( + + ))} +
+
+ + {/* Canvas para curvas */} +
+ +
+ + {/* Selector de canales */} +
+ +
+ {curves.map((curve) => ( + + ))} +
+
+ + {/* Controles */} + + +
+ + + + + +
+
+
+ ); +}; + +export default AdjusmentCurves; diff --git a/src/components/editor/floating-controls/color-adjusment/adjusment-hsl.tsx b/src/components/editor/floating-controls/color-adjusment/adjusment-hsl.tsx new file mode 100644 index 0000000..defbf36 --- /dev/null +++ b/src/components/editor/floating-controls/color-adjusment/adjusment-hsl.tsx @@ -0,0 +1,291 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Slider } from "@/components/ui/slider"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import useLayoutStore from "../../store/use-layout-store"; +import { useStudioStore } from "@/stores/studio-store"; +import { Button } from "@/components/ui/button"; +import { RotateCcw } from "lucide-react"; + +type HslAdjustment = "hue" | "saturation" | "lightness"; + +interface HslValue { + hue: number; + saturation: number; + lightness: number; +} + +type HslByColor = Record; + +const HSL_SWATCHES = [ + "#d64343", + "#e59d2b", + "#f8e436", + "#44d43f", + "#3dcddd", + "#4596ff", + "#8e57ff", + "#cf39f9", +]; + +const ADJUSTMENT_LIMITS: Record = { + hue: { min: -180, max: 180 }, + saturation: { min: -100, max: 100 }, + lightness: { min: -100, max: 100 }, +}; + +const DEFAULT_HSL_VALUE: HslValue = { + hue: 0, + saturation: 0, + lightness: 0, +}; + +const normalizeHex = (color: string) => { + const hex = color.trim().toLowerCase(); + if (!hex.startsWith("#")) return hex; + if (hex.length === 4) { + return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`; + } + return hex; +}; + +const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { + const normalized = normalizeHex(hex); + const match = /^#([0-9a-f]{6})$/i.exec(normalized); + if (!match) return null; + const int = parseInt(match[1], 16); + return { + r: (int >> 16) & 255, + g: (int >> 8) & 255, + b: int & 255, + }; +}; + +const colorDistance = (a: string, b: string) => { + const rgbA = hexToRgb(a); + const rgbB = hexToRgb(b); + if (!rgbA || !rgbB) return Number.MAX_SAFE_INTEGER; + const dr = rgbA.r - rgbB.r; + const dg = rgbA.g - rgbB.g; + const db = rgbA.b - rgbB.b; + return dr * dr + dg * dg + db * db; +}; + +const findClosestSwatch = (color: string | undefined) => { + if (!color) return HSL_SWATCHES[0]; + let closest = HSL_SWATCHES[0]; + let minDistance = Number.MAX_SAFE_INTEGER; + for (const swatch of HSL_SWATCHES) { + const distance = colorDistance(color, swatch); + if (distance < minDistance) { + minDistance = distance; + closest = swatch; + } + } + return closest; +}; + +const AdjusmentHsl = () => { + const { floatingControlData } = useLayoutStore(); + const { studio } = useStudioStore(); + const { clipId } = floatingControlData || {}; + const clip = studio?.getClipById(clipId) as any; + + const [selectedColor, setSelectedColor] = useState(() => + findClosestSwatch(clip?.colorAdjustment?.hsl?.selectedColor), + ); + const [hslValue, setHslValue] = useState(DEFAULT_HSL_VALUE); + + const getHslFromClipByColor = useCallback( + (color: string): HslValue => { + const hsl = clip?.colorAdjustment?.hsl; + const byColor = (hsl?.byColor ?? {}) as HslByColor; + const normalizedColor = normalizeHex(color); + return byColor[normalizedColor] ?? DEFAULT_HSL_VALUE; + }, + [clip], + ); + + useEffect(() => { + if (!clip) return; + const prevByColor = (clip?.colorAdjustment?.hsl?.byColor ?? {}) as HslByColor; + clip.update({ + colorAdjustment: { + enabled: true, + type: "hsl", + basic: clip?.colorAdjustment?.basic, + hsl: { + selectedColor, + byColor: { + ...prevByColor, + [selectedColor]: hslValue, + }, + }, + curves: clip?.colorAdjustment?.curves, + }, + }); + }, [clip, selectedColor, hslValue]); + + const getRangeStyle = useCallback((value: number, customMin?: number, customMax?: number) => { + const effectiveMin = customMin ?? -100; + const effectiveMax = customMax ?? 100; + const effectiveRange = effectiveMax - effectiveMin; + const effectiveCenterPercent = Math.abs(effectiveMin) / effectiveRange; + + if (value >= 0) { + const width = (value / effectiveMax) * (1 - effectiveCenterPercent) * 100; + return { + left: `${effectiveCenterPercent * 100}%`, + width: `${width}%`, + }; + } + + const width = (Math.abs(value) / Math.abs(effectiveMin)) * effectiveCenterPercent * 100; + return { + left: `${(effectiveCenterPercent - width / 100) * 100}%`, + width: `${width}%`, + }; + }, []); + + const sliderStyles = useMemo( + () => ({ + hue: getRangeStyle(hslValue.hue, ADJUSTMENT_LIMITS.hue.min, ADJUSTMENT_LIMITS.hue.max), + saturation: getRangeStyle( + hslValue.saturation, + ADJUSTMENT_LIMITS.saturation.min, + ADJUSTMENT_LIMITS.saturation.max, + ), + lightness: getRangeStyle( + hslValue.lightness, + ADJUSTMENT_LIMITS.lightness.min, + ADJUSTMENT_LIMITS.lightness.max, + ), + }), + [getRangeStyle, hslValue], + ); + + const handleSliderChange = useCallback((type: HslAdjustment, value: number[]) => { + setHslValue((prev) => ({ ...prev, [type]: value[0] })); + }, []); + + const handleInputChange = useCallback((type: HslAdjustment, value: string) => { + const numValue = Number(value); + if (!isNaN(numValue)) { + const { min, max } = ADJUSTMENT_LIMITS[type]; + setHslValue((prev) => ({ + ...prev, + [type]: Math.min(max, Math.max(min, numValue)), + })); + } + }, []); + + const handleSwatchSelect = useCallback( + (color: string) => { + const normalizedColor = normalizeHex(color); + const swatchValue = getHslFromClipByColor(normalizedColor); + setSelectedColor(normalizedColor); + setHslValue(swatchValue); + }, + [getHslFromClipByColor], + ); + + const handleReset = useCallback(() => { + setHslValue(DEFAULT_HSL_VALUE); + + // Reset all colors in the clip + Promise.resolve().then(() => { + clip?.update({ + colorAdjustment: { + enabled: true, + type: "hsl", + basic: clip?.colorAdjustment?.basic, + hsl: { + selectedColor, + byColor: {}, + }, + curves: clip?.colorAdjustment?.curves, + }, + }); + }); + }, [clip, selectedColor]); + + const controls: Array<{ id: HslAdjustment; label: string; value: number }> = [ + { id: "hue", label: "Hue", value: hslValue.hue }, + { id: "saturation", label: "Saturation", value: hslValue.saturation }, + { id: "lightness", label: "Lightness", value: hslValue.lightness }, + ]; + + return ( + +
+
+ + +
+
+ +
+ {HSL_SWATCHES.map((color) => ( +
handleSwatchSelect(color)} + /> + ))} +
+
+ +
+ {controls.map(({ id, label, value }) => ( +
+ {label} + +
+
+ + + handleSliderChange(id, newValue)} + min={ADJUSTMENT_LIMITS[id].min} + max={ADJUSTMENT_LIMITS[id].max} + step={1} + className={`slider-hsl-${id} w-full`} + /> +
+ + handleInputChange(id, e.target.value)} + min={ADJUSTMENT_LIMITS[id].min} + max={ADJUSTMENT_LIMITS[id].max} + step={1} + className="w-20 text-[10px]" + /> +
+
+ ))} +
+
+ + ); +}; + +export default AdjusmentHsl; diff --git a/src/components/editor/floating-controls/color-adjusment/index.tsx b/src/components/editor/floating-controls/color-adjusment/index.tsx new file mode 100644 index 0000000..83d8675 --- /dev/null +++ b/src/components/editor/floating-controls/color-adjusment/index.tsx @@ -0,0 +1,3 @@ +export { default as AdjusmentBasic } from "./adjusment-basic"; +export { default as AdjusmentHsl } from "./adjusment-hsl"; +export { default as AdjusmentCurves } from "./adjusment-curves"; diff --git a/src/components/editor/floating-controls/color-adjustment.tsx b/src/components/editor/floating-controls/color-adjustment.tsx new file mode 100644 index 0000000..d9d4ab8 --- /dev/null +++ b/src/components/editor/floating-controls/color-adjustment.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useRef } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { AdjusmentBasic, AdjusmentCurves, AdjusmentHsl } from "./color-adjusment"; +import useLayoutStore from "../store/use-layout-store"; + +const ColorAdjustment = () => { + const { setFloatingControl } = useLayoutStore(); + + const containerRef = useRef(null); + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if ( + containerRef.current && + !containerRef.current.contains(target) && + !target.closest("[data-radix-portal]") && + !target.closest("[data-radix-popper-content-wrapper]") + ) { + setFloatingControl(""); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [setFloatingControl]); + + return ( +
+ + + Basic + HSL + Curves + + + + + + + + + + + +
+ ); +}; + +export default ColorAdjustment; diff --git a/src/components/editor/floating-controls/floating-control.tsx b/src/components/editor/floating-controls/floating-control.tsx index cf54728..5e27f78 100644 --- a/src/components/editor/floating-controls/floating-control.tsx +++ b/src/components/editor/floating-controls/floating-control.tsx @@ -1,6 +1,7 @@ import useLayoutStore from "../store/use-layout-store"; import { AnimationPropertiesPicker } from "./animation-properties-picker"; import CaptionPresetPicker from "./caption-preset-picker"; +import ColorAdjustment from "./color-adjustment"; export default function FloatingControl() { const { floatingControl } = useLayoutStore(); @@ -13,5 +14,9 @@ export default function FloatingControl() { return ; } + if (floatingControl === "color-adjustment") { + return ; + } + return null; } diff --git a/src/components/editor/properties-panel/caption-properties.tsx b/src/components/editor/properties-panel/caption-properties.tsx index eba90bf..f54f69a 100644 --- a/src/components/editor/properties-panel/caption-properties.tsx +++ b/src/components/editor/properties-panel/caption-properties.tsx @@ -108,7 +108,13 @@ export function CaptionProperties({ clip }: CaptionPropertiesProps) { (clip as any).opts.keyword = colorUpdates.keyword; } Object.assign((clip as any).caption.colors, colorUpdates); - clip.emit("propsChange", {}); + if (typeof (clip as any).refreshCaptions === "function") { + (clip as any).refreshCaptions().then(() => { + clip.emit("propsChange", {}); + }); + } else { + clip.emit("propsChange", {}); + } } studio.emit("propsChange", {}); diff --git a/src/components/editor/properties-panel/image-properties.tsx b/src/components/editor/properties-panel/image-properties.tsx index b931c19..c4707c5 100644 --- a/src/components/editor/properties-panel/image-properties.tsx +++ b/src/components/editor/properties-panel/image-properties.tsx @@ -10,7 +10,7 @@ import { ColorPickerSelection, } from "@/components/ui/color-picker"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { IClip, AnimationOptions, KeyframeData } from "openvideo"; +import { IClip } from "openvideo"; import { IconAlignLeft, IconAlignCenter, @@ -43,6 +43,8 @@ import color from "color"; import { NumberInput } from "@/components/ui/number-input"; import { Switch } from "@/components/ui/switch"; import useLayoutStore from "../store/use-layout-store"; +import { Button } from "@/components/ui/button"; +import { ChevronDown, ChevronRight } from "lucide-react"; interface ImagePropertiesProps { clip: IClip; @@ -254,6 +256,26 @@ export function ImageProperties({ clip }: ImagePropertiesProps) {
+ {/* Color Adjustment Section */} + +
+ +
+ +
+
+ {/* Animations Section */}
diff --git a/src/components/editor/properties-panel/video-properties.tsx b/src/components/editor/properties-panel/video-properties.tsx index cdb6e81..ff64be7 100644 --- a/src/components/editor/properties-panel/video-properties.tsx +++ b/src/components/editor/properties-panel/video-properties.tsx @@ -10,7 +10,7 @@ import { ColorPickerSelection, } from "@/components/ui/color-picker"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { IClip, AnimationOptions, KeyframeData } from "openvideo"; +import { IClip } from "openvideo"; import { IconAlignLeft, IconAlignCenter, @@ -44,6 +44,8 @@ import color from "color"; import { NumberInput } from "@/components/ui/number-input"; import { Switch } from "@/components/ui/switch"; import useLayoutStore from "../store/use-layout-store"; +import { Button } from "@/components/ui/button"; +import { ChevronRight } from "lucide-react"; interface VideoPropertiesProps { clip: IClip; @@ -281,6 +283,26 @@ export function VideoProperties({ clip }: VideoPropertiesProps) {
+ {/* Color Adjustment Section */} + +
+ +
+ +
+
+ {/* Animations Section */}