@pixerate/editor 0.199.0 → 0.201.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-editor/controller.d.cts +35 -0
- package/dist/image-editor/controller.d.ts +35 -0
- package/dist/image-editor/index.cjs +980 -667
- package/dist/image-editor/index.js +1238 -925
- package/dist/image-editor/selection/segment/focus.d.cts +22 -0
- package/dist/image-editor/selection/segment/focus.d.ts +22 -0
- package/dist/image-editor/selection/segment/guided.d.cts +13 -0
- package/dist/image-editor/selection/segment/guided.d.ts +13 -0
- package/dist/image-editor/selection/segment/index.d.cts +3 -0
- package/dist/image-editor/selection/segment/index.d.ts +3 -0
- package/dist/image-editor/selection/segment/sky.d.cts +21 -0
- package/dist/image-editor/selection/segment/sky.d.ts +21 -0
- package/dist/image-editor/selection/segment/work.d.cts +2 -2
- package/dist/image-editor/selection/segment/work.d.ts +2 -2
- package/dist/image-editor/selection/tool-options.d.cts +6 -0
- package/dist/image-editor/selection/tool-options.d.ts +6 -0
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +2 -0
- package/skills/pixerate-image-selection-masking/SKILL.md +6 -2
|
@@ -3049,8 +3049,9 @@ var GridGraph = class {
|
|
|
3049
3049
|
var WORK_SIZE = 512;
|
|
3050
3050
|
var workCache = /* @__PURE__ */ new WeakMap();
|
|
3051
3051
|
function workImage(pixels, maxSide = WORK_SIZE) {
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3052
|
+
const sizes = workCache.get(pixels.data);
|
|
3053
|
+
const cached = sizes?.get(maxSide);
|
|
3054
|
+
if (cached && cached.sourceWidth === pixels.width && cached.sourceHeight === pixels.height) return cached;
|
|
3054
3055
|
const { width: W, height: H, data } = pixels;
|
|
3055
3056
|
const scale = Math.min(1, maxSide / Math.max(W, H));
|
|
3056
3057
|
const w = Math.max(1, Math.round(W * scale));
|
|
@@ -3079,7 +3080,8 @@ function workImage(pixels, maxSide = WORK_SIZE) {
|
|
|
3079
3080
|
rgb[i * 3 + 2] = sum[i * 3 + 2] / c;
|
|
3080
3081
|
}
|
|
3081
3082
|
const work = { sourceWidth: W, sourceHeight: H, width: w, height: h, scale: w / W, rgb };
|
|
3082
|
-
|
|
3083
|
+
if (sizes) sizes.set(maxSide, work);
|
|
3084
|
+
else workCache.set(pixels.data, /* @__PURE__ */ new Map([[maxSide, work]]));
|
|
3083
3085
|
return work;
|
|
3084
3086
|
}
|
|
3085
3087
|
var GAMMA = 50;
|
|
@@ -3690,14 +3692,14 @@ function toLab(r, g, b) {
|
|
|
3690
3692
|
function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
|
|
3691
3693
|
const { width: w, height: h, rgb } = work;
|
|
3692
3694
|
const n = w * h;
|
|
3693
|
-
const
|
|
3694
|
-
for (let i = 0; i < n; i++)
|
|
3695
|
+
const lab2 = new Float32Array(n * 3);
|
|
3696
|
+
for (let i = 0; i < n; i++) lab2.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
|
|
3695
3697
|
const S = Math.max(2, Math.round(Math.sqrt(n / target)));
|
|
3696
3698
|
const seeds = [];
|
|
3697
3699
|
for (let y = Math.floor(S / 2); y < h; y += S) {
|
|
3698
3700
|
for (let x = Math.floor(S / 2); x < w; x += S) {
|
|
3699
3701
|
const i = y * w + x;
|
|
3700
|
-
seeds.push([
|
|
3702
|
+
seeds.push([lab2[i * 3], lab2[i * 3 + 1], lab2[i * 3 + 2], x, y]);
|
|
3701
3703
|
}
|
|
3702
3704
|
}
|
|
3703
3705
|
const k = seeds.length;
|
|
@@ -3715,7 +3717,7 @@ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
|
|
|
3715
3717
|
for (let y = y0; y <= y1; y++) {
|
|
3716
3718
|
for (let x = x0; x <= x1; x++) {
|
|
3717
3719
|
const i = y * w + x;
|
|
3718
|
-
const d = (
|
|
3720
|
+
const d = (lab2[i * 3] - L) ** 2 + (lab2[i * 3 + 1] - A) ** 2 + (lab2[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
|
|
3719
3721
|
if (d < best[i]) {
|
|
3720
3722
|
best[i] = d;
|
|
3721
3723
|
label[i] = c;
|
|
@@ -3726,9 +3728,9 @@ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
|
|
|
3726
3728
|
const sum = new Float64Array(k * 6);
|
|
3727
3729
|
for (let i = 0; i < n; i++) {
|
|
3728
3730
|
const c = label[i];
|
|
3729
|
-
sum[c * 6] +=
|
|
3730
|
-
sum[c * 6 + 1] +=
|
|
3731
|
-
sum[c * 6 + 2] +=
|
|
3731
|
+
sum[c * 6] += lab2[i * 3];
|
|
3732
|
+
sum[c * 6 + 1] += lab2[i * 3 + 1];
|
|
3733
|
+
sum[c * 6 + 2] += lab2[i * 3 + 2];
|
|
3732
3734
|
sum[c * 6 + 3] += i % w;
|
|
3733
3735
|
sum[c * 6 + 4] += Math.floor(i / w);
|
|
3734
3736
|
sum[c * 6 + 5]++;
|
|
@@ -3746,9 +3748,9 @@ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
|
|
|
3746
3748
|
const size = new Int32Array(count);
|
|
3747
3749
|
for (let i = 0; i < n; i++) {
|
|
3748
3750
|
const c = label[i] = remap[label[i]];
|
|
3749
|
-
meanLab[c * 3] +=
|
|
3750
|
-
meanLab[c * 3 + 1] +=
|
|
3751
|
-
meanLab[c * 3 + 2] +=
|
|
3751
|
+
meanLab[c * 3] += lab2[i * 3];
|
|
3752
|
+
meanLab[c * 3 + 1] += lab2[i * 3 + 1];
|
|
3753
|
+
meanLab[c * 3 + 2] += lab2[i * 3 + 2];
|
|
3752
3754
|
centre[c * 2] += i % w;
|
|
3753
3755
|
centre[c * 2 + 1] += Math.floor(i / w);
|
|
3754
3756
|
size[c]++;
|
|
@@ -3761,8 +3763,8 @@ function slicSuperpixels(work, target = 400, compactness = 10, rounds = 5) {
|
|
|
3761
3763
|
return { label, count, lab: meanLab, centre, size };
|
|
3762
3764
|
}
|
|
3763
3765
|
function superpixelSaliency(sp, width, height) {
|
|
3764
|
-
const { count, lab, centre, size, label } = sp;
|
|
3765
|
-
const dist = (a, b) => Math.hypot(
|
|
3766
|
+
const { count, lab: lab2, centre, size, label } = sp;
|
|
3767
|
+
const dist = (a, b) => Math.hypot(lab2[a * 3] - lab2[b * 3], lab2[a * 3 + 1] - lab2[b * 3 + 1], lab2[a * 3 + 2] - lab2[b * 3 + 2]);
|
|
3766
3768
|
const onBorder = new Uint8Array(count);
|
|
3767
3769
|
for (let x = 0; x < width; x++) {
|
|
3768
3770
|
onBorder[label[x]] = 1;
|
|
@@ -3875,102 +3877,559 @@ function findSubject(pixels, options = {}) {
|
|
|
3875
3877
|
return { selection: upscaleRegion(work, pixels, roi, label, options.enhanceEdge ?? false) };
|
|
3876
3878
|
}
|
|
3877
3879
|
|
|
3878
|
-
// src/image-editor/
|
|
3879
|
-
var
|
|
3880
|
-
kind: "rect",
|
|
3881
|
-
fill: "#4f46e5",
|
|
3882
|
-
stroke: null,
|
|
3883
|
-
strokeWidth: 2,
|
|
3880
|
+
// src/image-editor/selection/refine.ts
|
|
3881
|
+
var DEFAULT_REFINE_EDGE_PARAMS = {
|
|
3884
3882
|
radius: 0,
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3883
|
+
smartRadius: false,
|
|
3884
|
+
smooth: 0,
|
|
3885
|
+
feather: 0,
|
|
3886
|
+
contrast: 0,
|
|
3887
|
+
shiftEdge: 0
|
|
3890
3888
|
};
|
|
3891
|
-
var
|
|
3892
|
-
var
|
|
3893
|
-
function
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3889
|
+
var GUIDED_FILTER_EPS = 1e-4;
|
|
3890
|
+
var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
3891
|
+
function normalizeRefineEdgeParams(p = {}) {
|
|
3892
|
+
return {
|
|
3893
|
+
radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
|
|
3894
|
+
smartRadius: p.smartRadius === true,
|
|
3895
|
+
smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
|
|
3896
|
+
feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
|
|
3897
|
+
contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
|
|
3898
|
+
shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
|
|
3899
|
+
};
|
|
3900
|
+
}
|
|
3901
|
+
var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
|
|
3902
|
+
var smoothSigma = (p) => p.smooth / 100 * 4;
|
|
3903
|
+
var featherSigma = (p) => p.feather / 2;
|
|
3904
|
+
function shiftAmount(p) {
|
|
3905
|
+
const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
|
|
3906
|
+
return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
|
|
3907
|
+
}
|
|
3908
|
+
function refineEdgeHalo(params = {}) {
|
|
3909
|
+
const p = normalizeRefineEdgeParams(params);
|
|
3910
|
+
const g = gfRadius(p);
|
|
3911
|
+
const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
|
|
3912
|
+
return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
|
|
3913
|
+
}
|
|
3914
|
+
function boxMean(src, w, h, r) {
|
|
3915
|
+
const tmp = new Float32Array(w * h);
|
|
3916
|
+
const pre = new Float64Array(Math.max(w, h) + 1);
|
|
3917
|
+
for (let y = 0; y < h; y++) {
|
|
3918
|
+
const row = y * w;
|
|
3919
|
+
for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
|
|
3920
|
+
for (let x = 0; x < w; x++) {
|
|
3921
|
+
const lo = Math.max(0, x - r);
|
|
3922
|
+
const hi = Math.min(w, x + r + 1);
|
|
3923
|
+
tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
3902
3924
|
}
|
|
3903
|
-
if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;
|
|
3904
|
-
const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};
|
|
3905
|
-
return { kind: "line", x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };
|
|
3906
|
-
}
|
|
3907
|
-
let w = b.x - a.x;
|
|
3908
|
-
let h = b.y - a.y;
|
|
3909
|
-
if (mods.shift) {
|
|
3910
|
-
const s = Math.max(Math.abs(w), Math.abs(h));
|
|
3911
|
-
w = Math.sign(w || 1) * s;
|
|
3912
|
-
h = Math.sign(h || 1) * s;
|
|
3913
3925
|
}
|
|
3914
|
-
|
|
3915
|
-
let
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3926
|
+
const out = new Float32Array(w * h);
|
|
3927
|
+
for (let x = 0; x < w; x++) {
|
|
3928
|
+
for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
|
|
3929
|
+
for (let y = 0; y < h; y++) {
|
|
3930
|
+
const lo = Math.max(0, y - r);
|
|
3931
|
+
const hi = Math.min(h, y + r + 1);
|
|
3932
|
+
out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
|
|
3933
|
+
}
|
|
3921
3934
|
}
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3935
|
+
return out;
|
|
3936
|
+
}
|
|
3937
|
+
function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
|
|
3938
|
+
const n = w * h;
|
|
3939
|
+
const [c0, c1, c2] = guide;
|
|
3940
|
+
const prod = (a, b2) => {
|
|
3941
|
+
const o = new Float32Array(n);
|
|
3942
|
+
for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
|
|
3943
|
+
return o;
|
|
3944
|
+
};
|
|
3945
|
+
const m0 = boxMean(c0, w, h, r);
|
|
3946
|
+
const m1 = boxMean(c1, w, h, r);
|
|
3947
|
+
const m2 = boxMean(c2, w, h, r);
|
|
3948
|
+
const mp = boxMean(p, w, h, r);
|
|
3949
|
+
const mip0 = boxMean(prod(c0, p), w, h, r);
|
|
3950
|
+
const mip1 = boxMean(prod(c1, p), w, h, r);
|
|
3951
|
+
const mip2 = boxMean(prod(c2, p), w, h, r);
|
|
3952
|
+
const s00 = boxMean(prod(c0, c0), w, h, r);
|
|
3953
|
+
const s01 = boxMean(prod(c0, c1), w, h, r);
|
|
3954
|
+
const s02 = boxMean(prod(c0, c2), w, h, r);
|
|
3955
|
+
const s11 = boxMean(prod(c1, c1), w, h, r);
|
|
3956
|
+
const s12 = boxMean(prod(c1, c2), w, h, r);
|
|
3957
|
+
const s22 = boxMean(prod(c2, c2), w, h, r);
|
|
3958
|
+
const a0 = new Float32Array(n);
|
|
3959
|
+
const a1 = new Float32Array(n);
|
|
3960
|
+
const a2 = new Float32Array(n);
|
|
3961
|
+
const b = new Float32Array(n);
|
|
3962
|
+
for (let i = 0; i < n; i++) {
|
|
3963
|
+
const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
|
|
3964
|
+
const rr = s00[i] - u0 * u0 + eps;
|
|
3965
|
+
const rg = s01[i] - u0 * u1;
|
|
3966
|
+
const rb = s02[i] - u0 * u2;
|
|
3967
|
+
const gg = s11[i] - u1 * u1 + eps;
|
|
3968
|
+
const gb = s12[i] - u1 * u2;
|
|
3969
|
+
const bb = s22[i] - u2 * u2 + eps;
|
|
3970
|
+
const v0 = mip0[i] - u0 * up;
|
|
3971
|
+
const v1 = mip1[i] - u1 * up;
|
|
3972
|
+
const v2 = mip2[i] - u2 * up;
|
|
3973
|
+
const i00 = gg * bb - gb * gb;
|
|
3974
|
+
const i01 = gb * rb - rg * bb;
|
|
3975
|
+
const i02 = rg * gb - gg * rb;
|
|
3976
|
+
const i11 = rr * bb - rb * rb;
|
|
3977
|
+
const i12 = rb * rg - rr * gb;
|
|
3978
|
+
const i22 = rr * gg - rg * rg;
|
|
3979
|
+
const det = rr * i00 + rg * i01 + rb * i02;
|
|
3980
|
+
let x0 = 0, x1 = 0, x2 = 0;
|
|
3981
|
+
if (Math.abs(det) >= 1e-30) {
|
|
3982
|
+
x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
|
|
3983
|
+
x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
|
|
3984
|
+
x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
|
|
3985
|
+
}
|
|
3986
|
+
a0[i] = x0;
|
|
3987
|
+
a1[i] = x1;
|
|
3988
|
+
a2[i] = x2;
|
|
3989
|
+
b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
|
|
3933
3990
|
}
|
|
3934
|
-
|
|
3991
|
+
const ma0 = boxMean(a0, w, h, r);
|
|
3992
|
+
const ma1 = boxMean(a1, w, h, r);
|
|
3993
|
+
const ma2 = boxMean(a2, w, h, r);
|
|
3994
|
+
const mb = boxMean(b, w, h, r);
|
|
3995
|
+
const out = new Float32Array(n);
|
|
3996
|
+
for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
|
|
3997
|
+
return out;
|
|
3935
3998
|
}
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3999
|
+
function edgeWidth(guide, w, h, r) {
|
|
4000
|
+
const n = w * h;
|
|
4001
|
+
const y = new Float32Array(n);
|
|
4002
|
+
for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
|
|
4003
|
+
const g = new Float32Array(n);
|
|
4004
|
+
for (let yy = 0; yy < h; yy++) {
|
|
4005
|
+
for (let xx = 0; xx < w; xx++) {
|
|
4006
|
+
const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
|
|
4007
|
+
const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
|
|
4008
|
+
g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
|
|
4009
|
+
}
|
|
3940
4010
|
}
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
4011
|
+
const gm = greyMorphology(g, w, h, r, true);
|
|
4012
|
+
const hi = greyMorphology(y, w, h, r, true);
|
|
4013
|
+
const lo = greyMorphology(y, w, h, r, false);
|
|
4014
|
+
const out = new Float32Array(n);
|
|
4015
|
+
for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
|
|
4016
|
+
return out;
|
|
4017
|
+
}
|
|
4018
|
+
function refineCoverage(guide, m, w, h, params = {}) {
|
|
4019
|
+
const p = normalizeRefineEdgeParams(params);
|
|
4020
|
+
let a = Float32Array.from(m);
|
|
4021
|
+
if (p.radius > 0 && guide) {
|
|
4022
|
+
const n = w * h;
|
|
4023
|
+
const boundary = new Uint8Array(n);
|
|
4024
|
+
let any = false;
|
|
4025
|
+
for (let y = 0; y < h; y++) {
|
|
4026
|
+
for (let x = 0; x < w; x++) {
|
|
4027
|
+
const i = y * w + x;
|
|
4028
|
+
const v = m[i] >= 0.5;
|
|
4029
|
+
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;
|
|
4030
|
+
if (diff) {
|
|
4031
|
+
boundary[i] = 1;
|
|
4032
|
+
any = true;
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
if (any) {
|
|
4037
|
+
const dist = distanceTransform(boundary, w, h);
|
|
4038
|
+
const r = gfRadius(p);
|
|
4039
|
+
const q = guidedFilterColor(guide, m, w, h, r);
|
|
4040
|
+
const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
|
|
4041
|
+
for (let i = 0; i < n; i++) {
|
|
4042
|
+
const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
|
|
4043
|
+
const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
|
|
4044
|
+
if (wgt > 0) {
|
|
4045
|
+
const qi = Math.min(1, Math.max(0, q[i]));
|
|
4046
|
+
a[i] = m[i] + (qi - m[i]) * wgt;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
3948
4050
|
}
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4051
|
+
const ss = smoothSigma(p);
|
|
4052
|
+
if (ss >= 0.3) {
|
|
4053
|
+
a = gaussianBlur(a, w, h, ss, "clamp");
|
|
4054
|
+
const gain = 1 + ss;
|
|
4055
|
+
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
3952
4056
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
4057
|
+
a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
|
|
4058
|
+
if (p.contrast > 0) {
|
|
4059
|
+
const gain = 1 / (1 - 0.99 * p.contrast / 100);
|
|
4060
|
+
for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
|
|
3957
4061
|
}
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
4062
|
+
const amount = shiftAmount(p);
|
|
4063
|
+
if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
|
|
4064
|
+
return a;
|
|
4065
|
+
}
|
|
4066
|
+
function guidePlanes(pixels, win) {
|
|
4067
|
+
const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
|
|
4068
|
+
const n = box.width * box.height;
|
|
4069
|
+
const r = new Float32Array(n);
|
|
4070
|
+
const g = new Float32Array(n);
|
|
4071
|
+
const b = new Float32Array(n);
|
|
4072
|
+
const d = pixels.data;
|
|
4073
|
+
for (let y = 0; y < box.height; y++) {
|
|
4074
|
+
for (let x = 0; x < box.width; x++) {
|
|
4075
|
+
const i = y * box.width + x;
|
|
4076
|
+
const p = ((box.y + y) * pixels.width + box.x + x) * 4;
|
|
4077
|
+
r[i] = d[p] / 255;
|
|
4078
|
+
g[i] = d[p + 1] / 255;
|
|
4079
|
+
b[i] = d[p + 2] / 255;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
return [r, g, b];
|
|
4083
|
+
}
|
|
4084
|
+
function refineSelection(pixels, mask, params = {}) {
|
|
4085
|
+
const p = normalizeRefineEdgeParams(params);
|
|
4086
|
+
if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
|
|
4087
|
+
throw new Error("Refine Edge needs pixels the size of the selection");
|
|
4088
|
+
}
|
|
4089
|
+
if (!mask.bounds) return mask;
|
|
4090
|
+
const halo = refineEdgeHalo(p);
|
|
4091
|
+
const x0 = Math.max(0, mask.bounds.x - halo);
|
|
4092
|
+
const y0 = Math.max(0, mask.bounds.y - halo);
|
|
4093
|
+
const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
|
|
4094
|
+
const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
|
|
4095
|
+
const ww = x1 - x0;
|
|
4096
|
+
const wh = y1 - y0;
|
|
4097
|
+
const m = new Float32Array(ww * wh);
|
|
4098
|
+
for (let y = 0; y < wh; y++) {
|
|
4099
|
+
const src = (y0 + y) * mask.width + x0;
|
|
4100
|
+
for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
|
|
4101
|
+
}
|
|
4102
|
+
const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
|
|
4103
|
+
const refined = refineCoverage(guide, m, ww, wh, p);
|
|
4104
|
+
const out = new Uint8Array(mask.width * mask.height);
|
|
4105
|
+
for (let y = 0; y < wh; y++) {
|
|
4106
|
+
const dst = (y0 + y) * mask.width + x0;
|
|
4107
|
+
for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
|
|
4108
|
+
}
|
|
4109
|
+
return createSelectionMask(mask.width, mask.height, out);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
// src/image-editor/selection/segment/guided.ts
|
|
4113
|
+
function workPlanes(work) {
|
|
4114
|
+
const n = work.width * work.height;
|
|
4115
|
+
const planes = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];
|
|
4116
|
+
for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = work.rgb[i * 3 + c] / 255;
|
|
4117
|
+
return planes;
|
|
4118
|
+
}
|
|
4119
|
+
function workLuma(work) {
|
|
4120
|
+
const n = work.width * work.height;
|
|
4121
|
+
const out = new Float32Array(n);
|
|
4122
|
+
const { rgb } = work;
|
|
4123
|
+
for (let i = 0; i < n; i++) out[i] = (0.299 * rgb[i * 3] + 0.587 * rgb[i * 3 + 1] + 0.114 * rgb[i * 3 + 2]) / 255;
|
|
4124
|
+
return out;
|
|
4125
|
+
}
|
|
4126
|
+
function softMaskToSelection(work, mask) {
|
|
4127
|
+
const { sourceWidth: W, sourceHeight: H, width: w, height: h, scale } = work;
|
|
4128
|
+
const out = new Uint8Array(W * H);
|
|
4129
|
+
const col = new Int32Array(W);
|
|
4130
|
+
const fx = new Float32Array(W);
|
|
4131
|
+
for (let X = 0; X < W; X++) {
|
|
4132
|
+
const u = Math.min(w - 1, Math.max(0, (X + 0.5) * scale - 0.5));
|
|
4133
|
+
col[X] = Math.min(w - 2, Math.floor(u));
|
|
4134
|
+
fx[X] = u - col[X];
|
|
4135
|
+
if (w === 1) {
|
|
4136
|
+
col[X] = 0;
|
|
4137
|
+
fx[X] = 0;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
for (let Y = 0; Y < H; Y++) {
|
|
4141
|
+
const v = Math.min(h - 1, Math.max(0, (Y + 0.5) * scale - 0.5));
|
|
4142
|
+
const j = h === 1 ? 0 : Math.min(h - 2, Math.floor(v));
|
|
4143
|
+
const fy = h === 1 ? 0 : v - j;
|
|
4144
|
+
const r0 = j * w;
|
|
4145
|
+
const r1 = h === 1 ? r0 : r0 + w;
|
|
4146
|
+
for (let X = 0; X < W; X++) {
|
|
4147
|
+
const i = col[X];
|
|
4148
|
+
const i1 = w === 1 ? i : i + 1;
|
|
4149
|
+
const t = fx[X];
|
|
4150
|
+
const m = (mask[r0 + i] * (1 - t) + mask[r0 + i1] * t) * (1 - fy) + (mask[r1 + i] * (1 - t) + mask[r1 + i1] * t) * fy;
|
|
4151
|
+
out[Y * W + X] = Math.round(Math.min(1, Math.max(0, m)) * 255);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
return selectionFromRegion(W, H, { x: 0, y: 0, width: W, height: H }, out);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// src/image-editor/selection/segment/focus.ts
|
|
4158
|
+
var FOCUS_SIZE = 1024;
|
|
4159
|
+
function estimateNoise(luma2, w, h) {
|
|
4160
|
+
if (w < 3 || h < 3) return 0;
|
|
4161
|
+
let sum = 0;
|
|
4162
|
+
for (let y = 1; y < h - 1; y++) {
|
|
4163
|
+
for (let x = 1; x < w - 1; x++) {
|
|
4164
|
+
const i = y * w + x;
|
|
4165
|
+
const v = luma2[i - w - 1] - 2 * luma2[i - w] + luma2[i - w + 1] - 2 * luma2[i - 1] + 4 * luma2[i] - 2 * luma2[i + 1] + luma2[i + w - 1] - 2 * luma2[i + w] + luma2[i + w + 1];
|
|
4166
|
+
sum += Math.abs(v);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
return Math.sqrt(Math.PI / 2) * sum / (6 * (w - 2) * (h - 2));
|
|
4170
|
+
}
|
|
4171
|
+
function focusArea(pixels, options = {}) {
|
|
4172
|
+
const work = workImage(pixels, FOCUS_SIZE);
|
|
4173
|
+
const { width: w, height: h, scale } = work;
|
|
4174
|
+
const n = w * h;
|
|
4175
|
+
const luma2 = workLuma(work);
|
|
4176
|
+
const sigma = estimateNoise(luma2, w, h);
|
|
4177
|
+
const noise = Math.min(100, Math.max(0, options.noise ?? 0));
|
|
4178
|
+
const floor = 2 * sigma * (1 + noise / 25);
|
|
4179
|
+
const detail = new Float32Array(n);
|
|
4180
|
+
for (let y = 1; y < h - 1; y++) {
|
|
4181
|
+
for (let x = 1; x < w - 1; x++) {
|
|
4182
|
+
const i = y * w + x;
|
|
4183
|
+
const lap = Math.abs(4 * luma2[i] - luma2[i - 1] - luma2[i + 1] - luma2[i - w] - luma2[i + w]);
|
|
4184
|
+
const d = Math.max(0, lap - floor);
|
|
4185
|
+
detail[i] = d * d;
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
const r = Math.max(2, Math.round(0.012 * Math.max(w, h)));
|
|
4189
|
+
const energy = boxMean(detail, w, h, r);
|
|
4190
|
+
const sharp = new Float32Array(n);
|
|
4191
|
+
for (let i = 0; i < n; i++) sharp[i] = Math.log1p(Math.sqrt(energy[i]) * 400);
|
|
4192
|
+
const sorted = Float32Array.from(sharp).sort();
|
|
4193
|
+
const top = sorted[Math.floor(n * 0.995)] || 1;
|
|
4194
|
+
for (let i = 0; i < n; i++) sharp[i] = Math.min(1, sharp[i] / top);
|
|
4195
|
+
const ones = new Float32Array(n).fill(1);
|
|
4196
|
+
const otsu = otsuThreshold(sharp, ones);
|
|
4197
|
+
const range = Math.min(100, Math.max(0, options.range ?? 50));
|
|
4198
|
+
const t = Math.min(0.98, otsu * Math.pow(2, (50 - range) / 25));
|
|
4199
|
+
let mask = new Float32Array(n);
|
|
4200
|
+
for (let i = 0; i < n; i++) mask[i] = sharp[i] >= t ? 1 : 0;
|
|
4201
|
+
const c = 2 * r;
|
|
4202
|
+
const grown = boxMean(mask, w, h, c);
|
|
4203
|
+
for (let i = 0; i < n; i++) grown[i] = grown[i] > 1e-3 ? 1 : 0;
|
|
4204
|
+
const shrunk = boxMean(grown, w, h, c);
|
|
4205
|
+
for (let i = 0; i < n; i++) mask[i] = shrunk[i] >= 0.999 ? 1 : 0;
|
|
4206
|
+
const pulled = boxMean(mask, w, h, Math.max(1, Math.round(r / 2)));
|
|
4207
|
+
for (let i = 0; i < n; i++) mask[i] = pulled[i] >= 0.999 ? 1 : 0;
|
|
4208
|
+
const softness = Math.max(0, options.softness ?? 4) * scale;
|
|
4209
|
+
if (softness > 0) {
|
|
4210
|
+
const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
|
|
4211
|
+
for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
|
|
4212
|
+
}
|
|
4213
|
+
return { selection: softMaskToSelection(work, mask), noiseLevel: Math.round(sigma * 255 * 10) / 10 };
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// src/image-editor/selection/segment/sky.ts
|
|
4217
|
+
var SKY_SIZE = 768;
|
|
4218
|
+
function lab(r, g, b) {
|
|
4219
|
+
const lin = (v) => {
|
|
4220
|
+
v /= 255;
|
|
4221
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
4222
|
+
};
|
|
4223
|
+
const R = lin(r);
|
|
4224
|
+
const G = lin(g);
|
|
4225
|
+
const B = lin(b);
|
|
4226
|
+
const f = (t) => t > 8856e-6 ? Math.cbrt(t) : 7.787 * t + 16 / 116;
|
|
4227
|
+
const x = f((0.4124 * R + 0.3576 * G + 0.1805 * B) / 0.95047);
|
|
4228
|
+
const y = f(0.2126 * R + 0.7152 * G + 0.0722 * B);
|
|
4229
|
+
const z = f((0.0193 * R + 0.1192 * G + 0.9505 * B) / 1.08883);
|
|
4230
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
4231
|
+
}
|
|
4232
|
+
var sigmoid = (v) => 1 / (1 + Math.exp(-v));
|
|
4233
|
+
function findSky(pixels, options = {}) {
|
|
4234
|
+
const work = workImage(pixels, SKY_SIZE);
|
|
4235
|
+
const { width: w, height: h, rgb, scale } = work;
|
|
4236
|
+
const n = w * h;
|
|
4237
|
+
const L = new Float32Array(n);
|
|
4238
|
+
const A = new Float32Array(n);
|
|
4239
|
+
const B = new Float32Array(n);
|
|
4240
|
+
for (let i = 0; i < n; i++) {
|
|
4241
|
+
const [l, a, b] = lab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]);
|
|
4242
|
+
L[i] = l;
|
|
4243
|
+
A[i] = a;
|
|
4244
|
+
B[i] = b;
|
|
4245
|
+
}
|
|
4246
|
+
const luma2 = boxMean(workLuma(work), w, h, 1);
|
|
4247
|
+
const grad = new Float32Array(n);
|
|
4248
|
+
for (let y = 0; y < h; y++) {
|
|
4249
|
+
for (let x = 0; x < w; x++) {
|
|
4250
|
+
const i = y * w + x;
|
|
4251
|
+
const gx = x + 1 < w ? luma2[i + 1] - luma2[i] : 0;
|
|
4252
|
+
const gy = y + 1 < h ? luma2[i + w] - luma2[i] : 0;
|
|
4253
|
+
grad[i] = Math.hypot(gx, gy);
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
const roughR = Math.max(1, Math.round(6e-3 * Math.max(w, h)));
|
|
4257
|
+
const rough = boxMean(grad, w, h, roughR);
|
|
4258
|
+
const score = new Float32Array(n);
|
|
4259
|
+
for (let i = 0; i < n; i++) {
|
|
4260
|
+
const chroma = Math.hypot(A[i], B[i]);
|
|
4261
|
+
const blue = sigmoid((-B[i] - 6) / 4) * sigmoid((L[i] - 35) / 6);
|
|
4262
|
+
const grey = sigmoid((L[i] - 68) / 5) * sigmoid((18 - chroma) / 4);
|
|
4263
|
+
const smooth = Math.exp(-rough[i] / 0.02);
|
|
4264
|
+
const y = Math.floor(i / w) / h;
|
|
4265
|
+
const high = 1 - 0.35 * y;
|
|
4266
|
+
score[i] = Math.max(blue, grey) * smooth * high;
|
|
4267
|
+
}
|
|
4268
|
+
const threshold = Math.min(100, Math.max(0, options.threshold ?? 50));
|
|
4269
|
+
const t = 0.55 - (threshold - 50) * 6e-3;
|
|
4270
|
+
const sky = new Uint8Array(n);
|
|
4271
|
+
const stack = [];
|
|
4272
|
+
const seedRows = Math.max(1, Math.round(h * 0.04));
|
|
4273
|
+
for (let y = 0; y < seedRows; y++) {
|
|
4274
|
+
for (let x = 0; x < w; x++) {
|
|
4275
|
+
const i = y * w + x;
|
|
4276
|
+
if (score[i] >= t) {
|
|
4277
|
+
sky[i] = 1;
|
|
4278
|
+
stack.push(i);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
if (!stack.length) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
|
|
4283
|
+
const low = t * 0.7;
|
|
4284
|
+
while (stack.length) {
|
|
4285
|
+
const p = stack.pop();
|
|
4286
|
+
const x = p % w;
|
|
4287
|
+
for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
|
|
4288
|
+
if (q < 0 || sky[q] || score[q] < low) continue;
|
|
4289
|
+
if (Math.hypot(L[q] - L[p], A[q] - A[p], B[q] - B[p]) > 6) continue;
|
|
4290
|
+
sky[q] = 1;
|
|
4291
|
+
stack.push(q);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
let count = 0;
|
|
4295
|
+
const mean = [0, 0, 0];
|
|
4296
|
+
for (let i = 0; i < n; i++) {
|
|
4297
|
+
if (!sky[i]) continue;
|
|
4298
|
+
mean[0] += L[i];
|
|
4299
|
+
mean[1] += A[i];
|
|
4300
|
+
mean[2] += B[i];
|
|
4301
|
+
count++;
|
|
4302
|
+
}
|
|
4303
|
+
for (let k = 0; k < 3; k++) mean[k] /= count;
|
|
4304
|
+
let spread = 0;
|
|
4305
|
+
for (let i = 0; i < n; i++) if (sky[i]) spread += (L[i] - mean[0]) ** 2 + (A[i] - mean[1]) ** 2 + (B[i] - mean[2]) ** 2;
|
|
4306
|
+
const limit = Math.max(8, 2.5 * Math.sqrt(spread / count));
|
|
4307
|
+
for (let i = 0; i < n; i++) {
|
|
4308
|
+
if (sky[i] || score[i] < t) continue;
|
|
4309
|
+
if (Math.hypot(L[i] - mean[0], A[i] - mean[1], B[i] - mean[2]) <= limit) sky[i] = 1;
|
|
4310
|
+
}
|
|
4311
|
+
let front = [];
|
|
4312
|
+
for (let i = 0; i < n; i++) if (sky[i]) front.push(i);
|
|
4313
|
+
for (let step = 0; step <= roughR + 1 && front.length; step++) {
|
|
4314
|
+
const next = [];
|
|
4315
|
+
for (const p of front) {
|
|
4316
|
+
const x = p % w;
|
|
4317
|
+
for (const q of [x + 1 < w ? p + 1 : -1, x > 0 ? p - 1 : -1, p + w < n ? p + w : -1, p - w]) {
|
|
4318
|
+
if (q < 0 || sky[q]) continue;
|
|
4319
|
+
if (Math.hypot(L[q] - mean[0], A[q] - mean[1], B[q] - mean[2]) > limit) continue;
|
|
4320
|
+
sky[q] = 1;
|
|
4321
|
+
next.push(q);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
front = next;
|
|
4325
|
+
}
|
|
4326
|
+
if (count < n * 5e-3) return { selection: createSelectionMask(pixels.width, pixels.height), reason: "No sky was found along the top of the image." };
|
|
4327
|
+
let mask = Float32Array.from(sky);
|
|
4328
|
+
const softness = Math.max(0, options.softness ?? 4) * scale;
|
|
4329
|
+
if (softness > 0) {
|
|
4330
|
+
const fitted = guidedFilterColor(workPlanes(work), mask, w, h, Math.max(1, Math.round(softness * 2)), 1e-3);
|
|
4331
|
+
mask = new Float32Array(n);
|
|
4332
|
+
for (let i = 0; i < n; i++) mask[i] = Math.min(1, Math.max(0, fitted[i]));
|
|
4333
|
+
}
|
|
4334
|
+
return { selection: softMaskToSelection(work, mask) };
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
// src/image-editor/layers/shape-type-ui.ts
|
|
4338
|
+
var DEFAULT_SHAPE_TOOL_OPTIONS = {
|
|
4339
|
+
kind: "rect",
|
|
4340
|
+
fill: "#4f46e5",
|
|
4341
|
+
stroke: null,
|
|
4342
|
+
strokeWidth: 2,
|
|
4343
|
+
radius: 0,
|
|
4344
|
+
sides: 6,
|
|
4345
|
+
points: 5,
|
|
4346
|
+
innerRatio: 0.5,
|
|
4347
|
+
weight: 4,
|
|
4348
|
+
arrow: false
|
|
4349
|
+
};
|
|
4350
|
+
var SHAPE_KIND_LABELS = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
|
|
4351
|
+
var DEFAULT_TYPE_TOOL_OPTIONS = { fontFamily: "sans-serif", fontSize: 32, fontWeight: 400, italic: false, color: "#111827", align: "left" };
|
|
4352
|
+
function liveShapeForDrag(o, a, b, mods = {}) {
|
|
4353
|
+
if (o.kind === "line") {
|
|
4354
|
+
let x2 = b.x;
|
|
4355
|
+
let y2 = b.y;
|
|
4356
|
+
if (mods.shift) {
|
|
4357
|
+
const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);
|
|
4358
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
4359
|
+
x2 = a.x + Math.cos(ang) * len;
|
|
4360
|
+
y2 = a.y + Math.sin(ang) * len;
|
|
4361
|
+
}
|
|
4362
|
+
if (Math.hypot(x2 - a.x, y2 - a.y) < 1) return null;
|
|
4363
|
+
const head = o.arrow ? { endArrow: { width: o.weight * 4, length: o.weight * 4 } } : {};
|
|
4364
|
+
return { kind: "line", x1: a.x, y1: a.y, x2, y2, weight: o.weight, ...head };
|
|
4365
|
+
}
|
|
4366
|
+
let w = b.x - a.x;
|
|
4367
|
+
let h = b.y - a.y;
|
|
4368
|
+
if (mods.shift) {
|
|
4369
|
+
const s = Math.max(Math.abs(w), Math.abs(h));
|
|
4370
|
+
w = Math.sign(w || 1) * s;
|
|
4371
|
+
h = Math.sign(h || 1) * s;
|
|
4372
|
+
}
|
|
4373
|
+
let x = a.x;
|
|
4374
|
+
let y = a.y;
|
|
4375
|
+
if (mods.alt) {
|
|
4376
|
+
x = a.x - w;
|
|
4377
|
+
y = a.y - h;
|
|
4378
|
+
w *= 2;
|
|
4379
|
+
h *= 2;
|
|
4380
|
+
}
|
|
4381
|
+
const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };
|
|
4382
|
+
if (box.width < 1 || box.height < 1) return null;
|
|
4383
|
+
switch (o.kind) {
|
|
4384
|
+
case "rect":
|
|
4385
|
+
return { kind: "rect", ...box, ...o.radius > 0 ? { radius: o.radius } : {} };
|
|
4386
|
+
case "ellipse":
|
|
4387
|
+
return { kind: "ellipse", ...box };
|
|
4388
|
+
case "polygon":
|
|
4389
|
+
return { kind: "polygon", ...box, sides: o.sides };
|
|
4390
|
+
case "star":
|
|
4391
|
+
return { kind: "star", ...box, points: o.points, innerRatio: o.innerRatio };
|
|
4392
|
+
}
|
|
4393
|
+
return null;
|
|
4394
|
+
}
|
|
4395
|
+
var ShapeDrawGesture = class {
|
|
4396
|
+
constructor(controller, options) {
|
|
4397
|
+
this.controller = controller;
|
|
4398
|
+
this.options = options;
|
|
4399
|
+
}
|
|
4400
|
+
controller;
|
|
4401
|
+
options;
|
|
4402
|
+
start = null;
|
|
4403
|
+
layerId = null;
|
|
4404
|
+
error = null;
|
|
4405
|
+
get active() {
|
|
4406
|
+
return this.start !== null;
|
|
4407
|
+
}
|
|
4408
|
+
/** The shape layer drawn by the last drag (until the next one starts). */
|
|
4409
|
+
get drawnLayerId() {
|
|
4410
|
+
return this.layerId;
|
|
4411
|
+
}
|
|
4412
|
+
down(p) {
|
|
4413
|
+
this.error = null;
|
|
4414
|
+
this.start = { x: p.x, y: p.y };
|
|
4415
|
+
this.layerId = null;
|
|
4416
|
+
}
|
|
4417
|
+
move(p, mods = {}) {
|
|
4418
|
+
if (!this.start) return;
|
|
4419
|
+
const o = this.options();
|
|
4420
|
+
const shape = liveShapeForDrag(o, this.start, p, mods);
|
|
4421
|
+
if (!shape) return;
|
|
4422
|
+
try {
|
|
4423
|
+
if (!this.layerId) {
|
|
4424
|
+
const isLine = o.kind === "line";
|
|
4425
|
+
this.layerId = this.controller.addShapeLayer({
|
|
4426
|
+
shape,
|
|
4427
|
+
fill: isLine ? o.stroke ?? o.fill ?? "#000000" : o.fill,
|
|
4428
|
+
stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null
|
|
4429
|
+
});
|
|
4430
|
+
} else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);
|
|
4431
|
+
} catch (e) {
|
|
4432
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
3974
4433
|
this.start = null;
|
|
3975
4434
|
}
|
|
3976
4435
|
}
|
|
@@ -4160,876 +4619,644 @@ var TextEditSession = class {
|
|
|
4160
4619
|
case "Home":
|
|
4161
4620
|
this.move("home", e.shiftKey);
|
|
4162
4621
|
return true;
|
|
4163
|
-
case "End":
|
|
4164
|
-
this.move("end", e.shiftKey);
|
|
4165
|
-
return true;
|
|
4166
|
-
case "Tab":
|
|
4167
|
-
this.insert(" ");
|
|
4168
|
-
return true;
|
|
4169
|
-
}
|
|
4170
|
-
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
4171
|
-
this.insert(e.key);
|
|
4172
|
-
return true;
|
|
4173
|
-
}
|
|
4174
|
-
return false;
|
|
4175
|
-
}
|
|
4176
|
-
/** Sets character style fields on the selection, or on the whole text when nothing is selected. */
|
|
4177
|
-
setStyle(style) {
|
|
4178
|
-
const s = this.start;
|
|
4179
|
-
const e = this.end;
|
|
4180
|
-
const range = s === e ? {} : { start: s, end: e };
|
|
4181
|
-
this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
|
|
4182
|
-
}
|
|
4183
|
-
/** Sets paragraph fields on the paragraphs the selection touches. */
|
|
4184
|
-
setParagraph(style) {
|
|
4185
|
-
const s = this.start;
|
|
4186
|
-
const e = this.end;
|
|
4187
|
-
this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
|
|
4188
|
-
}
|
|
4189
|
-
/** The style at the caret (or the selection's start), for the options bar. */
|
|
4190
|
-
styleHere() {
|
|
4191
|
-
const layer = this.layer;
|
|
4192
|
-
const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
|
|
4193
|
-
return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
|
|
4194
|
-
}
|
|
4195
|
-
info() {
|
|
4196
|
-
return this.controller.getTextLayer(this.layerId);
|
|
4197
|
-
}
|
|
4198
|
-
/** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
|
|
4199
|
-
overlay() {
|
|
4200
|
-
const layer = this.layer;
|
|
4201
|
-
const layout = this.layout();
|
|
4202
|
-
const m = textMatrix(layer);
|
|
4203
|
-
const at = (x, y) => applyMatrix(m, { x, y });
|
|
4204
|
-
let frame = null;
|
|
4205
|
-
if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
|
|
4206
|
-
else if (layout) {
|
|
4207
|
-
const b = layout.bounds;
|
|
4208
|
-
frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
|
|
4209
|
-
}
|
|
4210
|
-
if (!layout) return { caret: null, selection: [], frame };
|
|
4211
|
-
const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
|
|
4212
|
-
const c = caretAt(layout, this.focus);
|
|
4213
|
-
return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
|
|
4214
|
-
}
|
|
4215
|
-
/** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
|
|
4216
|
-
close() {
|
|
4217
|
-
if (!this.alive) return false;
|
|
4218
|
-
if (!this.text.trim().length) {
|
|
4219
|
-
try {
|
|
4220
|
-
this.controller.removeLayer(this.layerId);
|
|
4221
|
-
} catch {
|
|
4222
|
-
}
|
|
4223
|
-
return false;
|
|
4224
|
-
}
|
|
4225
|
-
if (this.layer.name === "Text" && this.changed) {
|
|
4226
|
-
try {
|
|
4227
|
-
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
4228
|
-
} catch {
|
|
4229
|
-
}
|
|
4230
|
-
}
|
|
4231
|
-
return true;
|
|
4232
|
-
}
|
|
4233
|
-
};
|
|
4234
|
-
function typeLayerAt(controller, p, slop = 4) {
|
|
4235
|
-
const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
|
|
4236
|
-
for (const l of layers) {
|
|
4237
|
-
if (!l.visible) continue;
|
|
4238
|
-
const inv = invert(textMatrix(l));
|
|
4239
|
-
const q = applyMatrix(inv, p);
|
|
4240
|
-
let b = null;
|
|
4241
|
-
if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
|
|
4242
|
-
else {
|
|
4243
|
-
const layout = controller.getTextLayout(l.id);
|
|
4244
|
-
if (layout) b = layout.bounds;
|
|
4245
|
-
}
|
|
4246
|
-
if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
|
|
4247
|
-
}
|
|
4248
|
-
return null;
|
|
4249
|
-
}
|
|
4250
|
-
function flattenTypeLayers(layers) {
|
|
4251
|
-
const out = [];
|
|
4252
|
-
const walk = (list) => {
|
|
4253
|
-
for (const l of list) {
|
|
4254
|
-
if (l.type === "type") out.push(l);
|
|
4255
|
-
else if (l.type === "group") walk(l.children);
|
|
4256
|
-
}
|
|
4257
|
-
};
|
|
4258
|
-
walk(layers);
|
|
4259
|
-
return out;
|
|
4260
|
-
}
|
|
4261
|
-
function invert(m) {
|
|
4262
|
-
const [a, b, c, d, e, f] = m;
|
|
4263
|
-
const det = a * d - b * c || 1;
|
|
4264
|
-
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
4265
|
-
}
|
|
4266
|
-
var TypeToolGesture = class {
|
|
4267
|
-
constructor(controller, options) {
|
|
4268
|
-
this.controller = controller;
|
|
4269
|
-
this.options = options;
|
|
4270
|
-
}
|
|
4271
|
-
controller;
|
|
4272
|
-
options;
|
|
4273
|
-
session = null;
|
|
4274
|
-
dragStart = null;
|
|
4275
|
-
error = null;
|
|
4276
|
-
down(p, mods = {}) {
|
|
4277
|
-
this.error = null;
|
|
4278
|
-
const hit = typeLayerAt(this.controller, p);
|
|
4279
|
-
if (this.session && this.session.alive && hit === this.session.layerId) {
|
|
4280
|
-
this.session.pointAt(p, mods.shift);
|
|
4281
|
-
this.dragStart = null;
|
|
4282
|
-
return;
|
|
4283
|
-
}
|
|
4284
|
-
this.finish();
|
|
4285
|
-
if (hit) {
|
|
4286
|
-
this.controller.setActiveLayer(hit);
|
|
4287
|
-
this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
|
|
4288
|
-
this.dragStart = null;
|
|
4289
|
-
return;
|
|
4290
|
-
}
|
|
4291
|
-
this.dragStart = { x: p.x, y: p.y };
|
|
4292
|
-
}
|
|
4293
|
-
/** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
|
|
4294
|
-
up(p) {
|
|
4295
|
-
const start = this.dragStart;
|
|
4296
|
-
this.dragStart = null;
|
|
4297
|
-
if (!start) return this.session;
|
|
4298
|
-
const o = this.options();
|
|
4299
|
-
const w = Math.abs(p.x - start.x);
|
|
4300
|
-
const h = Math.abs(p.y - start.y);
|
|
4301
|
-
try {
|
|
4302
|
-
const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
|
|
4303
|
-
const id = this.controller.addTextLayer({
|
|
4304
|
-
text: "",
|
|
4305
|
-
x: box ? Math.min(start.x, p.x) : start.x,
|
|
4306
|
-
y: box ? Math.min(start.y, p.y) : start.y,
|
|
4307
|
-
...box ? { box } : {},
|
|
4308
|
-
style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
|
|
4309
|
-
paragraph: o.align !== "left" ? { align: o.align } : {},
|
|
4310
|
-
...o.orientation === "vertical" ? { orientation: "vertical" } : {},
|
|
4311
|
-
name: "Text"
|
|
4312
|
-
});
|
|
4313
|
-
this.session = new TextEditSession(this.controller, id, 0);
|
|
4314
|
-
} catch (e) {
|
|
4315
|
-
this.error = e instanceof Error ? e.message : String(e);
|
|
4316
|
-
}
|
|
4317
|
-
return this.session;
|
|
4318
|
-
}
|
|
4319
|
-
/** Ends the open session (an empty new layer is removed). */
|
|
4320
|
-
finish() {
|
|
4321
|
-
const s = this.session;
|
|
4322
|
-
this.session = null;
|
|
4323
|
-
s?.close();
|
|
4324
|
-
}
|
|
4325
|
-
};
|
|
4326
|
-
|
|
4327
|
-
// src/image-editor/selection/shapes.ts
|
|
4328
|
-
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
4329
|
-
const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
|
|
4330
|
-
if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
|
|
4331
|
-
const m = new Float32Array(width * height);
|
|
4332
|
-
for (let y = 0; y < box.height; y++) m.set(coverage.subarray(y * box.width, (y + 1) * box.width), (box.y + y) * width + box.x);
|
|
4333
|
-
return m;
|
|
4334
|
-
}
|
|
4335
|
-
function polygonCoverageBox(points, width, height, antiAlias = true) {
|
|
4336
|
-
const none = { box: null, coverage: new Float32Array(0) };
|
|
4337
|
-
if (points.length < 3 || width <= 0 || height <= 0) return none;
|
|
4338
|
-
let minX = Infinity;
|
|
4339
|
-
let minY = Infinity;
|
|
4340
|
-
let maxX = -Infinity;
|
|
4341
|
-
let maxY = -Infinity;
|
|
4342
|
-
for (const p of points) {
|
|
4343
|
-
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
|
4344
|
-
minX = Math.min(minX, p.x);
|
|
4345
|
-
minY = Math.min(minY, p.y);
|
|
4346
|
-
maxX = Math.max(maxX, p.x);
|
|
4347
|
-
maxY = Math.max(maxY, p.y);
|
|
4348
|
-
}
|
|
4349
|
-
const bx0 = Math.max(0, Math.floor(minX));
|
|
4350
|
-
const by0 = Math.max(0, Math.floor(minY));
|
|
4351
|
-
const bx1 = Math.min(width, Math.ceil(maxX));
|
|
4352
|
-
const by1 = Math.min(height, Math.ceil(maxY));
|
|
4353
|
-
if (!(bx1 > bx0 && by1 > by0)) return none;
|
|
4354
|
-
const bw = bx1 - bx0;
|
|
4355
|
-
const m = new Float32Array(bw * (by1 - by0));
|
|
4356
|
-
const sub2 = antiAlias ? 4 : 1;
|
|
4357
|
-
const edges = [];
|
|
4358
|
-
for (let i = 0; i < points.length; i++) {
|
|
4359
|
-
const a = points[i];
|
|
4360
|
-
const b = points[(i + 1) % points.length];
|
|
4361
|
-
if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;
|
|
4362
|
-
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 });
|
|
4363
|
-
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 });
|
|
4364
|
-
}
|
|
4365
|
-
edges.sort((p, q) => p.top - q.top);
|
|
4366
|
-
let next = 0;
|
|
4367
|
-
let active = [];
|
|
4368
|
-
const xs = [];
|
|
4369
|
-
for (let py = by0; py < by1; py++) {
|
|
4370
|
-
const row = (py - by0) * bw - bx0;
|
|
4371
|
-
for (let s = 0; s < sub2; s++) {
|
|
4372
|
-
const y = py + (s + 0.5) / sub2;
|
|
4373
|
-
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
4374
|
-
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
4375
|
-
if (!active.length) continue;
|
|
4376
|
-
xs.length = 0;
|
|
4377
|
-
for (const e of active) {
|
|
4378
|
-
if (e.top <= y) xs.push(e.ax + (y - e.ay) / (e.by - e.ay) * (e.bx - e.ax));
|
|
4379
|
-
}
|
|
4380
|
-
xs.sort((a, b) => a - b);
|
|
4381
|
-
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
4382
|
-
const x0 = Math.max(0, xs[k]);
|
|
4383
|
-
const x1 = Math.min(width, xs[k + 1]);
|
|
4384
|
-
if (x1 <= x0) continue;
|
|
4385
|
-
const c0 = Math.floor(x0);
|
|
4386
|
-
const c1 = Math.min(width, Math.ceil(x1));
|
|
4387
|
-
for (let cx = c0; cx < c1; cx++) {
|
|
4388
|
-
let cov;
|
|
4389
|
-
if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);
|
|
4390
|
-
else {
|
|
4391
|
-
const c = cx + 0.5;
|
|
4392
|
-
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
4393
|
-
}
|
|
4394
|
-
if (cov > 0) m[row + cx] += cov / sub2;
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
}
|
|
4399
|
-
for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
|
|
4400
|
-
return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
|
|
4401
|
-
}
|
|
4402
|
-
function polygonSelection(width, height, points, options = {}) {
|
|
4403
|
-
const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
|
|
4404
|
-
if (!box) return createSelectionMask(width, height);
|
|
4405
|
-
const out = new Uint8Array(coverage.length);
|
|
4406
|
-
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
4407
|
-
return selectionFromRegion(width, height, box, out);
|
|
4408
|
-
}
|
|
4409
|
-
function normalizeRect(rect) {
|
|
4410
|
-
const x = rect.width < 0 ? rect.x + rect.width : rect.x;
|
|
4411
|
-
const y = rect.height < 0 ? rect.y + rect.height : rect.y;
|
|
4412
|
-
return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };
|
|
4413
|
-
}
|
|
4414
|
-
function rectSelection(width, height, rect, options = {}) {
|
|
4415
|
-
const r = normalizeRect(rect);
|
|
4416
|
-
const pts = [
|
|
4417
|
-
{ x: r.x, y: r.y },
|
|
4418
|
-
{ x: r.x + r.width, y: r.y },
|
|
4419
|
-
{ x: r.x + r.width, y: r.y + r.height },
|
|
4420
|
-
{ x: r.x, y: r.y + r.height }
|
|
4421
|
-
];
|
|
4422
|
-
return polygonSelection(width, height, pts, options);
|
|
4423
|
-
}
|
|
4424
|
-
function ellipsePoints(rect) {
|
|
4425
|
-
const r = normalizeRect(rect);
|
|
4426
|
-
const rx = r.width / 2;
|
|
4427
|
-
const ry = r.height / 2;
|
|
4428
|
-
const cx = r.x + rx;
|
|
4429
|
-
const cy = r.y + ry;
|
|
4430
|
-
const rmax = Math.max(rx, ry, 0.5);
|
|
4431
|
-
const step = Math.sqrt(0.4 / rmax);
|
|
4432
|
-
const n = Math.min(4096, Math.max(16, Math.ceil(2 * Math.PI / step)));
|
|
4433
|
-
const pts = [];
|
|
4434
|
-
for (let i = 0; i < n; i++) {
|
|
4435
|
-
const t = i / n * 2 * Math.PI;
|
|
4436
|
-
pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
|
|
4437
|
-
}
|
|
4438
|
-
return pts;
|
|
4439
|
-
}
|
|
4440
|
-
function ellipseSelection(width, height, rect, options = {}) {
|
|
4441
|
-
return polygonSelection(width, height, ellipsePoints(rect), options);
|
|
4442
|
-
}
|
|
4443
|
-
|
|
4444
|
-
// src/image-editor/selection/wand.ts
|
|
4445
|
-
function checkPixels(p) {
|
|
4446
|
-
const n = p.width * p.height;
|
|
4447
|
-
if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
|
|
4448
|
-
throw new Error("Invalid pixel buffer size");
|
|
4449
|
-
}
|
|
4450
|
-
if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
|
|
4451
|
-
return n;
|
|
4452
|
-
}
|
|
4453
|
-
var SRGB_TO_LINEAR2 = (() => {
|
|
4454
|
-
const t = new Float32Array(256);
|
|
4455
|
-
for (let i = 0; i < 256; i++) {
|
|
4456
|
-
const v = i / 255;
|
|
4457
|
-
t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
4458
|
-
}
|
|
4459
|
-
return t;
|
|
4460
|
-
})();
|
|
4461
|
-
var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
|
|
4462
|
-
function srgb8ToLab(r, g, b) {
|
|
4463
|
-
const rl = SRGB_TO_LINEAR2[r & 255];
|
|
4464
|
-
const gl = SRGB_TO_LINEAR2[g & 255];
|
|
4465
|
-
const bl = SRGB_TO_LINEAR2[b & 255];
|
|
4466
|
-
const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
|
|
4467
|
-
const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
|
|
4468
|
-
const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
|
|
4469
|
-
const fx = labF(x);
|
|
4470
|
-
const fy = labF(y);
|
|
4471
|
-
const fz = labF(z);
|
|
4472
|
-
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
4473
|
-
}
|
|
4474
|
-
function similarity(pixels, seed, tolerance, distance) {
|
|
4475
|
-
const d = pixels.data;
|
|
4476
|
-
const tr = d[seed * 4];
|
|
4477
|
-
const tg = d[seed * 4 + 1];
|
|
4478
|
-
const tb = d[seed * 4 + 2];
|
|
4479
|
-
const ta = d[seed * 4 + 3];
|
|
4480
|
-
const tol = Math.max(0, Math.floor(tolerance + 1e-3));
|
|
4481
|
-
if (distance === "perceptual") {
|
|
4482
|
-
const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
|
|
4483
|
-
const de = Math.max(0, tolerance) * 100 / 255;
|
|
4484
|
-
const de2 = de * de + 1e-6;
|
|
4485
|
-
return (i) => {
|
|
4486
|
-
const p = i * 4;
|
|
4487
|
-
const a = d[p + 3];
|
|
4488
|
-
if (ta === 0 && a === 0) return true;
|
|
4489
|
-
if (Math.abs(a - ta) > tol) return false;
|
|
4490
|
-
const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
|
|
4491
|
-
return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
|
|
4492
|
-
};
|
|
4493
|
-
}
|
|
4494
|
-
return (i) => {
|
|
4495
|
-
const p = i * 4;
|
|
4496
|
-
const a = d[p + 3];
|
|
4497
|
-
if (ta === 0 && a === 0) return true;
|
|
4498
|
-
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;
|
|
4499
|
-
};
|
|
4500
|
-
}
|
|
4501
|
-
function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
|
|
4502
|
-
const out = marks.slice();
|
|
4503
|
-
for (let y = y0; y < y1; y++) {
|
|
4504
|
-
for (let x = x0; x < x1; x++) {
|
|
4505
|
-
const i = y * w + x;
|
|
4506
|
-
const v = marks[i];
|
|
4507
|
-
let sum = 0;
|
|
4508
|
-
let edge = false;
|
|
4509
|
-
for (let dy = -1; dy <= 1; dy++) {
|
|
4510
|
-
const ny = y + dy;
|
|
4511
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
4512
|
-
const nx = x + dx;
|
|
4513
|
-
const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
|
|
4514
|
-
if (n !== v) edge = true;
|
|
4515
|
-
sum += n;
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
if (edge) {
|
|
4519
|
-
const avg = sum / (9 * 255);
|
|
4520
|
-
out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
|
|
4521
|
-
}
|
|
4522
|
-
}
|
|
4523
|
-
}
|
|
4524
|
-
return out;
|
|
4525
|
-
}
|
|
4526
|
-
function magicWand(pixels, seed, options = {}) {
|
|
4527
|
-
const n = checkPixels(pixels);
|
|
4528
|
-
const { width: w, height: h } = pixels;
|
|
4529
|
-
const sx = Math.floor(seed.x);
|
|
4530
|
-
const sy = Math.floor(seed.y);
|
|
4531
|
-
if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
|
|
4532
|
-
const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
|
|
4533
|
-
const marks = new Uint8Array(n);
|
|
4534
|
-
let bx0 = w;
|
|
4535
|
-
let by0 = h;
|
|
4536
|
-
let bx1 = -1;
|
|
4537
|
-
let by1 = -1;
|
|
4538
|
-
if (options.contiguous ?? true) {
|
|
4539
|
-
const stack = [sy * w + sx];
|
|
4540
|
-
while (stack.length) {
|
|
4541
|
-
const idx = stack.pop();
|
|
4542
|
-
if (marks[idx]) continue;
|
|
4543
|
-
const y = idx / w | 0;
|
|
4544
|
-
const row = y * w;
|
|
4545
|
-
let l = idx - row;
|
|
4546
|
-
let r = l;
|
|
4547
|
-
while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
|
|
4548
|
-
while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
|
|
4549
|
-
marks.fill(255, row + l, row + r + 1);
|
|
4550
|
-
if (l < bx0) bx0 = l;
|
|
4551
|
-
if (r > bx1) bx1 = r;
|
|
4552
|
-
if (y < by0) by0 = y;
|
|
4553
|
-
if (y > by1) by1 = y;
|
|
4554
|
-
for (const ny of [y - 1, y + 1]) {
|
|
4555
|
-
if (ny < 0 || ny >= h) continue;
|
|
4556
|
-
const nrow = ny * w;
|
|
4557
|
-
let inRun = false;
|
|
4558
|
-
for (let nx = l; nx <= r; nx++) {
|
|
4559
|
-
const ok = !marks[nrow + nx] && similar(nrow + nx);
|
|
4560
|
-
if (ok && !inRun) stack.push(nrow + nx);
|
|
4561
|
-
inRun = ok;
|
|
4562
|
-
}
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
} else {
|
|
4566
|
-
for (let y = 0; y < h; y++) {
|
|
4567
|
-
const row = y * w;
|
|
4568
|
-
for (let x = 0; x < w; x++) {
|
|
4569
|
-
if (similar(row + x)) {
|
|
4570
|
-
marks[row + x] = 255;
|
|
4571
|
-
if (x < bx0) bx0 = x;
|
|
4572
|
-
if (x > bx1) bx1 = x;
|
|
4573
|
-
if (y < by0) by0 = y;
|
|
4574
|
-
if (y > by1) by1 = y;
|
|
4575
|
-
}
|
|
4576
|
-
}
|
|
4622
|
+
case "End":
|
|
4623
|
+
this.move("end", e.shiftKey);
|
|
4624
|
+
return true;
|
|
4625
|
+
case "Tab":
|
|
4626
|
+
this.insert(" ");
|
|
4627
|
+
return true;
|
|
4628
|
+
}
|
|
4629
|
+
if (!mod2 && !e.altKey && e.key.length >= 1 && [...e.key].length === 1) {
|
|
4630
|
+
this.insert(e.key);
|
|
4631
|
+
return true;
|
|
4577
4632
|
}
|
|
4633
|
+
return false;
|
|
4578
4634
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
);
|
|
4586
|
-
}
|
|
4587
|
-
function sampleColor(c) {
|
|
4588
|
-
if (typeof c === "string") {
|
|
4589
|
-
const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
|
|
4590
|
-
if (!m) throw new Error(`Invalid colour "${c}"`);
|
|
4591
|
-
const v = parseInt(m[1], 16);
|
|
4592
|
-
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
4635
|
+
/** Sets character style fields on the selection, or on the whole text when nothing is selected. */
|
|
4636
|
+
setStyle(style) {
|
|
4637
|
+
const s = this.start;
|
|
4638
|
+
const e = this.end;
|
|
4639
|
+
const range = s === e ? {} : { start: s, end: e };
|
|
4640
|
+
this.run((commit) => this.controller.setTextStyle(this.layerId, style, range, commit));
|
|
4593
4641
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4642
|
+
/** Sets paragraph fields on the paragraphs the selection touches. */
|
|
4643
|
+
setParagraph(style) {
|
|
4644
|
+
const s = this.start;
|
|
4645
|
+
const e = this.end;
|
|
4646
|
+
this.run((commit) => this.controller.setParagraphStyle(this.layerId, style, { start: s, end: e }, commit));
|
|
4647
|
+
}
|
|
4648
|
+
/** The style at the caret (or the selection's start), for the options bar. */
|
|
4649
|
+
styleHere() {
|
|
4650
|
+
const layer = this.layer;
|
|
4651
|
+
const at = this.start > 0 && this.start === this.end ? this.start - 1 : this.start;
|
|
4652
|
+
return styleAt(layer.runs, at, DEFAULT_TEXT_STYLE);
|
|
4653
|
+
}
|
|
4654
|
+
info() {
|
|
4655
|
+
return this.controller.getTextLayer(this.layerId);
|
|
4656
|
+
}
|
|
4657
|
+
/** The caret, the selection and the frame in natural pixels, for `RenderOptions.textEdit`. */
|
|
4658
|
+
overlay() {
|
|
4659
|
+
const layer = this.layer;
|
|
4660
|
+
const layout = this.layout();
|
|
4661
|
+
const m = textMatrix(layer);
|
|
4662
|
+
const at = (x, y) => applyMatrix(m, { x, y });
|
|
4663
|
+
let frame = null;
|
|
4664
|
+
if (layer.frame.kind === "box") frame = [at(0, 0), at(layer.frame.width, 0), at(layer.frame.width, layer.frame.height), at(0, layer.frame.height)];
|
|
4665
|
+
else if (layout) {
|
|
4666
|
+
const b = layout.bounds;
|
|
4667
|
+
frame = [at(b.x, b.y), at(b.x + b.width, b.y), at(b.x + b.width, b.y + b.height), at(b.x, b.y + b.height)];
|
|
4668
|
+
}
|
|
4669
|
+
if (!layout) return { caret: null, selection: [], frame };
|
|
4670
|
+
const selection = textSelectionQuads(layout, this.start, this.end).map((q) => q.map((p) => at(p.x, p.y)));
|
|
4671
|
+
const c = caretAt(layout, this.focus);
|
|
4672
|
+
return { caret: [at(c.from.x, c.from.y), at(c.to.x, c.to.y)], selection, frame };
|
|
4673
|
+
}
|
|
4674
|
+
/** Ends editing: an empty layer made for this session is deleted (as Photoshop does). Returns whether the layer remains. */
|
|
4675
|
+
close() {
|
|
4676
|
+
if (!this.alive) return false;
|
|
4677
|
+
if (!this.text.trim().length) {
|
|
4678
|
+
try {
|
|
4679
|
+
this.controller.removeLayer(this.layerId);
|
|
4680
|
+
} catch {
|
|
4616
4681
|
}
|
|
4617
|
-
|
|
4682
|
+
return false;
|
|
4618
4683
|
}
|
|
4619
|
-
|
|
4684
|
+
if (this.layer.name === "Text" && this.changed) {
|
|
4685
|
+
try {
|
|
4686
|
+
this.controller.setLayerProperties(this.layerId, { name: typeLayerName(this.text) }, false);
|
|
4687
|
+
} catch {
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
return true;
|
|
4620
4691
|
}
|
|
4621
|
-
return createSelectionMask(w, h, out);
|
|
4622
|
-
}
|
|
4623
|
-
var TONE_RANGE_PRESETS = {
|
|
4624
|
-
shadows: [0, 65],
|
|
4625
|
-
midtones: [105, 150],
|
|
4626
|
-
highlights: [190, 255]
|
|
4627
4692
|
};
|
|
4628
|
-
function
|
|
4629
|
-
const
|
|
4630
|
-
const
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4642
|
-
}
|
|
4643
|
-
function hueRange(pixels, center) {
|
|
4644
|
-
const n = checkPixels(pixels);
|
|
4645
|
-
const out = new Uint8Array(n);
|
|
4646
|
-
const d = pixels.data;
|
|
4647
|
-
for (let i = 0; i < n; i++) {
|
|
4648
|
-
const p = i * 4;
|
|
4649
|
-
if (d[p + 3] <= 0) continue;
|
|
4650
|
-
const r = d[p] / 255;
|
|
4651
|
-
const g = d[p + 1] / 255;
|
|
4652
|
-
const b = d[p + 2] / 255;
|
|
4653
|
-
const mx = Math.max(r, g, b);
|
|
4654
|
-
const mn = Math.min(r, g, b);
|
|
4655
|
-
const chroma = mx - mn;
|
|
4656
|
-
if (chroma <= 0) continue;
|
|
4657
|
-
let hue;
|
|
4658
|
-
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
4659
|
-
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
4660
|
-
else hue = (r - g) / chroma + 4;
|
|
4661
|
-
hue *= 60;
|
|
4662
|
-
let dh = ((hue - center) % 360 + 360) % 360;
|
|
4663
|
-
dh = Math.min(dh, 360 - dh);
|
|
4664
|
-
out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
|
|
4693
|
+
function typeLayerAt(controller, p, slop = 4) {
|
|
4694
|
+
const layers = [...flattenTypeLayers(controller.getLayers())].reverse();
|
|
4695
|
+
for (const l of layers) {
|
|
4696
|
+
if (!l.visible) continue;
|
|
4697
|
+
const inv = invert(textMatrix(l));
|
|
4698
|
+
const q = applyMatrix(inv, p);
|
|
4699
|
+
let b = null;
|
|
4700
|
+
if (l.frame.kind === "box") b = { x: 0, y: 0, width: l.frame.width, height: l.frame.height };
|
|
4701
|
+
else {
|
|
4702
|
+
const layout = controller.getTextLayout(l.id);
|
|
4703
|
+
if (layout) b = layout.bounds;
|
|
4704
|
+
}
|
|
4705
|
+
if (b && q.x >= b.x - slop && q.x <= b.x + b.width + slop && q.y >= b.y - slop && q.y <= b.y + b.height + slop) return l.id;
|
|
4665
4706
|
}
|
|
4666
|
-
return
|
|
4667
|
-
}
|
|
4668
|
-
var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
|
|
4669
|
-
var COLOR_RANGE_PRESETS = [
|
|
4670
|
-
...Object.keys(HUE_RANGE_PRESETS),
|
|
4671
|
-
"highlights",
|
|
4672
|
-
"midtones",
|
|
4673
|
-
"shadows",
|
|
4674
|
-
"skinTones"
|
|
4675
|
-
];
|
|
4676
|
-
function isColorRangePreset(v) {
|
|
4677
|
-
return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
|
|
4707
|
+
return null;
|
|
4678
4708
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
const b = d[p + 2] / 255;
|
|
4690
|
-
const mx = Math.max(r, g, b);
|
|
4691
|
-
const mn = Math.min(r, g, b);
|
|
4692
|
-
const chroma = mx - mn;
|
|
4693
|
-
if (chroma <= 0 || mx <= 0) continue;
|
|
4694
|
-
let hue;
|
|
4695
|
-
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
4696
|
-
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
4697
|
-
else hue = (r - g) / chroma + 4;
|
|
4698
|
-
hue *= 60;
|
|
4699
|
-
if (hue > 180) hue -= 360;
|
|
4700
|
-
const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
|
|
4701
|
-
out[i] = coverageToByte(k);
|
|
4702
|
-
}
|
|
4703
|
-
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4709
|
+
function flattenTypeLayers(layers) {
|
|
4710
|
+
const out = [];
|
|
4711
|
+
const walk = (list) => {
|
|
4712
|
+
for (const l of list) {
|
|
4713
|
+
if (l.type === "type") out.push(l);
|
|
4714
|
+
else if (l.type === "group") walk(l.children);
|
|
4715
|
+
}
|
|
4716
|
+
};
|
|
4717
|
+
walk(layers);
|
|
4718
|
+
return out;
|
|
4704
4719
|
}
|
|
4705
|
-
function
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
4720
|
+
function invert(m) {
|
|
4721
|
+
const [a, b, c, d, e, f] = m;
|
|
4722
|
+
const det = a * d - b * c || 1;
|
|
4723
|
+
return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
|
|
4710
4724
|
}
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
const n = pixels.width * pixels.height;
|
|
4716
|
-
for (let i = 0; i < n; i++) {
|
|
4717
|
-
if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
|
|
4718
|
-
bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
|
|
4725
|
+
var TypeToolGesture = class {
|
|
4726
|
+
constructor(controller, options) {
|
|
4727
|
+
this.controller = controller;
|
|
4728
|
+
this.options = options;
|
|
4719
4729
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4730
|
+
controller;
|
|
4731
|
+
options;
|
|
4732
|
+
session = null;
|
|
4733
|
+
dragStart = null;
|
|
4734
|
+
error = null;
|
|
4735
|
+
down(p, mods = {}) {
|
|
4736
|
+
this.error = null;
|
|
4737
|
+
const hit = typeLayerAt(this.controller, p);
|
|
4738
|
+
if (this.session && this.session.alive && hit === this.session.layerId) {
|
|
4739
|
+
this.session.pointAt(p, mods.shift);
|
|
4740
|
+
this.dragStart = null;
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
this.finish();
|
|
4744
|
+
if (hit) {
|
|
4745
|
+
this.controller.setActiveLayer(hit);
|
|
4746
|
+
this.session = new TextEditSession(this.controller, hit, this.controller.textIndexAt(hit, p));
|
|
4747
|
+
this.dragStart = null;
|
|
4748
|
+
return;
|
|
4731
4749
|
}
|
|
4732
|
-
|
|
4750
|
+
this.dragStart = { x: p.x, y: p.y };
|
|
4733
4751
|
}
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
if (x > 0) visit(i - 1);
|
|
4760
|
-
if (x < w - 1) visit(i + 1);
|
|
4761
|
-
if (y > 0) visit(i - w);
|
|
4762
|
-
if (y < h - 1) visit(i + w);
|
|
4752
|
+
/** Releases a click or drag that did not land on type: adds a new text layer (point text, or a box for a drag). */
|
|
4753
|
+
up(p) {
|
|
4754
|
+
const start = this.dragStart;
|
|
4755
|
+
this.dragStart = null;
|
|
4756
|
+
if (!start) return this.session;
|
|
4757
|
+
const o = this.options();
|
|
4758
|
+
const w = Math.abs(p.x - start.x);
|
|
4759
|
+
const h = Math.abs(p.y - start.y);
|
|
4760
|
+
try {
|
|
4761
|
+
const box = w >= 8 && h >= 8 ? { width: w, height: h } : void 0;
|
|
4762
|
+
const id = this.controller.addTextLayer({
|
|
4763
|
+
text: "",
|
|
4764
|
+
x: box ? Math.min(start.x, p.x) : start.x,
|
|
4765
|
+
y: box ? Math.min(start.y, p.y) : start.y,
|
|
4766
|
+
...box ? { box } : {},
|
|
4767
|
+
style: { fontFamily: o.fontFamily, fontSize: o.fontSize, fontWeight: o.fontWeight, italic: o.italic, color: o.color },
|
|
4768
|
+
paragraph: o.align !== "left" ? { align: o.align } : {},
|
|
4769
|
+
...o.orientation === "vertical" ? { orientation: "vertical" } : {},
|
|
4770
|
+
name: "Text"
|
|
4771
|
+
});
|
|
4772
|
+
this.session = new TextEditSession(this.controller, id, 0);
|
|
4773
|
+
} catch (e) {
|
|
4774
|
+
this.error = e instanceof Error ? e.message : String(e);
|
|
4775
|
+
}
|
|
4776
|
+
return this.session;
|
|
4763
4777
|
}
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4772
|
-
}
|
|
4778
|
+
/** Ends the open session (an empty new layer is removed). */
|
|
4779
|
+
finish() {
|
|
4780
|
+
const s = this.session;
|
|
4781
|
+
this.session = null;
|
|
4782
|
+
s?.close();
|
|
4783
|
+
}
|
|
4784
|
+
};
|
|
4773
4785
|
|
|
4774
|
-
// src/image-editor/selection/
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
return
|
|
4786
|
+
// src/image-editor/selection/shapes.ts
|
|
4787
|
+
function polygonCoverage(points, width, height, antiAlias = true) {
|
|
4788
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, antiAlias);
|
|
4789
|
+
if (!box || box.width === width && box.height === height) return box ? coverage : new Float32Array(width * height);
|
|
4790
|
+
const m = new Float32Array(width * height);
|
|
4791
|
+
for (let y = 0; y < box.height; y++) m.set(coverage.subarray(y * box.width, (y + 1) * box.width), (box.y + y) * width + box.x);
|
|
4792
|
+
return m;
|
|
4781
4793
|
}
|
|
4782
|
-
function
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4794
|
+
function polygonCoverageBox(points, width, height, antiAlias = true) {
|
|
4795
|
+
const none = { box: null, coverage: new Float32Array(0) };
|
|
4796
|
+
if (points.length < 3 || width <= 0 || height <= 0) return none;
|
|
4797
|
+
let minX = Infinity;
|
|
4798
|
+
let minY = Infinity;
|
|
4799
|
+
let maxX = -Infinity;
|
|
4800
|
+
let maxY = -Infinity;
|
|
4801
|
+
for (const p of points) {
|
|
4802
|
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
|
4803
|
+
minX = Math.min(minX, p.x);
|
|
4804
|
+
minY = Math.min(minY, p.y);
|
|
4805
|
+
maxX = Math.max(maxX, p.x);
|
|
4806
|
+
maxY = Math.max(maxY, p.y);
|
|
4786
4807
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4808
|
+
const bx0 = Math.max(0, Math.floor(minX));
|
|
4809
|
+
const by0 = Math.max(0, Math.floor(minY));
|
|
4810
|
+
const bx1 = Math.min(width, Math.ceil(maxX));
|
|
4811
|
+
const by1 = Math.min(height, Math.ceil(maxY));
|
|
4812
|
+
if (!(bx1 > bx0 && by1 > by0)) return none;
|
|
4813
|
+
const bw = bx1 - bx0;
|
|
4814
|
+
const m = new Float32Array(bw * (by1 - by0));
|
|
4815
|
+
const sub2 = antiAlias ? 4 : 1;
|
|
4816
|
+
const edges = [];
|
|
4817
|
+
for (let i = 0; i < points.length; i++) {
|
|
4818
|
+
const a = points[i];
|
|
4819
|
+
const b = points[(i + 1) % points.length];
|
|
4820
|
+
if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;
|
|
4821
|
+
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 });
|
|
4822
|
+
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 });
|
|
4791
4823
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
for (
|
|
4797
|
-
const
|
|
4798
|
-
|
|
4824
|
+
edges.sort((p, q) => p.top - q.top);
|
|
4825
|
+
let next = 0;
|
|
4826
|
+
let active = [];
|
|
4827
|
+
const xs = [];
|
|
4828
|
+
for (let py = by0; py < by1; py++) {
|
|
4829
|
+
const row = (py - by0) * bw - bx0;
|
|
4830
|
+
for (let s = 0; s < sub2; s++) {
|
|
4831
|
+
const y = py + (s + 0.5) / sub2;
|
|
4832
|
+
while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);
|
|
4833
|
+
if (active.length) active = active.filter((e) => e.bottom > y);
|
|
4834
|
+
if (!active.length) continue;
|
|
4835
|
+
xs.length = 0;
|
|
4836
|
+
for (const e of active) {
|
|
4837
|
+
if (e.top <= y) xs.push(e.ax + (y - e.ay) / (e.by - e.ay) * (e.bx - e.ax));
|
|
4838
|
+
}
|
|
4839
|
+
xs.sort((a, b) => a - b);
|
|
4840
|
+
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
4841
|
+
const x0 = Math.max(0, xs[k]);
|
|
4842
|
+
const x1 = Math.min(width, xs[k + 1]);
|
|
4843
|
+
if (x1 <= x0) continue;
|
|
4844
|
+
const c0 = Math.floor(x0);
|
|
4845
|
+
const c1 = Math.min(width, Math.ceil(x1));
|
|
4846
|
+
for (let cx = c0; cx < c1; cx++) {
|
|
4847
|
+
let cov;
|
|
4848
|
+
if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);
|
|
4849
|
+
else {
|
|
4850
|
+
const c = cx + 0.5;
|
|
4851
|
+
cov = c >= x0 && c < x1 ? 1 : 0;
|
|
4852
|
+
}
|
|
4853
|
+
if (cov > 0) m[row + cx] += cov / sub2;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4799
4857
|
}
|
|
4800
|
-
|
|
4858
|
+
for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;
|
|
4859
|
+
return { box: { x: bx0, y: by0, width: bw, height: by1 - by0 }, coverage: m };
|
|
4801
4860
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
feather: 0,
|
|
4809
|
-
contrast: 0,
|
|
4810
|
-
shiftEdge: 0
|
|
4811
|
-
};
|
|
4812
|
-
var GUIDED_FILTER_EPS = 1e-4;
|
|
4813
|
-
var fin = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
|
4814
|
-
function normalizeRefineEdgeParams(p = {}) {
|
|
4815
|
-
return {
|
|
4816
|
-
radius: Math.min(250, Math.max(0, fin(p.radius, 0))),
|
|
4817
|
-
smartRadius: p.smartRadius === true,
|
|
4818
|
-
smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),
|
|
4819
|
-
feather: Math.min(1e3, Math.max(0, fin(p.feather, 0))),
|
|
4820
|
-
contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),
|
|
4821
|
-
shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0)))
|
|
4822
|
-
};
|
|
4861
|
+
function polygonSelection(width, height, points, options = {}) {
|
|
4862
|
+
const { box, coverage } = polygonCoverageBox(points, width, height, options.antiAlias ?? true);
|
|
4863
|
+
if (!box) return createSelectionMask(width, height);
|
|
4864
|
+
const out = new Uint8Array(coverage.length);
|
|
4865
|
+
for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);
|
|
4866
|
+
return selectionFromRegion(width, height, box, out);
|
|
4823
4867
|
}
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
const px2 = p.shiftEdge / 100 * Math.max(p.radius, 4);
|
|
4829
|
-
return Number.isFinite(px2) ? Math.min(64, Math.max(-64, px2)) : 0;
|
|
4868
|
+
function normalizeRect(rect) {
|
|
4869
|
+
const x = rect.width < 0 ? rect.x + rect.width : rect.x;
|
|
4870
|
+
const y = rect.height < 0 ? rect.y + rect.height : rect.y;
|
|
4871
|
+
return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };
|
|
4830
4872
|
}
|
|
4831
|
-
function
|
|
4832
|
-
const
|
|
4833
|
-
const
|
|
4834
|
-
|
|
4835
|
-
|
|
4873
|
+
function rectSelection(width, height, rect, options = {}) {
|
|
4874
|
+
const r = normalizeRect(rect);
|
|
4875
|
+
const pts = [
|
|
4876
|
+
{ x: r.x, y: r.y },
|
|
4877
|
+
{ x: r.x + r.width, y: r.y },
|
|
4878
|
+
{ x: r.x + r.width, y: r.y + r.height },
|
|
4879
|
+
{ x: r.x, y: r.y + r.height }
|
|
4880
|
+
];
|
|
4881
|
+
return polygonSelection(width, height, pts, options);
|
|
4836
4882
|
}
|
|
4837
|
-
function
|
|
4838
|
-
const
|
|
4839
|
-
const
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4883
|
+
function ellipsePoints(rect) {
|
|
4884
|
+
const r = normalizeRect(rect);
|
|
4885
|
+
const rx = r.width / 2;
|
|
4886
|
+
const ry = r.height / 2;
|
|
4887
|
+
const cx = r.x + rx;
|
|
4888
|
+
const cy = r.y + ry;
|
|
4889
|
+
const rmax = Math.max(rx, ry, 0.5);
|
|
4890
|
+
const step = Math.sqrt(0.4 / rmax);
|
|
4891
|
+
const n = Math.min(4096, Math.max(16, Math.ceil(2 * Math.PI / step)));
|
|
4892
|
+
const pts = [];
|
|
4893
|
+
for (let i = 0; i < n; i++) {
|
|
4894
|
+
const t = i / n * 2 * Math.PI;
|
|
4895
|
+
pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });
|
|
4848
4896
|
}
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4897
|
+
return pts;
|
|
4898
|
+
}
|
|
4899
|
+
function ellipseSelection(width, height, rect, options = {}) {
|
|
4900
|
+
return polygonSelection(width, height, ellipsePoints(rect), options);
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
// src/image-editor/selection/wand.ts
|
|
4904
|
+
function checkPixels(p) {
|
|
4905
|
+
const n = p.width * p.height;
|
|
4906
|
+
if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {
|
|
4907
|
+
throw new Error("Invalid pixel buffer size");
|
|
4857
4908
|
}
|
|
4858
|
-
|
|
4909
|
+
if (p.data.length < n * 4) throw new Error("Pixel data is shorter than width \xD7 height \xD7 4");
|
|
4910
|
+
return n;
|
|
4859
4911
|
}
|
|
4860
|
-
|
|
4861
|
-
const
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
for (let i = 0; i < n; i++) o[i] = a[i] * b2[i];
|
|
4866
|
-
return o;
|
|
4867
|
-
};
|
|
4868
|
-
const m0 = boxMean(c0, w, h, r);
|
|
4869
|
-
const m1 = boxMean(c1, w, h, r);
|
|
4870
|
-
const m2 = boxMean(c2, w, h, r);
|
|
4871
|
-
const mp = boxMean(p, w, h, r);
|
|
4872
|
-
const mip0 = boxMean(prod(c0, p), w, h, r);
|
|
4873
|
-
const mip1 = boxMean(prod(c1, p), w, h, r);
|
|
4874
|
-
const mip2 = boxMean(prod(c2, p), w, h, r);
|
|
4875
|
-
const s00 = boxMean(prod(c0, c0), w, h, r);
|
|
4876
|
-
const s01 = boxMean(prod(c0, c1), w, h, r);
|
|
4877
|
-
const s02 = boxMean(prod(c0, c2), w, h, r);
|
|
4878
|
-
const s11 = boxMean(prod(c1, c1), w, h, r);
|
|
4879
|
-
const s12 = boxMean(prod(c1, c2), w, h, r);
|
|
4880
|
-
const s22 = boxMean(prod(c2, c2), w, h, r);
|
|
4881
|
-
const a0 = new Float32Array(n);
|
|
4882
|
-
const a1 = new Float32Array(n);
|
|
4883
|
-
const a2 = new Float32Array(n);
|
|
4884
|
-
const b = new Float32Array(n);
|
|
4885
|
-
for (let i = 0; i < n; i++) {
|
|
4886
|
-
const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];
|
|
4887
|
-
const rr = s00[i] - u0 * u0 + eps;
|
|
4888
|
-
const rg = s01[i] - u0 * u1;
|
|
4889
|
-
const rb = s02[i] - u0 * u2;
|
|
4890
|
-
const gg = s11[i] - u1 * u1 + eps;
|
|
4891
|
-
const gb = s12[i] - u1 * u2;
|
|
4892
|
-
const bb = s22[i] - u2 * u2 + eps;
|
|
4893
|
-
const v0 = mip0[i] - u0 * up;
|
|
4894
|
-
const v1 = mip1[i] - u1 * up;
|
|
4895
|
-
const v2 = mip2[i] - u2 * up;
|
|
4896
|
-
const i00 = gg * bb - gb * gb;
|
|
4897
|
-
const i01 = gb * rb - rg * bb;
|
|
4898
|
-
const i02 = rg * gb - gg * rb;
|
|
4899
|
-
const i11 = rr * bb - rb * rb;
|
|
4900
|
-
const i12 = rb * rg - rr * gb;
|
|
4901
|
-
const i22 = rr * gg - rg * rg;
|
|
4902
|
-
const det = rr * i00 + rg * i01 + rb * i02;
|
|
4903
|
-
let x0 = 0, x1 = 0, x2 = 0;
|
|
4904
|
-
if (Math.abs(det) >= 1e-30) {
|
|
4905
|
-
x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;
|
|
4906
|
-
x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;
|
|
4907
|
-
x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;
|
|
4908
|
-
}
|
|
4909
|
-
a0[i] = x0;
|
|
4910
|
-
a1[i] = x1;
|
|
4911
|
-
a2[i] = x2;
|
|
4912
|
-
b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;
|
|
4912
|
+
var SRGB_TO_LINEAR2 = (() => {
|
|
4913
|
+
const t = new Float32Array(256);
|
|
4914
|
+
for (let i = 0; i < 256; i++) {
|
|
4915
|
+
const v = i / 255;
|
|
4916
|
+
t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
4913
4917
|
}
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
|
|
4918
|
+
return t;
|
|
4919
|
+
})();
|
|
4920
|
+
var labF = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
|
|
4921
|
+
function srgb8ToLab(r, g, b) {
|
|
4922
|
+
const rl = SRGB_TO_LINEAR2[r & 255];
|
|
4923
|
+
const gl = SRGB_TO_LINEAR2[g & 255];
|
|
4924
|
+
const bl = SRGB_TO_LINEAR2[b & 255];
|
|
4925
|
+
const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;
|
|
4926
|
+
const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;
|
|
4927
|
+
const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;
|
|
4928
|
+
const fx = labF(x);
|
|
4929
|
+
const fy = labF(y);
|
|
4930
|
+
const fz = labF(z);
|
|
4931
|
+
return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
|
|
4921
4932
|
}
|
|
4922
|
-
function
|
|
4923
|
-
const
|
|
4924
|
-
const
|
|
4925
|
-
|
|
4926
|
-
const
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4933
|
+
function similarity(pixels, seed, tolerance, distance) {
|
|
4934
|
+
const d = pixels.data;
|
|
4935
|
+
const tr = d[seed * 4];
|
|
4936
|
+
const tg = d[seed * 4 + 1];
|
|
4937
|
+
const tb = d[seed * 4 + 2];
|
|
4938
|
+
const ta = d[seed * 4 + 3];
|
|
4939
|
+
const tol = Math.max(0, Math.floor(tolerance + 1e-3));
|
|
4940
|
+
if (distance === "perceptual") {
|
|
4941
|
+
const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);
|
|
4942
|
+
const de = Math.max(0, tolerance) * 100 / 255;
|
|
4943
|
+
const de2 = de * de + 1e-6;
|
|
4944
|
+
return (i) => {
|
|
4945
|
+
const p = i * 4;
|
|
4946
|
+
const a = d[p + 3];
|
|
4947
|
+
if (ta === 0 && a === 0) return true;
|
|
4948
|
+
if (Math.abs(a - ta) > tol) return false;
|
|
4949
|
+
const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);
|
|
4950
|
+
return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
return (i) => {
|
|
4954
|
+
const p = i * 4;
|
|
4955
|
+
const a = d[p + 3];
|
|
4956
|
+
if (ta === 0 && a === 0) return true;
|
|
4957
|
+
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;
|
|
4958
|
+
};
|
|
4959
|
+
}
|
|
4960
|
+
function antialiasMarks(marks, w, h, x0, y0, x1, y1) {
|
|
4961
|
+
const out = marks.slice();
|
|
4962
|
+
for (let y = y0; y < y1; y++) {
|
|
4963
|
+
for (let x = x0; x < x1; x++) {
|
|
4964
|
+
const i = y * w + x;
|
|
4965
|
+
const v = marks[i];
|
|
4966
|
+
let sum = 0;
|
|
4967
|
+
let edge = false;
|
|
4968
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
4969
|
+
const ny = y + dy;
|
|
4970
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
4971
|
+
const nx = x + dx;
|
|
4972
|
+
const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];
|
|
4973
|
+
if (n !== v) edge = true;
|
|
4974
|
+
sum += n;
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
if (edge) {
|
|
4978
|
+
const avg = sum / (9 * 255);
|
|
4979
|
+
out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);
|
|
4980
|
+
}
|
|
4932
4981
|
}
|
|
4933
4982
|
}
|
|
4934
|
-
const gm = greyMorphology(g, w, h, r, true);
|
|
4935
|
-
const hi = greyMorphology(y, w, h, r, true);
|
|
4936
|
-
const lo = greyMorphology(y, w, h, r, false);
|
|
4937
|
-
const out = new Float32Array(n);
|
|
4938
|
-
for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);
|
|
4939
4983
|
return out;
|
|
4940
4984
|
}
|
|
4941
|
-
function
|
|
4942
|
-
const
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4985
|
+
function magicWand(pixels, seed, options = {}) {
|
|
4986
|
+
const n = checkPixels(pixels);
|
|
4987
|
+
const { width: w, height: h } = pixels;
|
|
4988
|
+
const sx = Math.floor(seed.x);
|
|
4989
|
+
const sy = Math.floor(seed.y);
|
|
4990
|
+
if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);
|
|
4991
|
+
const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? "rgb");
|
|
4992
|
+
const marks = new Uint8Array(n);
|
|
4993
|
+
let bx0 = w;
|
|
4994
|
+
let by0 = h;
|
|
4995
|
+
let bx1 = -1;
|
|
4996
|
+
let by1 = -1;
|
|
4997
|
+
if (options.contiguous ?? true) {
|
|
4998
|
+
const stack = [sy * w + sx];
|
|
4999
|
+
while (stack.length) {
|
|
5000
|
+
const idx = stack.pop();
|
|
5001
|
+
if (marks[idx]) continue;
|
|
5002
|
+
const y = idx / w | 0;
|
|
5003
|
+
const row = y * w;
|
|
5004
|
+
let l = idx - row;
|
|
5005
|
+
let r = l;
|
|
5006
|
+
while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;
|
|
5007
|
+
while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;
|
|
5008
|
+
marks.fill(255, row + l, row + r + 1);
|
|
5009
|
+
if (l < bx0) bx0 = l;
|
|
5010
|
+
if (r > bx1) bx1 = r;
|
|
5011
|
+
if (y < by0) by0 = y;
|
|
5012
|
+
if (y > by1) by1 = y;
|
|
5013
|
+
for (const ny of [y - 1, y + 1]) {
|
|
5014
|
+
if (ny < 0 || ny >= h) continue;
|
|
5015
|
+
const nrow = ny * w;
|
|
5016
|
+
let inRun = false;
|
|
5017
|
+
for (let nx = l; nx <= r; nx++) {
|
|
5018
|
+
const ok = !marks[nrow + nx] && similar(nrow + nx);
|
|
5019
|
+
if (ok && !inRun) stack.push(nrow + nx);
|
|
5020
|
+
inRun = ok;
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
} else {
|
|
4948
5025
|
for (let y = 0; y < h; y++) {
|
|
5026
|
+
const row = y * w;
|
|
4949
5027
|
for (let x = 0; x < w; x++) {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5028
|
+
if (similar(row + x)) {
|
|
5029
|
+
marks[row + x] = 255;
|
|
5030
|
+
if (x < bx0) bx0 = x;
|
|
5031
|
+
if (x > bx1) bx1 = x;
|
|
5032
|
+
if (y < by0) by0 = y;
|
|
5033
|
+
if (y > by1) by1 = y;
|
|
4956
5034
|
}
|
|
4957
5035
|
}
|
|
4958
5036
|
}
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
5037
|
+
}
|
|
5038
|
+
if (bx1 < 0) return createSelectionMask(w, h);
|
|
5039
|
+
if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
|
|
5040
|
+
return createSelectionMask(
|
|
5041
|
+
w,
|
|
5042
|
+
h,
|
|
5043
|
+
antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
function sampleColor(c) {
|
|
5047
|
+
if (typeof c === "string") {
|
|
5048
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());
|
|
5049
|
+
if (!m) throw new Error(`Invalid colour "${c}"`);
|
|
5050
|
+
const v = parseInt(m[1], 16);
|
|
5051
|
+
return [v >> 16 & 255, v >> 8 & 255, v & 255];
|
|
5052
|
+
}
|
|
5053
|
+
return [c[0], c[1], c[2]];
|
|
5054
|
+
}
|
|
5055
|
+
function colorRange(pixels, samples, options = {}) {
|
|
5056
|
+
const n = checkPixels(pixels);
|
|
5057
|
+
const { width: w, height: h, data } = pixels;
|
|
5058
|
+
const f = Math.max(0, options.fuzziness ?? 40) + 1;
|
|
5059
|
+
const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
|
|
5060
|
+
const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
|
|
5061
|
+
const out = new Uint8Array(n);
|
|
5062
|
+
if (!parsed.length) return createSelectionMask(w, h, out);
|
|
5063
|
+
for (let i = 0; i < n; i++) {
|
|
5064
|
+
const p = i * 4;
|
|
5065
|
+
if (data[p + 3] <= 0) continue;
|
|
5066
|
+
const x = i % w;
|
|
5067
|
+
const y = (i - x) / w;
|
|
5068
|
+
let best = 0;
|
|
5069
|
+
for (const s of parsed) {
|
|
5070
|
+
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]));
|
|
5071
|
+
let k = Math.min(1, Math.max(0, 1 - d / f));
|
|
5072
|
+
if (k > 0 && radius !== void 0 && s.at) {
|
|
5073
|
+
const dist = Math.hypot(x - s.at.x, y - s.at.y);
|
|
5074
|
+
k *= Math.min(1, Math.max(0, 1 - dist / radius));
|
|
4971
5075
|
}
|
|
5076
|
+
if (k > best) best = k;
|
|
4972
5077
|
}
|
|
5078
|
+
out[i] = coverageToByte(best);
|
|
4973
5079
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
5080
|
+
return createSelectionMask(w, h, out);
|
|
5081
|
+
}
|
|
5082
|
+
var TONE_RANGE_PRESETS = {
|
|
5083
|
+
shadows: [0, 65],
|
|
5084
|
+
midtones: [105, 150],
|
|
5085
|
+
highlights: [190, 255]
|
|
5086
|
+
};
|
|
5087
|
+
function toneRange(pixels, range, falloff = 20) {
|
|
5088
|
+
const n = checkPixels(pixels);
|
|
5089
|
+
const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
|
|
5090
|
+
const ramp2 = Math.max(1e-3, falloff);
|
|
5091
|
+
const out = new Uint8Array(n);
|
|
5092
|
+
const d = pixels.data;
|
|
5093
|
+
for (let i = 0; i < n; i++) {
|
|
5094
|
+
const p = i * 4;
|
|
5095
|
+
if (d[p + 3] <= 0) continue;
|
|
5096
|
+
const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
|
|
5097
|
+
const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
|
|
5098
|
+
out[i] = coverageToByte(1 - outside / ramp2);
|
|
4979
5099
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
5100
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
5101
|
+
}
|
|
5102
|
+
function hueRange(pixels, center) {
|
|
5103
|
+
const n = checkPixels(pixels);
|
|
5104
|
+
const out = new Uint8Array(n);
|
|
5105
|
+
const d = pixels.data;
|
|
5106
|
+
for (let i = 0; i < n; i++) {
|
|
5107
|
+
const p = i * 4;
|
|
5108
|
+
if (d[p + 3] <= 0) continue;
|
|
5109
|
+
const r = d[p] / 255;
|
|
5110
|
+
const g = d[p + 1] / 255;
|
|
5111
|
+
const b = d[p + 2] / 255;
|
|
5112
|
+
const mx = Math.max(r, g, b);
|
|
5113
|
+
const mn = Math.min(r, g, b);
|
|
5114
|
+
const chroma = mx - mn;
|
|
5115
|
+
if (chroma <= 0) continue;
|
|
5116
|
+
let hue;
|
|
5117
|
+
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
5118
|
+
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
5119
|
+
else hue = (r - g) / chroma + 4;
|
|
5120
|
+
hue *= 60;
|
|
5121
|
+
let dh = ((hue - center) % 360 + 360) % 360;
|
|
5122
|
+
dh = Math.min(dh, 360 - dh);
|
|
5123
|
+
out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));
|
|
4984
5124
|
}
|
|
4985
|
-
|
|
4986
|
-
if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);
|
|
4987
|
-
return a;
|
|
5125
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
4988
5126
|
}
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
5127
|
+
var HUE_RANGE_PRESETS = { reds: 0, yellows: 60, greens: 120, cyans: 180, blues: 240, magentas: 300 };
|
|
5128
|
+
var COLOR_RANGE_PRESETS = [
|
|
5129
|
+
...Object.keys(HUE_RANGE_PRESETS),
|
|
5130
|
+
"highlights",
|
|
5131
|
+
"midtones",
|
|
5132
|
+
"shadows",
|
|
5133
|
+
"skinTones"
|
|
5134
|
+
];
|
|
5135
|
+
function isColorRangePreset(v) {
|
|
5136
|
+
return typeof v === "string" && COLOR_RANGE_PRESETS.includes(v);
|
|
5137
|
+
}
|
|
5138
|
+
var ramp = (v, lo, hi, soft) => v < lo ? Math.max(0, 1 - (lo - v) / soft) : v > hi ? Math.max(0, 1 - (v - hi) / soft) : 1;
|
|
5139
|
+
function skinToneRange(pixels) {
|
|
5140
|
+
const n = checkPixels(pixels);
|
|
5141
|
+
const out = new Uint8Array(n);
|
|
4995
5142
|
const d = pixels.data;
|
|
4996
|
-
for (let
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5143
|
+
for (let i = 0; i < n; i++) {
|
|
5144
|
+
const p = i * 4;
|
|
5145
|
+
if (d[p + 3] <= 0) continue;
|
|
5146
|
+
const r = d[p] / 255;
|
|
5147
|
+
const g = d[p + 1] / 255;
|
|
5148
|
+
const b = d[p + 2] / 255;
|
|
5149
|
+
const mx = Math.max(r, g, b);
|
|
5150
|
+
const mn = Math.min(r, g, b);
|
|
5151
|
+
const chroma = mx - mn;
|
|
5152
|
+
if (chroma <= 0 || mx <= 0) continue;
|
|
5153
|
+
let hue;
|
|
5154
|
+
if (mx === r) hue = ((g - b) / chroma % 6 + 6) % 6;
|
|
5155
|
+
else if (mx === g) hue = (b - r) / chroma + 2;
|
|
5156
|
+
else hue = (r - g) / chroma + 4;
|
|
5157
|
+
hue *= 60;
|
|
5158
|
+
if (hue > 180) hue -= 360;
|
|
5159
|
+
const k = ramp(hue, 5, 40, 20) * ramp(chroma / mx, 0.12, 0.8, 0.1) * ramp(mx, 0.3, 1, 0.15);
|
|
5160
|
+
out[i] = coverageToByte(k);
|
|
5161
|
+
}
|
|
5162
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
5163
|
+
}
|
|
5164
|
+
function colorRangePreset(pixels, preset, options = {}) {
|
|
5165
|
+
if (preset === "skinTones") return skinToneRange(pixels);
|
|
5166
|
+
if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
|
|
5167
|
+
if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
|
|
5168
|
+
throw new Error(`Unknown Color Range preset "${String(preset)}". Use one of ${COLOR_RANGE_PRESETS.join(", ")}.`);
|
|
5169
|
+
}
|
|
5170
|
+
function selectedColorBins(pixels, sel, tolerance) {
|
|
5171
|
+
const S = 64;
|
|
5172
|
+
let bins = new Uint8Array(S * S * S);
|
|
5173
|
+
const d = pixels.data;
|
|
5174
|
+
const n = pixels.width * pixels.height;
|
|
5175
|
+
for (let i = 0; i < n; i++) {
|
|
5176
|
+
if (sel.data[i] < 128 || d[i * 4 + 3] === 0) continue;
|
|
5177
|
+
bins[((d[i * 4] >> 2) * S + (d[i * 4 + 1] >> 2)) * S + (d[i * 4 + 2] >> 2)] = 1;
|
|
5178
|
+
}
|
|
5179
|
+
const r = Math.round(Math.max(0, tolerance) / 4);
|
|
5180
|
+
if (r === 0) return bins;
|
|
5181
|
+
for (const stride of [1, S, S * S]) {
|
|
5182
|
+
const out = new Uint8Array(bins.length);
|
|
5183
|
+
for (let i = 0; i < bins.length; i++) {
|
|
5184
|
+
if (!bins[i]) continue;
|
|
5185
|
+
const c = Math.floor(i / stride) % S;
|
|
5186
|
+
const lo = Math.max(0, c - r);
|
|
5187
|
+
const hi = Math.min(S - 1, c + r);
|
|
5188
|
+
const base = i - c * stride;
|
|
5189
|
+
for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
|
|
5003
5190
|
}
|
|
5191
|
+
bins = out;
|
|
5004
5192
|
}
|
|
5005
|
-
return
|
|
5193
|
+
return bins;
|
|
5006
5194
|
}
|
|
5007
|
-
function
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5195
|
+
function matcher(pixels, sel, options) {
|
|
5196
|
+
if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
|
|
5197
|
+
const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
|
|
5198
|
+
const d = pixels.data;
|
|
5199
|
+
return (i) => d[i * 4 + 3] > 0 && bins[((d[i * 4] >> 2 << 6) + (d[i * 4 + 1] >> 2)) * 64 + (d[i * 4 + 2] >> 2)] === 1;
|
|
5200
|
+
}
|
|
5201
|
+
function growSelection(pixels, sel, options = {}) {
|
|
5202
|
+
checkPixels(pixels);
|
|
5203
|
+
const match = matcher(pixels, sel, options);
|
|
5204
|
+
const { width: w, height: h } = pixels;
|
|
5205
|
+
const out = new Uint8Array(sel.data);
|
|
5206
|
+
const stack = [];
|
|
5207
|
+
for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
|
|
5208
|
+
while (stack.length) {
|
|
5209
|
+
const i = stack.pop();
|
|
5210
|
+
const x = i % w;
|
|
5211
|
+
const y = (i - x) / w;
|
|
5212
|
+
const visit = (j) => {
|
|
5213
|
+
if (out[j] < 255 && match(j)) {
|
|
5214
|
+
out[j] = 255;
|
|
5215
|
+
stack.push(j);
|
|
5216
|
+
}
|
|
5217
|
+
};
|
|
5218
|
+
if (x > 0) visit(i - 1);
|
|
5219
|
+
if (x < w - 1) visit(i + 1);
|
|
5220
|
+
if (y > 0) visit(i - w);
|
|
5221
|
+
if (y < h - 1) visit(i + w);
|
|
5011
5222
|
}
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
const
|
|
5016
|
-
const
|
|
5017
|
-
const
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5223
|
+
return createSelectionMask(w, h, out);
|
|
5224
|
+
}
|
|
5225
|
+
function selectSimilar(pixels, sel, options = {}) {
|
|
5226
|
+
const n = checkPixels(pixels);
|
|
5227
|
+
const match = matcher(pixels, sel, options);
|
|
5228
|
+
const out = new Uint8Array(sel.data);
|
|
5229
|
+
for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
|
|
5230
|
+
return createSelectionMask(pixels.width, pixels.height, out);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
// src/image-editor/selection/segmentation.ts
|
|
5234
|
+
var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
|
|
5235
|
+
function segmentationOutlines(result) {
|
|
5236
|
+
const p = result.polygon;
|
|
5237
|
+
if (!Array.isArray(p) || !p.length) return [];
|
|
5238
|
+
const rings = Array.isArray(p[0]) ? p : [p];
|
|
5239
|
+
return rings.map((ring) => Array.isArray(ring) ? ring.filter(isPoint) : []).filter((ring) => ring.length >= 3);
|
|
5240
|
+
}
|
|
5241
|
+
function segmentToSelection(width, height, result) {
|
|
5242
|
+
let out = null;
|
|
5243
|
+
for (const ring of segmentationOutlines(result)) {
|
|
5244
|
+
out = combineSelections(out, polygonSelection(width, height, ring), "add");
|
|
5024
5245
|
}
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
const dst = (y0 + y) * mask.width + x0;
|
|
5030
|
-
for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);
|
|
5246
|
+
if (result.mask && result.mask.width > 0 && result.mask.height > 0) {
|
|
5247
|
+
const raw = selectionFromPixels(result.mask, { channel: "luminance" });
|
|
5248
|
+
const sized = raw.width === width && raw.height === height ? raw : resizeSelection(raw, width, height);
|
|
5249
|
+
out = combineSelections(out, sized, "add");
|
|
5031
5250
|
}
|
|
5032
|
-
return
|
|
5251
|
+
return out?.bounds ? out : null;
|
|
5252
|
+
}
|
|
5253
|
+
function segmentsToSelection(width, height, results) {
|
|
5254
|
+
let out = null;
|
|
5255
|
+
for (const r of results) {
|
|
5256
|
+
const sel = segmentToSelection(width, height, r);
|
|
5257
|
+
if (sel) out = combineSelections(out, sel, "add");
|
|
5258
|
+
}
|
|
5259
|
+
return out;
|
|
5033
5260
|
}
|
|
5034
5261
|
|
|
5035
5262
|
// src/image-editor/selection/stroke.ts
|
|
@@ -5836,10 +6063,10 @@ function colorModePlanes(doc, mode) {
|
|
|
5836
6063
|
}
|
|
5837
6064
|
const t = Transform.create(src, builtinProfile("lab-d50"), { intent: "relative", bpc: false });
|
|
5838
6065
|
return (rgba, n) => {
|
|
5839
|
-
const
|
|
5840
|
-
t.convert8(rgba, 4,
|
|
6066
|
+
const lab2 = new Uint8Array(n * 4);
|
|
6067
|
+
t.convert8(rgba, 4, lab2, 4, false);
|
|
5841
6068
|
const planes = [0, 1, 2].map(() => new Uint8Array(n));
|
|
5842
|
-
for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] =
|
|
6069
|
+
for (let i = 0; i < n; i++) for (let c = 0; c < 3; c++) planes[c][i] = lab2[i * 4 + c];
|
|
5843
6070
|
return planes;
|
|
5844
6071
|
};
|
|
5845
6072
|
}
|
|
@@ -9866,6 +10093,30 @@ var ImageEditorController = class {
|
|
|
9866
10093
|
this.setSelection(region, options.mode ?? "replace");
|
|
9867
10094
|
return this.state.selection ?? null;
|
|
9868
10095
|
}
|
|
10096
|
+
/**
|
|
10097
|
+
* Select › Focus Area: selects the parts of the image that are in focus (fine detail above the
|
|
10098
|
+
* image's noise), fitted to its edges, combined with the selection by `mode` as one undo step.
|
|
10099
|
+
* Resolves to the selection and the noise level measured (σ of luminance in levels).
|
|
10100
|
+
*/
|
|
10101
|
+
async selectFocusArea(options = {}) {
|
|
10102
|
+
const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
|
|
10103
|
+
const { selection, noiseLevel } = focusArea(pixels, options);
|
|
10104
|
+
this.setSelection(selection, options.mode ?? "replace");
|
|
10105
|
+
return { selection: this.state.selection ?? null, noiseLevel };
|
|
10106
|
+
}
|
|
10107
|
+
/**
|
|
10108
|
+
* Select › Sky: selects the sky (blue, or bright and grey when overcast; smooth; grown down from
|
|
10109
|
+
* the top of the image, with patches seen through gaps), fitted to its edges, combined with the
|
|
10110
|
+
* selection by `mode` as one undo step. Resolves to the selection and, when no sky was found
|
|
10111
|
+
* (the selection is then unchanged), why.
|
|
10112
|
+
*/
|
|
10113
|
+
async selectSky(options = {}) {
|
|
10114
|
+
const pixels = await this.requirePixels({ sample: options.sampleAllLayers === false ? "active-layer" : "composite" });
|
|
10115
|
+
const { selection, reason } = findSky(pixels, options);
|
|
10116
|
+
if (!selection.bounds) return { selection: this.state.selection ?? null, note: reason };
|
|
10117
|
+
this.setSelection(selection, options.mode ?? "replace");
|
|
10118
|
+
return { selection: this.state.selection ?? null };
|
|
10119
|
+
}
|
|
9869
10120
|
/**
|
|
9870
10121
|
* Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker
|
|
9871
10122
|
* or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)
|
|
@@ -11383,8 +11634,8 @@ var ImageEditorController = class {
|
|
|
11383
11634
|
const values = Array.from(out.subarray(0, t.outputs), (v) => Math.round(Math.min(1, Math.max(0, v)) * 1e4) / 1e4);
|
|
11384
11635
|
const l = new Float32Array(3);
|
|
11385
11636
|
toLab2.eval(rgb, l);
|
|
11386
|
-
const
|
|
11387
|
-
const res = { input, values, lab };
|
|
11637
|
+
const lab2 = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
|
|
11638
|
+
const res = { input, values, lab: lab2 };
|
|
11388
11639
|
if (dst.colorSpace === "rgb") res.hex = `#${values.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
11389
11640
|
return res;
|
|
11390
11641
|
});
|
|
@@ -12766,7 +13017,7 @@ var DEPTH_MASK_CONTROLS = [
|
|
|
12766
13017
|
// src/image-editor/playback.ts
|
|
12767
13018
|
var IMAGE_PLAYBACK_KIND = "layer";
|
|
12768
13019
|
var PAINT = /^(paint_stroke|clone_stamp|heal_brush|spot_heal|dodge|burn|sponge|smudge|blur_stroke|sharpen_stroke|history_brush|set_clone_source|set_clone_transform|fill_pixels|clear_pixels|set_paint|set_retouch_options|load_mixer_brush|clean_mixer_brush)$/;
|
|
12769
|
-
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
|
|
13020
|
+
var SELECT = /^(select_region|select_subject|select_by_color|select_all|deselect|invert_selection|select_rect|select_ellipse|select_polygon|quick_select|select_object|select_focus_area|select_sky|select_line|magic_wand|grow_selection|select_similar|reselect|select_layer_transparency|select_color_range|load_selection_from_image|load_selection_from_mask|set_selection_display|modify_selection|refine_selection|transform_selection|apply_selection_as_mask|layer_via_copy|stroke_selection|content_aware_fill|copy_pixels|cut_pixels|paste_pixels|save_selection|load_selection_from_channel|set_channel_properties|duplicate_channel|delete_channel|load_channel_from_image|set_quick_mask|path_to_selection|select_text_shape)$/;
|
|
12770
13021
|
var ANNOTATE = /^(add_shape_layer|update_shape|add_shape_component|set_shape_component|remove_shape_component|merge_shape_components|add_text_layer|set_text|set_text_style|set_paragraph_style|set_text_frame|set_text_orientation|convert_text_to_shape)$/;
|
|
12771
13022
|
var CROP = /^(set_crop|crop_to_selection|reset_crop|set_rotation|flip_image|transform_layer|warp_layer|flip_layer|offset_layer)$/;
|
|
12772
13023
|
var ADJUST = /^(set_adjustments|add_adjustment_layer|update_adjustment_layer|remove_adjustment_layer|move_adjustment_layer)$/;
|
|
@@ -12932,6 +13183,9 @@ var DEFAULT_SELECTION_TOOL_OPTIONS = {
|
|
|
12932
13183
|
quickEnhance: false,
|
|
12933
13184
|
objectSampleAll: true,
|
|
12934
13185
|
objectEnhance: false,
|
|
13186
|
+
focusRange: 50,
|
|
13187
|
+
focusNoise: 0,
|
|
13188
|
+
focusSoftness: 4,
|
|
12935
13189
|
typeText: "Type",
|
|
12936
13190
|
typeSize: 72,
|
|
12937
13191
|
typeFamily: "sans-serif"
|
|
@@ -13400,6 +13654,58 @@ function createImageSelectionTools(controller) {
|
|
|
13400
13654
|
return summary(controller);
|
|
13401
13655
|
}
|
|
13402
13656
|
};
|
|
13657
|
+
const selectFocusAreaTool = {
|
|
13658
|
+
name: "select_focus_area",
|
|
13659
|
+
title: "Select the focus area",
|
|
13660
|
+
description: "Select \u203A Focus Area: selects the parts of the image that are in focus (sharp detail, above the image noise), with the edge fitted to the image, combined with the selection by mode. Use it to separate a sharp subject from a blurred background (shallow depth of field), e.g. before blurring or darkening the background. Raise range to take in less sharp areas, raise noise if grain in the background gets selected. One undoable step. Returns the selection and noiseLevel (\u03C3 of the image noise in levels of 255).",
|
|
13661
|
+
inputSchema: {
|
|
13662
|
+
type: "object",
|
|
13663
|
+
properties: {
|
|
13664
|
+
range: { type: "number", minimum: 0, maximum: 100, description: "In-Focus Range 0..100 (default 50): higher takes in less sharp areas too." },
|
|
13665
|
+
noise: { type: "number", minimum: 0, maximum: 100, description: "Image Noise Level 0..100 (default 0): raise it when grain is selected as detail." },
|
|
13666
|
+
softness: { type: "number", minimum: 0, maximum: 100, description: "Soften the edge over this many natural pixels (default 4; 0 for a hard edge)." },
|
|
13667
|
+
mode: modeSchema,
|
|
13668
|
+
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." }
|
|
13669
|
+
},
|
|
13670
|
+
additionalProperties: false
|
|
13671
|
+
},
|
|
13672
|
+
annotations: writeAnnotations("Select the focus area", true),
|
|
13673
|
+
handler: async (args) => {
|
|
13674
|
+
const { noiseLevel } = await controller.selectFocusArea({
|
|
13675
|
+
range: args.range === void 0 ? void 0 : num2(args.range, 50),
|
|
13676
|
+
noise: args.noise === void 0 ? void 0 : num2(args.noise, 0),
|
|
13677
|
+
softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
|
|
13678
|
+
mode: parseSelectionMode(args.mode),
|
|
13679
|
+
sampleAllLayers: args.sampleAllLayers
|
|
13680
|
+
});
|
|
13681
|
+
return { ...summary(controller), noiseLevel };
|
|
13682
|
+
}
|
|
13683
|
+
};
|
|
13684
|
+
const selectSkyTool = {
|
|
13685
|
+
name: "select_sky",
|
|
13686
|
+
title: "Select the sky",
|
|
13687
|
+
description: "Select \u203A Sky: selects the sky (blue, or bright and grey when overcast, smooth, reaching down from the top of the image, including patches seen through branches or gaps), with the edge fitted to the skyline, combined with the selection by mode. Use it to replace or recolour a sky or to adjust the land without it (invert_selection). Raise threshold to take in haze or a sunset glow, lower it if bright buildings come in. One undoable step. Returns the selection, and a note when no sky was found (the selection is then unchanged).",
|
|
13688
|
+
inputSchema: {
|
|
13689
|
+
type: "object",
|
|
13690
|
+
properties: {
|
|
13691
|
+
threshold: { type: "number", minimum: 0, maximum: 100, description: "How sky-like a pixel must be, 0..100 (default 50): higher takes in less sky-like pixels." },
|
|
13692
|
+
softness: { type: "number", minimum: 0, maximum: 100, description: "Soften the edge over this many natural pixels (default 4; 0 for a hard edge)." },
|
|
13693
|
+
mode: modeSchema,
|
|
13694
|
+
sampleAllLayers: { type: "boolean", description: "Read every layer (default true); false reads only the active pixel layer." }
|
|
13695
|
+
},
|
|
13696
|
+
additionalProperties: false
|
|
13697
|
+
},
|
|
13698
|
+
annotations: writeAnnotations("Select the sky", true),
|
|
13699
|
+
handler: async (args) => {
|
|
13700
|
+
const { note } = await controller.selectSky({
|
|
13701
|
+
threshold: args.threshold === void 0 ? void 0 : num2(args.threshold, 50),
|
|
13702
|
+
softness: args.softness === void 0 ? void 0 : num2(args.softness, 4),
|
|
13703
|
+
mode: parseSelectionMode(args.mode),
|
|
13704
|
+
sampleAllLayers: args.sampleAllLayers
|
|
13705
|
+
});
|
|
13706
|
+
return { ...summary(controller), ...note ? { note } : {} };
|
|
13707
|
+
}
|
|
13708
|
+
};
|
|
13403
13709
|
const magicWandTool = {
|
|
13404
13710
|
name: "magic_wand",
|
|
13405
13711
|
title: "Magic wand",
|
|
@@ -13955,6 +14261,8 @@ function createImageSelectionTools(controller) {
|
|
|
13955
14261
|
quickSelectTool,
|
|
13956
14262
|
selectObjectTool,
|
|
13957
14263
|
createImageSubjectSelectionTool(controller),
|
|
14264
|
+
selectFocusAreaTool,
|
|
14265
|
+
selectSkyTool,
|
|
13958
14266
|
lineTool,
|
|
13959
14267
|
magicWandTool,
|
|
13960
14268
|
colorRangeTool,
|
|
@@ -20668,6 +20976,7 @@ export {
|
|
|
20668
20976
|
findAdjustmentControl,
|
|
20669
20977
|
findLayer,
|
|
20670
20978
|
findLayerControl,
|
|
20979
|
+
findSky,
|
|
20671
20980
|
findSubject,
|
|
20672
20981
|
findWebGpu,
|
|
20673
20982
|
finishTile,
|
|
@@ -20677,6 +20986,7 @@ export {
|
|
|
20677
20986
|
flattenPath,
|
|
20678
20987
|
flipCorners,
|
|
20679
20988
|
flowField,
|
|
20989
|
+
focusArea,
|
|
20680
20990
|
followLayerMove,
|
|
20681
20991
|
fontOf,
|
|
20682
20992
|
formatControlValue,
|
|
@@ -21095,6 +21405,7 @@ export {
|
|
|
21095
21405
|
snapPolygonToEdges,
|
|
21096
21406
|
snapTo45,
|
|
21097
21407
|
snapToEdge,
|
|
21408
|
+
softMaskToSelection,
|
|
21098
21409
|
solidPaint,
|
|
21099
21410
|
solidSurface,
|
|
21100
21411
|
solveMembrane,
|
|
@@ -21209,6 +21520,8 @@ export {
|
|
|
21209
21520
|
withImagePlayback,
|
|
21210
21521
|
withNewIds,
|
|
21211
21522
|
workImage,
|
|
21523
|
+
workLuma,
|
|
21524
|
+
workPlanes,
|
|
21212
21525
|
writeAdjustmentBlock,
|
|
21213
21526
|
writeDeepRegion,
|
|
21214
21527
|
writeLfx2,
|