From b0a844d3a3e6f9b94d8438866498b796b8d48b8d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Apr 2026 23:29:48 +0200 Subject: [PATCH] Refactor adjustment nodes to use ParameterSlider for enhanced UI and functionality - Replaced SliderRow components with ParameterSlider in color-adjust, curves, detail-adjust, and light-adjust nodes for improved user interaction. - Updated minimum width and height configurations for adjustment nodes to accommodate new slider designs. - Enhanced node templates and default dimensions to reflect the changes in adjustment node sizes. - Improved state management and data handling for slider values in adjustment nodes. --- components/canvas/nodes/base-node-wrapper.tsx | 8 +- components/canvas/nodes/color-adjust-node.tsx | 169 +++++++++++------- components/canvas/nodes/curves-node.tsx | 111 +++++++----- .../canvas/nodes/detail-adjust-node.tsx | 130 ++++++++++++-- components/canvas/nodes/light-adjust-node.tsx | 137 ++++++++++++-- components/ui/button.tsx | 2 +- components/ui/slider.tsx | 49 +++-- lib/canvas-node-templates.ts | 16 +- lib/canvas-utils.ts | 8 +- 9 files changed, 464 insertions(+), 166 deletions(-) diff --git a/components/canvas/nodes/base-node-wrapper.tsx b/components/canvas/nodes/base-node-wrapper.tsx index 17fe358..6678e5f 100644 --- a/components/canvas/nodes/base-node-wrapper.tsx +++ b/components/canvas/nodes/base-node-wrapper.tsx @@ -39,10 +39,10 @@ const RESIZE_CONFIGS: Record = { "ai-image": { minWidth: 200, minHeight: 208, keepAspectRatio: false }, compare: { minWidth: 300, minHeight: 200 }, prompt: { minWidth: 260, minHeight: 220 }, - curves: { minWidth: 240, minHeight: 320 }, - "color-adjust": { minWidth: 240, minHeight: 360 }, - "light-adjust": { minWidth: 240, minHeight: 360 }, - "detail-adjust": { minWidth: 240, minHeight: 360 }, + curves: { minWidth: 300, minHeight: 620 }, + "color-adjust": { minWidth: 300, minHeight: 760 }, + "light-adjust": { minWidth: 300, minHeight: 860 }, + "detail-adjust": { minWidth: 300, minHeight: 820 }, render: { minWidth: 260, minHeight: 300, keepAspectRatio: true }, text: { minWidth: 220, minHeight: 90 }, note: { minWidth: 200, minHeight: 90 }, diff --git a/components/canvas/nodes/color-adjust-node.tsx b/components/canvas/nodes/color-adjust-node.tsx index c28770e..ca38ea5 100644 --- a/components/canvas/nodes/color-adjust-node.tsx +++ b/components/canvas/nodes/color-adjust-node.tsx @@ -10,9 +10,13 @@ import type { Id } from "@/convex/_generated/dataModel"; import { useAuthQuery } from "@/hooks/use-auth-query"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { useCanvasSync } from "@/components/canvas/canvas-sync-context"; -import { SliderRow } from "@/components/canvas/nodes/adjustment-controls"; import BaseNodeWrapper from "@/components/canvas/nodes/base-node-wrapper"; import AdjustmentPreview from "@/components/canvas/nodes/adjustment-preview"; +import { + ParameterSlider, + type SliderConfig, + type SliderValue, +} from "@/src/components/tool-ui/parameter-slider"; import { cloneAdjustmentData, DEFAULT_COLOR_ADJUST_DATA, @@ -80,6 +84,71 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps }; const builtinOptions = useMemo(() => Object.entries(COLOR_PRESETS), []); + const sliderConfigs = useMemo( + () => [ + { + id: "hue", + label: "Hue", + min: -180, + max: 180, + value: DEFAULT_COLOR_ADJUST_DATA.hsl.hue, + }, + { + id: "saturation", + label: "Saturation", + min: -100, + max: 100, + value: DEFAULT_COLOR_ADJUST_DATA.hsl.saturation, + }, + { + id: "luminance", + label: "Luminance", + min: -100, + max: 100, + value: DEFAULT_COLOR_ADJUST_DATA.hsl.luminance, + }, + { + id: "temperature", + label: "Temperature", + min: -100, + max: 100, + value: DEFAULT_COLOR_ADJUST_DATA.temperature, + }, + { + id: "tint", + label: "Tint", + min: -100, + max: 100, + value: DEFAULT_COLOR_ADJUST_DATA.tint, + }, + { + id: "vibrance", + label: "Vibrance", + min: -100, + max: 100, + value: DEFAULT_COLOR_ADJUST_DATA.vibrance, + }, + ], + [], + ); + const sliderValues = useMemo( + () => [ + { id: "hue", value: localData.hsl.hue }, + { id: "saturation", value: localData.hsl.saturation }, + { id: "luminance", value: localData.hsl.luminance }, + { id: "temperature", value: localData.temperature }, + { id: "tint", value: localData.tint }, + { id: "vibrance", value: localData.vibrance }, + ], + [ + localData.hsl.hue, + localData.hsl.luminance, + localData.hsl.saturation, + localData.temperature, + localData.tint, + localData.vibrance, + ], + ); const applyPresetValue = (value: string) => { if (value === "custom") { @@ -126,7 +195,7 @@ export default function ColorAdjustNode({ id, data, selected, width }: NodeProps selected={selected} status={data._status} statusMessage={data._statusMessage} - className="min-w-[240px] border-cyan-500/30" + className="min-w-[300px] border-cyan-500/30" > -
- - updateData((current) => ({ - ...current, - hsl: { ...current.hsl, hue: value }, - preset: null, - })) + { + const valueById = new Map(values.map((entry) => [entry.id, entry.value])); + updateData((current) => ({ + ...current, + hsl: { + ...current.hsl, + hue: valueById.get("hue") ?? current.hsl.hue, + saturation: valueById.get("saturation") ?? current.hsl.saturation, + luminance: valueById.get("luminance") ?? current.hsl.luminance, + }, + temperature: valueById.get("temperature") ?? current.temperature, + tint: valueById.get("tint") ?? current.tint, + vibrance: valueById.get("vibrance") ?? current.vibrance, + preset: null, + })); + }} + onAction={async (actionId) => { + if (actionId === "apply") { + queueSave(); } - /> - - updateData((current) => ({ - ...current, - hsl: { ...current.hsl, saturation: value }, - preset: null, - })) - } - /> - - updateData((current) => ({ - ...current, - hsl: { ...current.hsl, luminance: value }, - preset: null, - })) - } - /> - - updateData((current) => ({ ...current, temperature: value, preset: null })) - } - /> - - updateData((current) => ({ ...current, tint: value, preset: null })) - } - /> - - updateData((current) => ({ ...current, vibrance: value, preset: null })) - } - /> -
+ }} + /> Object.entries(CURVE_PRESETS), []); + const sliderConfigs = useMemo( + () => [ + { + id: "black-point", + label: "Black Point", + min: 0, + max: 255, + value: DEFAULT_CURVES_DATA.levels.blackPoint, + }, + { + id: "white-point", + label: "White Point", + min: 0, + max: 255, + value: DEFAULT_CURVES_DATA.levels.whitePoint, + }, + { + id: "gamma", + label: "Gamma", + min: 0.1, + max: 3, + step: 0.01, + precision: 2, + value: DEFAULT_CURVES_DATA.levels.gamma, + }, + ], + [], + ); + const sliderValues = useMemo( + () => [ + { id: "black-point", value: localData.levels.blackPoint }, + { id: "white-point", value: localData.levels.whitePoint }, + { id: "gamma", value: localData.levels.gamma }, + ], + [localData.levels.blackPoint, localData.levels.gamma, localData.levels.whitePoint], + ); const applyPresetValue = (value: string) => { if (value === "custom") { @@ -127,7 +167,7 @@ export default function CurvesNode({ id, data, selected, width }: NodeProps -
- - updateData((current) => ({ - ...current, - levels: { ...current.levels, blackPoint: value }, - preset: null, - })) + { + const valueById = new Map(values.map((entry) => [entry.id, entry.value])); + updateData((current) => ({ + ...current, + levels: { + ...current.levels, + blackPoint: valueById.get("black-point") ?? current.levels.blackPoint, + whitePoint: valueById.get("white-point") ?? current.levels.whitePoint, + gamma: valueById.get("gamma") ?? current.levels.gamma, + }, + preset: null, + })); + }} + onAction={async (actionId) => { + if (actionId === "apply") { + queueSave(); } - /> - - updateData((current) => ({ - ...current, - levels: { ...current.levels, whitePoint: value }, - preset: null, - })) - } - /> - - updateData((current) => ({ - ...current, - levels: { ...current.levels, gamma: value }, - preset: null, - })) - } - /> -
+ }} + /> Object.entries(DETAIL_PRESETS), []); + const sliderConfigs = useMemo( + () => [ + { + id: "sharpen-amount", + label: "Sharpen", + min: 0, + max: 500, + value: DEFAULT_DETAIL_ADJUST_DATA.sharpen.amount, + }, + { + id: "sharpen-radius", + label: "Radius", + min: 0.5, + max: 5, + step: 0.01, + precision: 2, + value: DEFAULT_DETAIL_ADJUST_DATA.sharpen.radius, + }, + { + id: "sharpen-threshold", + label: "Threshold", + min: 0, + max: 255, + value: DEFAULT_DETAIL_ADJUST_DATA.sharpen.threshold, + }, + { + id: "clarity", + label: "Clarity", + min: -100, + max: 100, + value: DEFAULT_DETAIL_ADJUST_DATA.clarity, + }, + { + id: "denoise-luminance", + label: "Denoise Luma", + min: 0, + max: 100, + value: DEFAULT_DETAIL_ADJUST_DATA.denoise.luminance, + }, + { + id: "denoise-color", + label: "Denoise Color", + min: 0, + max: 100, + value: DEFAULT_DETAIL_ADJUST_DATA.denoise.color, + }, + { + id: "grain-amount", + label: "Grain", + min: 0, + max: 100, + value: DEFAULT_DETAIL_ADJUST_DATA.grain.amount, + }, + ], + [], + ); + const sliderValues = useMemo( + () => [ + { id: "sharpen-amount", value: localData.sharpen.amount }, + { id: "sharpen-radius", value: localData.sharpen.radius }, + { id: "sharpen-threshold", value: localData.sharpen.threshold }, + { id: "clarity", value: localData.clarity }, + { id: "denoise-luminance", value: localData.denoise.luminance }, + { id: "denoise-color", value: localData.denoise.color }, + { id: "grain-amount", value: localData.grain.amount }, + ], + [ + localData.clarity, + localData.denoise.color, + localData.denoise.luminance, + localData.grain.amount, + localData.sharpen.amount, + localData.sharpen.radius, + localData.sharpen.threshold, + ], + ); const applyPresetValue = (value: string) => { if (value === "custom") { @@ -126,7 +206,7 @@ export default function DetailAdjustNode({ id, data, selected, width }: NodeProp selected={selected} status={data._status} statusMessage={data._statusMessage} - className="min-w-[240px] border-indigo-500/30" + className="min-w-[300px] border-indigo-500/30" > -
- updateData((current) => ({ ...current, sharpen: { ...current.sharpen, amount: value }, preset: null }))} /> - updateData((current) => ({ ...current, sharpen: { ...current.sharpen, radius: value }, preset: null }))} /> - updateData((current) => ({ ...current, sharpen: { ...current.sharpen, threshold: value }, preset: null }))} /> - updateData((current) => ({ ...current, clarity: value, preset: null }))} /> - updateData((current) => ({ ...current, denoise: { ...current.denoise, luminance: value }, preset: null }))} /> - updateData((current) => ({ ...current, denoise: { ...current.denoise, color: value }, preset: null }))} /> - updateData((current) => ({ ...current, grain: { ...current.grain, amount: value }, preset: null }))} /> -
+ { + const valueById = new Map(values.map((entry) => [entry.id, entry.value])); + updateData((current) => ({ + ...current, + sharpen: { + ...current.sharpen, + amount: valueById.get("sharpen-amount") ?? current.sharpen.amount, + radius: valueById.get("sharpen-radius") ?? current.sharpen.radius, + threshold: valueById.get("sharpen-threshold") ?? current.sharpen.threshold, + }, + clarity: valueById.get("clarity") ?? current.clarity, + denoise: { + ...current.denoise, + luminance: valueById.get("denoise-luminance") ?? current.denoise.luminance, + color: valueById.get("denoise-color") ?? current.denoise.color, + }, + grain: { + ...current.grain, + amount: valueById.get("grain-amount") ?? current.grain.amount, + }, + preset: null, + })); + }} + onAction={async (actionId) => { + if (actionId === "apply") { + queueSave(); + } + }} + /> Object.entries(LIGHT_PRESETS), []); + const sliderConfigs = useMemo( + () => [ + { + id: "brightness", + label: "Brightness", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.brightness, + }, + { + id: "contrast", + label: "Contrast", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.contrast, + }, + { + id: "exposure", + label: "Exposure", + min: -5, + max: 5, + step: 0.01, + precision: 2, + value: DEFAULT_LIGHT_ADJUST_DATA.exposure, + }, + { + id: "highlights", + label: "Highlights", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.highlights, + }, + { + id: "shadows", + label: "Shadows", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.shadows, + }, + { + id: "whites", + label: "Whites", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.whites, + }, + { + id: "blacks", + label: "Blacks", + min: -100, + max: 100, + value: DEFAULT_LIGHT_ADJUST_DATA.blacks, + }, + { + id: "vignette-amount", + label: "Vignette", + min: 0, + max: 1, + step: 0.01, + precision: 2, + value: DEFAULT_LIGHT_ADJUST_DATA.vignette.amount, + }, + ], + [], + ); + const sliderValues = useMemo( + () => [ + { id: "brightness", value: localData.brightness }, + { id: "contrast", value: localData.contrast }, + { id: "exposure", value: localData.exposure }, + { id: "highlights", value: localData.highlights }, + { id: "shadows", value: localData.shadows }, + { id: "whites", value: localData.whites }, + { id: "blacks", value: localData.blacks }, + { id: "vignette-amount", value: localData.vignette.amount }, + ], + [ + localData.blacks, + localData.brightness, + localData.contrast, + localData.exposure, + localData.highlights, + localData.shadows, + localData.vignette.amount, + localData.whites, + ], + ); const applyPresetValue = (value: string) => { if (value === "custom") { @@ -126,7 +217,7 @@ export default function LightAdjustNode({ id, data, selected, width }: NodeProps selected={selected} status={data._status} statusMessage={data._statusMessage} - className="min-w-[240px] border-amber-500/30" + className="min-w-[300px] border-amber-500/30" > -
- updateData((current) => ({ ...current, brightness: value, preset: null }))} /> - updateData((current) => ({ ...current, contrast: value, preset: null }))} /> - updateData((current) => ({ ...current, exposure: value, preset: null }))} /> - updateData((current) => ({ ...current, highlights: value, preset: null }))} /> - updateData((current) => ({ ...current, shadows: value, preset: null }))} /> - updateData((current) => ({ ...current, whites: value, preset: null }))} /> - updateData((current) => ({ ...current, blacks: value, preset: null }))} /> - updateData((current) => ({ ...current, vignette: { ...current.vignette, amount: value }, preset: null }))} /> -
+ { + const valueById = new Map(values.map((entry) => [entry.id, entry.value])); + updateData((current) => ({ + ...current, + brightness: valueById.get("brightness") ?? current.brightness, + contrast: valueById.get("contrast") ?? current.contrast, + exposure: valueById.get("exposure") ?? current.exposure, + highlights: valueById.get("highlights") ?? current.highlights, + shadows: valueById.get("shadows") ?? current.shadows, + whites: valueById.get("whites") ?? current.whites, + blacks: valueById.get("blacks") ?? current.blacks, + vignette: { + ...current.vignette, + amount: valueById.get("vignette-amount") ?? current.vignette.amount, + }, + preset: null, + })); + }} + onAction={async (actionId) => { + if (actionId === "apply") { + queueSave(); + } + }} + /> ) { + const _values = React.useMemo( + () => + Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max], + [value, defaultValue, min, max] + ) -function Slider({ className, ...props }: React.ComponentProps) { return ( - {Array.from({ length: props.value?.length ?? props.defaultValue?.length ?? 1 }).map((_, index) => ( + {Array.from({ length: _values.length }, (_, index) => ( ))} - ); + ) } -export { Slider }; +export { Slider } diff --git a/lib/canvas-node-templates.ts b/lib/canvas-node-templates.ts index c8a19f5..f5e3635 100644 --- a/lib/canvas-node-templates.ts +++ b/lib/canvas-node-templates.ts @@ -65,29 +65,29 @@ export const CANVAS_NODE_TEMPLATES = [ { type: "curves", label: "Kurven", - width: 280, - height: 460, + width: 320, + height: 660, defaultData: {}, }, { type: "color-adjust", label: "Farbe", - width: 280, - height: 560, + width: 320, + height: 800, defaultData: {}, }, { type: "light-adjust", label: "Licht", - width: 280, - height: 620, + width: 320, + height: 920, defaultData: {}, }, { type: "detail-adjust", label: "Detail", - width: 280, - height: 620, + width: 320, + height: 880, defaultData: {}, }, { diff --git a/lib/canvas-utils.ts b/lib/canvas-utils.ts index 4c202b4..f2a0d68 100644 --- a/lib/canvas-utils.ts +++ b/lib/canvas-utils.ts @@ -244,10 +244,10 @@ export const NODE_DEFAULTS: Record< compare: { width: 500, height: 380, data: {} }, asset: { width: 260, height: 240, data: {} }, video: { width: 320, height: 180, data: {} }, - curves: { width: 280, height: 460, data: DEFAULT_CURVES_DATA }, - "color-adjust": { width: 280, height: 560, data: DEFAULT_COLOR_ADJUST_DATA }, - "light-adjust": { width: 280, height: 620, data: DEFAULT_LIGHT_ADJUST_DATA }, - "detail-adjust": { width: 280, height: 620, data: DEFAULT_DETAIL_ADJUST_DATA }, + curves: { width: 320, height: 660, data: DEFAULT_CURVES_DATA }, + "color-adjust": { width: 320, height: 800, data: DEFAULT_COLOR_ADJUST_DATA }, + "light-adjust": { width: 320, height: 920, data: DEFAULT_LIGHT_ADJUST_DATA }, + "detail-adjust": { width: 320, height: 880, data: DEFAULT_DETAIL_ADJUST_DATA }, render: { width: 300, height: 420,