@pixerate/editor 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -2
- package/THIRD_PARTY_NOTICES.md +80 -0
- package/dist/agent/index.cjs +10 -8
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +4 -3
- package/dist/agent/index.d.ts +4 -3
- package/dist/agent/index.js +4 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +4437 -411
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +934 -4
- package/dist/canvas/index.d.ts +934 -4
- package/dist/canvas/index.js +4309 -283
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-4BVP64JJ.cjs +1406 -0
- package/dist/chunk-4BVP64JJ.cjs.map +1 -0
- package/dist/chunk-664LQYFJ.js +115 -0
- package/dist/chunk-664LQYFJ.js.map +1 -0
- package/dist/chunk-7WISYZFO.cjs +740 -0
- package/dist/chunk-7WISYZFO.cjs.map +1 -0
- package/dist/chunk-AL6JMGWW.cjs +783 -0
- package/dist/chunk-AL6JMGWW.cjs.map +1 -0
- package/dist/chunk-BVP3TSLH.cjs +69 -0
- package/dist/chunk-BVP3TSLH.cjs.map +1 -0
- package/dist/{chunk-RIKBJLS2.cjs → chunk-C7P22VIP.cjs} +2001 -279
- package/dist/chunk-C7P22VIP.cjs.map +1 -0
- package/dist/chunk-D6FH2YAX.cjs +131 -0
- package/dist/chunk-D6FH2YAX.cjs.map +1 -0
- package/dist/chunk-ECCCYKGL.js +740 -0
- package/dist/chunk-ECCCYKGL.js.map +1 -0
- package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
- package/dist/chunk-FD4LPFAE.cjs.map +1 -0
- package/dist/{chunk-QBJ22YZR.js → chunk-FH6F67DN.js} +1999 -277
- package/dist/chunk-FH6F67DN.js.map +1 -0
- package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
- package/dist/chunk-FPENXAEJ.js.map +1 -0
- package/dist/chunk-HLIMTC6N.js +1 -0
- package/dist/chunk-HLIMTC6N.js.map +1 -0
- package/dist/chunk-HRMEDT2L.js +1406 -0
- package/dist/chunk-HRMEDT2L.js.map +1 -0
- package/dist/{chunk-QGHGJ56O.js → chunk-IXHSYOVY.js} +2518 -122
- package/dist/chunk-IXHSYOVY.js.map +1 -0
- package/dist/chunk-MKA3AHLV.cjs +256 -0
- package/dist/chunk-MKA3AHLV.cjs.map +1 -0
- package/dist/chunk-NAXVVS5I.js +783 -0
- package/dist/chunk-NAXVVS5I.js.map +1 -0
- package/dist/chunk-O5RYH2PG.js +69 -0
- package/dist/chunk-O5RYH2PG.js.map +1 -0
- package/dist/chunk-OXSV64JX.js +689 -0
- package/dist/chunk-OXSV64JX.js.map +1 -0
- package/dist/chunk-P3RQ5UJE.cjs +689 -0
- package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
- package/dist/chunk-P6ODPZ4W.js +256 -0
- package/dist/chunk-P6ODPZ4W.js.map +1 -0
- package/dist/chunk-PL4JB3BF.cjs +1 -0
- package/dist/chunk-PL4JB3BF.cjs.map +1 -0
- package/dist/{chunk-GVSMRPHB.cjs → chunk-PO6A6QQH.cjs} +2607 -211
- package/dist/chunk-PO6A6QQH.cjs.map +1 -0
- package/dist/{chunk-JJDLPEWL.js → chunk-QGYDWPRM.js} +2 -2
- package/dist/chunk-RR7F5GUM.js +5726 -0
- package/dist/chunk-RR7F5GUM.js.map +1 -0
- package/dist/chunk-TNNJXF46.cjs +115 -0
- package/dist/chunk-TNNJXF46.cjs.map +1 -0
- package/dist/chunk-U5HZSFCS.js +131 -0
- package/dist/chunk-U5HZSFCS.js.map +1 -0
- package/dist/{chunk-MJZBNWG7.js → chunk-UAKDG62Z.js} +309 -581
- package/dist/chunk-UAKDG62Z.js.map +1 -0
- package/dist/{chunk-CL37JGFW.cjs → chunk-V3CHHWOE.cjs} +4 -4
- package/dist/{chunk-CL37JGFW.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
- package/dist/chunk-WSQKHXW7.cjs +5726 -0
- package/dist/chunk-WSQKHXW7.cjs.map +1 -0
- package/dist/{chunk-AA4ZEVCC.cjs → chunk-X4N4WUKB.cjs} +336 -608
- package/dist/chunk-X4N4WUKB.cjs.map +1 -0
- package/dist/commands/index.cjs +74 -1
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +38 -353
- package/dist/commands/index.d.ts +38 -353
- package/dist/commands/index.js +73 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/comment-store-BEMgVlAA.d.cts +135 -0
- package/dist/comment-store-BEMgVlAA.d.ts +135 -0
- package/dist/compare/index.cjs +15 -0
- package/dist/compare/index.cjs.map +1 -0
- package/dist/compare/index.d.cts +58 -0
- package/dist/compare/index.d.ts +58 -0
- package/dist/compare/index.js +15 -0
- package/dist/compare/index.js.map +1 -0
- package/dist/controller-BIJiNoVs.d.cts +1842 -0
- package/dist/controller-BIJiNoVs.d.ts +1842 -0
- package/dist/documents-CHT1U5Vs.d.ts +86 -0
- package/dist/documents-Gp7wb0Me.d.cts +86 -0
- package/dist/extensions/index.cjs +100 -5
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +16 -4
- package/dist/extensions/index.d.ts +16 -4
- package/dist/extensions/index.js +100 -5
- package/dist/generation/index.cjs +2317 -0
- package/dist/generation/index.cjs.map +1 -0
- package/dist/generation/index.d.cts +1015 -0
- package/dist/generation/index.d.ts +1015 -0
- package/dist/generation/index.js +2317 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/grammar/index.cjs +48 -3
- package/dist/grammar/index.cjs.map +1 -1
- package/dist/grammar/index.d.cts +322 -100
- package/dist/grammar/index.d.ts +322 -100
- package/dist/grammar/index.js +54 -9
- package/dist/image-editor/index.cjs +4392 -481
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +1249 -97
- package/dist/image-editor/index.d.ts +1249 -97
- package/dist/image-editor/index.js +4281 -370
- package/dist/image-editor/index.js.map +1 -1
- package/dist/index-7a6bHTLy.d.ts +1754 -0
- package/dist/index-Csd5qI8u.d.cts +1754 -0
- package/dist/index.cjs +1682 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -21
- package/dist/index.d.ts +118 -21
- package/dist/index.js +1630 -47
- package/dist/index.js.map +1 -1
- package/dist/mention-text-CDnCFd3c.d.cts +53 -0
- package/dist/mention-text-CDnCFd3c.d.ts +53 -0
- package/dist/migrate-iTlm3a8h.d.cts +112 -0
- package/dist/migrate-iTlm3a8h.d.ts +112 -0
- package/dist/model-C1zApQM8.d.cts +219 -0
- package/dist/model-C1zApQM8.d.ts +219 -0
- package/dist/persistence/index.cjs +676 -0
- package/dist/persistence/index.cjs.map +1 -0
- package/dist/persistence/index.d.cts +230 -0
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +676 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
- package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.cts} +12 -1
- package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.ts} +12 -1
- package/dist/registry-D32LpIc4.d.cts +354 -0
- package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
- package/dist/registry-evKTP4ls.d.ts +354 -0
- package/dist/schema-DYDGdYL9.d.cts +41 -0
- package/dist/schema-DYDGdYL9.d.ts +41 -0
- package/dist/serializers/index.cjs +12 -3
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +40 -12
- package/dist/serializers/index.d.ts +40 -12
- package/dist/serializers/index.js +12 -3
- package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
- package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
- package/dist/spreadsheet/index.cjs +161 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +611 -20
- package/dist/spreadsheet/index.d.ts +611 -20
- package/dist/spreadsheet/index.js +168 -10
- package/dist/storage-BpFfspHm.d.cts +120 -0
- package/dist/storage-BpFfspHm.d.ts +120 -0
- package/dist/tokens/index.cjs +115 -0
- package/dist/tokens/index.cjs.map +1 -0
- package/dist/tokens/index.d.cts +212 -0
- package/dist/tokens/index.d.ts +212 -0
- package/dist/tokens/index.js +115 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
- package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
- package/dist/{track-changes-BSF58RK0.d.cts → track-changes-D_VAPELi.d.cts} +110 -144
- package/dist/{track-changes-BSF58RK0.d.ts → track-changes-Db_fGEvx.d.ts} +110 -144
- package/package.json +21 -1
- package/dist/chunk-4SUNJZWB.cjs.map +0 -1
- package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
- package/dist/chunk-AXV2ZW34.cjs +0 -118
- package/dist/chunk-AXV2ZW34.cjs.map +0 -1
- package/dist/chunk-B2ZRLN4S.js +0 -118
- package/dist/chunk-B2ZRLN4S.js.map +0 -1
- package/dist/chunk-GVSMRPHB.cjs.map +0 -1
- package/dist/chunk-K3R5NG2D.js +0 -3100
- package/dist/chunk-K3R5NG2D.js.map +0 -1
- package/dist/chunk-MJZBNWG7.js.map +0 -1
- package/dist/chunk-OYXSKQFR.js.map +0 -1
- package/dist/chunk-QBJ22YZR.js.map +0 -1
- package/dist/chunk-QGHGJ56O.js.map +0 -1
- package/dist/chunk-RIKBJLS2.cjs.map +0 -1
- package/dist/chunk-XDMXXC4Z.cjs +0 -3100
- package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
- package/dist/controller-pePCJLT1.d.cts +0 -959
- package/dist/controller-pePCJLT1.d.ts +0 -959
- package/dist/index-CCr_z9kF.d.cts +0 -899
- package/dist/index-Ck6ajwXh.d.ts +0 -899
- /package/dist/{chunk-JJDLPEWL.js.map → chunk-QGYDWPRM.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadDocument
|
|
3
|
+
} from "../chunk-U5HZSFCS.js";
|
|
4
|
+
|
|
1
5
|
// src/image-editor/color.ts
|
|
2
6
|
var LEGACY_PROCESS_VERSION = 1;
|
|
3
7
|
var LATEST_PROCESS_VERSION = 2;
|
|
@@ -231,284 +235,232 @@ function sampleToneLut(lut, headroom, y) {
|
|
|
231
235
|
return lut[i] + (lut[i + 1] - lut[i]) * (f - i);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
// src/image-editor/
|
|
235
|
-
|
|
236
|
-
|
|
238
|
+
// src/image-editor/blend.ts
|
|
239
|
+
var BLEND_MODES = [
|
|
240
|
+
{ id: "normal", label: "Normal", canvas: "source-over" },
|
|
241
|
+
{ id: "darken", label: "Darken", canvas: "darken" },
|
|
242
|
+
{ id: "multiply", label: "Multiply", canvas: "multiply" },
|
|
243
|
+
{ id: "color-burn", label: "Color Burn", canvas: "color-burn" },
|
|
244
|
+
{ id: "linear-burn", label: "Linear Burn", canvas: null },
|
|
245
|
+
{ id: "darker-color", label: "Darker Color", canvas: null },
|
|
246
|
+
{ id: "lighten", label: "Lighten", canvas: "lighten" },
|
|
247
|
+
{ id: "screen", label: "Screen", canvas: "screen" },
|
|
248
|
+
{ id: "color-dodge", label: "Color Dodge", canvas: "color-dodge" },
|
|
249
|
+
{ id: "linear-dodge", label: "Linear Dodge (Add)", canvas: null },
|
|
250
|
+
{ id: "lighter-color", label: "Lighter Color", canvas: null },
|
|
251
|
+
{ id: "overlay", label: "Overlay", canvas: "overlay" },
|
|
252
|
+
// Canvas soft-light is the W3C formula, not Photoshop's: use the scalar path.
|
|
253
|
+
{ id: "soft-light", label: "Soft Light", canvas: null },
|
|
254
|
+
{ id: "hard-light", label: "Hard Light", canvas: "hard-light" },
|
|
255
|
+
{ id: "vivid-light", label: "Vivid Light", canvas: null },
|
|
256
|
+
{ id: "linear-light", label: "Linear Light", canvas: null },
|
|
257
|
+
{ id: "pin-light", label: "Pin Light", canvas: null },
|
|
258
|
+
{ id: "hard-mix", label: "Hard Mix", canvas: null },
|
|
259
|
+
{ id: "difference", label: "Difference", canvas: "difference" },
|
|
260
|
+
{ id: "exclusion", label: "Exclusion", canvas: "exclusion" },
|
|
261
|
+
{ id: "subtract", label: "Subtract", canvas: null },
|
|
262
|
+
{ id: "divide", label: "Divide", canvas: null },
|
|
263
|
+
{ id: "hue", label: "Hue", canvas: "hue" },
|
|
264
|
+
{ id: "saturation", label: "Saturation", canvas: "saturation" },
|
|
265
|
+
{ id: "color", label: "Color", canvas: "color" },
|
|
266
|
+
{ id: "luminosity", label: "Luminosity", canvas: "luminosity" }
|
|
267
|
+
];
|
|
268
|
+
var BLEND_INFO = new Map(BLEND_MODES.map((m) => [m.id, m]));
|
|
269
|
+
function isBlendMode(value) {
|
|
270
|
+
return typeof value === "string" && BLEND_INFO.has(value);
|
|
237
271
|
}
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
272
|
+
function normalizeBlendMode(value) {
|
|
273
|
+
return isBlendMode(value) ? value : "normal";
|
|
274
|
+
}
|
|
275
|
+
function canvasCompositeOperation(mode) {
|
|
276
|
+
return BLEND_INFO.get(mode ?? "normal")?.canvas ?? null;
|
|
277
|
+
}
|
|
278
|
+
function isSeparableBlendMode(mode) {
|
|
279
|
+
return !(mode === "hue" || mode === "saturation" || mode === "color" || mode === "luminosity" || mode === "darker-color" || mode === "lighter-color");
|
|
280
|
+
}
|
|
281
|
+
function colorBurn(cb, cs) {
|
|
282
|
+
if (cb >= 1) return 1;
|
|
283
|
+
if (cs <= 0) return 0;
|
|
284
|
+
return 1 - Math.min(1, (1 - cb) / cs);
|
|
285
|
+
}
|
|
286
|
+
function colorDodge(cb, cs) {
|
|
287
|
+
if (cb <= 0) return 0;
|
|
288
|
+
if (cs >= 1) return 1;
|
|
289
|
+
return Math.min(1, cb / (1 - cs));
|
|
290
|
+
}
|
|
291
|
+
function hardLight(cb, cs) {
|
|
292
|
+
if (cs <= 0.5) return cb * 2 * cs;
|
|
293
|
+
const s = 2 * cs - 1;
|
|
294
|
+
return cb + s - cb * s;
|
|
295
|
+
}
|
|
296
|
+
function softLight(cb, cs) {
|
|
297
|
+
return cs <= 0.5 ? 2 * cb * cs + cb * cb * (1 - 2 * cs) : 2 * cb * (1 - cs) + Math.sqrt(Math.max(0, cb)) * (2 * cs - 1);
|
|
298
|
+
}
|
|
299
|
+
function vividLightHardMix(cb, cs) {
|
|
300
|
+
const EDGE = 1e-4;
|
|
301
|
+
if (cs <= 0.5) {
|
|
302
|
+
if (cb >= 1 - EDGE) return 1;
|
|
303
|
+
if (cs <= 0) return 0;
|
|
304
|
+
return 1 - Math.min(1, (1 - cb) / (2 * cs));
|
|
243
305
|
}
|
|
244
|
-
if (
|
|
245
|
-
|
|
306
|
+
if (cb <= EDGE) return 0;
|
|
307
|
+
if (cs >= 1) return 1;
|
|
308
|
+
return Math.min(1, cb / (2 * (1 - cs)));
|
|
309
|
+
}
|
|
310
|
+
function blendChannel(mode, cb, cs) {
|
|
311
|
+
switch (mode) {
|
|
312
|
+
case "normal":
|
|
313
|
+
return cs;
|
|
314
|
+
case "darken":
|
|
315
|
+
return Math.min(cb, cs);
|
|
316
|
+
case "multiply":
|
|
317
|
+
return cb * cs;
|
|
318
|
+
case "color-burn":
|
|
319
|
+
return colorBurn(cb, cs);
|
|
320
|
+
case "linear-burn":
|
|
321
|
+
return Math.max(0, cb + cs - 1);
|
|
322
|
+
case "lighten":
|
|
323
|
+
return Math.max(cb, cs);
|
|
324
|
+
case "screen":
|
|
325
|
+
return cb + cs - cb * cs;
|
|
326
|
+
case "color-dodge":
|
|
327
|
+
return colorDodge(cb, cs);
|
|
328
|
+
case "linear-dodge":
|
|
329
|
+
return Math.min(1, cb + cs);
|
|
330
|
+
case "overlay":
|
|
331
|
+
return hardLight(cs, cb);
|
|
332
|
+
case "soft-light":
|
|
333
|
+
return softLight(cb, cs);
|
|
334
|
+
case "hard-light":
|
|
335
|
+
return hardLight(cb, cs);
|
|
336
|
+
case "vivid-light":
|
|
337
|
+
if (cs <= 0) return 0;
|
|
338
|
+
if (cs >= 1) return 1;
|
|
339
|
+
return cs <= 0.5 ? colorBurn(cb, 2 * cs) : colorDodge(cb, 2 * (cs - 0.5));
|
|
340
|
+
case "linear-light":
|
|
341
|
+
return Math.min(1, Math.max(0, cb + 2 * cs - 1));
|
|
342
|
+
case "pin-light":
|
|
343
|
+
return cs <= 0.5 ? Math.min(cb, 2 * cs) : Math.max(cb, 2 * cs - 1);
|
|
344
|
+
case "hard-mix":
|
|
345
|
+
return vividLightHardMix(cb, cs) >= 0.5 - 1e-6 ? 1 : 0;
|
|
346
|
+
case "difference":
|
|
347
|
+
return Math.abs(cb - cs);
|
|
348
|
+
case "exclusion":
|
|
349
|
+
return cb + cs - 2 * cb * cs;
|
|
350
|
+
case "subtract":
|
|
351
|
+
return Math.max(0, cb - cs);
|
|
352
|
+
case "divide":
|
|
353
|
+
if (cs <= 0) return cb <= 0 ? 0 : 1;
|
|
354
|
+
return Math.min(1, cb / cs);
|
|
355
|
+
default:
|
|
356
|
+
return cs;
|
|
246
357
|
}
|
|
247
358
|
}
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;
|
|
260
|
-
const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;
|
|
261
|
-
const tempOffset = hasTemperature ? temperature * 0.8 : 0;
|
|
262
|
-
if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {
|
|
263
|
-
for (let i = 0; i < len; i += 4) {
|
|
264
|
-
let r = data[i];
|
|
265
|
-
let g = data[i + 1];
|
|
266
|
-
let b = data[i + 2];
|
|
267
|
-
let a = data[i + 3];
|
|
268
|
-
if (hasExposure) {
|
|
269
|
-
r = clamp(r * exposureMultiplier);
|
|
270
|
-
g = clamp(g * exposureMultiplier);
|
|
271
|
-
b = clamp(b * exposureMultiplier);
|
|
272
|
-
}
|
|
273
|
-
if (hasBrightness) {
|
|
274
|
-
const bOffset = brightness * 2.55;
|
|
275
|
-
r = clamp(r + bOffset);
|
|
276
|
-
g = clamp(g + bOffset);
|
|
277
|
-
b = clamp(b + bOffset);
|
|
278
|
-
}
|
|
279
|
-
if (hasContrast) {
|
|
280
|
-
r = clamp(contrastFactor * (r - 128) + 128);
|
|
281
|
-
g = clamp(contrastFactor * (g - 128) + 128);
|
|
282
|
-
b = clamp(contrastFactor * (b - 128) + 128);
|
|
283
|
-
}
|
|
284
|
-
if (hasSaturation) {
|
|
285
|
-
const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;
|
|
286
|
-
r = clamp(luminance + (r - luminance) * satMultiplier);
|
|
287
|
-
g = clamp(luminance + (g - luminance) * satMultiplier);
|
|
288
|
-
b = clamp(luminance + (b - luminance) * satMultiplier);
|
|
289
|
-
}
|
|
290
|
-
if (hasTemperature) {
|
|
291
|
-
r = clamp(r + tempOffset);
|
|
292
|
-
b = clamp(b - tempOffset);
|
|
293
|
-
}
|
|
294
|
-
if (hasOpacity) {
|
|
295
|
-
a = clamp(a * opacity);
|
|
296
|
-
}
|
|
297
|
-
data[i] = r;
|
|
298
|
-
data[i + 1] = g;
|
|
299
|
-
data[i + 2] = b;
|
|
300
|
-
data[i + 3] = a;
|
|
301
|
-
}
|
|
359
|
+
function blendLum(c) {
|
|
360
|
+
return 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];
|
|
361
|
+
}
|
|
362
|
+
function clipColor(c) {
|
|
363
|
+
const l = blendLum(c);
|
|
364
|
+
const n = Math.min(c[0], c[1], c[2]);
|
|
365
|
+
const x = Math.max(c[0], c[1], c[2]);
|
|
366
|
+
let out = [c[0], c[1], c[2]];
|
|
367
|
+
if (n < 0) {
|
|
368
|
+
const d = l - n;
|
|
369
|
+
out = out.map((v) => l > 0 && d > 0 ? l + (v - l) * l / d : l);
|
|
302
370
|
}
|
|
371
|
+
if (x > 1) {
|
|
372
|
+
const d = x - l;
|
|
373
|
+
out = out.map((v) => l < 1 && d > 0 ? l + (v - l) * (1 - l) / d : l);
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
303
376
|
}
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
377
|
+
function setLum(c, l) {
|
|
378
|
+
const d = l - blendLum(c);
|
|
379
|
+
return clipColor([c[0] + d, c[1] + d, c[2] + d]);
|
|
380
|
+
}
|
|
381
|
+
function sat(c) {
|
|
382
|
+
return Math.max(c[0], c[1], c[2]) - Math.min(c[0], c[1], c[2]);
|
|
383
|
+
}
|
|
384
|
+
function setSat(c, s) {
|
|
385
|
+
const idx = [0, 1, 2].sort((a, b) => c[a] - c[b]);
|
|
386
|
+
const [imin, imid, imax] = idx;
|
|
387
|
+
const out = [0, 0, 0];
|
|
388
|
+
if (c[imax] > c[imin]) {
|
|
389
|
+
out[imid] = (c[imid] - c[imin]) * s / (c[imax] - c[imin]);
|
|
390
|
+
out[imax] = s;
|
|
317
391
|
}
|
|
318
|
-
|
|
392
|
+
out[imin] = 0;
|
|
393
|
+
return out;
|
|
319
394
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (let c = 0; c < 3; c++) {
|
|
337
|
-
let reach = 0;
|
|
338
|
-
for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);
|
|
339
|
-
const headroom = Math.max(1, reach);
|
|
340
|
-
luts[c] = buildToneLut(contrast, brightness, headroom);
|
|
341
|
-
scale[c] = TONE_LUT_SIZE / headroom;
|
|
395
|
+
function blendRgb(mode, cb, cs) {
|
|
396
|
+
switch (mode) {
|
|
397
|
+
case "hue":
|
|
398
|
+
return setLum(setSat(cs, sat(cb)), blendLum(cb));
|
|
399
|
+
case "saturation":
|
|
400
|
+
return setLum(setSat(cb, sat(cs)), blendLum(cb));
|
|
401
|
+
case "color":
|
|
402
|
+
return setLum(cs, blendLum(cb));
|
|
403
|
+
case "luminosity":
|
|
404
|
+
return setLum(cb, blendLum(cs));
|
|
405
|
+
case "darker-color":
|
|
406
|
+
return blendLum(cs) < blendLum(cb) ? [cs[0], cs[1], cs[2]] : [cb[0], cb[1], cb[2]];
|
|
407
|
+
case "lighter-color":
|
|
408
|
+
return blendLum(cs) > blendLum(cb) ? [cs[0], cs[1], cs[2]] : [cb[0], cb[1], cb[2]];
|
|
409
|
+
default:
|
|
410
|
+
return [blendChannel(mode, cb[0], cs[0]), blendChannel(mode, cb[1], cs[1]), blendChannel(mode, cb[2], cs[2])];
|
|
342
411
|
}
|
|
343
|
-
cachedPlan = { key, m, luts, scale, saturation };
|
|
344
|
-
return cachedPlan;
|
|
345
412
|
}
|
|
346
|
-
function
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return;
|
|
413
|
+
function blendPixel(mode, backdrop, source, alpha = 1) {
|
|
414
|
+
const ab = backdrop[3] ?? 1;
|
|
415
|
+
const as = (source[3] ?? 1) * alpha;
|
|
416
|
+
if (!(as > 0)) return [backdrop[0], backdrop[1], backdrop[2], ab];
|
|
417
|
+
const b = blendRgb(mode, backdrop, source);
|
|
418
|
+
const ao = as + ab * (1 - as);
|
|
419
|
+
if (ao <= 0) return [0, 0, 0, 0];
|
|
420
|
+
const out = [0, 0, 0, ao];
|
|
421
|
+
for (let i = 0; i < 3; i++) {
|
|
422
|
+
out[i] = ((1 - as) * ab * backdrop[i] + (1 - ab) * as * source[i] + as * ab * b[i]) / ao;
|
|
357
423
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
const hasSaturation = saturation !== 1;
|
|
367
|
-
const wr = LUMA_709[0], wg = LUMA_709[1], wb = LUMA_709[2];
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function blendImageData(backdrop, source, mode, alpha = 1) {
|
|
427
|
+
const d = backdrop.data;
|
|
428
|
+
const s = source.data;
|
|
429
|
+
const len = Math.min(d.length, s.length);
|
|
430
|
+
const cb = [0, 0, 0, 0];
|
|
431
|
+
const cs = [0, 0, 0, 0];
|
|
368
432
|
for (let i = 0; i < len; i += 4) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
let g = m3 * r0 + m4 * g0 + m5 * b0;
|
|
374
|
-
let b = m6 * r0 + m7 * g0 + m8 * b0;
|
|
375
|
-
let f;
|
|
376
|
-
let k;
|
|
377
|
-
if (r <= 0) r = lutR[0];
|
|
378
|
-
else if ((f = r * sR) >= N) r = lutR[N];
|
|
379
|
-
else {
|
|
380
|
-
k = f | 0;
|
|
381
|
-
r = lutR[k] + (lutR[k + 1] - lutR[k]) * (f - k);
|
|
382
|
-
}
|
|
383
|
-
if (g <= 0) g = lutG[0];
|
|
384
|
-
else if ((f = g * sG) >= N) g = lutG[N];
|
|
385
|
-
else {
|
|
386
|
-
k = f | 0;
|
|
387
|
-
g = lutG[k] + (lutG[k + 1] - lutG[k]) * (f - k);
|
|
433
|
+
if (s[i + 3] === 0) continue;
|
|
434
|
+
for (let k = 0; k < 4; k++) {
|
|
435
|
+
cb[k] = d[i + k] / 255;
|
|
436
|
+
cs[k] = s[i + k] / 255;
|
|
388
437
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
if (hasSaturation) {
|
|
396
|
-
const y = wr * r + wg * g + wb * b;
|
|
397
|
-
r = y + (r - y) * saturation;
|
|
398
|
-
g = y + (g - y) * saturation;
|
|
399
|
-
b = y + (b - y) * saturation;
|
|
400
|
-
}
|
|
401
|
-
data[i] = enc[r <= 0 ? 0 : r >= 1 ? ENCODE_LUT_SIZE : r * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
402
|
-
data[i + 1] = enc[g <= 0 ? 0 : g >= 1 ? ENCODE_LUT_SIZE : g * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
403
|
-
data[i + 2] = enc[b <= 0 ? 0 : b >= 1 ? ENCODE_LUT_SIZE : b * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
404
|
-
if (hasOpacity) data[i + 3] = clamp(data[i + 3] * opacity);
|
|
438
|
+
const o = blendPixel(mode, cb, cs, alpha);
|
|
439
|
+
d[i] = Math.round(o[0] * 255);
|
|
440
|
+
d[i + 1] = Math.round(o[1] * 255);
|
|
441
|
+
d[i + 2] = Math.round(o[2] * 255);
|
|
442
|
+
d[i + 3] = Math.round(o[3] * 255);
|
|
405
443
|
}
|
|
406
444
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const dstIdx = (y * w + x) * 4;
|
|
427
|
-
temp[dstIdx] = Math.round(r / count);
|
|
428
|
-
temp[dstIdx + 1] = Math.round(g / count);
|
|
429
|
-
temp[dstIdx + 2] = Math.round(b / count);
|
|
430
|
-
temp[dstIdx + 3] = Math.round(a / count);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
for (let x = 0; x < w; x++) {
|
|
434
|
-
for (let y = 0; y < h; y++) {
|
|
435
|
-
let r = 0, g = 0, b = 0, a = 0;
|
|
436
|
-
let count = 0;
|
|
437
|
-
for (let k = -radius; k <= radius; k++) {
|
|
438
|
-
const py = Math.min(h - 1, Math.max(0, y + k));
|
|
439
|
-
const idx = (py * w + x) * 4;
|
|
440
|
-
r += temp[idx];
|
|
441
|
-
g += temp[idx + 1];
|
|
442
|
-
b += temp[idx + 2];
|
|
443
|
-
a += temp[idx + 3];
|
|
444
|
-
count++;
|
|
445
|
-
}
|
|
446
|
-
const dstIdx = (y * w + x) * 4;
|
|
447
|
-
src[dstIdx] = Math.round(r / count);
|
|
448
|
-
src[dstIdx + 1] = Math.round(g / count);
|
|
449
|
-
src[dstIdx + 2] = Math.round(b / count);
|
|
450
|
-
src[dstIdx + 3] = Math.round(a / count);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
function applyDepthMask(mainImageData, maskImageData, options) {
|
|
455
|
-
const { mode, depthRange, softness, invert } = options;
|
|
456
|
-
const mainData = mainImageData.data;
|
|
457
|
-
const maskData = maskImageData.data;
|
|
458
|
-
const len = Math.min(mainData.length, maskData.length);
|
|
459
|
-
const effectiveSoftness = Math.max(1, softness || 10);
|
|
460
|
-
const minDepth = depthRange[0];
|
|
461
|
-
const maxDepth = depthRange[1];
|
|
462
|
-
for (let i = 0; i < len; i += 4) {
|
|
463
|
-
let maskValue = maskData[i];
|
|
464
|
-
if (invert) {
|
|
465
|
-
maskValue = 255 - maskValue;
|
|
466
|
-
}
|
|
467
|
-
let alphaValue;
|
|
468
|
-
if (mode === "threshold") {
|
|
469
|
-
if (maskValue >= maxDepth + effectiveSoftness) {
|
|
470
|
-
alphaValue = 0;
|
|
471
|
-
} else if (maskValue > maxDepth) {
|
|
472
|
-
alphaValue = Math.round((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness * 255);
|
|
473
|
-
} else if (maskValue >= minDepth) {
|
|
474
|
-
alphaValue = 255;
|
|
475
|
-
} else if (maskValue < minDepth - effectiveSoftness) {
|
|
476
|
-
alphaValue = 0;
|
|
477
|
-
} else {
|
|
478
|
-
alphaValue = Math.round((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness * 255);
|
|
479
|
-
}
|
|
480
|
-
} else {
|
|
481
|
-
alphaValue = clamp(maskValue + minDepth);
|
|
482
|
-
}
|
|
483
|
-
mainData[i + 3] = clamp(mainData[i + 3] * alphaValue / 255);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
function removeAlpha(imageData) {
|
|
487
|
-
const data = imageData.data;
|
|
488
|
-
const len = data.length;
|
|
489
|
-
for (let i = 3; i < len; i += 4) {
|
|
490
|
-
data[i] = 255;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// src/image-editor/geometry.ts
|
|
495
|
-
function normalizeAngle(degrees) {
|
|
496
|
-
const a = (degrees % 360 + 360) % 360;
|
|
497
|
-
return a === 360 ? 0 : a;
|
|
498
|
-
}
|
|
499
|
-
function cosSin(angle) {
|
|
500
|
-
switch (angle) {
|
|
501
|
-
case 0:
|
|
502
|
-
return [1, 0];
|
|
503
|
-
case 90:
|
|
504
|
-
return [0, 1];
|
|
505
|
-
case 180:
|
|
506
|
-
return [-1, 0];
|
|
507
|
-
case 270:
|
|
508
|
-
return [0, -1];
|
|
509
|
-
default: {
|
|
510
|
-
const rad = angle * Math.PI / 180;
|
|
511
|
-
return [Math.cos(rad), Math.sin(rad)];
|
|
445
|
+
|
|
446
|
+
// src/image-editor/geometry.ts
|
|
447
|
+
function normalizeAngle(degrees) {
|
|
448
|
+
const a = (degrees % 360 + 360) % 360;
|
|
449
|
+
return a === 360 ? 0 : a;
|
|
450
|
+
}
|
|
451
|
+
function cosSin(angle) {
|
|
452
|
+
switch (angle) {
|
|
453
|
+
case 0:
|
|
454
|
+
return [1, 0];
|
|
455
|
+
case 90:
|
|
456
|
+
return [0, 1];
|
|
457
|
+
case 180:
|
|
458
|
+
return [-1, 0];
|
|
459
|
+
case 270:
|
|
460
|
+
return [0, -1];
|
|
461
|
+
default: {
|
|
462
|
+
const rad = angle * Math.PI / 180;
|
|
463
|
+
return [Math.cos(rad), Math.sin(rad)];
|
|
512
464
|
}
|
|
513
465
|
}
|
|
514
466
|
}
|
|
@@ -735,103 +687,1890 @@ function calculateCropDrag(startCrop, handle, startImgPt, currentImgPt, imgWidth
|
|
|
735
687
|
height: startCrop.height
|
|
736
688
|
};
|
|
737
689
|
}
|
|
738
|
-
let left = startCrop.x;
|
|
739
|
-
let top = startCrop.y;
|
|
740
|
-
let right = startCrop.x + startCrop.width;
|
|
741
|
-
let bottom = startCrop.y + startCrop.height;
|
|
742
|
-
const minSize = 20;
|
|
743
|
-
if (handle === "e" || handle === "ne" || handle === "se") {
|
|
744
|
-
right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
|
|
690
|
+
let left = startCrop.x;
|
|
691
|
+
let top = startCrop.y;
|
|
692
|
+
let right = startCrop.x + startCrop.width;
|
|
693
|
+
let bottom = startCrop.y + startCrop.height;
|
|
694
|
+
const minSize = 20;
|
|
695
|
+
if (handle === "e" || handle === "ne" || handle === "se") {
|
|
696
|
+
right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));
|
|
697
|
+
}
|
|
698
|
+
if (handle === "w" || handle === "nw" || handle === "sw") {
|
|
699
|
+
left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));
|
|
700
|
+
}
|
|
701
|
+
if (handle === "s" || handle === "se" || handle === "sw") {
|
|
702
|
+
bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));
|
|
703
|
+
}
|
|
704
|
+
if (handle === "n" || handle === "nw" || handle === "ne") {
|
|
705
|
+
top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));
|
|
706
|
+
}
|
|
707
|
+
if (aspectRatio && aspectRatio > 0) {
|
|
708
|
+
let currentW = right - left;
|
|
709
|
+
let currentH = bottom - top;
|
|
710
|
+
if (handle === "e" || handle === "w") {
|
|
711
|
+
currentH = Math.min(imgHeight, currentW / aspectRatio);
|
|
712
|
+
bottom = Math.min(imgHeight, top + currentH);
|
|
713
|
+
} else if (handle === "n" || handle === "s") {
|
|
714
|
+
currentW = Math.min(imgWidth, currentH * aspectRatio);
|
|
715
|
+
right = Math.min(imgWidth, left + currentW);
|
|
716
|
+
} else {
|
|
717
|
+
const targetH = currentW / aspectRatio;
|
|
718
|
+
if (top + targetH <= imgHeight) {
|
|
719
|
+
bottom = top + targetH;
|
|
720
|
+
} else {
|
|
721
|
+
const targetW = currentH * aspectRatio;
|
|
722
|
+
right = Math.min(imgWidth, left + targetW);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
x: Math.round(left),
|
|
728
|
+
y: Math.round(top),
|
|
729
|
+
width: Math.round(Math.max(minSize, right - left)),
|
|
730
|
+
height: Math.round(Math.max(minSize, bottom - top))
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function getAnnotationBounds(ann) {
|
|
734
|
+
switch (ann.type) {
|
|
735
|
+
case "rect":
|
|
736
|
+
case "image":
|
|
737
|
+
return {
|
|
738
|
+
x: Math.min(ann.x, ann.x + ann.width),
|
|
739
|
+
y: Math.min(ann.y, ann.y + ann.height),
|
|
740
|
+
width: Math.abs(ann.width),
|
|
741
|
+
height: Math.abs(ann.height)
|
|
742
|
+
};
|
|
743
|
+
case "circle":
|
|
744
|
+
return {
|
|
745
|
+
x: ann.x - Math.abs(ann.radiusX),
|
|
746
|
+
y: ann.y - Math.abs(ann.radiusY),
|
|
747
|
+
width: Math.abs(ann.radiusX) * 2,
|
|
748
|
+
height: Math.abs(ann.radiusY) * 2
|
|
749
|
+
};
|
|
750
|
+
case "arrow":
|
|
751
|
+
case "line": {
|
|
752
|
+
const minX = Math.min(ann.x, ann.endX);
|
|
753
|
+
const minY = Math.min(ann.y, ann.endY);
|
|
754
|
+
return {
|
|
755
|
+
x: minX,
|
|
756
|
+
y: minY,
|
|
757
|
+
width: Math.max(Math.abs(ann.endX - ann.x), 10),
|
|
758
|
+
height: Math.max(Math.abs(ann.endY - ann.y), 10)
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
case "pen": {
|
|
762
|
+
if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
|
|
763
|
+
let minX = ann.points[0].x;
|
|
764
|
+
let maxX = ann.points[0].x;
|
|
765
|
+
let minY = ann.points[0].y;
|
|
766
|
+
let maxY = ann.points[0].y;
|
|
767
|
+
for (const pt of ann.points) {
|
|
768
|
+
if (pt.x < minX) minX = pt.x;
|
|
769
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
770
|
+
if (pt.y < minY) minY = pt.y;
|
|
771
|
+
if (pt.y > maxY) maxY = pt.y;
|
|
772
|
+
}
|
|
773
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
774
|
+
}
|
|
775
|
+
case "text":
|
|
776
|
+
return {
|
|
777
|
+
x: ann.x,
|
|
778
|
+
y: ann.y,
|
|
779
|
+
width: ann.text.length * ann.fontSize * 0.6,
|
|
780
|
+
height: ann.fontSize * 1.2
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function hitTestAnnotation(imgPt, ann, padding = 10) {
|
|
785
|
+
const b = getAnnotationBounds(ann);
|
|
786
|
+
return imgPt.x >= b.x - padding && imgPt.x <= b.x + b.width + padding && imgPt.y >= b.y - padding && imgPt.y <= b.y + b.height + padding;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/image-editor/selection/mask.ts
|
|
790
|
+
var SELECTION_MODES = ["replace", "add", "subtract", "intersect"];
|
|
791
|
+
function isSelectionMode(value) {
|
|
792
|
+
return typeof value === "string" && SELECTION_MODES.includes(value);
|
|
793
|
+
}
|
|
794
|
+
function parseSelectionMode(value) {
|
|
795
|
+
return isSelectionMode(value) ? value : "replace";
|
|
796
|
+
}
|
|
797
|
+
var MAX_SIDE = 1 << 15;
|
|
798
|
+
function checkSize(width, height) {
|
|
799
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE) {
|
|
800
|
+
throw new Error(`Invalid selection size ${width}\xD7${height}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function computeSelectionBounds(width, height, data) {
|
|
804
|
+
let x0 = width;
|
|
805
|
+
let x1 = -1;
|
|
806
|
+
let y0 = -1;
|
|
807
|
+
let y1 = -1;
|
|
808
|
+
for (let y = 0; y < height; y++) {
|
|
809
|
+
const row = y * width;
|
|
810
|
+
let first = -1;
|
|
811
|
+
for (let x = 0; x < width; x++) {
|
|
812
|
+
if (data[row + x] !== 0) {
|
|
813
|
+
first = x;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (first < 0) continue;
|
|
818
|
+
let last = first;
|
|
819
|
+
for (let x = width - 1; x > first; x--) {
|
|
820
|
+
if (data[row + x] !== 0) {
|
|
821
|
+
last = x;
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (y0 < 0) y0 = y;
|
|
826
|
+
y1 = y;
|
|
827
|
+
if (first < x0) x0 = first;
|
|
828
|
+
if (last > x1) x1 = last;
|
|
829
|
+
}
|
|
830
|
+
if (y0 < 0) return null;
|
|
831
|
+
return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };
|
|
832
|
+
}
|
|
833
|
+
function createSelectionMask(width, height, data) {
|
|
834
|
+
checkSize(width, height);
|
|
835
|
+
const bytes = data ?? new Uint8Array(width * height);
|
|
836
|
+
if (bytes.length !== width * height) throw new Error("Selection data does not match its size");
|
|
837
|
+
return { width, height, data: bytes, bounds: data ? computeSelectionBounds(width, height, bytes) : null };
|
|
838
|
+
}
|
|
839
|
+
function fullSelection(width, height) {
|
|
840
|
+
checkSize(width, height);
|
|
841
|
+
return { width, height, data: new Uint8Array(width * height).fill(255), bounds: { x: 0, y: 0, width, height } };
|
|
842
|
+
}
|
|
843
|
+
function isSelectionEmpty(mask) {
|
|
844
|
+
return !mask || mask.bounds === null;
|
|
845
|
+
}
|
|
846
|
+
function selectionValueAt(mask, x, y) {
|
|
847
|
+
const xi = Math.floor(x);
|
|
848
|
+
const yi = Math.floor(y);
|
|
849
|
+
if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;
|
|
850
|
+
return mask.data[yi * mask.width + xi] / 255;
|
|
851
|
+
}
|
|
852
|
+
function coverageToByte(v) {
|
|
853
|
+
if (!(v > 0)) return 0;
|
|
854
|
+
if (v >= 1) return 255;
|
|
855
|
+
return Math.floor(v * 255 + 0.5);
|
|
856
|
+
}
|
|
857
|
+
function selectionFromCoverage(width, height, coverage) {
|
|
858
|
+
const n = width * height;
|
|
859
|
+
const out = new Uint8Array(n);
|
|
860
|
+
for (let i = 0; i < n; i++) out[i] = coverageToByte(coverage[i]);
|
|
861
|
+
return createSelectionMask(width, height, out);
|
|
862
|
+
}
|
|
863
|
+
function selectionToCoverage(mask) {
|
|
864
|
+
const out = new Float32Array(mask.data.length);
|
|
865
|
+
for (let i = 0; i < out.length; i++) out[i] = mask.data[i] / 255;
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
868
|
+
function selectionArea(mask) {
|
|
869
|
+
if (!mask || !mask.bounds) return 0;
|
|
870
|
+
let sum = 0;
|
|
871
|
+
const { x, y, width, height } = mask.bounds;
|
|
872
|
+
for (let yy = y; yy < y + height; yy++) {
|
|
873
|
+
const row = yy * mask.width;
|
|
874
|
+
for (let xx = x; xx < x + width; xx++) sum += mask.data[row + xx];
|
|
875
|
+
}
|
|
876
|
+
return sum / 255;
|
|
877
|
+
}
|
|
878
|
+
function combineSelections(prev, next, mode) {
|
|
879
|
+
let result;
|
|
880
|
+
if (mode === "replace" || !prev && mode === "add") {
|
|
881
|
+
result = next;
|
|
882
|
+
} else if (!prev) {
|
|
883
|
+
return null;
|
|
884
|
+
} else {
|
|
885
|
+
if (prev.width !== next.width || prev.height !== next.height) throw new Error("Selections differ in size");
|
|
886
|
+
const a = prev.data;
|
|
887
|
+
const b = next.data;
|
|
888
|
+
const out = new Uint8Array(a.length);
|
|
889
|
+
if (mode === "add") for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] : b[i];
|
|
890
|
+
else if (mode === "subtract") for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] - b[i] : 0;
|
|
891
|
+
else for (let i = 0; i < a.length; i++) out[i] = a[i] < b[i] ? a[i] : b[i];
|
|
892
|
+
result = createSelectionMask(prev.width, prev.height, out);
|
|
893
|
+
}
|
|
894
|
+
return result.bounds ? result : null;
|
|
895
|
+
}
|
|
896
|
+
function invertSelection(mask) {
|
|
897
|
+
const out = new Uint8Array(mask.data.length);
|
|
898
|
+
for (let i = 0; i < out.length; i++) out[i] = 255 - mask.data[i];
|
|
899
|
+
return createSelectionMask(mask.width, mask.height, out);
|
|
900
|
+
}
|
|
901
|
+
function resizeSelection(mask, width, height) {
|
|
902
|
+
if (mask.width === width && mask.height === height) return mask;
|
|
903
|
+
checkSize(width, height);
|
|
904
|
+
const out = new Uint8Array(width * height);
|
|
905
|
+
const sx = mask.width / width;
|
|
906
|
+
const sy = mask.height / height;
|
|
907
|
+
const src = mask.data;
|
|
908
|
+
const mw = mask.width;
|
|
909
|
+
for (let y = 0; y < height; y++) {
|
|
910
|
+
const fy = Math.min(mask.height - 1, Math.max(0, (y + 0.5) * sy - 0.5));
|
|
911
|
+
const y0 = Math.floor(fy);
|
|
912
|
+
const y1 = Math.min(mask.height - 1, y0 + 1);
|
|
913
|
+
const ty = fy - y0;
|
|
914
|
+
for (let x = 0; x < width; x++) {
|
|
915
|
+
const fx = Math.min(mw - 1, Math.max(0, (x + 0.5) * sx - 0.5));
|
|
916
|
+
const x0 = Math.floor(fx);
|
|
917
|
+
const x1 = Math.min(mw - 1, x0 + 1);
|
|
918
|
+
const tx = fx - x0;
|
|
919
|
+
const top = src[y0 * mw + x0] * (1 - tx) + src[y0 * mw + x1] * tx;
|
|
920
|
+
const bot = src[y1 * mw + x0] * (1 - tx) + src[y1 * mw + x1] * tx;
|
|
921
|
+
out[y * width + x] = Math.round(top * (1 - ty) + bot * ty);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return createSelectionMask(width, height, out);
|
|
925
|
+
}
|
|
926
|
+
function selectionFromPixels(pixels, options = {}) {
|
|
927
|
+
const { width, height, data } = pixels;
|
|
928
|
+
checkSize(width, height);
|
|
929
|
+
if (data.length < width * height * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
|
|
930
|
+
const channel = options.channel ?? "luminance";
|
|
931
|
+
const out = new Uint8Array(width * height);
|
|
932
|
+
const threshold = options.threshold;
|
|
933
|
+
for (let i = 0, p = 0; i < out.length; i++, p += 4) {
|
|
934
|
+
let v;
|
|
935
|
+
switch (channel) {
|
|
936
|
+
case "alpha":
|
|
937
|
+
v = data[p + 3];
|
|
938
|
+
break;
|
|
939
|
+
case "red":
|
|
940
|
+
v = data[p];
|
|
941
|
+
break;
|
|
942
|
+
case "green":
|
|
943
|
+
v = data[p + 1];
|
|
944
|
+
break;
|
|
945
|
+
case "blue":
|
|
946
|
+
v = data[p + 2];
|
|
947
|
+
break;
|
|
948
|
+
default:
|
|
949
|
+
v = Math.round((0.2126 * data[p] + 0.7152 * data[p + 1] + 0.0722 * data[p + 2]) * data[p + 3] / 255);
|
|
950
|
+
}
|
|
951
|
+
if (options.invert) v = 255 - v;
|
|
952
|
+
if (threshold !== void 0) v = v >= threshold ? 255 : 0;
|
|
953
|
+
out[i] = v;
|
|
954
|
+
}
|
|
955
|
+
return createSelectionMask(width, height, out);
|
|
956
|
+
}
|
|
957
|
+
function selectionToPixels(mask, as = "gray") {
|
|
958
|
+
const out = new Uint8ClampedArray(mask.data.length * 4);
|
|
959
|
+
for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
|
|
960
|
+
const v = mask.data[i];
|
|
961
|
+
if (as === "gray") {
|
|
962
|
+
out[p] = out[p + 1] = out[p + 2] = v;
|
|
963
|
+
out[p + 3] = 255;
|
|
964
|
+
} else {
|
|
965
|
+
out[p] = out[p + 1] = out[p + 2] = 255;
|
|
966
|
+
out[p + 3] = v;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return { width: mask.width, height: mask.height, data: out };
|
|
970
|
+
}
|
|
971
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
972
|
+
var B64_INDEX = (() => {
|
|
973
|
+
const t = new Int16Array(128).fill(-1);
|
|
974
|
+
for (let i = 0; i < B64.length; i++) t[B64.charCodeAt(i)] = i;
|
|
975
|
+
return t;
|
|
976
|
+
})();
|
|
977
|
+
function toBase64(bytes, length) {
|
|
978
|
+
const parts = [];
|
|
979
|
+
let chunk = "";
|
|
980
|
+
for (let i = 0; i < length; i += 3) {
|
|
981
|
+
const a = bytes[i];
|
|
982
|
+
const b = i + 1 < length ? bytes[i + 1] : 0;
|
|
983
|
+
const c = i + 2 < length ? bytes[i + 2] : 0;
|
|
984
|
+
chunk += B64[a >> 2] + B64[(a & 3) << 4 | b >> 4];
|
|
985
|
+
chunk += i + 1 < length ? B64[(b & 15) << 2 | c >> 6] : "=";
|
|
986
|
+
chunk += i + 2 < length ? B64[c & 63] : "=";
|
|
987
|
+
if (chunk.length >= 8192) {
|
|
988
|
+
parts.push(chunk);
|
|
989
|
+
chunk = "";
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
parts.push(chunk);
|
|
993
|
+
return parts.join("");
|
|
994
|
+
}
|
|
995
|
+
function fromBase64(text) {
|
|
996
|
+
if (text.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
997
|
+
let pad = 0;
|
|
998
|
+
if (text.endsWith("==")) pad = 2;
|
|
999
|
+
else if (text.endsWith("=")) pad = 1;
|
|
1000
|
+
const out = new Uint8Array(text.length / 4 * 3 - pad);
|
|
1001
|
+
let o = 0;
|
|
1002
|
+
const digit = (i, k) => {
|
|
1003
|
+
const ch = text.charCodeAt(i + k);
|
|
1004
|
+
if (ch === 61 && i + 4 === text.length && k >= 4 - pad) return 0;
|
|
1005
|
+
const idx = ch < 128 ? B64_INDEX[ch] : -1;
|
|
1006
|
+
if (idx < 0) throw new Error("Invalid base64 character");
|
|
1007
|
+
return idx;
|
|
1008
|
+
};
|
|
1009
|
+
for (let i = 0; i < text.length; i += 4) {
|
|
1010
|
+
const n = digit(i, 0) << 18 | digit(i, 1) << 12 | digit(i, 2) << 6 | digit(i, 3);
|
|
1011
|
+
if (o < out.length) out[o++] = n >> 16 & 255;
|
|
1012
|
+
if (o < out.length) out[o++] = n >> 8 & 255;
|
|
1013
|
+
if (o < out.length) out[o++] = n & 255;
|
|
1014
|
+
}
|
|
1015
|
+
return out;
|
|
1016
|
+
}
|
|
1017
|
+
function encodeSelectionRle(data) {
|
|
1018
|
+
let buf = new Uint8Array(64);
|
|
1019
|
+
let len = 0;
|
|
1020
|
+
const push = (b) => {
|
|
1021
|
+
if (len === buf.length) {
|
|
1022
|
+
const next = new Uint8Array(buf.length * 2);
|
|
1023
|
+
next.set(buf);
|
|
1024
|
+
buf = next;
|
|
1025
|
+
}
|
|
1026
|
+
buf[len++] = b;
|
|
1027
|
+
};
|
|
1028
|
+
let i = 0;
|
|
1029
|
+
while (i < data.length) {
|
|
1030
|
+
const v = data[i];
|
|
1031
|
+
let j = i + 1;
|
|
1032
|
+
while (j < data.length && data[j] === v) j++;
|
|
1033
|
+
push(v);
|
|
1034
|
+
let run = j - i;
|
|
1035
|
+
while (run >= 128) {
|
|
1036
|
+
push(run & 127 | 128);
|
|
1037
|
+
run = Math.floor(run / 128);
|
|
1038
|
+
}
|
|
1039
|
+
push(run);
|
|
1040
|
+
i = j;
|
|
1041
|
+
}
|
|
1042
|
+
return "r1:" + toBase64(buf, len);
|
|
1043
|
+
}
|
|
1044
|
+
function decodeSelectionRle(rle, length) {
|
|
1045
|
+
if (typeof rle !== "string" || !rle.startsWith("r1:")) throw new Error("Unknown selection encoding");
|
|
1046
|
+
const bytes = fromBase64(rle.slice(3));
|
|
1047
|
+
const out = new Uint8Array(length);
|
|
1048
|
+
let o = 0;
|
|
1049
|
+
let i = 0;
|
|
1050
|
+
while (i < bytes.length) {
|
|
1051
|
+
const v = bytes[i++];
|
|
1052
|
+
let run = 0;
|
|
1053
|
+
let scale = 1;
|
|
1054
|
+
for (; ; ) {
|
|
1055
|
+
if (i >= bytes.length) throw new Error("Truncated selection run");
|
|
1056
|
+
const b = bytes[i++];
|
|
1057
|
+
run += (b & 127) * scale;
|
|
1058
|
+
if (b < 128) break;
|
|
1059
|
+
scale *= 128;
|
|
1060
|
+
if (scale > 2 ** 35) throw new Error("Selection run too long");
|
|
1061
|
+
}
|
|
1062
|
+
if (run === 0 || o + run > length) throw new Error("Selection runs do not match its size");
|
|
1063
|
+
if (v !== 0) out.fill(v, o, o + run);
|
|
1064
|
+
o += run;
|
|
1065
|
+
}
|
|
1066
|
+
if (o !== length) throw new Error("Selection runs do not match its size");
|
|
1067
|
+
return out;
|
|
1068
|
+
}
|
|
1069
|
+
function serializeSelection(mask) {
|
|
1070
|
+
return { width: mask.width, height: mask.height, rle: encodeSelectionRle(mask.data) };
|
|
1071
|
+
}
|
|
1072
|
+
function deserializeSelection(value) {
|
|
1073
|
+
if (!value || typeof value !== "object") throw new Error("selection must be an object");
|
|
1074
|
+
const v = value;
|
|
1075
|
+
const width = v.width;
|
|
1076
|
+
const height = v.height;
|
|
1077
|
+
checkSize(width, height);
|
|
1078
|
+
return createSelectionMask(width, height, decodeSelectionRle(v.rle, width * height));
|
|
1079
|
+
}
|
|
1080
|
+
function hashMaskContent(content) {
|
|
1081
|
+
let h = 2166136261;
|
|
1082
|
+
if (typeof content === "string") {
|
|
1083
|
+
for (let i = 0; i < content.length; i++) {
|
|
1084
|
+
h ^= content.charCodeAt(i);
|
|
1085
|
+
h = Math.imul(h, 16777619);
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
for (let i = 0; i < content.length; i++) {
|
|
1089
|
+
h ^= content[i];
|
|
1090
|
+
h = Math.imul(h, 16777619);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return `${(h >>> 0).toString(16)}:${content.length}`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/image-editor/selection/modify.ts
|
|
1097
|
+
var EMPTY_DISTANCE = 1e10;
|
|
1098
|
+
function distanceTransform(inside, width, height) {
|
|
1099
|
+
const n = width * height;
|
|
1100
|
+
const out = new Float32Array(n);
|
|
1101
|
+
let any = false;
|
|
1102
|
+
let all = true;
|
|
1103
|
+
for (let i = 0; i < n; i++) {
|
|
1104
|
+
if (inside[i]) any = true;
|
|
1105
|
+
else all = false;
|
|
1106
|
+
}
|
|
1107
|
+
if (!any) return out.fill(EMPTY_DISTANCE);
|
|
1108
|
+
if (all) return out;
|
|
1109
|
+
const NONE = 4294967295;
|
|
1110
|
+
const vertical = new Uint32Array(n).fill(NONE);
|
|
1111
|
+
for (let x = 0; x < width; x++) {
|
|
1112
|
+
let nearest = -1;
|
|
1113
|
+
for (let y = 0; y < height; y++) {
|
|
1114
|
+
const i = y * width + x;
|
|
1115
|
+
if (inside[i]) nearest = y;
|
|
1116
|
+
if (nearest >= 0) vertical[i] = y - nearest;
|
|
1117
|
+
}
|
|
1118
|
+
nearest = -1;
|
|
1119
|
+
for (let y = height - 1; y >= 0; y--) {
|
|
1120
|
+
const i = y * width + x;
|
|
1121
|
+
if (vertical[i] === 0) nearest = y;
|
|
1122
|
+
if (nearest >= 0) vertical[i] = Math.min(vertical[i], nearest - y);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const sites = new Int32Array(width);
|
|
1126
|
+
const starts = new Float64Array(width);
|
|
1127
|
+
for (let y = 0; y < height; y++) {
|
|
1128
|
+
const row = y * width;
|
|
1129
|
+
let count = 0;
|
|
1130
|
+
for (let q = 0; q < width; q++) {
|
|
1131
|
+
const dq = vertical[row + q];
|
|
1132
|
+
if (dq === NONE) continue;
|
|
1133
|
+
const fq = dq * dq;
|
|
1134
|
+
let start = -Infinity;
|
|
1135
|
+
while (count > 0) {
|
|
1136
|
+
const p = sites[count - 1];
|
|
1137
|
+
const dp = vertical[row + p];
|
|
1138
|
+
start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;
|
|
1139
|
+
if (start > starts[count - 1]) break;
|
|
1140
|
+
count--;
|
|
1141
|
+
}
|
|
1142
|
+
if (count === 0) start = -Infinity;
|
|
1143
|
+
sites[count] = q;
|
|
1144
|
+
starts[count] = start;
|
|
1145
|
+
count++;
|
|
1146
|
+
}
|
|
1147
|
+
if (count === 0) {
|
|
1148
|
+
out.fill(EMPTY_DISTANCE, row, row + width);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
let k = 0;
|
|
1152
|
+
for (let x = 0; x < width; x++) {
|
|
1153
|
+
while (k + 1 < count && starts[k + 1] < x) k++;
|
|
1154
|
+
const site = sites[k];
|
|
1155
|
+
const dx = x - site;
|
|
1156
|
+
const dv = vertical[row + site];
|
|
1157
|
+
out[row + x] = Math.sqrt(dx * dx + dv * dv);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return out;
|
|
1161
|
+
}
|
|
1162
|
+
function thresholdInside(m, threshold = 0.5) {
|
|
1163
|
+
const out = new Uint8Array(m.length);
|
|
1164
|
+
for (let i = 0; i < m.length; i++) out[i] = m[i] >= threshold ? 1 : 0;
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
function expandCoverage(m, w, h, r) {
|
|
1168
|
+
const d = distanceTransform(thresholdInside(m), w, h);
|
|
1169
|
+
const out = new Float32Array(m.length);
|
|
1170
|
+
for (let i = 0; i < out.length; i++) out[i] = Math.max(m[i], Math.min(1, Math.max(0, r + 1 - d[i])));
|
|
1171
|
+
return out;
|
|
1172
|
+
}
|
|
1173
|
+
function contractCoverage(m, w, h, r) {
|
|
1174
|
+
const inv = new Float32Array(m.length);
|
|
1175
|
+
for (let i = 0; i < inv.length; i++) inv[i] = 1 - m[i];
|
|
1176
|
+
const e = expandCoverage(inv, w, h, r);
|
|
1177
|
+
for (let i = 0; i < e.length; i++) e[i] = 1 - e[i];
|
|
1178
|
+
return e;
|
|
1179
|
+
}
|
|
1180
|
+
function borderCoverage(m, w, h, r) {
|
|
1181
|
+
const inside = thresholdInside(m);
|
|
1182
|
+
const outside = new Uint8Array(inside.length);
|
|
1183
|
+
for (let i = 0; i < inside.length; i++) outside[i] = inside[i] ? 0 : 1;
|
|
1184
|
+
const out = new Float32Array(m.length);
|
|
1185
|
+
if (!(r > 0)) return out;
|
|
1186
|
+
const toInside = distanceTransform(inside, w, h);
|
|
1187
|
+
const toOutside = distanceTransform(outside, w, h);
|
|
1188
|
+
for (let i = 0; i < out.length; i++) {
|
|
1189
|
+
const d = inside[i] ? toOutside[i] : toInside[i];
|
|
1190
|
+
out[i] = Math.min(1, Math.max(0, 1 - (d - 1) / r));
|
|
1191
|
+
}
|
|
1192
|
+
return out;
|
|
1193
|
+
}
|
|
1194
|
+
function boxLineReplicate(src, srcOff, stride, len, r, dst) {
|
|
1195
|
+
const at = (i) => src[srcOff + Math.min(len - 1, Math.max(0, i)) * stride];
|
|
1196
|
+
let sum = 0;
|
|
1197
|
+
for (let i = -r; i <= r; i++) sum += at(i);
|
|
1198
|
+
const k = 2 * r + 1;
|
|
1199
|
+
for (let i = 0; i < len; i++) {
|
|
1200
|
+
dst[i] = sum / k;
|
|
1201
|
+
sum += at(i + r + 1) - at(i - r);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function smoothCoverage(m, w, h, r) {
|
|
1205
|
+
const rr = Number.isFinite(r) ? Math.max(0, Math.round(r)) : 0;
|
|
1206
|
+
const src = Float32Array.from(m);
|
|
1207
|
+
const tmp = new Float32Array(src.length);
|
|
1208
|
+
const line = new Float32Array(Math.max(w, h));
|
|
1209
|
+
if (rr > 0) {
|
|
1210
|
+
for (let y = 0; y < h; y++) {
|
|
1211
|
+
boxLineReplicate(src, y * w, 1, w, rr, line);
|
|
1212
|
+
tmp.set(line.subarray(0, w), y * w);
|
|
1213
|
+
}
|
|
1214
|
+
for (let x = 0; x < w; x++) {
|
|
1215
|
+
boxLineReplicate(tmp, x, w, h, rr, line);
|
|
1216
|
+
for (let y = 0; y < h; y++) src[y * w + x] = line[y];
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (let i = 0; i < src.length; i++) src[i] = src[i] >= 0.5 ? 1 : 0;
|
|
1220
|
+
return src;
|
|
1221
|
+
}
|
|
1222
|
+
function gaussianKernel(sigma) {
|
|
1223
|
+
const r = Math.ceil(sigma * 3);
|
|
1224
|
+
const k = new Float32Array(2 * r + 1);
|
|
1225
|
+
let s = 0;
|
|
1226
|
+
for (let i = -r; i <= r; i++) {
|
|
1227
|
+
const v = Math.exp(-(i * i) / (2 * sigma * sigma));
|
|
1228
|
+
k[i + r] = v;
|
|
1229
|
+
s += v;
|
|
1230
|
+
}
|
|
1231
|
+
for (let i = 0; i < k.length; i++) k[i] /= s;
|
|
1232
|
+
return k;
|
|
1233
|
+
}
|
|
1234
|
+
var DIRECT_GAUSSIAN_MAX_SIGMA = 8;
|
|
1235
|
+
function boxRadii(sigma) {
|
|
1236
|
+
const n = 3;
|
|
1237
|
+
const wIdeal = Math.sqrt(12 * sigma * sigma / n + 1);
|
|
1238
|
+
let wl = Math.floor(wIdeal);
|
|
1239
|
+
if (wl % 2 === 0) wl--;
|
|
1240
|
+
const wu = wl + 2;
|
|
1241
|
+
const mIdeal = (12 * sigma * sigma - n * wl * wl - 4 * n * wl - 3 * n) / (-4 * wl - 4);
|
|
1242
|
+
const m = Math.round(mIdeal);
|
|
1243
|
+
const sizes = [0, 1, 2].map((i) => i < m ? wl : wu);
|
|
1244
|
+
return sizes.map((s) => (s - 1) / 2);
|
|
1245
|
+
}
|
|
1246
|
+
function convolveLine(src, off, stride, len, k, edge, dst) {
|
|
1247
|
+
const r = (k.length - 1) / 2;
|
|
1248
|
+
for (let i = 0; i < len; i++) {
|
|
1249
|
+
let a = 0;
|
|
1250
|
+
const lo = i - r;
|
|
1251
|
+
for (let j = 0; j < k.length; j++) {
|
|
1252
|
+
let x = lo + j;
|
|
1253
|
+
if (x < 0 || x >= len) {
|
|
1254
|
+
if (edge === "zero") continue;
|
|
1255
|
+
x = x < 0 ? 0 : len - 1;
|
|
1256
|
+
}
|
|
1257
|
+
a += src[off + x * stride] * k[j];
|
|
1258
|
+
}
|
|
1259
|
+
dst[i] = a;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function boxesLine(src, off, stride, len, radii, edge, a, b, dst) {
|
|
1263
|
+
const pad = radii.reduce((s, r) => s + r, 0);
|
|
1264
|
+
const plen = len + 2 * pad;
|
|
1265
|
+
for (let i = 0; i < plen; i++) {
|
|
1266
|
+
const j = i - pad;
|
|
1267
|
+
a[i] = j < 0 ? edge === "zero" ? 0 : src[off] : j >= len ? edge === "zero" ? 0 : src[off + (len - 1) * stride] : src[off + j * stride];
|
|
1268
|
+
}
|
|
1269
|
+
let from = a;
|
|
1270
|
+
let to = b;
|
|
1271
|
+
for (const r of radii) {
|
|
1272
|
+
let sum = 0;
|
|
1273
|
+
for (let i = 0; i <= Math.min(r, plen - 1); i++) sum += from[i];
|
|
1274
|
+
const k = 2 * r + 1;
|
|
1275
|
+
for (let i = 0; i < plen; i++) {
|
|
1276
|
+
to[i] = sum / k;
|
|
1277
|
+
const add = i + r + 1;
|
|
1278
|
+
const drop = i - r;
|
|
1279
|
+
if (add < plen) sum += from[add];
|
|
1280
|
+
if (drop >= 0) sum -= from[drop];
|
|
1281
|
+
}
|
|
1282
|
+
const t = from;
|
|
1283
|
+
from = to;
|
|
1284
|
+
to = t;
|
|
1285
|
+
}
|
|
1286
|
+
for (let i = 0; i < len; i++) dst[i] = from[i + pad];
|
|
1287
|
+
}
|
|
1288
|
+
function gaussianBlur(src, w, h, sigma, edge = "zero") {
|
|
1289
|
+
const out = Float32Array.from(src);
|
|
1290
|
+
if (!(sigma >= 0.1) || w <= 0 || h <= 0) return out;
|
|
1291
|
+
const s = Math.min(sigma, 4 * Math.max(w, h));
|
|
1292
|
+
const line = new Float32Array(Math.max(w, h));
|
|
1293
|
+
const passes = [];
|
|
1294
|
+
if (s <= DIRECT_GAUSSIAN_MAX_SIGMA) {
|
|
1295
|
+
const k = gaussianKernel(s);
|
|
1296
|
+
passes.push((buf, off, stride, len) => convolveLine(buf, off, stride, len, k, edge, line));
|
|
1297
|
+
} else {
|
|
1298
|
+
const radii = boxRadii(s);
|
|
1299
|
+
const plen = Math.max(w, h) + 2 * radii.reduce((a2, r) => a2 + r, 0);
|
|
1300
|
+
const a = new Float32Array(plen);
|
|
1301
|
+
const b = new Float32Array(plen);
|
|
1302
|
+
passes.push((buf, off, stride, len) => boxesLine(buf, off, stride, len, radii, edge, a, b, line));
|
|
1303
|
+
}
|
|
1304
|
+
for (const pass of passes) {
|
|
1305
|
+
for (let y = 0; y < h; y++) {
|
|
1306
|
+
pass(out, y * w, 1, w);
|
|
1307
|
+
out.set(line.subarray(0, w), y * w);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
for (const pass of passes) {
|
|
1311
|
+
for (let x = 0; x < w; x++) {
|
|
1312
|
+
pass(out, x, w, h);
|
|
1313
|
+
for (let y = 0; y < h; y++) out[y * w + x] = line[y];
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return out;
|
|
1317
|
+
}
|
|
1318
|
+
function featherCoverage(m, w, h, radius) {
|
|
1319
|
+
const out = gaussianBlur(m, w, h, Number.isFinite(radius) ? Math.max(0, radius) : 0, "zero");
|
|
1320
|
+
for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) < 1e-7) out[i] = 0;
|
|
1321
|
+
return out;
|
|
1322
|
+
}
|
|
1323
|
+
function runningExtreme(src, off, stride, len, r, max, g, hb, dst) {
|
|
1324
|
+
const k = 2 * r + 1;
|
|
1325
|
+
const pick = max ? (a, b) => a > b ? a : b : (a, b) => a < b ? a : b;
|
|
1326
|
+
const fill = max ? -Infinity : Infinity;
|
|
1327
|
+
const plen = len + 2 * r;
|
|
1328
|
+
const at = (i) => i < r || i >= r + len ? fill : src[off + (i - r) * stride];
|
|
1329
|
+
for (let start = 0; start < plen; start += k) {
|
|
1330
|
+
const end = Math.min(plen, start + k);
|
|
1331
|
+
g[start] = at(start);
|
|
1332
|
+
for (let i = start + 1; i < end; i++) g[i] = pick(g[i - 1], at(i));
|
|
1333
|
+
hb[end - 1] = at(end - 1);
|
|
1334
|
+
for (let i = end - 2; i >= start; i--) hb[i] = pick(hb[i + 1], at(i));
|
|
1335
|
+
}
|
|
1336
|
+
for (let i = 0; i < len; i++) {
|
|
1337
|
+
const a = hb[i];
|
|
1338
|
+
const b = g[i + 2 * r];
|
|
1339
|
+
dst[i] = pick(a, b);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function greyMorphology(src, w, h, r, grow) {
|
|
1343
|
+
const out = Float32Array.from(src);
|
|
1344
|
+
if (r <= 0) return out;
|
|
1345
|
+
const n = Math.max(w, h);
|
|
1346
|
+
const g = new Float32Array(n + 2 * r);
|
|
1347
|
+
const hb = new Float32Array(n + 2 * r);
|
|
1348
|
+
const line = new Float32Array(n);
|
|
1349
|
+
for (let y = 0; y < h; y++) {
|
|
1350
|
+
runningExtreme(out, y * w, 1, w, r, grow, g, hb, line);
|
|
1351
|
+
out.set(line.subarray(0, w), y * w);
|
|
1352
|
+
}
|
|
1353
|
+
for (let x = 0; x < w; x++) {
|
|
1354
|
+
runningExtreme(out, x, w, h, r, grow, g, hb, line);
|
|
1355
|
+
for (let y = 0; y < h; y++) out[y * w + x] = line[y];
|
|
1356
|
+
}
|
|
1357
|
+
return out;
|
|
1358
|
+
}
|
|
1359
|
+
function shiftCoverage(src, w, h, amount) {
|
|
1360
|
+
const mag = Math.abs(amount);
|
|
1361
|
+
if (!Number.isFinite(mag) || mag < 1e-4) return Float32Array.from(src);
|
|
1362
|
+
const grow = amount > 0;
|
|
1363
|
+
const whole = Math.floor(mag);
|
|
1364
|
+
const frac = mag - whole;
|
|
1365
|
+
if (frac < 1e-4) return greyMorphology(src, w, h, whole, grow);
|
|
1366
|
+
const lo = whole === 0 ? Float32Array.from(src) : greyMorphology(src, w, h, whole, grow);
|
|
1367
|
+
const hi = greyMorphology(src, w, h, whole + 1, grow);
|
|
1368
|
+
for (let i = 0; i < lo.length; i++) lo[i] += (hi[i] - lo[i]) * frac;
|
|
1369
|
+
return lo;
|
|
1370
|
+
}
|
|
1371
|
+
var SELECTION_MODIFY_OPS = ["grow", "shrink", "feather", "border", "smooth"];
|
|
1372
|
+
function wrap(mask, coverage) {
|
|
1373
|
+
const out = new Uint8Array(coverage.length);
|
|
1374
|
+
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
1375
|
+
return createSelectionMask(mask.width, mask.height, out);
|
|
1376
|
+
}
|
|
1377
|
+
function modifySelection(mask, op, radius) {
|
|
1378
|
+
const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
|
|
1379
|
+
if (r === 0 && op !== "border") return mask;
|
|
1380
|
+
const m = selectionToCoverage(mask);
|
|
1381
|
+
const { width: w, height: h } = mask;
|
|
1382
|
+
switch (op) {
|
|
1383
|
+
case "grow":
|
|
1384
|
+
return wrap(mask, expandCoverage(m, w, h, r));
|
|
1385
|
+
case "shrink":
|
|
1386
|
+
return wrap(mask, contractCoverage(m, w, h, r));
|
|
1387
|
+
case "feather":
|
|
1388
|
+
return wrap(mask, featherCoverage(m, w, h, r));
|
|
1389
|
+
case "border":
|
|
1390
|
+
return wrap(mask, borderCoverage(m, w, h, r));
|
|
1391
|
+
case "smooth":
|
|
1392
|
+
return wrap(mask, smoothCoverage(m, w, h, r));
|
|
1393
|
+
default:
|
|
1394
|
+
throw new Error(`Unknown selection modification "${op}"`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/image-editor/selection/layer-mask.ts
|
|
1399
|
+
var MAX_SIDE2 = 1 << 15;
|
|
1400
|
+
function normalizeLayerMask(raw) {
|
|
1401
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1402
|
+
const r = raw;
|
|
1403
|
+
const { width, height, rle } = r;
|
|
1404
|
+
if (typeof width !== "number" || typeof height !== "number" || !Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE2 || height > MAX_SIDE2 || typeof rle !== "string" || !rle.startsWith("r1:")) {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
const out = { width, height, rle };
|
|
1408
|
+
if (typeof r.density === "number" && Number.isFinite(r.density)) {
|
|
1409
|
+
const d = Math.min(1, Math.max(0, r.density));
|
|
1410
|
+
if (d !== 1) out.density = d;
|
|
1411
|
+
}
|
|
1412
|
+
if (typeof r.feather === "number" && Number.isFinite(r.feather)) {
|
|
1413
|
+
const f = Math.min(1e3, Math.max(0, r.feather));
|
|
1414
|
+
if (f !== 0) out.feather = f;
|
|
1415
|
+
}
|
|
1416
|
+
if (r.enabled === false) out.enabled = false;
|
|
1417
|
+
return out;
|
|
1418
|
+
}
|
|
1419
|
+
function layerMaskFromSelection(selection, props = {}) {
|
|
1420
|
+
return normalizeLayerMask({ width: selection.width, height: selection.height, rle: encodeSelectionRle(selection.data), ...props });
|
|
1421
|
+
}
|
|
1422
|
+
function updateLayerMaskProps(mask, props) {
|
|
1423
|
+
return normalizeLayerMask({
|
|
1424
|
+
...mask,
|
|
1425
|
+
...props.density !== void 0 ? { density: props.density } : {},
|
|
1426
|
+
...props.feather !== void 0 ? { feather: props.feather } : {},
|
|
1427
|
+
...props.enabled !== void 0 ? { enabled: props.enabled } : {}
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
var DECODE_LIMIT = 8;
|
|
1431
|
+
var decoded = /* @__PURE__ */ new Map();
|
|
1432
|
+
var coverageCache = /* @__PURE__ */ new Map();
|
|
1433
|
+
function lru(cache, key, make) {
|
|
1434
|
+
if (cache.has(key)) {
|
|
1435
|
+
const v2 = cache.get(key);
|
|
1436
|
+
cache.delete(key);
|
|
1437
|
+
cache.set(key, v2);
|
|
1438
|
+
return v2;
|
|
1439
|
+
}
|
|
1440
|
+
const v = make();
|
|
1441
|
+
cache.set(key, v);
|
|
1442
|
+
if (cache.size > DECODE_LIMIT) cache.delete(cache.keys().next().value);
|
|
1443
|
+
return v;
|
|
1444
|
+
}
|
|
1445
|
+
function decodeLayerMask(mask) {
|
|
1446
|
+
const key = `${mask.width}x${mask.height}:${mask.rle}`;
|
|
1447
|
+
return lru(decoded, key, () => {
|
|
1448
|
+
try {
|
|
1449
|
+
return createSelectionMask(mask.width, mask.height, decodeSelectionRle(mask.rle, mask.width * mask.height));
|
|
1450
|
+
} catch {
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
function layerMaskKey(mask) {
|
|
1456
|
+
if (!mask) return "";
|
|
1457
|
+
return `${mask.width}x${mask.height}:${hashMaskContent(mask.rle)}:${mask.density ?? 1}:${mask.feather ?? 0}:${mask.enabled !== false}`;
|
|
1458
|
+
}
|
|
1459
|
+
function layerMaskCoverage(mask) {
|
|
1460
|
+
if (mask.enabled === false) return null;
|
|
1461
|
+
return lru(coverageCache, layerMaskKey(mask), () => {
|
|
1462
|
+
const sel = decodeLayerMask(mask);
|
|
1463
|
+
if (!sel) return null;
|
|
1464
|
+
let v = new Float32Array(sel.data.length);
|
|
1465
|
+
for (let i = 0; i < v.length; i++) v[i] = sel.data[i] / 255;
|
|
1466
|
+
const feather = mask.feather ?? 0;
|
|
1467
|
+
if (feather > 0) v = gaussianBlur(v, mask.width, mask.height, feather, "clamp");
|
|
1468
|
+
const density = mask.density ?? 1;
|
|
1469
|
+
if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);
|
|
1470
|
+
return v;
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
function projectMaskCoverage(coverage, maskWidth, maskHeight, naturalWidth, naturalHeight, projection, targetWidth, targetHeight) {
|
|
1474
|
+
const out = new Float32Array(targetWidth * targetHeight);
|
|
1475
|
+
const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, projection);
|
|
1476
|
+
const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, projection);
|
|
1477
|
+
const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, projection);
|
|
1478
|
+
const kx = maskWidth / naturalWidth;
|
|
1479
|
+
const ky = maskHeight / naturalHeight;
|
|
1480
|
+
const ux = (ox.x - o.x) * kx;
|
|
1481
|
+
const vx = (ox.y - o.y) * ky;
|
|
1482
|
+
const uy = (oy.x - o.x) * kx;
|
|
1483
|
+
const vy = (oy.y - o.y) * ky;
|
|
1484
|
+
const u0 = o.x * kx - 0.5;
|
|
1485
|
+
const v0 = o.y * ky - 0.5;
|
|
1486
|
+
const mw1 = maskWidth - 1;
|
|
1487
|
+
const mh1 = maskHeight - 1;
|
|
1488
|
+
for (let y = 0; y < targetHeight; y++) {
|
|
1489
|
+
let u = u0 + y * uy;
|
|
1490
|
+
let v = v0 + y * vy;
|
|
1491
|
+
const row = y * targetWidth;
|
|
1492
|
+
for (let x = 0; x < targetWidth; x++, u += ux, v += vx) {
|
|
1493
|
+
const fu = u < 0 ? 0 : u > mw1 ? mw1 : u;
|
|
1494
|
+
const fv = v < 0 ? 0 : v > mh1 ? mh1 : v;
|
|
1495
|
+
const x0 = fu | 0;
|
|
1496
|
+
const y0 = fv | 0;
|
|
1497
|
+
const x1 = x0 < mw1 ? x0 + 1 : x0;
|
|
1498
|
+
const y1 = y0 < mh1 ? y0 + 1 : y0;
|
|
1499
|
+
const tx = fu - x0;
|
|
1500
|
+
const ty = fv - y0;
|
|
1501
|
+
const a = coverage[y0 * maskWidth + x0] * (1 - tx) + coverage[y0 * maskWidth + x1] * tx;
|
|
1502
|
+
const b = coverage[y1 * maskWidth + x0] * (1 - tx) + coverage[y1 * maskWidth + x1] * tx;
|
|
1503
|
+
out[row + x] = a * (1 - ty) + b * ty;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return out;
|
|
1507
|
+
}
|
|
1508
|
+
function buildLayerMaskPlanes(layers, naturalWidth, naturalHeight, projection, targetWidth, targetHeight) {
|
|
1509
|
+
if (!layers?.some((l) => l.mask && l.mask.enabled !== false)) return void 0;
|
|
1510
|
+
const planes = /* @__PURE__ */ new Map();
|
|
1511
|
+
for (const layer of layers) {
|
|
1512
|
+
if (!layer.mask) continue;
|
|
1513
|
+
const coverage = layerMaskCoverage(layer.mask);
|
|
1514
|
+
if (!coverage) continue;
|
|
1515
|
+
planes.set(
|
|
1516
|
+
layer.id,
|
|
1517
|
+
projectMaskCoverage(coverage, layer.mask.width, layer.mask.height, naturalWidth, naturalHeight, projection, targetWidth, targetHeight)
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
return planes.size ? planes : void 0;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/image-editor/adjustment-layers.ts
|
|
1524
|
+
var ADJUSTMENT_LUT_SIZE = 4096;
|
|
1525
|
+
var clamp01 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
1526
|
+
var finite = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
1527
|
+
var clampTo = (v, lo, hi, fallback) => Math.min(hi, Math.max(lo, finite(v, fallback)));
|
|
1528
|
+
var remEuclid = (a, b) => (a % b + b) % b;
|
|
1529
|
+
function sampleLut(table, v) {
|
|
1530
|
+
const n = table.length - 1;
|
|
1531
|
+
const x = clamp01(v) * n;
|
|
1532
|
+
const i = Math.floor(x);
|
|
1533
|
+
const j = Math.min(i + 1, n);
|
|
1534
|
+
const f = x - i;
|
|
1535
|
+
return table[i] * (1 - f) + table[j] * f;
|
|
1536
|
+
}
|
|
1537
|
+
var ADJUSTMENT_KINDS = [
|
|
1538
|
+
{ kind: "curves", label: "Curves" },
|
|
1539
|
+
{ kind: "levels", label: "Levels" },
|
|
1540
|
+
{ kind: "hueSaturation", label: "Hue/Saturation" },
|
|
1541
|
+
{ kind: "vibrance", label: "Vibrance" },
|
|
1542
|
+
{ kind: "colorBalance", label: "Color Balance" },
|
|
1543
|
+
{ kind: "blackWhite", label: "Black & White" },
|
|
1544
|
+
{ kind: "photoFilter", label: "Photo Filter" },
|
|
1545
|
+
{ kind: "invert", label: "Invert" },
|
|
1546
|
+
{ kind: "posterize", label: "Posterize" },
|
|
1547
|
+
{ kind: "threshold", label: "Threshold" }
|
|
1548
|
+
];
|
|
1549
|
+
var KIND_LABELS = new Map(ADJUSTMENT_KINDS.map((k) => [k.kind, k.label]));
|
|
1550
|
+
function isAdjustmentKind(value) {
|
|
1551
|
+
return typeof value === "string" && KIND_LABELS.has(value);
|
|
1552
|
+
}
|
|
1553
|
+
function adjustmentKindLabel(kind) {
|
|
1554
|
+
return KIND_LABELS.get(kind) ?? kind;
|
|
1555
|
+
}
|
|
1556
|
+
var HUE_RANGE_LABELS = ["Reds", "Yellows", "Greens", "Cyans", "Blues", "Magentas"];
|
|
1557
|
+
function neutralHueRange(i) {
|
|
1558
|
+
const c = 60 * i;
|
|
1559
|
+
return { hue: 0, saturation: 0, lightness: 0, bounds: [c - 45, c - 15, c + 15, c + 45] };
|
|
1560
|
+
}
|
|
1561
|
+
var identityCurve = () => [
|
|
1562
|
+
{ input: 0, output: 0 },
|
|
1563
|
+
{ input: 255, output: 255 }
|
|
1564
|
+
];
|
|
1565
|
+
var identityLevels = () => ({ inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 });
|
|
1566
|
+
function defaultAdjustmentParams(kind) {
|
|
1567
|
+
const defaults = {
|
|
1568
|
+
curves: () => ({ master: identityCurve(), red: identityCurve(), green: identityCurve(), blue: identityCurve() }),
|
|
1569
|
+
levels: () => ({ master: identityLevels(), red: identityLevels(), green: identityLevels(), blue: identityLevels() }),
|
|
1570
|
+
hueSaturation: () => ({
|
|
1571
|
+
hue: 0,
|
|
1572
|
+
saturation: 0,
|
|
1573
|
+
lightness: 0,
|
|
1574
|
+
colorize: false,
|
|
1575
|
+
ranges: [0, 1, 2, 3, 4, 5].map(neutralHueRange)
|
|
1576
|
+
}),
|
|
1577
|
+
vibrance: () => ({ vibrance: 0, saturation: 0 }),
|
|
1578
|
+
colorBalance: () => ({ shadows: [0, 0, 0], midtones: [0, 0, 0], highlights: [0, 0, 0], preserveLuminosity: true }),
|
|
1579
|
+
blackWhite: () => ({ reds: 40, yellows: 60, greens: 40, cyans: 60, blues: 20, magentas: 80, tint: null }),
|
|
1580
|
+
invert: () => ({}),
|
|
1581
|
+
posterize: () => ({ levels: 4 }),
|
|
1582
|
+
threshold: () => ({ level: 128 }),
|
|
1583
|
+
photoFilter: () => ({ color: "#ec8a00", density: 25, preserveLuminosity: true })
|
|
1584
|
+
};
|
|
1585
|
+
return defaults[kind]();
|
|
1586
|
+
}
|
|
1587
|
+
var layerCounter = 0;
|
|
1588
|
+
function createAdjustmentLayer(kind, options = {}) {
|
|
1589
|
+
const raw = {
|
|
1590
|
+
id: options.id ?? `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,
|
|
1591
|
+
kind,
|
|
1592
|
+
name: options.name,
|
|
1593
|
+
params: { ...defaultAdjustmentParams(kind), ...options.params ?? {} },
|
|
1594
|
+
enabled: options.enabled ?? true,
|
|
1595
|
+
opacity: options.opacity ?? 1,
|
|
1596
|
+
blend: options.blend ?? "normal",
|
|
1597
|
+
mask: options.mask
|
|
1598
|
+
};
|
|
1599
|
+
return normalizeAdjustmentLayer(raw);
|
|
1600
|
+
}
|
|
1601
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
1602
|
+
function normCurve(points) {
|
|
1603
|
+
if (!Array.isArray(points)) return identityCurve();
|
|
1604
|
+
const out = points.filter((p) => p && typeof p === "object").map((p) => ({ input: clampTo(p.input, 0, 255, 0), output: clampTo(p.output, 0, 255, 0) })).sort((a, b) => a.input - b.input);
|
|
1605
|
+
return out.length >= 2 ? out : identityCurve();
|
|
1606
|
+
}
|
|
1607
|
+
function normLevels(ch) {
|
|
1608
|
+
const c = ch && typeof ch === "object" ? ch : {};
|
|
1609
|
+
return {
|
|
1610
|
+
inBlack: clampTo(c.inBlack, 0, 254, 0),
|
|
1611
|
+
inWhite: clampTo(c.inWhite, 1, 255, 255),
|
|
1612
|
+
gamma: clampTo(c.gamma, 0.1, 9.99, 1),
|
|
1613
|
+
outBlack: clampTo(c.outBlack, 0, 255, 0),
|
|
1614
|
+
outWhite: clampTo(c.outWhite, 0, 255, 255)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function normTone(t) {
|
|
1618
|
+
const a = Array.isArray(t) ? t : [];
|
|
1619
|
+
return [clampTo(a[0], -100, 100, 0), clampTo(a[1], -100, 100, 0), clampTo(a[2], -100, 100, 0)];
|
|
1620
|
+
}
|
|
1621
|
+
function normParams(kind, raw) {
|
|
1622
|
+
const p = raw && typeof raw === "object" ? raw : {};
|
|
1623
|
+
const d = defaultAdjustmentParams(kind);
|
|
1624
|
+
switch (kind) {
|
|
1625
|
+
case "curves":
|
|
1626
|
+
return { master: normCurve(p.master), red: normCurve(p.red), green: normCurve(p.green), blue: normCurve(p.blue) };
|
|
1627
|
+
case "levels":
|
|
1628
|
+
return { master: normLevels(p.master), red: normLevels(p.red), green: normLevels(p.green), blue: normLevels(p.blue) };
|
|
1629
|
+
case "hueSaturation": {
|
|
1630
|
+
const ranges = [0, 1, 2, 3, 4, 5].map((i) => {
|
|
1631
|
+
const r = Array.isArray(p.ranges) && p.ranges[i] && typeof p.ranges[i] === "object" ? p.ranges[i] : {};
|
|
1632
|
+
const n = neutralHueRange(i);
|
|
1633
|
+
const b = Array.isArray(r.bounds) && r.bounds.length === 4 && r.bounds.every((v) => Number.isFinite(v)) ? r.bounds : n.bounds;
|
|
1634
|
+
return {
|
|
1635
|
+
hue: clampTo(r.hue, -180, 180, 0),
|
|
1636
|
+
saturation: clampTo(r.saturation, -100, 100, 0),
|
|
1637
|
+
lightness: clampTo(r.lightness, -100, 100, 0),
|
|
1638
|
+
bounds: [b[0], b[1], b[2], b[3]]
|
|
1639
|
+
};
|
|
1640
|
+
});
|
|
1641
|
+
const colorize = p.colorize === true;
|
|
1642
|
+
return {
|
|
1643
|
+
hue: colorize ? clampTo(p.hue, 0, 360, 0) : clampTo(p.hue, -180, 180, 0),
|
|
1644
|
+
saturation: clampTo(p.saturation, -100, 100, 0),
|
|
1645
|
+
lightness: clampTo(p.lightness, -100, 100, 0),
|
|
1646
|
+
colorize,
|
|
1647
|
+
ranges
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
case "vibrance":
|
|
1651
|
+
return { vibrance: clampTo(p.vibrance, -100, 100, 0), saturation: clampTo(p.saturation, -100, 100, 0) };
|
|
1652
|
+
case "colorBalance":
|
|
1653
|
+
return {
|
|
1654
|
+
shadows: normTone(p.shadows),
|
|
1655
|
+
midtones: normTone(p.midtones),
|
|
1656
|
+
highlights: normTone(p.highlights),
|
|
1657
|
+
preserveLuminosity: p.preserveLuminosity !== false
|
|
1658
|
+
};
|
|
1659
|
+
case "blackWhite": {
|
|
1660
|
+
const out = {};
|
|
1661
|
+
for (const k of ["reds", "yellows", "greens", "cyans", "blues", "magentas"]) out[k] = clampTo(p[k], -200, 300, d[k]);
|
|
1662
|
+
out.tint = typeof p.tint === "string" && HEX.test(p.tint) ? p.tint.toLowerCase() : null;
|
|
1663
|
+
return out;
|
|
1664
|
+
}
|
|
1665
|
+
case "photoFilter":
|
|
1666
|
+
return {
|
|
1667
|
+
color: typeof p.color === "string" && HEX.test(p.color) ? p.color.toLowerCase() : d.color,
|
|
1668
|
+
density: clampTo(p.density, 0, 100, d.density),
|
|
1669
|
+
preserveLuminosity: p.preserveLuminosity !== false
|
|
1670
|
+
};
|
|
1671
|
+
case "posterize":
|
|
1672
|
+
return { levels: Math.round(clampTo(p.levels, 2, 255, 4)) };
|
|
1673
|
+
case "threshold":
|
|
1674
|
+
return { level: Math.round(clampTo(p.level, 1, 255, 128)) };
|
|
1675
|
+
case "invert":
|
|
1676
|
+
return {};
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function normalizeAdjustmentLayer(raw) {
|
|
1680
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1681
|
+
const r = raw;
|
|
1682
|
+
if (!isAdjustmentKind(r.kind)) return null;
|
|
1683
|
+
const layer = {
|
|
1684
|
+
id: typeof r.id === "string" && r.id ? r.id : `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,
|
|
1685
|
+
kind: r.kind,
|
|
1686
|
+
enabled: r.enabled !== false,
|
|
1687
|
+
opacity: clampTo(r.opacity, 0, 1, 1),
|
|
1688
|
+
blend: normalizeBlendMode(r.blend),
|
|
1689
|
+
params: normParams(r.kind, r.params)
|
|
1690
|
+
};
|
|
1691
|
+
if (typeof r.name === "string" && r.name) layer.name = r.name;
|
|
1692
|
+
const mask = normalizeLayerMask(r.mask);
|
|
1693
|
+
if (mask) layer.mask = mask;
|
|
1694
|
+
return layer;
|
|
1695
|
+
}
|
|
1696
|
+
function normalizeAdjustmentLayers(raw) {
|
|
1697
|
+
if (!Array.isArray(raw)) return [];
|
|
1698
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1699
|
+
const out = [];
|
|
1700
|
+
for (const item of raw) {
|
|
1701
|
+
const layer = normalizeAdjustmentLayer(item);
|
|
1702
|
+
if (!layer) continue;
|
|
1703
|
+
while (seen.has(layer.id)) layer.id = `${layer.id}_${(++layerCounter).toString(36)}`;
|
|
1704
|
+
seen.add(layer.id);
|
|
1705
|
+
out.push(layer);
|
|
1706
|
+
}
|
|
1707
|
+
return out;
|
|
1708
|
+
}
|
|
1709
|
+
function curveLut(points, size = ADJUSTMENT_LUT_SIZE) {
|
|
1710
|
+
const pts = points.filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output)).map((p) => [p.input, p.output]).sort((a, b) => a[0] - b[0]);
|
|
1711
|
+
const dedup = [];
|
|
1712
|
+
for (const p of pts) {
|
|
1713
|
+
if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;
|
|
1714
|
+
dedup.push(p);
|
|
1715
|
+
}
|
|
1716
|
+
const out = new Float32Array(size);
|
|
1717
|
+
const n = dedup.length;
|
|
1718
|
+
if (n < 2) {
|
|
1719
|
+
for (let k = 0; k < size; k++) out[k] = k / (size - 1);
|
|
1720
|
+
return out;
|
|
1721
|
+
}
|
|
1722
|
+
const x = dedup.map((p) => p[0]);
|
|
1723
|
+
const y = dedup.map((p) => p[1]);
|
|
1724
|
+
const m2 = new Float64Array(n);
|
|
1725
|
+
if (n > 2) {
|
|
1726
|
+
const c = new Float64Array(n);
|
|
1727
|
+
const d = new Float64Array(n);
|
|
1728
|
+
for (let i = 1; i < n - 1; i++) {
|
|
1729
|
+
const h0 = x[i] - x[i - 1];
|
|
1730
|
+
const h1 = x[i + 1] - x[i];
|
|
1731
|
+
const a = h0 / 6;
|
|
1732
|
+
const b = (h0 + h1) / 3;
|
|
1733
|
+
const cc = h1 / 6;
|
|
1734
|
+
const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;
|
|
1735
|
+
const denom = b - a * c[i - 1];
|
|
1736
|
+
c[i] = cc / denom;
|
|
1737
|
+
d[i] = (r - a * d[i - 1]) / denom;
|
|
1738
|
+
}
|
|
1739
|
+
for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];
|
|
1740
|
+
}
|
|
1741
|
+
let seg = 0;
|
|
1742
|
+
for (let k = 0; k < size; k++) {
|
|
1743
|
+
const xv = k / (size - 1);
|
|
1744
|
+
if (xv <= x[0]) {
|
|
1745
|
+
out[k] = clamp01(y[0]);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
if (xv >= x[n - 1]) {
|
|
1749
|
+
out[k] = clamp01(y[n - 1]);
|
|
1750
|
+
continue;
|
|
1751
|
+
}
|
|
1752
|
+
while (seg < n - 2 && xv > x[seg + 1]) seg++;
|
|
1753
|
+
const x0 = x[seg], x1 = x[seg + 1];
|
|
1754
|
+
const h = x1 - x0;
|
|
1755
|
+
const a = (x1 - xv) / h;
|
|
1756
|
+
const b = (xv - x0) / h;
|
|
1757
|
+
const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;
|
|
1758
|
+
out[k] = clamp01(v);
|
|
1759
|
+
}
|
|
1760
|
+
return out;
|
|
1761
|
+
}
|
|
1762
|
+
function curveLutFromLevels(points) {
|
|
1763
|
+
return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));
|
|
1764
|
+
}
|
|
1765
|
+
var toUnitLevels = (c) => ({
|
|
1766
|
+
inBlack: c.inBlack / 255,
|
|
1767
|
+
inWhite: c.inWhite / 255,
|
|
1768
|
+
gamma: c.gamma,
|
|
1769
|
+
outBlack: c.outBlack / 255,
|
|
1770
|
+
outWhite: c.outWhite / 255
|
|
1771
|
+
});
|
|
1772
|
+
function levelsValue(ch, v, quantum) {
|
|
1773
|
+
const range = Math.max(1e-6, ch.inWhite - ch.inBlack);
|
|
1774
|
+
let u;
|
|
1775
|
+
if (quantum !== void 0 && Number.isFinite(quantum) && quantum >= 1) {
|
|
1776
|
+
const q = quantum;
|
|
1777
|
+
const vq = Math.round(v * q) / q;
|
|
1778
|
+
u = Math.floor(clamp01((vq - ch.inBlack) / range) * q + 0.5 + 1e-3) / q;
|
|
1779
|
+
} else {
|
|
1780
|
+
u = clamp01((v - ch.inBlack) / range);
|
|
1781
|
+
}
|
|
1782
|
+
const g = Math.max(0.01, ch.gamma);
|
|
1783
|
+
let t;
|
|
1784
|
+
if (g > 1) {
|
|
1785
|
+
const power = Math.pow(u, 1 / g);
|
|
1786
|
+
const line = 0.93 * Math.pow(2, g) * u;
|
|
1787
|
+
if (power <= 1e-6 || line <= 1e-6) t = Math.min(power, line);
|
|
1788
|
+
else {
|
|
1789
|
+
const p = 10;
|
|
1790
|
+
t = Math.pow(Math.pow(power, -p) + Math.pow(line, -p), -1 / p);
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
t = Math.pow(u, 1 / g);
|
|
1794
|
+
}
|
|
1795
|
+
return ch.outBlack + t * (ch.outWhite - ch.outBlack);
|
|
1796
|
+
}
|
|
1797
|
+
function rgbToHsl(c) {
|
|
1798
|
+
const max = Math.max(c[0], c[1], c[2]);
|
|
1799
|
+
const min = Math.min(c[0], c[1], c[2]);
|
|
1800
|
+
const l = (max + min) / 2;
|
|
1801
|
+
if (Math.abs(max - min) < 1e-7) return [0, 0, l];
|
|
1802
|
+
const d = max - min;
|
|
1803
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1804
|
+
let h;
|
|
1805
|
+
if (max === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);
|
|
1806
|
+
else if (max === c[1]) h = (c[2] - c[0]) / d + 2;
|
|
1807
|
+
else h = (c[0] - c[1]) / d + 4;
|
|
1808
|
+
return [h / 6, s, l];
|
|
1809
|
+
}
|
|
1810
|
+
function hslToRgb(h, s, l) {
|
|
1811
|
+
if (s <= 0) return [l, l, l];
|
|
1812
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
1813
|
+
const p = 2 * l - q;
|
|
1814
|
+
const f = (t) => {
|
|
1815
|
+
t = remEuclid(t, 1);
|
|
1816
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
1817
|
+
if (t < 0.5) return q;
|
|
1818
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
1819
|
+
return p;
|
|
1820
|
+
};
|
|
1821
|
+
return [f(h + 1 / 3), f(h), f(h - 1 / 3)];
|
|
1822
|
+
}
|
|
1823
|
+
function hueRangeWeight(range, deg) {
|
|
1824
|
+
const [a, b0, c0, d0] = range.bounds;
|
|
1825
|
+
const rel = (x) => Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0;
|
|
1826
|
+
const b = rel(b0);
|
|
1827
|
+
const c = Math.max(rel(c0), b);
|
|
1828
|
+
const d = Math.max(rel(d0), c);
|
|
1829
|
+
const h = rel(deg);
|
|
1830
|
+
if (h <= b) return b <= 0 ? 1 : h / b;
|
|
1831
|
+
if (h <= c) return 1;
|
|
1832
|
+
if (h <= d) return d <= c ? 1 : (d - h) / (d - c);
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
var isNeutralRange = (r) => r.hue === 0 && r.saturation === 0 && r.lightness === 0;
|
|
1836
|
+
function hueRangeTables(ranges) {
|
|
1837
|
+
const n = ADJUSTMENT_LUT_SIZE;
|
|
1838
|
+
const t = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
|
|
1839
|
+
const active = ranges.filter((r) => !isNeutralRange(r));
|
|
1840
|
+
for (let k = 0; k < n; k++) {
|
|
1841
|
+
const deg = k / (n - 1) * 360;
|
|
1842
|
+
for (const r of active) {
|
|
1843
|
+
const w = hueRangeWeight(r, deg);
|
|
1844
|
+
t[0][k] += w * r.hue;
|
|
1845
|
+
t[1][k] += w * r.saturation / 100;
|
|
1846
|
+
t[2][k] += w * r.lightness / 100;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return t;
|
|
1850
|
+
}
|
|
1851
|
+
function hueSaturation(c, hue, s, l, colorize) {
|
|
1852
|
+
const lighten = (v) => l > 0 ? v + (1 - v) * l : v * (1 + l);
|
|
1853
|
+
if (colorize) {
|
|
1854
|
+
const ll2 = rgbToHsl(c)[2];
|
|
1855
|
+
return hslToRgb(remEuclid(hue, 360) / 360, Math.min(1, Math.abs(s)), lighten(ll2));
|
|
1856
|
+
}
|
|
1857
|
+
const [hh0, ss0, ll] = rgbToHsl([lighten(c[0]), lighten(c[1]), lighten(c[2])]);
|
|
1858
|
+
const hh = remEuclid(hh0 + hue / 360, 1);
|
|
1859
|
+
const ss = Math.min(1, Math.max(0, s > 0 ? ss0 / Math.max(1 / 512, 1 - s) : ss0 * (1 + s)));
|
|
1860
|
+
return hslToRgb(hh, ss, ll);
|
|
1861
|
+
}
|
|
1862
|
+
function rangeLightness(c, l) {
|
|
1863
|
+
const k = Math.min(1, Math.max(-1, l));
|
|
1864
|
+
const target = k > 0 ? Math.max(c[0], c[1], c[2]) : Math.min(c[0], c[1], c[2]);
|
|
1865
|
+
const a = Math.abs(k);
|
|
1866
|
+
return [c[0] + (target - c[0]) * a, c[1] + (target - c[1]) * a, c[2] + (target - c[2]) * a];
|
|
1867
|
+
}
|
|
1868
|
+
var VIBRANCE_GREY = [0.288, 0.712];
|
|
1869
|
+
var VIBRANCE_BOOST = [0.5001, 0.0887, 0.4374, 0.7573, -0.8113, 0.5154, 2.8026];
|
|
1870
|
+
function vibranceSkin(h) {
|
|
1871
|
+
if (h <= 30) return 1;
|
|
1872
|
+
if (h < 45) return (45 - h) / 15;
|
|
1873
|
+
if (h < 300) return 0;
|
|
1874
|
+
return (h - 300) / 60;
|
|
1875
|
+
}
|
|
1876
|
+
function hsvHue(c, mx, mn) {
|
|
1877
|
+
const d = mx - mn;
|
|
1878
|
+
let h;
|
|
1879
|
+
if (mx === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);
|
|
1880
|
+
else if (mx === c[1]) h = (c[2] - c[0]) / d + 2;
|
|
1881
|
+
else h = (c[0] - c[1]) / d + 4;
|
|
1882
|
+
return h * 60;
|
|
1883
|
+
}
|
|
1884
|
+
function vibranceLinear(c, v) {
|
|
1885
|
+
const mx = Math.max(c[0], c[1], c[2]);
|
|
1886
|
+
const mn = Math.min(c[0], c[1], c[2]);
|
|
1887
|
+
if (mx <= 0 || mx - mn <= 1e-9) return c;
|
|
1888
|
+
const t = 1 - mn / mx;
|
|
1889
|
+
const shape = t * (1 - t) * (2 - t + t * t) * mx * Math.max(0, 1 - mx);
|
|
1890
|
+
let t2, v2;
|
|
1891
|
+
if (v < 0) {
|
|
1892
|
+
const a = -v;
|
|
1893
|
+
t2 = t * (1 - a / 4) * (1 - a + a * t * (1 + t) / 2);
|
|
1894
|
+
v2 = mx - a * shape;
|
|
1895
|
+
} else {
|
|
1896
|
+
const [c0, c1, p, q, r0, ra, rt] = VIBRANCE_BOOST;
|
|
1897
|
+
const w = vibranceSkin(hsvHue(c, mx, mn));
|
|
1898
|
+
const a = v * (1 - w) + 0.64 * Math.pow(v, 1.6) * w;
|
|
1899
|
+
const boost = (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));
|
|
1900
|
+
t2 = Math.min(1, t * (1 + boost));
|
|
1901
|
+
v2 = mx + v / 4 * shape;
|
|
1902
|
+
}
|
|
1903
|
+
const m2 = v2 * (1 - t2);
|
|
1904
|
+
const k = (v2 - m2) / (mx - mn);
|
|
1905
|
+
return [m2 + (c[0] - mn) * k, m2 + (c[1] - mn) * k, m2 + (c[2] - mn) * k];
|
|
1906
|
+
}
|
|
1907
|
+
function vibrancePixel(c, v, s) {
|
|
1908
|
+
let l = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];
|
|
1909
|
+
if (v !== 0) l = vibranceLinear(l, v);
|
|
1910
|
+
if (s !== 0) {
|
|
1911
|
+
const g = VIBRANCE_GREY[0] * l[0] + VIBRANCE_GREY[1] * l[1];
|
|
1912
|
+
l = l.map((x) => Math.min(1, Math.max(0, g + (1 + s) * (x - g))));
|
|
1913
|
+
}
|
|
1914
|
+
return l.map((x) => linearToSrgb(clamp01(x)));
|
|
1915
|
+
}
|
|
1916
|
+
var BALANCE_LEVELS = 0.39;
|
|
1917
|
+
function colorBalanceChannel(tones, preserveLuminosity, ch, v) {
|
|
1918
|
+
const [shadows, midtones, highlights] = tones;
|
|
1919
|
+
const black = (x, b) => Math.max(0, (x - b) / (1 - b));
|
|
1920
|
+
const white = (x, w) => Math.min(1, x / w);
|
|
1921
|
+
let out = clamp01(v);
|
|
1922
|
+
for (const [tone2, sliders] of [
|
|
1923
|
+
[2, highlights],
|
|
1924
|
+
[0, shadows],
|
|
1925
|
+
[1, midtones]
|
|
1926
|
+
]) {
|
|
1927
|
+
const a = [0, 1, 2].map((i) => Math.min(1, Math.max(-1, sliders[i] / 100)));
|
|
1928
|
+
if (preserveLuminosity) {
|
|
1929
|
+
const own = a[ch] ?? 0;
|
|
1930
|
+
const lo = Math.min(a[0], a[1], a[2]);
|
|
1931
|
+
const hi = Math.max(a[0], a[1], a[2]);
|
|
1932
|
+
if (tone2 === 0) out = black(out, BALANCE_LEVELS * (hi - own));
|
|
1933
|
+
else if (tone2 === 1) out = Math.pow(out, Math.pow(2, (hi + lo) / 2 - own));
|
|
1934
|
+
else out = white(out, 1 - BALANCE_LEVELS * (own - lo));
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
const s = a[ch];
|
|
1938
|
+
if (s === 0) continue;
|
|
1939
|
+
if (tone2 === 1) out = Math.pow(out, Math.pow(2, -s));
|
|
1940
|
+
else if (tone2 === 0 && s < 0) out = Math.pow(black(out, BALANCE_LEVELS * -s), Math.pow(2, -s / 2));
|
|
1941
|
+
else if (tone2 === 2 && s > 0) out = Math.pow(white(out, 1 - BALANCE_LEVELS * s), Math.pow(2, -s / 2));
|
|
1942
|
+
else out = Math.pow(out, Math.pow(2, -s / 2));
|
|
1943
|
+
}
|
|
1944
|
+
return out;
|
|
1945
|
+
}
|
|
1946
|
+
function blackWhiteGray(c, weights) {
|
|
1947
|
+
const [r, g, b] = c;
|
|
1948
|
+
const max = Math.max(r, g, b);
|
|
1949
|
+
const min = Math.min(r, g, b);
|
|
1950
|
+
const mid = r + g + b - max - min;
|
|
1951
|
+
const primary = r >= g && r >= b ? 0 : g >= b ? 2 : 4;
|
|
1952
|
+
const secondary = b <= r && b <= g ? 1 : r <= g ? 3 : 5;
|
|
1953
|
+
return clamp01(min + (mid - min) * weights[secondary] / 100 + (max - mid) * weights[primary] / 100);
|
|
1954
|
+
}
|
|
1955
|
+
var D50 = [0.96422, 1, 0.82521];
|
|
1956
|
+
var SRGB_TO_XYZ_D50 = [
|
|
1957
|
+
[0.436074, 0.385064, 0.14308],
|
|
1958
|
+
[0.222504, 0.716878, 0.060618],
|
|
1959
|
+
[0.013932, 0.097104, 0.714173]
|
|
1960
|
+
];
|
|
1961
|
+
var XYZ_D50_TO_SRGB = [
|
|
1962
|
+
[3.133856, -1.616867, -0.490615],
|
|
1963
|
+
[-0.978768, 1.916142, 0.033454],
|
|
1964
|
+
[0.071945, -0.228991, 1.405243]
|
|
1965
|
+
];
|
|
1966
|
+
function photoFilterMatrix(color, density, normalizeY = false) {
|
|
1967
|
+
const d = Number.isFinite(density) ? clamp01(density) : 0;
|
|
1968
|
+
const lin = [0, 1, 2].map((i) => srgbToLinear(clamp01(color[i])));
|
|
1969
|
+
const xyz = SRGB_TO_XYZ_D50.map((r) => r[0] * lin[0] + r[1] * lin[1] + r[2] * lin[2]);
|
|
1970
|
+
let s = [0, 1, 2].map((i) => 1 - d + d * xyz[i] / D50[i]);
|
|
1971
|
+
if (normalizeY && s[1] > 1e-6) {
|
|
1972
|
+
const y = s[1];
|
|
1973
|
+
s = s.map((v) => v / y);
|
|
1974
|
+
}
|
|
1975
|
+
return [0, 1, 2].map(
|
|
1976
|
+
(r) => [0, 1, 2].map((c) => XYZ_D50_TO_SRGB[r][0] * s[0] * SRGB_TO_XYZ_D50[0][c] + XYZ_D50_TO_SRGB[r][1] * s[1] * SRGB_TO_XYZ_D50[1][c] + XYZ_D50_TO_SRGB[r][2] * s[2] * SRGB_TO_XYZ_D50[2][c])
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
function posterizeValue(v, levels) {
|
|
1980
|
+
const n = Math.min(255, Math.max(2, Math.round(levels)));
|
|
1981
|
+
const x = Math.round(clamp01(v) * 255);
|
|
1982
|
+
const bin = Math.min(n - 1, Math.floor(x * n / 256));
|
|
1983
|
+
return Math.floor(bin * 255 / (n - 1)) / 255;
|
|
1984
|
+
}
|
|
1985
|
+
var rgbToGray = (c) => 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];
|
|
1986
|
+
function hexToRgb(hex) {
|
|
1987
|
+
if (!HEX.test(hex)) return [0, 0, 0];
|
|
1988
|
+
const n = parseInt(hex.slice(1), 16);
|
|
1989
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
1990
|
+
}
|
|
1991
|
+
function lutOp(luts) {
|
|
1992
|
+
const [lr, lg, lb] = luts;
|
|
1993
|
+
return (c) => {
|
|
1994
|
+
c[0] = sampleLut(lr, c[0]);
|
|
1995
|
+
c[1] = sampleLut(lg, c[1]);
|
|
1996
|
+
c[2] = sampleLut(lb, c[2]);
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
var isIdentityCurve = (pts) => pts.length === 2 && pts[0].input === 0 && pts[0].output === 0 && pts[1].input === 255 && pts[1].output === 255;
|
|
2000
|
+
var isIdentityLevels = (c) => c.inBlack === 0 && c.inWhite === 255 && c.gamma === 1 && c.outBlack === 0 && c.outWhite === 255;
|
|
2001
|
+
var LEVELS_QUANTUM = 255;
|
|
2002
|
+
function toneLuts(layer) {
|
|
2003
|
+
const n = ADJUSTMENT_LUT_SIZE;
|
|
2004
|
+
if (layer.kind === "curves") {
|
|
2005
|
+
const m = curveLutFromLevels(layer.params.master);
|
|
2006
|
+
return [layer.params.red, layer.params.green, layer.params.blue].map((pts) => {
|
|
2007
|
+
const ch = curveLutFromLevels(pts);
|
|
2008
|
+
for (let k = 0; k < n; k++) ch[k] = sampleLut(m, ch[k]);
|
|
2009
|
+
return ch;
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
if (layer.kind === "levels") {
|
|
2013
|
+
const m = toUnitLevels(layer.params.master);
|
|
2014
|
+
return [layer.params.red, layer.params.green, layer.params.blue].map((rec) => {
|
|
2015
|
+
const c = toUnitLevels(rec);
|
|
2016
|
+
const out = new Float32Array(n);
|
|
2017
|
+
for (let k = 0; k < n; k++) out[k] = levelsValue(m, levelsValue(c, k / (n - 1), LEVELS_QUANTUM), LEVELS_QUANTUM);
|
|
2018
|
+
return out;
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
function compileAdjustmentLayer(layer) {
|
|
2024
|
+
switch (layer.kind) {
|
|
2025
|
+
case "curves": {
|
|
2026
|
+
const p = layer.params;
|
|
2027
|
+
if ([p.master, p.red, p.green, p.blue].every(isIdentityCurve)) return null;
|
|
2028
|
+
return lutOp(toneLuts(layer));
|
|
2029
|
+
}
|
|
2030
|
+
case "levels": {
|
|
2031
|
+
const p = layer.params;
|
|
2032
|
+
if ([p.master, p.red, p.green, p.blue].every(isIdentityLevels)) return null;
|
|
2033
|
+
return lutOp(toneLuts(layer));
|
|
2034
|
+
}
|
|
2035
|
+
case "hueSaturation": {
|
|
2036
|
+
const p = layer.params;
|
|
2037
|
+
const rangesActive = !p.colorize && p.ranges.some((r) => !isNeutralRange(r));
|
|
2038
|
+
if (!p.colorize && !rangesActive && p.hue === 0 && p.saturation === 0 && p.lightness === 0) return null;
|
|
2039
|
+
const tables = rangesActive ? hueRangeTables(p.ranges) : null;
|
|
2040
|
+
const sat2 = p.saturation / 100;
|
|
2041
|
+
const light = Math.min(1, Math.max(-1, p.lightness / 100));
|
|
2042
|
+
return (c) => {
|
|
2043
|
+
let dh = 0, ds = 0, dl = 0;
|
|
2044
|
+
if (tables) {
|
|
2045
|
+
const h0 = rgbToHsl(c)[0];
|
|
2046
|
+
dh = sampleLut(tables[0], h0);
|
|
2047
|
+
ds = sampleLut(tables[1], h0);
|
|
2048
|
+
dl = sampleLut(tables[2], h0);
|
|
2049
|
+
}
|
|
2050
|
+
const out = rangeLightness(hueSaturation(c, p.hue + dh, Math.min(1, Math.max(-1, sat2 + ds)), light, p.colorize), dl);
|
|
2051
|
+
c[0] = out[0];
|
|
2052
|
+
c[1] = out[1];
|
|
2053
|
+
c[2] = out[2];
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
case "vibrance": {
|
|
2057
|
+
const v = layer.params.vibrance / 100;
|
|
2058
|
+
const s = layer.params.saturation / 100;
|
|
2059
|
+
if (v === 0 && s === 0) return null;
|
|
2060
|
+
return (c) => {
|
|
2061
|
+
const out = vibrancePixel(c, v, s);
|
|
2062
|
+
c[0] = out[0];
|
|
2063
|
+
c[1] = out[1];
|
|
2064
|
+
c[2] = out[2];
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
case "colorBalance": {
|
|
2068
|
+
const p = layer.params;
|
|
2069
|
+
const tones = [p.shadows, p.midtones, p.highlights];
|
|
2070
|
+
if (tones.every((t) => t[0] === 0 && t[1] === 0 && t[2] === 0)) return null;
|
|
2071
|
+
const n = ADJUSTMENT_LUT_SIZE;
|
|
2072
|
+
const luts = [0, 1, 2].map((ch) => {
|
|
2073
|
+
const out = new Float32Array(n);
|
|
2074
|
+
for (let k = 0; k < n; k++) out[k] = colorBalanceChannel(tones, p.preserveLuminosity, ch, k / (n - 1));
|
|
2075
|
+
return out;
|
|
2076
|
+
});
|
|
2077
|
+
return lutOp(luts);
|
|
2078
|
+
}
|
|
2079
|
+
case "blackWhite": {
|
|
2080
|
+
const p = layer.params;
|
|
2081
|
+
const weights = [p.reds, p.yellows, p.greens, p.cyans, p.blues, p.magentas];
|
|
2082
|
+
const tint = p.tint ? hexToRgb(p.tint) : null;
|
|
2083
|
+
return (c) => {
|
|
2084
|
+
const g = blackWhiteGray(c, weights);
|
|
2085
|
+
if (tint) {
|
|
2086
|
+
const t = setLum(tint, g);
|
|
2087
|
+
c[0] = clamp01(t[0]);
|
|
2088
|
+
c[1] = clamp01(t[1]);
|
|
2089
|
+
c[2] = clamp01(t[2]);
|
|
2090
|
+
} else {
|
|
2091
|
+
c[0] = c[1] = c[2] = g;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
case "photoFilter": {
|
|
2096
|
+
const p = layer.params;
|
|
2097
|
+
if (p.density <= 0) return null;
|
|
2098
|
+
const m = photoFilterMatrix(hexToRgb(p.color), p.density / 100, false);
|
|
2099
|
+
const preserve = p.preserveLuminosity;
|
|
2100
|
+
return (c) => {
|
|
2101
|
+
const l = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];
|
|
2102
|
+
let f = [0, 1, 2].map((r) => linearToSrgb(Math.max(0, m[r][0] * l[0] + m[r][1] * l[1] + m[r][2] * l[2])));
|
|
2103
|
+
if (preserve) {
|
|
2104
|
+
const target = 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];
|
|
2105
|
+
f = setLum(f, target);
|
|
2106
|
+
}
|
|
2107
|
+
c[0] = clamp01(f[0]);
|
|
2108
|
+
c[1] = clamp01(f[1]);
|
|
2109
|
+
c[2] = clamp01(f[2]);
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
case "invert":
|
|
2113
|
+
return (c) => {
|
|
2114
|
+
c[0] = 1 - c[0];
|
|
2115
|
+
c[1] = 1 - c[1];
|
|
2116
|
+
c[2] = 1 - c[2];
|
|
2117
|
+
};
|
|
2118
|
+
case "posterize": {
|
|
2119
|
+
const levels = layer.params.levels;
|
|
2120
|
+
return (c) => {
|
|
2121
|
+
c[0] = posterizeValue(c[0], levels);
|
|
2122
|
+
c[1] = posterizeValue(c[1], levels);
|
|
2123
|
+
c[2] = posterizeValue(c[2], levels);
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
case "threshold": {
|
|
2127
|
+
const t = Math.round(layer.params.level);
|
|
2128
|
+
return (c) => {
|
|
2129
|
+
const v = Math.round(rgbToGray(c) * 255) >= t ? 1 : 0;
|
|
2130
|
+
c[0] = c[1] = c[2] = v;
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
var COMPILE_CACHE_LIMIT = 64;
|
|
2137
|
+
var compileCache = /* @__PURE__ */ new Map();
|
|
2138
|
+
function compiledFor(layer) {
|
|
2139
|
+
const key = `${layer.kind}:${JSON.stringify(layer.params)}`;
|
|
2140
|
+
if (compileCache.has(key)) {
|
|
2141
|
+
const op2 = compileCache.get(key);
|
|
2142
|
+
compileCache.delete(key);
|
|
2143
|
+
compileCache.set(key, op2);
|
|
2144
|
+
return op2;
|
|
2145
|
+
}
|
|
2146
|
+
const op = compileAdjustmentLayer(layer);
|
|
2147
|
+
compileCache.set(key, op);
|
|
2148
|
+
if (compileCache.size > COMPILE_CACHE_LIMIT) compileCache.delete(compileCache.keys().next().value);
|
|
2149
|
+
return op;
|
|
2150
|
+
}
|
|
2151
|
+
function activeAdjustmentLayers(layers) {
|
|
2152
|
+
if (!layers?.length) return [];
|
|
2153
|
+
return layers.filter((l) => l.enabled !== false && (l.opacity ?? 1) > 0 && compiledFor(l) !== null);
|
|
2154
|
+
}
|
|
2155
|
+
function evaluateAdjustmentLayers(color, layers) {
|
|
2156
|
+
const c = [color[0], color[1], color[2]];
|
|
2157
|
+
for (const layer of layers) applyLayer(c, layer, compiledFor(layer));
|
|
2158
|
+
return c;
|
|
2159
|
+
}
|
|
2160
|
+
function applyLayer(c, layer, op, coverage = 1) {
|
|
2161
|
+
if (!op || layer.enabled === false) return;
|
|
2162
|
+
const alpha = Math.min(1, Math.max(0, layer.opacity ?? 1)) * coverage;
|
|
2163
|
+
if (alpha <= 0) return;
|
|
2164
|
+
const blend = layer.blend ?? "normal";
|
|
2165
|
+
if (blend === "normal" && alpha >= 1) {
|
|
2166
|
+
op(c);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
const cb0 = c[0], cb1 = c[1], cb2 = c[2];
|
|
2170
|
+
op(c);
|
|
2171
|
+
const b = blend === "normal" ? c : blendRgb(blend, [cb0, cb1, cb2], c);
|
|
2172
|
+
c[0] = (1 - alpha) * cb0 + alpha * b[0];
|
|
2173
|
+
c[1] = (1 - alpha) * cb1 + alpha * b[1];
|
|
2174
|
+
c[2] = (1 - alpha) * cb2 + alpha * b[2];
|
|
2175
|
+
}
|
|
2176
|
+
function applyAdjustmentLayers(imageData, layers, masks) {
|
|
2177
|
+
const active = activeAdjustmentLayers(layers);
|
|
2178
|
+
if (!active.length) return;
|
|
2179
|
+
const ops = active.map((l) => compiledFor(l));
|
|
2180
|
+
const planes = active.map((l) => masks?.get(l.id));
|
|
2181
|
+
const data = imageData.data;
|
|
2182
|
+
const len = data.length;
|
|
2183
|
+
const c = [0, 0, 0];
|
|
2184
|
+
for (let i = 0; i < len; i += 4) {
|
|
2185
|
+
if (data[i + 3] === 0) continue;
|
|
2186
|
+
c[0] = data[i] / 255;
|
|
2187
|
+
c[1] = data[i + 1] / 255;
|
|
2188
|
+
c[2] = data[i + 2] / 255;
|
|
2189
|
+
for (let k = 0; k < active.length; k++) {
|
|
2190
|
+
const plane = planes[k];
|
|
2191
|
+
if (plane) {
|
|
2192
|
+
const cov = plane[i >> 2];
|
|
2193
|
+
if (cov > 0) applyLayer(c, active[k], ops[k], cov >= 1 ? 1 : cov);
|
|
2194
|
+
} else {
|
|
2195
|
+
applyLayer(c, active[k], ops[k]);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
data[i] = Math.round(clamp01(c[0]) * 255);
|
|
2199
|
+
data[i + 1] = Math.round(clamp01(c[1]) * 255);
|
|
2200
|
+
data[i + 2] = Math.round(clamp01(c[2]) * 255);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function adjustmentLayersKey(layers) {
|
|
2204
|
+
if (!layers?.length) return "";
|
|
2205
|
+
return JSON.stringify(
|
|
2206
|
+
layers.map((l) => {
|
|
2207
|
+
const key = [l.kind, l.enabled !== false, l.opacity ?? 1, l.blend ?? "normal", l.params];
|
|
2208
|
+
if (l.mask) key.push(layerMaskKey(l.mask));
|
|
2209
|
+
return key;
|
|
2210
|
+
})
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/image-editor/filters.ts
|
|
2215
|
+
function clamp(value, min = 0, max = 255) {
|
|
2216
|
+
return Math.max(min, Math.min(max, value));
|
|
2217
|
+
}
|
|
2218
|
+
function applyAdjustments(imageData, adjustments, options = {}) {
|
|
2219
|
+
if (normalizeProcessVersion(adjustments.process) === LATEST_PROCESS_VERSION) {
|
|
2220
|
+
applyProcess2Adjustments(imageData, adjustments);
|
|
2221
|
+
} else {
|
|
2222
|
+
applyLegacyAdjustments(imageData, adjustments);
|
|
2223
|
+
}
|
|
2224
|
+
applyAdjustmentLayers(imageData, adjustments.layers, options.layerMasks);
|
|
2225
|
+
if (adjustments.blur > 0) {
|
|
2226
|
+
applyFastBoxBlur(imageData, Math.min(Math.round(adjustments.blur), 40));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function applyLegacyAdjustments(imageData, adjustments) {
|
|
2230
|
+
const { brightness, contrast, saturation, exposure, temperature, opacity } = adjustments;
|
|
2231
|
+
const data = imageData.data;
|
|
2232
|
+
const len = data.length;
|
|
2233
|
+
const hasBrightness = brightness !== 0;
|
|
2234
|
+
const hasContrast = contrast !== 0;
|
|
2235
|
+
const hasSaturation = saturation !== 0;
|
|
2236
|
+
const hasExposure = exposure !== 0;
|
|
2237
|
+
const hasTemperature = temperature !== 0;
|
|
2238
|
+
const hasOpacity = opacity < 1;
|
|
2239
|
+
const contrastFactor = hasContrast ? 259 * (contrast + 255) / (255 * (259 - contrast)) : 1;
|
|
2240
|
+
const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;
|
|
2241
|
+
const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;
|
|
2242
|
+
const tempOffset = hasTemperature ? temperature * 0.8 : 0;
|
|
2243
|
+
if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {
|
|
2244
|
+
for (let i = 0; i < len; i += 4) {
|
|
2245
|
+
let r = data[i];
|
|
2246
|
+
let g = data[i + 1];
|
|
2247
|
+
let b = data[i + 2];
|
|
2248
|
+
let a = data[i + 3];
|
|
2249
|
+
if (hasExposure) {
|
|
2250
|
+
r = clamp(r * exposureMultiplier);
|
|
2251
|
+
g = clamp(g * exposureMultiplier);
|
|
2252
|
+
b = clamp(b * exposureMultiplier);
|
|
2253
|
+
}
|
|
2254
|
+
if (hasBrightness) {
|
|
2255
|
+
const bOffset = brightness * 2.55;
|
|
2256
|
+
r = clamp(r + bOffset);
|
|
2257
|
+
g = clamp(g + bOffset);
|
|
2258
|
+
b = clamp(b + bOffset);
|
|
2259
|
+
}
|
|
2260
|
+
if (hasContrast) {
|
|
2261
|
+
r = clamp(contrastFactor * (r - 128) + 128);
|
|
2262
|
+
g = clamp(contrastFactor * (g - 128) + 128);
|
|
2263
|
+
b = clamp(contrastFactor * (b - 128) + 128);
|
|
2264
|
+
}
|
|
2265
|
+
if (hasSaturation) {
|
|
2266
|
+
const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;
|
|
2267
|
+
r = clamp(luminance + (r - luminance) * satMultiplier);
|
|
2268
|
+
g = clamp(luminance + (g - luminance) * satMultiplier);
|
|
2269
|
+
b = clamp(luminance + (b - luminance) * satMultiplier);
|
|
2270
|
+
}
|
|
2271
|
+
if (hasTemperature) {
|
|
2272
|
+
r = clamp(r + tempOffset);
|
|
2273
|
+
b = clamp(b - tempOffset);
|
|
2274
|
+
}
|
|
2275
|
+
if (hasOpacity) {
|
|
2276
|
+
a = clamp(a * opacity);
|
|
2277
|
+
}
|
|
2278
|
+
data[i] = r;
|
|
2279
|
+
data[i + 1] = g;
|
|
2280
|
+
data[i + 2] = b;
|
|
2281
|
+
data[i + 3] = a;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
var DECODE_LUT = (() => {
|
|
2286
|
+
const lut = new Float32Array(256);
|
|
2287
|
+
for (let i = 0; i < 256; i++) lut[i] = srgbToLinear(i / 255);
|
|
2288
|
+
return lut;
|
|
2289
|
+
})();
|
|
2290
|
+
var ENCODE_LUT_SIZE = 65535;
|
|
2291
|
+
var encodeLut = null;
|
|
2292
|
+
function getEncodeLut() {
|
|
2293
|
+
if (!encodeLut) {
|
|
2294
|
+
encodeLut = new Uint8Array(ENCODE_LUT_SIZE + 1);
|
|
2295
|
+
for (let i = 0; i <= ENCODE_LUT_SIZE; i++) {
|
|
2296
|
+
encodeLut[i] = Math.round(linearToSrgb(i / ENCODE_LUT_SIZE) * 255);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return encodeLut;
|
|
2300
|
+
}
|
|
2301
|
+
var cachedPlan = null;
|
|
2302
|
+
function getProcess2Plan(adjustments) {
|
|
2303
|
+
const exposure = adjustments.exposure || 0;
|
|
2304
|
+
const temperature = adjustments.temperature || 0;
|
|
2305
|
+
const tint = adjustments.tint || 0;
|
|
2306
|
+
const contrast = adjustments.contrast || 0;
|
|
2307
|
+
const brightness = adjustments.brightness || 0;
|
|
2308
|
+
const saturation = 1 + (adjustments.saturation || 0) / 100;
|
|
2309
|
+
const key = `${exposure},${temperature},${tint},${contrast},${brightness},${saturation}`;
|
|
2310
|
+
if (cachedPlan && cachedPlan.key === key) return cachedPlan;
|
|
2311
|
+
const gain = Math.pow(2, exposure / 50);
|
|
2312
|
+
const wb = whiteBalanceMatrix(temperature, tint);
|
|
2313
|
+
const m = new Float64Array(9);
|
|
2314
|
+
for (let i = 0; i < 9; i++) m[i] = wb[i] * gain;
|
|
2315
|
+
const luts = [];
|
|
2316
|
+
const scale = [0, 0, 0];
|
|
2317
|
+
for (let c = 0; c < 3; c++) {
|
|
2318
|
+
let reach = 0;
|
|
2319
|
+
for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);
|
|
2320
|
+
const headroom = Math.max(1, reach);
|
|
2321
|
+
luts[c] = buildToneLut(contrast, brightness, headroom);
|
|
2322
|
+
scale[c] = TONE_LUT_SIZE / headroom;
|
|
2323
|
+
}
|
|
2324
|
+
cachedPlan = { key, m, luts, scale, saturation };
|
|
2325
|
+
return cachedPlan;
|
|
2326
|
+
}
|
|
2327
|
+
function applyProcess2Adjustments(imageData, adjustments) {
|
|
2328
|
+
const data = imageData.data;
|
|
2329
|
+
const len = data.length;
|
|
2330
|
+
const opacity = adjustments.opacity ?? 1;
|
|
2331
|
+
const hasOpacity = opacity < 1;
|
|
2332
|
+
const hasColor = !!adjustments.brightness || !!adjustments.contrast || !!adjustments.saturation || !!adjustments.exposure || !!adjustments.temperature || !!adjustments.tint;
|
|
2333
|
+
if (!hasColor) {
|
|
2334
|
+
if (hasOpacity) {
|
|
2335
|
+
for (let i = 3; i < len; i += 4) data[i] = clamp(data[i] * opacity);
|
|
2336
|
+
}
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
const plan = getProcess2Plan(adjustments);
|
|
2340
|
+
const dec = DECODE_LUT;
|
|
2341
|
+
const enc = getEncodeLut();
|
|
2342
|
+
const { m, luts, scale, saturation } = plan;
|
|
2343
|
+
const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3], m4 = m[4], m5 = m[5], m6 = m[6], m7 = m[7], m8 = m[8];
|
|
2344
|
+
const lutR = luts[0], lutG = luts[1], lutB = luts[2];
|
|
2345
|
+
const sR = scale[0], sG = scale[1], sB = scale[2];
|
|
2346
|
+
const N = TONE_LUT_SIZE;
|
|
2347
|
+
const hasSaturation = saturation !== 1;
|
|
2348
|
+
const wr = LUMA_709[0], wg = LUMA_709[1], wb = LUMA_709[2];
|
|
2349
|
+
for (let i = 0; i < len; i += 4) {
|
|
2350
|
+
const r0 = dec[data[i]];
|
|
2351
|
+
const g0 = dec[data[i + 1]];
|
|
2352
|
+
const b0 = dec[data[i + 2]];
|
|
2353
|
+
let r = m0 * r0 + m1 * g0 + m2 * b0;
|
|
2354
|
+
let g = m3 * r0 + m4 * g0 + m5 * b0;
|
|
2355
|
+
let b = m6 * r0 + m7 * g0 + m8 * b0;
|
|
2356
|
+
let f;
|
|
2357
|
+
let k;
|
|
2358
|
+
if (r <= 0) r = lutR[0];
|
|
2359
|
+
else if ((f = r * sR) >= N) r = lutR[N];
|
|
2360
|
+
else {
|
|
2361
|
+
k = f | 0;
|
|
2362
|
+
r = lutR[k] + (lutR[k + 1] - lutR[k]) * (f - k);
|
|
2363
|
+
}
|
|
2364
|
+
if (g <= 0) g = lutG[0];
|
|
2365
|
+
else if ((f = g * sG) >= N) g = lutG[N];
|
|
2366
|
+
else {
|
|
2367
|
+
k = f | 0;
|
|
2368
|
+
g = lutG[k] + (lutG[k + 1] - lutG[k]) * (f - k);
|
|
2369
|
+
}
|
|
2370
|
+
if (b <= 0) b = lutB[0];
|
|
2371
|
+
else if ((f = b * sB) >= N) b = lutB[N];
|
|
2372
|
+
else {
|
|
2373
|
+
k = f | 0;
|
|
2374
|
+
b = lutB[k] + (lutB[k + 1] - lutB[k]) * (f - k);
|
|
2375
|
+
}
|
|
2376
|
+
if (hasSaturation) {
|
|
2377
|
+
const y = wr * r + wg * g + wb * b;
|
|
2378
|
+
r = y + (r - y) * saturation;
|
|
2379
|
+
g = y + (g - y) * saturation;
|
|
2380
|
+
b = y + (b - y) * saturation;
|
|
2381
|
+
}
|
|
2382
|
+
data[i] = enc[r <= 0 ? 0 : r >= 1 ? ENCODE_LUT_SIZE : r * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
2383
|
+
data[i + 1] = enc[g <= 0 ? 0 : g >= 1 ? ENCODE_LUT_SIZE : g * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
2384
|
+
data[i + 2] = enc[b <= 0 ? 0 : b >= 1 ? ENCODE_LUT_SIZE : b * ENCODE_LUT_SIZE + 0.5 | 0];
|
|
2385
|
+
if (hasOpacity) data[i + 3] = clamp(data[i + 3] * opacity);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function applyFastBoxBlur(imageData, radius) {
|
|
2389
|
+
if (radius < 1) return;
|
|
2390
|
+
const w = imageData.width;
|
|
2391
|
+
const h = imageData.height;
|
|
2392
|
+
const src = imageData.data;
|
|
2393
|
+
const temp = new Uint8ClampedArray(src.length);
|
|
2394
|
+
for (let y = 0; y < h; y++) {
|
|
2395
|
+
for (let x = 0; x < w; x++) {
|
|
2396
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
2397
|
+
let count = 0;
|
|
2398
|
+
for (let k = -radius; k <= radius; k++) {
|
|
2399
|
+
const px = Math.min(w - 1, Math.max(0, x + k));
|
|
2400
|
+
const idx = (y * w + px) * 4;
|
|
2401
|
+
r += src[idx];
|
|
2402
|
+
g += src[idx + 1];
|
|
2403
|
+
b += src[idx + 2];
|
|
2404
|
+
a += src[idx + 3];
|
|
2405
|
+
count++;
|
|
2406
|
+
}
|
|
2407
|
+
const dstIdx = (y * w + x) * 4;
|
|
2408
|
+
temp[dstIdx] = Math.round(r / count);
|
|
2409
|
+
temp[dstIdx + 1] = Math.round(g / count);
|
|
2410
|
+
temp[dstIdx + 2] = Math.round(b / count);
|
|
2411
|
+
temp[dstIdx + 3] = Math.round(a / count);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
for (let x = 0; x < w; x++) {
|
|
2415
|
+
for (let y = 0; y < h; y++) {
|
|
2416
|
+
let r = 0, g = 0, b = 0, a = 0;
|
|
2417
|
+
let count = 0;
|
|
2418
|
+
for (let k = -radius; k <= radius; k++) {
|
|
2419
|
+
const py = Math.min(h - 1, Math.max(0, y + k));
|
|
2420
|
+
const idx = (py * w + x) * 4;
|
|
2421
|
+
r += temp[idx];
|
|
2422
|
+
g += temp[idx + 1];
|
|
2423
|
+
b += temp[idx + 2];
|
|
2424
|
+
a += temp[idx + 3];
|
|
2425
|
+
count++;
|
|
2426
|
+
}
|
|
2427
|
+
const dstIdx = (y * w + x) * 4;
|
|
2428
|
+
src[dstIdx] = Math.round(r / count);
|
|
2429
|
+
src[dstIdx + 1] = Math.round(g / count);
|
|
2430
|
+
src[dstIdx + 2] = Math.round(b / count);
|
|
2431
|
+
src[dstIdx + 3] = Math.round(a / count);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function applyDepthMask(mainImageData, maskImageData, options) {
|
|
2436
|
+
const { mode, depthRange, softness, invert } = options;
|
|
2437
|
+
const mainData = mainImageData.data;
|
|
2438
|
+
const maskData = maskImageData.data;
|
|
2439
|
+
const len = Math.min(mainData.length, maskData.length);
|
|
2440
|
+
const effectiveSoftness = Math.max(1, softness || 10);
|
|
2441
|
+
const minDepth = depthRange[0];
|
|
2442
|
+
const maxDepth = depthRange[1];
|
|
2443
|
+
for (let i = 0; i < len; i += 4) {
|
|
2444
|
+
let maskValue = maskData[i];
|
|
2445
|
+
if (invert) {
|
|
2446
|
+
maskValue = 255 - maskValue;
|
|
2447
|
+
}
|
|
2448
|
+
let alphaValue;
|
|
2449
|
+
if (mode === "threshold") {
|
|
2450
|
+
if (maskValue >= maxDepth + effectiveSoftness) {
|
|
2451
|
+
alphaValue = 0;
|
|
2452
|
+
} else if (maskValue > maxDepth) {
|
|
2453
|
+
alphaValue = Math.round((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness * 255);
|
|
2454
|
+
} else if (maskValue >= minDepth) {
|
|
2455
|
+
alphaValue = 255;
|
|
2456
|
+
} else if (maskValue < minDepth - effectiveSoftness) {
|
|
2457
|
+
alphaValue = 0;
|
|
2458
|
+
} else {
|
|
2459
|
+
alphaValue = Math.round((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness * 255);
|
|
2460
|
+
}
|
|
2461
|
+
} else {
|
|
2462
|
+
alphaValue = clamp(maskValue + minDepth);
|
|
2463
|
+
}
|
|
2464
|
+
mainData[i + 3] = clamp(mainData[i + 3] * alphaValue / 255);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function removeAlpha(imageData) {
|
|
2468
|
+
const data = imageData.data;
|
|
2469
|
+
const len = data.length;
|
|
2470
|
+
for (let i = 3; i < len; i += 4) {
|
|
2471
|
+
data[i] = 255;
|
|
745
2472
|
}
|
|
746
|
-
|
|
747
|
-
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/image-editor/histogram.ts
|
|
2476
|
+
var HISTOGRAM_BINS = 256;
|
|
2477
|
+
function computeHistogram(imageData) {
|
|
2478
|
+
const red = new Uint32Array(HISTOGRAM_BINS);
|
|
2479
|
+
const green = new Uint32Array(HISTOGRAM_BINS);
|
|
2480
|
+
const blue = new Uint32Array(HISTOGRAM_BINS);
|
|
2481
|
+
const luminance = new Uint32Array(HISTOGRAM_BINS);
|
|
2482
|
+
const shadows = [0, 0, 0];
|
|
2483
|
+
const highlights = [0, 0, 0];
|
|
2484
|
+
let samples = 0;
|
|
2485
|
+
let transparent = 0;
|
|
2486
|
+
const data = imageData.data;
|
|
2487
|
+
const len = data.length - data.length % 4;
|
|
2488
|
+
for (let i = 0; i < len; i += 4) {
|
|
2489
|
+
if (!(data[i + 3] > 0)) {
|
|
2490
|
+
transparent++;
|
|
2491
|
+
continue;
|
|
2492
|
+
}
|
|
2493
|
+
samples++;
|
|
2494
|
+
const r = clampByte(data[i]);
|
|
2495
|
+
const g = clampByte(data[i + 1]);
|
|
2496
|
+
const b = clampByte(data[i + 2]);
|
|
2497
|
+
red[r]++;
|
|
2498
|
+
green[g]++;
|
|
2499
|
+
blue[b]++;
|
|
2500
|
+
luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;
|
|
2501
|
+
if (r === 0) shadows[0]++;
|
|
2502
|
+
if (g === 0) shadows[1]++;
|
|
2503
|
+
if (b === 0) shadows[2]++;
|
|
2504
|
+
if (r === 255) highlights[0]++;
|
|
2505
|
+
if (g === 255) highlights[1]++;
|
|
2506
|
+
if (b === 255) highlights[2]++;
|
|
748
2507
|
}
|
|
749
|
-
|
|
750
|
-
|
|
2508
|
+
let max = 0;
|
|
2509
|
+
for (const bins of [red, green, blue, luminance]) {
|
|
2510
|
+
for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];
|
|
751
2511
|
}
|
|
752
|
-
|
|
753
|
-
|
|
2512
|
+
return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };
|
|
2513
|
+
}
|
|
2514
|
+
function clampByte(v) {
|
|
2515
|
+
return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);
|
|
2516
|
+
}
|
|
2517
|
+
function histogramPath(histogram, channel, width, height, options = {}) {
|
|
2518
|
+
const bins = histogram[channel];
|
|
2519
|
+
const sqrt = (options.scale ?? "sqrt") === "sqrt";
|
|
2520
|
+
const f = (v) => sqrt ? Math.sqrt(v) : v;
|
|
2521
|
+
const top = f(options.max ?? histogram.max) || 1;
|
|
2522
|
+
const dx = width / (HISTOGRAM_BINS - 1);
|
|
2523
|
+
let d = `M0 ${height}`;
|
|
2524
|
+
for (let k = 0; k < HISTOGRAM_BINS; k++) {
|
|
2525
|
+
const y = height - f(bins[k]) / top * height;
|
|
2526
|
+
d += ` L${round2(k * dx)} ${round2(y)}`;
|
|
754
2527
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
2528
|
+
return `${d} L${round2(width)} ${height} Z`;
|
|
2529
|
+
}
|
|
2530
|
+
var round2 = (v) => Math.round(v * 100) / 100;
|
|
2531
|
+
|
|
2532
|
+
// src/image-editor/selection/overlay.ts
|
|
2533
|
+
function selectionOutline(mask, threshold = 128) {
|
|
2534
|
+
const { width: w, height: h, data } = mask;
|
|
2535
|
+
const segs = [];
|
|
2536
|
+
if (!mask.bounds) return new Float32Array(0);
|
|
2537
|
+
const b = mask.bounds;
|
|
2538
|
+
const inside = (x, y) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;
|
|
2539
|
+
for (let y = b.y; y <= b.y + b.height; y++) {
|
|
2540
|
+
let start = -1;
|
|
2541
|
+
for (let x = b.x; x <= b.x + b.width; x++) {
|
|
2542
|
+
const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);
|
|
2543
|
+
if (edge && start < 0) start = x;
|
|
2544
|
+
else if (!edge && start >= 0) {
|
|
2545
|
+
segs.push(start, y, x, y);
|
|
2546
|
+
start = -1;
|
|
771
2547
|
}
|
|
772
2548
|
}
|
|
773
2549
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
y
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
switch (ann.type) {
|
|
783
|
-
case "rect":
|
|
784
|
-
case "image":
|
|
785
|
-
return {
|
|
786
|
-
x: Math.min(ann.x, ann.x + ann.width),
|
|
787
|
-
y: Math.min(ann.y, ann.y + ann.height),
|
|
788
|
-
width: Math.abs(ann.width),
|
|
789
|
-
height: Math.abs(ann.height)
|
|
790
|
-
};
|
|
791
|
-
case "circle":
|
|
792
|
-
return {
|
|
793
|
-
x: ann.x - Math.abs(ann.radiusX),
|
|
794
|
-
y: ann.y - Math.abs(ann.radiusY),
|
|
795
|
-
width: Math.abs(ann.radiusX) * 2,
|
|
796
|
-
height: Math.abs(ann.radiusY) * 2
|
|
797
|
-
};
|
|
798
|
-
case "arrow":
|
|
799
|
-
case "line": {
|
|
800
|
-
const minX = Math.min(ann.x, ann.endX);
|
|
801
|
-
const minY = Math.min(ann.y, ann.endY);
|
|
802
|
-
return {
|
|
803
|
-
x: minX,
|
|
804
|
-
y: minY,
|
|
805
|
-
width: Math.max(Math.abs(ann.endX - ann.x), 10),
|
|
806
|
-
height: Math.max(Math.abs(ann.endY - ann.y), 10)
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
case "pen": {
|
|
810
|
-
if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };
|
|
811
|
-
let minX = ann.points[0].x;
|
|
812
|
-
let maxX = ann.points[0].x;
|
|
813
|
-
let minY = ann.points[0].y;
|
|
814
|
-
let maxY = ann.points[0].y;
|
|
815
|
-
for (const pt of ann.points) {
|
|
816
|
-
if (pt.x < minX) minX = pt.x;
|
|
817
|
-
if (pt.x > maxX) maxX = pt.x;
|
|
818
|
-
if (pt.y < minY) minY = pt.y;
|
|
819
|
-
if (pt.y > maxY) maxY = pt.y;
|
|
2550
|
+
for (let x = b.x; x <= b.x + b.width; x++) {
|
|
2551
|
+
let start = -1;
|
|
2552
|
+
for (let y = b.y; y <= b.y + b.height; y++) {
|
|
2553
|
+
const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);
|
|
2554
|
+
if (edge && start < 0) start = y;
|
|
2555
|
+
else if (!edge && start >= 0) {
|
|
2556
|
+
segs.push(x, start, x, y);
|
|
2557
|
+
start = -1;
|
|
820
2558
|
}
|
|
821
|
-
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
822
2559
|
}
|
|
823
|
-
case "text":
|
|
824
|
-
return {
|
|
825
|
-
x: ann.x,
|
|
826
|
-
y: ann.y,
|
|
827
|
-
width: ann.text.length * ann.fontSize * 0.6,
|
|
828
|
-
height: ann.fontSize * 1.2
|
|
829
|
-
};
|
|
830
2560
|
}
|
|
2561
|
+
return Float32Array.from(segs);
|
|
831
2562
|
}
|
|
832
|
-
function
|
|
833
|
-
const b =
|
|
834
|
-
|
|
2563
|
+
function quickMaskPixels(mask, style = {}) {
|
|
2564
|
+
const [r, g, b] = style.color ?? [255, 0, 0];
|
|
2565
|
+
const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));
|
|
2566
|
+
const out = new Uint8ClampedArray(mask.data.length * 4);
|
|
2567
|
+
for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {
|
|
2568
|
+
out[p] = r;
|
|
2569
|
+
out[p + 1] = g;
|
|
2570
|
+
out[p + 2] = b;
|
|
2571
|
+
out[p + 3] = Math.round((255 - mask.data[i]) * k);
|
|
2572
|
+
}
|
|
2573
|
+
return out;
|
|
835
2574
|
}
|
|
836
2575
|
|
|
837
2576
|
// src/image-editor/renderer.ts
|
|
@@ -881,6 +2620,13 @@ var ImageEditorRenderer = class {
|
|
|
881
2620
|
imageCache = globalImageCache;
|
|
882
2621
|
cachedBaseCanvas = null;
|
|
883
2622
|
cachedBaseKey = "";
|
|
2623
|
+
/**
|
|
2624
|
+
* Histogram of the last viewport base render (after adjustments and layers, before
|
|
2625
|
+
* annotations); `null` until a render with pixel access has happened.
|
|
2626
|
+
*/
|
|
2627
|
+
previewHistogram = null;
|
|
2628
|
+
/** Called whenever the viewport base image is re-rendered with the new histogram. */
|
|
2629
|
+
onPreviewHistogram;
|
|
884
2630
|
sourceIds = /* @__PURE__ */ new WeakMap();
|
|
885
2631
|
nextSourceId = 0;
|
|
886
2632
|
/**
|
|
@@ -937,13 +2683,19 @@ var ImageEditorRenderer = class {
|
|
|
937
2683
|
`${region.x},${region.y},${region.width},${region.height}`,
|
|
938
2684
|
`${projection.angle},${transform.flipH},${transform.flipV}`,
|
|
939
2685
|
`${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,
|
|
2686
|
+
adjustmentLayersKey(adjustments.layers),
|
|
940
2687
|
`${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`
|
|
941
2688
|
].join("|");
|
|
942
2689
|
let baseCanvas = this.cachedBaseCanvas;
|
|
943
2690
|
if (!baseCanvas || this.cachedBaseKey !== baseKey) {
|
|
944
|
-
|
|
2691
|
+
let histogram = null;
|
|
2692
|
+
baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {
|
|
2693
|
+
histogram = computeHistogram(pixels);
|
|
2694
|
+
});
|
|
945
2695
|
this.cachedBaseCanvas = baseCanvas;
|
|
946
2696
|
this.cachedBaseKey = baseKey;
|
|
2697
|
+
this.previewHistogram = histogram;
|
|
2698
|
+
this.onPreviewHistogram?.(histogram);
|
|
947
2699
|
}
|
|
948
2700
|
const drawW = rotated.width * projection.scale;
|
|
949
2701
|
const drawH = rotated.height * projection.scale;
|
|
@@ -965,6 +2717,21 @@ var ImageEditorRenderer = class {
|
|
|
965
2717
|
}
|
|
966
2718
|
ctx.restore();
|
|
967
2719
|
}
|
|
2720
|
+
const display = options.selectionDisplay ?? (state.quickMask ? "quickMask" : "ants");
|
|
2721
|
+
if (state.selection?.bounds && display !== "none" || options.draftSelectionPath?.length) {
|
|
2722
|
+
ctx.save();
|
|
2723
|
+
try {
|
|
2724
|
+
applyImageProjectionToContext(ctx, projection);
|
|
2725
|
+
ctx.beginPath();
|
|
2726
|
+
ctx.rect(region.x, region.y, region.width, region.height);
|
|
2727
|
+
ctx.clip();
|
|
2728
|
+
if (state.selection?.bounds && display === "quickMask") this.drawQuickMask(ctx, state.selection);
|
|
2729
|
+
else if (state.selection?.bounds && display === "ants") this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);
|
|
2730
|
+
if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);
|
|
2731
|
+
} catch {
|
|
2732
|
+
}
|
|
2733
|
+
ctx.restore();
|
|
2734
|
+
}
|
|
968
2735
|
if (isActivelyCropping) {
|
|
969
2736
|
const activeCrop = options.draftCrop !== void 0 && options.draftCrop !== null ? options.draftCrop : crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight };
|
|
970
2737
|
this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));
|
|
@@ -974,7 +2741,7 @@ var ImageEditorRenderer = class {
|
|
|
974
2741
|
* Draws `region` of the source image with rotation/flip applied, then pixel adjustments
|
|
975
2742
|
* and the depth mask (which receives the identical transform so it stays aligned).
|
|
976
2743
|
*/
|
|
977
|
-
async renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state) {
|
|
2744
|
+
async renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, onPixels) {
|
|
978
2745
|
const { adjustments, depthMask } = state;
|
|
979
2746
|
const rotated = getRotatedSize(region.width, region.height, transform);
|
|
980
2747
|
const baseW = Math.max(1, Math.round(rotated.width));
|
|
@@ -999,7 +2766,8 @@ var ImageEditorRenderer = class {
|
|
|
999
2766
|
baseCtx.restore();
|
|
1000
2767
|
const imageData = baseCtx.getImageData(0, 0, baseW, baseH);
|
|
1001
2768
|
if (imageData) {
|
|
1002
|
-
|
|
2769
|
+
const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);
|
|
2770
|
+
applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});
|
|
1003
2771
|
if (depthMask.enabled && depthMask.maskSource) {
|
|
1004
2772
|
try {
|
|
1005
2773
|
const maskImg = await this.imageCache.get(depthMask.maskSource);
|
|
@@ -1031,10 +2799,65 @@ var ImageEditorRenderer = class {
|
|
|
1031
2799
|
} catch {
|
|
1032
2800
|
}
|
|
1033
2801
|
}
|
|
2802
|
+
onPixels?.(imageData);
|
|
1034
2803
|
baseCtx.putImageData(imageData, 0, 0);
|
|
1035
2804
|
}
|
|
1036
2805
|
return baseCanvas;
|
|
1037
2806
|
}
|
|
2807
|
+
quickMaskCache = /* @__PURE__ */ new WeakMap();
|
|
2808
|
+
outlineCache = /* @__PURE__ */ new WeakMap();
|
|
2809
|
+
/** Rubylith over the unselected area, drawn in natural image space. */
|
|
2810
|
+
drawQuickMask(ctx, selection) {
|
|
2811
|
+
let canvas = this.quickMaskCache.get(selection);
|
|
2812
|
+
if (!canvas) {
|
|
2813
|
+
canvas = this.createCanvas(selection.width, selection.height);
|
|
2814
|
+
const mctx = canvas.getContext("2d");
|
|
2815
|
+
if (!mctx || typeof mctx.createImageData !== "function") return;
|
|
2816
|
+
const img = mctx.createImageData(selection.width, selection.height);
|
|
2817
|
+
img.data.set(quickMaskPixels(selection));
|
|
2818
|
+
mctx.putImageData(img, 0, 0);
|
|
2819
|
+
this.quickMaskCache.set(selection, canvas);
|
|
2820
|
+
}
|
|
2821
|
+
ctx.drawImage(canvas, 0, 0, selection.width, selection.height);
|
|
2822
|
+
}
|
|
2823
|
+
/** Marching ants: a white line under a black dashed one, one screen pixel wide. */
|
|
2824
|
+
drawAnts(ctx, selection, scale, offset) {
|
|
2825
|
+
let segs = this.outlineCache.get(selection);
|
|
2826
|
+
if (!segs) {
|
|
2827
|
+
segs = selectionOutline(selection);
|
|
2828
|
+
this.outlineCache.set(selection, segs);
|
|
2829
|
+
}
|
|
2830
|
+
if (!segs.length) return;
|
|
2831
|
+
const px = 1 / (scale || 1);
|
|
2832
|
+
ctx.beginPath();
|
|
2833
|
+
for (let i = 0; i < segs.length; i += 4) {
|
|
2834
|
+
ctx.moveTo(segs[i], segs[i + 1]);
|
|
2835
|
+
ctx.lineTo(segs[i + 2], segs[i + 3]);
|
|
2836
|
+
}
|
|
2837
|
+
ctx.lineWidth = px;
|
|
2838
|
+
ctx.strokeStyle = "#ffffff";
|
|
2839
|
+
ctx.stroke();
|
|
2840
|
+
if (typeof ctx.setLineDash === "function") {
|
|
2841
|
+
ctx.setLineDash([4 * px, 4 * px]);
|
|
2842
|
+
ctx.lineDashOffset = -offset * px;
|
|
2843
|
+
}
|
|
2844
|
+
ctx.strokeStyle = "#000000";
|
|
2845
|
+
ctx.stroke();
|
|
2846
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
2847
|
+
}
|
|
2848
|
+
drawDraftPath(ctx, points, scale) {
|
|
2849
|
+
const px = 1 / (scale || 1);
|
|
2850
|
+
ctx.beginPath();
|
|
2851
|
+
points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
|
|
2852
|
+
ctx.closePath();
|
|
2853
|
+
ctx.lineWidth = px;
|
|
2854
|
+
ctx.strokeStyle = "#ffffff";
|
|
2855
|
+
ctx.stroke();
|
|
2856
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash([4 * px, 4 * px]);
|
|
2857
|
+
ctx.strokeStyle = "#000000";
|
|
2858
|
+
ctx.stroke();
|
|
2859
|
+
if (typeof ctx.setLineDash === "function") ctx.setLineDash([]);
|
|
2860
|
+
}
|
|
1038
2861
|
getSourceId(img) {
|
|
1039
2862
|
const key = img;
|
|
1040
2863
|
let id = this.sourceIds.get(key);
|
|
@@ -1048,6 +2871,59 @@ var ImageEditorRenderer = class {
|
|
|
1048
2871
|
* Draws an individual annotation onto the canvas.
|
|
1049
2872
|
*/
|
|
1050
2873
|
async drawAnnotation(ctx, ann) {
|
|
2874
|
+
const blend = normalizeBlendMode(ann.blend);
|
|
2875
|
+
if (blend === "normal") {
|
|
2876
|
+
await this.drawAnnotationContent(ctx, ann);
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const op = canvasCompositeOperation(blend);
|
|
2880
|
+
if (op) {
|
|
2881
|
+
ctx.save();
|
|
2882
|
+
ctx.globalCompositeOperation = op;
|
|
2883
|
+
await this.drawAnnotationContent(ctx, ann);
|
|
2884
|
+
ctx.restore();
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
if (!await this.drawAnnotationBlended(ctx, ann)) {
|
|
2888
|
+
await this.drawAnnotationContent(ctx, ann);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /
|
|
2893
|
+
* vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):
|
|
2894
|
+
* draws the annotation on a same-size layer with the same transform, blends it into a copy of
|
|
2895
|
+
* the target's pixels with `blendImageData`, and copies the result back inside the current clip.
|
|
2896
|
+
*/
|
|
2897
|
+
async drawAnnotationBlended(ctx, ann) {
|
|
2898
|
+
const canvas = ctx.canvas;
|
|
2899
|
+
if (!canvas || typeof ctx.getTransform !== "function" || typeof ctx.getImageData !== "function") return false;
|
|
2900
|
+
const w = canvas.width;
|
|
2901
|
+
const h = canvas.height;
|
|
2902
|
+
const layer = this.createCanvas(w, h);
|
|
2903
|
+
const lctx = layer.getContext("2d");
|
|
2904
|
+
if (!lctx) return false;
|
|
2905
|
+
try {
|
|
2906
|
+
lctx.setTransform(ctx.getTransform());
|
|
2907
|
+
lctx.globalAlpha = ctx.globalAlpha;
|
|
2908
|
+
await this.drawAnnotationContent(lctx, ann);
|
|
2909
|
+
const backdrop = ctx.getImageData(0, 0, w, h);
|
|
2910
|
+
const source = lctx.getImageData(0, 0, w, h);
|
|
2911
|
+
if (!backdrop || !source) return false;
|
|
2912
|
+
blendImageData(backdrop, source, normalizeBlendMode(ann.blend));
|
|
2913
|
+
lctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2914
|
+
lctx.putImageData(backdrop, 0, 0);
|
|
2915
|
+
} catch {
|
|
2916
|
+
return false;
|
|
2917
|
+
}
|
|
2918
|
+
ctx.save();
|
|
2919
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2920
|
+
ctx.globalAlpha = 1;
|
|
2921
|
+
ctx.globalCompositeOperation = "copy";
|
|
2922
|
+
ctx.drawImage(layer, 0, 0);
|
|
2923
|
+
ctx.restore();
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2926
|
+
async drawAnnotationContent(ctx, ann) {
|
|
1051
2927
|
ctx.save();
|
|
1052
2928
|
if (ann.opacity !== void 0) {
|
|
1053
2929
|
ctx.globalAlpha = ann.opacity;
|
|
@@ -1315,10 +3191,11 @@ var ImageEditorRenderer = class {
|
|
|
1315
3191
|
}
|
|
1316
3192
|
}
|
|
1317
3193
|
const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);
|
|
3194
|
+
const selection = (options?.useSelection ?? true) && state.selection?.bounds && state.selection.width === naturalWidth && state.selection.height === naturalHeight ? state.selection : null;
|
|
1318
3195
|
if (imageData) {
|
|
1319
3196
|
const d = imageData.data;
|
|
1320
3197
|
for (let i = 0; i < d.length; i += 4) {
|
|
1321
|
-
const v = d[i] >= 128 ? 255 : 0;
|
|
3198
|
+
const v = d[i] >= 128 || selection !== null && selection.data[i >> 2] >= 128 ? 255 : 0;
|
|
1322
3199
|
d[i] = v;
|
|
1323
3200
|
d[i + 1] = v;
|
|
1324
3201
|
d[i + 2] = v;
|
|
@@ -1388,19 +3265,570 @@ function getSourceSize(img, fallbackW = 800, fallbackH = 600) {
|
|
|
1388
3265
|
height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH
|
|
1389
3266
|
};
|
|
1390
3267
|
}
|
|
1391
|
-
function getBaseProjection(region, transform, baseW, baseH) {
|
|
1392
|
-
return {
|
|
1393
|
-
region,
|
|
1394
|
-
angle: normalizeAngle(transform.rotate),
|
|
1395
|
-
flipH: transform.flipH,
|
|
1396
|
-
flipV: transform.flipV,
|
|
1397
|
-
scale: 1,
|
|
1398
|
-
originX: baseW / 2,
|
|
1399
|
-
originY: baseH / 2
|
|
1400
|
-
};
|
|
3268
|
+
function getBaseProjection(region, transform, baseW, baseH) {
|
|
3269
|
+
return {
|
|
3270
|
+
region,
|
|
3271
|
+
angle: normalizeAngle(transform.rotate),
|
|
3272
|
+
flipH: transform.flipH,
|
|
3273
|
+
flipV: transform.flipV,
|
|
3274
|
+
scale: 1,
|
|
3275
|
+
originX: baseW / 2,
|
|
3276
|
+
originY: baseH / 2
|
|
3277
|
+
};
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// src/image-editor/selection/shapes.ts
|
|
3281
|
+
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
3282
|
+
const m = new Float32Array(width * height);
|
|
3283
|
+
if (points.length < 3 || width <= 0 || height <= 0) return m;
|
|
3284
|
+
const sub = antiAlias ? 4 : 1;
|
|
3285
|
+
const edges = [];
|
|
3286
|
+
for (let i = 0; i < points.length; i++) {
|
|
3287
|
+
const a = points[i];
|
|
3288
|
+
const b = points[(i + 1) % points.length];
|
|
3289
|
+
if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;
|
|
3290
|
+
if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
|
|
3291
|
+
else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });
|
|
3292
|
+
}
|
|
3293
|
+
edges.sort((p, q) => p.top - q.top);
|
|
3294
|
+
let next = 0;
|
|
3295
|
+
let active = [];
|
|
3296
|
+
const xs = [];
|
|
3297
|
+
for (let py = 0; py < height; py++) {
|
|
3298
|
+
const row = py * width;
|
|
3299
|
+
for (let s = 0; s < sub; s++) {
|
|
3300
|
+
const y = py + (s + 0.5) / sub;
|
|
3301
|
+
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
3302
|
+
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
3303
|
+
if (!active.length) continue;
|
|
3304
|
+
xs.length = 0;
|
|
3305
|
+
for (const e of active) {
|
|
3306
|
+
if (e.top <= y) xs.push(e.ax + (y - e.ay) / (e.by - e.ay) * (e.bx - e.ax));
|
|
3307
|
+
}
|
|
3308
|
+
xs.sort((a, b) => a - b);
|
|
3309
|
+
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
3310
|
+
const x0 = Math.max(0, xs[k]);
|
|
3311
|
+
const x1 = Math.min(width, xs[k + 1]);
|
|
3312
|
+
if (x1 <= x0) continue;
|
|
3313
|
+
const c0 = Math.floor(x0);
|
|
3314
|
+
const c1 = Math.min(width, Math.ceil(x1));
|
|
3315
|
+
for (let cx = c0; cx < c1; cx++) {
|
|
3316
|
+
let cov;
|
|
3317
|
+
if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);
|
|
3318
|
+
else {
|
|
3319
|
+
const c = cx + 0.5;
|
|
3320
|
+
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
3321
|
+
}
|
|
3322
|
+
if (cov > 0) m[row + cx] += cov / sub;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
|
|
3328
|
+
return m;
|
|
3329
|
+
}
|
|
3330
|
+
function toMask(width, height, coverage) {
|
|
3331
|
+
const out = new Uint8Array(coverage.length);
|
|
3332
|
+
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
3333
|
+
return createSelectionMask(width, height, out);
|
|
3334
|
+
}
|
|
3335
|
+
function polygonSelection(width, height, points, options = {}) {
|
|
3336
|
+
return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));
|
|
3337
|
+
}
|
|
3338
|
+
function normalizeRect(rect) {
|
|
3339
|
+
const x = rect.width < 0 ? rect.x + rect.width : rect.x;
|
|
3340
|
+
const y = rect.height < 0 ? rect.y + rect.height : rect.y;
|
|
3341
|
+
return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };
|
|
3342
|
+
}
|
|
3343
|
+
function rectSelection(width, height, rect, options = {}) {
|
|
3344
|
+
const r = normalizeRect(rect);
|
|
3345
|
+
const pts = [
|
|
3346
|
+
{ x: r.x, y: r.y },
|
|
3347
|
+
{ x: r.x + r.width, y: r.y },
|
|
3348
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
3349
|
+
{ x: r.x, y: r.y + r.height }
|
|
3350
|
+
];
|
|
3351
|
+
return polygonSelection(width, height, pts, options);
|
|
3352
|
+
}
|
|
3353
|
+
function ellipsePoints(rect) {
|
|
3354
|
+
const r = normalizeRect(rect);
|
|
3355
|
+
const rx = r.width / 2;
|
|
3356
|
+
const ry = r.height / 2;
|
|
3357
|
+
const cx = r.x + rx;
|
|
3358
|
+
const cy = r.y + ry;
|
|
3359
|
+
const rmax = Math.max(rx, ry, 0.5);
|
|
3360
|
+
const step = Math.sqrt(0.4 / rmax);
|
|
3361
|
+
const n = Math.min(4096, Math.max(16, Math.ceil(2 * Math.PI / step)));
|
|
3362
|
+
const pts = [];
|
|
3363
|
+
for (let i = 0; i < n; i++) {
|
|
3364
|
+
const t = i / n * 2 * Math.PI;
|
|
3365
|
+
pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
|
|
3366
|
+
}
|
|
3367
|
+
return pts;
|
|
3368
|
+
}
|
|
3369
|
+
function ellipseSelection(width, height, rect, options = {}) {
|
|
3370
|
+
return polygonSelection(width, height, ellipsePoints(rect), options);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/image-editor/selection/wand.ts
|
|
3374
|
+
function checkPixels(p) {
|
|
3375
|
+
const n = p.width * p.height;
|
|
3376
|
+
if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
|
|
3377
|
+
throw new Error("Invalid pixel buffer size");
|
|
3378
|
+
}
|
|
3379
|
+
if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
|
|
3380
|
+
return n;
|
|
3381
|
+
}
|
|
3382
|
+
var SRGB_TO_LINEAR = (() => {
|
|
3383
|
+
const t = new Float32Array(256);
|
|
3384
|
+
for (let i = 0; i < 256; i++) {
|
|
3385
|
+
const v = i / 255;
|
|
3386
|
+
t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
3387
|
+
}
|
|
3388
|
+
return t;
|
|
3389
|
+
})();
|
|
3390
|
+
var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
|
|
3391
|
+
function srgb8ToLab(r, g, b) {
|
|
3392
|
+
const rl = SRGB_TO_LINEAR[r & 255];
|
|
3393
|
+
const gl = SRGB_TO_LINEAR[g & 255];
|
|
3394
|
+
const bl = SRGB_TO_LINEAR[b & 255];
|
|
3395
|
+
const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
|
|
3396
|
+
const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
|
|
3397
|
+
const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
|
|
3398
|
+
const fx = labF(x);
|
|
3399
|
+
const fy = labF(y);
|
|
3400
|
+
const fz = labF(z);
|
|
3401
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
3402
|
+
}
|
|
3403
|
+
function similarity(pixels, seed, tolerance, distance) {
|
|
3404
|
+
const d = pixels.data;
|
|
3405
|
+
const tr = d[seed * 4];
|
|
3406
|
+
const tg = d[seed * 4 + 1];
|
|
3407
|
+
const tb = d[seed * 4 + 2];
|
|
3408
|
+
const ta = d[seed * 4 + 3];
|
|
3409
|
+
const tol = Math.max(0, Math.floor(tolerance + 1e-3));
|
|
3410
|
+
if (distance === "perceptual") {
|
|
3411
|
+
const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
|
|
3412
|
+
const de = Math.max(0, tolerance) * 100 / 255;
|
|
3413
|
+
const de2 = de * de + 1e-6;
|
|
3414
|
+
return (i) => {
|
|
3415
|
+
const p = i * 4;
|
|
3416
|
+
const a = d[p + 3];
|
|
3417
|
+
if (ta === 0 && a === 0) return true;
|
|
3418
|
+
if (Math.abs(a - ta) > tol) return false;
|
|
3419
|
+
const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
|
|
3420
|
+
return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
return (i) => {
|
|
3424
|
+
const p = i * 4;
|
|
3425
|
+
const a = d[p + 3];
|
|
3426
|
+
if (ta === 0 && a === 0) return true;
|
|
3427
|
+
return Math.abs(d[p] - tr) <= tol && Math.abs(d[p + 1] - tg) <= tol && Math.abs(d[p + 2] - tb) <= tol && Math.abs(a - ta) <= tol;
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
|
|
3431
|
+
const out = marks.slice();
|
|
3432
|
+
for (let y = y0; y < y1; y++) {
|
|
3433
|
+
for (let x = x0; x < x1; x++) {
|
|
3434
|
+
const i = y * w + x;
|
|
3435
|
+
const v = marks[i];
|
|
3436
|
+
let sum = 0;
|
|
3437
|
+
let edge = false;
|
|
3438
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3439
|
+
const ny = y + dy;
|
|
3440
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
3441
|
+
const nx = x + dx;
|
|
3442
|
+
const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
|
|
3443
|
+
if (n !== v) edge = true;
|
|
3444
|
+
sum += n;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
if (edge) {
|
|
3448
|
+
const avg = sum / (9 * 255);
|
|
3449
|
+
out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
return out;
|
|
3454
|
+
}
|
|
3455
|
+
function magicWand(pixels, seed, options = {}) {
|
|
3456
|
+
const n = checkPixels(pixels);
|
|
3457
|
+
const { width: w, height: h } = pixels;
|
|
3458
|
+
const sx = Math.floor(seed.x);
|
|
3459
|
+
const sy = Math.floor(seed.y);
|
|
3460
|
+
if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
|
|
3461
|
+
const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
|
|
3462
|
+
const marks = new Uint8Array(n);
|
|
3463
|
+
let bx0 = w;
|
|
3464
|
+
let by0 = h;
|
|
3465
|
+
let bx1 = -1;
|
|
3466
|
+
let by1 = -1;
|
|
3467
|
+
if (options.contiguous ?? true) {
|
|
3468
|
+
const stack = [sy * w + sx];
|
|
3469
|
+
while (stack.length) {
|
|
3470
|
+
const idx = stack.pop();
|
|
3471
|
+
if (marks[idx]) continue;
|
|
3472
|
+
const y = idx / w | 0;
|
|
3473
|
+
const row = y * w;
|
|
3474
|
+
let l = idx - row;
|
|
3475
|
+
let r = l;
|
|
3476
|
+
while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
|
|
3477
|
+
while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
|
|
3478
|
+
marks.fill(255, row + l, row + r + 1);
|
|
3479
|
+
if (l < bx0) bx0 = l;
|
|
3480
|
+
if (r > bx1) bx1 = r;
|
|
3481
|
+
if (y < by0) by0 = y;
|
|
3482
|
+
if (y > by1) by1 = y;
|
|
3483
|
+
for (const ny of [y - 1, y + 1]) {
|
|
3484
|
+
if (ny < 0 || ny >= h) continue;
|
|
3485
|
+
const nrow = ny * w;
|
|
3486
|
+
let inRun = false;
|
|
3487
|
+
for (let nx = l; nx <= r; nx++) {
|
|
3488
|
+
const ok = !marks[nrow + nx] && similar(nrow + nx);
|
|
3489
|
+
if (ok && !inRun) stack.push(nrow + nx);
|
|
3490
|
+
inRun = ok;
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
} else {
|
|
3495
|
+
for (let y = 0; y < h; y++) {
|
|
3496
|
+
const row = y * w;
|
|
3497
|
+
for (let x = 0; x < w; x++) {
|
|
3498
|
+
if (similar(row + x)) {
|
|
3499
|
+
marks[row + x] = 255;
|
|
3500
|
+
if (x < bx0) bx0 = x;
|
|
3501
|
+
if (x > bx1) bx1 = x;
|
|
3502
|
+
if (y < by0) by0 = y;
|
|
3503
|
+
if (y > by1) by1 = y;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
if (bx1 < 0) return createSelectionMask(w, h);
|
|
3509
|
+
if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
|
|
3510
|
+
return createSelectionMask(
|
|
3511
|
+
w,
|
|
3512
|
+
h,
|
|
3513
|
+
antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
function sampleColor(c) {
|
|
3517
|
+
if (typeof c === "string") {
|
|
3518
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
|
|
3519
|
+
if (!m) throw new Error(`Invalid colour "${c}"`);
|
|
3520
|
+
const v = parseInt(m[1], 16);
|
|
3521
|
+
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
3522
|
+
}
|
|
3523
|
+
return [c[0], c[1], c[2]];
|
|
3524
|
+
}
|
|
3525
|
+
function colorRange(pixels, samples, options = {}) {
|
|
3526
|
+
const n = checkPixels(pixels);
|
|
3527
|
+
const { width: w, height: h, data } = pixels;
|
|
3528
|
+
const f = Math.max(0, options.fuzziness ?? 40) + 1;
|
|
3529
|
+
const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
|
|
3530
|
+
const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
|
|
3531
|
+
const out = new Uint8Array(n);
|
|
3532
|
+
if (!parsed.length) return createSelectionMask(w, h, out);
|
|
3533
|
+
for (let i = 0; i < n; i++) {
|
|
3534
|
+
const p = i * 4;
|
|
3535
|
+
if (data[p + 3] <= 0) continue;
|
|
3536
|
+
const x = i % w;
|
|
3537
|
+
const y = (i - x) / w;
|
|
3538
|
+
let best = 0;
|
|
3539
|
+
for (const s of parsed) {
|
|
3540
|
+
const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));
|
|
3541
|
+
let k = Math.min(1, Math.max(0, 1 - d / f));
|
|
3542
|
+
if (k > 0 && radius !== void 0 && s.at) {
|
|
3543
|
+
const dist = Math.hypot(x - s.at.x, y - s.at.y);
|
|
3544
|
+
k *= Math.min(1, Math.max(0, 1 - dist / radius));
|
|
3545
|
+
}
|
|
3546
|
+
if (k > best) best = k;
|
|
3547
|
+
}
|
|
3548
|
+
out[i] = coverageToByte(best);
|
|
3549
|
+
}
|
|
3550
|
+
return createSelectionMask(w, h, out);
|
|
3551
|
+
}
|
|
3552
|
+
var TONE_RANGE_PRESETS = {
|
|
3553
|
+
shadows: [0, 65],
|
|
3554
|
+
midtones: [105, 150],
|
|
3555
|
+
highlights: [190, 255]
|
|
3556
|
+
};
|
|
3557
|
+
function toneRange(pixels, range, falloff = 20) {
|
|
3558
|
+
const n = checkPixels(pixels);
|
|
3559
|
+
const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
|
|
3560
|
+
const ramp = Math.max(1e-3, falloff);
|
|
3561
|
+
const out = new Uint8Array(n);
|
|
3562
|
+
const d = pixels.data;
|
|
3563
|
+
for (let i = 0; i < n; i++) {
|
|
3564
|
+
const p = i * 4;
|
|
3565
|
+
if (d[p + 3] <= 0) continue;
|
|
3566
|
+
const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
|
|
3567
|
+
const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
|
|
3568
|
+
out[i] = coverageToByte(1 - outside / ramp);
|
|
3569
|
+
}
|
|
3570
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3571
|
+
}
|
|
3572
|
+
function hueRange(pixels, center) {
|
|
3573
|
+
const n = checkPixels(pixels);
|
|
3574
|
+
const out = new Uint8Array(n);
|
|
3575
|
+
const d = pixels.data;
|
|
3576
|
+
for (let i = 0; i < n; i++) {
|
|
3577
|
+
const p = i * 4;
|
|
3578
|
+
if (d[p + 3] <= 0) continue;
|
|
3579
|
+
const r = d[p] / 255;
|
|
3580
|
+
const g = d[p + 1] / 255;
|
|
3581
|
+
const b = d[p + 2] / 255;
|
|
3582
|
+
const mx = Math.max(r, g, b);
|
|
3583
|
+
const mn = Math.min(r, g, b);
|
|
3584
|
+
const chroma = mx - mn;
|
|
3585
|
+
if (chroma <= 0) continue;
|
|
3586
|
+
let hue;
|
|
3587
|
+
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
3588
|
+
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
3589
|
+
else hue = (r - g) / chroma + 4;
|
|
3590
|
+
hue *= 60;
|
|
3591
|
+
let dh = ((hue - center) % 360 + 360) % 360;
|
|
3592
|
+
dh = Math.min(dh, 360 - dh);
|
|
3593
|
+
out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
|
|
3594
|
+
}
|
|
3595
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// src/image-editor/selection/refine.ts
|
|
3599
|
+
var DEFAULT_REFINE_EDGE_PARAMS = {
|
|
3600
|
+
radius: 0,
|
|
3601
|
+
smartRadius: false,
|
|
3602
|
+
smooth: 0,
|
|
3603
|
+
feather: 0,
|
|
3604
|
+
contrast: 0,
|
|
3605
|
+
shiftEdge: 0
|
|
3606
|
+
};
|
|
3607
|
+
var GUIDED_FILTER_EPS = 1e-4;
|
|
3608
|
+
var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
3609
|
+
function normalizeRefineEdgeParams(p = {}) {
|
|
3610
|
+
return {
|
|
3611
|
+
radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
|
|
3612
|
+
smartRadius: p.smartRadius === true,
|
|
3613
|
+
smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
|
|
3614
|
+
feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
|
|
3615
|
+
contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
|
|
3616
|
+
shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
|
|
3620
|
+
var smoothSigma = (p) => p.smooth / 100 * 4;
|
|
3621
|
+
var featherSigma = (p) => p.feather / 2;
|
|
3622
|
+
function shiftAmount(p) {
|
|
3623
|
+
const px = p.shiftEdge / 100 * Math.max(p.radius, 4);
|
|
3624
|
+
return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;
|
|
3625
|
+
}
|
|
3626
|
+
function refineEdgeHalo(params = {}) {
|
|
3627
|
+
const p = normalizeRefineEdgeParams(params);
|
|
3628
|
+
const g = gfRadius(p);
|
|
3629
|
+
const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
|
|
3630
|
+
return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
|
|
3631
|
+
}
|
|
3632
|
+
function boxMean(src, w, h, r) {
|
|
3633
|
+
const tmp = new Float32Array(w * h);
|
|
3634
|
+
const pre = new Float64Array(Math.max(w, h) + 1);
|
|
3635
|
+
for (let y = 0; y < h; y++) {
|
|
3636
|
+
const row = y * w;
|
|
3637
|
+
for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
|
|
3638
|
+
for (let x = 0; x < w; x++) {
|
|
3639
|
+
const lo = Math.max(0, x - r);
|
|
3640
|
+
const hi = Math.min(w, x + r + 1);
|
|
3641
|
+
tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
const out = new Float32Array(w * h);
|
|
3645
|
+
for (let x = 0; x < w; x++) {
|
|
3646
|
+
for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
|
|
3647
|
+
for (let y = 0; y < h; y++) {
|
|
3648
|
+
const lo = Math.max(0, y - r);
|
|
3649
|
+
const hi = Math.min(h, y + r + 1);
|
|
3650
|
+
out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
return out;
|
|
3654
|
+
}
|
|
3655
|
+
function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
|
|
3656
|
+
const n = w * h;
|
|
3657
|
+
const [c0, c1, c2] = guide;
|
|
3658
|
+
const prod = (a, b2) => {
|
|
3659
|
+
const o = new Float32Array(n);
|
|
3660
|
+
for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
|
|
3661
|
+
return o;
|
|
3662
|
+
};
|
|
3663
|
+
const m0 = boxMean(c0, w, h, r);
|
|
3664
|
+
const m1 = boxMean(c1, w, h, r);
|
|
3665
|
+
const m2 = boxMean(c2, w, h, r);
|
|
3666
|
+
const mp = boxMean(p, w, h, r);
|
|
3667
|
+
const mip0 = boxMean(prod(c0, p), w, h, r);
|
|
3668
|
+
const mip1 = boxMean(prod(c1, p), w, h, r);
|
|
3669
|
+
const mip2 = boxMean(prod(c2, p), w, h, r);
|
|
3670
|
+
const s00 = boxMean(prod(c0, c0), w, h, r);
|
|
3671
|
+
const s01 = boxMean(prod(c0, c1), w, h, r);
|
|
3672
|
+
const s02 = boxMean(prod(c0, c2), w, h, r);
|
|
3673
|
+
const s11 = boxMean(prod(c1, c1), w, h, r);
|
|
3674
|
+
const s12 = boxMean(prod(c1, c2), w, h, r);
|
|
3675
|
+
const s22 = boxMean(prod(c2, c2), w, h, r);
|
|
3676
|
+
const a0 = new Float32Array(n);
|
|
3677
|
+
const a1 = new Float32Array(n);
|
|
3678
|
+
const a2 = new Float32Array(n);
|
|
3679
|
+
const b = new Float32Array(n);
|
|
3680
|
+
for (let i = 0; i < n; i++) {
|
|
3681
|
+
const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
|
|
3682
|
+
const rr = s00[i] - u0 * u0 + eps;
|
|
3683
|
+
const rg = s01[i] - u0 * u1;
|
|
3684
|
+
const rb = s02[i] - u0 * u2;
|
|
3685
|
+
const gg = s11[i] - u1 * u1 + eps;
|
|
3686
|
+
const gb = s12[i] - u1 * u2;
|
|
3687
|
+
const bb = s22[i] - u2 * u2 + eps;
|
|
3688
|
+
const v0 = mip0[i] - u0 * up;
|
|
3689
|
+
const v1 = mip1[i] - u1 * up;
|
|
3690
|
+
const v2 = mip2[i] - u2 * up;
|
|
3691
|
+
const i00 = gg * bb - gb * gb;
|
|
3692
|
+
const i01 = gb * rb - rg * bb;
|
|
3693
|
+
const i02 = rg * gb - gg * rb;
|
|
3694
|
+
const i11 = rr * bb - rb * rb;
|
|
3695
|
+
const i12 = rb * rg - rr * gb;
|
|
3696
|
+
const i22 = rr * gg - rg * rg;
|
|
3697
|
+
const det = rr * i00 + rg * i01 + rb * i02;
|
|
3698
|
+
let x0 = 0, x1 = 0, x2 = 0;
|
|
3699
|
+
if (Math.abs(det) >= 1e-30) {
|
|
3700
|
+
x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
|
|
3701
|
+
x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
|
|
3702
|
+
x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
|
|
3703
|
+
}
|
|
3704
|
+
a0[i] = x0;
|
|
3705
|
+
a1[i] = x1;
|
|
3706
|
+
a2[i] = x2;
|
|
3707
|
+
b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
|
|
3708
|
+
}
|
|
3709
|
+
const ma0 = boxMean(a0, w, h, r);
|
|
3710
|
+
const ma1 = boxMean(a1, w, h, r);
|
|
3711
|
+
const ma2 = boxMean(a2, w, h, r);
|
|
3712
|
+
const mb = boxMean(b, w, h, r);
|
|
3713
|
+
const out = new Float32Array(n);
|
|
3714
|
+
for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
|
|
3715
|
+
return out;
|
|
3716
|
+
}
|
|
3717
|
+
function edgeWidth(guide, w, h, r) {
|
|
3718
|
+
const n = w * h;
|
|
3719
|
+
const y = new Float32Array(n);
|
|
3720
|
+
for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
|
|
3721
|
+
const g = new Float32Array(n);
|
|
3722
|
+
for (let yy = 0; yy < h; yy++) {
|
|
3723
|
+
for (let xx = 0; xx < w; xx++) {
|
|
3724
|
+
const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
|
|
3725
|
+
const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
|
|
3726
|
+
g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
const gm = greyMorphology(g, w, h, r, true);
|
|
3730
|
+
const hi = greyMorphology(y, w, h, r, true);
|
|
3731
|
+
const lo = greyMorphology(y, w, h, r, false);
|
|
3732
|
+
const out = new Float32Array(n);
|
|
3733
|
+
for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
|
|
3734
|
+
return out;
|
|
3735
|
+
}
|
|
3736
|
+
function refineCoverage(guide, m, w, h, params = {}) {
|
|
3737
|
+
const p = normalizeRefineEdgeParams(params);
|
|
3738
|
+
let a = Float32Array.from(m);
|
|
3739
|
+
if (p.radius > 0 && guide) {
|
|
3740
|
+
const n = w * h;
|
|
3741
|
+
const boundary = new Uint8Array(n);
|
|
3742
|
+
let any = false;
|
|
3743
|
+
for (let y = 0; y < h; y++) {
|
|
3744
|
+
for (let x = 0; x < w; x++) {
|
|
3745
|
+
const i = y * w + x;
|
|
3746
|
+
const v = m[i] >= 0.5;
|
|
3747
|
+
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;
|
|
3748
|
+
if (diff) {
|
|
3749
|
+
boundary[i] = 1;
|
|
3750
|
+
any = true;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
if (any) {
|
|
3755
|
+
const dist = distanceTransform(boundary, w, h);
|
|
3756
|
+
const r = gfRadius(p);
|
|
3757
|
+
const q = guidedFilterColor(guide, m, w, h, r);
|
|
3758
|
+
const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
|
|
3759
|
+
for (let i = 0; i < n; i++) {
|
|
3760
|
+
const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
|
|
3761
|
+
const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
|
|
3762
|
+
if (wgt > 0) {
|
|
3763
|
+
const qi = Math.min(1, Math.max(0, q[i]));
|
|
3764
|
+
a[i] = m[i] + (qi - m[i]) * wgt;
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
const ss = smoothSigma(p);
|
|
3770
|
+
if (ss >= 0.3) {
|
|
3771
|
+
a = gaussianBlur(a, w, h, ss, "clamp");
|
|
3772
|
+
const gain = 1 + ss;
|
|
3773
|
+
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
3774
|
+
}
|
|
3775
|
+
a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
|
|
3776
|
+
if (p.contrast > 0) {
|
|
3777
|
+
const gain = 1 / (1 - 0.99 * p.contrast / 100);
|
|
3778
|
+
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
3779
|
+
}
|
|
3780
|
+
const amount = shiftAmount(p);
|
|
3781
|
+
if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
|
|
3782
|
+
return a;
|
|
3783
|
+
}
|
|
3784
|
+
function guidePlanes(pixels, win) {
|
|
3785
|
+
const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
|
|
3786
|
+
const n = box.width * box.height;
|
|
3787
|
+
const r = new Float32Array(n);
|
|
3788
|
+
const g = new Float32Array(n);
|
|
3789
|
+
const b = new Float32Array(n);
|
|
3790
|
+
const d = pixels.data;
|
|
3791
|
+
for (let y = 0; y < box.height; y++) {
|
|
3792
|
+
for (let x = 0; x < box.width; x++) {
|
|
3793
|
+
const i = y * box.width + x;
|
|
3794
|
+
const p = ((box.y + y) * pixels.width + box.x + x) * 4;
|
|
3795
|
+
r[i] = d[p] / 255;
|
|
3796
|
+
g[i] = d[p + 1] / 255;
|
|
3797
|
+
b[i] = d[p + 2] / 255;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
return [r, g, b];
|
|
3801
|
+
}
|
|
3802
|
+
function refineSelection(pixels, mask, params = {}) {
|
|
3803
|
+
const p = normalizeRefineEdgeParams(params);
|
|
3804
|
+
if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
|
|
3805
|
+
throw new Error("Refine Edge needs pixels the size of the selection");
|
|
3806
|
+
}
|
|
3807
|
+
if (!mask.bounds) return mask;
|
|
3808
|
+
const halo = refineEdgeHalo(p);
|
|
3809
|
+
const x0 = Math.max(0, mask.bounds.x - halo);
|
|
3810
|
+
const y0 = Math.max(0, mask.bounds.y - halo);
|
|
3811
|
+
const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
|
|
3812
|
+
const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
|
|
3813
|
+
const ww = x1 - x0;
|
|
3814
|
+
const wh = y1 - y0;
|
|
3815
|
+
const m = new Float32Array(ww * wh);
|
|
3816
|
+
for (let y = 0; y < wh; y++) {
|
|
3817
|
+
const src = (y0 + y) * mask.width + x0;
|
|
3818
|
+
for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
|
|
3819
|
+
}
|
|
3820
|
+
const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
|
|
3821
|
+
const refined = refineCoverage(guide, m, ww, wh, p);
|
|
3822
|
+
const out = new Uint8Array(mask.width * mask.height);
|
|
3823
|
+
for (let y = 0; y < wh; y++) {
|
|
3824
|
+
const dst = (y0 + y) * mask.width + x0;
|
|
3825
|
+
for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
|
|
3826
|
+
}
|
|
3827
|
+
return createSelectionMask(mask.width, mask.height, out);
|
|
1401
3828
|
}
|
|
1402
3829
|
|
|
1403
3830
|
// src/image-editor/controller.ts
|
|
3831
|
+
var IMAGE_EDITOR_SCHEMA_VERSION = 2;
|
|
1404
3832
|
var DEFAULT_ADJUSTMENTS = {
|
|
1405
3833
|
brightness: 0,
|
|
1406
3834
|
contrast: 0,
|
|
@@ -1414,11 +3842,31 @@ var DEFAULT_ADJUSTMENTS = {
|
|
|
1414
3842
|
};
|
|
1415
3843
|
function normalizeStoredAdjustments(stored) {
|
|
1416
3844
|
if (!stored) return { ...DEFAULT_ADJUSTMENTS };
|
|
1417
|
-
|
|
3845
|
+
const out = {
|
|
1418
3846
|
...DEFAULT_ADJUSTMENTS,
|
|
1419
3847
|
...stored,
|
|
1420
3848
|
process: normalizeProcessVersion(stored.process)
|
|
1421
3849
|
};
|
|
3850
|
+
const layers = normalizeAdjustmentLayers(stored.layers);
|
|
3851
|
+
if (layers.length) out.layers = layers;
|
|
3852
|
+
else delete out.layers;
|
|
3853
|
+
return out;
|
|
3854
|
+
}
|
|
3855
|
+
function migrateSerializedImageEditorState(data) {
|
|
3856
|
+
const adjustments = normalizeStoredAdjustments(data.adjustments);
|
|
3857
|
+
return {
|
|
3858
|
+
version: adjustments.layers?.length ? IMAGE_EDITOR_SCHEMA_VERSION : 1,
|
|
3859
|
+
crop: data.crop ? { ...data.crop } : null,
|
|
3860
|
+
transform: { ...DEFAULT_TRANSFORM, ...data.transform },
|
|
3861
|
+
adjustments: serializeAdjustments(adjustments),
|
|
3862
|
+
depthMask: {
|
|
3863
|
+
...DEFAULT_DEPTH_MASK,
|
|
3864
|
+
...data.depthMask,
|
|
3865
|
+
depthRange: [...data.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange]
|
|
3866
|
+
},
|
|
3867
|
+
annotations: JSON.parse(JSON.stringify(data.annotations ?? [])),
|
|
3868
|
+
...data.selection ? { selection: { ...data.selection } } : {}
|
|
3869
|
+
};
|
|
1422
3870
|
}
|
|
1423
3871
|
var DEFAULT_TRANSFORM = {
|
|
1424
3872
|
rotate: 0,
|
|
@@ -1438,6 +3886,7 @@ var ImageEditorController = class {
|
|
|
1438
3886
|
renderer;
|
|
1439
3887
|
imageElement = null;
|
|
1440
3888
|
listeners = /* @__PURE__ */ new Set();
|
|
3889
|
+
histogramListeners = /* @__PURE__ */ new Set();
|
|
1441
3890
|
onStateChange;
|
|
1442
3891
|
// History stacks
|
|
1443
3892
|
undoStack = [];
|
|
@@ -1445,10 +3894,16 @@ var ImageEditorController = class {
|
|
|
1445
3894
|
maxHistoryDepth;
|
|
1446
3895
|
isApplyingHistory = false;
|
|
1447
3896
|
initialSnapshot;
|
|
3897
|
+
/** Pixels the selection tools sample (host-provided or read back from the source). */
|
|
3898
|
+
samplePixels = null;
|
|
3899
|
+
samplePixelsOverride = false;
|
|
1448
3900
|
constructor(options = {}) {
|
|
1449
3901
|
this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
|
|
1450
3902
|
this.onStateChange = options.onStateChange;
|
|
1451
3903
|
this.renderer = new ImageEditorRenderer();
|
|
3904
|
+
this.renderer.onPreviewHistogram = (histogram) => {
|
|
3905
|
+
this.histogramListeners.forEach((l) => l(histogram));
|
|
3906
|
+
};
|
|
1452
3907
|
const initialAdjustments = normalizeStoredAdjustments(options.initialState?.adjustments);
|
|
1453
3908
|
const initialTransform = {
|
|
1454
3909
|
...DEFAULT_TRANSFORM,
|
|
@@ -1470,8 +3925,16 @@ var ImageEditorController = class {
|
|
|
1470
3925
|
activeTool: "select",
|
|
1471
3926
|
zoom: 1,
|
|
1472
3927
|
pan: { x: 0, y: 0 },
|
|
1473
|
-
isOriginalCompared: false
|
|
3928
|
+
isOriginalCompared: false,
|
|
3929
|
+
selection: null,
|
|
3930
|
+
quickMask: false
|
|
1474
3931
|
};
|
|
3932
|
+
if (options.initialState?.selection) {
|
|
3933
|
+
try {
|
|
3934
|
+
this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));
|
|
3935
|
+
} catch {
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
1475
3938
|
if (options.image instanceof Object && "width" in options.image) {
|
|
1476
3939
|
this.imageElement = options.image;
|
|
1477
3940
|
this.state.sourceUrl = options.image.src || null;
|
|
@@ -1497,6 +3960,8 @@ var ImageEditorController = class {
|
|
|
1497
3960
|
this.imageElement = source.element;
|
|
1498
3961
|
this.state.sourceUrl = source.sourceUrl;
|
|
1499
3962
|
this.state.imageDimensions = source.dimensions;
|
|
3963
|
+
this.samplePixels = null;
|
|
3964
|
+
this.dropMismatchedSelection();
|
|
1500
3965
|
this.initialSnapshot = this.captureHistoryEntry();
|
|
1501
3966
|
this.notify();
|
|
1502
3967
|
}
|
|
@@ -1552,11 +4017,14 @@ var ImageEditorController = class {
|
|
|
1552
4017
|
sourceUrl: this.state.sourceUrl,
|
|
1553
4018
|
element: this.imageElement,
|
|
1554
4019
|
dimensions: { ...this.state.imageDimensions }
|
|
1555
|
-
}
|
|
4020
|
+
},
|
|
4021
|
+
selection: this.state.selection ?? null
|
|
1556
4022
|
};
|
|
1557
4023
|
}
|
|
1558
4024
|
restoreHistoryEntry(entry) {
|
|
1559
4025
|
this.applySerializedState(entry.state);
|
|
4026
|
+
this.state.selection = entry.selection;
|
|
4027
|
+
if (this.imageElement !== entry.source.element) this.samplePixels = null;
|
|
1560
4028
|
this.imageElement = entry.source.element;
|
|
1561
4029
|
this.state.sourceUrl = entry.source.sourceUrl;
|
|
1562
4030
|
this.state.imageDimensions = { ...entry.source.dimensions };
|
|
@@ -1690,10 +4158,16 @@ var ImageEditorController = class {
|
|
|
1690
4158
|
if (commitToHistory) {
|
|
1691
4159
|
this.recordHistory();
|
|
1692
4160
|
}
|
|
1693
|
-
|
|
4161
|
+
const next = {
|
|
1694
4162
|
...this.state.adjustments,
|
|
1695
4163
|
...partial
|
|
1696
4164
|
};
|
|
4165
|
+
if ("layers" in partial) {
|
|
4166
|
+
const layers = normalizeAdjustmentLayers(partial.layers);
|
|
4167
|
+
if (layers.length) next.layers = layers;
|
|
4168
|
+
else delete next.layers;
|
|
4169
|
+
}
|
|
4170
|
+
this.state.adjustments = next;
|
|
1697
4171
|
this.notify();
|
|
1698
4172
|
}
|
|
1699
4173
|
/** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */
|
|
@@ -1709,11 +4183,338 @@ var ImageEditorController = class {
|
|
|
1709
4183
|
this.setAdjustments({ process: LATEST_PROCESS_VERSION });
|
|
1710
4184
|
return true;
|
|
1711
4185
|
}
|
|
4186
|
+
/** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */
|
|
1712
4187
|
resetAdjustments() {
|
|
1713
4188
|
this.recordHistory();
|
|
4189
|
+
const layers = this.state.adjustments.layers;
|
|
1714
4190
|
this.state.adjustments = { ...DEFAULT_ADJUSTMENTS };
|
|
4191
|
+
if (layers?.length) this.state.adjustments.layers = layers;
|
|
4192
|
+
this.notify();
|
|
4193
|
+
}
|
|
4194
|
+
// --- Adjustment layers ---
|
|
4195
|
+
/** The adjustment layers, bottom (first) to top (last). */
|
|
4196
|
+
getAdjustmentLayers() {
|
|
4197
|
+
return this.state.adjustments.layers ?? [];
|
|
4198
|
+
}
|
|
4199
|
+
getAdjustmentLayer(id) {
|
|
4200
|
+
return this.getAdjustmentLayers().find((l) => l.id === id);
|
|
4201
|
+
}
|
|
4202
|
+
setLayers(layers) {
|
|
4203
|
+
const next = { ...this.state.adjustments };
|
|
4204
|
+
if (layers.length) next.layers = layers;
|
|
4205
|
+
else delete next.layers;
|
|
4206
|
+
this.state.adjustments = next;
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Adds an adjustment layer on top (or at `index`, 0 = bottom) as one undo step and returns
|
|
4210
|
+
* its id. `kind` alone gives neutral parameters.
|
|
4211
|
+
*/
|
|
4212
|
+
addAdjustmentLayer(init, index) {
|
|
4213
|
+
const spec = typeof init === "string" ? { kind: init } : init;
|
|
4214
|
+
const layers = [...this.getAdjustmentLayers()];
|
|
4215
|
+
const layer = createAdjustmentLayer(spec.kind, {
|
|
4216
|
+
id: spec.id && !layers.some((l) => l.id === spec.id) ? spec.id : void 0,
|
|
4217
|
+
name: spec.name,
|
|
4218
|
+
params: spec.params,
|
|
4219
|
+
enabled: spec.enabled,
|
|
4220
|
+
opacity: spec.opacity,
|
|
4221
|
+
blend: spec.blend,
|
|
4222
|
+
mask: spec.mask ?? void 0
|
|
4223
|
+
});
|
|
4224
|
+
this.recordHistory();
|
|
4225
|
+
const at = index === void 0 ? layers.length : Math.max(0, Math.min(layers.length, Math.round(index)));
|
|
4226
|
+
layers.splice(at, 0, layer);
|
|
4227
|
+
this.setLayers(layers);
|
|
4228
|
+
this.notify();
|
|
4229
|
+
return layer.id;
|
|
4230
|
+
}
|
|
4231
|
+
/**
|
|
4232
|
+
* Updates a layer (`params` merged into the current ones, then normalised). Pass
|
|
4233
|
+
* `commitToHistory = false` for the moves of a drag after recording its start once.
|
|
4234
|
+
* Returns false for an unknown id.
|
|
4235
|
+
*/
|
|
4236
|
+
updateAdjustmentLayer(id, update, commitToHistory = true) {
|
|
4237
|
+
const layers = this.getAdjustmentLayers();
|
|
4238
|
+
const index = layers.findIndex((l) => l.id === id);
|
|
4239
|
+
if (index === -1) return false;
|
|
4240
|
+
const current = layers[index];
|
|
4241
|
+
const merged = normalizeAdjustmentLayer({
|
|
4242
|
+
...current,
|
|
4243
|
+
...update.enabled !== void 0 ? { enabled: update.enabled } : {},
|
|
4244
|
+
...update.opacity !== void 0 ? { opacity: update.opacity } : {},
|
|
4245
|
+
...update.blend !== void 0 ? { blend: update.blend } : {},
|
|
4246
|
+
...update.name !== void 0 ? { name: update.name } : {},
|
|
4247
|
+
...update.mask !== void 0 ? { mask: update.mask ?? void 0 } : {},
|
|
4248
|
+
params: { ...current.params, ...update.params ?? {} }
|
|
4249
|
+
});
|
|
4250
|
+
if (!merged) return false;
|
|
4251
|
+
if (commitToHistory) this.recordHistory();
|
|
4252
|
+
const next = [...layers];
|
|
4253
|
+
next[index] = merged;
|
|
4254
|
+
this.setLayers(next);
|
|
4255
|
+
this.notify();
|
|
4256
|
+
return true;
|
|
4257
|
+
}
|
|
4258
|
+
/** Removes a layer as one undo step. Returns false for an unknown id. */
|
|
4259
|
+
removeAdjustmentLayer(id) {
|
|
4260
|
+
const layers = this.getAdjustmentLayers();
|
|
4261
|
+
if (!layers.some((l) => l.id === id)) return false;
|
|
4262
|
+
this.recordHistory();
|
|
4263
|
+
this.setLayers(layers.filter((l) => l.id !== id));
|
|
4264
|
+
this.notify();
|
|
4265
|
+
return true;
|
|
4266
|
+
}
|
|
4267
|
+
/** Moves a layer to `toIndex` (0 = bottom) as one undo step. Returns false if nothing moved. */
|
|
4268
|
+
moveAdjustmentLayer(id, toIndex) {
|
|
4269
|
+
const layers = [...this.getAdjustmentLayers()];
|
|
4270
|
+
const from = layers.findIndex((l) => l.id === id);
|
|
4271
|
+
if (from === -1) return false;
|
|
4272
|
+
const to = Math.max(0, Math.min(layers.length - 1, Math.round(toIndex)));
|
|
4273
|
+
if (to === from) return false;
|
|
4274
|
+
this.recordHistory();
|
|
4275
|
+
const [layer] = layers.splice(from, 1);
|
|
4276
|
+
layers.splice(to, 0, layer);
|
|
4277
|
+
this.setLayers(layers);
|
|
4278
|
+
this.notify();
|
|
4279
|
+
return true;
|
|
4280
|
+
}
|
|
4281
|
+
/** Shows or hides a layer (flips it without `enabled`) as one undo step. */
|
|
4282
|
+
toggleAdjustmentLayer(id, enabled) {
|
|
4283
|
+
const layer = this.getAdjustmentLayer(id);
|
|
4284
|
+
if (!layer) return false;
|
|
4285
|
+
const next = enabled ?? !layer.enabled;
|
|
4286
|
+
if (next === layer.enabled) return false;
|
|
4287
|
+
return this.updateAdjustmentLayer(id, { enabled: next });
|
|
4288
|
+
}
|
|
4289
|
+
/** Removes every adjustment layer as one undo step. */
|
|
4290
|
+
clearAdjustmentLayers() {
|
|
4291
|
+
if (!this.getAdjustmentLayers().length) return;
|
|
4292
|
+
this.recordHistory();
|
|
4293
|
+
this.setLayers([]);
|
|
4294
|
+
this.notify();
|
|
4295
|
+
}
|
|
4296
|
+
// --- Selection ---
|
|
4297
|
+
/** The active selection, or `null`. */
|
|
4298
|
+
getSelection() {
|
|
4299
|
+
return this.state.selection ?? null;
|
|
4300
|
+
}
|
|
4301
|
+
get hasSelection() {
|
|
4302
|
+
return Boolean(this.state.selection?.bounds);
|
|
4303
|
+
}
|
|
4304
|
+
get naturalSize() {
|
|
4305
|
+
return {
|
|
4306
|
+
width: Math.max(1, Math.round(this.state.imageDimensions.width)),
|
|
4307
|
+
height: Math.max(1, Math.round(this.state.imageDimensions.height))
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
dropMismatchedSelection() {
|
|
4311
|
+
const sel = this.state.selection;
|
|
4312
|
+
if (!sel) return;
|
|
4313
|
+
const { width, height } = this.naturalSize;
|
|
4314
|
+
if (sel.width !== width || sel.height !== height) this.state.selection = null;
|
|
4315
|
+
}
|
|
4316
|
+
/**
|
|
4317
|
+
* Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a
|
|
4318
|
+
* different size (e.g. an AI segmentation at another resolution) is resampled to the image.
|
|
4319
|
+
* Returns false when nothing changed.
|
|
4320
|
+
*/
|
|
4321
|
+
setSelection(mask, mode = "replace") {
|
|
4322
|
+
const { width, height } = this.naturalSize;
|
|
4323
|
+
let next = null;
|
|
4324
|
+
if (mask) {
|
|
4325
|
+
const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);
|
|
4326
|
+
next = combineSelections(this.state.selection, sized, mode);
|
|
4327
|
+
} else if (mode !== "replace") {
|
|
4328
|
+
return false;
|
|
4329
|
+
}
|
|
4330
|
+
const prev = normalizeSelection(this.state.selection ?? null);
|
|
4331
|
+
if (prev === next || prev && next && sameSelection(prev, next)) return false;
|
|
4332
|
+
this.recordHistory();
|
|
4333
|
+
this.state.selection = next;
|
|
4334
|
+
this.notify();
|
|
4335
|
+
return true;
|
|
4336
|
+
}
|
|
4337
|
+
/** Select › All. */
|
|
4338
|
+
selectAll() {
|
|
4339
|
+
const { width, height } = this.naturalSize;
|
|
4340
|
+
return this.setSelection(fullSelection(width, height));
|
|
4341
|
+
}
|
|
4342
|
+
/** Select › Deselect. */
|
|
4343
|
+
deselect() {
|
|
4344
|
+
return this.setSelection(null);
|
|
4345
|
+
}
|
|
4346
|
+
/** Select › Inverse (with nothing selected, selects everything). */
|
|
4347
|
+
invertSelection() {
|
|
4348
|
+
const { width, height } = this.naturalSize;
|
|
4349
|
+
const current = this.state.selection ?? createSelectionMask(width, height);
|
|
4350
|
+
return this.setSelection(invertSelection(current));
|
|
4351
|
+
}
|
|
4352
|
+
shapeCommand(mask, options) {
|
|
4353
|
+
const shaped = options.feather && options.feather > 0 ? modifySelection(mask, "feather", options.feather) : mask;
|
|
4354
|
+
return this.setSelection(shaped, options.mode ?? "replace");
|
|
4355
|
+
}
|
|
4356
|
+
/** Rectangular marquee in natural image pixels. */
|
|
4357
|
+
selectRect(rect, options = {}) {
|
|
4358
|
+
const { width, height } = this.naturalSize;
|
|
4359
|
+
return this.shapeCommand(rectSelection(width, height, rect, options), options);
|
|
4360
|
+
}
|
|
4361
|
+
/** Elliptical marquee inscribed in `rect`. */
|
|
4362
|
+
selectEllipse(rect, options = {}) {
|
|
4363
|
+
const { width, height } = this.naturalSize;
|
|
4364
|
+
return this.shapeCommand(ellipseSelection(width, height, rect, options), options);
|
|
4365
|
+
}
|
|
4366
|
+
/** Lasso / polygonal lasso. */
|
|
4367
|
+
selectPolygon(points, options = {}) {
|
|
4368
|
+
const { width, height } = this.naturalSize;
|
|
4369
|
+
return this.shapeCommand(polygonSelection(width, height, points, options), options);
|
|
4370
|
+
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
|
|
4373
|
+
* or in a test; `null` goes back to reading the source image. A new source image clears it.
|
|
4374
|
+
*/
|
|
4375
|
+
setSamplePixels(pixels) {
|
|
4376
|
+
this.samplePixels = pixels;
|
|
4377
|
+
this.samplePixelsOverride = pixels !== null;
|
|
4378
|
+
}
|
|
4379
|
+
/**
|
|
4380
|
+
* The pixels the selection tools sample: the source image at natural size, or with
|
|
4381
|
+
* `sampleMerged` the source after the base adjustments and adjustment layers (no crop,
|
|
4382
|
+
* rotation or annotations). `null` without pixel access (no DOM, tainted canvas).
|
|
4383
|
+
*/
|
|
4384
|
+
async getSamplePixels(options = {}) {
|
|
4385
|
+
const { width, height } = this.naturalSize;
|
|
4386
|
+
let base = options.pixels ?? this.samplePixels;
|
|
4387
|
+
if (!base) {
|
|
4388
|
+
try {
|
|
4389
|
+
const canvas = await this.renderSource();
|
|
4390
|
+
const ctx = canvas.getContext("2d");
|
|
4391
|
+
const data = ctx?.getImageData?.(0, 0, width, height);
|
|
4392
|
+
if (data && data.data && data.data.length >= width * height * 4) {
|
|
4393
|
+
base = { width, height, data: data.data };
|
|
4394
|
+
this.samplePixels = base;
|
|
4395
|
+
this.samplePixelsOverride = false;
|
|
4396
|
+
}
|
|
4397
|
+
} catch {
|
|
4398
|
+
base = null;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
if (!base) return null;
|
|
4402
|
+
if (base.width !== width || base.height !== height) {
|
|
4403
|
+
if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;
|
|
4404
|
+
return null;
|
|
4405
|
+
}
|
|
4406
|
+
if (!options.sampleMerged) return base;
|
|
4407
|
+
const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data) };
|
|
4408
|
+
applyAdjustments(copy, this.state.adjustments);
|
|
4409
|
+
return copy;
|
|
4410
|
+
}
|
|
4411
|
+
async requirePixels(options) {
|
|
4412
|
+
const pixels = await this.getSamplePixels(options);
|
|
4413
|
+
if (!pixels) throw new Error("The image pixels are not available (no canvas access); pass `pixels`.");
|
|
4414
|
+
return pixels;
|
|
4415
|
+
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when
|
|
4418
|
+
* nothing is selected).
|
|
4419
|
+
*/
|
|
4420
|
+
async magicWand(point, options = {}) {
|
|
4421
|
+
const pixels = await this.requirePixels(options);
|
|
4422
|
+
this.shapeCommand(magicWand(pixels, point, options), options);
|
|
4423
|
+
return this.getSelection();
|
|
4424
|
+
}
|
|
4425
|
+
/** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */
|
|
4426
|
+
async selectColorRange(samples, options = {}) {
|
|
4427
|
+
const pixels = await this.requirePixels(options);
|
|
4428
|
+
this.shapeCommand(colorRange(pixels, samples, options), options);
|
|
4429
|
+
return this.getSelection();
|
|
4430
|
+
}
|
|
4431
|
+
/** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */
|
|
4432
|
+
modifySelection(op, radius) {
|
|
4433
|
+
const sel = this.state.selection;
|
|
4434
|
+
if (!sel?.bounds) return false;
|
|
4435
|
+
return this.setSelection(modifySelection(sel, op, radius));
|
|
4436
|
+
}
|
|
4437
|
+
/**
|
|
4438
|
+
* Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)
|
|
4439
|
+
* to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.
|
|
4440
|
+
*/
|
|
4441
|
+
async refineSelection(params, options = {}) {
|
|
4442
|
+
const sel = this.state.selection;
|
|
4443
|
+
if (!sel?.bounds) return false;
|
|
4444
|
+
const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;
|
|
4445
|
+
return this.setSelection(refineSelection(pixels, sel, params));
|
|
4446
|
+
}
|
|
4447
|
+
/**
|
|
4448
|
+
* Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or
|
|
4449
|
+
* RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.
|
|
4450
|
+
*/
|
|
4451
|
+
async loadSelectionFromImage(source, options = {}) {
|
|
4452
|
+
let pixels;
|
|
4453
|
+
if (typeof source === "string") {
|
|
4454
|
+
const img = await this.renderer.imageCache.get(source);
|
|
4455
|
+
const w = img.naturalWidth || img.width;
|
|
4456
|
+
const h = img.naturalHeight || img.height;
|
|
4457
|
+
const canvas = this.renderer.createCanvas(w, h);
|
|
4458
|
+
const ctx = canvas.getContext("2d");
|
|
4459
|
+
if (ctx) ctx.drawImage(img, 0, 0, w, h);
|
|
4460
|
+
const data = ctx?.getImageData?.(0, 0, w, h);
|
|
4461
|
+
if (!data) throw new Error("The mask image pixels are not available (no canvas access); pass pixels.");
|
|
4462
|
+
pixels = { width: w, height: h, data: data.data };
|
|
4463
|
+
} else {
|
|
4464
|
+
pixels = source;
|
|
4465
|
+
}
|
|
4466
|
+
this.setSelection(selectionFromPixels(pixels, options), options.mode ?? "replace");
|
|
4467
|
+
return this.getSelection();
|
|
4468
|
+
}
|
|
4469
|
+
/** Quick Mask view (tints the unselected area). View state: no undo step. */
|
|
4470
|
+
setQuickMask(on) {
|
|
4471
|
+
if (Boolean(this.state.quickMask) === on) return;
|
|
4472
|
+
this.state.quickMask = on;
|
|
1715
4473
|
this.notify();
|
|
1716
4474
|
}
|
|
4475
|
+
// --- Layer masks ---
|
|
4476
|
+
/**
|
|
4477
|
+
* Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo
|
|
4478
|
+
* step. Without a selection, returns false.
|
|
4479
|
+
*/
|
|
4480
|
+
applySelectionAsLayerMask(layerId, props = {}) {
|
|
4481
|
+
const sel = this.state.selection;
|
|
4482
|
+
if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;
|
|
4483
|
+
return this.updateAdjustmentLayer(layerId, { mask: layerMaskFromSelection(sel, props) });
|
|
4484
|
+
}
|
|
4485
|
+
/** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */
|
|
4486
|
+
setAdjustmentLayerMask(layerId, mask, props = {}) {
|
|
4487
|
+
return this.updateAdjustmentLayer(layerId, { mask: mask ? layerMaskFromSelection(mask, props) : null });
|
|
4488
|
+
}
|
|
4489
|
+
/** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */
|
|
4490
|
+
setAdjustmentLayerMaskProps(layerId, props, commitToHistory = true) {
|
|
4491
|
+
const layer = this.getAdjustmentLayer(layerId);
|
|
4492
|
+
if (!layer?.mask) return false;
|
|
4493
|
+
return this.updateAdjustmentLayer(layerId, { mask: updateLayerMaskProps(layer.mask, props) }, commitToHistory);
|
|
4494
|
+
}
|
|
4495
|
+
/** Load Selection from a layer mask (its pixels; density and feather are not applied). */
|
|
4496
|
+
selectionFromLayerMask(layerId, mode = "replace") {
|
|
4497
|
+
const layer = this.getAdjustmentLayer(layerId);
|
|
4498
|
+
const sel = layer?.mask ? decodeLayerMask(layer.mask) : null;
|
|
4499
|
+
if (!sel) return false;
|
|
4500
|
+
return this.setSelection(sel, mode);
|
|
4501
|
+
}
|
|
4502
|
+
// --- Histogram ---
|
|
4503
|
+
/**
|
|
4504
|
+
* Histogram of the last rendered preview (after adjustments and layers, before annotations),
|
|
4505
|
+
* or `null` before the first render or without canvas pixel access.
|
|
4506
|
+
*/
|
|
4507
|
+
getHistogram() {
|
|
4508
|
+
return this.renderer.previewHistogram;
|
|
4509
|
+
}
|
|
4510
|
+
/** Calls `listener` now and whenever a render produces a new preview histogram. */
|
|
4511
|
+
subscribeHistogram(listener) {
|
|
4512
|
+
this.histogramListeners.add(listener);
|
|
4513
|
+
listener(this.renderer.previewHistogram);
|
|
4514
|
+
return () => {
|
|
4515
|
+
this.histogramListeners.delete(listener);
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
1717
4518
|
// --- Depth Masking (Gleamforge) ---
|
|
1718
4519
|
setDepthMask(partial) {
|
|
1719
4520
|
this.recordHistory();
|
|
@@ -1892,6 +4693,12 @@ var ImageEditorController = class {
|
|
|
1892
4693
|
this.imageElement = next.element;
|
|
1893
4694
|
this.state.sourceUrl = next.sourceUrl;
|
|
1894
4695
|
this.state.imageDimensions = next.dimensions;
|
|
4696
|
+
this.samplePixels = null;
|
|
4697
|
+
if (this.state.selection && (sx !== 1 || sy !== 1)) {
|
|
4698
|
+
this.state.selection = normalizeSelection(
|
|
4699
|
+
resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
1895
4702
|
if (sx !== 1 || sy !== 1) {
|
|
1896
4703
|
if (this.state.crop) {
|
|
1897
4704
|
this.state.crop = clampCropToImage(
|
|
@@ -1914,17 +4721,31 @@ var ImageEditorController = class {
|
|
|
1914
4721
|
this.notify();
|
|
1915
4722
|
}
|
|
1916
4723
|
// --- Serialization ---
|
|
4724
|
+
/** The serializable state; `selection` is included (run-length encoded) only when something is selected. */
|
|
1917
4725
|
toJSON() {
|
|
1918
|
-
|
|
4726
|
+
const out = this.serializeState();
|
|
4727
|
+
if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);
|
|
4728
|
+
return out;
|
|
1919
4729
|
}
|
|
1920
4730
|
loadJSON(data) {
|
|
1921
4731
|
this.recordHistory();
|
|
1922
|
-
this.applySerializedState(data);
|
|
4732
|
+
this.applySerializedState(migrateSerializedImageEditorState(data));
|
|
4733
|
+
let selection = null;
|
|
4734
|
+
if (data.selection) {
|
|
4735
|
+
try {
|
|
4736
|
+
selection = normalizeSelection(deserializeSelection(data.selection));
|
|
4737
|
+
} catch {
|
|
4738
|
+
selection = null;
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
this.state.selection = selection;
|
|
4742
|
+
this.dropMismatchedSelection();
|
|
1923
4743
|
this.notify();
|
|
1924
4744
|
}
|
|
1925
4745
|
serializeState() {
|
|
1926
4746
|
return {
|
|
1927
|
-
|
|
4747
|
+
// States without adjustment layers keep schema 1, so they round-trip unchanged.
|
|
4748
|
+
version: this.state.adjustments.layers?.length ? IMAGE_EDITOR_SCHEMA_VERSION : 1,
|
|
1928
4749
|
crop: this.state.crop ? { ...this.state.crop } : null,
|
|
1929
4750
|
transform: { ...this.state.transform },
|
|
1930
4751
|
adjustments: serializeAdjustments(this.state.adjustments),
|
|
@@ -1948,12 +4769,24 @@ var ImageEditorController = class {
|
|
|
1948
4769
|
};
|
|
1949
4770
|
function serializeAdjustments(adjustments) {
|
|
1950
4771
|
const out = { ...adjustments };
|
|
4772
|
+
if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));
|
|
4773
|
+
else delete out.layers;
|
|
1951
4774
|
if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {
|
|
1952
4775
|
delete out.process;
|
|
1953
4776
|
if (!out.tint) delete out.tint;
|
|
1954
4777
|
}
|
|
1955
4778
|
return out;
|
|
1956
4779
|
}
|
|
4780
|
+
function normalizeSelection(mask) {
|
|
4781
|
+
return mask?.bounds ? mask : null;
|
|
4782
|
+
}
|
|
4783
|
+
function sameSelection(a, b) {
|
|
4784
|
+
if (a.width !== b.width || a.height !== b.height) return false;
|
|
4785
|
+
const x = a.data;
|
|
4786
|
+
const y = b.data;
|
|
4787
|
+
for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;
|
|
4788
|
+
return true;
|
|
4789
|
+
}
|
|
1957
4790
|
function scaleAnnotation(ann, sx, sy) {
|
|
1958
4791
|
const k = Math.sqrt(Math.abs(sx * sy));
|
|
1959
4792
|
const base = { ...ann, x: ann.x * sx, y: ann.y * sy };
|
|
@@ -1973,13 +4806,955 @@ function scaleAnnotation(ann, sx, sy) {
|
|
|
1973
4806
|
return { ...base, fontSize: base.fontSize * k };
|
|
1974
4807
|
}
|
|
1975
4808
|
}
|
|
4809
|
+
|
|
4810
|
+
// src/image-editor/document.ts
|
|
4811
|
+
var IMAGE_EDITOR_STATE_VERSION = 1;
|
|
4812
|
+
var imageEditorStateMigrations = {};
|
|
4813
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4814
|
+
var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
4815
|
+
function expectObject(value, what) {
|
|
4816
|
+
if (!isObject(value)) throw new Error(`${what} must be an object`);
|
|
4817
|
+
return value;
|
|
4818
|
+
}
|
|
4819
|
+
function checkFields(value, types, what) {
|
|
4820
|
+
for (const [field, ok] of Object.entries(types)) {
|
|
4821
|
+
if (field in value && value[field] !== void 0 && !ok(value[field])) {
|
|
4822
|
+
throw new Error(`${what}.${field} has an invalid value`);
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
var isBoolean = (v) => typeof v === "boolean";
|
|
4827
|
+
function parseCrop(value) {
|
|
4828
|
+
if (value === null) return null;
|
|
4829
|
+
const crop = expectObject(value, "crop");
|
|
4830
|
+
for (const field of ["x", "y", "width", "height"]) {
|
|
4831
|
+
if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);
|
|
4832
|
+
}
|
|
4833
|
+
return { ...crop };
|
|
4834
|
+
}
|
|
4835
|
+
function parseTransform(value) {
|
|
4836
|
+
const transform = expectObject(value, "transform");
|
|
4837
|
+
checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, "transform");
|
|
4838
|
+
return { ...DEFAULT_TRANSFORM, ...transform };
|
|
4839
|
+
}
|
|
4840
|
+
var ADJUSTMENT_NUMBERS = [
|
|
4841
|
+
"brightness",
|
|
4842
|
+
"contrast",
|
|
4843
|
+
"saturation",
|
|
4844
|
+
"exposure",
|
|
4845
|
+
"temperature",
|
|
4846
|
+
"tint",
|
|
4847
|
+
"blur",
|
|
4848
|
+
"opacity"
|
|
4849
|
+
];
|
|
4850
|
+
function parseAdjustments(value) {
|
|
4851
|
+
const adjustments = expectObject(value, "adjustments");
|
|
4852
|
+
checkFields(
|
|
4853
|
+
adjustments,
|
|
4854
|
+
{
|
|
4855
|
+
...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),
|
|
4856
|
+
process: (v) => v === 1 || v === 2
|
|
4857
|
+
},
|
|
4858
|
+
"adjustments"
|
|
4859
|
+
);
|
|
4860
|
+
return { ...adjustments };
|
|
4861
|
+
}
|
|
4862
|
+
function parseDepthMask(value) {
|
|
4863
|
+
const mask = expectObject(value, "depthMask");
|
|
4864
|
+
checkFields(
|
|
4865
|
+
mask,
|
|
4866
|
+
{
|
|
4867
|
+
enabled: isBoolean,
|
|
4868
|
+
maskSource: (v) => v === null || typeof v === "string",
|
|
4869
|
+
mode: (v) => v === "threshold" || v === "brightness",
|
|
4870
|
+
depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),
|
|
4871
|
+
softness: isNumber,
|
|
4872
|
+
invert: isBoolean
|
|
4873
|
+
},
|
|
4874
|
+
"depthMask"
|
|
4875
|
+
);
|
|
4876
|
+
const merged = { ...DEFAULT_DEPTH_MASK, ...mask };
|
|
4877
|
+
return { ...merged, depthRange: [...merged.depthRange] };
|
|
4878
|
+
}
|
|
4879
|
+
var ANNOTATION_TYPES = /* @__PURE__ */ new Set(["pen", "rect", "circle", "arrow", "line", "text", "image"]);
|
|
4880
|
+
var isAnnotation = (value) => isObject(value) && typeof value.id === "string" && typeof value.type === "string" && ANNOTATION_TYPES.has(value.type) && isNumber(value.x) && isNumber(value.y);
|
|
4881
|
+
function loadImageEditorDocument(input) {
|
|
4882
|
+
let dropped = 0;
|
|
4883
|
+
const result = loadDocument(
|
|
4884
|
+
input,
|
|
4885
|
+
{
|
|
4886
|
+
crop: { parse: parseCrop, defaults: null },
|
|
4887
|
+
transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },
|
|
4888
|
+
adjustments: {
|
|
4889
|
+
parse: parseAdjustments,
|
|
4890
|
+
// Same as what `loadJSON` derives from missing adjustments.
|
|
4891
|
+
defaults: () => normalizeStoredAdjustments(void 0)
|
|
4892
|
+
},
|
|
4893
|
+
depthMask: {
|
|
4894
|
+
parse: parseDepthMask,
|
|
4895
|
+
defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] })
|
|
4896
|
+
},
|
|
4897
|
+
annotations: {
|
|
4898
|
+
parse: (value) => {
|
|
4899
|
+
if (!Array.isArray(value)) throw new Error("annotations must be an array");
|
|
4900
|
+
const kept = value.filter(isAnnotation);
|
|
4901
|
+
dropped = value.length - kept.length;
|
|
4902
|
+
return JSON.parse(JSON.stringify(kept));
|
|
4903
|
+
},
|
|
4904
|
+
defaults: () => []
|
|
4905
|
+
},
|
|
4906
|
+
// Optional: only present when something was selected when saving.
|
|
4907
|
+
selection: {
|
|
4908
|
+
parse: (value) => {
|
|
4909
|
+
const mask = deserializeSelection(value);
|
|
4910
|
+
const v = value;
|
|
4911
|
+
return { width: mask.width, height: mask.height, rle: v.rle };
|
|
4912
|
+
},
|
|
4913
|
+
defaults: void 0
|
|
4914
|
+
}
|
|
4915
|
+
},
|
|
4916
|
+
{
|
|
4917
|
+
currentVersion: IMAGE_EDITOR_STATE_VERSION,
|
|
4918
|
+
steps: imageEditorStateMigrations
|
|
4919
|
+
}
|
|
4920
|
+
);
|
|
4921
|
+
if (dropped > 0) {
|
|
4922
|
+
result.report.problems.push({
|
|
4923
|
+
section: "annotations",
|
|
4924
|
+
message: `${dropped} invalid annotation${dropped === 1 ? "" : "s"} dropped`
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
const state = { version: 1, ...result.value };
|
|
4928
|
+
if (state.selection === void 0) delete state.selection;
|
|
4929
|
+
return { state, report: result.report, unknown: result.unknown };
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
// src/image-editor/controls.ts
|
|
4933
|
+
var plain = { kind: "plain" };
|
|
4934
|
+
var centered = { kind: "centered" };
|
|
4935
|
+
var IMAGE_ADJUSTMENT_CONTROLS = [
|
|
4936
|
+
{ id: "brightness", label: "Brightness", section: "light", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },
|
|
4937
|
+
{ id: "contrast", label: "Contrast", section: "light", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },
|
|
4938
|
+
{
|
|
4939
|
+
id: "saturation",
|
|
4940
|
+
label: "Saturation",
|
|
4941
|
+
section: "color",
|
|
4942
|
+
min: -100,
|
|
4943
|
+
max: 100,
|
|
4944
|
+
step: 1,
|
|
4945
|
+
default: 0,
|
|
4946
|
+
decimals: 0,
|
|
4947
|
+
track: { kind: "gradient", from: "#7a7a7a", to: "#e04a3a" }
|
|
4948
|
+
},
|
|
4949
|
+
{ id: "exposure", label: "Exposure", section: "light", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },
|
|
4950
|
+
{ id: "temperature", label: "Warmth / Temperature", section: "color", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "temp" } },
|
|
4951
|
+
{ id: "tint", label: "Tint", section: "color", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "tint" }, requiresProcess: 2 },
|
|
4952
|
+
{ id: "blur", label: "Blur", section: "effects", min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: "px" },
|
|
4953
|
+
{ id: "opacity", label: "Opacity", section: "effects", min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: "%" }
|
|
4954
|
+
];
|
|
4955
|
+
var tone = (prefix, label) => [
|
|
4956
|
+
["0", "Cyan \u2013 Red", "#00b7c7", "#e0343a"],
|
|
4957
|
+
["1", "Magenta \u2013 Green", "#d23ad2", "#2fb34a"],
|
|
4958
|
+
["2", "Yellow \u2013 Blue", "#e0c63a", "#3a5be0"]
|
|
4959
|
+
].map(([i, l, from, to]) => ({
|
|
4960
|
+
id: `${prefix}.${i}`,
|
|
4961
|
+
label: `${label} ${l}`,
|
|
4962
|
+
section: "layer",
|
|
4963
|
+
min: -100,
|
|
4964
|
+
max: 100,
|
|
4965
|
+
step: 1,
|
|
4966
|
+
default: 0,
|
|
4967
|
+
decimals: 0,
|
|
4968
|
+
track: { kind: "gradient", from, to }
|
|
4969
|
+
}));
|
|
4970
|
+
var levelsRecord = (ch, label) => [
|
|
4971
|
+
{ id: `${ch}.inBlack`, label: `${label} input black`, section: "layer", min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },
|
|
4972
|
+
{ id: `${ch}.gamma`, label: `${label} gamma`, section: "layer", min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },
|
|
4973
|
+
{ id: `${ch}.inWhite`, label: `${label} input white`, section: "layer", min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },
|
|
4974
|
+
{ id: `${ch}.outBlack`, label: `${label} output black`, section: "layer", min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },
|
|
4975
|
+
{ id: `${ch}.outWhite`, label: `${label} output white`, section: "layer", min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain }
|
|
4976
|
+
];
|
|
4977
|
+
var RANGE_NAMES = ["Reds", "Yellows", "Greens", "Cyans", "Blues", "Magentas"];
|
|
4978
|
+
var ADJUSTMENT_LAYER_CONTROLS = {
|
|
4979
|
+
curves: [],
|
|
4980
|
+
levels: [
|
|
4981
|
+
...levelsRecord("master", "RGB"),
|
|
4982
|
+
...levelsRecord("red", "Red"),
|
|
4983
|
+
...levelsRecord("green", "Green"),
|
|
4984
|
+
...levelsRecord("blue", "Blue")
|
|
4985
|
+
],
|
|
4986
|
+
hueSaturation: [
|
|
4987
|
+
{ id: "hue", label: "Hue", section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: "rainbow" }, unit: "\xB0" },
|
|
4988
|
+
{ id: "saturation", label: "Saturation", section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "gradient", from: "#7a7a7a", to: "#e04a3a" } },
|
|
4989
|
+
{ id: "lightness", label: "Lightness", section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "gradient", from: "#000000", to: "#ffffff" } },
|
|
4990
|
+
...RANGE_NAMES.flatMap((name, i) => [
|
|
4991
|
+
{ id: `ranges.${i}.hue`, label: `${name} hue`, section: "layer", min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: "rainbow" }, unit: "\xB0" },
|
|
4992
|
+
{ id: `ranges.${i}.saturation`, label: `${name} saturation`, section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },
|
|
4993
|
+
{ id: `ranges.${i}.lightness`, label: `${name} lightness`, section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered }
|
|
4994
|
+
])
|
|
4995
|
+
],
|
|
4996
|
+
vibrance: [
|
|
4997
|
+
{ id: "vibrance", label: "Vibrance", section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "gradient", from: "#7a7a7a", to: "#d8406a" } },
|
|
4998
|
+
{ id: "saturation", label: "Saturation", section: "layer", min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: "gradient", from: "#7a7a7a", to: "#e04a3a" } }
|
|
4999
|
+
],
|
|
5000
|
+
colorBalance: [...tone("shadows", "Shadows"), ...tone("midtones", "Midtones"), ...tone("highlights", "Highlights")],
|
|
5001
|
+
blackWhite: [
|
|
5002
|
+
["reds", "Reds", 40, "#e0343a"],
|
|
5003
|
+
["yellows", "Yellows", 60, "#e0c63a"],
|
|
5004
|
+
["greens", "Greens", 40, "#2fb34a"],
|
|
5005
|
+
["cyans", "Cyans", 60, "#00b7c7"],
|
|
5006
|
+
["blues", "Blues", 20, "#3a5be0"],
|
|
5007
|
+
["magentas", "Magentas", 80, "#d23ad2"]
|
|
5008
|
+
].map(([id, label, def, to]) => ({
|
|
5009
|
+
id,
|
|
5010
|
+
label,
|
|
5011
|
+
section: "layer",
|
|
5012
|
+
min: -200,
|
|
5013
|
+
max: 300,
|
|
5014
|
+
step: 1,
|
|
5015
|
+
default: def,
|
|
5016
|
+
decimals: 0,
|
|
5017
|
+
track: { kind: "gradient", from: "#000000", to },
|
|
5018
|
+
unit: "%"
|
|
5019
|
+
})),
|
|
5020
|
+
invert: [],
|
|
5021
|
+
posterize: [{ id: "levels", label: "Levels", section: "layer", min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],
|
|
5022
|
+
threshold: [{ id: "level", label: "Threshold level", section: "layer", min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: "gradient", from: "#000000", to: "#ffffff" } }],
|
|
5023
|
+
photoFilter: [{ id: "density", label: "Density", section: "layer", min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: "%" }]
|
|
5024
|
+
};
|
|
5025
|
+
var ADJUSTMENT_LAYER_OPACITY_CONTROL = {
|
|
5026
|
+
id: "opacity",
|
|
5027
|
+
label: "Opacity",
|
|
5028
|
+
section: "layer",
|
|
5029
|
+
min: 0,
|
|
5030
|
+
max: 1,
|
|
5031
|
+
step: 0.01,
|
|
5032
|
+
default: 1,
|
|
5033
|
+
decimals: 0,
|
|
5034
|
+
track: plain,
|
|
5035
|
+
displayScale: 100,
|
|
5036
|
+
unit: "%"
|
|
5037
|
+
};
|
|
5038
|
+
var BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));
|
|
5039
|
+
function findAdjustmentControl(id) {
|
|
5040
|
+
return BASE_BY_ID.get(id);
|
|
5041
|
+
}
|
|
5042
|
+
function findLayerControl(kind, id) {
|
|
5043
|
+
if (id === "opacity") return ADJUSTMENT_LAYER_OPACITY_CONTROL;
|
|
5044
|
+
return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);
|
|
5045
|
+
}
|
|
5046
|
+
function clampControlValue(spec, v) {
|
|
5047
|
+
if (typeof v !== "number" || !Number.isFinite(v)) return spec.default;
|
|
5048
|
+
const clamped = Math.min(spec.max, Math.max(spec.min, v));
|
|
5049
|
+
const steps = Math.round((clamped - spec.min) / spec.step);
|
|
5050
|
+
const snapped = spec.min + steps * spec.step;
|
|
5051
|
+
return Math.min(spec.max, Number(snapped.toFixed(6)));
|
|
5052
|
+
}
|
|
5053
|
+
function formatControlValue(spec, v) {
|
|
5054
|
+
const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);
|
|
5055
|
+
const s = shown.toFixed(spec.decimals);
|
|
5056
|
+
return `${s}${spec.unit ?? ""}`;
|
|
5057
|
+
}
|
|
5058
|
+
function controlPosition(spec, v) {
|
|
5059
|
+
const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);
|
|
5060
|
+
return Math.min(1, Math.max(0, p));
|
|
5061
|
+
}
|
|
5062
|
+
function controlTrackBackground(spec) {
|
|
5063
|
+
const t = spec.track;
|
|
5064
|
+
switch (t.kind) {
|
|
5065
|
+
case "temp":
|
|
5066
|
+
return "linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)";
|
|
5067
|
+
case "tint":
|
|
5068
|
+
return "linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)";
|
|
5069
|
+
case "gradient":
|
|
5070
|
+
return `linear-gradient(to right, ${t.from}, ${t.to})`;
|
|
5071
|
+
case "rainbow":
|
|
5072
|
+
return "linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)";
|
|
5073
|
+
default:
|
|
5074
|
+
return void 0;
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
function getControlPath(obj, path) {
|
|
5078
|
+
let cur = obj;
|
|
5079
|
+
for (const part of path.split(".")) {
|
|
5080
|
+
if (cur === null || cur === void 0) return void 0;
|
|
5081
|
+
cur = cur[part];
|
|
5082
|
+
}
|
|
5083
|
+
return cur;
|
|
5084
|
+
}
|
|
5085
|
+
function setControlPath(obj, path, value) {
|
|
5086
|
+
const parts = path.split(".");
|
|
5087
|
+
const root = Array.isArray(obj) ? [...obj] : { ...obj };
|
|
5088
|
+
let cur = root;
|
|
5089
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
5090
|
+
const next = cur[parts[i]];
|
|
5091
|
+
cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === "object" ? { ...next } : {};
|
|
5092
|
+
cur = cur[parts[i]];
|
|
5093
|
+
}
|
|
5094
|
+
cur[parts[parts.length - 1]] = value;
|
|
5095
|
+
return root;
|
|
5096
|
+
}
|
|
5097
|
+
function readAdjustmentControls(adjustments) {
|
|
5098
|
+
const out = {};
|
|
5099
|
+
for (const c of IMAGE_ADJUSTMENT_CONTROLS) {
|
|
5100
|
+
const v = adjustments[c.id];
|
|
5101
|
+
out[c.id] = typeof v === "number" && Number.isFinite(v) ? v : c.default;
|
|
5102
|
+
}
|
|
5103
|
+
return out;
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
// src/image-editor/selection/tool-options.ts
|
|
5107
|
+
var SELECTION_TOOLS = [
|
|
5108
|
+
{ id: "wand", label: "Magic Wand" },
|
|
5109
|
+
{ id: "rect", label: "Rectangle" },
|
|
5110
|
+
{ id: "ellipse", label: "Ellipse" },
|
|
5111
|
+
{ id: "lasso", label: "Lasso" }
|
|
5112
|
+
];
|
|
5113
|
+
var SELECTION_MODE_LABELS = {
|
|
5114
|
+
replace: "New",
|
|
5115
|
+
add: "Add",
|
|
5116
|
+
subtract: "Subtract",
|
|
5117
|
+
intersect: "Intersect"
|
|
5118
|
+
};
|
|
5119
|
+
var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
5120
|
+
tool: "wand",
|
|
5121
|
+
mode: "replace",
|
|
5122
|
+
tolerance: 32,
|
|
5123
|
+
contiguous: true,
|
|
5124
|
+
antiAlias: true,
|
|
5125
|
+
sampleMerged: false,
|
|
5126
|
+
distance: "rgb"
|
|
5127
|
+
};
|
|
5128
|
+
function selectionModeFromModifiers(modifiers, fallback) {
|
|
5129
|
+
if (modifiers.shiftKey && modifiers.altKey) return "intersect";
|
|
5130
|
+
if (modifiers.shiftKey) return "add";
|
|
5131
|
+
if (modifiers.altKey) return "subtract";
|
|
5132
|
+
return fallback;
|
|
5133
|
+
}
|
|
5134
|
+
var REFINE_EDGE_CONTROLS = [
|
|
5135
|
+
{ id: "radius", label: "Radius", min: 0, max: 64, step: 1, unit: "px" },
|
|
5136
|
+
{ id: "smooth", label: "Smooth", min: 0, max: 100, step: 1, unit: "" },
|
|
5137
|
+
{ id: "feather", label: "Feather", min: 0, max: 50, step: 0.5, unit: "px" },
|
|
5138
|
+
{ id: "contrast", label: "Contrast", min: 0, max: 100, step: 1, unit: "%" },
|
|
5139
|
+
{ id: "shiftEdge", label: "Shift Edge", min: -100, max: 100, step: 1, unit: "%" }
|
|
5140
|
+
];
|
|
5141
|
+
function describeSelection(mask) {
|
|
5142
|
+
if (!mask || !mask.bounds) return "No selection";
|
|
5143
|
+
let sum = 0;
|
|
5144
|
+
for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];
|
|
5145
|
+
const pct = sum / 255 / mask.data.length * 100;
|
|
5146
|
+
const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;
|
|
5147
|
+
return `${mask.bounds.width} \xD7 ${mask.bounds.height} px selected (${shown}% of the image)`;
|
|
5148
|
+
}
|
|
5149
|
+
|
|
5150
|
+
// src/image-editor/selection-tools.ts
|
|
5151
|
+
var modeSchema = {
|
|
5152
|
+
type: "string",
|
|
5153
|
+
enum: [...SELECTION_MODES],
|
|
5154
|
+
description: "How the result combines with the current selection (default replace)."
|
|
5155
|
+
};
|
|
5156
|
+
var num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
5157
|
+
function summary(controller) {
|
|
5158
|
+
const sel = controller.getSelection();
|
|
5159
|
+
const { width, height } = controller.getState().imageDimensions;
|
|
5160
|
+
const area = selectionArea(sel);
|
|
5161
|
+
return {
|
|
5162
|
+
empty: !sel,
|
|
5163
|
+
bounds: sel?.bounds ?? null,
|
|
5164
|
+
/** Σ coverage in pixels. */
|
|
5165
|
+
area: Math.round(area),
|
|
5166
|
+
percent: Math.round(area / Math.max(1, width * height) * 1e4) / 100,
|
|
5167
|
+
description: describeSelection(sel)
|
|
5168
|
+
};
|
|
5169
|
+
}
|
|
5170
|
+
var writeAnnotations = (title, idempotent = false) => ({
|
|
5171
|
+
title,
|
|
5172
|
+
readOnlyHint: false,
|
|
5173
|
+
destructiveHint: false,
|
|
5174
|
+
idempotentHint: idempotent,
|
|
5175
|
+
openWorldHint: false
|
|
5176
|
+
});
|
|
5177
|
+
function createImageSelectionTools(controller) {
|
|
5178
|
+
const getSelection = {
|
|
5179
|
+
name: "get_selection",
|
|
5180
|
+
title: "Read the selection",
|
|
5181
|
+
description: "Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.",
|
|
5182
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5183
|
+
annotations: { title: "Read the selection", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
5184
|
+
handler: () => summary(controller)
|
|
5185
|
+
};
|
|
5186
|
+
const selectRegion = {
|
|
5187
|
+
name: "select_region",
|
|
5188
|
+
title: "Select a region",
|
|
5189
|
+
description: 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything ("all"), nothing ("none") or the inverse of the current selection ("invert"). One undoable step.',
|
|
5190
|
+
inputSchema: {
|
|
5191
|
+
type: "object",
|
|
5192
|
+
properties: {
|
|
5193
|
+
shape: { type: "string", enum: ["rect", "ellipse", "all", "none", "invert"] },
|
|
5194
|
+
x: { type: "number" },
|
|
5195
|
+
y: { type: "number" },
|
|
5196
|
+
width: { type: "number" },
|
|
5197
|
+
height: { type: "number" },
|
|
5198
|
+
mode: modeSchema,
|
|
5199
|
+
feather: { type: "number", minimum: 0, description: "Feather radius in pixels (default 0)." }
|
|
5200
|
+
},
|
|
5201
|
+
required: ["shape"],
|
|
5202
|
+
additionalProperties: false
|
|
5203
|
+
},
|
|
5204
|
+
annotations: writeAnnotations("Select a region", true),
|
|
5205
|
+
handler: ({ shape, x, y, width, height, mode, feather }) => {
|
|
5206
|
+
const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };
|
|
5207
|
+
switch (shape) {
|
|
5208
|
+
case "all":
|
|
5209
|
+
controller.selectAll();
|
|
5210
|
+
break;
|
|
5211
|
+
case "none":
|
|
5212
|
+
controller.deselect();
|
|
5213
|
+
break;
|
|
5214
|
+
case "invert":
|
|
5215
|
+
controller.invertSelection();
|
|
5216
|
+
break;
|
|
5217
|
+
case "rect":
|
|
5218
|
+
case "ellipse": {
|
|
5219
|
+
const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };
|
|
5220
|
+
if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {
|
|
5221
|
+
throw new Error("rect and ellipse need numeric x, y, width and height.");
|
|
5222
|
+
}
|
|
5223
|
+
if (shape === "rect") controller.selectRect(rect, opts);
|
|
5224
|
+
else controller.selectEllipse(rect, opts);
|
|
5225
|
+
break;
|
|
5226
|
+
}
|
|
5227
|
+
default:
|
|
5228
|
+
throw new Error(`Unknown shape "${String(shape)}". Use rect, ellipse, all, none or invert.`);
|
|
5229
|
+
}
|
|
5230
|
+
return summary(controller);
|
|
5231
|
+
}
|
|
5232
|
+
};
|
|
5233
|
+
const selectByColor = {
|
|
5234
|
+
name: "select_by_color",
|
|
5235
|
+
title: "Select by colour",
|
|
5236
|
+
description: 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with "color" ("#rrggbb") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',
|
|
5237
|
+
inputSchema: {
|
|
5238
|
+
type: "object",
|
|
5239
|
+
properties: {
|
|
5240
|
+
x: { type: "number" },
|
|
5241
|
+
y: { type: "number" },
|
|
5242
|
+
color: { type: "string", description: "#rrggbb: select this colour everywhere instead of a seed point." },
|
|
5243
|
+
tolerance: { type: "number", minimum: 0, maximum: 255, description: "Magic wand tolerance (default 32)." },
|
|
5244
|
+
contiguous: { type: "boolean", description: "Only the connected region around (x, y) (default true)." },
|
|
5245
|
+
fuzziness: { type: "number", minimum: 0, maximum: 200, description: "Color Range fuzziness (default 40)." },
|
|
5246
|
+
sampleMerged: { type: "boolean", description: "Sample the adjusted image rather than the source." },
|
|
5247
|
+
mode: modeSchema
|
|
5248
|
+
},
|
|
5249
|
+
additionalProperties: false
|
|
5250
|
+
},
|
|
5251
|
+
annotations: writeAnnotations("Select by colour", true),
|
|
5252
|
+
handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, mode }) => {
|
|
5253
|
+
const common = { mode: parseSelectionMode(mode), sampleMerged: sampleMerged === true };
|
|
5254
|
+
if (typeof color === "string") {
|
|
5255
|
+
await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });
|
|
5256
|
+
} else if (typeof x === "number" && typeof y === "number") {
|
|
5257
|
+
await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });
|
|
5258
|
+
} else {
|
|
5259
|
+
throw new Error("Pass x and y (magic wand) or color (#rrggbb).");
|
|
5260
|
+
}
|
|
5261
|
+
return summary(controller);
|
|
5262
|
+
}
|
|
5263
|
+
};
|
|
5264
|
+
const modify = {
|
|
5265
|
+
name: "modify_selection",
|
|
5266
|
+
title: "Modify the selection",
|
|
5267
|
+
description: "Select \u203A Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.",
|
|
5268
|
+
inputSchema: {
|
|
5269
|
+
type: "object",
|
|
5270
|
+
properties: {
|
|
5271
|
+
op: { type: "string", enum: [...SELECTION_MODIFY_OPS] },
|
|
5272
|
+
radius: { type: "number", minimum: 0, maximum: 500 }
|
|
5273
|
+
},
|
|
5274
|
+
required: ["op", "radius"],
|
|
5275
|
+
additionalProperties: false
|
|
5276
|
+
},
|
|
5277
|
+
annotations: writeAnnotations("Modify the selection"),
|
|
5278
|
+
handler: ({ op, radius }) => {
|
|
5279
|
+
if (!SELECTION_MODIFY_OPS.includes(op)) throw new Error(`Unknown op "${String(op)}".`);
|
|
5280
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected.");
|
|
5281
|
+
controller.modifySelection(op, Math.min(500, Math.max(0, num(radius, 0))));
|
|
5282
|
+
return summary(controller);
|
|
5283
|
+
}
|
|
5284
|
+
};
|
|
5285
|
+
const refine = {
|
|
5286
|
+
name: "refine_selection",
|
|
5287
|
+
title: "Refine the selection edge",
|
|
5288
|
+
description: "Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.",
|
|
5289
|
+
inputSchema: {
|
|
5290
|
+
type: "object",
|
|
5291
|
+
properties: {
|
|
5292
|
+
radius: { type: "number", minimum: 0, maximum: 64 },
|
|
5293
|
+
smartRadius: { type: "boolean" },
|
|
5294
|
+
smooth: { type: "number", minimum: 0, maximum: 100 },
|
|
5295
|
+
feather: { type: "number", minimum: 0, maximum: 250 },
|
|
5296
|
+
contrast: { type: "number", minimum: 0, maximum: 100 },
|
|
5297
|
+
shiftEdge: { type: "number", minimum: -100, maximum: 100 }
|
|
5298
|
+
},
|
|
5299
|
+
additionalProperties: false
|
|
5300
|
+
},
|
|
5301
|
+
annotations: writeAnnotations("Refine the selection edge"),
|
|
5302
|
+
handler: async (params) => {
|
|
5303
|
+
if (!controller.getSelection()) throw new Error("Nothing is selected.");
|
|
5304
|
+
await controller.refineSelection({
|
|
5305
|
+
radius: num(params.radius, 8),
|
|
5306
|
+
smartRadius: params.smartRadius === true,
|
|
5307
|
+
smooth: num(params.smooth, 0),
|
|
5308
|
+
feather: num(params.feather, 0),
|
|
5309
|
+
contrast: num(params.contrast, 0),
|
|
5310
|
+
shiftEdge: num(params.shiftEdge, 0)
|
|
5311
|
+
});
|
|
5312
|
+
return summary(controller);
|
|
5313
|
+
}
|
|
5314
|
+
};
|
|
5315
|
+
const applyMask = {
|
|
5316
|
+
name: "apply_selection_as_mask",
|
|
5317
|
+
title: "Mask a layer with the selection",
|
|
5318
|
+
description: "Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.",
|
|
5319
|
+
inputSchema: {
|
|
5320
|
+
type: "object",
|
|
5321
|
+
properties: {
|
|
5322
|
+
layerId: { type: "string" },
|
|
5323
|
+
kind: { type: "string", enum: ADJUSTMENT_KINDS.map((k) => k.kind) },
|
|
5324
|
+
params: { type: "object" },
|
|
5325
|
+
density: { type: "number", minimum: 0, maximum: 1 },
|
|
5326
|
+
feather: { type: "number", minimum: 0, maximum: 1e3 }
|
|
5327
|
+
},
|
|
5328
|
+
additionalProperties: false
|
|
5329
|
+
},
|
|
5330
|
+
annotations: writeAnnotations("Mask a layer with the selection"),
|
|
5331
|
+
handler: ({ layerId, kind, params, density, feather }) => {
|
|
5332
|
+
const sel = controller.getSelection();
|
|
5333
|
+
if (!sel) throw new Error("Nothing is selected.");
|
|
5334
|
+
const props = {
|
|
5335
|
+
...typeof density === "number" ? { density } : {},
|
|
5336
|
+
...typeof feather === "number" ? { feather } : {}
|
|
5337
|
+
};
|
|
5338
|
+
let id;
|
|
5339
|
+
if (typeof layerId === "string") {
|
|
5340
|
+
if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer "${layerId}".`);
|
|
5341
|
+
controller.applySelectionAsLayerMask(layerId, props);
|
|
5342
|
+
id = layerId;
|
|
5343
|
+
} else if (isAdjustmentKind(kind)) {
|
|
5344
|
+
id = controller.addAdjustmentLayer({
|
|
5345
|
+
kind,
|
|
5346
|
+
params,
|
|
5347
|
+
mask: layerMaskFromSelection(sel, props)
|
|
5348
|
+
});
|
|
5349
|
+
} else {
|
|
5350
|
+
throw new Error("Pass layerId (an existing layer) or kind (a new adjustment layer).");
|
|
5351
|
+
}
|
|
5352
|
+
const layer = controller.getAdjustmentLayer(id);
|
|
5353
|
+
const { rle: _rle, ...mask } = layer.mask;
|
|
5354
|
+
return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };
|
|
5355
|
+
}
|
|
5356
|
+
};
|
|
5357
|
+
return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
// src/image-editor/tools.ts
|
|
5361
|
+
var LAYER_PATH = /^layers\.([^.]+)\.(.+)$/;
|
|
5362
|
+
function adjustmentsSummary(adjustments) {
|
|
5363
|
+
return {
|
|
5364
|
+
process: normalizeProcessVersion(adjustments.process),
|
|
5365
|
+
values: readAdjustmentControls(adjustments),
|
|
5366
|
+
layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l)))
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5369
|
+
function createImageEditorToolProvider(controller, options = {}) {
|
|
5370
|
+
const getAdjustments = {
|
|
5371
|
+
name: "get_adjustments",
|
|
5372
|
+
title: "Read image adjustments",
|
|
5373
|
+
description: "Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).",
|
|
5374
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5375
|
+
annotations: {
|
|
5376
|
+
title: "Read image adjustments",
|
|
5377
|
+
readOnlyHint: true,
|
|
5378
|
+
destructiveHint: false,
|
|
5379
|
+
idempotentHint: true,
|
|
5380
|
+
openWorldHint: false
|
|
5381
|
+
},
|
|
5382
|
+
handler: () => adjustmentsSummary(controller.getState().adjustments)
|
|
5383
|
+
};
|
|
5384
|
+
const listControls = {
|
|
5385
|
+
name: "list_controls",
|
|
5386
|
+
title: "List adjustment controls",
|
|
5387
|
+
description: "Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.",
|
|
5388
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5389
|
+
annotations: {
|
|
5390
|
+
title: "List adjustment controls",
|
|
5391
|
+
readOnlyHint: true,
|
|
5392
|
+
destructiveHint: false,
|
|
5393
|
+
idempotentHint: true,
|
|
5394
|
+
openWorldHint: false
|
|
5395
|
+
},
|
|
5396
|
+
handler: () => ({
|
|
5397
|
+
controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({
|
|
5398
|
+
id,
|
|
5399
|
+
label,
|
|
5400
|
+
section,
|
|
5401
|
+
min,
|
|
5402
|
+
max,
|
|
5403
|
+
step,
|
|
5404
|
+
default: def,
|
|
5405
|
+
...requiresProcess ? { requiresProcess } : {}
|
|
5406
|
+
})),
|
|
5407
|
+
layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({
|
|
5408
|
+
kind,
|
|
5409
|
+
label,
|
|
5410
|
+
controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({
|
|
5411
|
+
id,
|
|
5412
|
+
label: l,
|
|
5413
|
+
min,
|
|
5414
|
+
max,
|
|
5415
|
+
step,
|
|
5416
|
+
default: def
|
|
5417
|
+
}))
|
|
5418
|
+
})),
|
|
5419
|
+
blendModes: BLEND_MODES.map((m) => m.id),
|
|
5420
|
+
layerPathFormat: "layers.<layerId>.<controlId>"
|
|
5421
|
+
})
|
|
5422
|
+
};
|
|
5423
|
+
const setAdjustments = {
|
|
5424
|
+
name: "set_adjustments",
|
|
5425
|
+
title: "Set image adjustments",
|
|
5426
|
+
description: 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. "exposure"); layer parameters use "layers.<layerId>.<controlId>" (e.g. "layers.adj_1.vibrance"). Values are clamped to each control range; the applied values are returned.',
|
|
5427
|
+
inputSchema: {
|
|
5428
|
+
type: "object",
|
|
5429
|
+
properties: {
|
|
5430
|
+
values: {
|
|
5431
|
+
type: "object",
|
|
5432
|
+
description: "Map of control id to number. See list_controls for ids and ranges.",
|
|
5433
|
+
additionalProperties: { type: "number" }
|
|
5434
|
+
},
|
|
5435
|
+
label: { type: "string", description: "Short description of the change (echoed back)." }
|
|
5436
|
+
},
|
|
5437
|
+
required: ["values"],
|
|
5438
|
+
additionalProperties: false
|
|
5439
|
+
},
|
|
5440
|
+
annotations: {
|
|
5441
|
+
title: "Set image adjustments",
|
|
5442
|
+
readOnlyHint: false,
|
|
5443
|
+
destructiveHint: false,
|
|
5444
|
+
idempotentHint: true,
|
|
5445
|
+
openWorldHint: false
|
|
5446
|
+
},
|
|
5447
|
+
handler: ({ values, label }) => {
|
|
5448
|
+
const entries = Object.entries(values ?? {});
|
|
5449
|
+
if (!entries.length) throw new Error('No values given. Pass {"values": {"<control id>": number}}.');
|
|
5450
|
+
const state = controller.getState();
|
|
5451
|
+
const base = {};
|
|
5452
|
+
const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));
|
|
5453
|
+
const applied = {};
|
|
5454
|
+
const clamped = [];
|
|
5455
|
+
const unknown = [];
|
|
5456
|
+
for (const [id, raw] of entries) {
|
|
5457
|
+
const m = LAYER_PATH.exec(id);
|
|
5458
|
+
if (m) {
|
|
5459
|
+
const layer = layers.find((l) => l.id === m[1]);
|
|
5460
|
+
const spec2 = layer ? findLayerControl(layer.kind, m[2]) : void 0;
|
|
5461
|
+
if (!layer || !spec2) {
|
|
5462
|
+
unknown.push(id);
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
const v2 = clampControlValue(spec2, raw);
|
|
5466
|
+
if (v2 !== raw) clamped.push(id);
|
|
5467
|
+
if (spec2.id === "opacity") layer.opacity = v2;
|
|
5468
|
+
else layer.params = setControlPath(layer.params, spec2.id, v2);
|
|
5469
|
+
applied[id] = v2;
|
|
5470
|
+
continue;
|
|
5471
|
+
}
|
|
5472
|
+
const spec = findAdjustmentControl(id);
|
|
5473
|
+
if (!spec) {
|
|
5474
|
+
unknown.push(id);
|
|
5475
|
+
continue;
|
|
5476
|
+
}
|
|
5477
|
+
const v = clampControlValue(spec, raw);
|
|
5478
|
+
if (v !== raw) clamped.push(id);
|
|
5479
|
+
base[id] = v;
|
|
5480
|
+
applied[id] = v;
|
|
5481
|
+
}
|
|
5482
|
+
if (unknown.length) {
|
|
5483
|
+
const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(", ");
|
|
5484
|
+
throw new Error(
|
|
5485
|
+
`Unknown control id(s): ${unknown.join(", ")}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`
|
|
5486
|
+
);
|
|
5487
|
+
}
|
|
5488
|
+
const changedLayers = Object.keys(applied).some((id) => id.startsWith("layers."));
|
|
5489
|
+
controller.setAdjustments(changedLayers ? { ...base, layers } : base);
|
|
5490
|
+
const result = { applied, clamped };
|
|
5491
|
+
if (typeof label === "string" && label) result.label = label;
|
|
5492
|
+
const notes = [];
|
|
5493
|
+
if ("tint" in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {
|
|
5494
|
+
notes.push("Tint has no effect on process 1 edits.");
|
|
5495
|
+
}
|
|
5496
|
+
if (notes.length) result.notes = notes;
|
|
5497
|
+
return result;
|
|
5498
|
+
}
|
|
5499
|
+
};
|
|
5500
|
+
const addLayer = {
|
|
5501
|
+
name: "add_adjustment_layer",
|
|
5502
|
+
title: "Add an adjustment layer",
|
|
5503
|
+
description: "Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: " + ADJUSTMENT_KINDS.map((k) => k.kind).join(", ") + ". Omitted params are neutral.",
|
|
5504
|
+
inputSchema: {
|
|
5505
|
+
type: "object",
|
|
5506
|
+
properties: {
|
|
5507
|
+
kind: { type: "string", enum: ADJUSTMENT_KINDS.map((k) => k.kind) },
|
|
5508
|
+
params: { type: "object", description: "Initial parameters (see list_controls); merged with neutral defaults." },
|
|
5509
|
+
opacity: { type: "number", minimum: 0, maximum: 1 },
|
|
5510
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id) },
|
|
5511
|
+
name: { type: "string" }
|
|
5512
|
+
},
|
|
5513
|
+
required: ["kind"],
|
|
5514
|
+
additionalProperties: false
|
|
5515
|
+
},
|
|
5516
|
+
annotations: {
|
|
5517
|
+
title: "Add an adjustment layer",
|
|
5518
|
+
readOnlyHint: false,
|
|
5519
|
+
destructiveHint: false,
|
|
5520
|
+
idempotentHint: false,
|
|
5521
|
+
openWorldHint: false
|
|
5522
|
+
},
|
|
5523
|
+
handler: ({ kind, params, opacity, blend, name }) => {
|
|
5524
|
+
if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind "${kind}".`);
|
|
5525
|
+
if (blend !== void 0 && !isBlendMode(blend)) throw new Error(`Unknown blend mode "${blend}".`);
|
|
5526
|
+
const id = controller.addAdjustmentLayer({ kind, params, opacity, blend, name });
|
|
5527
|
+
const layer = controller.getAdjustmentLayer(id);
|
|
5528
|
+
return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().length - 1 };
|
|
5529
|
+
}
|
|
5530
|
+
};
|
|
5531
|
+
const renderPreview = {
|
|
5532
|
+
name: "render_preview",
|
|
5533
|
+
title: "Render a preview",
|
|
5534
|
+
description: "Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels.",
|
|
5535
|
+
inputSchema: {
|
|
5536
|
+
type: "object",
|
|
5537
|
+
properties: {
|
|
5538
|
+
maxSide: { type: "integer", minimum: 16, maximum: 2048, description: "Longest side in pixels (default 512)." }
|
|
5539
|
+
},
|
|
5540
|
+
additionalProperties: false
|
|
5541
|
+
},
|
|
5542
|
+
annotations: {
|
|
5543
|
+
title: "Render a preview",
|
|
5544
|
+
readOnlyHint: true,
|
|
5545
|
+
destructiveHint: false,
|
|
5546
|
+
idempotentHint: true,
|
|
5547
|
+
openWorldHint: false
|
|
5548
|
+
},
|
|
5549
|
+
handler: async ({ maxSide }) => {
|
|
5550
|
+
const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === "number" ? maxSide : 512)));
|
|
5551
|
+
const state = controller.getState();
|
|
5552
|
+
const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };
|
|
5553
|
+
const quarter = Math.abs(Math.round(state.transform.rotate % 180 / 90)) % 2 === 1;
|
|
5554
|
+
const w = quarter ? crop.height : crop.width;
|
|
5555
|
+
const h = quarter ? crop.width : crop.height;
|
|
5556
|
+
const k = Math.min(1, side / Math.max(w, h, 1));
|
|
5557
|
+
const dataUrl = await controller.toDataURL({
|
|
5558
|
+
format: "image/png",
|
|
5559
|
+
width: Math.max(1, Math.round(w * k)),
|
|
5560
|
+
height: Math.max(1, Math.round(h * k))
|
|
5561
|
+
});
|
|
5562
|
+
const comma = dataUrl.indexOf(",");
|
|
5563
|
+
if (!dataUrl.startsWith("data:image/png") || comma < 0 || comma === dataUrl.length - 1) {
|
|
5564
|
+
throw new Error("Rendering is not available in this environment.");
|
|
5565
|
+
}
|
|
5566
|
+
return {
|
|
5567
|
+
content: [
|
|
5568
|
+
{ type: "image", data: dataUrl.slice(comma + 1), mimeType: "image/png" },
|
|
5569
|
+
{ type: "text", text: `Preview ${Math.round(w * k)}\xD7${Math.round(h * k)}` }
|
|
5570
|
+
]
|
|
5571
|
+
};
|
|
5572
|
+
}
|
|
5573
|
+
};
|
|
5574
|
+
return {
|
|
5575
|
+
id: options.id ?? "image-editor",
|
|
5576
|
+
namespace: options.namespace,
|
|
5577
|
+
tools: [getAdjustments, listControls, setAdjustments, addLayer, renderPreview, ...createImageSelectionTools(controller)]
|
|
5578
|
+
};
|
|
5579
|
+
}
|
|
5580
|
+
|
|
5581
|
+
// src/image-editor/curve-editing.ts
|
|
5582
|
+
var clampLevel = (v) => Math.min(255, Math.max(0, Math.round(v)));
|
|
5583
|
+
function curvePointToBox(p, width, height) {
|
|
5584
|
+
return { x: p.input / 255 * width, y: height - p.output / 255 * height };
|
|
5585
|
+
}
|
|
5586
|
+
function boxToCurvePoint(x, y, width, height) {
|
|
5587
|
+
return { input: clampLevel(x / (width || 1) * 255), output: clampLevel((height - y) / (height || 1) * 255) };
|
|
5588
|
+
}
|
|
5589
|
+
function curveSvgPath(points, width, height, samples = 64) {
|
|
5590
|
+
const lut = curveLutFromLevels(points);
|
|
5591
|
+
const n = lut.length - 1;
|
|
5592
|
+
let d = "";
|
|
5593
|
+
for (let i = 0; i <= samples; i++) {
|
|
5594
|
+
const t = i / samples;
|
|
5595
|
+
const y = lut[Math.round(t * n)];
|
|
5596
|
+
d += `${i === 0 ? "M" : " L"}${round22(t * width)} ${round22(height - y * height)}`;
|
|
5597
|
+
}
|
|
5598
|
+
return d;
|
|
5599
|
+
}
|
|
5600
|
+
function hitTestCurvePoint(points, x, y, width, height, radius = 8) {
|
|
5601
|
+
let best = -1;
|
|
5602
|
+
let bestD = radius * radius;
|
|
5603
|
+
points.forEach((p, i) => {
|
|
5604
|
+
const b = curvePointToBox(p, width, height);
|
|
5605
|
+
const d = (b.x - x) ** 2 + (b.y - y) ** 2;
|
|
5606
|
+
if (d <= bestD) {
|
|
5607
|
+
best = i;
|
|
5608
|
+
bestD = d;
|
|
5609
|
+
}
|
|
5610
|
+
});
|
|
5611
|
+
return best;
|
|
5612
|
+
}
|
|
5613
|
+
function insertCurvePoint(points, p) {
|
|
5614
|
+
const point = { input: clampLevel(p.input), output: clampLevel(p.output) };
|
|
5615
|
+
const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));
|
|
5616
|
+
let index = out.findIndex((q) => q.input > point.input);
|
|
5617
|
+
if (index === -1) index = out.length;
|
|
5618
|
+
out.splice(index, 0, point);
|
|
5619
|
+
return { points: out, index };
|
|
5620
|
+
}
|
|
5621
|
+
function moveCurvePoint(points, index, p) {
|
|
5622
|
+
const out = points.map((q) => ({ ...q }));
|
|
5623
|
+
if (index < 0 || index >= out.length) return out;
|
|
5624
|
+
const lo = index > 0 ? out[index - 1].input + 1 : 0;
|
|
5625
|
+
const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;
|
|
5626
|
+
out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };
|
|
5627
|
+
return out;
|
|
5628
|
+
}
|
|
5629
|
+
function removeCurvePoint(points, index) {
|
|
5630
|
+
if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));
|
|
5631
|
+
return points.filter((_, i) => i !== index).map((q) => ({ ...q }));
|
|
5632
|
+
}
|
|
5633
|
+
var round22 = (v) => Math.round(v * 100) / 100;
|
|
5634
|
+
|
|
5635
|
+
// src/image-editor/selection/gesture.ts
|
|
5636
|
+
var SelectionGesture = class {
|
|
5637
|
+
constructor(controller, getOptions) {
|
|
5638
|
+
this.controller = controller;
|
|
5639
|
+
this.getOptions = getOptions;
|
|
5640
|
+
}
|
|
5641
|
+
controller;
|
|
5642
|
+
getOptions;
|
|
5643
|
+
start = null;
|
|
5644
|
+
points = [];
|
|
5645
|
+
current = null;
|
|
5646
|
+
modifiers = {};
|
|
5647
|
+
/** The last error (e.g. pixels unavailable for the wand), for the UI to show. */
|
|
5648
|
+
error = null;
|
|
5649
|
+
get active() {
|
|
5650
|
+
return this.start !== null;
|
|
5651
|
+
}
|
|
5652
|
+
/** The outline of the shape being drawn, or `null`. */
|
|
5653
|
+
get draftPath() {
|
|
5654
|
+
if (!this.start || !this.current) return null;
|
|
5655
|
+
const { tool } = this.getOptions();
|
|
5656
|
+
if (tool === "lasso") return this.points.length > 1 ? this.points.slice() : null;
|
|
5657
|
+
const rect = normalizeRect({
|
|
5658
|
+
x: this.start.x,
|
|
5659
|
+
y: this.start.y,
|
|
5660
|
+
width: this.current.x - this.start.x,
|
|
5661
|
+
height: this.current.y - this.start.y
|
|
5662
|
+
});
|
|
5663
|
+
if (rect.width < 1 && rect.height < 1) return null;
|
|
5664
|
+
if (tool === "ellipse") return ellipsePoints(rect);
|
|
5665
|
+
return [
|
|
5666
|
+
{ x: rect.x, y: rect.y },
|
|
5667
|
+
{ x: rect.x + rect.width, y: rect.y },
|
|
5668
|
+
{ x: rect.x + rect.width, y: rect.y + rect.height },
|
|
5669
|
+
{ x: rect.x, y: rect.y + rect.height }
|
|
5670
|
+
];
|
|
5671
|
+
}
|
|
5672
|
+
/** Pointer down. Resolves once a wand click has been applied. */
|
|
5673
|
+
async begin(point, modifiers = {}) {
|
|
5674
|
+
const options = this.getOptions();
|
|
5675
|
+
this.error = null;
|
|
5676
|
+
this.modifiers = modifiers;
|
|
5677
|
+
if (options.tool === "wand") {
|
|
5678
|
+
this.start = null;
|
|
5679
|
+
try {
|
|
5680
|
+
await this.controller.magicWand(point, {
|
|
5681
|
+
tolerance: options.tolerance,
|
|
5682
|
+
contiguous: options.contiguous,
|
|
5683
|
+
antiAlias: options.antiAlias,
|
|
5684
|
+
distance: options.distance,
|
|
5685
|
+
sampleMerged: options.sampleMerged,
|
|
5686
|
+
mode: selectionModeFromModifiers(modifiers, options.mode)
|
|
5687
|
+
});
|
|
5688
|
+
} catch (err) {
|
|
5689
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
5690
|
+
}
|
|
5691
|
+
return;
|
|
5692
|
+
}
|
|
5693
|
+
this.start = point;
|
|
5694
|
+
this.current = point;
|
|
5695
|
+
this.points = [point];
|
|
5696
|
+
}
|
|
5697
|
+
/** Pointer move; returns true when the draft changed (re-render). */
|
|
5698
|
+
move(point) {
|
|
5699
|
+
if (!this.start) return false;
|
|
5700
|
+
this.current = point;
|
|
5701
|
+
if (this.getOptions().tool === "lasso") {
|
|
5702
|
+
const last = this.points[this.points.length - 1];
|
|
5703
|
+
if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;
|
|
5704
|
+
this.points.push(point);
|
|
5705
|
+
}
|
|
5706
|
+
return true;
|
|
5707
|
+
}
|
|
5708
|
+
/** Pointer up: commits the shape. Returns true when the selection changed. */
|
|
5709
|
+
end(point, modifiers) {
|
|
5710
|
+
if (!this.start) return false;
|
|
5711
|
+
if (point) this.move(point);
|
|
5712
|
+
const options = this.getOptions();
|
|
5713
|
+
const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);
|
|
5714
|
+
const path = this.draftPath;
|
|
5715
|
+
const rect = normalizeRect({
|
|
5716
|
+
x: this.start.x,
|
|
5717
|
+
y: this.start.y,
|
|
5718
|
+
width: this.current.x - this.start.x,
|
|
5719
|
+
height: this.current.y - this.start.y
|
|
5720
|
+
});
|
|
5721
|
+
this.start = null;
|
|
5722
|
+
this.current = null;
|
|
5723
|
+
this.points = [];
|
|
5724
|
+
if (!path) return mode === "replace" ? this.controller.deselect() : false;
|
|
5725
|
+
const opts = { mode, antiAlias: options.antiAlias };
|
|
5726
|
+
if (options.tool === "lasso") return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;
|
|
5727
|
+
return options.tool === "ellipse" ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);
|
|
5728
|
+
}
|
|
5729
|
+
/** Abandons a drag without changing the selection. */
|
|
5730
|
+
cancel() {
|
|
5731
|
+
this.start = null;
|
|
5732
|
+
this.current = null;
|
|
5733
|
+
this.points = [];
|
|
5734
|
+
}
|
|
5735
|
+
};
|
|
1976
5736
|
export {
|
|
5737
|
+
ADJUSTMENT_KINDS,
|
|
5738
|
+
ADJUSTMENT_LAYER_CONTROLS,
|
|
5739
|
+
ADJUSTMENT_LAYER_OPACITY_CONTROL,
|
|
5740
|
+
ADJUSTMENT_LUT_SIZE,
|
|
1977
5741
|
AS_SHOT_KELVIN,
|
|
5742
|
+
BLEND_MODES,
|
|
1978
5743
|
D65,
|
|
1979
5744
|
DEFAULT_ADJUSTMENTS,
|
|
1980
5745
|
DEFAULT_DEPTH_MASK,
|
|
5746
|
+
DEFAULT_REFINE_EDGE_PARAMS,
|
|
5747
|
+
DEFAULT_SELECTION_TOOL_OPTIONS,
|
|
1981
5748
|
DEFAULT_TRANSFORM,
|
|
5749
|
+
DIRECT_GAUSSIAN_MAX_SIGMA,
|
|
5750
|
+
EMPTY_DISTANCE,
|
|
5751
|
+
GUIDED_FILTER_EPS,
|
|
5752
|
+
HISTOGRAM_BINS,
|
|
5753
|
+
HUE_RANGE_LABELS,
|
|
1982
5754
|
IDENTITY_MATRIX3,
|
|
5755
|
+
IMAGE_ADJUSTMENT_CONTROLS,
|
|
5756
|
+
IMAGE_EDITOR_SCHEMA_VERSION,
|
|
5757
|
+
IMAGE_EDITOR_STATE_VERSION,
|
|
1983
5758
|
ImageEditorController,
|
|
1984
5759
|
ImageEditorRenderer,
|
|
1985
5760
|
LATEST_PROCESS_VERSION,
|
|
@@ -1988,48 +5763,184 @@ export {
|
|
|
1988
5763
|
MAX_KELVIN,
|
|
1989
5764
|
MIN_KELVIN,
|
|
1990
5765
|
MIRED_PER_TEMPERATURE_UNIT,
|
|
5766
|
+
REFINE_EDGE_CONTROLS,
|
|
5767
|
+
SELECTION_MODES,
|
|
5768
|
+
SELECTION_MODE_LABELS,
|
|
5769
|
+
SELECTION_MODIFY_OPS,
|
|
5770
|
+
SELECTION_TOOLS,
|
|
5771
|
+
SelectionGesture,
|
|
1991
5772
|
TINT_SCALE,
|
|
1992
5773
|
TONE_LUT_SIZE,
|
|
5774
|
+
TONE_RANGE_PRESETS,
|
|
5775
|
+
VIBRANCE_GREY,
|
|
5776
|
+
activeAdjustmentLayers,
|
|
5777
|
+
adjustmentKindLabel,
|
|
5778
|
+
adjustmentLayersKey,
|
|
5779
|
+
applyAdjustmentLayers,
|
|
1993
5780
|
applyAdjustments,
|
|
1994
5781
|
applyDepthMask,
|
|
1995
5782
|
applyImageProjectionToContext,
|
|
1996
5783
|
applyLegacyAdjustments,
|
|
1997
5784
|
applyMatrix3,
|
|
1998
5785
|
applyProcess2Adjustments,
|
|
5786
|
+
blackWhiteGray,
|
|
5787
|
+
blendChannel,
|
|
5788
|
+
blendImageData,
|
|
5789
|
+
blendLum,
|
|
5790
|
+
blendPixel,
|
|
5791
|
+
blendRgb,
|
|
5792
|
+
borderCoverage,
|
|
5793
|
+
boxMean,
|
|
5794
|
+
boxToCurvePoint,
|
|
1999
5795
|
bradfordAdaptation,
|
|
5796
|
+
buildLayerMaskPlanes,
|
|
2000
5797
|
buildToneLut,
|
|
2001
5798
|
calculateCropDrag,
|
|
5799
|
+
canvasCompositeOperation,
|
|
2002
5800
|
canvasToImagePoint,
|
|
5801
|
+
clampControlValue,
|
|
2003
5802
|
clampCropToImage,
|
|
5803
|
+
colorBalanceChannel,
|
|
5804
|
+
colorRange,
|
|
5805
|
+
combineSelections,
|
|
5806
|
+
compileAdjustmentLayer,
|
|
5807
|
+
computeHistogram,
|
|
5808
|
+
computeSelectionBounds,
|
|
5809
|
+
contractCoverage,
|
|
5810
|
+
controlPosition,
|
|
5811
|
+
controlTrackBackground,
|
|
5812
|
+
coverageToByte,
|
|
5813
|
+
createAdjustmentLayer,
|
|
5814
|
+
createImageEditorToolProvider,
|
|
5815
|
+
createImageSelectionTools,
|
|
5816
|
+
createSelectionMask,
|
|
5817
|
+
curveLut,
|
|
5818
|
+
curveLutFromLevels,
|
|
5819
|
+
curvePointToBox,
|
|
5820
|
+
curveSvgPath,
|
|
5821
|
+
decodeLayerMask,
|
|
5822
|
+
decodeSelectionRle,
|
|
5823
|
+
defaultAdjustmentParams,
|
|
5824
|
+
describeSelection,
|
|
5825
|
+
deserializeSelection,
|
|
5826
|
+
distanceTransform,
|
|
5827
|
+
edgeWidth,
|
|
5828
|
+
ellipsePoints,
|
|
5829
|
+
ellipseSelection,
|
|
5830
|
+
encodeSelectionRle,
|
|
5831
|
+
evaluateAdjustmentLayers,
|
|
5832
|
+
expandCoverage,
|
|
5833
|
+
featherCoverage,
|
|
5834
|
+
findAdjustmentControl,
|
|
5835
|
+
findLayerControl,
|
|
5836
|
+
formatControlValue,
|
|
5837
|
+
fullSelection,
|
|
5838
|
+
gaussianBlur,
|
|
2004
5839
|
getAnnotationBounds,
|
|
5840
|
+
getControlPath,
|
|
2005
5841
|
getCropCursor,
|
|
2006
5842
|
getRotatedSize,
|
|
2007
5843
|
getViewportMetrics,
|
|
2008
5844
|
getViewportProjection,
|
|
2009
5845
|
globalImageCache,
|
|
5846
|
+
greyMorphology,
|
|
5847
|
+
guidePlanes,
|
|
5848
|
+
guidedFilterColor,
|
|
5849
|
+
hashMaskContent,
|
|
5850
|
+
hexToRgb,
|
|
5851
|
+
histogramPath,
|
|
2010
5852
|
hitTestAnnotation,
|
|
2011
5853
|
hitTestCrop,
|
|
5854
|
+
hitTestCurvePoint,
|
|
5855
|
+
hslToRgb,
|
|
5856
|
+
hueRange,
|
|
5857
|
+
hueRangeTables,
|
|
5858
|
+
hueRangeWeight,
|
|
5859
|
+
hueSaturation,
|
|
5860
|
+
identityCurve,
|
|
5861
|
+
identityLevels,
|
|
5862
|
+
imageEditorStateMigrations,
|
|
2012
5863
|
imageToCanvasPoint,
|
|
5864
|
+
insertCurvePoint,
|
|
2013
5865
|
invertMatrix3,
|
|
5866
|
+
invertSelection,
|
|
5867
|
+
isAdjustmentKind,
|
|
5868
|
+
isBlendMode,
|
|
2014
5869
|
isQuarterTurnSwapped,
|
|
5870
|
+
isSelectionEmpty,
|
|
5871
|
+
isSelectionMode,
|
|
5872
|
+
isSeparableBlendMode,
|
|
2015
5873
|
kelvinToTemperature,
|
|
5874
|
+
layerMaskCoverage,
|
|
5875
|
+
layerMaskFromSelection,
|
|
5876
|
+
layerMaskKey,
|
|
5877
|
+
levelsValue,
|
|
2016
5878
|
linearSrgbToXy,
|
|
2017
5879
|
linearToSrgb,
|
|
5880
|
+
loadImageEditorDocument,
|
|
5881
|
+
magicWand,
|
|
2018
5882
|
mapCropHandle,
|
|
5883
|
+
migrateSerializedImageEditorState,
|
|
5884
|
+
modifySelection,
|
|
5885
|
+
moveCurvePoint,
|
|
2019
5886
|
mulMatrix3,
|
|
5887
|
+
neutralHueRange,
|
|
5888
|
+
normalizeAdjustmentLayer,
|
|
5889
|
+
normalizeAdjustmentLayers,
|
|
2020
5890
|
normalizeAngle,
|
|
5891
|
+
normalizeBlendMode,
|
|
5892
|
+
normalizeLayerMask,
|
|
2021
5893
|
normalizeProcessVersion,
|
|
5894
|
+
normalizeRect,
|
|
5895
|
+
normalizeRefineEdgeParams,
|
|
2022
5896
|
normalizeStoredAdjustments,
|
|
5897
|
+
parseSelectionMode,
|
|
5898
|
+
photoFilterMatrix,
|
|
5899
|
+
polygonCoverage,
|
|
5900
|
+
polygonSelection,
|
|
5901
|
+
posterizeValue,
|
|
2023
5902
|
projectImagePoint,
|
|
2024
5903
|
projectImageRect,
|
|
5904
|
+
projectMaskCoverage,
|
|
5905
|
+
quickMaskPixels,
|
|
5906
|
+
rangeLightness,
|
|
5907
|
+
readAdjustmentControls,
|
|
5908
|
+
rectSelection,
|
|
5909
|
+
refineCoverage,
|
|
5910
|
+
refineEdgeHalo,
|
|
5911
|
+
refineSelection,
|
|
2025
5912
|
removeAlpha,
|
|
5913
|
+
removeCurvePoint,
|
|
5914
|
+
resizeSelection,
|
|
5915
|
+
rgbToGray,
|
|
5916
|
+
rgbToHsl,
|
|
5917
|
+
sampleLut,
|
|
2026
5918
|
sampleToneLut,
|
|
5919
|
+
selectionArea,
|
|
5920
|
+
selectionFromCoverage,
|
|
5921
|
+
selectionFromPixels,
|
|
5922
|
+
selectionModeFromModifiers,
|
|
5923
|
+
selectionOutline,
|
|
5924
|
+
selectionToCoverage,
|
|
5925
|
+
selectionToPixels,
|
|
5926
|
+
selectionValueAt,
|
|
5927
|
+
serializeSelection,
|
|
5928
|
+
setControlPath,
|
|
5929
|
+
setLum,
|
|
5930
|
+
shiftCoverage,
|
|
5931
|
+
smoothCoverage,
|
|
5932
|
+
srgb8ToLab,
|
|
2027
5933
|
srgbToLinear,
|
|
2028
5934
|
tempTintToXy,
|
|
2029
5935
|
temperatureToKelvin,
|
|
2030
5936
|
toneCurve,
|
|
5937
|
+
toneLuts,
|
|
5938
|
+
toneRange,
|
|
2031
5939
|
unprojectCanvasPoint,
|
|
5940
|
+
updateLayerMaskProps,
|
|
2032
5941
|
uvToXy,
|
|
5942
|
+
vibranceLinear,
|
|
5943
|
+
vibrancePixel,
|
|
2033
5944
|
wbMatrix,
|
|
2034
5945
|
whiteBalanceFromSample,
|
|
2035
5946
|
whiteBalanceMatrix,
|