feat: add non-destructive adjustment layers
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useMemo, useRef, useState, type DragEvent, type MutableRefObject } from "react";
|
||||
import { ArrowDown, ArrowUp, DownloadSimple, Eye, EyeSlash, FolderPlus, Lock, LockOpen, Plus, Stack, Trash } from "@phosphor-icons/react";
|
||||
import { useEffect, useMemo, useRef, useState, type DragEvent, type MutableRefObject } from "react";
|
||||
import { ArrowDown, ArrowUp, DownloadSimple, Eye, EyeSlash, FolderPlus, Lock, LockOpen, Plus, SlidersHorizontal, Stack, Trash } from "@phosphor-icons/react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { ColorAdjustment } from "@core/adjustment-layer";
|
||||
import { getLayerMask } from "@core/layer-mask-utils";
|
||||
import type { ArtboardId } from "@core/id";
|
||||
import { createDocumentReadIndex, type DocumentReadIndex } from "@editor/document-indexes";
|
||||
@@ -10,7 +11,7 @@ import type { MaskEditState, SelectionState } from "@editor/state";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import { resolveLayerDrop } from "@input/index";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
import { addArtboard, addEmptyLayer, addGroupLayer, addLayerMask, deleteSelection, groupLayers, moveLayer } from "@operations/document/layerActions";
|
||||
import { addAdjustmentLayer, addArtboard, addEmptyLayer, addGroupLayer, addLayerMask, deleteSelection, groupLayers, moveLayer } from "@operations/document/layerActions";
|
||||
import { MaskOperationButtons, MaskStatus } from "./layers/MaskControls";
|
||||
import { LayerThumbnail } from "./layers/LayerThumbnail";
|
||||
import { createLayerThumbnailIndex, type LayerThumbnailModel } from "./layers/thumbnailModel";
|
||||
@@ -91,6 +92,9 @@ function LayersSheetBody({
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Add group" title="Add group" disabled={!selectedArtboardId} onClick={() => selectedArtboardId && addGroupLayer(selectedArtboardId, dispatch)}>
|
||||
<FolderPlus size={24} />
|
||||
</button>
|
||||
<button type="button" className={toolbarButtonClass()} aria-label="Add color adjustment" title="Add non-destructive artboard color adjustment" disabled={!selectedArtboardId} onClick={() => selectedArtboardId && addAdjustmentLayer(selectedArtboardId, dispatch)}>
|
||||
<SlidersHorizontal size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="mb-4 grid grid-cols-5 gap-2">
|
||||
@@ -110,6 +114,7 @@ function LayersSheetBody({
|
||||
<Trash size={24} />
|
||||
</button>
|
||||
</div>
|
||||
{selectedLayer?.layer.type === "adjustment" ? <AdjustmentInspector layer={selectedLayer.layer} dispatch={dispatch} /> : null}
|
||||
<div className="min-h-0 flex-1 overflow-auto pb-2">
|
||||
{document.artboards.map((artboard) => {
|
||||
const displayLayerCount = documentIndex.displayLayerCountByArtboardId.get(artboard.id) ?? 0;
|
||||
@@ -199,6 +204,61 @@ function LayersSheetBody({
|
||||
);
|
||||
}
|
||||
|
||||
function AdjustmentInspector({ layer, dispatch }: { layer: Extract<Layer, { type: "adjustment" }>; dispatch: AppStore["dispatch"] }) {
|
||||
const [draft, setDraft] = useState<ColorAdjustment>(() => cloneAdjustment(layer.adjustment));
|
||||
useEffect(() => setDraft(cloneAdjustment(layer.adjustment)), [layer.id, layer.adjustment]);
|
||||
|
||||
const fields = [
|
||||
["Brightness", "brightness", draft.brightness],
|
||||
["Contrast", "contrast", draft.contrast],
|
||||
["Saturation", "saturation", draft.saturation],
|
||||
["Red", "red", draft.colorBalance.red],
|
||||
["Green", "green", draft.colorBalance.green],
|
||||
["Blue", "blue", draft.colorBalance.blue],
|
||||
] as const;
|
||||
|
||||
const commit = () => {
|
||||
if (JSON.stringify(draft) === JSON.stringify(layer.adjustment)) return;
|
||||
dispatch(commandIds.documentSetAdjustment, { layerId: layer.id, adjustment: draft });
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="Adjustment settings" className="mb-3 rounded-[1.5rem] bg-white/[0.05] p-3">
|
||||
<p className="mb-2 text-xs text-white/45">
|
||||
Affects visible artboard layers beneath it. Adjustment layers stay at artboard level.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{fields.map(([label, key, value]) => (
|
||||
<label key={key} className="text-[0.7rem] text-white/55">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
className="mt-1 w-full accent-violet-300"
|
||||
type="range"
|
||||
min="-1"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={value}
|
||||
disabled={layer.locked}
|
||||
onChange={(event) => setDraft(withAdjustmentValue(draft, key, Number(event.currentTarget.value)))}
|
||||
onPointerUp={commit}
|
||||
onBlur={commit}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function cloneAdjustment(adjustment: ColorAdjustment): ColorAdjustment {
|
||||
return { ...adjustment, colorBalance: { ...adjustment.colorBalance } };
|
||||
}
|
||||
|
||||
function withAdjustmentValue(adjustment: ColorAdjustment, key: "brightness" | "contrast" | "saturation" | "red" | "green" | "blue", value: number): ColorAdjustment {
|
||||
if (key === "brightness" || key === "contrast" || key === "saturation") return { ...adjustment, [key]: value };
|
||||
return { ...adjustment, colorBalance: { ...adjustment.colorBalance, [key]: value } };
|
||||
}
|
||||
|
||||
function LayerRow({
|
||||
document,
|
||||
documentIndex,
|
||||
@@ -234,8 +294,8 @@ function LayerRow({
|
||||
const layerInfo = documentIndex.layerInfoById.get(layer.id);
|
||||
const layerMask = getLayerMask(layer);
|
||||
const maskLayer = layerMask ? documentIndex.layerById.get(layerMask.maskLayerId) : undefined;
|
||||
const maskAsset = maskLayer && maskLayer.type !== "group" ? documentIndex.assetById.get(maskLayer.assetId) : undefined;
|
||||
const canAddMask = Boolean(layerInfo && layer.type !== "group" && !layerMask);
|
||||
const maskAsset = maskLayer && maskLayer.type !== "group" && maskLayer.type !== "adjustment" ? documentIndex.assetById.get(maskLayer.assetId) : undefined;
|
||||
const canAddMask = Boolean(layerInfo && layer.type !== "group" && layer.type !== "adjustment" && !layerMask);
|
||||
const editingMask = Boolean(maskEdit && layerMask && maskEdit.targetLayerId === layer.id && maskEdit.maskLayerId === layerMask.maskLayerId);
|
||||
const thumbnail = thumbnailByLayerId.get(layer.id) ?? { kind: "empty" };
|
||||
const maskThumbnail = maskLayer ? thumbnailByLayerId.get(maskLayer.id) : undefined;
|
||||
@@ -354,7 +414,7 @@ function LayerRow({
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
{maskAsset && maskLayer.type !== "group" ? (
|
||||
{maskAsset && maskLayer.type !== "group" && maskLayer.type !== "adjustment" ? (
|
||||
<MaskOperationButtons maskLayerId={maskLayer.id} maskAsset={maskAsset} dispatch={dispatch} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user