@pixerate/editor 0.187.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-X2KQQZBT.cjs → chunk-ED4L4UD6.cjs} +389 -180
- package/dist/{chunk-FW7N64XK.js → chunk-ZEHTUR7Z.js} +229 -20
- package/dist/image-editor/controller.d.cts +20 -3
- package/dist/image-editor/controller.d.ts +20 -3
- package/dist/image-editor/index.cjs +1618 -1467
- package/dist/image-editor/index.js +623 -472
- 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/selection/mask.d.cts +20 -1
- package/dist/image-editor/selection/mask.d.ts +20 -1
- package/dist/image-editor/selection/shapes.d.cts +8 -0
- package/dist/image-editor/selection/shapes.d.ts +8 -0
- package/dist/image-editor/selection/tool-options.d.cts +1 -1
- package/dist/image-editor/selection/tool-options.d.ts +1 -1
- 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,
|
|
@@ -298,6 +299,8 @@ import {
|
|
|
298
299
|
selectionArea,
|
|
299
300
|
selectionFromCoverage,
|
|
300
301
|
selectionFromPixels,
|
|
302
|
+
selectionFromRegion,
|
|
303
|
+
selectionFromTiles,
|
|
301
304
|
selectionToCoverage,
|
|
302
305
|
selectionToPixels,
|
|
303
306
|
selectionValueAt,
|
|
@@ -370,7 +373,7 @@ import {
|
|
|
370
373
|
writePsd,
|
|
371
374
|
xyToTempTint,
|
|
372
375
|
xyToUv
|
|
373
|
-
} from "../chunk-
|
|
376
|
+
} from "../chunk-ZEHTUR7Z.js";
|
|
374
377
|
import {
|
|
375
378
|
parseCssColor
|
|
376
379
|
} from "../chunk-3CI7FRMK.js";
|
|
@@ -1363,6 +1366,24 @@ var ImageEditorRenderer = class {
|
|
|
1363
1366
|
}
|
|
1364
1367
|
ctx.restore();
|
|
1365
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
|
+
}
|
|
1366
1387
|
const display = options.selectionDisplay ?? state.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
1367
1388
|
const shownChannels = (state.channels ?? []).filter((c) => c.visible);
|
|
1368
1389
|
if (shownChannels.length) {
|
|
@@ -1802,6 +1823,32 @@ var ImageEditorRenderer = class {
|
|
|
1802
1823
|
return this.emptyMaskCache;
|
|
1803
1824
|
}
|
|
1804
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
|
+
}
|
|
1805
1852
|
/** A channel's tint (colour, opacity, masked or selected area), drawn in natural image space. */
|
|
1806
1853
|
drawMaskOverlay(ctx, mask, style) {
|
|
1807
1854
|
const key = `${style.color?.join(",")}|${style.opacity}|${style.indicates}`;
|
|
@@ -2835,8 +2882,33 @@ var TypeToolGesture = class {
|
|
|
2835
2882
|
|
|
2836
2883
|
// src/image-editor/selection/shapes.ts
|
|
2837
2884
|
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
2885
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
|
|
2886
|
+
if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
|
|
2838
2887
|
const m = new Float32Array(width * height);
|
|
2839
|
-
|
|
2888
|
+
for (let y = 0; y < box.height; y++) m.set(coverage.subarray(y * box.width, (y + 1) * box.width), (box.y + y) * width + box.x);
|
|
2889
|
+
return m;
|
|
2890
|
+
}
|
|
2891
|
+
function polygonCoverageBox(points, width, height, antiAlias = true) {
|
|
2892
|
+
const none = { box: null, coverage: new Float32Array(0) };
|
|
2893
|
+
if (points.length < 3 || width <= 0 || height <= 0) return none;
|
|
2894
|
+
let minX = Infinity;
|
|
2895
|
+
let minY = Infinity;
|
|
2896
|
+
let maxX = -Infinity;
|
|
2897
|
+
let maxY = -Infinity;
|
|
2898
|
+
for (const p of points) {
|
|
2899
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
|
2900
|
+
minX = Math.min(minX, p.x);
|
|
2901
|
+
minY = Math.min(minY, p.y);
|
|
2902
|
+
maxX = Math.max(maxX, p.x);
|
|
2903
|
+
maxY = Math.max(maxY, p.y);
|
|
2904
|
+
}
|
|
2905
|
+
const bx0 = Math.max(0, Math.floor(minX));
|
|
2906
|
+
const by0 = Math.max(0, Math.floor(minY));
|
|
2907
|
+
const bx1 = Math.min(width, Math.ceil(maxX));
|
|
2908
|
+
const by1 = Math.min(height, Math.ceil(maxY));
|
|
2909
|
+
if (!(bx1 > bx0 && by1 > by0)) return none;
|
|
2910
|
+
const bw = bx1 - bx0;
|
|
2911
|
+
const m = new Float32Array(bw * (by1 - by0));
|
|
2840
2912
|
const sub2 = antiAlias ? 4 : 1;
|
|
2841
2913
|
const edges = [];
|
|
2842
2914
|
for (let i = 0; i < points.length; i++) {
|
|
@@ -2850,8 +2922,8 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
2850
2922
|
let next = 0;
|
|
2851
2923
|
let active = [];
|
|
2852
2924
|
const xs = [];
|
|
2853
|
-
for (let py =
|
|
2854
|
-
const row = py *
|
|
2925
|
+
for (let py = by0; py < by1; py++) {
|
|
2926
|
+
const row = (py - by0) * bw - bx0;
|
|
2855
2927
|
for (let s = 0; s < sub2; s++) {
|
|
2856
2928
|
const y = py + (s + 0.5) / sub2;
|
|
2857
2929
|
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
@@ -2881,15 +2953,14 @@ function polygonCoverage(points, width, height, antiAlias = true) {
|
|
|
2881
2953
|
}
|
|
2882
2954
|
}
|
|
2883
2955
|
for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
|
|
2884
|
-
return m;
|
|
2956
|
+
return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
|
|
2885
2957
|
}
|
|
2886
|
-
function
|
|
2958
|
+
function polygonSelection(width, height, points, options = {}) {
|
|
2959
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
|
|
2960
|
+
if (!box) return createSelectionMask(width, height);
|
|
2887
2961
|
const out = new Uint8Array(coverage.length);
|
|
2888
2962
|
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
2889
|
-
return
|
|
2890
|
-
}
|
|
2891
|
-
function polygonSelection(width, height, points, options = {}) {
|
|
2892
|
-
return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));
|
|
2963
|
+
return selectionFromRegion(width, height, box, out);
|
|
2893
2964
|
}
|
|
2894
2965
|
function normalizeRect(rect) {
|
|
2895
2966
|
const x = rect.width < 0 ? rect.x + rect.width : rect.x;
|
|
@@ -3531,9 +3602,7 @@ function selectionRing(sel, width, location) {
|
|
|
3531
3602
|
var DEFAULT_CHANNEL_COLOR = "#ff0000";
|
|
3532
3603
|
function describeChannel(channel) {
|
|
3533
3604
|
const { mask, ...rest } = channel;
|
|
3534
|
-
|
|
3535
|
-
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
3536
|
-
const percent = Math.round(sum / 255 / Math.max(1, mask.data.length) * 1e4) / 100;
|
|
3605
|
+
const percent = Math.round(selectionArea(mask) / Math.max(1, mask.width * mask.height) * 1e4) / 100;
|
|
3537
3606
|
return { ...rest, bounds: mask.bounds, percent };
|
|
3538
3607
|
}
|
|
3539
3608
|
function serializeChannels(channels) {
|
|
@@ -4144,8 +4213,7 @@ var FreeTransformSession = class {
|
|
|
4144
4213
|
const result = part.base ? compositeSurfaceOver(part.base, moved) : moved;
|
|
4145
4214
|
if (part.what === "selection") {
|
|
4146
4215
|
const { width, height } = this.host.canvas();
|
|
4147
|
-
const
|
|
4148
|
-
const next = createSelectionMask(width, height, data);
|
|
4216
|
+
const next = selectionFromTiles(width, height, result);
|
|
4149
4217
|
selection = next.bounds ? next : null;
|
|
4150
4218
|
} else if (part.what === "pixels") {
|
|
4151
4219
|
layers = mapLayer(layers, targetId, (l) => {
|
|
@@ -5471,14 +5539,18 @@ var ImageEditorController = class {
|
|
|
5471
5539
|
this.trimHistoryBytes();
|
|
5472
5540
|
}
|
|
5473
5541
|
/**
|
|
5474
|
-
* Bytes of pixel tiles held only by undo / redo steps (not by the
|
|
5475
|
-
* undo steps are dropped while it exceeds `maxHistoryBytes`.
|
|
5542
|
+
* Bytes of pixel, selection and channel tiles held only by undo / redo steps (not by the
|
|
5543
|
+
* current state). The oldest undo steps are dropped while it exceeds `maxHistoryBytes`.
|
|
5476
5544
|
*/
|
|
5477
5545
|
get historyBytes() {
|
|
5478
5546
|
const live = /* @__PURE__ */ new Set();
|
|
5479
5547
|
collectStateTiles(this.state.layers, live);
|
|
5548
|
+
collectSelectionTiles(this.state.selection, this.state.channels, live);
|
|
5480
5549
|
const held = /* @__PURE__ */ new Set();
|
|
5481
|
-
for (const e of [...this.undoStack, ...this.redoStack])
|
|
5550
|
+
for (const e of [...this.undoStack, ...this.redoStack]) {
|
|
5551
|
+
collectStateTiles(e.layers, held);
|
|
5552
|
+
collectSelectionTiles(e.selection, e.channels, held);
|
|
5553
|
+
}
|
|
5482
5554
|
let n = 0;
|
|
5483
5555
|
for (const t of held) if (!live.has(t)) n += t.data.byteLength;
|
|
5484
5556
|
return n;
|
|
@@ -7376,7 +7448,7 @@ var ImageEditorController = class {
|
|
|
7376
7448
|
/** @internal Shows a stroke's live surface (no history). */
|
|
7377
7449
|
_strokeUpdate(layerId, channel, surface, rects) {
|
|
7378
7450
|
if (channel === "quick-mask") {
|
|
7379
|
-
this.state.selection = this.selectionFromSurface(surface
|
|
7451
|
+
this.state.selection = this.selectionFromSurface(surface);
|
|
7380
7452
|
this.notify();
|
|
7381
7453
|
return;
|
|
7382
7454
|
}
|
|
@@ -8324,6 +8396,41 @@ var ImageEditorController = class {
|
|
|
8324
8396
|
this.state.quickMask = display === "quickMask";
|
|
8325
8397
|
this.notify();
|
|
8326
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
|
+
}
|
|
8327
8434
|
// --- Alpha channels ---
|
|
8328
8435
|
/** The alpha channels (saved selections), in panel order. */
|
|
8329
8436
|
getChannels() {
|
|
@@ -8454,35 +8561,18 @@ var ImageEditorController = class {
|
|
|
8454
8561
|
const { width, height } = this.naturalSize;
|
|
8455
8562
|
const sel = this.state.selection;
|
|
8456
8563
|
if (!sel?.bounds) return emptySurface(1, 0);
|
|
8457
|
-
|
|
8458
|
-
return surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);
|
|
8564
|
+
return (sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height)).surface;
|
|
8459
8565
|
}
|
|
8460
|
-
/** The selection a painted Quick Mask surface stands for (
|
|
8461
|
-
selectionFromSurface(surface
|
|
8566
|
+
/** The selection a painted Quick Mask surface stands for (it shares the surface's tiles). */
|
|
8567
|
+
selectionFromSurface(surface) {
|
|
8462
8568
|
const { width, height } = this.naturalSize;
|
|
8463
|
-
|
|
8464
|
-
if (base && rects && base.width === width && base.height === height) {
|
|
8465
|
-
data = new Uint8Array(base.data);
|
|
8466
|
-
for (const r of rects) {
|
|
8467
|
-
const x0 = Math.max(0, Math.floor(r.x));
|
|
8468
|
-
const y0 = Math.max(0, Math.floor(r.y));
|
|
8469
|
-
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
8470
|
-
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
8471
|
-
if (x1 <= x0 || y1 <= y0) continue;
|
|
8472
|
-
const part = readRegion(surface, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
8473
|
-
for (let y = y0; y < y1; y++) data.set(part.subarray((y - y0) * (x1 - x0), (y - y0 + 1) * (x1 - x0)), y * width + x0);
|
|
8474
|
-
}
|
|
8475
|
-
} else {
|
|
8476
|
-
const all = readRegion(surface, { x: 0, y: 0, width, height });
|
|
8477
|
-
data = new Uint8Array(all.buffer, all.byteOffset, all.length);
|
|
8478
|
-
}
|
|
8479
|
-
return normalizeSelection(createSelectionMask(width, height, data));
|
|
8569
|
+
return normalizeSelection(selectionFromTiles(width, height, surface));
|
|
8480
8570
|
}
|
|
8481
8571
|
// --- Layer masks ---
|
|
8482
8572
|
maskFromSelection(sel, props = {}) {
|
|
8483
8573
|
const { width, height } = this.naturalSize;
|
|
8484
8574
|
const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);
|
|
8485
|
-
const mask = { surface:
|
|
8575
|
+
const mask = { surface: sized.surface, defaultValue: 0 };
|
|
8486
8576
|
return withMaskProps(mask, props);
|
|
8487
8577
|
}
|
|
8488
8578
|
/**
|
|
@@ -10154,6 +10244,10 @@ function isOpaqueInside(surface, width, height) {
|
|
|
10154
10244
|
}
|
|
10155
10245
|
return true;
|
|
10156
10246
|
}
|
|
10247
|
+
function collectSelectionTiles(selection, channels, into) {
|
|
10248
|
+
if (selection) collectTiles(selection.surface, into);
|
|
10249
|
+
for (const c of channels ?? []) collectTiles(c.mask.surface, into);
|
|
10250
|
+
}
|
|
10157
10251
|
function collectStateTiles(layers, into) {
|
|
10158
10252
|
for (const l of flattenLayers(layers)) {
|
|
10159
10253
|
if (l.type === "pixel" && l.source !== "document") collectTiles(l.surface, into);
|
|
@@ -11063,9 +11157,7 @@ var REFINE_EDGE_CONTROLS = [
|
|
|
11063
11157
|
];
|
|
11064
11158
|
function describeSelection(mask) {
|
|
11065
11159
|
if (!mask || !mask.bounds) return "No selection";
|
|
11066
|
-
|
|
11067
|
-
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
11068
|
-
const pct2 = sum / 255 / mask.data.length * 100;
|
|
11160
|
+
const pct2 = selectionArea(mask) / (mask.width * mask.height) * 100;
|
|
11069
11161
|
const shown = pct2 >= 10 ? Math.round(pct2) : Math.round(pct2 * 10) / 10;
|
|
11070
11162
|
return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
|
|
11071
11163
|
}
|
|
@@ -12075,6 +12167,22 @@ function selectionShade(selection, doc) {
|
|
|
12075
12167
|
context.putImageData(pixels, 0, 0);
|
|
12076
12168
|
return canvas;
|
|
12077
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
|
+
}
|
|
12078
12186
|
async function renderImageRegion(controller, request) {
|
|
12079
12187
|
const state = controller.getState();
|
|
12080
12188
|
const k = Math.min(1, request.scale);
|
|
@@ -12097,6 +12205,8 @@ async function renderImageRegion(controller, request) {
|
|
|
12097
12205
|
context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);
|
|
12098
12206
|
if (request.includeOverlays) {
|
|
12099
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);
|
|
12100
12210
|
const selection = state.selection;
|
|
12101
12211
|
if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {
|
|
12102
12212
|
const shade = selectionShade(selection, document);
|
|
@@ -12124,7 +12234,7 @@ function createImageCaptureSurface(controller) {
|
|
|
12124
12234
|
units: "image pixels",
|
|
12125
12235
|
content: whole,
|
|
12126
12236
|
selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),
|
|
12127
|
-
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)",
|
|
12128
12238
|
maxScale: 8,
|
|
12129
12239
|
render: (request) => renderImageRegion(controller, request)
|
|
12130
12240
|
};
|
|
@@ -12812,6 +12922,27 @@ function createImageLayerTools(controller) {
|
|
|
12812
12922
|
return { layer: describe(id) };
|
|
12813
12923
|
}
|
|
12814
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
|
+
};
|
|
12815
12946
|
const maskProps = {
|
|
12816
12947
|
name: "set_layer_mask_properties",
|
|
12817
12948
|
title: "Set layer mask properties",
|
|
@@ -12952,6 +13083,7 @@ function createImageLayerTools(controller) {
|
|
|
12952
13083
|
addMask,
|
|
12953
13084
|
applyMask,
|
|
12954
13085
|
removeMask,
|
|
13086
|
+
maskView,
|
|
12955
13087
|
maskProps,
|
|
12956
13088
|
viaCopy,
|
|
12957
13089
|
rasterize,
|
|
@@ -13292,196 +13424,447 @@ function createImageEffectTools(controller) {
|
|
|
13292
13424
|
return [list, add2, update, remove, setEnabled, light];
|
|
13293
13425
|
}
|
|
13294
13426
|
|
|
13295
|
-
// src/image-editor/
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
{
|
|
13303
|
-
];
|
|
13304
|
-
var MIXER_OPTION_CONTROLS = [
|
|
13305
|
-
{ id: "wet", label: "Wet", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13306
|
-
{ id: "load", label: "Load", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" },
|
|
13307
|
-
{ id: "mix", label: "Mix", section: "layer", min: 0, max: 1, step: 0.01, default: 0.5, decimals: 0, track: plain2, displayScale: 100, unit: "%" }
|
|
13308
|
-
];
|
|
13309
|
-
var MIXER_MIXING_OPTIONS = [
|
|
13310
|
-
{ id: "rgb", label: "RGB", title: "Mix colours as RGB averages" },
|
|
13311
|
-
{ id: "pigment", label: "Pigment", title: "Mix colours like paint: blue and yellow make green" }
|
|
13312
|
-
];
|
|
13313
|
-
var PAINT_TOOLS = [
|
|
13314
|
-
{ id: "move", label: "Move", shortcut: "V" },
|
|
13315
|
-
{ id: "brush", label: "Brush", shortcut: "B" },
|
|
13316
|
-
{ id: "mixer", label: "Mixer", shortcut: "Shift+B" },
|
|
13317
|
-
{ id: "eraser", label: "Eraser", shortcut: "E" },
|
|
13318
|
-
{ id: "clone", label: "Clone", shortcut: "S" },
|
|
13319
|
-
{ id: "heal", label: "Heal", shortcut: "Shift+J" },
|
|
13320
|
-
{ id: "spot-heal", label: "Spot Heal", shortcut: "J" },
|
|
13321
|
-
{ id: "smudge", label: "Smudge", shortcut: "" },
|
|
13322
|
-
{ id: "dodge", label: "Dodge", shortcut: "O" },
|
|
13323
|
-
{ id: "burn", label: "Burn", shortcut: "Shift+O" },
|
|
13324
|
-
{ id: "sponge", label: "Sponge", shortcut: "Shift+O" },
|
|
13325
|
-
{ id: "blur", label: "Blur", shortcut: "" },
|
|
13326
|
-
{ id: "sharpen", label: "Sharpen", shortcut: "" },
|
|
13327
|
-
{ id: "history", label: "History", shortcut: "Y" }
|
|
13328
|
-
];
|
|
13329
|
-
var isPaintTool = (tool) => PAINT_TOOLS.some((t) => t.id === tool);
|
|
13330
|
-
var isRetouchPaintTool = (tool) => tool === "clone" || tool === "heal" || tool === "spot-heal" || tool === "smudge" || tool === "dodge" || tool === "burn" || tool === "sponge" || tool === "blur" || tool === "sharpen" || tool === "history";
|
|
13331
|
-
var RETOUCH_OPTION_CONTROLS = [
|
|
13332
|
-
{ 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"] },
|
|
13333
|
-
{ 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"] },
|
|
13334
|
-
{ id: "historySteps", label: "Steps back", section: "layer", min: 0, max: 50, step: 1, default: 0, decimals: 0, track: plain2, unit: "", tools: ["history"] }
|
|
13335
|
-
];
|
|
13336
|
-
var CLONE_TRANSFORM_CONTROLS = [
|
|
13337
|
-
{ id: "width", label: "Source W", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13338
|
-
{ id: "height", label: "Source H", section: "layer", min: 1, max: 400, step: 1, default: 100, decimals: 0, track: plain2, unit: "%" },
|
|
13339
|
-
{ id: "angle", label: "Source angle", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: plain2, unit: "\xB0" }
|
|
13340
|
-
];
|
|
13341
|
-
function cloneTransformView(controller) {
|
|
13342
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13343
|
-
return {
|
|
13344
|
-
width: Math.round(Math.abs(t.scaleX) * 100),
|
|
13345
|
-
height: Math.round(Math.abs(t.scaleY) * 100),
|
|
13346
|
-
angle: t.rotate,
|
|
13347
|
-
flipH: t.scaleX < 0,
|
|
13348
|
-
flipV: t.scaleY < 0,
|
|
13349
|
-
identity: t.scaleX === 1 && t.scaleY === 1 && t.rotate === 0
|
|
13350
|
-
};
|
|
13351
|
-
}
|
|
13352
|
-
function setCloneTransformValue(controller, id, value) {
|
|
13353
|
-
const t = controller.getState().paint.cloneTransform;
|
|
13354
|
-
if (id === "width") controller.setCloneTransform({ scaleX: Math.sign(t.scaleX) * (value ?? 100) / 100 });
|
|
13355
|
-
else if (id === "height") controller.setCloneTransform({ scaleY: Math.sign(t.scaleY) * (value ?? 100) / 100 });
|
|
13356
|
-
else if (id === "angle") controller.setCloneTransform({ rotate: value ?? 0 });
|
|
13357
|
-
else if (id === "flipH") controller.setCloneTransform({ scaleX: -t.scaleX });
|
|
13358
|
-
else controller.setCloneTransform({ scaleY: -t.scaleY });
|
|
13359
|
-
}
|
|
13360
|
-
var RETOUCH_CHOICES = [
|
|
13361
|
-
{
|
|
13362
|
-
id: "sample",
|
|
13363
|
-
label: "Sample",
|
|
13364
|
-
tools: ["clone", "heal", "spot-heal"],
|
|
13365
|
-
options: [
|
|
13366
|
-
{ id: "current", label: "Current", title: "Sample the layer being retouched" },
|
|
13367
|
-
{ id: "current-below", label: "Current & Below", title: "Sample this layer and the layers under it" },
|
|
13368
|
-
{ id: "all", label: "All", title: "Sample every layer (retouch onto an empty layer)" }
|
|
13369
|
-
]
|
|
13370
|
-
},
|
|
13371
|
-
{
|
|
13372
|
-
id: "spotType",
|
|
13373
|
-
label: "Type",
|
|
13374
|
-
tools: ["spot-heal"],
|
|
13375
|
-
options: [
|
|
13376
|
-
{ id: "proximity-match", label: "Proximity Match", title: "Copy the best matching patch nearby" },
|
|
13377
|
-
{ id: "content-aware", label: "Content-Aware", title: "Synthesise texture and edges from patches around the spot" },
|
|
13378
|
-
{ id: "smooth", label: "Smooth", title: "Fill smoothly from the edges" }
|
|
13379
|
-
]
|
|
13380
|
-
},
|
|
13381
|
-
{
|
|
13382
|
-
id: "range",
|
|
13383
|
-
label: "Range",
|
|
13384
|
-
tools: ["dodge", "burn"],
|
|
13385
|
-
options: [
|
|
13386
|
-
{ id: "shadows", label: "Shadows", title: "Change the dark tones most" },
|
|
13387
|
-
{ id: "midtones", label: "Midtones", title: "Change the middle tones most" },
|
|
13388
|
-
{ id: "highlights", label: "Highlights", title: "Change the light tones most" }
|
|
13389
|
-
]
|
|
13390
|
-
},
|
|
13391
|
-
{
|
|
13392
|
-
id: "spongeMode",
|
|
13393
|
-
label: "Mode",
|
|
13394
|
-
tools: ["sponge"],
|
|
13395
|
-
options: [
|
|
13396
|
-
{ id: "desaturate", label: "Desaturate", title: "Take colour out" },
|
|
13397
|
-
{ id: "saturate", label: "Saturate", title: "Add colour" }
|
|
13398
|
-
]
|
|
13399
|
-
}
|
|
13400
|
-
];
|
|
13401
|
-
var RETOUCH_TOGGLES = [
|
|
13402
|
-
{ id: "aligned", label: "Aligned", tools: ["clone", "heal"] },
|
|
13403
|
-
{ id: "protectTones", label: "Protect tones", tools: ["dodge", "burn"] },
|
|
13404
|
-
{ id: "vibrance", label: "Vibrance", tools: ["sponge"] }
|
|
13405
|
-
];
|
|
13406
|
-
function setRetouchChoice(controller, id, value) {
|
|
13407
|
-
controller.setPaintOptions({ retouch: { [id]: value } });
|
|
13408
|
-
}
|
|
13409
|
-
function cloneSourceLabel(controller) {
|
|
13410
|
-
const cs = controller.getState().paint.cloneSource;
|
|
13411
|
-
return cs ? `Source ${cs.x}, ${cs.y}${cs.offset ? ` (offset ${cs.offset.x}, ${cs.offset.y})` : ""}` : "Alt-click to set the source";
|
|
13412
|
-
}
|
|
13413
|
-
function rgbUnitToHex(c) {
|
|
13414
|
-
return `#${c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
13415
|
-
}
|
|
13416
|
-
function sizeStep(size) {
|
|
13417
|
-
return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;
|
|
13418
|
-
}
|
|
13419
|
-
var isTyping = (t) => {
|
|
13420
|
-
const el = t;
|
|
13421
|
-
return Boolean(el && (el.isContentEditable || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT"));
|
|
13422
|
-
};
|
|
13423
|
-
function handlePaintShortcut(controller, e) {
|
|
13424
|
-
if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;
|
|
13425
|
-
const brush = controller.getState().paint.brush;
|
|
13426
|
-
const set = (b) => controller.setPaintOptions({ brush: b });
|
|
13427
|
-
const key = e.key;
|
|
13428
|
-
switch (key.toLowerCase()) {
|
|
13429
|
-
case "q":
|
|
13430
|
-
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
13431
|
-
return true;
|
|
13432
|
-
case "b":
|
|
13433
|
-
controller.setTool(e.shiftKey || key === "B" ? "mixer" : "brush");
|
|
13434
|
-
return true;
|
|
13435
|
-
case "e":
|
|
13436
|
-
controller.setTool("eraser");
|
|
13437
|
-
return true;
|
|
13438
|
-
case "v":
|
|
13439
|
-
controller.setTool("move");
|
|
13440
|
-
return true;
|
|
13441
|
-
case "s":
|
|
13442
|
-
controller.setTool("clone");
|
|
13443
|
-
return true;
|
|
13444
|
-
case "j":
|
|
13445
|
-
controller.setTool(e.shiftKey || key === "J" ? controller.getState().activeTool === "heal" ? "spot-heal" : "heal" : "spot-heal");
|
|
13446
|
-
return true;
|
|
13447
|
-
case "o": {
|
|
13448
|
-
const cur = controller.getState().activeTool;
|
|
13449
|
-
const next = e.shiftKey || key === "O" ? cur === "dodge" ? "burn" : cur === "burn" ? "sponge" : "dodge" : "dodge";
|
|
13450
|
-
controller.setTool(next);
|
|
13451
|
-
return true;
|
|
13452
|
-
}
|
|
13453
|
-
case "y":
|
|
13454
|
-
controller.setTool("history");
|
|
13455
|
-
return true;
|
|
13456
|
-
case "d":
|
|
13457
|
-
controller.resetPaintColors();
|
|
13458
|
-
return true;
|
|
13459
|
-
case "x":
|
|
13460
|
-
controller.swapPaintColors();
|
|
13461
|
-
return true;
|
|
13462
|
-
}
|
|
13463
|
-
if (key === "[" || key === "{") {
|
|
13464
|
-
if (e.shiftKey || key === "{") set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });
|
|
13465
|
-
else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });
|
|
13466
|
-
return true;
|
|
13467
|
-
}
|
|
13468
|
-
if (key === "]" || key === "}") {
|
|
13469
|
-
if (e.shiftKey || key === "}") set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });
|
|
13470
|
-
else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });
|
|
13471
|
-
return true;
|
|
13472
|
-
}
|
|
13473
|
-
if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {
|
|
13474
|
-
set({ opacity: key === "0" ? 1 : Number(key) / 10 });
|
|
13475
|
-
return true;
|
|
13476
|
-
}
|
|
13477
|
-
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 };
|
|
13478
13435
|
}
|
|
13479
|
-
var
|
|
13480
|
-
constructor(controller) {
|
|
13436
|
+
var SelectionGesture = class {
|
|
13437
|
+
constructor(controller, getOptions) {
|
|
13481
13438
|
this.controller = controller;
|
|
13439
|
+
this.getOptions = getOptions;
|
|
13482
13440
|
}
|
|
13483
13441
|
controller;
|
|
13484
|
-
|
|
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. */
|
|
13454
|
+
error = null;
|
|
13455
|
+
get active() {
|
|
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;
|
|
13485
13868
|
move = null;
|
|
13486
13869
|
/** The last refusal (e.g. "layer is locked"), for the editor to show. */
|
|
13487
13870
|
error = null;
|
|
@@ -15825,247 +16208,6 @@ function removeCurvePoint(points, index) {
|
|
|
15825
16208
|
}
|
|
15826
16209
|
var round22 = (v) => Math.round(v * 100) / 100;
|
|
15827
16210
|
|
|
15828
|
-
// src/image-editor/selection/gesture.ts
|
|
15829
|
-
function snapTo45(from, point) {
|
|
15830
|
-
const dx = point.x - from.x;
|
|
15831
|
-
const dy = point.y - from.y;
|
|
15832
|
-
const len = Math.hypot(dx, dy);
|
|
15833
|
-
if (len === 0) return point;
|
|
15834
|
-
const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
15835
|
-
return { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len };
|
|
15836
|
-
}
|
|
15837
|
-
var SelectionGesture = class {
|
|
15838
|
-
constructor(controller, getOptions) {
|
|
15839
|
-
this.controller = controller;
|
|
15840
|
-
this.getOptions = getOptions;
|
|
15841
|
-
}
|
|
15842
|
-
controller;
|
|
15843
|
-
getOptions;
|
|
15844
|
-
start = null;
|
|
15845
|
-
points = [];
|
|
15846
|
-
current = null;
|
|
15847
|
-
modifiers = {};
|
|
15848
|
-
/** Polygonal lasso corners while one is being drawn. */
|
|
15849
|
-
polygon = null;
|
|
15850
|
-
pressedAt = null;
|
|
15851
|
-
freehand = false;
|
|
15852
|
-
/** A click this close to the first corner (natural pixels) closes a polygonal lasso. */
|
|
15853
|
-
closeRadius = 8;
|
|
15854
|
-
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
15855
|
-
error = null;
|
|
15856
|
-
get active() {
|
|
15857
|
-
return this.start !== null || this.polygon !== null;
|
|
15858
|
-
}
|
|
15859
|
-
/** The outline of the shape being drawn, or `null`. */
|
|
15860
|
-
get draftPath() {
|
|
15861
|
-
if (this.polygon) return this.current ? [...this.polygon, this.current] : this.polygon.slice();
|
|
15862
|
-
if (!this.start || !this.current) return null;
|
|
15863
|
-
const { tool } = this.getOptions();
|
|
15864
|
-
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
15865
|
-
const rect = normalizeRect({
|
|
15866
|
-
x: this.start.x,
|
|
15867
|
-
y: this.start.y,
|
|
15868
|
-
width: this.current.x - this.start.x,
|
|
15869
|
-
height: this.current.y - this.start.y
|
|
15870
|
-
});
|
|
15871
|
-
if (rect.width < 1 && rect.height < 1) return null;
|
|
15872
|
-
if (tool === "ellipse") return ellipsePoints(rect);
|
|
15873
|
-
return [
|
|
15874
|
-
{ x: rect.x, y: rect.y },
|
|
15875
|
-
{ x: rect.x + rect.width, y: rect.y },
|
|
15876
|
-
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
15877
|
-
{ x: rect.x, y: rect.y + rect.height }
|
|
15878
|
-
];
|
|
15879
|
-
}
|
|
15880
|
-
/** Pointer down. Resolves once a wand click has been applied. */
|
|
15881
|
-
async begin(point, modifiers = {}) {
|
|
15882
|
-
const options = this.getOptions();
|
|
15883
|
-
this.error = null;
|
|
15884
|
-
if (this.polygon && options.tool !== "polygon") this.cancel();
|
|
15885
|
-
if (options.tool === "polygon") {
|
|
15886
|
-
this.pressedAt = point;
|
|
15887
|
-
this.freehand = false;
|
|
15888
|
-
if (!this.polygon) {
|
|
15889
|
-
this.modifiers = modifiers;
|
|
15890
|
-
this.polygon = [point];
|
|
15891
|
-
this.current = point;
|
|
15892
|
-
return;
|
|
15893
|
-
}
|
|
15894
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15895
|
-
const p = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15896
|
-
const first = this.polygon[0];
|
|
15897
|
-
if (this.polygon.length >= 3 && Math.hypot(p.x - first.x, p.y - first.y) <= this.closeRadius) {
|
|
15898
|
-
this.close();
|
|
15899
|
-
return;
|
|
15900
|
-
}
|
|
15901
|
-
if (Math.hypot(p.x - last.x, p.y - last.y) >= 0.5) this.polygon.push(p);
|
|
15902
|
-
this.current = p;
|
|
15903
|
-
if ((modifiers.detail ?? 1) >= 2) this.close();
|
|
15904
|
-
return;
|
|
15905
|
-
}
|
|
15906
|
-
this.modifiers = modifiers;
|
|
15907
|
-
if (options.tool === "row" || options.tool === "column") {
|
|
15908
|
-
this.start = null;
|
|
15909
|
-
try {
|
|
15910
|
-
this.controller.selectLine(options.tool, options.tool === "row" ? point.y : point.x, {
|
|
15911
|
-
mode: selectionModeFromModifiers(modifiers, options.mode),
|
|
15912
|
-
feather: options.feather ?? 0
|
|
15913
|
-
});
|
|
15914
|
-
} catch (err) {
|
|
15915
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15916
|
-
}
|
|
15917
|
-
return;
|
|
15918
|
-
}
|
|
15919
|
-
if (options.tool === "wand" || options.tool === "range") {
|
|
15920
|
-
this.start = null;
|
|
15921
|
-
const common = {
|
|
15922
|
-
sampleMerged: options.sampleMerged,
|
|
15923
|
-
sample: options.sample,
|
|
15924
|
-
feather: options.feather ?? 0,
|
|
15925
|
-
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
15926
|
-
};
|
|
15927
|
-
try {
|
|
15928
|
-
if (options.tool === "range") {
|
|
15929
|
-
const pixels = await this.controller.getSamplePixels(common);
|
|
15930
|
-
if (!pixels) throw new Error("The image pixels are not available to sample.");
|
|
15931
|
-
const x = Math.min(pixels.width - 1, Math.max(0, Math.floor(point.x)));
|
|
15932
|
-
const y = Math.min(pixels.height - 1, Math.max(0, Math.floor(point.y)));
|
|
15933
|
-
const p = (y * pixels.width + x) * 4;
|
|
15934
|
-
const color = [pixels.data[p], pixels.data[p + 1], pixels.data[p + 2]];
|
|
15935
|
-
await this.controller.selectColorRange([{ color, at: point }], {
|
|
15936
|
-
...common,
|
|
15937
|
-
fuzziness: options.fuzziness ?? 40,
|
|
15938
|
-
localized: options.localized ? options.localized : void 0,
|
|
15939
|
-
invert: options.rangeInvert ?? false
|
|
15940
|
-
});
|
|
15941
|
-
return;
|
|
15942
|
-
}
|
|
15943
|
-
await this.controller.magicWand(point, {
|
|
15944
|
-
...common,
|
|
15945
|
-
tolerance: options.tolerance,
|
|
15946
|
-
contiguous: options.contiguous,
|
|
15947
|
-
antiAlias: options.antiAlias,
|
|
15948
|
-
distance: options.distance
|
|
15949
|
-
});
|
|
15950
|
-
} catch (err) {
|
|
15951
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
15952
|
-
}
|
|
15953
|
-
return;
|
|
15954
|
-
}
|
|
15955
|
-
this.start = point;
|
|
15956
|
-
this.current = point;
|
|
15957
|
-
this.points = [point];
|
|
15958
|
-
}
|
|
15959
|
-
/** Pointer move; returns true when the draft changed (re-render). */
|
|
15960
|
-
move(point, modifiers = {}) {
|
|
15961
|
-
if (this.polygon) {
|
|
15962
|
-
const last = this.polygon[this.polygon.length - 1];
|
|
15963
|
-
if (this.pressedAt && (this.freehand || Math.hypot(point.x - this.pressedAt.x, point.y - this.pressedAt.y) >= 3)) {
|
|
15964
|
-
this.freehand = true;
|
|
15965
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) >= 0.5) this.polygon.push(point);
|
|
15966
|
-
this.current = null;
|
|
15967
|
-
return true;
|
|
15968
|
-
}
|
|
15969
|
-
this.current = modifiers.shiftKey ? snapTo45(last, point) : point;
|
|
15970
|
-
return true;
|
|
15971
|
-
}
|
|
15972
|
-
if (!this.start) return false;
|
|
15973
|
-
this.current = point;
|
|
15974
|
-
if (this.getOptions().tool === "lasso") {
|
|
15975
|
-
const last = this.points[this.points.length - 1];
|
|
15976
|
-
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
15977
|
-
this.points.push(point);
|
|
15978
|
-
}
|
|
15979
|
-
return true;
|
|
15980
|
-
}
|
|
15981
|
-
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
15982
|
-
end(point, modifiers) {
|
|
15983
|
-
if (this.polygon) {
|
|
15984
|
-
if (point && this.freehand) this.move(point);
|
|
15985
|
-
this.pressedAt = null;
|
|
15986
|
-
this.freehand = false;
|
|
15987
|
-
return false;
|
|
15988
|
-
}
|
|
15989
|
-
if (!this.start) return false;
|
|
15990
|
-
if (point) this.move(point);
|
|
15991
|
-
const options = this.getOptions();
|
|
15992
|
-
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
15993
|
-
const path = this.draftPath;
|
|
15994
|
-
const rect = normalizeRect({
|
|
15995
|
-
x: this.start.x,
|
|
15996
|
-
y: this.start.y,
|
|
15997
|
-
width: this.current.x - this.start.x,
|
|
15998
|
-
height: this.current.y - this.start.y
|
|
15999
|
-
});
|
|
16000
|
-
this.start = null;
|
|
16001
|
-
this.current = null;
|
|
16002
|
-
this.points = [];
|
|
16003
|
-
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
16004
|
-
const opts = { mode, antiAlias: options.antiAlias, feather: options.feather ?? 0 };
|
|
16005
|
-
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
16006
|
-
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
16007
|
-
}
|
|
16008
|
-
/** Closes a polygonal lasso and selects it. Returns true when the selection changed. */
|
|
16009
|
-
close() {
|
|
16010
|
-
const polygon = this.polygon;
|
|
16011
|
-
if (!polygon) return false;
|
|
16012
|
-
const options = this.getOptions();
|
|
16013
|
-
this.cancel();
|
|
16014
|
-
if (polygon.length < 3) return false;
|
|
16015
|
-
return this.controller.selectPolygon(polygon, {
|
|
16016
|
-
mode: selectionModeFromModifiers(this.modifiers, options.mode),
|
|
16017
|
-
antiAlias: options.antiAlias,
|
|
16018
|
-
feather: options.feather ?? 0
|
|
16019
|
-
});
|
|
16020
|
-
}
|
|
16021
|
-
/**
|
|
16022
|
-
* Keys for a polygonal lasso being drawn: Enter closes it, Escape cancels, Backspace / Delete
|
|
16023
|
-
* removes the last corner. Returns true when the key was used (the host prevents its default).
|
|
16024
|
-
*/
|
|
16025
|
-
key(key) {
|
|
16026
|
-
if (!this.polygon) return false;
|
|
16027
|
-
if (key === "Enter") this.close();
|
|
16028
|
-
else if (key === "Escape") this.cancel();
|
|
16029
|
-
else if (key === "Backspace" || key === "Delete") {
|
|
16030
|
-
this.polygon.pop();
|
|
16031
|
-
if (!this.polygon.length) this.cancel();
|
|
16032
|
-
} else return false;
|
|
16033
|
-
return true;
|
|
16034
|
-
}
|
|
16035
|
-
/** Abandons a drag (or a polygonal lasso) without changing the selection. */
|
|
16036
|
-
cancel() {
|
|
16037
|
-
this.start = null;
|
|
16038
|
-
this.current = null;
|
|
16039
|
-
this.points = [];
|
|
16040
|
-
this.polygon = null;
|
|
16041
|
-
this.pressedAt = null;
|
|
16042
|
-
this.freehand = false;
|
|
16043
|
-
}
|
|
16044
|
-
};
|
|
16045
|
-
function handleSelectionShortcut(controller, e, onError) {
|
|
16046
|
-
if (!(e.ctrlKey || e.metaKey)) return false;
|
|
16047
|
-
const key = e.key.toLowerCase();
|
|
16048
|
-
const run = (fn) => {
|
|
16049
|
-
try {
|
|
16050
|
-
fn();
|
|
16051
|
-
} catch (err) {
|
|
16052
|
-
onError?.(err instanceof Error ? err.message : String(err));
|
|
16053
|
-
}
|
|
16054
|
-
return true;
|
|
16055
|
-
};
|
|
16056
|
-
if (key === "c") return run(() => controller.copyPixels({ merged: Boolean(e.shiftKey) }));
|
|
16057
|
-
if (key === "x" && !e.shiftKey) return run(() => controller.cutPixels());
|
|
16058
|
-
if (key === "v") return run(() => controller.pastePixels(e.shiftKey && e.altKey ? "into" : e.shiftKey ? "in-place" : "new-layer"));
|
|
16059
|
-
if (key === "d") return run(() => e.shiftKey ? controller.reselect() : controller.deselect());
|
|
16060
|
-
if (key === "i" && e.shiftKey) return run(() => controller.invertSelection());
|
|
16061
|
-
return false;
|
|
16062
|
-
}
|
|
16063
|
-
function handleQuickMaskKey(controller, e) {
|
|
16064
|
-
if (e.ctrlKey || e.metaKey || e.altKey || e.key.toLowerCase() !== "q") return false;
|
|
16065
|
-
controller.setQuickMaskMode(!controller.quickMaskEditing);
|
|
16066
|
-
return true;
|
|
16067
|
-
}
|
|
16068
|
-
|
|
16069
16211
|
// src/image-editor/selection/mask-files.ts
|
|
16070
16212
|
var MASK_FILE_ACCEPT = "image/png,image/jpeg,image/webp,image/gif,image/bmp";
|
|
16071
16213
|
function blobDataUrl(file) {
|
|
@@ -16241,6 +16383,10 @@ function maskThumbnailClick(controller, layer, e) {
|
|
|
16241
16383
|
if (!layer.mask) return;
|
|
16242
16384
|
if (e.ctrlKey || e.metaKey) {
|
|
16243
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);
|
|
16244
16390
|
} else if (e.shiftKey) {
|
|
16245
16391
|
controller.setLayerMaskProps(layer.id, { enabled: layer.mask.enabled === false });
|
|
16246
16392
|
} else {
|
|
@@ -18183,6 +18329,7 @@ export {
|
|
|
18183
18329
|
growSelection,
|
|
18184
18330
|
guidePlanes,
|
|
18185
18331
|
guidedFilterColor,
|
|
18332
|
+
handleMaskViewKey,
|
|
18186
18333
|
handlePaintShortcut,
|
|
18187
18334
|
handleQuickMaskKey,
|
|
18188
18335
|
handleSelectionShortcut,
|
|
@@ -18273,6 +18420,7 @@ export {
|
|
|
18273
18420
|
layerMaskCoverage,
|
|
18274
18421
|
layerMaskFromSelection,
|
|
18275
18422
|
layerMaskKey,
|
|
18423
|
+
layerMaskSelection,
|
|
18276
18424
|
layerMenu,
|
|
18277
18425
|
layerName,
|
|
18278
18426
|
layerPanelRows,
|
|
@@ -18410,6 +18558,7 @@ export {
|
|
|
18410
18558
|
pointerStrokeSample,
|
|
18411
18559
|
pointerStrokeSamples,
|
|
18412
18560
|
polygonCoverage,
|
|
18561
|
+
polygonCoverageBox,
|
|
18413
18562
|
polygonSelection,
|
|
18414
18563
|
posterizeValue,
|
|
18415
18564
|
presetWarp,
|
|
@@ -18509,6 +18658,8 @@ export {
|
|
|
18509
18658
|
selectionArea,
|
|
18510
18659
|
selectionFromCoverage,
|
|
18511
18660
|
selectionFromPixels,
|
|
18661
|
+
selectionFromRegion,
|
|
18662
|
+
selectionFromTiles,
|
|
18512
18663
|
selectionModeFromModifiers,
|
|
18513
18664
|
selectionOutline,
|
|
18514
18665
|
selectionRing,
|