@pixerate/editor-react 0.8.0 → 0.9.1

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.
@@ -88,6 +88,22 @@ function useImageEditor(options = {}) {
88
88
  const toBlob = (0, import_react.useCallback)((opts) => controller.toBlob(opts), [controller]);
89
89
  const toJSON = (0, import_react.useCallback)(() => controller.toJSON(), [controller]);
90
90
  const loadJSON = (0, import_react.useCallback)((data) => controller.loadJSON(data), [controller]);
91
+ const renderMask = (0, import_react.useCallback)(
92
+ (opts) => controller.renderMask(opts),
93
+ [controller]
94
+ );
95
+ const toMaskDataURL = (0, import_react.useCallback)(
96
+ (opts) => controller.toMaskDataURL(opts),
97
+ [controller]
98
+ );
99
+ const applyInpaintedImage = (0, import_react.useCallback)(
100
+ (newDataUrl) => controller.applyInpaintedImage(newDataUrl),
101
+ [controller]
102
+ );
103
+ const applyBackgroundRemovedImage = (0, import_react.useCallback)(
104
+ (newDataUrl) => controller.applyBackgroundRemovedImage(newDataUrl),
105
+ [controller]
106
+ );
91
107
  return (0, import_react.useMemo)(
92
108
  () => ({
93
109
  controller,
@@ -124,7 +140,11 @@ function useImageEditor(options = {}) {
124
140
  toDataURL,
125
141
  toBlob,
126
142
  toJSON,
127
- loadJSON
143
+ loadJSON,
144
+ renderMask,
145
+ toMaskDataURL,
146
+ applyInpaintedImage,
147
+ applyBackgroundRemovedImage
128
148
  }),
129
149
  // eslint-disable-next-line react-hooks/exhaustive-deps
130
150
  [controller, state, historyTicks]
@@ -141,7 +161,10 @@ var ImageEditor = ({
141
161
  className = "",
142
162
  style,
143
163
  onSave,
144
- exportOptions
164
+ exportOptions,
165
+ onInpaint,
166
+ onRemoveBackground,
167
+ aiHooks
145
168
  }) => {
146
169
  const internalEditor = useImageEditor({ image: src });
147
170
  const editor = externalEditor || internalEditor;
@@ -150,6 +173,73 @@ var ImageEditor = ({
150
173
  const containerRef = (0, import_react2.useRef)(null);
151
174
  const [activeTab, setActiveTab] = (0, import_react2.useState)("crop");
152
175
  const [activeCropRatio, setActiveCropRatio] = (0, import_react2.useState)(null);
176
+ const [inpaintPrompt, setInpaintPrompt] = (0, import_react2.useState)("");
177
+ const [inpaintMaskMode, setInpaintMaskMode] = (0, import_react2.useState)("annotations");
178
+ const [isRemovingBg, setIsRemovingBg] = (0, import_react2.useState)(false);
179
+ const [isInpainting, setIsInpainting] = (0, import_react2.useState)(false);
180
+ const [aiError, setAiError] = (0, import_react2.useState)(null);
181
+ const [aiSuccess, setAiSuccess] = (0, import_react2.useState)(null);
182
+ const effectiveOnInpaint = onInpaint || aiHooks?.onInpaint;
183
+ const effectiveOnRemoveBg = onRemoveBackground || aiHooks?.onRemoveBackground;
184
+ const handleRemoveBackground = async () => {
185
+ if (!effectiveOnRemoveBg) {
186
+ setAiError("No background removal hook is configured.");
187
+ return;
188
+ }
189
+ setAiError(null);
190
+ setAiSuccess(null);
191
+ setIsRemovingBg(true);
192
+ try {
193
+ const imageDataUrl = await editor.toDataURL(exportOptions);
194
+ const result = await effectiveOnRemoveBg({ imageDataUrl });
195
+ if (typeof result === "string") {
196
+ await editor.applyBackgroundRemovedImage(result);
197
+ setAiSuccess("Background removed successfully!");
198
+ } else {
199
+ setAiSuccess("Background removal complete.");
200
+ }
201
+ } catch (err) {
202
+ setAiError(err?.message || "Failed to remove background.");
203
+ } finally {
204
+ setIsRemovingBg(false);
205
+ }
206
+ };
207
+ const handleInpaint = async () => {
208
+ if (!effectiveOnInpaint) {
209
+ setAiError("No inpainting hook is configured.");
210
+ return;
211
+ }
212
+ if (!inpaintPrompt.trim()) {
213
+ setAiError("Please enter an inpainting prompt.");
214
+ return;
215
+ }
216
+ setAiError(null);
217
+ setAiSuccess(null);
218
+ setIsInpainting(true);
219
+ try {
220
+ const imageDataUrl = await editor.toDataURL(exportOptions);
221
+ const maskDataUrl = await editor.toMaskDataURL({
222
+ useAnnotations: inpaintMaskMode === "annotations",
223
+ useCrop: inpaintMaskMode === "crop"
224
+ });
225
+ const result = await effectiveOnInpaint({
226
+ prompt: inpaintPrompt.trim(),
227
+ imageDataUrl,
228
+ maskDataUrl,
229
+ cropBox: inpaintMaskMode === "crop" ? editor.state.crop : null
230
+ });
231
+ if (typeof result === "string") {
232
+ await editor.applyInpaintedImage(result);
233
+ setAiSuccess("Inpainting applied successfully!");
234
+ } else {
235
+ setAiSuccess("Inpainting complete.");
236
+ }
237
+ } catch (err) {
238
+ setAiError(err?.message || "Failed to apply inpainting.");
239
+ } finally {
240
+ setIsInpainting(false);
241
+ }
242
+ };
153
243
  const [activeColor, setActiveColor] = (0, import_react2.useState)("#ef4444");
154
244
  const [strokeWidth, setStrokeWidth] = (0, import_react2.useState)(3);
155
245
  const [fontSize, setFontSize] = (0, import_react2.useState)(24);
@@ -605,6 +695,21 @@ var ImageEditor = ({
605
695
  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"}`,
606
696
  children: "Depth Mask"
607
697
  }
698
+ ),
699
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ onClick: () => {
704
+ setActiveTab("ai");
705
+ editor.setTool("select");
706
+ },
707
+ 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"}`,
708
+ children: [
709
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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" }) }),
710
+ "AI Tools"
711
+ ]
712
+ }
608
713
  )
609
714
  ] }),
610
715
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2 text-xs", children: [
@@ -1204,6 +1309,110 @@ var ImageEditor = ({
1204
1309
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Invert Mask" })
1205
1310
  ] })
1206
1311
  ] })
1312
+ ] }),
1313
+ activeTab === "ai" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-4 text-xs", children: [
1314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-3 rounded-lg bg-slate-950/60 border border-slate-800 flex flex-col gap-2.5", children: [
1315
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
1316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px] flex items-center gap-1.5", children: [
1317
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-3.5 h-3.5 text-indigo-400", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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" }) }),
1318
+ "Background Removal"
1319
+ ] }),
1320
+ isRemovingBg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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..." })
1321
+ ] }),
1322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-slate-400 text-[11px] leading-relaxed", children: "Isolate foreground subject with AI alpha transparency segmentation." }),
1323
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1324
+ "button",
1325
+ {
1326
+ type: "button",
1327
+ onClick: handleRemoveBackground,
1328
+ disabled: isRemovingBg || isInpainting,
1329
+ 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",
1330
+ children: isRemovingBg ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
1332
+ "Removing Background..."
1333
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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" }) }),
1335
+ "Remove Background"
1336
+ ] })
1337
+ }
1338
+ )
1339
+ ] }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-3 rounded-lg bg-slate-950/60 border border-slate-800 flex flex-col gap-2.5", children: [
1341
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
1342
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-semibold text-slate-200 uppercase tracking-wider text-[11px] flex items-center gap-1.5", children: [
1343
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { className: "w-3.5 h-3.5 text-purple-400", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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" }),
1345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "m14 7 3 3" })
1346
+ ] }),
1347
+ "Generative Inpainting"
1348
+ ] }),
1349
+ isInpainting && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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..." })
1350
+ ] }),
1351
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-slate-400 text-[11px] leading-relaxed", children: "Replace, modify, or fill selected regions using prompt-guided generative inpainting." }),
1352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-1", children: [
1353
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: "ai-inpaint-prompt-react", className: "text-slate-300 text-[11px] font-medium", children: "Prompt" }),
1354
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1355
+ "textarea",
1356
+ {
1357
+ id: "ai-inpaint-prompt-react",
1358
+ value: inpaintPrompt,
1359
+ onChange: (e) => setInpaintPrompt(e.target.value),
1360
+ placeholder: "e.g. replace sunglasses with steampunk goggles, remove person in background...",
1361
+ rows: 3,
1362
+ 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"
1363
+ }
1364
+ )
1365
+ ] }),
1366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-1", children: [
1367
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-slate-300 text-[11px] font-medium", children: "Target Mask Region" }),
1368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "grid grid-cols-2 gap-1.5", children: [
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1370
+ "button",
1371
+ {
1372
+ type: "button",
1373
+ onClick: () => setInpaintMaskMode("annotations"),
1374
+ 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"}`,
1375
+ children: [
1376
+ "Drawn Shapes (",
1377
+ state.annotations.length,
1378
+ ")"
1379
+ ]
1380
+ }
1381
+ ),
1382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1383
+ "button",
1384
+ {
1385
+ type: "button",
1386
+ onClick: () => setInpaintMaskMode("crop"),
1387
+ 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"}`,
1388
+ children: [
1389
+ "Crop Box (",
1390
+ state.crop ? "Selected" : "Full",
1391
+ ")"
1392
+ ]
1393
+ }
1394
+ )
1395
+ ] })
1396
+ ] }),
1397
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ onClick: handleInpaint,
1402
+ disabled: isInpainting || isRemovingBg || !inpaintPrompt.trim(),
1403
+ 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",
1404
+ children: isInpainting ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" }),
1406
+ "Generating Inpaint..."
1407
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1408
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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" }) }),
1409
+ "Apply Inpainting"
1410
+ ] })
1411
+ }
1412
+ )
1413
+ ] }),
1414
+ aiError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-2.5 rounded-lg bg-rose-950/60 border border-rose-800 text-rose-300 text-[11px]", children: aiError }),
1415
+ aiSuccess && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-2.5 rounded-lg bg-emerald-950/60 border border-emerald-800 text-emerald-300 text-[11px]", children: aiSuccess })
1207
1416
  ] })
1208
1417
  ] })
1209
1418
  ] })