@pixerate/editor-react 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/canvas/index.cjs +484 -119
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +122 -3
- package/dist/canvas/index.d.ts +122 -3
- package/dist/canvas/index.js +380 -0
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-I3E4KSA2.js +515 -0
- package/dist/chunk-I3E4KSA2.js.map +1 -0
- package/dist/chunk-JR5ZBDR7.js +2012 -0
- package/dist/chunk-JR5ZBDR7.js.map +1 -0
- package/dist/chunk-XWFU2CSG.js +167 -0
- package/dist/chunk-XWFU2CSG.js.map +1 -0
- package/dist/chunk-ZU54BMDC.js +2128 -0
- package/dist/chunk-ZU54BMDC.js.map +1 -0
- package/dist/generation/index.cjs +525 -0
- package/dist/generation/index.cjs.map +1 -0
- package/dist/generation/index.d.cts +124 -0
- package/dist/generation/index.d.ts +124 -0
- package/dist/generation/index.js +19 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/image-editor/index.cjs +988 -337
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +92 -2
- package/dist/image-editor/index.d.ts +92 -2
- package/dist/image-editor/index.js +17 -3
- package/dist/index.cjs +3628 -754
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +226 -5
- package/dist/index.d.ts +226 -5
- package/dist/index.js +868 -77
- package/dist/index.js.map +1 -1
- package/dist/persistence/index.cjs +191 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +77 -0
- package/dist/persistence/index.d.ts +77 -0
- package/dist/persistence/index.js +9 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/spreadsheet/index.cjs +949 -111
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +121 -7
- package/dist/spreadsheet/index.d.ts +121 -7
- package/dist/spreadsheet/index.js +11 -1
- package/dist/tokens/index.cjs +395 -0
- package/dist/tokens/index.cjs.map +1 -0
- package/dist/tokens/index.d.cts +54 -0
- package/dist/tokens/index.d.ts +54 -0
- package/dist/tokens/index.js +375 -0
- package/dist/tokens/index.js.map +1 -0
- package/package.json +26 -2
- package/dist/chunk-LPFZMZ3L.js +0 -1461
- package/dist/chunk-LPFZMZ3L.js.map +0 -1
- package/dist/chunk-QVQYYHJ5.js +0 -1161
- package/dist/chunk-QVQYYHJ5.js.map +0 -1
|
@@ -0,0 +1,2128 @@
|
|
|
1
|
+
// src/image-editor/useImageEditor.ts
|
|
2
|
+
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
ImageEditorController
|
|
5
|
+
} from "@pixerate/editor/image-editor";
|
|
6
|
+
function useImageEditor(options = {}) {
|
|
7
|
+
const controllerRef = useRef(null);
|
|
8
|
+
if (!controllerRef.current) {
|
|
9
|
+
controllerRef.current = new ImageEditorController(options);
|
|
10
|
+
}
|
|
11
|
+
const controller = controllerRef.current;
|
|
12
|
+
const [state, setState] = useState(() => controller.getState());
|
|
13
|
+
const [historyTicks, setHistoryTicks] = useState(0);
|
|
14
|
+
const [histogram, setHistogram] = useState(() => controller.getHistogram());
|
|
15
|
+
useEffect(() => controller.subscribeHistogram(setHistogram), [controller]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const unsub = controller.subscribe((next) => {
|
|
18
|
+
setState(next);
|
|
19
|
+
setHistoryTicks((t) => t + 1);
|
|
20
|
+
});
|
|
21
|
+
return unsub;
|
|
22
|
+
}, [controller]);
|
|
23
|
+
const loadedImageRef = useRef(options.image);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (options.image === loadedImageRef.current) return;
|
|
26
|
+
loadedImageRef.current = options.image;
|
|
27
|
+
if (options.image) {
|
|
28
|
+
controller.loadImage(options.image);
|
|
29
|
+
}
|
|
30
|
+
}, [options.image, controller]);
|
|
31
|
+
const setTool = useCallback((tool) => controller.setTool(tool), [controller]);
|
|
32
|
+
const setCrop = useCallback(
|
|
33
|
+
(crop, addToHistory = true) => controller.setCrop(crop, addToHistory),
|
|
34
|
+
[controller]
|
|
35
|
+
);
|
|
36
|
+
const applyCropPreset = useCallback((ratio) => controller.applyCropPreset(ratio), [controller]);
|
|
37
|
+
const resetCrop = useCallback(() => controller.resetCrop(), [controller]);
|
|
38
|
+
const rotate = useCallback((deg) => controller.rotate(deg), [controller]);
|
|
39
|
+
const setRotation = useCallback((deg) => controller.setRotation(deg), [controller]);
|
|
40
|
+
const flipHorizontal = useCallback(() => controller.flipHorizontal(), [controller]);
|
|
41
|
+
const flipVertical = useCallback(() => controller.flipVertical(), [controller]);
|
|
42
|
+
const setAdjustments = useCallback(
|
|
43
|
+
(partial, commitToHistory = true) => controller.setAdjustments(partial, commitToHistory),
|
|
44
|
+
[controller]
|
|
45
|
+
);
|
|
46
|
+
const resetAdjustments = useCallback(() => controller.resetAdjustments(), [controller]);
|
|
47
|
+
const upgradeProcess = useCallback(() => controller.upgradeProcess(), [controller]);
|
|
48
|
+
const addAdjustmentLayer = useCallback(
|
|
49
|
+
(init, index) => controller.addAdjustmentLayer(init, index),
|
|
50
|
+
[controller]
|
|
51
|
+
);
|
|
52
|
+
const updateAdjustmentLayer = useCallback(
|
|
53
|
+
(id, update, commitToHistory = true) => controller.updateAdjustmentLayer(id, update, commitToHistory),
|
|
54
|
+
[controller]
|
|
55
|
+
);
|
|
56
|
+
const removeAdjustmentLayer = useCallback((id) => controller.removeAdjustmentLayer(id), [controller]);
|
|
57
|
+
const moveAdjustmentLayer = useCallback((id, to) => controller.moveAdjustmentLayer(id, to), [controller]);
|
|
58
|
+
const toggleAdjustmentLayer = useCallback(
|
|
59
|
+
(id, enabled) => controller.toggleAdjustmentLayer(id, enabled),
|
|
60
|
+
[controller]
|
|
61
|
+
);
|
|
62
|
+
const setDepthMask = useCallback((partial) => controller.setDepthMask(partial), [controller]);
|
|
63
|
+
const resetDepthMask = useCallback(() => controller.resetDepthMask(), [controller]);
|
|
64
|
+
const addAnnotation = useCallback((ann) => controller.addAnnotation(ann), [controller]);
|
|
65
|
+
const updateAnnotation = useCallback(
|
|
66
|
+
(id, partial, commitToHistory = true) => controller.updateAnnotation(id, partial, commitToHistory),
|
|
67
|
+
[controller]
|
|
68
|
+
);
|
|
69
|
+
const removeAnnotation = useCallback((id) => controller.removeAnnotation(id), [controller]);
|
|
70
|
+
const clearAnnotations = useCallback(() => controller.clearAnnotations(), [controller]);
|
|
71
|
+
const selectAnnotation = useCallback((id) => controller.selectAnnotation(id), [controller]);
|
|
72
|
+
const undo = useCallback(() => controller.undo(), [controller]);
|
|
73
|
+
const redo = useCallback(() => controller.redo(), [controller]);
|
|
74
|
+
const reset = useCallback(() => controller.reset(), [controller]);
|
|
75
|
+
const zoomIn = useCallback((factor) => controller.zoomIn(factor), [controller]);
|
|
76
|
+
const zoomOut = useCallback((factor) => controller.zoomOut(factor), [controller]);
|
|
77
|
+
const resetZoom = useCallback(() => controller.resetZoom(), [controller]);
|
|
78
|
+
const setZoom = useCallback((zoom) => controller.setZoom(zoom), [controller]);
|
|
79
|
+
const setPan = useCallback((pan) => controller.setPan(pan), [controller]);
|
|
80
|
+
const setComparing = useCallback((comp) => controller.setComparing(comp), [controller]);
|
|
81
|
+
const loadImage = useCallback((src) => controller.loadImage(src), [controller]);
|
|
82
|
+
const toDataURL = useCallback((opts) => controller.toDataURL(opts), [controller]);
|
|
83
|
+
const toBlob = useCallback((opts) => controller.toBlob(opts), [controller]);
|
|
84
|
+
const toJSON = useCallback(() => controller.toJSON(), [controller]);
|
|
85
|
+
const loadJSON = useCallback((data) => controller.loadJSON(data), [controller]);
|
|
86
|
+
const renderMask = useCallback(
|
|
87
|
+
(opts) => controller.renderMask(opts),
|
|
88
|
+
[controller]
|
|
89
|
+
);
|
|
90
|
+
const toMaskDataURL = useCallback(
|
|
91
|
+
(opts) => controller.toMaskDataURL(opts),
|
|
92
|
+
[controller]
|
|
93
|
+
);
|
|
94
|
+
const toSourceDataURL = useCallback(
|
|
95
|
+
(opts) => controller.toSourceDataURL(opts),
|
|
96
|
+
[controller]
|
|
97
|
+
);
|
|
98
|
+
const applyInpaintedImage = useCallback(
|
|
99
|
+
(newDataUrl) => controller.applyInpaintedImage(newDataUrl),
|
|
100
|
+
[controller]
|
|
101
|
+
);
|
|
102
|
+
const applyBackgroundRemovedImage = useCallback(
|
|
103
|
+
(newDataUrl) => controller.applyBackgroundRemovedImage(newDataUrl),
|
|
104
|
+
[controller]
|
|
105
|
+
);
|
|
106
|
+
return useMemo(
|
|
107
|
+
() => ({
|
|
108
|
+
controller,
|
|
109
|
+
state,
|
|
110
|
+
canUndo: controller.canUndo,
|
|
111
|
+
canRedo: controller.canRedo,
|
|
112
|
+
setTool,
|
|
113
|
+
setCrop,
|
|
114
|
+
applyCropPreset,
|
|
115
|
+
resetCrop,
|
|
116
|
+
rotate,
|
|
117
|
+
setRotation,
|
|
118
|
+
flipHorizontal,
|
|
119
|
+
flipVertical,
|
|
120
|
+
setAdjustments,
|
|
121
|
+
resetAdjustments,
|
|
122
|
+
upgradeProcess,
|
|
123
|
+
addAdjustmentLayer,
|
|
124
|
+
updateAdjustmentLayer,
|
|
125
|
+
removeAdjustmentLayer,
|
|
126
|
+
moveAdjustmentLayer,
|
|
127
|
+
toggleAdjustmentLayer,
|
|
128
|
+
histogram,
|
|
129
|
+
setDepthMask,
|
|
130
|
+
resetDepthMask,
|
|
131
|
+
addAnnotation,
|
|
132
|
+
updateAnnotation,
|
|
133
|
+
removeAnnotation,
|
|
134
|
+
clearAnnotations,
|
|
135
|
+
selectAnnotation,
|
|
136
|
+
undo,
|
|
137
|
+
redo,
|
|
138
|
+
reset,
|
|
139
|
+
zoomIn,
|
|
140
|
+
zoomOut,
|
|
141
|
+
resetZoom,
|
|
142
|
+
setZoom,
|
|
143
|
+
setPan,
|
|
144
|
+
setComparing,
|
|
145
|
+
loadImage,
|
|
146
|
+
toDataURL,
|
|
147
|
+
toBlob,
|
|
148
|
+
toJSON,
|
|
149
|
+
loadJSON,
|
|
150
|
+
renderMask,
|
|
151
|
+
toMaskDataURL,
|
|
152
|
+
toSourceDataURL,
|
|
153
|
+
applyInpaintedImage,
|
|
154
|
+
applyBackgroundRemovedImage
|
|
155
|
+
}),
|
|
156
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
157
|
+
[controller, state, historyTicks, histogram]
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/image-editor/ImageEditor.tsx
|
|
162
|
+
import React6, { useRef as useRef3, useEffect as useEffect4, useState as useState7, useCallback as useCallback2 } from "react";
|
|
163
|
+
import {
|
|
164
|
+
canvasToImagePoint,
|
|
165
|
+
hitTestCrop,
|
|
166
|
+
getCropCursor,
|
|
167
|
+
calculateCropDrag,
|
|
168
|
+
hitTestAnnotation,
|
|
169
|
+
normalizeProcessVersion,
|
|
170
|
+
LATEST_PROCESS_VERSION,
|
|
171
|
+
IMAGE_ADJUSTMENT_CONTROLS,
|
|
172
|
+
readAdjustmentControls,
|
|
173
|
+
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
174
|
+
SelectionGesture
|
|
175
|
+
} from "@pixerate/editor/image-editor";
|
|
176
|
+
|
|
177
|
+
// src/image-editor/ControlSlider.tsx
|
|
178
|
+
import { controlTrackBackground, formatControlValue } from "@pixerate/editor/image-editor";
|
|
179
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
180
|
+
var ControlSlider = ({ spec, value, onChange, disabled, label }) => {
|
|
181
|
+
const name = label ?? spec.label;
|
|
182
|
+
const track = controlTrackBackground(spec);
|
|
183
|
+
return /* @__PURE__ */ jsxs("div", { "data-control-id": spec.id, children: [
|
|
184
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-slate-400 mb-1", children: [
|
|
185
|
+
/* @__PURE__ */ jsx("span", { children: name }),
|
|
186
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: formatControlValue(spec, value) })
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
189
|
+
"input",
|
|
190
|
+
{
|
|
191
|
+
type: "range",
|
|
192
|
+
min: spec.min,
|
|
193
|
+
max: spec.max,
|
|
194
|
+
step: spec.step,
|
|
195
|
+
"aria-label": name,
|
|
196
|
+
disabled,
|
|
197
|
+
value,
|
|
198
|
+
onChange: (e) => onChange(Number(e.target.value)),
|
|
199
|
+
className: "w-full accent-indigo-500 disabled:opacity-40"
|
|
200
|
+
}
|
|
201
|
+
),
|
|
202
|
+
track && /* @__PURE__ */ jsx("div", { className: "h-0.5 rounded-full opacity-70", style: { background: track } })
|
|
203
|
+
] });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/image-editor/Histogram.tsx
|
|
207
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
208
|
+
import { histogramPath } from "@pixerate/editor/image-editor";
|
|
209
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
210
|
+
var FILL = {
|
|
211
|
+
luminance: "rgba(148, 163, 184, 0.45)",
|
|
212
|
+
red: "rgba(239, 68, 68, 0.45)",
|
|
213
|
+
green: "rgba(34, 197, 94, 0.45)",
|
|
214
|
+
blue: "rgba(59, 130, 246, 0.45)"
|
|
215
|
+
};
|
|
216
|
+
var W = 256;
|
|
217
|
+
var Histogram = ({ histogram, channel = "rgb", height = 64, className = "", style }) => {
|
|
218
|
+
const channels = channel === "rgb" ? ["luminance", "red", "green", "blue"] : [channel];
|
|
219
|
+
const paths = useMemo2(
|
|
220
|
+
() => histogram && histogram.samples > 0 ? channels.map((c) => ({ c, d: histogramPath(histogram, c, W, height) })) : [],
|
|
221
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
222
|
+
[histogram, channel, height]
|
|
223
|
+
);
|
|
224
|
+
const clipLow = histogram && histogram.samples ? Math.max(...histogram.shadows) / histogram.samples : 0;
|
|
225
|
+
const clipHigh = histogram && histogram.samples ? Math.max(...histogram.highlights) / histogram.samples : 0;
|
|
226
|
+
return /* @__PURE__ */ jsxs2(
|
|
227
|
+
"svg",
|
|
228
|
+
{
|
|
229
|
+
viewBox: `0 0 ${W} ${height}`,
|
|
230
|
+
preserveAspectRatio: "none",
|
|
231
|
+
role: "img",
|
|
232
|
+
"aria-label": histogram ? `Histogram of ${histogram.samples} pixels` : "Histogram (no data)",
|
|
233
|
+
"data-testid": "image-editor-histogram",
|
|
234
|
+
className: `w-full rounded bg-slate-950/60 ${className}`,
|
|
235
|
+
style: { height, ...style },
|
|
236
|
+
children: [
|
|
237
|
+
paths.map(({ c, d }) => /* @__PURE__ */ jsx2("path", { d, fill: FILL[c], stroke: "none" }, c)),
|
|
238
|
+
clipLow > 5e-3 && /* @__PURE__ */ jsx2("rect", { x: 0, y: 0, width: 3, height, fill: "#3b82f6", "data-testid": "histogram-clip-shadows" }),
|
|
239
|
+
clipHigh > 5e-3 && /* @__PURE__ */ jsx2("rect", { x: W - 3, y: 0, width: 3, height, fill: "#ef4444", "data-testid": "histogram-clip-highlights" })
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
function useImageHistogram(controller) {
|
|
245
|
+
const [histogram, setHistogram] = useState2(() => controller.getHistogram());
|
|
246
|
+
useEffect2(() => controller.subscribeHistogram(setHistogram), [controller]);
|
|
247
|
+
return histogram;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/image-editor/AdjustmentLayersPanel.tsx
|
|
251
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
252
|
+
import {
|
|
253
|
+
ADJUSTMENT_KINDS,
|
|
254
|
+
ADJUSTMENT_LAYER_CONTROLS,
|
|
255
|
+
ADJUSTMENT_LAYER_OPACITY_CONTROL,
|
|
256
|
+
BLEND_MODES,
|
|
257
|
+
HUE_RANGE_LABELS,
|
|
258
|
+
adjustmentKindLabel,
|
|
259
|
+
getControlPath,
|
|
260
|
+
hueRangeWeight,
|
|
261
|
+
setControlPath
|
|
262
|
+
} from "@pixerate/editor/image-editor";
|
|
263
|
+
|
|
264
|
+
// src/image-editor/CurvesEditor.tsx
|
|
265
|
+
import { useRef as useRef2, useState as useState3 } from "react";
|
|
266
|
+
import {
|
|
267
|
+
boxToCurvePoint,
|
|
268
|
+
curvePointToBox,
|
|
269
|
+
curveSvgPath,
|
|
270
|
+
hitTestCurvePoint,
|
|
271
|
+
histogramPath as histogramPath2,
|
|
272
|
+
insertCurvePoint,
|
|
273
|
+
moveCurvePoint,
|
|
274
|
+
removeCurvePoint
|
|
275
|
+
} from "@pixerate/editor/image-editor";
|
|
276
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
277
|
+
var BOX = 256;
|
|
278
|
+
var STROKE = { master: "#e2e8f0", red: "#ef4444", green: "#22c55e", blue: "#3b82f6" };
|
|
279
|
+
var CurvesEditor = ({ points, onChange, histogram, channel = "master", size = 200 }) => {
|
|
280
|
+
const svgRef = useRef2(null);
|
|
281
|
+
const [drag, setDrag] = useState3(null);
|
|
282
|
+
const toBox = (e) => {
|
|
283
|
+
const rect = svgRef.current?.getBoundingClientRect();
|
|
284
|
+
if (!rect || !rect.width || !rect.height) return { x: 0, y: 0 };
|
|
285
|
+
return { x: (e.clientX - rect.left) / rect.width * BOX, y: (e.clientY - rect.top) / rect.height * BOX };
|
|
286
|
+
};
|
|
287
|
+
const onPointerDown = (e) => {
|
|
288
|
+
const { x, y } = toBox(e);
|
|
289
|
+
let index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
|
|
290
|
+
if (index === -1) {
|
|
291
|
+
const inserted = insertCurvePoint(points, boxToCurvePoint(x, y, BOX, BOX));
|
|
292
|
+
onChange(inserted.points, true);
|
|
293
|
+
index = inserted.index;
|
|
294
|
+
} else {
|
|
295
|
+
onChange(points, true);
|
|
296
|
+
}
|
|
297
|
+
setDrag(index);
|
|
298
|
+
e.target.setPointerCapture?.(e.pointerId);
|
|
299
|
+
};
|
|
300
|
+
const onPointerMove = (e) => {
|
|
301
|
+
if (drag === null) return;
|
|
302
|
+
onChange(moveCurvePoint(points, drag, boxToCurvePoint(toBox(e).x, toBox(e).y, BOX, BOX)), false);
|
|
303
|
+
};
|
|
304
|
+
const onDoubleClick = (e) => {
|
|
305
|
+
const { x, y } = toBox(e);
|
|
306
|
+
const index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
|
|
307
|
+
if (index !== -1 && points.length > 2) onChange(removeCurvePoint(points, index), true);
|
|
308
|
+
};
|
|
309
|
+
const histChannel = channel === "master" ? "luminance" : channel;
|
|
310
|
+
return /* @__PURE__ */ jsxs3(
|
|
311
|
+
"svg",
|
|
312
|
+
{
|
|
313
|
+
ref: svgRef,
|
|
314
|
+
viewBox: `0 0 ${BOX} ${BOX}`,
|
|
315
|
+
width: size,
|
|
316
|
+
height: size,
|
|
317
|
+
role: "application",
|
|
318
|
+
"aria-label": `${channel} curve`,
|
|
319
|
+
"data-testid": "curves-editor",
|
|
320
|
+
className: "rounded bg-slate-950/70 touch-none select-none cursor-crosshair",
|
|
321
|
+
onPointerDown,
|
|
322
|
+
onPointerMove,
|
|
323
|
+
onPointerUp: () => setDrag(null),
|
|
324
|
+
onPointerCancel: () => setDrag(null),
|
|
325
|
+
onDoubleClick,
|
|
326
|
+
children: [
|
|
327
|
+
histogram && histogram.samples > 0 && /* @__PURE__ */ jsx3("path", { d: histogramPath2(histogram, histChannel, BOX, BOX), fill: "rgba(148, 163, 184, 0.25)" }),
|
|
328
|
+
[64, 128, 192].map((v) => /* @__PURE__ */ jsxs3("g", { stroke: "rgba(148, 163, 184, 0.2)", strokeWidth: 1, children: [
|
|
329
|
+
/* @__PURE__ */ jsx3("line", { x1: v, y1: 0, x2: v, y2: BOX }),
|
|
330
|
+
/* @__PURE__ */ jsx3("line", { x1: 0, y1: v, x2: BOX, y2: v })
|
|
331
|
+
] }, v)),
|
|
332
|
+
/* @__PURE__ */ jsx3("line", { x1: 0, y1: BOX, x2: BOX, y2: 0, stroke: "rgba(148, 163, 184, 0.35)", strokeDasharray: "4 4" }),
|
|
333
|
+
/* @__PURE__ */ jsx3("path", { d: curveSvgPath(points, BOX, BOX), fill: "none", stroke: STROKE[channel], strokeWidth: 2 }),
|
|
334
|
+
points.map((p, i) => {
|
|
335
|
+
const b = curvePointToBox(p, BOX, BOX);
|
|
336
|
+
return /* @__PURE__ */ jsx3(
|
|
337
|
+
"circle",
|
|
338
|
+
{
|
|
339
|
+
cx: b.x,
|
|
340
|
+
cy: b.y,
|
|
341
|
+
r: 5,
|
|
342
|
+
fill: drag === i ? STROKE[channel] : "#0f172a",
|
|
343
|
+
stroke: STROKE[channel],
|
|
344
|
+
strokeWidth: 2,
|
|
345
|
+
"aria-label": `Point ${p.input} to ${p.output}`
|
|
346
|
+
},
|
|
347
|
+
i
|
|
348
|
+
);
|
|
349
|
+
})
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/image-editor/AdjustmentLayersPanel.tsx
|
|
356
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
357
|
+
var CHANNELS = [
|
|
358
|
+
{ id: "master", label: "RGB" },
|
|
359
|
+
{ id: "red", label: "Red" },
|
|
360
|
+
{ id: "green", label: "Green" },
|
|
361
|
+
{ id: "blue", label: "Blue" }
|
|
362
|
+
];
|
|
363
|
+
var TONES = [
|
|
364
|
+
{ id: "shadows", label: "Shadows" },
|
|
365
|
+
{ id: "midtones", label: "Midtones" },
|
|
366
|
+
{ id: "highlights", label: "Highlights" }
|
|
367
|
+
];
|
|
368
|
+
var selectClass = "bg-slate-800 border border-slate-700 rounded px-1.5 py-1 text-slate-200";
|
|
369
|
+
var chipClass = (on) => `px-2 py-0.5 rounded text-[10px] ${on ? "bg-indigo-600 text-white" : "bg-slate-800 text-slate-400 hover:text-slate-200"}`;
|
|
370
|
+
var HueRangeStrip = ({ bounds }) => {
|
|
371
|
+
const pts = Array.from({ length: 73 }, (_, i) => `${i / 72 * 360},${20 - hueRangeWeight({ bounds }, i * 5) * 18}`).join(" ");
|
|
372
|
+
return /* @__PURE__ */ jsxs4("svg", { viewBox: "0 0 360 20", preserveAspectRatio: "none", className: "w-full h-4 rounded", "data-testid": "hue-range-strip", children: [
|
|
373
|
+
/* @__PURE__ */ jsx4("defs", { children: /* @__PURE__ */ jsx4("linearGradient", { id: "pixerate-hue-strip", children: [0, 60, 120, 180, 240, 300, 360].map((h) => /* @__PURE__ */ jsx4("stop", { offset: `${h / 360 * 100}%`, stopColor: `hsl(${h}, 90%, 50%)` }, h)) }) }),
|
|
374
|
+
/* @__PURE__ */ jsx4("rect", { width: 360, height: 20, fill: "url(#pixerate-hue-strip)", opacity: 0.6 }),
|
|
375
|
+
/* @__PURE__ */ jsx4("polyline", { points: pts, fill: "none", stroke: "#fff", strokeWidth: 1.5 })
|
|
376
|
+
] });
|
|
377
|
+
};
|
|
378
|
+
var AdjustmentLayersPanel = ({ controller, layers: layersProp, histogram }) => {
|
|
379
|
+
const follow = layersProp === void 0;
|
|
380
|
+
const [, setTick] = useState4(0);
|
|
381
|
+
useEffect3(() => follow ? controller.subscribe(() => setTick((n) => n + 1)) : void 0, [controller, follow]);
|
|
382
|
+
const layers = layersProp ?? controller.getAdjustmentLayers();
|
|
383
|
+
const [newKind, setNewKind] = useState4("curves");
|
|
384
|
+
const [selectedId, setSelectedId] = useState4(null);
|
|
385
|
+
const [channel, setChannel] = useState4("master");
|
|
386
|
+
const [range, setRange] = useState4(-1);
|
|
387
|
+
const [tone, setTone] = useState4("midtones");
|
|
388
|
+
const selected = layers.find((l) => l.id === selectedId) ?? null;
|
|
389
|
+
const topFirst = [...layers].reverse();
|
|
390
|
+
const setParam = (layer, path, value, commit = true) => controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) }, commit);
|
|
391
|
+
const slider = (layer, spec, label) => /* @__PURE__ */ jsx4(
|
|
392
|
+
ControlSlider,
|
|
393
|
+
{
|
|
394
|
+
spec,
|
|
395
|
+
label,
|
|
396
|
+
value: Number(getControlPath(layer.params, spec.id) ?? spec.default),
|
|
397
|
+
onChange: (v) => setParam(layer, spec.id, v)
|
|
398
|
+
},
|
|
399
|
+
spec.id
|
|
400
|
+
);
|
|
401
|
+
const controlsOf = (layer, filter = () => true) => ADJUSTMENT_LAYER_CONTROLS[layer.kind].filter(filter);
|
|
402
|
+
const checkbox = (layer, path, label) => /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
403
|
+
/* @__PURE__ */ jsx4(
|
|
404
|
+
"input",
|
|
405
|
+
{
|
|
406
|
+
type: "checkbox",
|
|
407
|
+
checked: Boolean(getControlPath(layer.params, path)),
|
|
408
|
+
onChange: (e) => setParam(layer, path, e.target.checked),
|
|
409
|
+
className: "accent-indigo-500"
|
|
410
|
+
}
|
|
411
|
+
),
|
|
412
|
+
label
|
|
413
|
+
] });
|
|
414
|
+
const editor = (layer) => {
|
|
415
|
+
switch (layer.kind) {
|
|
416
|
+
case "curves":
|
|
417
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
418
|
+
/* @__PURE__ */ jsx4("div", { className: "flex gap-1", role: "group", "aria-label": "Curve channel", children: CHANNELS.map((c) => /* @__PURE__ */ jsx4("button", { type: "button", className: chipClass(channel === c.id), onClick: () => setChannel(c.id), children: c.label }, c.id)) }),
|
|
419
|
+
/* @__PURE__ */ jsx4(
|
|
420
|
+
CurvesEditor,
|
|
421
|
+
{
|
|
422
|
+
points: layer.params[channel],
|
|
423
|
+
channel,
|
|
424
|
+
histogram,
|
|
425
|
+
onChange: (points, commit) => setParam(layer, channel, points, commit)
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
] });
|
|
429
|
+
case "levels":
|
|
430
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
431
|
+
/* @__PURE__ */ jsx4("div", { className: "flex gap-1", role: "group", "aria-label": "Levels channel", children: CHANNELS.map((c) => /* @__PURE__ */ jsx4("button", { type: "button", className: chipClass(channel === c.id), onClick: () => setChannel(c.id), children: c.label }, c.id)) }),
|
|
432
|
+
controlsOf(layer, (s) => s.id.startsWith(`${channel}.`)).map((s) => slider(layer, s))
|
|
433
|
+
] });
|
|
434
|
+
case "hueSaturation":
|
|
435
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
436
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Hue range", children: [
|
|
437
|
+
/* @__PURE__ */ jsx4("button", { type: "button", className: chipClass(range === -1), onClick: () => setRange(-1), children: "Master" }),
|
|
438
|
+
HUE_RANGE_LABELS.map((name, i) => /* @__PURE__ */ jsx4("button", { type: "button", className: chipClass(range === i), onClick: () => setRange(i), disabled: layer.params.colorize, children: name }, name))
|
|
439
|
+
] }),
|
|
440
|
+
range >= 0 && !layer.params.colorize && /* @__PURE__ */ jsx4(HueRangeStrip, { bounds: layer.params.ranges[range].bounds }),
|
|
441
|
+
controlsOf(layer, (s) => range === -1 || layer.params.colorize ? !s.id.startsWith("ranges.") : s.id.startsWith(`ranges.${range}.`)).map(
|
|
442
|
+
(s) => slider(layer, s)
|
|
443
|
+
),
|
|
444
|
+
range === -1 && checkbox(layer, "colorize", "Colorize")
|
|
445
|
+
] });
|
|
446
|
+
case "colorBalance":
|
|
447
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
448
|
+
/* @__PURE__ */ jsx4("div", { className: "flex gap-1", role: "group", "aria-label": "Tone", children: TONES.map((t) => /* @__PURE__ */ jsx4("button", { type: "button", className: chipClass(tone === t.id), onClick: () => setTone(t.id), children: t.label }, t.id)) }),
|
|
449
|
+
controlsOf(layer, (s) => s.id.startsWith(`${tone}.`)).map((s) => slider(layer, s)),
|
|
450
|
+
checkbox(layer, "preserveLuminosity", "Preserve luminosity")
|
|
451
|
+
] });
|
|
452
|
+
case "blackWhite":
|
|
453
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
454
|
+
controlsOf(layer).map((s) => slider(layer, s)),
|
|
455
|
+
/* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
456
|
+
/* @__PURE__ */ jsx4(
|
|
457
|
+
"input",
|
|
458
|
+
{
|
|
459
|
+
type: "checkbox",
|
|
460
|
+
checked: layer.params.tint !== null,
|
|
461
|
+
onChange: (e) => setParam(layer, "tint", e.target.checked ? "#e1d3b3" : null),
|
|
462
|
+
className: "accent-indigo-500"
|
|
463
|
+
}
|
|
464
|
+
),
|
|
465
|
+
"Tint",
|
|
466
|
+
layer.params.tint && /* @__PURE__ */ jsx4(
|
|
467
|
+
"input",
|
|
468
|
+
{
|
|
469
|
+
type: "color",
|
|
470
|
+
"aria-label": "Tint colour",
|
|
471
|
+
value: layer.params.tint,
|
|
472
|
+
onChange: (e) => setParam(layer, "tint", e.target.value),
|
|
473
|
+
className: "h-5 w-8 bg-transparent"
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
] })
|
|
477
|
+
] });
|
|
478
|
+
case "photoFilter":
|
|
479
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
|
|
480
|
+
/* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
481
|
+
"Filter colour",
|
|
482
|
+
/* @__PURE__ */ jsx4(
|
|
483
|
+
"input",
|
|
484
|
+
{
|
|
485
|
+
type: "color",
|
|
486
|
+
"aria-label": "Filter colour",
|
|
487
|
+
value: layer.params.color,
|
|
488
|
+
onChange: (e) => setParam(layer, "color", e.target.value),
|
|
489
|
+
className: "h-5 w-8 bg-transparent"
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
] }),
|
|
493
|
+
controlsOf(layer).map((s) => slider(layer, s)),
|
|
494
|
+
checkbox(layer, "preserveLuminosity", "Preserve luminosity")
|
|
495
|
+
] });
|
|
496
|
+
case "invert":
|
|
497
|
+
return /* @__PURE__ */ jsx4("p", { className: "text-slate-500 text-[11px]", children: "Invert has no settings." });
|
|
498
|
+
default:
|
|
499
|
+
return /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-2", children: controlsOf(layer).map((s) => slider(layer, s)) });
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2 text-xs", "data-testid": "adjustment-layers-panel", children: [
|
|
503
|
+
/* @__PURE__ */ jsx4("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsx4("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Adjustment Layers" }) }),
|
|
504
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex gap-1.5", children: [
|
|
505
|
+
/* @__PURE__ */ jsx4(
|
|
506
|
+
"select",
|
|
507
|
+
{
|
|
508
|
+
"aria-label": "New adjustment layer kind",
|
|
509
|
+
className: `${selectClass} flex-1`,
|
|
510
|
+
value: newKind,
|
|
511
|
+
onChange: (e) => setNewKind(e.target.value),
|
|
512
|
+
children: ADJUSTMENT_KINDS.map((k) => /* @__PURE__ */ jsx4("option", { value: k.kind, children: k.label }, k.kind))
|
|
513
|
+
}
|
|
514
|
+
),
|
|
515
|
+
/* @__PURE__ */ jsx4(
|
|
516
|
+
"button",
|
|
517
|
+
{
|
|
518
|
+
type: "button",
|
|
519
|
+
className: "px-2 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-500",
|
|
520
|
+
onClick: () => {
|
|
521
|
+
setSelectedId(controller.addAdjustmentLayer(newKind));
|
|
522
|
+
setChannel("master");
|
|
523
|
+
setRange(-1);
|
|
524
|
+
},
|
|
525
|
+
children: "Add Layer"
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
] }),
|
|
529
|
+
topFirst.length > 0 && /* @__PURE__ */ jsx4("ul", { className: "flex flex-col gap-1", "aria-label": "Adjustment layers", children: topFirst.map((layer) => {
|
|
530
|
+
const index = layers.indexOf(layer);
|
|
531
|
+
const label = layer.name || adjustmentKindLabel(layer.kind);
|
|
532
|
+
return /* @__PURE__ */ jsxs4(
|
|
533
|
+
"li",
|
|
534
|
+
{
|
|
535
|
+
className: `flex items-center gap-1.5 rounded px-1.5 py-1 ${layer.id === selectedId ? "bg-indigo-600/20 ring-1 ring-indigo-500/50" : "bg-slate-800/60"}`,
|
|
536
|
+
children: [
|
|
537
|
+
/* @__PURE__ */ jsx4(
|
|
538
|
+
"input",
|
|
539
|
+
{
|
|
540
|
+
type: "checkbox",
|
|
541
|
+
"aria-label": `Show ${label}`,
|
|
542
|
+
checked: layer.enabled,
|
|
543
|
+
onChange: () => controller.toggleAdjustmentLayer(layer.id),
|
|
544
|
+
className: "accent-indigo-500"
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
/* @__PURE__ */ jsx4(
|
|
548
|
+
"button",
|
|
549
|
+
{
|
|
550
|
+
type: "button",
|
|
551
|
+
className: `flex-1 text-left truncate ${layer.enabled ? "text-slate-200" : "text-slate-500 line-through"}`,
|
|
552
|
+
onClick: () => setSelectedId(layer.id === selectedId ? null : layer.id),
|
|
553
|
+
"aria-pressed": layer.id === selectedId,
|
|
554
|
+
children: label
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
/* @__PURE__ */ jsx4(
|
|
558
|
+
"button",
|
|
559
|
+
{
|
|
560
|
+
type: "button",
|
|
561
|
+
"aria-label": `Move ${label} up`,
|
|
562
|
+
disabled: index === layers.length - 1,
|
|
563
|
+
onClick: () => controller.moveAdjustmentLayer(layer.id, index + 1),
|
|
564
|
+
className: "px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30",
|
|
565
|
+
children: "\u2191"
|
|
566
|
+
}
|
|
567
|
+
),
|
|
568
|
+
/* @__PURE__ */ jsx4(
|
|
569
|
+
"button",
|
|
570
|
+
{
|
|
571
|
+
type: "button",
|
|
572
|
+
"aria-label": `Move ${label} down`,
|
|
573
|
+
disabled: index === 0,
|
|
574
|
+
onClick: () => controller.moveAdjustmentLayer(layer.id, index - 1),
|
|
575
|
+
className: "px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30",
|
|
576
|
+
children: "\u2193"
|
|
577
|
+
}
|
|
578
|
+
),
|
|
579
|
+
/* @__PURE__ */ jsx4(
|
|
580
|
+
"button",
|
|
581
|
+
{
|
|
582
|
+
type: "button",
|
|
583
|
+
"aria-label": `Remove ${label}`,
|
|
584
|
+
onClick: () => {
|
|
585
|
+
controller.removeAdjustmentLayer(layer.id);
|
|
586
|
+
if (selectedId === layer.id) setSelectedId(null);
|
|
587
|
+
},
|
|
588
|
+
className: "px-1 text-rose-400 hover:text-rose-300",
|
|
589
|
+
children: "\u2715"
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
]
|
|
593
|
+
},
|
|
594
|
+
layer.id
|
|
595
|
+
);
|
|
596
|
+
}) }),
|
|
597
|
+
selected && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2 rounded border border-slate-700/60 p-2", "data-testid": "adjustment-layer-editor", children: [
|
|
598
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
|
|
599
|
+
/* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Blend" }),
|
|
600
|
+
/* @__PURE__ */ jsx4(
|
|
601
|
+
"select",
|
|
602
|
+
{
|
|
603
|
+
"aria-label": "Blend mode",
|
|
604
|
+
className: `${selectClass} flex-1`,
|
|
605
|
+
value: selected.blend,
|
|
606
|
+
onChange: (e) => controller.updateAdjustmentLayer(selected.id, { blend: e.target.value }),
|
|
607
|
+
children: BLEND_MODES.map((m) => /* @__PURE__ */ jsx4("option", { value: m.id, children: m.label }, m.id))
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsx4(
|
|
612
|
+
ControlSlider,
|
|
613
|
+
{
|
|
614
|
+
spec: ADJUSTMENT_LAYER_OPACITY_CONTROL,
|
|
615
|
+
label: "Layer opacity",
|
|
616
|
+
value: selected.opacity,
|
|
617
|
+
onChange: (v) => controller.updateAdjustmentLayer(selected.id, { opacity: v })
|
|
618
|
+
}
|
|
619
|
+
),
|
|
620
|
+
editor(selected)
|
|
621
|
+
] })
|
|
622
|
+
] });
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// src/image-editor/SelectionOptionsBar.tsx
|
|
626
|
+
import { useState as useState5 } from "react";
|
|
627
|
+
import {
|
|
628
|
+
SELECTION_MODES,
|
|
629
|
+
SELECTION_MODE_LABELS,
|
|
630
|
+
SELECTION_TOOLS,
|
|
631
|
+
describeSelection
|
|
632
|
+
} from "@pixerate/editor/image-editor";
|
|
633
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
634
|
+
var MODIFY_OPS = [
|
|
635
|
+
{ op: "grow", label: "Grow" },
|
|
636
|
+
{ op: "shrink", label: "Shrink" },
|
|
637
|
+
{ op: "feather", label: "Feather" },
|
|
638
|
+
{ op: "border", label: "Border" },
|
|
639
|
+
{ op: "smooth", label: "Smooth" }
|
|
640
|
+
];
|
|
641
|
+
var btn = (on) => `px-2 py-1 rounded-md border text-[11px] transition-colors ${on ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-950 border-slate-800 text-slate-300 hover:text-white"}`;
|
|
642
|
+
var SelectionOptionsBar = ({
|
|
643
|
+
controller,
|
|
644
|
+
options,
|
|
645
|
+
onOptionsChange,
|
|
646
|
+
selection,
|
|
647
|
+
quickMask,
|
|
648
|
+
className = ""
|
|
649
|
+
}) => {
|
|
650
|
+
const [radius, setRadius] = useState5(2);
|
|
651
|
+
const set = (patch) => onOptionsChange({ ...options, ...patch });
|
|
652
|
+
const sel = selection === void 0 ? controller.getSelection() : selection;
|
|
653
|
+
const qm = quickMask ?? Boolean(controller.getState().quickMask);
|
|
654
|
+
return /* @__PURE__ */ jsxs5("div", { "data-testid": "selection-options-bar", className: `flex flex-col gap-3 text-xs ${className}`, children: [
|
|
655
|
+
/* @__PURE__ */ jsx5("div", { role: "group", "aria-label": "Selection tool", className: "flex flex-wrap gap-1", children: SELECTION_TOOLS.map((t) => /* @__PURE__ */ jsx5("button", { type: "button", "aria-pressed": options.tool === t.id, className: btn(options.tool === t.id), onClick: () => set({ tool: t.id }), children: t.label }, t.id)) }),
|
|
656
|
+
/* @__PURE__ */ jsx5("div", { role: "group", "aria-label": "Selection mode", className: "flex flex-wrap gap-1", children: SELECTION_MODES.map((m) => /* @__PURE__ */ jsx5("button", { type: "button", "aria-pressed": options.mode === m, className: btn(options.mode === m), onClick: () => set({ mode: m }), children: SELECTION_MODE_LABELS[m] }, m)) }),
|
|
657
|
+
options.tool === "wand" && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
|
|
658
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex flex-col gap-1", children: [
|
|
659
|
+
/* @__PURE__ */ jsxs5("span", { className: "flex justify-between text-slate-400", children: [
|
|
660
|
+
/* @__PURE__ */ jsx5("span", { children: "Tolerance" }),
|
|
661
|
+
/* @__PURE__ */ jsx5("span", { className: "font-mono", children: options.tolerance })
|
|
662
|
+
] }),
|
|
663
|
+
/* @__PURE__ */ jsx5(
|
|
664
|
+
"input",
|
|
665
|
+
{
|
|
666
|
+
type: "range",
|
|
667
|
+
min: 0,
|
|
668
|
+
max: 255,
|
|
669
|
+
step: 1,
|
|
670
|
+
"aria-label": "Tolerance",
|
|
671
|
+
value: options.tolerance,
|
|
672
|
+
onChange: (e) => set({ tolerance: Number(e.target.value) }),
|
|
673
|
+
className: "w-full accent-indigo-500"
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
] }),
|
|
677
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
678
|
+
/* @__PURE__ */ jsx5("input", { type: "checkbox", checked: options.contiguous, onChange: (e) => set({ contiguous: e.target.checked }) }),
|
|
679
|
+
"Contiguous"
|
|
680
|
+
] }),
|
|
681
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
682
|
+
/* @__PURE__ */ jsx5("input", { type: "checkbox", checked: options.sampleMerged, onChange: (e) => set({ sampleMerged: e.target.checked }) }),
|
|
683
|
+
"Sample adjusted image"
|
|
684
|
+
] })
|
|
685
|
+
] }),
|
|
686
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
687
|
+
/* @__PURE__ */ jsx5("input", { type: "checkbox", checked: options.antiAlias, onChange: (e) => set({ antiAlias: e.target.checked }) }),
|
|
688
|
+
"Anti-alias"
|
|
689
|
+
] }),
|
|
690
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-1", children: [
|
|
691
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn(false), onClick: () => controller.selectAll(), children: "Select All" }),
|
|
692
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn(false), disabled: !sel, onClick: () => controller.deselect(), children: "Deselect" }),
|
|
693
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: btn(false), onClick: () => controller.invertSelection(), children: "Inverse" }),
|
|
694
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-pressed": qm, className: btn(qm), onClick: () => controller.setQuickMask(!qm), children: "Quick Mask" })
|
|
695
|
+
] }),
|
|
696
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-1", children: [
|
|
697
|
+
/* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-1 text-slate-400", children: [
|
|
698
|
+
"Radius",
|
|
699
|
+
/* @__PURE__ */ jsx5(
|
|
700
|
+
"input",
|
|
701
|
+
{
|
|
702
|
+
type: "number",
|
|
703
|
+
min: 0,
|
|
704
|
+
max: 500,
|
|
705
|
+
step: 1,
|
|
706
|
+
"aria-label": "Modify radius",
|
|
707
|
+
value: radius,
|
|
708
|
+
onChange: (e) => setRadius(Math.max(0, Number(e.target.value) || 0)),
|
|
709
|
+
className: "w-14 bg-slate-950 border border-slate-800 rounded px-1 py-0.5 text-slate-200"
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
] }),
|
|
713
|
+
MODIFY_OPS.map(({ op, label }) => /* @__PURE__ */ jsx5("button", { type: "button", className: btn(false), disabled: !sel, onClick: () => controller.modifySelection(op, radius), children: label }, op))
|
|
714
|
+
] }),
|
|
715
|
+
/* @__PURE__ */ jsx5("p", { "data-testid": "selection-status", className: "text-slate-400", children: describeSelection(sel) })
|
|
716
|
+
] });
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/image-editor/RefineEdgePanel.tsx
|
|
720
|
+
import { useState as useState6 } from "react";
|
|
721
|
+
import {
|
|
722
|
+
ADJUSTMENT_KINDS as ADJUSTMENT_KINDS2,
|
|
723
|
+
REFINE_EDGE_CONTROLS,
|
|
724
|
+
adjustmentKindLabel as adjustmentKindLabel2,
|
|
725
|
+
layerMaskFromSelection
|
|
726
|
+
} from "@pixerate/editor/image-editor";
|
|
727
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
728
|
+
var DEFAULT_PARAMS = { radius: 8, smartRadius: false, smooth: 0, feather: 0, contrast: 0, shiftEdge: 0 };
|
|
729
|
+
var button = "px-2 py-1 rounded-md border border-slate-800 bg-slate-950 text-[11px] text-slate-200 hover:text-white disabled:opacity-40";
|
|
730
|
+
var RefineEdgePanel = ({ controller, selection, layers, className = "" }) => {
|
|
731
|
+
const [params, setParams] = useState6(DEFAULT_PARAMS);
|
|
732
|
+
const [busy, setBusy] = useState6(false);
|
|
733
|
+
const [error, setError] = useState6(null);
|
|
734
|
+
const [layerId, setLayerId] = useState6("");
|
|
735
|
+
const [newKind, setNewKind] = useState6("hueSaturation");
|
|
736
|
+
const sel = selection === void 0 ? controller.getSelection() : selection;
|
|
737
|
+
const list = layers ?? controller.getAdjustmentLayers();
|
|
738
|
+
const target = list.find((l) => l.id === layerId) ?? list[list.length - 1];
|
|
739
|
+
const refine = async () => {
|
|
740
|
+
setBusy(true);
|
|
741
|
+
setError(null);
|
|
742
|
+
try {
|
|
743
|
+
await controller.refineSelection(params);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
746
|
+
} finally {
|
|
747
|
+
setBusy(false);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
return /* @__PURE__ */ jsxs6("div", { "data-testid": "refine-edge-panel", className: `flex flex-col gap-3 text-xs ${className}`, children: [
|
|
751
|
+
/* @__PURE__ */ jsx6("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Refine Edge" }),
|
|
752
|
+
REFINE_EDGE_CONTROLS.map((c) => /* @__PURE__ */ jsxs6("label", { className: "flex flex-col gap-1", children: [
|
|
753
|
+
/* @__PURE__ */ jsxs6("span", { className: "flex justify-between text-slate-400", children: [
|
|
754
|
+
/* @__PURE__ */ jsx6("span", { children: c.label }),
|
|
755
|
+
/* @__PURE__ */ jsxs6("span", { className: "font-mono", children: [
|
|
756
|
+
params[c.id],
|
|
757
|
+
c.unit
|
|
758
|
+
] })
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsx6(
|
|
761
|
+
"input",
|
|
762
|
+
{
|
|
763
|
+
type: "range",
|
|
764
|
+
min: c.min,
|
|
765
|
+
max: c.max,
|
|
766
|
+
step: c.step,
|
|
767
|
+
"aria-label": c.label,
|
|
768
|
+
value: params[c.id],
|
|
769
|
+
onChange: (e) => setParams({ ...params, [c.id]: Number(e.target.value) }),
|
|
770
|
+
className: "w-full accent-indigo-500"
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
] }, c.id)),
|
|
774
|
+
/* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2 text-slate-300", children: [
|
|
775
|
+
/* @__PURE__ */ jsx6("input", { type: "checkbox", checked: params.smartRadius, onChange: (e) => setParams({ ...params, smartRadius: e.target.checked }) }),
|
|
776
|
+
"Smart Radius"
|
|
777
|
+
] }),
|
|
778
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex gap-1", children: [
|
|
779
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: button, disabled: !sel || busy, onClick: refine, children: "Refine Edge" }),
|
|
780
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: button, onClick: () => setParams(DEFAULT_PARAMS), children: "Reset" })
|
|
781
|
+
] }),
|
|
782
|
+
error && /* @__PURE__ */ jsx6("p", { role: "alert", className: "text-rose-400", children: error }),
|
|
783
|
+
/* @__PURE__ */ jsx6("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px] mt-2", children: "Layer Mask" }),
|
|
784
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex gap-1 items-center", children: [
|
|
785
|
+
/* @__PURE__ */ jsx6(
|
|
786
|
+
"select",
|
|
787
|
+
{
|
|
788
|
+
"aria-label": "New masked layer kind",
|
|
789
|
+
value: newKind,
|
|
790
|
+
onChange: (e) => setNewKind(e.target.value),
|
|
791
|
+
className: "flex-1 bg-slate-950 border border-slate-800 rounded px-1 py-1 text-slate-200",
|
|
792
|
+
children: ADJUSTMENT_KINDS2.map((k) => /* @__PURE__ */ jsx6("option", { value: k.kind, children: k.label }, k.kind))
|
|
793
|
+
}
|
|
794
|
+
),
|
|
795
|
+
/* @__PURE__ */ jsx6(
|
|
796
|
+
"button",
|
|
797
|
+
{
|
|
798
|
+
type: "button",
|
|
799
|
+
className: button,
|
|
800
|
+
disabled: !sel,
|
|
801
|
+
onClick: () => sel && setLayerId(controller.addAdjustmentLayer({ kind: newKind, mask: layerMaskFromSelection(sel) })),
|
|
802
|
+
children: "New Masked Layer"
|
|
803
|
+
}
|
|
804
|
+
)
|
|
805
|
+
] }),
|
|
806
|
+
list.length > 0 && target && /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-2", children: [
|
|
807
|
+
/* @__PURE__ */ jsx6(
|
|
808
|
+
"select",
|
|
809
|
+
{
|
|
810
|
+
"aria-label": "Mask layer",
|
|
811
|
+
value: target.id,
|
|
812
|
+
onChange: (e) => setLayerId(e.target.value),
|
|
813
|
+
className: "bg-slate-950 border border-slate-800 rounded px-1 py-1 text-slate-200",
|
|
814
|
+
children: list.map((l) => /* @__PURE__ */ jsxs6("option", { value: l.id, children: [
|
|
815
|
+
l.name || adjustmentKindLabel2(l.kind),
|
|
816
|
+
l.mask ? " (masked)" : ""
|
|
817
|
+
] }, l.id))
|
|
818
|
+
}
|
|
819
|
+
),
|
|
820
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", children: [
|
|
821
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: button, disabled: !sel, onClick: () => controller.applySelectionAsLayerMask(target.id), children: "Mask with Selection" }),
|
|
822
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: button, disabled: !target.mask, onClick: () => controller.selectionFromLayerMask(target.id), children: "Load as Selection" }),
|
|
823
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: button, disabled: !target.mask, onClick: () => controller.setAdjustmentLayerMask(target.id, null), children: "Remove Mask" })
|
|
824
|
+
] }),
|
|
825
|
+
target.mask && /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
826
|
+
/* @__PURE__ */ jsxs6("label", { className: "flex flex-col gap-1", children: [
|
|
827
|
+
/* @__PURE__ */ jsxs6("span", { className: "flex justify-between text-slate-400", children: [
|
|
828
|
+
/* @__PURE__ */ jsx6("span", { children: "Density" }),
|
|
829
|
+
/* @__PURE__ */ jsxs6("span", { className: "font-mono", children: [
|
|
830
|
+
Math.round((target.mask.density ?? 1) * 100),
|
|
831
|
+
"%"
|
|
832
|
+
] })
|
|
833
|
+
] }),
|
|
834
|
+
/* @__PURE__ */ jsx6(
|
|
835
|
+
"input",
|
|
836
|
+
{
|
|
837
|
+
type: "range",
|
|
838
|
+
min: 0,
|
|
839
|
+
max: 100,
|
|
840
|
+
step: 1,
|
|
841
|
+
"aria-label": "Mask density",
|
|
842
|
+
value: Math.round((target.mask.density ?? 1) * 100),
|
|
843
|
+
onChange: (e) => controller.setAdjustmentLayerMaskProps(target.id, { density: Number(e.target.value) / 100 }),
|
|
844
|
+
className: "w-full accent-indigo-500"
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
] }),
|
|
848
|
+
/* @__PURE__ */ jsxs6("label", { className: "flex flex-col gap-1", children: [
|
|
849
|
+
/* @__PURE__ */ jsxs6("span", { className: "flex justify-between text-slate-400", children: [
|
|
850
|
+
/* @__PURE__ */ jsx6("span", { children: "Mask Feather" }),
|
|
851
|
+
/* @__PURE__ */ jsxs6("span", { className: "font-mono", children: [
|
|
852
|
+
target.mask.feather ?? 0,
|
|
853
|
+
"px"
|
|
854
|
+
] })
|
|
855
|
+
] }),
|
|
856
|
+
/* @__PURE__ */ jsx6(
|
|
857
|
+
"input",
|
|
858
|
+
{
|
|
859
|
+
type: "range",
|
|
860
|
+
min: 0,
|
|
861
|
+
max: 50,
|
|
862
|
+
step: 0.5,
|
|
863
|
+
"aria-label": "Mask feather",
|
|
864
|
+
value: target.mask.feather ?? 0,
|
|
865
|
+
onChange: (e) => controller.setAdjustmentLayerMaskProps(target.id, { feather: Number(e.target.value) }),
|
|
866
|
+
className: "w-full accent-indigo-500"
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
] })
|
|
870
|
+
] })
|
|
871
|
+
] })
|
|
872
|
+
] });
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/image-editor/ImageEditor.tsx
|
|
876
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
877
|
+
var ImageEditor = ({
|
|
878
|
+
editor: externalEditor,
|
|
879
|
+
src,
|
|
880
|
+
className = "",
|
|
881
|
+
style,
|
|
882
|
+
onSave,
|
|
883
|
+
exportOptions,
|
|
884
|
+
onInpaint,
|
|
885
|
+
onRemoveBackground,
|
|
886
|
+
aiHooks
|
|
887
|
+
}) => {
|
|
888
|
+
const internalEditor = useImageEditor({ image: src });
|
|
889
|
+
const editor = externalEditor || internalEditor;
|
|
890
|
+
const { state, controller } = editor;
|
|
891
|
+
const legacyProcess = normalizeProcessVersion(state.adjustments.process) < LATEST_PROCESS_VERSION;
|
|
892
|
+
const canvasRef = useRef3(null);
|
|
893
|
+
const containerRef = useRef3(null);
|
|
894
|
+
const [activeTab, setActiveTab] = useState7("crop");
|
|
895
|
+
const [activeCropRatio, setActiveCropRatio] = useState7(null);
|
|
896
|
+
const [selectionOptions, setSelectionOptions] = useState7(DEFAULT_SELECTION_TOOL_OPTIONS);
|
|
897
|
+
const selectionOptionsRef = useRef3(selectionOptions);
|
|
898
|
+
selectionOptionsRef.current = selectionOptions;
|
|
899
|
+
const selectionGestureRef = useRef3(null);
|
|
900
|
+
if (!selectionGestureRef.current || selectionGestureRef.current.controller !== controller) {
|
|
901
|
+
selectionGestureRef.current = new SelectionGesture(controller, () => selectionOptionsRef.current);
|
|
902
|
+
}
|
|
903
|
+
const selectionDraftRef = useRef3(null);
|
|
904
|
+
const [selectionError, setSelectionError] = useState7(null);
|
|
905
|
+
const [inpaintPrompt, setInpaintPrompt] = useState7("");
|
|
906
|
+
const [inpaintMaskMode, setInpaintMaskMode] = useState7("annotations");
|
|
907
|
+
const [isRemovingBg, setIsRemovingBg] = useState7(false);
|
|
908
|
+
const [isInpainting, setIsInpainting] = useState7(false);
|
|
909
|
+
const [aiError, setAiError] = useState7(null);
|
|
910
|
+
const [aiSuccess, setAiSuccess] = useState7(null);
|
|
911
|
+
const effectiveOnInpaint = onInpaint || aiHooks?.onInpaint;
|
|
912
|
+
const effectiveOnRemoveBg = onRemoveBackground || aiHooks?.onRemoveBackground;
|
|
913
|
+
const handleRemoveBackground = async () => {
|
|
914
|
+
if (!effectiveOnRemoveBg) {
|
|
915
|
+
setAiError("No background removal hook is configured.");
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
setAiError(null);
|
|
919
|
+
setAiSuccess(null);
|
|
920
|
+
setIsRemovingBg(true);
|
|
921
|
+
try {
|
|
922
|
+
const imageDataUrl = await editor.toSourceDataURL();
|
|
923
|
+
const result = await effectiveOnRemoveBg({ imageDataUrl });
|
|
924
|
+
if (typeof result === "string") {
|
|
925
|
+
await editor.applyBackgroundRemovedImage(result);
|
|
926
|
+
setAiSuccess("Background removed successfully!");
|
|
927
|
+
} else {
|
|
928
|
+
setAiSuccess("Background removal complete.");
|
|
929
|
+
}
|
|
930
|
+
} catch (err) {
|
|
931
|
+
setAiError(err?.message || "Failed to remove background.");
|
|
932
|
+
} finally {
|
|
933
|
+
setIsRemovingBg(false);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const handleInpaint = async () => {
|
|
937
|
+
if (!effectiveOnInpaint) {
|
|
938
|
+
setAiError("No inpainting hook is configured.");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (!inpaintPrompt.trim()) {
|
|
942
|
+
setAiError("Please enter an inpainting prompt.");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
setAiError(null);
|
|
946
|
+
setAiSuccess(null);
|
|
947
|
+
setIsInpainting(true);
|
|
948
|
+
try {
|
|
949
|
+
const imageDataUrl = await editor.toSourceDataURL();
|
|
950
|
+
const maskDataUrl = await editor.toMaskDataURL({
|
|
951
|
+
useAnnotations: inpaintMaskMode === "annotations",
|
|
952
|
+
useCrop: inpaintMaskMode === "crop"
|
|
953
|
+
});
|
|
954
|
+
const result = await effectiveOnInpaint({
|
|
955
|
+
prompt: inpaintPrompt.trim(),
|
|
956
|
+
imageDataUrl,
|
|
957
|
+
maskDataUrl,
|
|
958
|
+
cropBox: inpaintMaskMode === "crop" ? editor.state.crop : null
|
|
959
|
+
});
|
|
960
|
+
if (typeof result === "string") {
|
|
961
|
+
await editor.applyInpaintedImage(result);
|
|
962
|
+
setAiSuccess("Inpainting applied successfully!");
|
|
963
|
+
} else {
|
|
964
|
+
setAiSuccess("Inpainting complete.");
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
setAiError(err?.message || "Failed to apply inpainting.");
|
|
968
|
+
} finally {
|
|
969
|
+
setIsInpainting(false);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
const [activeColor, setActiveColor] = useState7("#ef4444");
|
|
973
|
+
const [strokeWidth, setStrokeWidth] = useState7(3);
|
|
974
|
+
const [fontSize, setFontSize] = useState7(24);
|
|
975
|
+
const [textInput, setTextInput] = useState7("");
|
|
976
|
+
const draftAnnotationRef = useRef3(null);
|
|
977
|
+
const draftCropRef = useRef3(null);
|
|
978
|
+
const cropDragInfoRef = useRef3(null);
|
|
979
|
+
const annotationDragInfoRef = useRef3(null);
|
|
980
|
+
const isDrawingRef = useRef3(false);
|
|
981
|
+
const currentPathRef = useRef3([]);
|
|
982
|
+
const dragStartPointRef = useRef3(null);
|
|
983
|
+
const renderCanvas = useCallback2(
|
|
984
|
+
async (draftAnnotation, draftCrop) => {
|
|
985
|
+
const canvas = canvasRef.current;
|
|
986
|
+
const container = containerRef.current;
|
|
987
|
+
if (!canvas || !container) return;
|
|
988
|
+
const vpWidth = container.clientWidth || 800;
|
|
989
|
+
const vpHeight = container.clientHeight || 500;
|
|
990
|
+
await controller.render(canvas, {
|
|
991
|
+
viewportWidth: vpWidth,
|
|
992
|
+
viewportHeight: vpHeight,
|
|
993
|
+
showCropOverlay: activeTab === "crop",
|
|
994
|
+
showSelectionOverlay: true,
|
|
995
|
+
interactive: true,
|
|
996
|
+
draftAnnotation: draftAnnotation !== void 0 ? draftAnnotation : draftAnnotationRef.current,
|
|
997
|
+
draftCrop: draftCrop !== void 0 ? draftCrop : draftCropRef.current,
|
|
998
|
+
draftSelectionPath: selectionDraftRef.current
|
|
999
|
+
});
|
|
1000
|
+
},
|
|
1001
|
+
[controller, activeTab]
|
|
1002
|
+
);
|
|
1003
|
+
useEffect4(() => {
|
|
1004
|
+
const raf = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
1005
|
+
const caf = typeof cancelAnimationFrame !== "undefined" ? cancelAnimationFrame : (id) => clearTimeout(id);
|
|
1006
|
+
const animId = raf(() => {
|
|
1007
|
+
renderCanvas();
|
|
1008
|
+
});
|
|
1009
|
+
return () => caf(animId);
|
|
1010
|
+
}, [renderCanvas, state]);
|
|
1011
|
+
useEffect4(() => {
|
|
1012
|
+
if (activeTab === "crop" && editor.state.activeTool !== "crop") {
|
|
1013
|
+
editor.setTool("crop");
|
|
1014
|
+
}
|
|
1015
|
+
}, [activeTab, editor]);
|
|
1016
|
+
useEffect4(() => {
|
|
1017
|
+
const container = containerRef.current;
|
|
1018
|
+
if (!container) return;
|
|
1019
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1020
|
+
const observer = new ResizeObserver(() => {
|
|
1021
|
+
renderCanvas();
|
|
1022
|
+
});
|
|
1023
|
+
observer.observe(container);
|
|
1024
|
+
return () => observer.disconnect();
|
|
1025
|
+
} else {
|
|
1026
|
+
const handleResize = () => renderCanvas();
|
|
1027
|
+
window.addEventListener("resize", handleResize);
|
|
1028
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
1029
|
+
}
|
|
1030
|
+
}, [renderCanvas]);
|
|
1031
|
+
const getCanvasEventPoint = (e) => {
|
|
1032
|
+
const canvas = canvasRef.current;
|
|
1033
|
+
if (!canvas) return { x: 0, y: 0 };
|
|
1034
|
+
const rect = canvas.getBoundingClientRect();
|
|
1035
|
+
const scaleX = rect.width > 0 ? canvas.width / rect.width : 1;
|
|
1036
|
+
const scaleY = rect.height > 0 ? canvas.height / rect.height : 1;
|
|
1037
|
+
return {
|
|
1038
|
+
x: (e.clientX - rect.left) * scaleX,
|
|
1039
|
+
y: (e.clientY - rect.top) * scaleY
|
|
1040
|
+
};
|
|
1041
|
+
};
|
|
1042
|
+
const handleMouseDown = (e) => {
|
|
1043
|
+
const canvas = canvasRef.current;
|
|
1044
|
+
if (!canvas) return;
|
|
1045
|
+
const canvasPt = getCanvasEventPoint(e);
|
|
1046
|
+
if (activeTab === "select") {
|
|
1047
|
+
const gesture = selectionGestureRef.current;
|
|
1048
|
+
const imgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1049
|
+
void gesture.begin(imgPt, e).then(() => setSelectionError(gesture.error));
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (activeTab === "crop") {
|
|
1053
|
+
const activeCrop = draftCropRef.current || state.crop || {
|
|
1054
|
+
x: 0,
|
|
1055
|
+
y: 0,
|
|
1056
|
+
width: state.imageDimensions.width,
|
|
1057
|
+
height: state.imageDimensions.height
|
|
1058
|
+
};
|
|
1059
|
+
const handle = hitTestCrop(canvasPt, state, canvas.width, canvas.height, 14, activeCrop);
|
|
1060
|
+
const startImgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, true);
|
|
1061
|
+
if (handle) {
|
|
1062
|
+
cropDragInfoRef.current = {
|
|
1063
|
+
handle,
|
|
1064
|
+
startCanvasPt: canvasPt,
|
|
1065
|
+
startImgPt,
|
|
1066
|
+
startCrop: activeCrop
|
|
1067
|
+
};
|
|
1068
|
+
} else {
|
|
1069
|
+
cropDragInfoRef.current = {
|
|
1070
|
+
handle: "new",
|
|
1071
|
+
startCanvasPt: canvasPt,
|
|
1072
|
+
startImgPt,
|
|
1073
|
+
startCrop: { x: startImgPt.x, y: startImgPt.y, width: 0, height: 0 }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (activeTab === "annotate") {
|
|
1079
|
+
const imgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1080
|
+
dragStartPointRef.current = imgPt;
|
|
1081
|
+
if (state.activeTool === "select") {
|
|
1082
|
+
for (let i = state.annotations.length - 1; i >= 0; i--) {
|
|
1083
|
+
const ann = state.annotations[i];
|
|
1084
|
+
if (hitTestAnnotation(imgPt, ann)) {
|
|
1085
|
+
editor.selectAnnotation(ann.id);
|
|
1086
|
+
annotationDragInfoRef.current = {
|
|
1087
|
+
annId: ann.id,
|
|
1088
|
+
startImgPt: imgPt,
|
|
1089
|
+
initialAnn: { ...ann }
|
|
1090
|
+
};
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
editor.selectAnnotation(null);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (state.activeTool === "pen") {
|
|
1098
|
+
isDrawingRef.current = true;
|
|
1099
|
+
currentPathRef.current = [imgPt];
|
|
1100
|
+
const draft = {
|
|
1101
|
+
id: "draft",
|
|
1102
|
+
type: "pen",
|
|
1103
|
+
x: 0,
|
|
1104
|
+
y: 0,
|
|
1105
|
+
points: [imgPt],
|
|
1106
|
+
strokeColor: activeColor,
|
|
1107
|
+
strokeWidth
|
|
1108
|
+
};
|
|
1109
|
+
draftAnnotationRef.current = draft;
|
|
1110
|
+
renderCanvas(draft);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const handleMouseMove = (e) => {
|
|
1115
|
+
const canvas = canvasRef.current;
|
|
1116
|
+
if (!canvas) return;
|
|
1117
|
+
const canvasPt = getCanvasEventPoint(e);
|
|
1118
|
+
if (activeTab === "select") {
|
|
1119
|
+
const gesture = selectionGestureRef.current;
|
|
1120
|
+
if (gesture.active && gesture.move(canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false))) {
|
|
1121
|
+
selectionDraftRef.current = gesture.draftPath;
|
|
1122
|
+
renderCanvas();
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (activeTab === "crop") {
|
|
1127
|
+
const drag = cropDragInfoRef.current;
|
|
1128
|
+
if (drag) {
|
|
1129
|
+
const currentImgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, true);
|
|
1130
|
+
let newCrop;
|
|
1131
|
+
if (drag.handle === "new") {
|
|
1132
|
+
const minX = Math.max(0, Math.min(drag.startImgPt.x, currentImgPt.x));
|
|
1133
|
+
const minY = Math.max(0, Math.min(drag.startImgPt.y, currentImgPt.y));
|
|
1134
|
+
const maxX = Math.min(state.imageDimensions.width, Math.max(drag.startImgPt.x, currentImgPt.x));
|
|
1135
|
+
const maxY = Math.min(state.imageDimensions.height, Math.max(drag.startImgPt.y, currentImgPt.y));
|
|
1136
|
+
newCrop = {
|
|
1137
|
+
x: Math.round(minX),
|
|
1138
|
+
y: Math.round(minY),
|
|
1139
|
+
width: Math.round(Math.max(20, maxX - minX)),
|
|
1140
|
+
height: Math.round(Math.max(20, maxY - minY))
|
|
1141
|
+
};
|
|
1142
|
+
} else {
|
|
1143
|
+
newCrop = calculateCropDrag(
|
|
1144
|
+
drag.startCrop,
|
|
1145
|
+
drag.handle,
|
|
1146
|
+
drag.startImgPt,
|
|
1147
|
+
currentImgPt,
|
|
1148
|
+
state.imageDimensions.width,
|
|
1149
|
+
state.imageDimensions.height,
|
|
1150
|
+
activeCropRatio,
|
|
1151
|
+
state.transform
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
draftCropRef.current = newCrop;
|
|
1155
|
+
renderCanvas(null, newCrop);
|
|
1156
|
+
} else {
|
|
1157
|
+
const activeCrop = draftCropRef.current || state.crop || {
|
|
1158
|
+
x: 0,
|
|
1159
|
+
y: 0,
|
|
1160
|
+
width: state.imageDimensions.width,
|
|
1161
|
+
height: state.imageDimensions.height
|
|
1162
|
+
};
|
|
1163
|
+
const handle = hitTestCrop(canvasPt, state, canvas.width, canvas.height, 14, activeCrop);
|
|
1164
|
+
canvas.style.cursor = getCropCursor(handle, state.transform);
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (activeTab === "annotate") {
|
|
1169
|
+
if (annotationDragInfoRef.current) {
|
|
1170
|
+
const drag = annotationDragInfoRef.current;
|
|
1171
|
+
const currentImgPt2 = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1172
|
+
const dx = currentImgPt2.x - drag.startImgPt.x;
|
|
1173
|
+
const dy = currentImgPt2.y - drag.startImgPt.y;
|
|
1174
|
+
editor.updateAnnotation(drag.annId, {
|
|
1175
|
+
x: Math.round(drag.initialAnn.x + dx),
|
|
1176
|
+
y: Math.round(drag.initialAnn.y + dy)
|
|
1177
|
+
});
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const startPt = dragStartPointRef.current;
|
|
1181
|
+
if (!startPt) {
|
|
1182
|
+
if (state.activeTool === "select") {
|
|
1183
|
+
const imgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1184
|
+
let hovered = false;
|
|
1185
|
+
for (let i = state.annotations.length - 1; i >= 0; i--) {
|
|
1186
|
+
if (hitTestAnnotation(imgPt, state.annotations[i])) {
|
|
1187
|
+
hovered = true;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
canvas.style.cursor = hovered ? "move" : "default";
|
|
1192
|
+
} else {
|
|
1193
|
+
canvas.style.cursor = "crosshair";
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
const currentImgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1198
|
+
if (state.activeTool === "pen" && isDrawingRef.current) {
|
|
1199
|
+
currentPathRef.current.push(currentImgPt);
|
|
1200
|
+
const draft = {
|
|
1201
|
+
id: "draft",
|
|
1202
|
+
type: "pen",
|
|
1203
|
+
x: 0,
|
|
1204
|
+
y: 0,
|
|
1205
|
+
points: [...currentPathRef.current],
|
|
1206
|
+
strokeColor: activeColor,
|
|
1207
|
+
strokeWidth
|
|
1208
|
+
};
|
|
1209
|
+
draftAnnotationRef.current = draft;
|
|
1210
|
+
renderCanvas(draft);
|
|
1211
|
+
} else if (state.activeTool === "rect") {
|
|
1212
|
+
const w = currentImgPt.x - startPt.x;
|
|
1213
|
+
const h = currentImgPt.y - startPt.y;
|
|
1214
|
+
const draft = {
|
|
1215
|
+
id: "draft",
|
|
1216
|
+
type: "rect",
|
|
1217
|
+
x: Math.min(startPt.x, currentImgPt.x),
|
|
1218
|
+
y: Math.min(startPt.y, currentImgPt.y),
|
|
1219
|
+
width: Math.abs(w),
|
|
1220
|
+
height: Math.abs(h),
|
|
1221
|
+
strokeColor: activeColor,
|
|
1222
|
+
strokeWidth
|
|
1223
|
+
};
|
|
1224
|
+
draftAnnotationRef.current = draft;
|
|
1225
|
+
renderCanvas(draft);
|
|
1226
|
+
} else if (state.activeTool === "circle") {
|
|
1227
|
+
const rx = Math.abs(currentImgPt.x - startPt.x) / 2;
|
|
1228
|
+
const ry = Math.abs(currentImgPt.y - startPt.y) / 2;
|
|
1229
|
+
const draft = {
|
|
1230
|
+
id: "draft",
|
|
1231
|
+
type: "circle",
|
|
1232
|
+
x: Math.min(startPt.x, currentImgPt.x) + rx,
|
|
1233
|
+
y: Math.min(startPt.y, currentImgPt.y) + ry,
|
|
1234
|
+
radiusX: rx,
|
|
1235
|
+
radiusY: ry,
|
|
1236
|
+
strokeColor: activeColor,
|
|
1237
|
+
strokeWidth
|
|
1238
|
+
};
|
|
1239
|
+
draftAnnotationRef.current = draft;
|
|
1240
|
+
renderCanvas(draft);
|
|
1241
|
+
} else if (state.activeTool === "arrow") {
|
|
1242
|
+
const draft = {
|
|
1243
|
+
id: "draft",
|
|
1244
|
+
type: "arrow",
|
|
1245
|
+
x: startPt.x,
|
|
1246
|
+
y: startPt.y,
|
|
1247
|
+
endX: currentImgPt.x,
|
|
1248
|
+
endY: currentImgPt.y,
|
|
1249
|
+
strokeColor: activeColor,
|
|
1250
|
+
strokeWidth
|
|
1251
|
+
};
|
|
1252
|
+
draftAnnotationRef.current = draft;
|
|
1253
|
+
renderCanvas(draft);
|
|
1254
|
+
} else if (state.activeTool === "line") {
|
|
1255
|
+
const draft = {
|
|
1256
|
+
id: "draft",
|
|
1257
|
+
type: "line",
|
|
1258
|
+
x: startPt.x,
|
|
1259
|
+
y: startPt.y,
|
|
1260
|
+
endX: currentImgPt.x,
|
|
1261
|
+
endY: currentImgPt.y,
|
|
1262
|
+
strokeColor: activeColor,
|
|
1263
|
+
strokeWidth
|
|
1264
|
+
};
|
|
1265
|
+
draftAnnotationRef.current = draft;
|
|
1266
|
+
renderCanvas(draft);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
const handleMouseUp = (e) => {
|
|
1271
|
+
const canvas = canvasRef.current;
|
|
1272
|
+
if (!canvas) return;
|
|
1273
|
+
const canvasPt = getCanvasEventPoint(e);
|
|
1274
|
+
if (activeTab === "select") {
|
|
1275
|
+
const gesture = selectionGestureRef.current;
|
|
1276
|
+
if (gesture.active) {
|
|
1277
|
+
gesture.end(canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false), e);
|
|
1278
|
+
selectionDraftRef.current = null;
|
|
1279
|
+
renderCanvas();
|
|
1280
|
+
}
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (activeTab === "crop") {
|
|
1284
|
+
if (cropDragInfoRef.current) {
|
|
1285
|
+
if (draftCropRef.current && draftCropRef.current.width >= 20 && draftCropRef.current.height >= 20) {
|
|
1286
|
+
editor.setCrop(draftCropRef.current, true);
|
|
1287
|
+
}
|
|
1288
|
+
cropDragInfoRef.current = null;
|
|
1289
|
+
draftCropRef.current = null;
|
|
1290
|
+
renderCanvas();
|
|
1291
|
+
}
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (activeTab === "annotate") {
|
|
1295
|
+
if (annotationDragInfoRef.current) {
|
|
1296
|
+
annotationDragInfoRef.current = null;
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const startPt = dragStartPointRef.current;
|
|
1300
|
+
if (startPt) {
|
|
1301
|
+
const currentImgPt = canvasToImagePoint(canvasPt, state, canvas.width, canvas.height, false);
|
|
1302
|
+
if (state.activeTool === "pen" && isDrawingRef.current) {
|
|
1303
|
+
if (currentPathRef.current.length > 1) {
|
|
1304
|
+
editor.addAnnotation({
|
|
1305
|
+
type: "pen",
|
|
1306
|
+
x: 0,
|
|
1307
|
+
y: 0,
|
|
1308
|
+
points: [...currentPathRef.current],
|
|
1309
|
+
strokeColor: activeColor,
|
|
1310
|
+
strokeWidth
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
isDrawingRef.current = false;
|
|
1314
|
+
currentPathRef.current = [];
|
|
1315
|
+
} else if (state.activeTool === "rect") {
|
|
1316
|
+
const w = Math.abs(currentImgPt.x - startPt.x);
|
|
1317
|
+
const h = Math.abs(currentImgPt.y - startPt.y);
|
|
1318
|
+
if (w > 4 && h > 4) {
|
|
1319
|
+
editor.addAnnotation({
|
|
1320
|
+
type: "rect",
|
|
1321
|
+
x: Math.min(startPt.x, currentImgPt.x),
|
|
1322
|
+
y: Math.min(startPt.y, currentImgPt.y),
|
|
1323
|
+
width: w,
|
|
1324
|
+
height: h,
|
|
1325
|
+
strokeColor: activeColor,
|
|
1326
|
+
strokeWidth
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
} else if (state.activeTool === "circle") {
|
|
1330
|
+
const rx = Math.abs(currentImgPt.x - startPt.x) / 2;
|
|
1331
|
+
const ry = Math.abs(currentImgPt.y - startPt.y) / 2;
|
|
1332
|
+
if (rx > 4 && ry > 4) {
|
|
1333
|
+
editor.addAnnotation({
|
|
1334
|
+
type: "circle",
|
|
1335
|
+
x: Math.min(startPt.x, currentImgPt.x) + rx,
|
|
1336
|
+
y: Math.min(startPt.y, currentImgPt.y) + ry,
|
|
1337
|
+
radiusX: rx,
|
|
1338
|
+
radiusY: ry,
|
|
1339
|
+
strokeColor: activeColor,
|
|
1340
|
+
strokeWidth
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
} else if (state.activeTool === "arrow") {
|
|
1344
|
+
if (Math.hypot(currentImgPt.x - startPt.x, currentImgPt.y - startPt.y) > 4) {
|
|
1345
|
+
editor.addAnnotation({
|
|
1346
|
+
type: "arrow",
|
|
1347
|
+
x: startPt.x,
|
|
1348
|
+
y: startPt.y,
|
|
1349
|
+
endX: currentImgPt.x,
|
|
1350
|
+
endY: currentImgPt.y,
|
|
1351
|
+
strokeColor: activeColor,
|
|
1352
|
+
strokeWidth
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
} else if (state.activeTool === "line") {
|
|
1356
|
+
if (Math.hypot(currentImgPt.x - startPt.x, currentImgPt.y - startPt.y) > 4) {
|
|
1357
|
+
editor.addAnnotation({
|
|
1358
|
+
type: "line",
|
|
1359
|
+
x: startPt.x,
|
|
1360
|
+
y: startPt.y,
|
|
1361
|
+
endX: currentImgPt.x,
|
|
1362
|
+
endY: currentImgPt.y,
|
|
1363
|
+
strokeColor: activeColor,
|
|
1364
|
+
strokeWidth
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
draftAnnotationRef.current = null;
|
|
1369
|
+
dragStartPointRef.current = null;
|
|
1370
|
+
renderCanvas();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
const handleAddText = () => {
|
|
1375
|
+
if (!textInput.trim()) return;
|
|
1376
|
+
editor.addAnnotation({
|
|
1377
|
+
type: "text",
|
|
1378
|
+
x: 50,
|
|
1379
|
+
y: 50,
|
|
1380
|
+
text: textInput.trim(),
|
|
1381
|
+
fontSize,
|
|
1382
|
+
color: activeColor
|
|
1383
|
+
});
|
|
1384
|
+
setTextInput("");
|
|
1385
|
+
};
|
|
1386
|
+
const handleExport = async () => {
|
|
1387
|
+
const dataUrl = await editor.toDataURL(exportOptions);
|
|
1388
|
+
if (onSave) {
|
|
1389
|
+
onSave(dataUrl);
|
|
1390
|
+
} else {
|
|
1391
|
+
const link = document.createElement("a");
|
|
1392
|
+
link.download = `edited-image-${Date.now()}.png`;
|
|
1393
|
+
link.href = dataUrl;
|
|
1394
|
+
link.click();
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
return /* @__PURE__ */ jsxs7(
|
|
1398
|
+
"div",
|
|
1399
|
+
{
|
|
1400
|
+
className: `flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 ${className}`,
|
|
1401
|
+
style,
|
|
1402
|
+
children: [
|
|
1403
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap items-center justify-between px-4 py-3 bg-slate-950/80 border-b border-slate-800 gap-3", children: [
|
|
1404
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1 bg-slate-900 p-1 rounded-lg border border-slate-800 text-xs", children: [
|
|
1405
|
+
/* @__PURE__ */ jsx7(
|
|
1406
|
+
"button",
|
|
1407
|
+
{
|
|
1408
|
+
type: "button",
|
|
1409
|
+
onClick: () => {
|
|
1410
|
+
setActiveTab("crop");
|
|
1411
|
+
editor.setTool("crop");
|
|
1412
|
+
},
|
|
1413
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors ${activeTab === "crop" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1414
|
+
children: "Transform & Crop"
|
|
1415
|
+
}
|
|
1416
|
+
),
|
|
1417
|
+
/* @__PURE__ */ jsx7(
|
|
1418
|
+
"button",
|
|
1419
|
+
{
|
|
1420
|
+
type: "button",
|
|
1421
|
+
onClick: () => {
|
|
1422
|
+
setActiveTab("adjust");
|
|
1423
|
+
editor.setTool("select");
|
|
1424
|
+
},
|
|
1425
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors ${activeTab === "adjust" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1426
|
+
children: "Adjustments"
|
|
1427
|
+
}
|
|
1428
|
+
),
|
|
1429
|
+
/* @__PURE__ */ jsx7(
|
|
1430
|
+
"button",
|
|
1431
|
+
{
|
|
1432
|
+
type: "button",
|
|
1433
|
+
onClick: () => {
|
|
1434
|
+
setActiveTab("select");
|
|
1435
|
+
editor.setTool("select");
|
|
1436
|
+
},
|
|
1437
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors ${activeTab === "select" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1438
|
+
children: "Select & Mask"
|
|
1439
|
+
}
|
|
1440
|
+
),
|
|
1441
|
+
/* @__PURE__ */ jsx7(
|
|
1442
|
+
"button",
|
|
1443
|
+
{
|
|
1444
|
+
type: "button",
|
|
1445
|
+
onClick: () => {
|
|
1446
|
+
setActiveTab("annotate");
|
|
1447
|
+
editor.setTool("pen");
|
|
1448
|
+
},
|
|
1449
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors ${activeTab === "annotate" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1450
|
+
children: "Annotations"
|
|
1451
|
+
}
|
|
1452
|
+
),
|
|
1453
|
+
/* @__PURE__ */ jsx7(
|
|
1454
|
+
"button",
|
|
1455
|
+
{
|
|
1456
|
+
type: "button",
|
|
1457
|
+
onClick: () => {
|
|
1458
|
+
setActiveTab("depth");
|
|
1459
|
+
editor.setTool("select");
|
|
1460
|
+
},
|
|
1461
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors ${activeTab === "depth" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1462
|
+
children: "Depth Mask"
|
|
1463
|
+
}
|
|
1464
|
+
),
|
|
1465
|
+
/* @__PURE__ */ jsxs7(
|
|
1466
|
+
"button",
|
|
1467
|
+
{
|
|
1468
|
+
type: "button",
|
|
1469
|
+
onClick: () => {
|
|
1470
|
+
setActiveTab("ai");
|
|
1471
|
+
editor.setTool("select");
|
|
1472
|
+
},
|
|
1473
|
+
className: `px-3 py-1.5 rounded-md font-medium transition-colors flex items-center gap-1.5 ${activeTab === "ai" ? "bg-indigo-600 text-white shadow" : "text-slate-400 hover:text-slate-200"}`,
|
|
1474
|
+
children: [
|
|
1475
|
+
/* @__PURE__ */ jsx7("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx7("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }),
|
|
1476
|
+
"AI Tools"
|
|
1477
|
+
]
|
|
1478
|
+
}
|
|
1479
|
+
)
|
|
1480
|
+
] }),
|
|
1481
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
1482
|
+
/* @__PURE__ */ jsx7(
|
|
1483
|
+
"button",
|
|
1484
|
+
{
|
|
1485
|
+
type: "button",
|
|
1486
|
+
disabled: !editor.canUndo,
|
|
1487
|
+
onClick: () => editor.undo(),
|
|
1488
|
+
className: "p-1.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-800",
|
|
1489
|
+
title: "Undo",
|
|
1490
|
+
children: "\u21A9"
|
|
1491
|
+
}
|
|
1492
|
+
),
|
|
1493
|
+
/* @__PURE__ */ jsx7(
|
|
1494
|
+
"button",
|
|
1495
|
+
{
|
|
1496
|
+
type: "button",
|
|
1497
|
+
disabled: !editor.canRedo,
|
|
1498
|
+
onClick: () => editor.redo(),
|
|
1499
|
+
className: "p-1.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-800",
|
|
1500
|
+
title: "Redo",
|
|
1501
|
+
children: "\u21AA"
|
|
1502
|
+
}
|
|
1503
|
+
),
|
|
1504
|
+
/* @__PURE__ */ jsx7(
|
|
1505
|
+
"button",
|
|
1506
|
+
{
|
|
1507
|
+
type: "button",
|
|
1508
|
+
onClick: () => editor.reset(),
|
|
1509
|
+
className: "px-2 py-1.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-400 hover:text-slate-200 hover:bg-slate-800",
|
|
1510
|
+
children: "Reset"
|
|
1511
|
+
}
|
|
1512
|
+
),
|
|
1513
|
+
/* @__PURE__ */ jsx7("div", { className: "h-4 w-px bg-slate-800 mx-1" }),
|
|
1514
|
+
/* @__PURE__ */ jsx7(
|
|
1515
|
+
"button",
|
|
1516
|
+
{
|
|
1517
|
+
type: "button",
|
|
1518
|
+
onClick: () => editor.zoomOut(),
|
|
1519
|
+
className: "p-1.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 hover:bg-slate-800",
|
|
1520
|
+
title: "Zoom Out",
|
|
1521
|
+
children: "-"
|
|
1522
|
+
}
|
|
1523
|
+
),
|
|
1524
|
+
/* @__PURE__ */ jsxs7("span", { className: "font-mono text-slate-400 w-10 text-center", children: [
|
|
1525
|
+
Math.round(state.zoom * 100),
|
|
1526
|
+
"%"
|
|
1527
|
+
] }),
|
|
1528
|
+
/* @__PURE__ */ jsx7(
|
|
1529
|
+
"button",
|
|
1530
|
+
{
|
|
1531
|
+
type: "button",
|
|
1532
|
+
onClick: () => editor.zoomIn(),
|
|
1533
|
+
className: "p-1.5 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 hover:bg-slate-800",
|
|
1534
|
+
title: "Zoom In",
|
|
1535
|
+
children: "+"
|
|
1536
|
+
}
|
|
1537
|
+
),
|
|
1538
|
+
/* @__PURE__ */ jsx7("div", { className: "h-4 w-px bg-slate-800 mx-1" }),
|
|
1539
|
+
/* @__PURE__ */ jsx7(
|
|
1540
|
+
"button",
|
|
1541
|
+
{
|
|
1542
|
+
type: "button",
|
|
1543
|
+
onMouseDown: () => editor.setComparing(true),
|
|
1544
|
+
onMouseUp: () => editor.setComparing(false),
|
|
1545
|
+
onMouseLeave: () => editor.setComparing(false),
|
|
1546
|
+
className: "px-2.5 py-1.5 rounded-lg border border-slate-800 bg-slate-900 text-amber-400 hover:bg-slate-800 select-none active:bg-amber-950/40",
|
|
1547
|
+
children: "Hold Compare"
|
|
1548
|
+
}
|
|
1549
|
+
),
|
|
1550
|
+
/* @__PURE__ */ jsx7(
|
|
1551
|
+
"button",
|
|
1552
|
+
{
|
|
1553
|
+
type: "button",
|
|
1554
|
+
onClick: handleExport,
|
|
1555
|
+
className: "px-4 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white font-medium shadow-md shadow-emerald-900/30 transition-colors",
|
|
1556
|
+
children: "Export Image"
|
|
1557
|
+
}
|
|
1558
|
+
)
|
|
1559
|
+
] })
|
|
1560
|
+
] }),
|
|
1561
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col md:flex-row flex-1 min-h-[520px] md:h-[560px]", children: [
|
|
1562
|
+
/* @__PURE__ */ jsx7(
|
|
1563
|
+
"div",
|
|
1564
|
+
{
|
|
1565
|
+
ref: containerRef,
|
|
1566
|
+
className: "relative flex-1 h-[360px] md:h-full min-h-[320px] bg-slate-950 overflow-hidden select-none",
|
|
1567
|
+
children: /* @__PURE__ */ jsx7(
|
|
1568
|
+
"canvas",
|
|
1569
|
+
{
|
|
1570
|
+
ref: canvasRef,
|
|
1571
|
+
onMouseDown: handleMouseDown,
|
|
1572
|
+
onMouseMove: handleMouseMove,
|
|
1573
|
+
onMouseUp: handleMouseUp,
|
|
1574
|
+
onMouseLeave: handleMouseUp,
|
|
1575
|
+
className: "absolute inset-0 w-full h-full block"
|
|
1576
|
+
}
|
|
1577
|
+
)
|
|
1578
|
+
}
|
|
1579
|
+
),
|
|
1580
|
+
/* @__PURE__ */ jsxs7("div", { className: "w-full md:w-80 bg-slate-900/90 border-t md:border-t-0 md:border-l border-slate-800 p-4 overflow-y-auto h-auto md:h-full md:max-h-[560px]", children: [
|
|
1581
|
+
activeTab === "crop" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-4 text-xs", children: [
|
|
1582
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Aspect Ratio Presets" }),
|
|
1583
|
+
/* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-3 gap-2", children: [
|
|
1584
|
+
/* @__PURE__ */ jsx7(
|
|
1585
|
+
"button",
|
|
1586
|
+
{
|
|
1587
|
+
type: "button",
|
|
1588
|
+
onClick: () => {
|
|
1589
|
+
setActiveCropRatio(null);
|
|
1590
|
+
editor.applyCropPreset(null);
|
|
1591
|
+
},
|
|
1592
|
+
className: `px-2.5 py-2 rounded-lg border font-medium ${activeCropRatio === null && state.crop ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700"}`,
|
|
1593
|
+
children: "Freeform"
|
|
1594
|
+
}
|
|
1595
|
+
),
|
|
1596
|
+
/* @__PURE__ */ jsx7(
|
|
1597
|
+
"button",
|
|
1598
|
+
{
|
|
1599
|
+
type: "button",
|
|
1600
|
+
onClick: () => {
|
|
1601
|
+
setActiveCropRatio(1);
|
|
1602
|
+
editor.applyCropPreset(1);
|
|
1603
|
+
},
|
|
1604
|
+
className: `px-2.5 py-2 rounded-lg border font-medium ${activeCropRatio === 1 ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700"}`,
|
|
1605
|
+
children: "1:1 Square"
|
|
1606
|
+
}
|
|
1607
|
+
),
|
|
1608
|
+
/* @__PURE__ */ jsx7(
|
|
1609
|
+
"button",
|
|
1610
|
+
{
|
|
1611
|
+
type: "button",
|
|
1612
|
+
onClick: () => {
|
|
1613
|
+
setActiveCropRatio(4 / 3);
|
|
1614
|
+
editor.applyCropPreset(4 / 3);
|
|
1615
|
+
},
|
|
1616
|
+
className: `px-2.5 py-2 rounded-lg border font-medium ${activeCropRatio === 4 / 3 ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700"}`,
|
|
1617
|
+
children: "4:3 Standard"
|
|
1618
|
+
}
|
|
1619
|
+
),
|
|
1620
|
+
/* @__PURE__ */ jsx7(
|
|
1621
|
+
"button",
|
|
1622
|
+
{
|
|
1623
|
+
type: "button",
|
|
1624
|
+
onClick: () => {
|
|
1625
|
+
setActiveCropRatio(16 / 9);
|
|
1626
|
+
editor.applyCropPreset(16 / 9);
|
|
1627
|
+
},
|
|
1628
|
+
className: `px-2.5 py-2 rounded-lg border font-medium ${activeCropRatio === 16 / 9 ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700"}`,
|
|
1629
|
+
children: "16:9 Wide"
|
|
1630
|
+
}
|
|
1631
|
+
),
|
|
1632
|
+
/* @__PURE__ */ jsx7(
|
|
1633
|
+
"button",
|
|
1634
|
+
{
|
|
1635
|
+
type: "button",
|
|
1636
|
+
onClick: () => {
|
|
1637
|
+
setActiveCropRatio(3 / 2);
|
|
1638
|
+
editor.applyCropPreset(3 / 2);
|
|
1639
|
+
},
|
|
1640
|
+
className: `px-2.5 py-2 rounded-lg border font-medium ${activeCropRatio === 3 / 2 ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700"}`,
|
|
1641
|
+
children: "3:2 Photo"
|
|
1642
|
+
}
|
|
1643
|
+
),
|
|
1644
|
+
/* @__PURE__ */ jsx7(
|
|
1645
|
+
"button",
|
|
1646
|
+
{
|
|
1647
|
+
type: "button",
|
|
1648
|
+
onClick: () => {
|
|
1649
|
+
setActiveCropRatio(null);
|
|
1650
|
+
editor.resetCrop();
|
|
1651
|
+
},
|
|
1652
|
+
className: "px-2.5 py-2 rounded-lg bg-rose-950/40 hover:bg-rose-900/60 text-rose-300 border border-rose-800/40 font-medium",
|
|
1653
|
+
children: "Clear Crop"
|
|
1654
|
+
}
|
|
1655
|
+
)
|
|
1656
|
+
] }),
|
|
1657
|
+
/* @__PURE__ */ jsx7("div", { className: "h-px bg-slate-800 my-1" }),
|
|
1658
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Orientation & Flips" }),
|
|
1659
|
+
/* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
1660
|
+
/* @__PURE__ */ jsx7(
|
|
1661
|
+
"button",
|
|
1662
|
+
{
|
|
1663
|
+
type: "button",
|
|
1664
|
+
onClick: () => editor.rotate(-90),
|
|
1665
|
+
className: "px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700",
|
|
1666
|
+
children: "Rotate -90\xB0"
|
|
1667
|
+
}
|
|
1668
|
+
),
|
|
1669
|
+
/* @__PURE__ */ jsx7(
|
|
1670
|
+
"button",
|
|
1671
|
+
{
|
|
1672
|
+
type: "button",
|
|
1673
|
+
onClick: () => editor.rotate(90),
|
|
1674
|
+
className: "px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700",
|
|
1675
|
+
children: "Rotate +90\xB0"
|
|
1676
|
+
}
|
|
1677
|
+
),
|
|
1678
|
+
/* @__PURE__ */ jsx7(
|
|
1679
|
+
"button",
|
|
1680
|
+
{
|
|
1681
|
+
type: "button",
|
|
1682
|
+
onClick: () => editor.flipHorizontal(),
|
|
1683
|
+
className: `px-3 py-2 rounded-lg border transition-colors ${state.transform.flipH ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 border-slate-700 text-slate-200 hover:bg-slate-700"}`,
|
|
1684
|
+
children: "Flip Horizontal"
|
|
1685
|
+
}
|
|
1686
|
+
),
|
|
1687
|
+
/* @__PURE__ */ jsx7(
|
|
1688
|
+
"button",
|
|
1689
|
+
{
|
|
1690
|
+
type: "button",
|
|
1691
|
+
onClick: () => editor.flipVertical(),
|
|
1692
|
+
className: `px-3 py-2 rounded-lg border transition-colors ${state.transform.flipV ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 border-slate-700 text-slate-200 hover:bg-slate-700"}`,
|
|
1693
|
+
children: "Flip Vertical"
|
|
1694
|
+
}
|
|
1695
|
+
)
|
|
1696
|
+
] })
|
|
1697
|
+
] }),
|
|
1698
|
+
activeTab === "adjust" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-3 text-xs", children: [
|
|
1699
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
1700
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Color Adjustments" }),
|
|
1701
|
+
/* @__PURE__ */ jsx7(
|
|
1702
|
+
"button",
|
|
1703
|
+
{
|
|
1704
|
+
type: "button",
|
|
1705
|
+
onClick: () => editor.resetAdjustments(),
|
|
1706
|
+
className: "text-[10px] text-rose-400 hover:underline",
|
|
1707
|
+
children: "Reset Sliders"
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
] }),
|
|
1711
|
+
/* @__PURE__ */ jsx7(Histogram, { histogram: editor.histogram }),
|
|
1712
|
+
IMAGE_ADJUSTMENT_CONTROLS.map((spec) => /* @__PURE__ */ jsxs7(React6.Fragment, { children: [
|
|
1713
|
+
/* @__PURE__ */ jsx7(
|
|
1714
|
+
ControlSlider,
|
|
1715
|
+
{
|
|
1716
|
+
spec,
|
|
1717
|
+
value: readAdjustmentControls(state.adjustments)[spec.id],
|
|
1718
|
+
disabled: spec.requiresProcess !== void 0 && legacyProcess,
|
|
1719
|
+
onChange: (v) => editor.setAdjustments({ [spec.id]: v })
|
|
1720
|
+
}
|
|
1721
|
+
),
|
|
1722
|
+
spec.id === "tint" && legacyProcess && /* @__PURE__ */ jsxs7(
|
|
1723
|
+
"div",
|
|
1724
|
+
{
|
|
1725
|
+
className: "flex items-center justify-between gap-2 rounded border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-[10px] text-amber-200",
|
|
1726
|
+
"data-testid": "image-editor-process-notice",
|
|
1727
|
+
children: [
|
|
1728
|
+
/* @__PURE__ */ jsx7("span", { children: "Legacy rendering (process 1). Tint needs the current process." }),
|
|
1729
|
+
/* @__PURE__ */ jsx7(
|
|
1730
|
+
"button",
|
|
1731
|
+
{
|
|
1732
|
+
type: "button",
|
|
1733
|
+
onClick: () => editor.upgradeProcess(),
|
|
1734
|
+
className: "shrink-0 rounded bg-amber-500/20 px-2 py-0.5 hover:bg-amber-500/30",
|
|
1735
|
+
children: "Update"
|
|
1736
|
+
}
|
|
1737
|
+
)
|
|
1738
|
+
]
|
|
1739
|
+
}
|
|
1740
|
+
)
|
|
1741
|
+
] }, spec.id)),
|
|
1742
|
+
/* @__PURE__ */ jsx7("div", { className: "border-t border-slate-700/60 pt-3", children: /* @__PURE__ */ jsx7(
|
|
1743
|
+
AdjustmentLayersPanel,
|
|
1744
|
+
{
|
|
1745
|
+
controller,
|
|
1746
|
+
layers: state.adjustments.layers,
|
|
1747
|
+
histogram: editor.histogram
|
|
1748
|
+
}
|
|
1749
|
+
) })
|
|
1750
|
+
] }),
|
|
1751
|
+
activeTab === "annotate" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-3 text-xs", children: [
|
|
1752
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Draw & Annotate" }),
|
|
1753
|
+
/* @__PURE__ */ jsx7("div", { className: "grid grid-cols-3 gap-1.5", children: ["pen", "rect", "circle", "arrow", "line"].map((t) => /* @__PURE__ */ jsx7(
|
|
1754
|
+
"button",
|
|
1755
|
+
{
|
|
1756
|
+
type: "button",
|
|
1757
|
+
onClick: () => editor.setTool(t),
|
|
1758
|
+
className: `py-1.5 px-2 rounded-lg font-medium capitalize border transition-colors ${state.activeTool === t ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700"}`,
|
|
1759
|
+
children: t
|
|
1760
|
+
},
|
|
1761
|
+
t
|
|
1762
|
+
)) }),
|
|
1763
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
1764
|
+
/* @__PURE__ */ jsx7("label", { className: "text-slate-400", children: "Color:" }),
|
|
1765
|
+
/* @__PURE__ */ jsx7(
|
|
1766
|
+
"input",
|
|
1767
|
+
{
|
|
1768
|
+
type: "color",
|
|
1769
|
+
value: activeColor,
|
|
1770
|
+
onChange: (e) => setActiveColor(e.target.value),
|
|
1771
|
+
className: "w-8 h-8 rounded border border-slate-700 cursor-pointer bg-transparent"
|
|
1772
|
+
}
|
|
1773
|
+
),
|
|
1774
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex-1 ml-2", children: [
|
|
1775
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex justify-between text-slate-400 mb-0.5", children: [
|
|
1776
|
+
/* @__PURE__ */ jsx7("span", { children: "Width:" }),
|
|
1777
|
+
/* @__PURE__ */ jsxs7("span", { className: "font-mono", children: [
|
|
1778
|
+
strokeWidth,
|
|
1779
|
+
"px"
|
|
1780
|
+
] })
|
|
1781
|
+
] }),
|
|
1782
|
+
/* @__PURE__ */ jsx7(
|
|
1783
|
+
"input",
|
|
1784
|
+
{
|
|
1785
|
+
type: "range",
|
|
1786
|
+
min: "1",
|
|
1787
|
+
max: "20",
|
|
1788
|
+
value: strokeWidth,
|
|
1789
|
+
onChange: (e) => setStrokeWidth(Number(e.target.value)),
|
|
1790
|
+
className: "w-full accent-indigo-500"
|
|
1791
|
+
}
|
|
1792
|
+
)
|
|
1793
|
+
] })
|
|
1794
|
+
] }),
|
|
1795
|
+
/* @__PURE__ */ jsx7("div", { className: "h-px bg-slate-800 my-1" }),
|
|
1796
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Add Text" }),
|
|
1797
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1798
|
+
/* @__PURE__ */ jsx7(
|
|
1799
|
+
"input",
|
|
1800
|
+
{
|
|
1801
|
+
type: "text",
|
|
1802
|
+
placeholder: "Type watermark or label...",
|
|
1803
|
+
value: textInput,
|
|
1804
|
+
onChange: (e) => setTextInput(e.target.value),
|
|
1805
|
+
onKeyDown: (e) => e.key === "Enter" && handleAddText(),
|
|
1806
|
+
className: "flex-1 bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-indigo-500"
|
|
1807
|
+
}
|
|
1808
|
+
),
|
|
1809
|
+
/* @__PURE__ */ jsx7(
|
|
1810
|
+
"button",
|
|
1811
|
+
{
|
|
1812
|
+
type: "button",
|
|
1813
|
+
onClick: handleAddText,
|
|
1814
|
+
className: "px-3 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-medium shadow",
|
|
1815
|
+
children: "Add"
|
|
1816
|
+
}
|
|
1817
|
+
)
|
|
1818
|
+
] }),
|
|
1819
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between text-slate-400", children: [
|
|
1820
|
+
/* @__PURE__ */ jsx7("span", { children: "Font Size:" }),
|
|
1821
|
+
/* @__PURE__ */ jsxs7("span", { className: "font-mono", children: [
|
|
1822
|
+
fontSize,
|
|
1823
|
+
"px"
|
|
1824
|
+
] })
|
|
1825
|
+
] }),
|
|
1826
|
+
/* @__PURE__ */ jsx7(
|
|
1827
|
+
"input",
|
|
1828
|
+
{
|
|
1829
|
+
type: "range",
|
|
1830
|
+
min: "12",
|
|
1831
|
+
max: "72",
|
|
1832
|
+
value: fontSize,
|
|
1833
|
+
onChange: (e) => setFontSize(Number(e.target.value)),
|
|
1834
|
+
className: "w-full accent-indigo-500"
|
|
1835
|
+
}
|
|
1836
|
+
),
|
|
1837
|
+
/* @__PURE__ */ jsx7("div", { className: "h-px bg-slate-800 my-1" }),
|
|
1838
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
1839
|
+
/* @__PURE__ */ jsxs7("span", { className: "text-slate-400", children: [
|
|
1840
|
+
"Layers (",
|
|
1841
|
+
state.annotations.length,
|
|
1842
|
+
")"
|
|
1843
|
+
] }),
|
|
1844
|
+
state.selectedAnnotationId && /* @__PURE__ */ jsx7(
|
|
1845
|
+
"button",
|
|
1846
|
+
{
|
|
1847
|
+
type: "button",
|
|
1848
|
+
onClick: () => editor.removeAnnotation(state.selectedAnnotationId),
|
|
1849
|
+
className: "text-rose-400 hover:underline text-[11px]",
|
|
1850
|
+
children: "Delete Selected"
|
|
1851
|
+
}
|
|
1852
|
+
),
|
|
1853
|
+
state.annotations.length > 0 && /* @__PURE__ */ jsx7(
|
|
1854
|
+
"button",
|
|
1855
|
+
{
|
|
1856
|
+
type: "button",
|
|
1857
|
+
onClick: () => editor.clearAnnotations(),
|
|
1858
|
+
className: "text-slate-400 hover:text-slate-200 text-[11px]",
|
|
1859
|
+
children: "Clear All"
|
|
1860
|
+
}
|
|
1861
|
+
)
|
|
1862
|
+
] })
|
|
1863
|
+
] }),
|
|
1864
|
+
activeTab === "depth" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-3 text-xs", children: [
|
|
1865
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
1866
|
+
/* @__PURE__ */ jsx7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px]", children: "Depth Masking" }),
|
|
1867
|
+
/* @__PURE__ */ jsx7(
|
|
1868
|
+
"button",
|
|
1869
|
+
{
|
|
1870
|
+
type: "button",
|
|
1871
|
+
onClick: () => editor.resetDepthMask(),
|
|
1872
|
+
className: "text-[10px] text-rose-400 hover:underline",
|
|
1873
|
+
children: "Reset Mask"
|
|
1874
|
+
}
|
|
1875
|
+
)
|
|
1876
|
+
] }),
|
|
1877
|
+
/* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 cursor-pointer text-slate-300", children: [
|
|
1878
|
+
/* @__PURE__ */ jsx7(
|
|
1879
|
+
"input",
|
|
1880
|
+
{
|
|
1881
|
+
type: "checkbox",
|
|
1882
|
+
checked: state.depthMask.enabled,
|
|
1883
|
+
onChange: (e) => editor.setDepthMask({ enabled: e.target.checked }),
|
|
1884
|
+
className: "rounded accent-indigo-500"
|
|
1885
|
+
}
|
|
1886
|
+
),
|
|
1887
|
+
/* @__PURE__ */ jsx7("span", { children: "Enable Depth / Alpha Mask" })
|
|
1888
|
+
] }),
|
|
1889
|
+
state.depthMask.enabled && /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
1890
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1891
|
+
/* @__PURE__ */ jsx7("label", { className: "text-slate-400 block mb-1", children: "Mask Image URL:" }),
|
|
1892
|
+
/* @__PURE__ */ jsx7(
|
|
1893
|
+
"input",
|
|
1894
|
+
{
|
|
1895
|
+
type: "text",
|
|
1896
|
+
placeholder: "https://.../depthmap.png",
|
|
1897
|
+
value: state.depthMask.maskSource || "",
|
|
1898
|
+
onChange: (e) => editor.setDepthMask({ maskSource: e.target.value || null }),
|
|
1899
|
+
className: "w-full bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-100 placeholder-slate-500"
|
|
1900
|
+
}
|
|
1901
|
+
)
|
|
1902
|
+
] }),
|
|
1903
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex gap-2", children: [
|
|
1904
|
+
/* @__PURE__ */ jsx7(
|
|
1905
|
+
"button",
|
|
1906
|
+
{
|
|
1907
|
+
type: "button",
|
|
1908
|
+
onClick: () => editor.setDepthMask({ mode: "threshold" }),
|
|
1909
|
+
className: `flex-1 py-1.5 rounded-lg border text-xs font-medium ${state.depthMask.mode === "threshold" ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 border-slate-700 text-slate-300"}`,
|
|
1910
|
+
children: "Threshold Mode"
|
|
1911
|
+
}
|
|
1912
|
+
),
|
|
1913
|
+
/* @__PURE__ */ jsx7(
|
|
1914
|
+
"button",
|
|
1915
|
+
{
|
|
1916
|
+
type: "button",
|
|
1917
|
+
onClick: () => editor.setDepthMask({ mode: "brightness" }),
|
|
1918
|
+
className: `flex-1 py-1.5 rounded-lg border text-xs font-medium ${state.depthMask.mode === "brightness" ? "bg-indigo-600 border-indigo-500 text-white" : "bg-slate-800 border-slate-700 text-slate-300"}`,
|
|
1919
|
+
children: "Brightness Mode"
|
|
1920
|
+
}
|
|
1921
|
+
)
|
|
1922
|
+
] }),
|
|
1923
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1924
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex justify-between text-slate-400 mb-1", children: [
|
|
1925
|
+
/* @__PURE__ */ jsx7("span", { children: "Depth Start (Lower):" }),
|
|
1926
|
+
/* @__PURE__ */ jsx7("span", { className: "font-mono", children: state.depthMask.depthRange[0] })
|
|
1927
|
+
] }),
|
|
1928
|
+
/* @__PURE__ */ jsx7(
|
|
1929
|
+
"input",
|
|
1930
|
+
{
|
|
1931
|
+
type: "range",
|
|
1932
|
+
min: "0",
|
|
1933
|
+
max: "255",
|
|
1934
|
+
value: state.depthMask.depthRange[0],
|
|
1935
|
+
onChange: (e) => editor.setDepthMask({
|
|
1936
|
+
depthRange: [Number(e.target.value), state.depthMask.depthRange[1]]
|
|
1937
|
+
}),
|
|
1938
|
+
className: "w-full accent-indigo-500"
|
|
1939
|
+
}
|
|
1940
|
+
)
|
|
1941
|
+
] }),
|
|
1942
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1943
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex justify-between text-slate-400 mb-1", children: [
|
|
1944
|
+
/* @__PURE__ */ jsx7("span", { children: "Depth Stop (Upper):" }),
|
|
1945
|
+
/* @__PURE__ */ jsx7("span", { className: "font-mono", children: state.depthMask.depthRange[1] })
|
|
1946
|
+
] }),
|
|
1947
|
+
/* @__PURE__ */ jsx7(
|
|
1948
|
+
"input",
|
|
1949
|
+
{
|
|
1950
|
+
type: "range",
|
|
1951
|
+
min: "0",
|
|
1952
|
+
max: "255",
|
|
1953
|
+
value: state.depthMask.depthRange[1],
|
|
1954
|
+
onChange: (e) => editor.setDepthMask({
|
|
1955
|
+
depthRange: [state.depthMask.depthRange[0], Number(e.target.value)]
|
|
1956
|
+
}),
|
|
1957
|
+
className: "w-full accent-indigo-500"
|
|
1958
|
+
}
|
|
1959
|
+
)
|
|
1960
|
+
] }),
|
|
1961
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1962
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex justify-between text-slate-400 mb-1", children: [
|
|
1963
|
+
/* @__PURE__ */ jsx7("span", { children: "Softness:" }),
|
|
1964
|
+
/* @__PURE__ */ jsx7("span", { className: "font-mono", children: state.depthMask.softness })
|
|
1965
|
+
] }),
|
|
1966
|
+
/* @__PURE__ */ jsx7(
|
|
1967
|
+
"input",
|
|
1968
|
+
{
|
|
1969
|
+
type: "range",
|
|
1970
|
+
min: "1",
|
|
1971
|
+
max: "50",
|
|
1972
|
+
value: state.depthMask.softness,
|
|
1973
|
+
onChange: (e) => editor.setDepthMask({ softness: Number(e.target.value) }),
|
|
1974
|
+
className: "w-full accent-indigo-500"
|
|
1975
|
+
}
|
|
1976
|
+
)
|
|
1977
|
+
] }),
|
|
1978
|
+
/* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2 cursor-pointer text-slate-300 mt-1", children: [
|
|
1979
|
+
/* @__PURE__ */ jsx7(
|
|
1980
|
+
"input",
|
|
1981
|
+
{
|
|
1982
|
+
type: "checkbox",
|
|
1983
|
+
checked: state.depthMask.invert,
|
|
1984
|
+
onChange: (e) => editor.setDepthMask({ invert: e.target.checked }),
|
|
1985
|
+
className: "rounded accent-indigo-500"
|
|
1986
|
+
}
|
|
1987
|
+
),
|
|
1988
|
+
/* @__PURE__ */ jsx7("span", { children: "Invert Mask" })
|
|
1989
|
+
] })
|
|
1990
|
+
] })
|
|
1991
|
+
] }),
|
|
1992
|
+
activeTab === "select" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-4 text-xs", children: [
|
|
1993
|
+
/* @__PURE__ */ jsx7(
|
|
1994
|
+
SelectionOptionsBar,
|
|
1995
|
+
{
|
|
1996
|
+
controller,
|
|
1997
|
+
options: selectionOptions,
|
|
1998
|
+
onOptionsChange: setSelectionOptions,
|
|
1999
|
+
selection: state.selection ?? null,
|
|
2000
|
+
quickMask: Boolean(state.quickMask)
|
|
2001
|
+
}
|
|
2002
|
+
),
|
|
2003
|
+
selectionError && /* @__PURE__ */ jsx7("p", { role: "alert", className: "text-rose-400", children: selectionError }),
|
|
2004
|
+
/* @__PURE__ */ jsx7(RefineEdgePanel, { controller, selection: state.selection ?? null, layers: state.adjustments.layers })
|
|
2005
|
+
] }),
|
|
2006
|
+
activeTab === "ai" && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-4 text-xs", children: [
|
|
2007
|
+
/* @__PURE__ */ jsxs7("div", { className: "p-3 rounded-lg bg-slate-950/60 border border-slate-800 flex flex-col gap-2.5", children: [
|
|
2008
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
2009
|
+
/* @__PURE__ */ jsxs7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px] flex items-center gap-1.5", children: [
|
|
2010
|
+
/* @__PURE__ */ jsx7("svg", { className: "w-3.5 h-3.5 text-indigo-400", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx7("path", { d: "M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" }) }),
|
|
2011
|
+
"Background Removal"
|
|
2012
|
+
] }),
|
|
2013
|
+
isRemovingBg && /* @__PURE__ */ jsx7("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-indigo-900/60 text-indigo-300 animate-pulse", children: "Removing..." })
|
|
2014
|
+
] }),
|
|
2015
|
+
/* @__PURE__ */ jsx7("p", { className: "text-slate-400 text-[11px] leading-relaxed", children: "Isolate foreground subject with AI alpha transparency segmentation." }),
|
|
2016
|
+
/* @__PURE__ */ jsx7(
|
|
2017
|
+
"button",
|
|
2018
|
+
{
|
|
2019
|
+
type: "button",
|
|
2020
|
+
onClick: handleRemoveBackground,
|
|
2021
|
+
disabled: isRemovingBg || isInpainting,
|
|
2022
|
+
className: "mt-1 px-3 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white font-medium shadow transition-colors flex items-center justify-center gap-2",
|
|
2023
|
+
children: isRemovingBg ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2024
|
+
/* @__PURE__ */ jsx7("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
|
|
2025
|
+
"Removing Background..."
|
|
2026
|
+
] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2027
|
+
/* @__PURE__ */ jsx7("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx7("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }),
|
|
2028
|
+
"Remove Background"
|
|
2029
|
+
] })
|
|
2030
|
+
}
|
|
2031
|
+
)
|
|
2032
|
+
] }),
|
|
2033
|
+
/* @__PURE__ */ jsxs7("div", { className: "p-3 rounded-lg bg-slate-950/60 border border-slate-800 flex flex-col gap-2.5", children: [
|
|
2034
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between", children: [
|
|
2035
|
+
/* @__PURE__ */ jsxs7("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px] flex items-center gap-1.5", children: [
|
|
2036
|
+
/* @__PURE__ */ jsxs7("svg", { className: "w-3.5 h-3.5 text-purple-400", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
|
|
2037
|
+
/* @__PURE__ */ jsx7("path", { d: "m21.64 3.64-1.28-1.28a1.2 1.2 0 0 0-1.7 0l-9.72 9.72-1.94 4.86 4.86-1.94 9.72-9.72a1.2 1.2 0 0 0 .06-1.64z" }),
|
|
2038
|
+
/* @__PURE__ */ jsx7("path", { d: "m14 7 3 3" })
|
|
2039
|
+
] }),
|
|
2040
|
+
"Generative Inpainting"
|
|
2041
|
+
] }),
|
|
2042
|
+
isInpainting && /* @__PURE__ */ jsx7("span", { className: "inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-900/60 text-purple-300 animate-pulse", children: "Inpainting..." })
|
|
2043
|
+
] }),
|
|
2044
|
+
/* @__PURE__ */ jsx7("p", { className: "text-slate-400 text-[11px] leading-relaxed", children: "Replace, modify, or fill selected regions using prompt-guided generative inpainting." }),
|
|
2045
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1", children: [
|
|
2046
|
+
/* @__PURE__ */ jsx7("label", { htmlFor: "ai-inpaint-prompt-react", className: "text-slate-300 text-[11px] font-medium", children: "Prompt" }),
|
|
2047
|
+
/* @__PURE__ */ jsx7(
|
|
2048
|
+
"textarea",
|
|
2049
|
+
{
|
|
2050
|
+
id: "ai-inpaint-prompt-react",
|
|
2051
|
+
value: inpaintPrompt,
|
|
2052
|
+
onChange: (e) => setInpaintPrompt(e.target.value),
|
|
2053
|
+
placeholder: "e.g. replace sunglasses with steampunk goggles, remove person in background...",
|
|
2054
|
+
rows: 3,
|
|
2055
|
+
className: "w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-xs text-slate-100 placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-none"
|
|
2056
|
+
}
|
|
2057
|
+
)
|
|
2058
|
+
] }),
|
|
2059
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1", children: [
|
|
2060
|
+
/* @__PURE__ */ jsx7("span", { className: "text-slate-300 text-[11px] font-medium", children: "Target Mask Region" }),
|
|
2061
|
+
/* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-2 gap-1.5", children: [
|
|
2062
|
+
/* @__PURE__ */ jsxs7(
|
|
2063
|
+
"button",
|
|
2064
|
+
{
|
|
2065
|
+
type: "button",
|
|
2066
|
+
onClick: () => setInpaintMaskMode("annotations"),
|
|
2067
|
+
className: `px-2 py-1.5 rounded border text-[11px] font-medium text-left transition-colors ${inpaintMaskMode === "annotations" ? "bg-purple-600/30 border-purple-500 text-purple-200" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-slate-200"}`,
|
|
2068
|
+
children: [
|
|
2069
|
+
"Drawn Shapes (",
|
|
2070
|
+
state.annotations.length,
|
|
2071
|
+
")"
|
|
2072
|
+
]
|
|
2073
|
+
}
|
|
2074
|
+
),
|
|
2075
|
+
/* @__PURE__ */ jsxs7(
|
|
2076
|
+
"button",
|
|
2077
|
+
{
|
|
2078
|
+
type: "button",
|
|
2079
|
+
onClick: () => setInpaintMaskMode("crop"),
|
|
2080
|
+
className: `px-2 py-1.5 rounded border text-[11px] font-medium text-left transition-colors ${inpaintMaskMode === "crop" ? "bg-purple-600/30 border-purple-500 text-purple-200" : "bg-slate-900 border-slate-700 text-slate-400 hover:text-slate-200"}`,
|
|
2081
|
+
children: [
|
|
2082
|
+
"Crop Box (",
|
|
2083
|
+
state.crop ? "Selected" : "Full",
|
|
2084
|
+
")"
|
|
2085
|
+
]
|
|
2086
|
+
}
|
|
2087
|
+
)
|
|
2088
|
+
] })
|
|
2089
|
+
] }),
|
|
2090
|
+
/* @__PURE__ */ jsx7(
|
|
2091
|
+
"button",
|
|
2092
|
+
{
|
|
2093
|
+
type: "button",
|
|
2094
|
+
onClick: handleInpaint,
|
|
2095
|
+
disabled: isInpainting || isRemovingBg || !inpaintPrompt.trim(),
|
|
2096
|
+
className: "mt-1 px-3 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 disabled:opacity-50 text-white font-medium shadow transition-colors flex items-center justify-center gap-2",
|
|
2097
|
+
children: isInpainting ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2098
|
+
/* @__PURE__ */ jsx7("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
|
|
2099
|
+
"Generating Inpaint..."
|
|
2100
|
+
] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
2101
|
+
/* @__PURE__ */ jsx7("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx7("path", { d: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" }) }),
|
|
2102
|
+
"Apply Inpainting"
|
|
2103
|
+
] })
|
|
2104
|
+
}
|
|
2105
|
+
)
|
|
2106
|
+
] }),
|
|
2107
|
+
aiError && /* @__PURE__ */ jsx7("div", { className: "p-2.5 rounded-lg bg-rose-950/60 border border-rose-800 text-rose-300 text-[11px]", children: aiError }),
|
|
2108
|
+
aiSuccess && /* @__PURE__ */ jsx7("div", { className: "p-2.5 rounded-lg bg-emerald-950/60 border border-emerald-800 text-emerald-300 text-[11px]", children: aiSuccess })
|
|
2109
|
+
] })
|
|
2110
|
+
] })
|
|
2111
|
+
] })
|
|
2112
|
+
]
|
|
2113
|
+
}
|
|
2114
|
+
);
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
export {
|
|
2118
|
+
useImageEditor,
|
|
2119
|
+
ControlSlider,
|
|
2120
|
+
Histogram,
|
|
2121
|
+
useImageHistogram,
|
|
2122
|
+
CurvesEditor,
|
|
2123
|
+
AdjustmentLayersPanel,
|
|
2124
|
+
SelectionOptionsBar,
|
|
2125
|
+
RefineEdgePanel,
|
|
2126
|
+
ImageEditor
|
|
2127
|
+
};
|
|
2128
|
+
//# sourceMappingURL=chunk-ZU54BMDC.js.map
|