feat(image-editor): Adjustments menu — 10 PS-style adjustment layers#51
Open
feat(image-editor): Adjustments menu — 10 PS-style adjustment layers#51
Conversation
Introduces the Image > Adjustments menu with 10 non-destructive adjustment layers: - Brightness/Contrast — centred-around-128 contrast curve + brightness shift - Levels — input/output black + white points + gamma (LUT) - Curves — interactive Catmull-Rom RGB tone curve editor (canvas-based, click-to-add, drag-to-move, Alt-click to remove interior points) - Exposure — log-stop exposure + offset + gamma - Vibrance — saturation that protects already-saturated pixels + flat sat - Hue/Saturation — full HSL shift (hue / sat / lightness) - Color Balance — additive shift along Cyan↔Red, Magenta↔Green, Yellow↔Blue - Invert — per-channel 255-x (no params) - Posterize — N-level quantization (LUT) - Threshold — binary B&W via BT.601 luma (LUT) Each opens a modal dialog with sliders (Curves gets its own canvas editor) and live preview through a new `extraPreviewLayer` slot the parent can pass to Canvas. On Apply, the draft commits via the regular `commitLayer` flow (so selection clipping from #48 + history work for free); Cancel discards. - `AdjustmentLayer` joins the `Layer` union — `kind: 'adjustment'` carries `params: AdjustmentParams` (discriminated union over the 10 kinds). - `applyAdjustment(data, params)` in `lib/image-editor/adjustments.ts` dispatches to per-kind pure-JS pixel functions. LUT-based adjustments precompute a 256-entry lookup once per layer; HSL-based ones go pixel by pixel through RGB↔HSL. - `applyAdjustmentLayer` in `render.ts`: snapshots the accumulated canvas to a temp, applies the transform via getImageData/putImageData, then composites the temp back through the layer's clip + opacity (which naturally blends adjusted-vs-original via globalAlpha). - `applyLayerClip` widens to accept any `{ clipRect?, clipPath? }`-bearing layer so the same selection-clip mechanism works for adjustments and annotations alike. The renderer applies adjustments at their position in the layer stack (PS-style: an adjustment affects everything below it). Stack two adjustments and they compose; toggle visibility on either to disable. All selection-clipping, undo/redo, and project save/load come through the existing layer plumbing — no special cases. `getImageData` + the per-layer LUT walk on a 900×900 preview takes ~5ms; fine for live preview. LUT-based kinds (Levels / Curves / Posterize / Brightness-Contrast / Exposure / Invert) all share a single `applyLut` runner. HSL kinds (Hue/Sat / Vibrance) cost ~3× more due to RGB↔HSL — still smooth at slider-drag cadence. `AdjustmentLayer` is additive — old projects load fine (no adjustment layers in their JSON). A handful of pre-existing helpers needed to learn about the new layer kind (`getLayerBBox`, `getHandles`, `translateLayer`, `resizeLayer`, `LayersPanel` label dispatch) — all return sensible no-ops for adjustments since they have no spatial geometry beyond the canvas. Author did not browser-verify (no browser at hand); typecheck + lint + build all green. ImageEditor bundle +14 KB for the adjustments module + dialog + Curves editor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5a7a1a8 to
4836a63
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces the Image > Adjustments menu with 10 PS-aligned non-destructive adjustment layers — the missing third leg of the editor's color/tone toolkit. Stack them, reorder them, toggle visibility, blend with selection clips, undo/redo all the way through.
What's in the menu
Each opens a modal dialog with sliders (Curves gets its own canvas editor). Live preview updates the canvas on every input change via a new
extraPreviewLayerslot. Apply commits as a real layer through the regularcommitLayerflow (so selection clipping from #48 + history work for free); Cancel discards.Architecture
AdjustmentLayerjoins theLayerunion —kind: 'adjustment'carriesparams: AdjustmentParams(discriminated union over the 10 kinds).applyAdjustment(data, params)inlib/image-editor/adjustments.tsdispatches to per-kind pure-JS pixel transforms. LUT-based adjustments (Levels, Curves, Posterize, B/C, Exposure, Invert) precompute a 256-entry lookup once per layer and share a singleapplyLutrunner — orders of magnitude faster than per-pixel math. HSL-based ones (Hue/Sat, Vibrance) go pixel by pixel through RGB↔HSL.applyAdjustmentLayerinrender.ts: snapshots the accumulated canvas to a temp, applies the transform viagetImageData/putImageData, then composites the temp back through the layer's clip + opacity (which naturally blends adjusted-vs-original viaglobalAlpha).applyLayerClipwidens to accept any{ clipRect?, clipPath? }-bearing layer so the same selection-clip mechanism works for adjustments and annotations alike.The renderer applies adjustments at their position in the layer stack (PS-style: affects everything below). Stack two and they compose; toggle visibility to disable.
Files
New:
lib/image-editor/adjustments.ts— pure-JS pixel transforms + defaults + dispatcher.components/image-editor/AdjustmentDialog.tsx— modal with per-kind forms (inline sub-components).components/image-editor/CurvesEditor.tsx— interactive Catmull-Rom curve editor on a 256×256 canvas.Modified:
lib/image-editor/types.ts—AdjustmentLayer+AdjustmentParamsdiscriminated union +AdjustmentKindalias.lib/image-editor/render.ts— adjustment-layer render block +applyAdjustmentLayerhelper; clip helper widened to{ clipRect?, clipPath? }.lib/image-editor/transform.ts—translateLayer/resizeLayerhandle the new layer kind.lib/image-editor/hit.ts— bbox + handles for adjustment layers (no-op spatial geometry; selection through the layers panel).components/image-editor/MenuBar.tsx— Image menu gets adjustment items as 4 sections (B/C+Levels+Curves+Exposure / Vibrance+H/S+ColorBalance / Invert+Posterize+Threshold).components/image-editor/Canvas.tsx— newextraPreviewLayerprop routed throughdrawingPreview.components/image-editor/LayersPanel.tsx— label dispatch for adjustment layers.pages/ImageEditor.tsx— dialog state + handlers + preview wiring.i18n/{en,zh-CN}.json— 24 adjustment-related strings.Backward compat
AdjustmentLayeris additive — old projects load fine (no adjustment layers in their JSON). All pre-existing helpers learned about the new layer kind in this PR.Test plan
Note
Author did not browser-verify (no browser available in this session).
pnpm typecheck,pnpm lint, andpnpm buildare all green; ImageEditor bundle +14 KB.🤖 Generated with Claude Code