feat: add adjustments, filters, levels, curves, histogram, and effects panels for image editor

Implements Features 17, 18, 35, 36, 41, 42, 43 for the editor:
- SliderRow reusable component (label + range + numeric input)
- AdjustmentsPanel with 8 adjustment sliders (brightness, contrast,
  hue, saturation, luminance, exposure, vibrance, warmth)
- Auto adjustments: Auto Tone, Auto Contrast, Auto Color, Auto Enhance
- Levels section with per-channel control, histogram display, black/white
  point, gamma, and output range sliders
- Curves section with 200x200 interactive graph, cubic spline
  interpolation, per-channel support, and 9 presets
- 12 filter controls: toggle (grayscale, sepia, invert, solarize) and
  slider (blur, sharpen, noise, pixelate, emboss, posterize, threshold,
  kaleidoscope)
- Additional blur types: motion blur, radial blur, surface blur
- Vignette with amount/midpoint/roundness/feather sliders
- Grain with amount/size/roughness sliders
- HistogramPanel with RGB channel overlays and stats (mean, stddev, median)
- Reset All and Apply action buttons
- 150ms debounce on adjustment sliders, 300ms on histogram updates
This commit is contained in:
SnapOtter
2026-05-06 23:23:45 +08:00
parent 9eae8369bb
commit c4bcb12173
3 changed files with 1409 additions and 0 deletions
@@ -0,0 +1,52 @@
// apps/web/src/components/editor/common/slider-row.tsx
import { cn } from "@/lib/utils";
interface SliderRowProps {
label: string;
value: number;
min: number;
max: number;
step?: number;
onChange: (value: number) => void;
className?: string;
}
export function SliderRow({
label,
value,
min,
max,
step = 1,
onChange,
className,
}: SliderRowProps) {
return (
<div className={cn("flex items-center gap-2", className)}>
<span className="text-xs text-muted-foreground w-20 shrink-0 truncate">{label}</span>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="flex-1 h-1 accent-primary cursor-pointer"
/>
<input
type="number"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => {
const v = Number(e.target.value);
if (!Number.isNaN(v)) {
onChange(Math.max(min, Math.min(max, v)));
}
}}
className="w-14 px-1 py-0.5 text-xs text-right bg-muted border border-border rounded text-foreground"
/>
</div>
);
}