@pixerate/editor 0.188.0 → 0.189.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/dist/{chunk-JYV4R62M.cjs → chunk-ED4L4UD6.cjs} +9 -1
- package/dist/{chunk-IGMNJM2H.js → chunk-ZEHTUR7Z.js} +8 -0
- package/dist/image-editor/controller.d.cts +17 -0
- package/dist/image-editor/controller.d.ts +17 -0
- package/dist/image-editor/index.cjs +1584 -1448
- package/dist/image-editor/index.js +568 -432
- package/dist/image-editor/layers/paint-ui.d.cts +2 -1
- package/dist/image-editor/layers/paint-ui.d.ts +2 -1
- package/dist/image-editor/layers/panel.d.cts +3 -2
- package/dist/image-editor/layers/panel.d.ts +3 -2
- package/dist/image-editor/renderer.d.cts +6 -0
- package/dist/image-editor/renderer.d.ts +6 -0
- package/dist/image-editor/selection/gesture.d.cts +10 -0
- package/dist/image-editor/selection/gesture.d.ts +10 -0
- package/dist/image-editor/selection/layer-mask.d.cts +6 -0
- package/dist/image-editor/selection/layer-mask.d.ts +6 -0
- package/dist/image-editor/types.d.cts +8 -0
- package/dist/image-editor/types.d.ts +8 -0
- package/dist/paint-editor/index.cjs +6 -6
- package/dist/paint-editor/index.js +1 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +4 -1
|
@@ -196,6 +196,7 @@ import {
|
|
|
196
196
|
layerMaskCoverage,
|
|
197
197
|
layerMaskFromSelection,
|
|
198
198
|
layerMaskKey,
|
|
199
|
+
layerMaskSelection,
|
|
199
200
|
layerName,
|
|
200
201
|
layerTree,
|
|
201
202
|
layerTypeLabel,
|
|
@@ -372,7 +373,7 @@ import {
|
|
|
372
373
|
writePsd,
|
|
373
374
|
xyToTempTint,
|
|
374
375
|
xyToUv
|
|
375
|
-
} from "../chunk-
|
|
376
|
+
} from "../chunk-ZEHTUR7Z.js";
|
|
376
377
|
import {
|
|
377
378
|
parseCssColor
|
|
378
379
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1365,6 +1366,24 @@ var ImageEditorRenderer = class {
|
|
|
1365
1366
|
}
|
|
1366
1367
|
ctx.restore();
|
|
1367
1368
|
}
|
|
1369
|
+
const maskView = state.maskView;
|
|
1370
|
+
if (maskView) {
|
|
1371
|
+
const layer = flattenLayers(state.layers ?? []).find((l) => l.id === maskView.layerId);
|
|
1372
|
+
const mask = layer ? this.maskViewSelection(layer, Math.round(state.imageDimensions.width), Math.round(state.imageDimensions.height)) : null;
|
|
1373
|
+
if (mask) {
|
|
1374
|
+
ctx.save();
|
|
1375
|
+
try {
|
|
1376
|
+
applyImageProjectionToContext(ctx, projection);
|
|
1377
|
+
ctx.beginPath();
|
|
1378
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
1379
|
+
ctx.clip();
|
|
1380
|
+
if (maskView.mode === "gray") this.drawMaskGray(ctx, mask);
|
|
1381
|
+
else this.drawMaskOverlay(ctx, mask, { color: [255, 0, 0], opacity: 0.5, indicates: "masked" });
|
|
1382
|
+
} catch {
|
|
1383
|
+
}
|
|
1384
|
+
ctx.restore();
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1368
1387
|
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1369
1388
|
const shownChannels = (state.channels ?? []).filter((c) => c.visible);
|
|
1370
1389
|
if (shownChannels.length) {
|
|
@@ -1804,6 +1823,32 @@ var ImageEditorRenderer = class {
|
|
|
1804
1823
|
return this.emptyMaskCache;
|
|
1805
1824
|
}
|
|
1806
1825
|
overlayCache = /* @__PURE__ */ new WeakMap();
|
|
1826
|
+
maskViewCache = /* @__PURE__ */ new WeakMap();
|
|
1827
|
+
grayCache = /* @__PURE__ */ new WeakMap();
|
|
1828
|
+
/** A layer's mask as canvas-sized coverage for its mask view, kept while the mask is unchanged. */
|
|
1829
|
+
maskViewSelection(layer, width, height) {
|
|
1830
|
+
if (!layer.mask || layer.mask.source || width < 1 || height < 1) return null;
|
|
1831
|
+
const key = `${width}x${height}:${layer.type === "pixel" ? `${layer.offset.x},${layer.offset.y}` : ""}`;
|
|
1832
|
+
const cached = this.maskViewCache.get(layer.mask.surface);
|
|
1833
|
+
if (cached?.key === key) return cached.mask;
|
|
1834
|
+
const mask = layerMaskSelection(layer, width, height);
|
|
1835
|
+
if (mask) this.maskViewCache.set(layer.mask.surface, { key, mask });
|
|
1836
|
+
return mask;
|
|
1837
|
+
}
|
|
1838
|
+
/** A mask alone in grey (white = revealed), opaque, drawn in natural image space. */
|
|
1839
|
+
drawMaskGray(ctx, mask) {
|
|
1840
|
+
let canvas = this.grayCache.get(mask);
|
|
1841
|
+
if (!canvas) {
|
|
1842
|
+
canvas = this.createCanvas(mask.width, mask.height);
|
|
1843
|
+
const mctx = canvas.getContext("2d");
|
|
1844
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
1845
|
+
const img = mctx.createImageData(mask.width, mask.height);
|
|
1846
|
+
img.data.set(selectionToPixels(mask, "gray").data);
|
|
1847
|
+
mctx.putImageData(img, 0, 0);
|
|
1848
|
+
this.grayCache.set(mask, canvas);
|
|
1849
|
+
}
|
|
1850
|
+
ctx.drawImage(canvas, 0, 0, mask.width, mask.height);
|
|
1851
|
+
}
|
|
1807
1852
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1808
1853
|
drawMaskOverlay(ctx, mask, style) {
|
|
1809
1854
|
const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
@@ -8351,6 +8396,41 @@ var ImageEditorController = class {
|
|
|
8351
8396
|
this.state.quickMask = display === "quickMask";
|
|
8352
8397
|
this.notify();
|
|
8353
8398
|
}
|
|
8399
|
+
// --- Mask views ---
|
|
8400
|
+
/** The layer mask shown on the canvas, or `null` (see `setMaskView`). */
|
|
8401
|
+
getMaskView() {
|
|
8402
|
+
const view = this.state.maskView;
|
|
8403
|
+
if (!view) return null;
|
|
8404
|
+
const layer = this.getLayer(view.layerId);
|
|
8405
|
+
return layer?.mask && !layer.mask.source ? view : null;
|
|
8406
|
+
}
|
|
8407
|
+
/**
|
|
8408
|
+
* Shows a layer's mask on the canvas: `gray` alone in grey (Alt-click the mask thumbnail),
|
|
8409
|
+
* `overlay` as a red tint over what it hides (the \ key), `off` back to the image. The layer
|
|
8410
|
+
* defaults to the active one. View state: no undo step, not saved. Throws when the layer has
|
|
8411
|
+
* no painted mask.
|
|
8412
|
+
*/
|
|
8413
|
+
setMaskView(mode, layerId) {
|
|
8414
|
+
if (mode !== "off" && mode !== "gray" && mode !== "overlay") throw new Error(`Unknown mask view "${String(mode)}". Use gray, overlay or off.`);
|
|
8415
|
+
if (mode === "off") {
|
|
8416
|
+
if (this.state.maskView) {
|
|
8417
|
+
this.state.maskView = null;
|
|
8418
|
+
this.notify();
|
|
8419
|
+
}
|
|
8420
|
+
return null;
|
|
8421
|
+
}
|
|
8422
|
+
const id = layerId ?? this.state.activeLayerId;
|
|
8423
|
+
if (!id) throw new LayerOperationError("No layer given and none is active; pass layerId (see list_layers).");
|
|
8424
|
+
const layer = requireLayer(this.state.layers, id).layer;
|
|
8425
|
+
if (!layer.mask) throw new LayerOperationError(`Layer "${layer.name || id}" has no mask to show; add one with add_layer_mask.`);
|
|
8426
|
+
if (layer.mask.source) throw new LayerOperationError(`Layer "${layer.name || id}" has a depth mask, which is computed, not painted, so it has no mask view.`);
|
|
8427
|
+
const cur = this.state.maskView;
|
|
8428
|
+
if (cur?.layerId !== id || cur.mode !== mode) {
|
|
8429
|
+
this.state.maskView = { layerId: id, mode };
|
|
8430
|
+
this.notify();
|
|
8431
|
+
}
|
|
8432
|
+
return this.state.maskView;
|
|
8433
|
+
}
|
|
8354
8434
|
// --- Alpha channels ---
|
|
8355
8435
|
/** The alpha channels (saved selections), in panel order. */
|
|
8356
8436
|
getChannels() {
|
|
@@ -12087,6 +12167,22 @@ function selectionShade(selection, doc) {
|
|
|
12087
12167
|
context.putImageData(pixels, 0, 0);
|
|
12088
12168
|
return canvas;
|
|
12089
12169
|
}
|
|
12170
|
+
function maskViewCanvas(controller, doc) {
|
|
12171
|
+
const view = controller.getMaskView();
|
|
12172
|
+
const layer = view ? controller.getLayer(view.layerId) : null;
|
|
12173
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
12174
|
+
const mask = layer ? layerMaskSelection(layer, Math.round(width), Math.round(height)) : null;
|
|
12175
|
+
if (!view || !mask) return null;
|
|
12176
|
+
const canvas = doc.createElement("canvas");
|
|
12177
|
+
canvas.width = mask.width;
|
|
12178
|
+
canvas.height = mask.height;
|
|
12179
|
+
const context = canvas.getContext("2d");
|
|
12180
|
+
if (!context) return null;
|
|
12181
|
+
const pixels = context.createImageData(mask.width, mask.height);
|
|
12182
|
+
pixels.data.set(view.mode === "gray" ? selectionToPixels(mask, "gray").data : quickMaskPixels(mask));
|
|
12183
|
+
context.putImageData(pixels, 0, 0);
|
|
12184
|
+
return canvas;
|
|
12185
|
+
}
|
|
12090
12186
|
async function renderImageRegion(controller, request) {
|
|
12091
12187
|
const state = controller.getState();
|
|
12092
12188
|
const k = Math.min(1, request.scale);
|
|
@@ -12109,6 +12205,8 @@ async function renderImageRegion(controller, request) {
|
|
|
12109
12205
|
context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);
|
|
12110
12206
|
if (request.includeOverlays) {
|
|
12111
12207
|
const s = request.scale;
|
|
12208
|
+
const maskView = maskViewCanvas(controller, document);
|
|
12209
|
+
if (maskView) context.drawImage(maskView, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);
|
|
12112
12210
|
const selection = state.selection;
|
|
12113
12211
|
if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
|
|
12114
12212
|
const shade = selectionShade(selection, document);
|
|
@@ -12136,7 +12234,7 @@ function createImageCaptureSurface(controller) {
|
|
|
12136
12234
|
units: "image pixels",
|
|
12137
12235
|
content: whole,
|
|
12138
12236
|
selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),
|
|
12139
|
-
overlays: "a shade over unselected pixels
|
|
12237
|
+
overlays: "a shade over unselected pixels, the crop box outline and a layer mask view (set_mask_view)",
|
|
12140
12238
|
maxScale: 8,
|
|
12141
12239
|
render: (request) => renderImageRegion(controller, request)
|
|
12142
12240
|
};
|
|
@@ -12824,6 +12922,27 @@ function createImageLayerTools(controller) {
|
|
|
12824
12922
|
return { layer: describe(id) };
|
|
12825
12923
|
}
|
|
12826
12924
|
};
|
|
12925
|
+
const maskView = {
|
|
12926
|
+
name: "set_mask_view",
|
|
12927
|
+
title: "Show a layer mask",
|
|
12928
|
+
description: "Shows a layer's mask on the canvas to check it: gray (the mask alone, white = revealed, as Alt-clicking its thumbnail does), overlay (a red tint over what it hides, as the backslash key does) or off (the image again). It changes only the view: no undo step, nothing saved, and render_preview and exports still show the image. Look with capture_view includeOverlays true, then turn it off. Fails when the layer has no painted mask. Returns the view.",
|
|
12929
|
+
inputSchema: {
|
|
12930
|
+
type: "object",
|
|
12931
|
+
properties: {
|
|
12932
|
+
view: { type: "string", enum: ["gray", "overlay", "off"], description: "gray, overlay or off." },
|
|
12933
|
+
layerId: { ...LAYER_ID2, description: "The layer whose mask to show (default the active layer; see list_layers). Ignored for off." }
|
|
12934
|
+
},
|
|
12935
|
+
required: ["view"],
|
|
12936
|
+
additionalProperties: false
|
|
12937
|
+
},
|
|
12938
|
+
annotations: { title: "Show a layer mask", ...WRITE_IDEMPOTENT2 },
|
|
12939
|
+
handler: ({ view, layerId }) => {
|
|
12940
|
+
if (view !== "gray" && view !== "overlay" && view !== "off") throw new Error(`Unknown view "${String(view)}". Use gray, overlay or off.`);
|
|
12941
|
+
const id = view === "off" || layerId === void 0 ? void 0 : exists(layerId);
|
|
12942
|
+
const shown = controller.setMaskView(view, id);
|
|
12943
|
+
return { view: shown?.mode ?? "off", layerId: shown?.layerId ?? null };
|
|
12944
|
+
}
|
|
12945
|
+
};
|
|
12827
12946
|
const maskProps = {
|
|
12828
12947
|
name: "set_layer_mask_properties",
|
|
12829
12948
|
title: "Set layer mask properties",
|
|
@@ -12964,6 +13083,7 @@ function createImageLayerTools(controller) {
|
|
|
12964
13083
|
addMask,
|
|
12965
13084
|
applyMask,
|
|
12966
13085
|
removeMask,
|
|
13086
|
+
maskView,
|
|
12967
13087
|
maskProps,
|
|
12968
13088
|
viaCopy,
|
|
12969
13089
|
rasterize,
|
|
@@ -13304,201 +13424,452 @@ function createImageEffectTools(controller) {
|
|
|
13304
13424
|
return [list, add2, update, remove, setEnabled, light];
|
|
13305
13425
|
}
|
|
13306
13426
|
|
|
13307
|
-
// src/image-editor/
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
{
|
|
13315
|
-
];
|
|
13316
|
-
var MIXER_OPTION_CONTROLS = [
|
|
13317
|
-
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13318
|
-
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13319
|
-
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13320
|
-
];
|
|
13321
|
-
var MIXER_MIXING_OPTIONS = [
|
|
13322
|
-
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
13323
|
-
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
13324
|
-
];
|
|
13325
|
-
var PAINT_TOOLS = [
|
|
13326
|
-
{ id: "move", label: "Move", shortcut: "V" },
|
|
13327
|
-
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
13328
|
-
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
13329
|
-
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
13330
|
-
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
13331
|
-
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
13332
|
-
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
13333
|
-
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
13334
|
-
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
13335
|
-
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
13336
|
-
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
13337
|
-
{ id: "blur", label: "Blur", shortcut: "" },
|
|
13338
|
-
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
13339
|
-
{ id: "history", label: "History", shortcut: "Y" }
|
|
13340
|
-
];
|
|
13341
|
-
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
13342
|
-
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
13343
|
-
var RETOUCH_OPTION_CONTROLS = [
|
|
13344
|
-
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
13345
|
-
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
|
|
13346
|
-
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain2, unit: "", tools: ["history"] }
|
|
13347
|
-
];
|
|
13348
|
-
var CLONE_TRANSFORM_CONTROLS = [
|
|
13349
|
-
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13350
|
-
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13351
|
-
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain2, unit: "\xB0" }
|
|
13352
|
-
];
|
|
13353
|
-
function cloneTransformView(controller) {
|
|
13354
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13355
|
-
return {
|
|
13356
|
-
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
13357
|
-
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13358
|
-
angle: t.rotate,
|
|
13359
|
-
flipH: t.scaleX < 0,
|
|
13360
|
-
flipV: t.scaleY < 0,
|
|
13361
|
-
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13362
|
-
};
|
|
13363
|
-
}
|
|
13364
|
-
function setCloneTransformValue(controller, id, value) {
|
|
13365
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13366
|
-
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13367
|
-
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13368
|
-
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13369
|
-
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13370
|
-
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13371
|
-
}
|
|
13372
|
-
var RETOUCH_CHOICES = [
|
|
13373
|
-
{
|
|
13374
|
-
id: "sample",
|
|
13375
|
-
label: "Sample",
|
|
13376
|
-
tools: ["clone", "heal", "spot-heal"],
|
|
13377
|
-
options: [
|
|
13378
|
-
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13379
|
-
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13380
|
-
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13381
|
-
]
|
|
13382
|
-
},
|
|
13383
|
-
{
|
|
13384
|
-
id: "spotType",
|
|
13385
|
-
label: "Type",
|
|
13386
|
-
tools: ["spot-heal"],
|
|
13387
|
-
options: [
|
|
13388
|
-
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13389
|
-
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13390
|
-
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13391
|
-
]
|
|
13392
|
-
},
|
|
13393
|
-
{
|
|
13394
|
-
id: "range",
|
|
13395
|
-
label: "Range",
|
|
13396
|
-
tools: ["dodge", "burn"],
|
|
13397
|
-
options: [
|
|
13398
|
-
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13399
|
-
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13400
|
-
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13401
|
-
]
|
|
13402
|
-
},
|
|
13403
|
-
{
|
|
13404
|
-
id: "spongeMode",
|
|
13405
|
-
label: "Mode",
|
|
13406
|
-
tools: ["sponge"],
|
|
13407
|
-
options: [
|
|
13408
|
-
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13409
|
-
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13410
|
-
]
|
|
13411
|
-
}
|
|
13412
|
-
];
|
|
13413
|
-
var RETOUCH_TOGGLES = [
|
|
13414
|
-
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
13415
|
-
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
13416
|
-
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
13417
|
-
];
|
|
13418
|
-
function setRetouchChoice(controller, id, value) {
|
|
13419
|
-
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
13420
|
-
}
|
|
13421
|
-
function cloneSourceLabel(controller) {
|
|
13422
|
-
const cs = controller.getState().paint.cloneSource;
|
|
13423
|
-
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
13424
|
-
}
|
|
13425
|
-
function rgbUnitToHex(c) {
|
|
13426
|
-
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
13427
|
-
}
|
|
13428
|
-
function sizeStep(size) {
|
|
13429
|
-
return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
|
|
13430
|
-
}
|
|
13431
|
-
var isTyping = (t) => {
|
|
13432
|
-
const el = t;
|
|
13433
|
-
return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
|
|
13434
|
-
};
|
|
13435
|
-
function handlePaintShortcut(controller, e) {
|
|
13436
|
-
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
13437
|
-
const brush = controller.getState().paint.brush;
|
|
13438
|
-
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
13439
|
-
const key = e.key;
|
|
13440
|
-
switch (key.toLowerCase()) {
|
|
13441
|
-
case "q":
|
|
13442
|
-
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13443
|
-
return true;
|
|
13444
|
-
case "b":
|
|
13445
|
-
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
13446
|
-
return true;
|
|
13447
|
-
case "e":
|
|
13448
|
-
controller.setTool("eraser");
|
|
13449
|
-
return true;
|
|
13450
|
-
case "v":
|
|
13451
|
-
controller.setTool("move");
|
|
13452
|
-
return true;
|
|
13453
|
-
case "s":
|
|
13454
|
-
controller.setTool("clone");
|
|
13455
|
-
return true;
|
|
13456
|
-
case "j":
|
|
13457
|
-
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
13458
|
-
return true;
|
|
13459
|
-
case "o": {
|
|
13460
|
-
const cur = controller.getState().activeTool;
|
|
13461
|
-
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
13462
|
-
controller.setTool(next);
|
|
13463
|
-
return true;
|
|
13464
|
-
}
|
|
13465
|
-
case "y":
|
|
13466
|
-
controller.setTool("history");
|
|
13467
|
-
return true;
|
|
13468
|
-
case "d":
|
|
13469
|
-
controller.resetPaintColors();
|
|
13470
|
-
return true;
|
|
13471
|
-
case "x":
|
|
13472
|
-
controller.swapPaintColors();
|
|
13473
|
-
return true;
|
|
13474
|
-
}
|
|
13475
|
-
if (key === "[" || key === "{") {
|
|
13476
|
-
if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
|
|
13477
|
-
else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
|
|
13478
|
-
return true;
|
|
13479
|
-
}
|
|
13480
|
-
if (key === "]" || key === "}") {
|
|
13481
|
-
if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
|
|
13482
|
-
else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
|
|
13483
|
-
return true;
|
|
13484
|
-
}
|
|
13485
|
-
if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
|
|
13486
|
-
set({ opacity: key === "0" ? 1 : Number(key) / 10 });
|
|
13487
|
-
return true;
|
|
13488
|
-
}
|
|
13489
|
-
return false;
|
|
13427
|
+
// src/image-editor/selection/gesture.ts
|
|
13428
|
+
function snapTo45(from, point) {
|
|
13429
|
+
const dx = point.x - from.x;
|
|
13430
|
+
const dy = point.y - from.y;
|
|
13431
|
+
const len = Math.hypot(dx, dy);
|
|
13432
|
+
if (len === 0) return point;
|
|
13433
|
+
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
13434
|
+
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
13490
13435
|
}
|
|
13491
|
-
var
|
|
13492
|
-
constructor(controller) {
|
|
13436
|
+
var SelectionGesture = class {
|
|
13437
|
+
constructor(controller, getOptions) {
|
|
13493
13438
|
this.controller = controller;
|
|
13439
|
+
this.getOptions = getOptions;
|
|
13494
13440
|
}
|
|
13495
13441
|
controller;
|
|
13496
|
-
|
|
13497
|
-
|
|
13498
|
-
|
|
13442
|
+
getOptions;
|
|
13443
|
+
start = null;
|
|
13444
|
+
points = [];
|
|
13445
|
+
current = null;
|
|
13446
|
+
modifiers = {};
|
|
13447
|
+
/** Polygonal lasso corners while one is being drawn. */
|
|
13448
|
+
polygon = null;
|
|
13449
|
+
pressedAt = null;
|
|
13450
|
+
freehand = false;
|
|
13451
|
+
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
13452
|
+
closeRadius = 8;
|
|
13453
|
+
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
13499
13454
|
error = null;
|
|
13500
13455
|
get active() {
|
|
13501
|
-
return
|
|
13456
|
+
return this.start !== null || this.polygon !== null;
|
|
13457
|
+
}
|
|
13458
|
+
/** The outline of the shape being drawn, or `null`. */
|
|
13459
|
+
get draftPath() {
|
|
13460
|
+
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
13461
|
+
if (!this.start || !this.current) return null;
|
|
13462
|
+
const { tool } = this.getOptions();
|
|
13463
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
13464
|
+
const rect = normalizeRect({
|
|
13465
|
+
x: this.start.x,
|
|
13466
|
+
y: this.start.y,
|
|
13467
|
+
width: this.current.x - this.start.x,
|
|
13468
|
+
height: this.current.y - this.start.y
|
|
13469
|
+
});
|
|
13470
|
+
if (rect.width < 1 && rect.height < 1) return null;
|
|
13471
|
+
if (tool === "ellipse") return ellipsePoints(rect);
|
|
13472
|
+
return [
|
|
13473
|
+
{ x: rect.x, y: rect.y },
|
|
13474
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
13475
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
13476
|
+
{ x: rect.x, y: rect.y + rect.height }
|
|
13477
|
+
];
|
|
13478
|
+
}
|
|
13479
|
+
/** Pointer down. Resolves once a wand click has been applied. */
|
|
13480
|
+
async begin(point, modifiers = {}) {
|
|
13481
|
+
const options = this.getOptions();
|
|
13482
|
+
this.error = null;
|
|
13483
|
+
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
13484
|
+
if (options.tool === "polygon") {
|
|
13485
|
+
this.pressedAt = point;
|
|
13486
|
+
this.freehand = false;
|
|
13487
|
+
if (!this.polygon) {
|
|
13488
|
+
this.modifiers = modifiers;
|
|
13489
|
+
this.polygon = [point];
|
|
13490
|
+
this.current = point;
|
|
13491
|
+
return;
|
|
13492
|
+
}
|
|
13493
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
13494
|
+
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
13495
|
+
const first = this.polygon[0];
|
|
13496
|
+
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
13497
|
+
this.close();
|
|
13498
|
+
return;
|
|
13499
|
+
}
|
|
13500
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
13501
|
+
this.current = p;
|
|
13502
|
+
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
13503
|
+
return;
|
|
13504
|
+
}
|
|
13505
|
+
this.modifiers = modifiers;
|
|
13506
|
+
if (options.tool === "row" || options.tool === "column") {
|
|
13507
|
+
this.start = null;
|
|
13508
|
+
try {
|
|
13509
|
+
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
13510
|
+
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
13511
|
+
feather: options.feather ?? 0
|
|
13512
|
+
});
|
|
13513
|
+
} catch (err) {
|
|
13514
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
13515
|
+
}
|
|
13516
|
+
return;
|
|
13517
|
+
}
|
|
13518
|
+
if (options.tool === "wand" || options.tool === "range") {
|
|
13519
|
+
this.start = null;
|
|
13520
|
+
const common = {
|
|
13521
|
+
sampleMerged: options.sampleMerged,
|
|
13522
|
+
sample: options.sample,
|
|
13523
|
+
feather: options.feather ?? 0,
|
|
13524
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
13525
|
+
};
|
|
13526
|
+
try {
|
|
13527
|
+
if (options.tool === "range") {
|
|
13528
|
+
const pixels = await this.controller.getSamplePixels(common);
|
|
13529
|
+
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
13530
|
+
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
13531
|
+
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
13532
|
+
const p = (y * pixels.width + x) * 4;
|
|
13533
|
+
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
13534
|
+
await this.controller.selectColorRange([{ color, at: point }], {
|
|
13535
|
+
...common,
|
|
13536
|
+
fuzziness: options.fuzziness ?? 40,
|
|
13537
|
+
localized: options.localized ? options.localized : void 0,
|
|
13538
|
+
invert: options.rangeInvert ?? false
|
|
13539
|
+
});
|
|
13540
|
+
return;
|
|
13541
|
+
}
|
|
13542
|
+
await this.controller.magicWand(point, {
|
|
13543
|
+
...common,
|
|
13544
|
+
tolerance: options.tolerance,
|
|
13545
|
+
contiguous: options.contiguous,
|
|
13546
|
+
antiAlias: options.antiAlias,
|
|
13547
|
+
distance: options.distance
|
|
13548
|
+
});
|
|
13549
|
+
} catch (err) {
|
|
13550
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
13551
|
+
}
|
|
13552
|
+
return;
|
|
13553
|
+
}
|
|
13554
|
+
this.start = point;
|
|
13555
|
+
this.current = point;
|
|
13556
|
+
this.points = [point];
|
|
13557
|
+
}
|
|
13558
|
+
/** Pointer move; returns true when the draft changed (re-render). */
|
|
13559
|
+
move(point, modifiers = {}) {
|
|
13560
|
+
if (this.polygon) {
|
|
13561
|
+
const last = this.polygon[this.polygon.length - 1];
|
|
13562
|
+
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
13563
|
+
this.freehand = true;
|
|
13564
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
13565
|
+
this.current = null;
|
|
13566
|
+
return true;
|
|
13567
|
+
}
|
|
13568
|
+
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
13569
|
+
return true;
|
|
13570
|
+
}
|
|
13571
|
+
if (!this.start) return false;
|
|
13572
|
+
this.current = point;
|
|
13573
|
+
if (this.getOptions().tool === "lasso") {
|
|
13574
|
+
const last = this.points[this.points.length - 1];
|
|
13575
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
13576
|
+
this.points.push(point);
|
|
13577
|
+
}
|
|
13578
|
+
return true;
|
|
13579
|
+
}
|
|
13580
|
+
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
13581
|
+
end(point, modifiers) {
|
|
13582
|
+
if (this.polygon) {
|
|
13583
|
+
if (point && this.freehand) this.move(point);
|
|
13584
|
+
this.pressedAt = null;
|
|
13585
|
+
this.freehand = false;
|
|
13586
|
+
return false;
|
|
13587
|
+
}
|
|
13588
|
+
if (!this.start) return false;
|
|
13589
|
+
if (point) this.move(point);
|
|
13590
|
+
const options = this.getOptions();
|
|
13591
|
+
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
13592
|
+
const path = this.draftPath;
|
|
13593
|
+
const rect = normalizeRect({
|
|
13594
|
+
x: this.start.x,
|
|
13595
|
+
y: this.start.y,
|
|
13596
|
+
width: this.current.x - this.start.x,
|
|
13597
|
+
height: this.current.y - this.start.y
|
|
13598
|
+
});
|
|
13599
|
+
this.start = null;
|
|
13600
|
+
this.current = null;
|
|
13601
|
+
this.points = [];
|
|
13602
|
+
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
13603
|
+
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
13604
|
+
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
13605
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
13606
|
+
}
|
|
13607
|
+
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
13608
|
+
close() {
|
|
13609
|
+
const polygon = this.polygon;
|
|
13610
|
+
if (!polygon) return false;
|
|
13611
|
+
const options = this.getOptions();
|
|
13612
|
+
this.cancel();
|
|
13613
|
+
if (polygon.length < 3) return false;
|
|
13614
|
+
return this.controller.selectPolygon(polygon, {
|
|
13615
|
+
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
13616
|
+
antiAlias: options.antiAlias,
|
|
13617
|
+
feather: options.feather ?? 0
|
|
13618
|
+
});
|
|
13619
|
+
}
|
|
13620
|
+
/**
|
|
13621
|
+
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
13622
|
+
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
13623
|
+
*/
|
|
13624
|
+
key(key) {
|
|
13625
|
+
if (!this.polygon) return false;
|
|
13626
|
+
if (key === "Enter") this.close();
|
|
13627
|
+
else if (key === "Escape") this.cancel();
|
|
13628
|
+
else if (key === "Backspace" || key === "Delete") {
|
|
13629
|
+
this.polygon.pop();
|
|
13630
|
+
if (!this.polygon.length) this.cancel();
|
|
13631
|
+
} else return false;
|
|
13632
|
+
return true;
|
|
13633
|
+
}
|
|
13634
|
+
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
13635
|
+
cancel() {
|
|
13636
|
+
this.start = null;
|
|
13637
|
+
this.current = null;
|
|
13638
|
+
this.points = [];
|
|
13639
|
+
this.polygon = null;
|
|
13640
|
+
this.pressedAt = null;
|
|
13641
|
+
this.freehand = false;
|
|
13642
|
+
}
|
|
13643
|
+
};
|
|
13644
|
+
function handleSelectionShortcut(controller, e, onError) {
|
|
13645
|
+
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
13646
|
+
const key = e.key.toLowerCase();
|
|
13647
|
+
const run = (fn) => {
|
|
13648
|
+
try {
|
|
13649
|
+
fn();
|
|
13650
|
+
} catch (err) {
|
|
13651
|
+
onError?.(err instanceof Error ? err.message : String(err));
|
|
13652
|
+
}
|
|
13653
|
+
return true;
|
|
13654
|
+
};
|
|
13655
|
+
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
13656
|
+
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
13657
|
+
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
13658
|
+
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
13659
|
+
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
13660
|
+
return false;
|
|
13661
|
+
}
|
|
13662
|
+
function handleMaskViewKey(controller, e) {
|
|
13663
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key !== "\\") return false;
|
|
13664
|
+
const id = controller.getState().activeLayerId;
|
|
13665
|
+
const layer = id ? controller.getLayer(id) : null;
|
|
13666
|
+
if (!layer?.mask || layer.mask.source) return false;
|
|
13667
|
+
const view = controller.getMaskView();
|
|
13668
|
+
controller.setMaskView(view?.layerId === layer.id && view.mode === "overlay" ? "off" : "overlay", layer.id);
|
|
13669
|
+
return true;
|
|
13670
|
+
}
|
|
13671
|
+
function handleQuickMaskKey(controller, e) {
|
|
13672
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
13673
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13674
|
+
return true;
|
|
13675
|
+
}
|
|
13676
|
+
|
|
13677
|
+
// src/image-editor/layers/paint-ui.ts
|
|
13678
|
+
var plain2 = { kind: "plain" };
|
|
13679
|
+
var BRUSH_OPTION_CONTROLS = [
|
|
13680
|
+
{ id: "size", label: "Size", section: "layer", min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain2, unit: "px" },
|
|
13681
|
+
{ id: "hardness", label: "Hardness", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13682
|
+
{ id: "opacity", label: "Opacity", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13683
|
+
{ id: "flow", label: "Flow", section: "layer", min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13684
|
+
{ id: "smoothing", label: "Smoothing", section: "layer", min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13685
|
+
];
|
|
13686
|
+
var MIXER_OPTION_CONTROLS = [
|
|
13687
|
+
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13688
|
+
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13689
|
+
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13690
|
+
];
|
|
13691
|
+
var MIXER_MIXING_OPTIONS = [
|
|
13692
|
+
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
13693
|
+
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
13694
|
+
];
|
|
13695
|
+
var PAINT_TOOLS = [
|
|
13696
|
+
{ id: "move", label: "Move", shortcut: "V" },
|
|
13697
|
+
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
13698
|
+
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
13699
|
+
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
13700
|
+
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
13701
|
+
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
13702
|
+
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
13703
|
+
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
13704
|
+
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
13705
|
+
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
13706
|
+
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
13707
|
+
{ id: "blur", label: "Blur", shortcut: "" },
|
|
13708
|
+
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
13709
|
+
{ id: "history", label: "History", shortcut: "Y" }
|
|
13710
|
+
];
|
|
13711
|
+
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
13712
|
+
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
13713
|
+
var RETOUCH_OPTION_CONTROLS = [
|
|
13714
|
+
{ id: "exposure", label: "Exposure", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["dodge", "burn"] },
|
|
13715
|
+
{ id: "strength", label: "Strength", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%", tools: ["smudge", "blur", "sharpen"] },
|
|
13716
|
+
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain2, unit: "", tools: ["history"] }
|
|
13717
|
+
];
|
|
13718
|
+
var CLONE_TRANSFORM_CONTROLS = [
|
|
13719
|
+
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13720
|
+
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13721
|
+
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain2, unit: "\xB0" }
|
|
13722
|
+
];
|
|
13723
|
+
function cloneTransformView(controller) {
|
|
13724
|
+
const t = controller.getState().paint.cloneTransform;
|
|
13725
|
+
return {
|
|
13726
|
+
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
13727
|
+
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13728
|
+
angle: t.rotate,
|
|
13729
|
+
flipH: t.scaleX < 0,
|
|
13730
|
+
flipV: t.scaleY < 0,
|
|
13731
|
+
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13732
|
+
};
|
|
13733
|
+
}
|
|
13734
|
+
function setCloneTransformValue(controller, id, value) {
|
|
13735
|
+
const t = controller.getState().paint.cloneTransform;
|
|
13736
|
+
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13737
|
+
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13738
|
+
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13739
|
+
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13740
|
+
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13741
|
+
}
|
|
13742
|
+
var RETOUCH_CHOICES = [
|
|
13743
|
+
{
|
|
13744
|
+
id: "sample",
|
|
13745
|
+
label: "Sample",
|
|
13746
|
+
tools: ["clone", "heal", "spot-heal"],
|
|
13747
|
+
options: [
|
|
13748
|
+
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13749
|
+
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13750
|
+
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13751
|
+
]
|
|
13752
|
+
},
|
|
13753
|
+
{
|
|
13754
|
+
id: "spotType",
|
|
13755
|
+
label: "Type",
|
|
13756
|
+
tools: ["spot-heal"],
|
|
13757
|
+
options: [
|
|
13758
|
+
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13759
|
+
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13760
|
+
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13761
|
+
]
|
|
13762
|
+
},
|
|
13763
|
+
{
|
|
13764
|
+
id: "range",
|
|
13765
|
+
label: "Range",
|
|
13766
|
+
tools: ["dodge", "burn"],
|
|
13767
|
+
options: [
|
|
13768
|
+
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13769
|
+
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13770
|
+
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13771
|
+
]
|
|
13772
|
+
},
|
|
13773
|
+
{
|
|
13774
|
+
id: "spongeMode",
|
|
13775
|
+
label: "Mode",
|
|
13776
|
+
tools: ["sponge"],
|
|
13777
|
+
options: [
|
|
13778
|
+
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13779
|
+
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13780
|
+
]
|
|
13781
|
+
}
|
|
13782
|
+
];
|
|
13783
|
+
var RETOUCH_TOGGLES = [
|
|
13784
|
+
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
13785
|
+
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
13786
|
+
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
13787
|
+
];
|
|
13788
|
+
function setRetouchChoice(controller, id, value) {
|
|
13789
|
+
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
13790
|
+
}
|
|
13791
|
+
function cloneSourceLabel(controller) {
|
|
13792
|
+
const cs = controller.getState().paint.cloneSource;
|
|
13793
|
+
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
13794
|
+
}
|
|
13795
|
+
function rgbUnitToHex(c) {
|
|
13796
|
+
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
13797
|
+
}
|
|
13798
|
+
function sizeStep(size) {
|
|
13799
|
+
return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
|
|
13800
|
+
}
|
|
13801
|
+
var isTyping = (t) => {
|
|
13802
|
+
const el = t;
|
|
13803
|
+
return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
|
|
13804
|
+
};
|
|
13805
|
+
function handlePaintShortcut(controller, e) {
|
|
13806
|
+
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
13807
|
+
const brush = controller.getState().paint.brush;
|
|
13808
|
+
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
13809
|
+
const key = e.key;
|
|
13810
|
+
if (handleMaskViewKey(controller, e)) return true;
|
|
13811
|
+
switch (key.toLowerCase()) {
|
|
13812
|
+
case "q":
|
|
13813
|
+
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13814
|
+
return true;
|
|
13815
|
+
case "b":
|
|
13816
|
+
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
13817
|
+
return true;
|
|
13818
|
+
case "e":
|
|
13819
|
+
controller.setTool("eraser");
|
|
13820
|
+
return true;
|
|
13821
|
+
case "v":
|
|
13822
|
+
controller.setTool("move");
|
|
13823
|
+
return true;
|
|
13824
|
+
case "s":
|
|
13825
|
+
controller.setTool("clone");
|
|
13826
|
+
return true;
|
|
13827
|
+
case "j":
|
|
13828
|
+
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
13829
|
+
return true;
|
|
13830
|
+
case "o": {
|
|
13831
|
+
const cur = controller.getState().activeTool;
|
|
13832
|
+
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
13833
|
+
controller.setTool(next);
|
|
13834
|
+
return true;
|
|
13835
|
+
}
|
|
13836
|
+
case "y":
|
|
13837
|
+
controller.setTool("history");
|
|
13838
|
+
return true;
|
|
13839
|
+
case "d":
|
|
13840
|
+
controller.resetPaintColors();
|
|
13841
|
+
return true;
|
|
13842
|
+
case "x":
|
|
13843
|
+
controller.swapPaintColors();
|
|
13844
|
+
return true;
|
|
13845
|
+
}
|
|
13846
|
+
if (key === "[" || key === "{") {
|
|
13847
|
+
if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
|
|
13848
|
+
else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
|
|
13849
|
+
return true;
|
|
13850
|
+
}
|
|
13851
|
+
if (key === "]" || key === "}") {
|
|
13852
|
+
if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
|
|
13853
|
+
else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
|
|
13854
|
+
return true;
|
|
13855
|
+
}
|
|
13856
|
+
if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
|
|
13857
|
+
set({ opacity: key === "0" ? 1 : Number(key) / 10 });
|
|
13858
|
+
return true;
|
|
13859
|
+
}
|
|
13860
|
+
return false;
|
|
13861
|
+
}
|
|
13862
|
+
var PaintGesture = class {
|
|
13863
|
+
constructor(controller) {
|
|
13864
|
+
this.controller = controller;
|
|
13865
|
+
}
|
|
13866
|
+
controller;
|
|
13867
|
+
stroke = null;
|
|
13868
|
+
move = null;
|
|
13869
|
+
/** The last refusal (e.g. "layer is locked"), for the editor to show. */
|
|
13870
|
+
error = null;
|
|
13871
|
+
get active() {
|
|
13872
|
+
return Boolean(this.stroke || this.move);
|
|
13502
13873
|
}
|
|
13503
13874
|
down(sample) {
|
|
13504
13875
|
this.error = null;
|
|
@@ -15837,247 +16208,6 @@ function removeCurvePoint(points, index) {
|
|
|
15837
16208
|
}
|
|
15838
16209
|
var round22 = (v) => Math.round(v * 100) / 100;
|
|
15839
16210
|
|
|
15840
|
-
// src/image-editor/selection/gesture.ts
|
|
15841
|
-
function snapTo45(from, point) {
|
|
15842
|
-
const dx = point.x - from.x;
|
|
15843
|
-
const dy = point.y - from.y;
|
|
15844
|
-
const len = Math.hypot(dx, dy);
|
|
15845
|
-
if (len === 0) return point;
|
|
15846
|
-
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15847
|
-
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15848
|
-
}
|
|
15849
|
-
var SelectionGesture = class {
|
|
15850
|
-
constructor(controller, getOptions) {
|
|
15851
|
-
this.controller = controller;
|
|
15852
|
-
this.getOptions = getOptions;
|
|
15853
|
-
}
|
|
15854
|
-
controller;
|
|
15855
|
-
getOptions;
|
|
15856
|
-
start = null;
|
|
15857
|
-
points = [];
|
|
15858
|
-
current = null;
|
|
15859
|
-
modifiers = {};
|
|
15860
|
-
/** Polygonal lasso corners while one is being drawn. */
|
|
15861
|
-
polygon = null;
|
|
15862
|
-
pressedAt = null;
|
|
15863
|
-
freehand = false;
|
|
15864
|
-
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
15865
|
-
closeRadius = 8;
|
|
15866
|
-
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
15867
|
-
error = null;
|
|
15868
|
-
get active() {
|
|
15869
|
-
return this.start !== null || this.polygon !== null;
|
|
15870
|
-
}
|
|
15871
|
-
/** The outline of the shape being drawn, or `null`. */
|
|
15872
|
-
get draftPath() {
|
|
15873
|
-
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
15874
|
-
if (!this.start || !this.current) return null;
|
|
15875
|
-
const { tool } = this.getOptions();
|
|
15876
|
-
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
15877
|
-
const rect = normalizeRect({
|
|
15878
|
-
x: this.start.x,
|
|
15879
|
-
y: this.start.y,
|
|
15880
|
-
width: this.current.x - this.start.x,
|
|
15881
|
-
height: this.current.y - this.start.y
|
|
15882
|
-
});
|
|
15883
|
-
if (rect.width < 1 && rect.height < 1) return null;
|
|
15884
|
-
if (tool === "ellipse") return ellipsePoints(rect);
|
|
15885
|
-
return [
|
|
15886
|
-
{ x: rect.x, y: rect.y },
|
|
15887
|
-
{ x: rect.x + rect.width, y: rect.y },
|
|
15888
|
-
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
15889
|
-
{ x: rect.x, y: rect.y + rect.height }
|
|
15890
|
-
];
|
|
15891
|
-
}
|
|
15892
|
-
/** Pointer down. Resolves once a wand click has been applied. */
|
|
15893
|
-
async begin(point, modifiers = {}) {
|
|
15894
|
-
const options = this.getOptions();
|
|
15895
|
-
this.error = null;
|
|
15896
|
-
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
15897
|
-
if (options.tool === "polygon") {
|
|
15898
|
-
this.pressedAt = point;
|
|
15899
|
-
this.freehand = false;
|
|
15900
|
-
if (!this.polygon) {
|
|
15901
|
-
this.modifiers = modifiers;
|
|
15902
|
-
this.polygon = [point];
|
|
15903
|
-
this.current = point;
|
|
15904
|
-
return;
|
|
15905
|
-
}
|
|
15906
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15907
|
-
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15908
|
-
const first = this.polygon[0];
|
|
15909
|
-
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
15910
|
-
this.close();
|
|
15911
|
-
return;
|
|
15912
|
-
}
|
|
15913
|
-
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
15914
|
-
this.current = p;
|
|
15915
|
-
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
15916
|
-
return;
|
|
15917
|
-
}
|
|
15918
|
-
this.modifiers = modifiers;
|
|
15919
|
-
if (options.tool === "row" || options.tool === "column") {
|
|
15920
|
-
this.start = null;
|
|
15921
|
-
try {
|
|
15922
|
-
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
15923
|
-
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
15924
|
-
feather: options.feather ?? 0
|
|
15925
|
-
});
|
|
15926
|
-
} catch (err) {
|
|
15927
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15928
|
-
}
|
|
15929
|
-
return;
|
|
15930
|
-
}
|
|
15931
|
-
if (options.tool === "wand" || options.tool === "range") {
|
|
15932
|
-
this.start = null;
|
|
15933
|
-
const common = {
|
|
15934
|
-
sampleMerged: options.sampleMerged,
|
|
15935
|
-
sample: options.sample,
|
|
15936
|
-
feather: options.feather ?? 0,
|
|
15937
|
-
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15938
|
-
};
|
|
15939
|
-
try {
|
|
15940
|
-
if (options.tool === "range") {
|
|
15941
|
-
const pixels = await this.controller.getSamplePixels(common);
|
|
15942
|
-
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
15943
|
-
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
15944
|
-
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
15945
|
-
const p = (y * pixels.width + x) * 4;
|
|
15946
|
-
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
15947
|
-
await this.controller.selectColorRange([{ color, at: point }], {
|
|
15948
|
-
...common,
|
|
15949
|
-
fuzziness: options.fuzziness ?? 40,
|
|
15950
|
-
localized: options.localized ? options.localized : void 0,
|
|
15951
|
-
invert: options.rangeInvert ?? false
|
|
15952
|
-
});
|
|
15953
|
-
return;
|
|
15954
|
-
}
|
|
15955
|
-
await this.controller.magicWand(point, {
|
|
15956
|
-
...common,
|
|
15957
|
-
tolerance: options.tolerance,
|
|
15958
|
-
contiguous: options.contiguous,
|
|
15959
|
-
antiAlias: options.antiAlias,
|
|
15960
|
-
distance: options.distance
|
|
15961
|
-
});
|
|
15962
|
-
} catch (err) {
|
|
15963
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15964
|
-
}
|
|
15965
|
-
return;
|
|
15966
|
-
}
|
|
15967
|
-
this.start = point;
|
|
15968
|
-
this.current = point;
|
|
15969
|
-
this.points = [point];
|
|
15970
|
-
}
|
|
15971
|
-
/** Pointer move; returns true when the draft changed (re-render). */
|
|
15972
|
-
move(point, modifiers = {}) {
|
|
15973
|
-
if (this.polygon) {
|
|
15974
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15975
|
-
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
15976
|
-
this.freehand = true;
|
|
15977
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
15978
|
-
this.current = null;
|
|
15979
|
-
return true;
|
|
15980
|
-
}
|
|
15981
|
-
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15982
|
-
return true;
|
|
15983
|
-
}
|
|
15984
|
-
if (!this.start) return false;
|
|
15985
|
-
this.current = point;
|
|
15986
|
-
if (this.getOptions().tool === "lasso") {
|
|
15987
|
-
const last = this.points[this.points.length - 1];
|
|
15988
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
15989
|
-
this.points.push(point);
|
|
15990
|
-
}
|
|
15991
|
-
return true;
|
|
15992
|
-
}
|
|
15993
|
-
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
15994
|
-
end(point, modifiers) {
|
|
15995
|
-
if (this.polygon) {
|
|
15996
|
-
if (point && this.freehand) this.move(point);
|
|
15997
|
-
this.pressedAt = null;
|
|
15998
|
-
this.freehand = false;
|
|
15999
|
-
return false;
|
|
16000
|
-
}
|
|
16001
|
-
if (!this.start) return false;
|
|
16002
|
-
if (point) this.move(point);
|
|
16003
|
-
const options = this.getOptions();
|
|
16004
|
-
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
16005
|
-
const path = this.draftPath;
|
|
16006
|
-
const rect = normalizeRect({
|
|
16007
|
-
x: this.start.x,
|
|
16008
|
-
y: this.start.y,
|
|
16009
|
-
width: this.current.x - this.start.x,
|
|
16010
|
-
height: this.current.y - this.start.y
|
|
16011
|
-
});
|
|
16012
|
-
this.start = null;
|
|
16013
|
-
this.current = null;
|
|
16014
|
-
this.points = [];
|
|
16015
|
-
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
16016
|
-
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
16017
|
-
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
16018
|
-
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
16019
|
-
}
|
|
16020
|
-
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
16021
|
-
close() {
|
|
16022
|
-
const polygon = this.polygon;
|
|
16023
|
-
if (!polygon) return false;
|
|
16024
|
-
const options = this.getOptions();
|
|
16025
|
-
this.cancel();
|
|
16026
|
-
if (polygon.length < 3) return false;
|
|
16027
|
-
return this.controller.selectPolygon(polygon, {
|
|
16028
|
-
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
16029
|
-
antiAlias: options.antiAlias,
|
|
16030
|
-
feather: options.feather ?? 0
|
|
16031
|
-
});
|
|
16032
|
-
}
|
|
16033
|
-
/**
|
|
16034
|
-
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
16035
|
-
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
16036
|
-
*/
|
|
16037
|
-
key(key) {
|
|
16038
|
-
if (!this.polygon) return false;
|
|
16039
|
-
if (key === "Enter") this.close();
|
|
16040
|
-
else if (key === "Escape") this.cancel();
|
|
16041
|
-
else if (key === "Backspace" || key === "Delete") {
|
|
16042
|
-
this.polygon.pop();
|
|
16043
|
-
if (!this.polygon.length) this.cancel();
|
|
16044
|
-
} else return false;
|
|
16045
|
-
return true;
|
|
16046
|
-
}
|
|
16047
|
-
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
16048
|
-
cancel() {
|
|
16049
|
-
this.start = null;
|
|
16050
|
-
this.current = null;
|
|
16051
|
-
this.points = [];
|
|
16052
|
-
this.polygon = null;
|
|
16053
|
-
this.pressedAt = null;
|
|
16054
|
-
this.freehand = false;
|
|
16055
|
-
}
|
|
16056
|
-
};
|
|
16057
|
-
function handleSelectionShortcut(controller, e, onError) {
|
|
16058
|
-
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
16059
|
-
const key = e.key.toLowerCase();
|
|
16060
|
-
const run = (fn) => {
|
|
16061
|
-
try {
|
|
16062
|
-
fn();
|
|
16063
|
-
} catch (err) {
|
|
16064
|
-
onError?.(err instanceof Error ? err.message : String(err));
|
|
16065
|
-
}
|
|
16066
|
-
return true;
|
|
16067
|
-
};
|
|
16068
|
-
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
16069
|
-
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
16070
|
-
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
16071
|
-
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
16072
|
-
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
16073
|
-
return false;
|
|
16074
|
-
}
|
|
16075
|
-
function handleQuickMaskKey(controller, e) {
|
|
16076
|
-
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
16077
|
-
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
16078
|
-
return true;
|
|
16079
|
-
}
|
|
16080
|
-
|
|
16081
16211
|
// src/image-editor/selection/mask-files.ts
|
|
16082
16212
|
var MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
16083
16213
|
function blobDataUrl(file) {
|
|
@@ -16253,6 +16383,10 @@ function maskThumbnailClick(controller, layer, e) {
|
|
|
16253
16383
|
if (!layer.mask) return;
|
|
16254
16384
|
if (e.ctrlKey || e.metaKey) {
|
|
16255
16385
|
controller.selectionFromLayerMask(layer.id, selectionModeFromModifiers(e, "replace"));
|
|
16386
|
+
} else if (e.altKey && !layer.mask.source) {
|
|
16387
|
+
const mode = e.shiftKey ? "overlay" : "gray";
|
|
16388
|
+
const view = controller.getMaskView();
|
|
16389
|
+
controller.setMaskView(view?.layerId === layer.id && view.mode === mode ? "off" : mode, layer.id);
|
|
16256
16390
|
} else if (e.shiftKey) {
|
|
16257
16391
|
controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
|
|
16258
16392
|
} else {
|
|
@@ -18195,6 +18329,7 @@ export {
|
|
|
18195
18329
|
growSelection,
|
|
18196
18330
|
guidePlanes,
|
|
18197
18331
|
guidedFilterColor,
|
|
18332
|
+
handleMaskViewKey,
|
|
18198
18333
|
handlePaintShortcut,
|
|
18199
18334
|
handleQuickMaskKey,
|
|
18200
18335
|
handleSelectionShortcut,
|
|
@@ -18285,6 +18420,7 @@ export {
|
|
|
18285
18420
|
layerMaskCoverage,
|
|
18286
18421
|
layerMaskFromSelection,
|
|
18287
18422
|
layerMaskKey,
|
|
18423
|
+
layerMaskSelection,
|
|
18288
18424
|
layerMenu,
|
|
18289
18425
|
layerName,
|
|
18290
18426
|
layerPanelRows,
|