@pixerate/editor 0.259.0 → 0.260.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/agent/index.cjs +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/index.cjs +61 -59
- package/dist/animation-editor/index.js +5 -3
- package/dist/brushes/index.cjs +52 -52
- package/dist/brushes/index.js +6 -6
- package/dist/{brushstrokeRender-JI4W6F2G.js → brushstrokeRender-25Q5BEY6.js} +17 -7
- package/dist/{brushstrokeRender-MQ6PSDM5.cjs → brushstrokeRender-N6UPZSQS.cjs} +31 -21
- package/dist/canvas/brushTypes.d.cts +5 -0
- package/dist/canvas/brushTypes.d.ts +5 -0
- package/dist/canvas/drawController.d.cts +15 -1
- package/dist/canvas/drawController.d.ts +15 -1
- package/dist/canvas/index.cjs +136 -102
- package/dist/canvas/index.js +35 -1
- package/dist/{chunk-3PPJRMIG.js → chunk-3O47TPGX.js} +197 -18
- package/dist/{chunk-5Q5MLXAD.cjs → chunk-3TQW5MPW.cjs} +23 -23
- package/dist/{chunk-JBBJE76O.cjs → chunk-3ZRNK3RO.cjs} +1138 -85
- package/dist/{chunk-YSGW7VV4.cjs → chunk-43NT242D.cjs} +1401 -228
- package/dist/{chunk-SMTUQWGY.js → chunk-5IW65VQ6.js} +1 -1
- package/dist/{chunk-YQ4PQTBB.js → chunk-6NZL24W4.js} +9 -1
- package/dist/{chunk-O3ZVZSNJ.cjs → chunk-7AUQ2UJ3.cjs} +137 -129
- package/dist/{chunk-4M4E3N6X.js → chunk-CQVTGBQI.js} +1 -1
- package/dist/{chunk-SSFMWJDK.js → chunk-D4TE5JNW.js} +1192 -19
- package/dist/{chunk-AORZ5M5C.js → chunk-EHALWBBD.js} +1 -1
- package/dist/{chunk-6WCQ35GI.cjs → chunk-NHA26O5X.cjs} +2 -2
- package/dist/{chunk-DPYAO5U6.cjs → chunk-PVJMTLPJ.cjs} +197 -18
- package/dist/{chunk-MQGQTKVW.cjs → chunk-PYTJBVW5.cjs} +29 -29
- package/dist/{chunk-IXQZHCOF.js → chunk-QAR2N7HZ.js} +1 -1
- package/dist/{chunk-5P4JEOIV.js → chunk-TMDDMJCR.js} +2 -2
- package/dist/{chunk-RSIKDWOP.cjs → chunk-TZOX7KWV.cjs} +15 -15
- package/dist/{chunk-IPUTIUGQ.cjs → chunk-UVZ7RQOE.cjs} +1 -1
- package/dist/{chunk-A7GJ6BXI.js → chunk-WJ4WQJCC.js} +1062 -9
- package/dist/{chunk-ICTPLVSR.js → chunk-X2EXYY4J.js} +1 -1
- package/dist/{chunk-WQCNC4DK.js → chunk-XC6DPRYI.js} +695 -4
- package/dist/{chunk-KBYB7OWB.cjs → chunk-YHDHFJUC.cjs} +776 -85
- package/dist/{chunk-P3PZ63M3.cjs → chunk-YLUIFXSL.cjs} +35 -35
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/image-editor/index.cjs +1385 -2484
- package/dist/image-editor/index.js +139 -1238
- package/dist/image-editor/selection-worker.cjs +3 -4
- package/dist/image-editor/selection-worker.js +1 -2
- package/dist/paint/index.cjs +3 -5
- package/dist/paint/index.js +40 -42
- package/dist/paint-editor/contract.d.cts +29 -0
- package/dist/paint-editor/contract.d.ts +29 -0
- package/dist/paint-editor/dab-stroke.d.cts +5 -0
- package/dist/paint-editor/dab-stroke.d.ts +5 -0
- package/dist/paint-editor/index.cjs +402 -258
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +197 -53
- package/dist/paint-editor/stamp-tip.d.cts +17 -0
- package/dist/paint-editor/stamp-tip.d.ts +17 -0
- package/dist/paint-editor/ui.d.cts +21 -1
- package/dist/paint-editor/ui.d.ts +21 -1
- package/dist/raster/index.cjs +2 -4
- package/dist/raster/index.js +6 -8
- package/dist/vector/brushstroke.d.cts +6 -0
- package/dist/vector/brushstroke.d.ts +6 -0
- package/dist/vector/controller.d.cts +11 -2
- package/dist/vector/controller.d.ts +11 -2
- package/dist/vector/index.cjs +990 -973
- package/dist/vector/index.js +28 -11
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -1
- package/skills/pixerate-canvas/SKILL.md +3 -1
- package/skills/pixerate-vector/SKILL.md +2 -2
- package/dist/chunk-2VJAVXLI.js +0 -192
- package/dist/chunk-62Z5WLRA.js +0 -700
- package/dist/chunk-BOHDJJ2O.cjs +0 -192
- package/dist/chunk-NKGB64CW.cjs +0 -700
- package/dist/chunk-SS53NNZO.cjs +0 -1066
- package/dist/chunk-ZNA3IQZF.js +0 -1066
package/dist/chunk-ZNA3IQZF.js
DELETED
|
@@ -1,1066 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
gaussianBlur
|
|
3
|
-
} from "./chunk-ZIF4UJJ6.js";
|
|
4
|
-
import {
|
|
5
|
-
TILE,
|
|
6
|
-
TiledSurface,
|
|
7
|
-
contentBounds,
|
|
8
|
-
finishTile,
|
|
9
|
-
readPixel,
|
|
10
|
-
readRegion,
|
|
11
|
-
tileCoord,
|
|
12
|
-
tileKey,
|
|
13
|
-
uniformTile,
|
|
14
|
-
writeRegion
|
|
15
|
-
} from "./chunk-HVVR5PZW.js";
|
|
16
|
-
|
|
17
|
-
// src/image-editor/selection/mask.ts
|
|
18
|
-
var SELECTION_MODES = ["replace", "add", "subtract", "intersect"];
|
|
19
|
-
function isSelectionMode(value) {
|
|
20
|
-
return typeof value === "string" && SELECTION_MODES.includes(value);
|
|
21
|
-
}
|
|
22
|
-
function parseSelectionMode(value) {
|
|
23
|
-
return isSelectionMode(value) ? value : "replace";
|
|
24
|
-
}
|
|
25
|
-
var MAX_SIDE = 1 << 15;
|
|
26
|
-
function checkSize(width, height) {
|
|
27
|
-
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE) {
|
|
28
|
-
throw new Error(`Invalid selection size ${width}\xD7${height}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function computeSelectionBounds(width, height, data) {
|
|
32
|
-
let x0 = width;
|
|
33
|
-
let x1 = -1;
|
|
34
|
-
let y0 = -1;
|
|
35
|
-
let y1 = -1;
|
|
36
|
-
for (let y = 0; y < height; y++) {
|
|
37
|
-
const row = y * width;
|
|
38
|
-
let first = -1;
|
|
39
|
-
for (let x = 0; x < width; x++) {
|
|
40
|
-
if (data[row + x] !== 0) {
|
|
41
|
-
first = x;
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (first < 0) continue;
|
|
46
|
-
let last = first;
|
|
47
|
-
for (let x = width - 1; x > first; x--) {
|
|
48
|
-
if (data[row + x] !== 0) {
|
|
49
|
-
last = x;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (y0 < 0) y0 = y;
|
|
54
|
-
y1 = y;
|
|
55
|
-
if (first < x0) x0 = first;
|
|
56
|
-
if (last > x1) x1 = last;
|
|
57
|
-
}
|
|
58
|
-
if (y0 < 0) return null;
|
|
59
|
-
return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
|
|
60
|
-
}
|
|
61
|
-
var TILE_PIXELS = TILE * TILE;
|
|
62
|
-
var MATERIALIZED_LIMIT = 4;
|
|
63
|
-
var materialized = /* @__PURE__ */ new Map();
|
|
64
|
-
var lastRead = null;
|
|
65
|
-
var lastData = null;
|
|
66
|
-
var TiledSelectionMask = class {
|
|
67
|
-
constructor(width, height, surface, bounds, data) {
|
|
68
|
-
this.width = width;
|
|
69
|
-
this.height = height;
|
|
70
|
-
this.surface = surface;
|
|
71
|
-
this.cachedBounds = bounds;
|
|
72
|
-
if (data) this.remember(data);
|
|
73
|
-
}
|
|
74
|
-
width;
|
|
75
|
-
height;
|
|
76
|
-
surface;
|
|
77
|
-
cachedBounds;
|
|
78
|
-
weak = null;
|
|
79
|
-
get bounds() {
|
|
80
|
-
if (this.cachedBounds === void 0) this.cachedBounds = contentBounds(this.surface);
|
|
81
|
-
return this.cachedBounds;
|
|
82
|
-
}
|
|
83
|
-
get data() {
|
|
84
|
-
if (lastRead === this) return lastData;
|
|
85
|
-
let d = materialized.get(this);
|
|
86
|
-
if (d) {
|
|
87
|
-
materialized.delete(this);
|
|
88
|
-
materialized.set(this, d);
|
|
89
|
-
lastRead = this;
|
|
90
|
-
lastData = d;
|
|
91
|
-
return d;
|
|
92
|
-
}
|
|
93
|
-
d = this.weak?.deref();
|
|
94
|
-
if (!d) {
|
|
95
|
-
const all = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
|
|
96
|
-
d = new Uint8Array(all.buffer, all.byteOffset, all.length);
|
|
97
|
-
}
|
|
98
|
-
this.remember(d);
|
|
99
|
-
return d;
|
|
100
|
-
}
|
|
101
|
-
remember(d) {
|
|
102
|
-
lastRead = this;
|
|
103
|
-
lastData = d;
|
|
104
|
-
this.weak = typeof WeakRef === "function" ? new WeakRef(d) : null;
|
|
105
|
-
materialized.set(this, d);
|
|
106
|
-
while (materialized.size > MATERIALIZED_LIMIT) materialized.delete(materialized.keys().next().value);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
function tileGrid(width, height) {
|
|
110
|
-
return { cols: Math.ceil(width / TILE), rows: Math.ceil(height / TILE) };
|
|
111
|
-
}
|
|
112
|
-
function insideTile(tx, ty, width, height) {
|
|
113
|
-
return (tx + 1) * TILE <= width && (ty + 1) * TILE <= height;
|
|
114
|
-
}
|
|
115
|
-
function surfaceFromCoverage(width, height, data) {
|
|
116
|
-
const tiles = /* @__PURE__ */ new Map();
|
|
117
|
-
const { cols, rows } = tileGrid(width, height);
|
|
118
|
-
for (let ty = 0; ty < rows; ty++) {
|
|
119
|
-
for (let tx = 0; tx < cols; tx++) {
|
|
120
|
-
const ox = tx * TILE;
|
|
121
|
-
const oy = ty * TILE;
|
|
122
|
-
const w = Math.min(TILE, width - ox);
|
|
123
|
-
const h = Math.min(TILE, height - oy);
|
|
124
|
-
let lo = 255;
|
|
125
|
-
let hi = 0;
|
|
126
|
-
for (let y = 0; y < h && !(hi > 0 && lo < hi); y++) {
|
|
127
|
-
const row = (oy + y) * width + ox;
|
|
128
|
-
for (let x = 0; x < w; x++) {
|
|
129
|
-
const v = data[row + x];
|
|
130
|
-
if (v < lo) lo = v;
|
|
131
|
-
if (v > hi) hi = v;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (hi === 0) continue;
|
|
135
|
-
if (lo === hi && w === TILE && h === TILE) {
|
|
136
|
-
tiles.set(tileKey(tx, ty), uniformTile(1, hi));
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
const t = new Uint8ClampedArray(TILE_PIXELS);
|
|
140
|
-
for (let y = 0; y < h; y++) {
|
|
141
|
-
const row = (oy + y) * width + ox;
|
|
142
|
-
for (let x = 0; x < w; x++) t[y * TILE + x] = data[row + x];
|
|
143
|
-
}
|
|
144
|
-
tiles.set(tileKey(tx, ty), { data: t, uniform: null });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return new TiledSurface(1, tiles, 0);
|
|
148
|
-
}
|
|
149
|
-
function clipTile(tile, w, h) {
|
|
150
|
-
const d = tile.data;
|
|
151
|
-
let outside = false;
|
|
152
|
-
for (let y = 0; y < TILE && !outside; y++) {
|
|
153
|
-
for (let x = y < h ? w : 0; x < TILE; x++) {
|
|
154
|
-
if (d[y * TILE + x] !== 0) {
|
|
155
|
-
outside = true;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!outside) return tile;
|
|
161
|
-
const t = new Uint8ClampedArray(TILE_PIXELS);
|
|
162
|
-
for (let y = 0; y < h; y++) t.set(d.subarray(y * TILE, y * TILE + w), y * TILE);
|
|
163
|
-
return finishTile(t, 1, 0);
|
|
164
|
-
}
|
|
165
|
-
function selectionFromTiles(width, height, surface) {
|
|
166
|
-
checkSize(width, height);
|
|
167
|
-
if (surface.channels !== 1) throw new Error("A selection needs a 1-channel surface");
|
|
168
|
-
if (surface.defaultValue !== 0) {
|
|
169
|
-
const all = readRegion(surface, { x: 0, y: 0, width, height });
|
|
170
|
-
return createSelectionMask(width, height, new Uint8Array(all.buffer, all.byteOffset, all.length));
|
|
171
|
-
}
|
|
172
|
-
const { cols, rows } = tileGrid(width, height);
|
|
173
|
-
let tiles = null;
|
|
174
|
-
for (const [key, tile] of surface.tiles) {
|
|
175
|
-
const { tx, ty } = tileCoord(key);
|
|
176
|
-
let next = tile;
|
|
177
|
-
if (tx < 0 || ty < 0 || tx >= cols || ty >= rows) next = null;
|
|
178
|
-
else if (!insideTile(tx, ty, width, height)) next = clipTile(tile, Math.min(TILE, width - tx * TILE), Math.min(TILE, height - ty * TILE));
|
|
179
|
-
if (next === tile) continue;
|
|
180
|
-
tiles ??= new Map(surface.tiles);
|
|
181
|
-
if (next) tiles.set(key, next);
|
|
182
|
-
else tiles.delete(key);
|
|
183
|
-
}
|
|
184
|
-
return new TiledSelectionMask(width, height, tiles ? new TiledSurface(1, tiles, 0) : surface);
|
|
185
|
-
}
|
|
186
|
-
function createSelectionMask(width, height, data) {
|
|
187
|
-
checkSize(width, height);
|
|
188
|
-
if (!data) return new TiledSelectionMask(width, height, new TiledSurface(1, /* @__PURE__ */ new Map(), 0), null);
|
|
189
|
-
if (data.length !== width * height) throw new Error("Selection data does not match its size");
|
|
190
|
-
return new TiledSelectionMask(width, height, surfaceFromCoverage(width, height, data), void 0, data);
|
|
191
|
-
}
|
|
192
|
-
function selectionFromRegion(width, height, rect, data) {
|
|
193
|
-
checkSize(width, height);
|
|
194
|
-
if (rect.x < 0 || rect.y < 0 || rect.x + rect.width > width || rect.y + rect.height > height) throw new Error("The region lies outside the selection");
|
|
195
|
-
return new TiledSelectionMask(width, height, writeRegion(new TiledSurface(1, /* @__PURE__ */ new Map(), 0), rect, data));
|
|
196
|
-
}
|
|
197
|
-
function fullSelection(width, height) {
|
|
198
|
-
checkSize(width, height);
|
|
199
|
-
const tiles = /* @__PURE__ */ new Map();
|
|
200
|
-
const { cols, rows } = tileGrid(width, height);
|
|
201
|
-
for (let ty = 0; ty < rows; ty++) {
|
|
202
|
-
for (let tx = 0; tx < cols; tx++) {
|
|
203
|
-
if (insideTile(tx, ty, width, height)) {
|
|
204
|
-
tiles.set(tileKey(tx, ty), uniformTile(1, 255));
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const t = new Uint8ClampedArray(TILE_PIXELS);
|
|
208
|
-
const w = Math.min(TILE, width - tx * TILE);
|
|
209
|
-
for (let y = 0; y < Math.min(TILE, height - ty * TILE); y++) t.fill(255, y * TILE, y * TILE + w);
|
|
210
|
-
tiles.set(tileKey(tx, ty), { data: t, uniform: null });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return new TiledSelectionMask(width, height, new TiledSurface(1, tiles, 0), { x: 0, y: 0, width, height });
|
|
214
|
-
}
|
|
215
|
-
function isSelectionEmpty(mask) {
|
|
216
|
-
return !mask || mask.bounds === null;
|
|
217
|
-
}
|
|
218
|
-
function selectionValueAt(mask, x, y) {
|
|
219
|
-
const xi = Math.floor(x);
|
|
220
|
-
const yi = Math.floor(y);
|
|
221
|
-
if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;
|
|
222
|
-
return readPixel(mask.surface, xi, yi)[0] / 255;
|
|
223
|
-
}
|
|
224
|
-
function coverageToByte(v) {
|
|
225
|
-
if (!(v > 0)) return 0;
|
|
226
|
-
if (v >= 1) return 255;
|
|
227
|
-
return Math.floor(v * 255 + 0.5);
|
|
228
|
-
}
|
|
229
|
-
function selectionFromCoverage(width, height, coverage) {
|
|
230
|
-
const n = width * height;
|
|
231
|
-
const out = new Uint8Array(n);
|
|
232
|
-
for (let i = 0; i < n; i++) out[i] = coverageToByte(coverage[i]);
|
|
233
|
-
return createSelectionMask(width, height, out);
|
|
234
|
-
}
|
|
235
|
-
function selectionToCoverage(mask) {
|
|
236
|
-
const out = new Float32Array(mask.data.length);
|
|
237
|
-
for (let i = 0; i < out.length; i++) out[i] = mask.data[i] / 255;
|
|
238
|
-
return out;
|
|
239
|
-
}
|
|
240
|
-
function selectionArea(mask) {
|
|
241
|
-
if (!mask || !mask.bounds) return 0;
|
|
242
|
-
let sum = 0;
|
|
243
|
-
for (const tile of mask.surface.tiles.values()) {
|
|
244
|
-
if (tile.uniform !== null) {
|
|
245
|
-
sum += tile.uniform * TILE_PIXELS;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
const d = tile.data;
|
|
249
|
-
for (let i = 0; i < d.length; i++) sum += d[i];
|
|
250
|
-
}
|
|
251
|
-
return sum / 255;
|
|
252
|
-
}
|
|
253
|
-
function combineSelections(prev, next, mode) {
|
|
254
|
-
let result;
|
|
255
|
-
if (mode === "replace" || !prev && mode === "add") {
|
|
256
|
-
result = next;
|
|
257
|
-
} else if (!prev) {
|
|
258
|
-
return null;
|
|
259
|
-
} else {
|
|
260
|
-
if (prev.width !== next.width || prev.height !== next.height) throw new Error("Selections differ in size");
|
|
261
|
-
result = new TiledSelectionMask(prev.width, prev.height, combineSurfaces(prev.surface, next.surface, mode));
|
|
262
|
-
}
|
|
263
|
-
return result.bounds ? result : null;
|
|
264
|
-
}
|
|
265
|
-
var FULL = 255;
|
|
266
|
-
function combineTile(a, b, mode) {
|
|
267
|
-
if (mode === "add") {
|
|
268
|
-
if (!b || a?.uniform === FULL) return a;
|
|
269
|
-
if (!a || b.uniform === FULL) return b;
|
|
270
|
-
} else if (mode === "subtract") {
|
|
271
|
-
if (!a || !b) return a;
|
|
272
|
-
if (b.uniform === FULL) return void 0;
|
|
273
|
-
} else {
|
|
274
|
-
if (!a || !b) return void 0;
|
|
275
|
-
if (a.uniform === FULL) return b;
|
|
276
|
-
if (b.uniform === FULL) return a;
|
|
277
|
-
}
|
|
278
|
-
const x = a.data;
|
|
279
|
-
const y = b.data;
|
|
280
|
-
const out = new Uint8ClampedArray(TILE_PIXELS);
|
|
281
|
-
if (mode === "add") for (let i = 0; i < out.length; i++) out[i] = x[i] > y[i] ? x[i] : y[i];
|
|
282
|
-
else if (mode === "subtract") for (let i = 0; i < out.length; i++) out[i] = x[i] > y[i] ? x[i] - y[i] : 0;
|
|
283
|
-
else for (let i = 0; i < out.length; i++) out[i] = x[i] < y[i] ? x[i] : y[i];
|
|
284
|
-
return finishTile(out, 1, 0) ?? void 0;
|
|
285
|
-
}
|
|
286
|
-
function combineSurfaces(a, b, mode) {
|
|
287
|
-
const tiles = /* @__PURE__ */ new Map();
|
|
288
|
-
for (const key of /* @__PURE__ */ new Set([...a.tiles.keys(), ...b.tiles.keys()])) {
|
|
289
|
-
const tile = combineTile(a.tiles.get(key), b.tiles.get(key), mode);
|
|
290
|
-
if (tile) tiles.set(key, tile);
|
|
291
|
-
}
|
|
292
|
-
return new TiledSurface(1, tiles, 0);
|
|
293
|
-
}
|
|
294
|
-
function invertSelection(mask) {
|
|
295
|
-
const { width, height } = mask;
|
|
296
|
-
const tiles = /* @__PURE__ */ new Map();
|
|
297
|
-
const { cols, rows } = tileGrid(width, height);
|
|
298
|
-
for (let ty = 0; ty < rows; ty++) {
|
|
299
|
-
for (let tx = 0; tx < cols; tx++) {
|
|
300
|
-
const src = mask.surface.getTile(tx, ty);
|
|
301
|
-
const key = tileKey(tx, ty);
|
|
302
|
-
if (insideTile(tx, ty, width, height)) {
|
|
303
|
-
if (!src) tiles.set(key, uniformTile(1, FULL));
|
|
304
|
-
else if (src.uniform !== null) {
|
|
305
|
-
if (src.uniform !== FULL) tiles.set(key, uniformTile(1, FULL - src.uniform));
|
|
306
|
-
} else {
|
|
307
|
-
const out2 = new Uint8ClampedArray(TILE_PIXELS);
|
|
308
|
-
for (let i = 0; i < out2.length; i++) out2[i] = FULL - src.data[i];
|
|
309
|
-
const t2 = finishTile(out2, 1, 0);
|
|
310
|
-
if (t2) tiles.set(key, t2);
|
|
311
|
-
}
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
const w = Math.min(TILE, width - tx * TILE);
|
|
315
|
-
const h = Math.min(TILE, height - ty * TILE);
|
|
316
|
-
const out = new Uint8ClampedArray(TILE_PIXELS);
|
|
317
|
-
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) out[y * TILE + x] = FULL - (src ? src.data[y * TILE + x] : 0);
|
|
318
|
-
const t = finishTile(out, 1, 0);
|
|
319
|
-
if (t) tiles.set(key, t);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return new TiledSelectionMask(width, height, new TiledSurface(1, tiles, 0));
|
|
323
|
-
}
|
|
324
|
-
function resizeSelection(mask, width, height) {
|
|
325
|
-
if (mask.width === width && mask.height === height) return mask;
|
|
326
|
-
checkSize(width, height);
|
|
327
|
-
const out = new Uint8Array(width * height);
|
|
328
|
-
const sx = mask.width / width;
|
|
329
|
-
const sy = mask.height / height;
|
|
330
|
-
const src = mask.data;
|
|
331
|
-
const mw = mask.width;
|
|
332
|
-
for (let y = 0; y < height; y++) {
|
|
333
|
-
const fy = Math.min(mask.height - 1, Math.max(0, (y + 0.5) * sy - 0.5));
|
|
334
|
-
const y0 = Math.floor(fy);
|
|
335
|
-
const y1 = Math.min(mask.height - 1, y0 + 1);
|
|
336
|
-
const ty = fy - y0;
|
|
337
|
-
for (let x = 0; x < width; x++) {
|
|
338
|
-
const fx = Math.min(mw - 1, Math.max(0, (x + 0.5) * sx - 0.5));
|
|
339
|
-
const x0 = Math.floor(fx);
|
|
340
|
-
const x1 = Math.min(mw - 1, x0 + 1);
|
|
341
|
-
const tx = fx - x0;
|
|
342
|
-
const top = src[y0 * mw + x0] * (1 - tx) + src[y0 * mw + x1] * tx;
|
|
343
|
-
const bot = src[y1 * mw + x0] * (1 - tx) + src[y1 * mw + x1] * tx;
|
|
344
|
-
out[y * width + x] = Math.round(top * (1 - ty) + bot * ty);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return createSelectionMask(width, height, out);
|
|
348
|
-
}
|
|
349
|
-
function selectionFromPixels(pixels, options = {}) {
|
|
350
|
-
const { width, height, data } = pixels;
|
|
351
|
-
checkSize(width, height);
|
|
352
|
-
if (data.length < width * height * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
|
|
353
|
-
const channel = options.channel ?? "luminance";
|
|
354
|
-
const out = new Uint8Array(width * height);
|
|
355
|
-
const threshold = options.threshold;
|
|
356
|
-
for (let i = 0, p = 0; i < out.length; i++, p += 4) {
|
|
357
|
-
let v;
|
|
358
|
-
switch (channel) {
|
|
359
|
-
case "alpha":
|
|
360
|
-
v = data[p + 3];
|
|
361
|
-
break;
|
|
362
|
-
case "red":
|
|
363
|
-
v = data[p];
|
|
364
|
-
break;
|
|
365
|
-
case "green":
|
|
366
|
-
v = data[p + 1];
|
|
367
|
-
break;
|
|
368
|
-
case "blue":
|
|
369
|
-
v = data[p + 2];
|
|
370
|
-
break;
|
|
371
|
-
default:
|
|
372
|
-
v = Math.round((0.2126 * data[p] + 0.7152 * data[p + 1] + 0.0722 * data[p + 2]) * data[p + 3] / 255);
|
|
373
|
-
}
|
|
374
|
-
if (options.invert) v = 255 - v;
|
|
375
|
-
if (threshold !== void 0) v = v >= threshold ? 255 : 0;
|
|
376
|
-
out[i] = v;
|
|
377
|
-
}
|
|
378
|
-
return createSelectionMask(width, height, out);
|
|
379
|
-
}
|
|
380
|
-
function selectionToPixels(mask, as = "gray") {
|
|
381
|
-
const out = new Uint8ClampedArray(mask.data.length * 4);
|
|
382
|
-
for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
|
|
383
|
-
const v = mask.data[i];
|
|
384
|
-
if (as === "gray") {
|
|
385
|
-
out[p] = out[p + 1] = out[p + 2] = v;
|
|
386
|
-
out[p + 3] = 255;
|
|
387
|
-
} else {
|
|
388
|
-
out[p] = out[p + 1] = out[p + 2] = 255;
|
|
389
|
-
out[p + 3] = v;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return { width: mask.width, height: mask.height, data: out };
|
|
393
|
-
}
|
|
394
|
-
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
395
|
-
var B64_INDEX = (() => {
|
|
396
|
-
const t = new Int16Array(128).fill(-1);
|
|
397
|
-
for (let i = 0; i < B64.length; i++) t[B64.charCodeAt(i)] = i;
|
|
398
|
-
return t;
|
|
399
|
-
})();
|
|
400
|
-
function toBase64(bytes, length) {
|
|
401
|
-
const parts = [];
|
|
402
|
-
let chunk = "";
|
|
403
|
-
for (let i = 0; i < length; i += 3) {
|
|
404
|
-
const a = bytes[i];
|
|
405
|
-
const b = i + 1 < length ? bytes[i + 1] : 0;
|
|
406
|
-
const c = i + 2 < length ? bytes[i + 2] : 0;
|
|
407
|
-
chunk += B64[a >> 2] + B64[(a & 3) << 4 | b >> 4];
|
|
408
|
-
chunk += i + 1 < length ? B64[(b & 15) << 2 | c >> 6] : "=";
|
|
409
|
-
chunk += i + 2 < length ? B64[c & 63] : "=";
|
|
410
|
-
if (chunk.length >= 8192) {
|
|
411
|
-
parts.push(chunk);
|
|
412
|
-
chunk = "";
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
parts.push(chunk);
|
|
416
|
-
return parts.join("");
|
|
417
|
-
}
|
|
418
|
-
function fromBase64(text) {
|
|
419
|
-
if (text.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
420
|
-
let pad = 0;
|
|
421
|
-
if (text.endsWith("==")) pad = 2;
|
|
422
|
-
else if (text.endsWith("=")) pad = 1;
|
|
423
|
-
const out = new Uint8Array(text.length / 4 * 3 - pad);
|
|
424
|
-
let o = 0;
|
|
425
|
-
const digit = (i, k) => {
|
|
426
|
-
const ch = text.charCodeAt(i + k);
|
|
427
|
-
if (ch === 61 && i + 4 === text.length && k >= 4 - pad) return 0;
|
|
428
|
-
const idx = ch < 128 ? B64_INDEX[ch] : -1;
|
|
429
|
-
if (idx < 0) throw new Error("Invalid base64 character");
|
|
430
|
-
return idx;
|
|
431
|
-
};
|
|
432
|
-
for (let i = 0; i < text.length; i += 4) {
|
|
433
|
-
const n = digit(i, 0) << 18 | digit(i, 1) << 12 | digit(i, 2) << 6 | digit(i, 3);
|
|
434
|
-
if (o < out.length) out[o++] = n >> 16 & 255;
|
|
435
|
-
if (o < out.length) out[o++] = n >> 8 & 255;
|
|
436
|
-
if (o < out.length) out[o++] = n & 255;
|
|
437
|
-
}
|
|
438
|
-
return out;
|
|
439
|
-
}
|
|
440
|
-
function encodeSelectionRle(data) {
|
|
441
|
-
let buf = new Uint8Array(64);
|
|
442
|
-
let len = 0;
|
|
443
|
-
const push = (b) => {
|
|
444
|
-
if (len === buf.length) {
|
|
445
|
-
const next = new Uint8Array(buf.length * 2);
|
|
446
|
-
next.set(buf);
|
|
447
|
-
buf = next;
|
|
448
|
-
}
|
|
449
|
-
buf[len++] = b;
|
|
450
|
-
};
|
|
451
|
-
let i = 0;
|
|
452
|
-
while (i < data.length) {
|
|
453
|
-
const v = data[i];
|
|
454
|
-
let j = i + 1;
|
|
455
|
-
while (j < data.length && data[j] === v) j++;
|
|
456
|
-
push(v);
|
|
457
|
-
let run = j - i;
|
|
458
|
-
while (run >= 128) {
|
|
459
|
-
push(run & 127 | 128);
|
|
460
|
-
run = Math.floor(run / 128);
|
|
461
|
-
}
|
|
462
|
-
push(run);
|
|
463
|
-
i = j;
|
|
464
|
-
}
|
|
465
|
-
return "r1:" + toBase64(buf, len);
|
|
466
|
-
}
|
|
467
|
-
function decodeSelectionRle(rle, length) {
|
|
468
|
-
if (typeof rle !== "string" || !rle.startsWith("r1:")) throw new Error("Unknown selection encoding");
|
|
469
|
-
const bytes = fromBase64(rle.slice(3));
|
|
470
|
-
const out = new Uint8Array(length);
|
|
471
|
-
let o = 0;
|
|
472
|
-
let i = 0;
|
|
473
|
-
while (i < bytes.length) {
|
|
474
|
-
const v = bytes[i++];
|
|
475
|
-
let run = 0;
|
|
476
|
-
let scale = 1;
|
|
477
|
-
for (; ; ) {
|
|
478
|
-
if (i >= bytes.length) throw new Error("Truncated selection run");
|
|
479
|
-
const b = bytes[i++];
|
|
480
|
-
run += (b & 127) * scale;
|
|
481
|
-
if (b < 128) break;
|
|
482
|
-
scale *= 128;
|
|
483
|
-
if (scale > 2 ** 35) throw new Error("Selection run too long");
|
|
484
|
-
}
|
|
485
|
-
if (run === 0 || o + run > length) throw new Error("Selection runs do not match its size");
|
|
486
|
-
if (v !== 0) out.fill(v, o, o + run);
|
|
487
|
-
o += run;
|
|
488
|
-
}
|
|
489
|
-
if (o !== length) throw new Error("Selection runs do not match its size");
|
|
490
|
-
return out;
|
|
491
|
-
}
|
|
492
|
-
function serializeSelection(mask) {
|
|
493
|
-
return { width: mask.width, height: mask.height, rle: encodeSelectionRle(mask.data) };
|
|
494
|
-
}
|
|
495
|
-
function deserializeSelection(value) {
|
|
496
|
-
if (!value || typeof value !== "object") throw new Error("selection must be an object");
|
|
497
|
-
const v = value;
|
|
498
|
-
const width = v.width;
|
|
499
|
-
const height = v.height;
|
|
500
|
-
checkSize(width, height);
|
|
501
|
-
return createSelectionMask(width, height, decodeSelectionRle(v.rle, width * height));
|
|
502
|
-
}
|
|
503
|
-
function hashMaskContent(content) {
|
|
504
|
-
let h = 2166136261;
|
|
505
|
-
if (typeof content === "string") {
|
|
506
|
-
for (let i = 0; i < content.length; i++) {
|
|
507
|
-
h ^= content.charCodeAt(i);
|
|
508
|
-
h = Math.imul(h, 16777619);
|
|
509
|
-
}
|
|
510
|
-
} else {
|
|
511
|
-
for (let i = 0; i < content.length; i++) {
|
|
512
|
-
h ^= content[i];
|
|
513
|
-
h = Math.imul(h, 16777619);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return `${(h >>> 0).toString(16)}:${content.length}`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// src/image-editor/selection/modify.ts
|
|
520
|
-
var EMPTY_DISTANCE = 1e10;
|
|
521
|
-
function distanceTransform(inside, width, height) {
|
|
522
|
-
const n = width * height;
|
|
523
|
-
const out = new Float32Array(n);
|
|
524
|
-
let any = false;
|
|
525
|
-
let all = true;
|
|
526
|
-
for (let i = 0; i < n; i++) {
|
|
527
|
-
if (inside[i]) any = true;
|
|
528
|
-
else all = false;
|
|
529
|
-
}
|
|
530
|
-
if (!any) return out.fill(EMPTY_DISTANCE);
|
|
531
|
-
if (all) return out;
|
|
532
|
-
const NONE = 4294967295;
|
|
533
|
-
const vertical = new Uint32Array(n).fill(NONE);
|
|
534
|
-
for (let x = 0; x < width; x++) {
|
|
535
|
-
let nearest = -1;
|
|
536
|
-
for (let y = 0; y < height; y++) {
|
|
537
|
-
const i = y * width + x;
|
|
538
|
-
if (inside[i]) nearest = y;
|
|
539
|
-
if (nearest >= 0) vertical[i] = y - nearest;
|
|
540
|
-
}
|
|
541
|
-
nearest = -1;
|
|
542
|
-
for (let y = height - 1; y >= 0; y--) {
|
|
543
|
-
const i = y * width + x;
|
|
544
|
-
if (vertical[i] === 0) nearest = y;
|
|
545
|
-
if (nearest >= 0) vertical[i] = Math.min(vertical[i], nearest - y);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const sites = new Int32Array(width);
|
|
549
|
-
const starts = new Float64Array(width);
|
|
550
|
-
for (let y = 0; y < height; y++) {
|
|
551
|
-
const row = y * width;
|
|
552
|
-
let count = 0;
|
|
553
|
-
for (let q = 0; q < width; q++) {
|
|
554
|
-
const dq = vertical[row + q];
|
|
555
|
-
if (dq === NONE) continue;
|
|
556
|
-
const fq = dq * dq;
|
|
557
|
-
let start = -Infinity;
|
|
558
|
-
while (count > 0) {
|
|
559
|
-
const p = sites[count - 1];
|
|
560
|
-
const dp = vertical[row + p];
|
|
561
|
-
start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;
|
|
562
|
-
if (start > starts[count - 1]) break;
|
|
563
|
-
count--;
|
|
564
|
-
}
|
|
565
|
-
if (count === 0) start = -Infinity;
|
|
566
|
-
sites[count] = q;
|
|
567
|
-
starts[count] = start;
|
|
568
|
-
count++;
|
|
569
|
-
}
|
|
570
|
-
if (count === 0) {
|
|
571
|
-
out.fill(EMPTY_DISTANCE, row, row + width);
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
let k = 0;
|
|
575
|
-
for (let x = 0; x < width; x++) {
|
|
576
|
-
while (k + 1 < count && starts[k + 1] < x) k++;
|
|
577
|
-
const site = sites[k];
|
|
578
|
-
const dx = x - site;
|
|
579
|
-
const dv = vertical[row + site];
|
|
580
|
-
out[row + x] = Math.sqrt(dx * dx + dv * dv);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return out;
|
|
584
|
-
}
|
|
585
|
-
function thresholdInside(m, threshold = 0.5) {
|
|
586
|
-
const out = new Uint8Array(m.length);
|
|
587
|
-
for (let i = 0; i < m.length; i++) out[i] = m[i] >= threshold ? 1 : 0;
|
|
588
|
-
return out;
|
|
589
|
-
}
|
|
590
|
-
function expandCoverage(m, w, h, r) {
|
|
591
|
-
const d = distanceTransform(thresholdInside(m), w, h);
|
|
592
|
-
const out = new Float32Array(m.length);
|
|
593
|
-
for (let i = 0; i < out.length; i++) out[i] = Math.max(m[i], Math.min(1, Math.max(0, r + 1 - d[i])));
|
|
594
|
-
return out;
|
|
595
|
-
}
|
|
596
|
-
function contractCoverage(m, w, h, r) {
|
|
597
|
-
const inv = new Float32Array(m.length);
|
|
598
|
-
for (let i = 0; i < inv.length; i++) inv[i] = 1 - m[i];
|
|
599
|
-
const e = expandCoverage(inv, w, h, r);
|
|
600
|
-
for (let i = 0; i < e.length; i++) e[i] = 1 - e[i];
|
|
601
|
-
return e;
|
|
602
|
-
}
|
|
603
|
-
function borderCoverage(m, w, h, r) {
|
|
604
|
-
const inside = thresholdInside(m);
|
|
605
|
-
const outside = new Uint8Array(inside.length);
|
|
606
|
-
for (let i = 0; i < inside.length; i++) outside[i] = inside[i] ? 0 : 1;
|
|
607
|
-
const out = new Float32Array(m.length);
|
|
608
|
-
if (!(r > 0)) return out;
|
|
609
|
-
const toInside = distanceTransform(inside, w, h);
|
|
610
|
-
const toOutside = distanceTransform(outside, w, h);
|
|
611
|
-
for (let i = 0; i < out.length; i++) {
|
|
612
|
-
const d = inside[i] ? toOutside[i] : toInside[i];
|
|
613
|
-
out[i] = Math.min(1, Math.max(0, 1 - (d - 1) / r));
|
|
614
|
-
}
|
|
615
|
-
return out;
|
|
616
|
-
}
|
|
617
|
-
function boxLineReplicate(src, srcOff, stride, len, r, dst) {
|
|
618
|
-
const at = (i) => src[srcOff + Math.min(len - 1, Math.max(0, i)) * stride];
|
|
619
|
-
let sum = 0;
|
|
620
|
-
for (let i = -r; i <= r; i++) sum += at(i);
|
|
621
|
-
const k = 2 * r + 1;
|
|
622
|
-
for (let i = 0; i < len; i++) {
|
|
623
|
-
dst[i] = sum / k;
|
|
624
|
-
sum += at(i + r + 1) - at(i - r);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function smoothCoverage(m, w, h, r) {
|
|
628
|
-
const rr = Number.isFinite(r) ? Math.max(0, Math.round(r)) : 0;
|
|
629
|
-
const src = Float32Array.from(m);
|
|
630
|
-
const tmp = new Float32Array(src.length);
|
|
631
|
-
const line = new Float32Array(Math.max(w, h));
|
|
632
|
-
if (rr > 0) {
|
|
633
|
-
for (let y = 0; y < h; y++) {
|
|
634
|
-
boxLineReplicate(src, y * w, 1, w, rr, line);
|
|
635
|
-
tmp.set(line.subarray(0, w), y * w);
|
|
636
|
-
}
|
|
637
|
-
for (let x = 0; x < w; x++) {
|
|
638
|
-
boxLineReplicate(tmp, x, w, h, rr, line);
|
|
639
|
-
for (let y = 0; y < h; y++) src[y * w + x] = line[y];
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
for (let i = 0; i < src.length; i++) src[i] = src[i] >= 0.5 ? 1 : 0;
|
|
643
|
-
return src;
|
|
644
|
-
}
|
|
645
|
-
function featherCoverage(m, w, h, radius) {
|
|
646
|
-
const out = gaussianBlur(m, w, h, Number.isFinite(radius) ? Math.max(0, radius) : 0, "zero");
|
|
647
|
-
for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) < 1e-7) out[i] = 0;
|
|
648
|
-
return out;
|
|
649
|
-
}
|
|
650
|
-
function runningExtreme(src, off, stride, len, r, max, g, hb, dst) {
|
|
651
|
-
const k = 2 * r + 1;
|
|
652
|
-
const pick = max ? (a, b) => a > b ? a : b : (a, b) => a < b ? a : b;
|
|
653
|
-
const fill = max ? -Infinity : Infinity;
|
|
654
|
-
const plen = len + 2 * r;
|
|
655
|
-
const at = (i) => i < r || i >= r + len ? fill : src[off + (i - r) * stride];
|
|
656
|
-
for (let start = 0; start < plen; start += k) {
|
|
657
|
-
const end = Math.min(plen, start + k);
|
|
658
|
-
g[start] = at(start);
|
|
659
|
-
for (let i = start + 1; i < end; i++) g[i] = pick(g[i - 1], at(i));
|
|
660
|
-
hb[end - 1] = at(end - 1);
|
|
661
|
-
for (let i = end - 2; i >= start; i--) hb[i] = pick(hb[i + 1], at(i));
|
|
662
|
-
}
|
|
663
|
-
for (let i = 0; i < len; i++) {
|
|
664
|
-
const a = hb[i];
|
|
665
|
-
const b = g[i + 2 * r];
|
|
666
|
-
dst[i] = pick(a, b);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
function greyMorphology(src, w, h, r, grow) {
|
|
670
|
-
const out = Float32Array.from(src);
|
|
671
|
-
if (r <= 0) return out;
|
|
672
|
-
const n = Math.max(w, h);
|
|
673
|
-
const g = new Float32Array(n + 2 * r);
|
|
674
|
-
const hb = new Float32Array(n + 2 * r);
|
|
675
|
-
const line = new Float32Array(n);
|
|
676
|
-
for (let y = 0; y < h; y++) {
|
|
677
|
-
runningExtreme(out, y * w, 1, w, r, grow, g, hb, line);
|
|
678
|
-
out.set(line.subarray(0, w), y * w);
|
|
679
|
-
}
|
|
680
|
-
for (let x = 0; x < w; x++) {
|
|
681
|
-
runningExtreme(out, x, w, h, r, grow, g, hb, line);
|
|
682
|
-
for (let y = 0; y < h; y++) out[y * w + x] = line[y];
|
|
683
|
-
}
|
|
684
|
-
return out;
|
|
685
|
-
}
|
|
686
|
-
function shiftCoverage(src, w, h, amount) {
|
|
687
|
-
const mag = Math.abs(amount);
|
|
688
|
-
if (!Number.isFinite(mag) || mag < 1e-4) return Float32Array.from(src);
|
|
689
|
-
const grow = amount > 0;
|
|
690
|
-
const whole = Math.floor(mag);
|
|
691
|
-
const frac = mag - whole;
|
|
692
|
-
if (frac < 1e-4) return greyMorphology(src, w, h, whole, grow);
|
|
693
|
-
const lo = whole === 0 ? Float32Array.from(src) : greyMorphology(src, w, h, whole, grow);
|
|
694
|
-
const hi = greyMorphology(src, w, h, whole + 1, grow);
|
|
695
|
-
for (let i = 0; i < lo.length; i++) lo[i] += (hi[i] - lo[i]) * frac;
|
|
696
|
-
return lo;
|
|
697
|
-
}
|
|
698
|
-
var SELECTION_MODIFY_OPS = ["expand", "contract", "feather", "border", "smooth", "grow", "shrink"];
|
|
699
|
-
function wrap(mask, coverage) {
|
|
700
|
-
const out = new Uint8Array(coverage.length);
|
|
701
|
-
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
702
|
-
return createSelectionMask(mask.width, mask.height, out);
|
|
703
|
-
}
|
|
704
|
-
function modifySelection(mask, op, radius) {
|
|
705
|
-
const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
|
|
706
|
-
if (r === 0 && op !== "border") return mask;
|
|
707
|
-
const m = selectionToCoverage(mask);
|
|
708
|
-
const { width: w, height: h } = mask;
|
|
709
|
-
switch (op) {
|
|
710
|
-
case "expand":
|
|
711
|
-
case "grow":
|
|
712
|
-
return wrap(mask, expandCoverage(m, w, h, r));
|
|
713
|
-
case "contract":
|
|
714
|
-
case "shrink":
|
|
715
|
-
return wrap(mask, contractCoverage(m, w, h, r));
|
|
716
|
-
case "feather":
|
|
717
|
-
return wrap(mask, featherCoverage(m, w, h, r));
|
|
718
|
-
case "border":
|
|
719
|
-
return wrap(mask, borderCoverage(m, w, h, r));
|
|
720
|
-
case "smooth":
|
|
721
|
-
return wrap(mask, smoothCoverage(m, w, h, r));
|
|
722
|
-
default:
|
|
723
|
-
throw new Error(`Unknown selection modification "${op}"`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// src/image-editor/selection/refine.ts
|
|
728
|
-
var DEFAULT_REFINE_EDGE_PARAMS = {
|
|
729
|
-
radius: 0,
|
|
730
|
-
smartRadius: false,
|
|
731
|
-
smooth: 0,
|
|
732
|
-
feather: 0,
|
|
733
|
-
contrast: 0,
|
|
734
|
-
shiftEdge: 0
|
|
735
|
-
};
|
|
736
|
-
var GUIDED_FILTER_EPS = 1e-4;
|
|
737
|
-
var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
738
|
-
function normalizeRefineEdgeParams(p = {}) {
|
|
739
|
-
return {
|
|
740
|
-
radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
|
|
741
|
-
smartRadius: p.smartRadius === true,
|
|
742
|
-
smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
|
|
743
|
-
feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
|
|
744
|
-
contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
|
|
745
|
-
shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
|
|
749
|
-
var smoothSigma = (p) => p.smooth / 100 * 4;
|
|
750
|
-
var featherSigma = (p) => p.feather / 2;
|
|
751
|
-
function shiftAmount(p) {
|
|
752
|
-
const px = p.shiftEdge / 100 * Math.max(p.radius, 4);
|
|
753
|
-
return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;
|
|
754
|
-
}
|
|
755
|
-
function refineEdgeHalo(params = {}) {
|
|
756
|
-
const p = normalizeRefineEdgeParams(params);
|
|
757
|
-
const g = gfRadius(p);
|
|
758
|
-
const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
|
|
759
|
-
return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
|
|
760
|
-
}
|
|
761
|
-
function boxMean(src, w, h, r) {
|
|
762
|
-
const tmp = new Float32Array(w * h);
|
|
763
|
-
const pre = new Float64Array(Math.max(w, h) + 1);
|
|
764
|
-
for (let y = 0; y < h; y++) {
|
|
765
|
-
const row = y * w;
|
|
766
|
-
for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
|
|
767
|
-
for (let x = 0; x < w; x++) {
|
|
768
|
-
const lo = Math.max(0, x - r);
|
|
769
|
-
const hi = Math.min(w, x + r + 1);
|
|
770
|
-
tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
const out = new Float32Array(w * h);
|
|
774
|
-
for (let x = 0; x < w; x++) {
|
|
775
|
-
for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
|
|
776
|
-
for (let y = 0; y < h; y++) {
|
|
777
|
-
const lo = Math.max(0, y - r);
|
|
778
|
-
const hi = Math.min(h, y + r + 1);
|
|
779
|
-
out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return out;
|
|
783
|
-
}
|
|
784
|
-
function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
|
|
785
|
-
const n = w * h;
|
|
786
|
-
const [c0, c1, c2] = guide;
|
|
787
|
-
const prod = (a, b2) => {
|
|
788
|
-
const o = new Float32Array(n);
|
|
789
|
-
for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
|
|
790
|
-
return o;
|
|
791
|
-
};
|
|
792
|
-
const m0 = boxMean(c0, w, h, r);
|
|
793
|
-
const m1 = boxMean(c1, w, h, r);
|
|
794
|
-
const m2 = boxMean(c2, w, h, r);
|
|
795
|
-
const mp = boxMean(p, w, h, r);
|
|
796
|
-
const mip0 = boxMean(prod(c0, p), w, h, r);
|
|
797
|
-
const mip1 = boxMean(prod(c1, p), w, h, r);
|
|
798
|
-
const mip2 = boxMean(prod(c2, p), w, h, r);
|
|
799
|
-
const s00 = boxMean(prod(c0, c0), w, h, r);
|
|
800
|
-
const s01 = boxMean(prod(c0, c1), w, h, r);
|
|
801
|
-
const s02 = boxMean(prod(c0, c2), w, h, r);
|
|
802
|
-
const s11 = boxMean(prod(c1, c1), w, h, r);
|
|
803
|
-
const s12 = boxMean(prod(c1, c2), w, h, r);
|
|
804
|
-
const s22 = boxMean(prod(c2, c2), w, h, r);
|
|
805
|
-
const a0 = new Float32Array(n);
|
|
806
|
-
const a1 = new Float32Array(n);
|
|
807
|
-
const a2 = new Float32Array(n);
|
|
808
|
-
const b = new Float32Array(n);
|
|
809
|
-
for (let i = 0; i < n; i++) {
|
|
810
|
-
const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
|
|
811
|
-
const rr = s00[i] - u0 * u0 + eps;
|
|
812
|
-
const rg = s01[i] - u0 * u1;
|
|
813
|
-
const rb = s02[i] - u0 * u2;
|
|
814
|
-
const gg = s11[i] - u1 * u1 + eps;
|
|
815
|
-
const gb = s12[i] - u1 * u2;
|
|
816
|
-
const bb = s22[i] - u2 * u2 + eps;
|
|
817
|
-
const v0 = mip0[i] - u0 * up;
|
|
818
|
-
const v1 = mip1[i] - u1 * up;
|
|
819
|
-
const v2 = mip2[i] - u2 * up;
|
|
820
|
-
const i00 = gg * bb - gb * gb;
|
|
821
|
-
const i01 = gb * rb - rg * bb;
|
|
822
|
-
const i02 = rg * gb - gg * rb;
|
|
823
|
-
const i11 = rr * bb - rb * rb;
|
|
824
|
-
const i12 = rb * rg - rr * gb;
|
|
825
|
-
const i22 = rr * gg - rg * rg;
|
|
826
|
-
const det = rr * i00 + rg * i01 + rb * i02;
|
|
827
|
-
let x0 = 0, x1 = 0, x2 = 0;
|
|
828
|
-
if (Math.abs(det) >= 1e-30) {
|
|
829
|
-
x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
|
|
830
|
-
x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
|
|
831
|
-
x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
|
|
832
|
-
}
|
|
833
|
-
a0[i] = x0;
|
|
834
|
-
a1[i] = x1;
|
|
835
|
-
a2[i] = x2;
|
|
836
|
-
b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
|
|
837
|
-
}
|
|
838
|
-
const ma0 = boxMean(a0, w, h, r);
|
|
839
|
-
const ma1 = boxMean(a1, w, h, r);
|
|
840
|
-
const ma2 = boxMean(a2, w, h, r);
|
|
841
|
-
const mb = boxMean(b, w, h, r);
|
|
842
|
-
const out = new Float32Array(n);
|
|
843
|
-
for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
|
|
844
|
-
return out;
|
|
845
|
-
}
|
|
846
|
-
function edgeWidth(guide, w, h, r) {
|
|
847
|
-
const n = w * h;
|
|
848
|
-
const y = new Float32Array(n);
|
|
849
|
-
for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
|
|
850
|
-
const g = new Float32Array(n);
|
|
851
|
-
for (let yy = 0; yy < h; yy++) {
|
|
852
|
-
for (let xx = 0; xx < w; xx++) {
|
|
853
|
-
const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
|
|
854
|
-
const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
|
|
855
|
-
g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const gm = greyMorphology(g, w, h, r, true);
|
|
859
|
-
const hi = greyMorphology(y, w, h, r, true);
|
|
860
|
-
const lo = greyMorphology(y, w, h, r, false);
|
|
861
|
-
const out = new Float32Array(n);
|
|
862
|
-
for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
|
|
863
|
-
return out;
|
|
864
|
-
}
|
|
865
|
-
function refineCoverage(guide, m, w, h, params = {}, band = null) {
|
|
866
|
-
const p = normalizeRefineEdgeParams(params);
|
|
867
|
-
let a = Float32Array.from(m);
|
|
868
|
-
if ((p.radius > 0 || band) && guide) {
|
|
869
|
-
const n = w * h;
|
|
870
|
-
const boundary = new Uint8Array(n);
|
|
871
|
-
let any = false;
|
|
872
|
-
for (let y = 0; y < h; y++) {
|
|
873
|
-
for (let x = 0; x < w; x++) {
|
|
874
|
-
const i = y * w + x;
|
|
875
|
-
const v = m[i] >= 0.5;
|
|
876
|
-
const diff = x > 0 && m[i - 1] >= 0.5 !== v || x + 1 < w && m[i + 1] >= 0.5 !== v || y > 0 && m[i - w] >= 0.5 !== v || y + 1 < h && m[i + w] >= 0.5 !== v;
|
|
877
|
-
if (diff) {
|
|
878
|
-
boundary[i] = 1;
|
|
879
|
-
any = true;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
if (any || band) {
|
|
884
|
-
const dist = distanceTransform(boundary, w, h);
|
|
885
|
-
const r = band ? Math.max(4, gfRadius(p)) : gfRadius(p);
|
|
886
|
-
const q = guidedFilterColor(guide, m, w, h, r);
|
|
887
|
-
const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
|
|
888
|
-
for (let i = 0; i < n; i++) {
|
|
889
|
-
const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
|
|
890
|
-
const edge = p.radius > 0 ? Math.min(1, Math.max(0, reff + 1 - dist[i])) : 0;
|
|
891
|
-
const wgt = band ? Math.max(edge, Math.min(1, Math.max(0, band[i]))) : edge;
|
|
892
|
-
if (wgt > 0) {
|
|
893
|
-
const qi = Math.min(1, Math.max(0, q[i]));
|
|
894
|
-
a[i] = m[i] + (qi - m[i]) * wgt;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
const ss = smoothSigma(p);
|
|
900
|
-
if (ss >= 0.3) {
|
|
901
|
-
a = gaussianBlur(a, w, h, ss, "clamp");
|
|
902
|
-
const gain = 1 + ss;
|
|
903
|
-
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
904
|
-
}
|
|
905
|
-
a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
|
|
906
|
-
if (p.contrast > 0) {
|
|
907
|
-
const gain = 1 / (1 - 0.99 * p.contrast / 100);
|
|
908
|
-
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
909
|
-
}
|
|
910
|
-
const amount = shiftAmount(p);
|
|
911
|
-
if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
|
|
912
|
-
return a;
|
|
913
|
-
}
|
|
914
|
-
function guidePlanes(pixels, win) {
|
|
915
|
-
const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
|
|
916
|
-
const n = box.width * box.height;
|
|
917
|
-
const r = new Float32Array(n);
|
|
918
|
-
const g = new Float32Array(n);
|
|
919
|
-
const b = new Float32Array(n);
|
|
920
|
-
const d = pixels.data;
|
|
921
|
-
for (let y = 0; y < box.height; y++) {
|
|
922
|
-
for (let x = 0; x < box.width; x++) {
|
|
923
|
-
const i = y * box.width + x;
|
|
924
|
-
const p = ((box.y + y) * pixels.width + box.x + x) * 4;
|
|
925
|
-
r[i] = d[p] / 255;
|
|
926
|
-
g[i] = d[p + 1] / 255;
|
|
927
|
-
b[i] = d[p + 2] / 255;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return [r, g, b];
|
|
931
|
-
}
|
|
932
|
-
function refineSelection(pixels, mask, params = {}, refineBrush = null) {
|
|
933
|
-
const p = normalizeRefineEdgeParams(params);
|
|
934
|
-
if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
|
|
935
|
-
throw new Error("Refine Edge needs pixels the size of the selection");
|
|
936
|
-
}
|
|
937
|
-
if (!mask.bounds) return mask;
|
|
938
|
-
const brush = refineBrush?.bounds ? refineBrush : null;
|
|
939
|
-
const halo = refineEdgeHalo(p) + (brush ? 2 * Math.max(4, gfRadius(p)) : 0);
|
|
940
|
-
const b = brush ? {
|
|
941
|
-
x: Math.min(mask.bounds.x, brush.bounds.x),
|
|
942
|
-
y: Math.min(mask.bounds.y, brush.bounds.y),
|
|
943
|
-
r: Math.max(mask.bounds.x + mask.bounds.width, brush.bounds.x + brush.bounds.width),
|
|
944
|
-
btm: Math.max(mask.bounds.y + mask.bounds.height, brush.bounds.y + brush.bounds.height)
|
|
945
|
-
} : { x: mask.bounds.x, y: mask.bounds.y, r: mask.bounds.x + mask.bounds.width, btm: mask.bounds.y + mask.bounds.height };
|
|
946
|
-
const x0 = Math.max(0, b.x - halo);
|
|
947
|
-
const y0 = Math.max(0, b.y - halo);
|
|
948
|
-
const x1 = Math.min(mask.width, b.r + halo);
|
|
949
|
-
const y1 = Math.min(mask.height, b.btm + halo);
|
|
950
|
-
const ww = x1 - x0;
|
|
951
|
-
const wh = y1 - y0;
|
|
952
|
-
const m = new Float32Array(ww * wh);
|
|
953
|
-
for (let y = 0; y < wh; y++) {
|
|
954
|
-
const src = (y0 + y) * mask.width + x0;
|
|
955
|
-
for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
|
|
956
|
-
}
|
|
957
|
-
const guide = pixels && (p.radius > 0 || brush) ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
|
|
958
|
-
let band = null;
|
|
959
|
-
if (brush) {
|
|
960
|
-
band = new Float32Array(ww * wh);
|
|
961
|
-
const bd = brush.data;
|
|
962
|
-
for (let y = 0; y < wh; y++) {
|
|
963
|
-
const src = (y0 + y) * mask.width + x0;
|
|
964
|
-
for (let x = 0; x < ww; x++) band[y * ww + x] = bd[src + x] / 255;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
const refined = refineCoverage(guide, m, ww, wh, p, band);
|
|
968
|
-
const out = new Uint8Array(mask.width * mask.height);
|
|
969
|
-
for (let y = 0; y < wh; y++) {
|
|
970
|
-
const dst = (y0 + y) * mask.width + x0;
|
|
971
|
-
for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
|
|
972
|
-
}
|
|
973
|
-
return createSelectionMask(mask.width, mask.height, out);
|
|
974
|
-
}
|
|
975
|
-
function decontaminateColors(rgba, coverage, w, h, amount, radius = 8) {
|
|
976
|
-
const out = Uint8ClampedArray.from(rgba);
|
|
977
|
-
const n = w * h;
|
|
978
|
-
const k = Math.min(1, Math.max(0, amount));
|
|
979
|
-
if (k === 0) return out;
|
|
980
|
-
const r = Math.max(2, Math.round(radius));
|
|
981
|
-
const fgW = new Float32Array(n);
|
|
982
|
-
const bgW = new Float32Array(n);
|
|
983
|
-
for (let i = 0; i < n; i++) {
|
|
984
|
-
fgW[i] = coverage[i] >= 250 ? 1 : 0;
|
|
985
|
-
bgW[i] = coverage[i] <= 5 ? 1 : 0;
|
|
986
|
-
}
|
|
987
|
-
const meanOf = (weights, radiusPx) => {
|
|
988
|
-
const wm = boxMean(weights, w, h, radiusPx);
|
|
989
|
-
const planes = [0, 1, 2].map((c) => {
|
|
990
|
-
const v = new Float32Array(n);
|
|
991
|
-
for (let i = 0; i < n; i++) v[i] = rgba[i * 4 + c] * weights[i];
|
|
992
|
-
const m = boxMean(v, w, h, radiusPx);
|
|
993
|
-
for (let i = 0; i < n; i++) m[i] = wm[i] > 1e-6 ? m[i] / wm[i] : NaN;
|
|
994
|
-
return m;
|
|
995
|
-
});
|
|
996
|
-
return { weight: wm, planes };
|
|
997
|
-
};
|
|
998
|
-
const fg = meanOf(fgW, r);
|
|
999
|
-
const fgWide = meanOf(fgW, r * 3);
|
|
1000
|
-
const bg = meanOf(bgW, r);
|
|
1001
|
-
for (let i = 0; i < n; i++) {
|
|
1002
|
-
const c = coverage[i];
|
|
1003
|
-
if (c <= 5 || c >= 250) continue;
|
|
1004
|
-
const a = c / 255;
|
|
1005
|
-
for (let ch = 0; ch < 3; ch++) {
|
|
1006
|
-
const C = rgba[i * 4 + ch];
|
|
1007
|
-
const B = bg.planes[ch][i];
|
|
1008
|
-
const Fm = Number.isNaN(fg.planes[ch][i]) ? fgWide.planes[ch][i] : fg.planes[ch][i];
|
|
1009
|
-
let F;
|
|
1010
|
-
if (a >= 0.5 && !Number.isNaN(B)) F = (C - (1 - a) * B) / a;
|
|
1011
|
-
else if (!Number.isNaN(Fm)) F = Fm;
|
|
1012
|
-
else F = C;
|
|
1013
|
-
out[i * 4 + ch] = Math.round(C + k * (Math.min(255, Math.max(0, F)) - C));
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return out;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
export {
|
|
1020
|
-
SELECTION_MODES,
|
|
1021
|
-
isSelectionMode,
|
|
1022
|
-
parseSelectionMode,
|
|
1023
|
-
computeSelectionBounds,
|
|
1024
|
-
selectionFromTiles,
|
|
1025
|
-
createSelectionMask,
|
|
1026
|
-
selectionFromRegion,
|
|
1027
|
-
fullSelection,
|
|
1028
|
-
isSelectionEmpty,
|
|
1029
|
-
selectionValueAt,
|
|
1030
|
-
coverageToByte,
|
|
1031
|
-
selectionFromCoverage,
|
|
1032
|
-
selectionToCoverage,
|
|
1033
|
-
selectionArea,
|
|
1034
|
-
combineSelections,
|
|
1035
|
-
invertSelection,
|
|
1036
|
-
resizeSelection,
|
|
1037
|
-
selectionFromPixels,
|
|
1038
|
-
selectionToPixels,
|
|
1039
|
-
encodeSelectionRle,
|
|
1040
|
-
decodeSelectionRle,
|
|
1041
|
-
serializeSelection,
|
|
1042
|
-
deserializeSelection,
|
|
1043
|
-
hashMaskContent,
|
|
1044
|
-
EMPTY_DISTANCE,
|
|
1045
|
-
distanceTransform,
|
|
1046
|
-
expandCoverage,
|
|
1047
|
-
contractCoverage,
|
|
1048
|
-
borderCoverage,
|
|
1049
|
-
smoothCoverage,
|
|
1050
|
-
featherCoverage,
|
|
1051
|
-
greyMorphology,
|
|
1052
|
-
shiftCoverage,
|
|
1053
|
-
SELECTION_MODIFY_OPS,
|
|
1054
|
-
modifySelection,
|
|
1055
|
-
DEFAULT_REFINE_EDGE_PARAMS,
|
|
1056
|
-
GUIDED_FILTER_EPS,
|
|
1057
|
-
normalizeRefineEdgeParams,
|
|
1058
|
-
refineEdgeHalo,
|
|
1059
|
-
boxMean,
|
|
1060
|
-
guidedFilterColor,
|
|
1061
|
-
edgeWidth,
|
|
1062
|
-
refineCoverage,
|
|
1063
|
-
guidePlanes,
|
|
1064
|
-
refineSelection,
|
|
1065
|
-
decontaminateColors
|
|
1066
|
-
};
|