@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.
@@ -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 cached = workCache.get(pixels.data);
3053
- if (cached && cached.maxSide === maxSide && cached.work.sourceWidth === pixels.width && cached.work.sourceHeight === pixels.height) return cached.work;
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
- workCache.set(pixels.data, { maxSide, work });
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 lab = new Float32Array(n * 3);
3694
- for (let i = 0; i < n; i++) lab.set(toLab(rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]), i * 3);
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([lab[i * 3], lab[i * 3 + 1], lab[i * 3 + 2], x, y]);
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 = (lab[i * 3] - L) ** 2 + (lab[i * 3 + 1] - A) ** 2 + (lab[i * 3 + 2] - B) ** 2 + m2 * ((x - cx) ** 2 + (y - cy) ** 2);
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] += lab[i * 3];
3730
- sum[c * 6 + 1] += lab[i * 3 + 1];
3731
- sum[c * 6 + 2] += lab[i * 3 + 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] += lab[i * 3];
3750
- meanLab[c * 3 + 1] += lab[i * 3 + 1];
3751
- meanLab[c * 3 + 2] += lab[i * 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(lab[a * 3] - lab[b * 3], lab[a * 3 + 1] - lab[b * 3 + 1], lab[a * 3 + 2] - lab[b * 3 + 2]);
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/layers/shape-type-ui.ts
3879
- var DEFAULT_SHAPE_TOOL_OPTIONS = {
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
- sides: 6,
3886
- points: 5,
3887
- innerRatio: 0.5,
3888
- weight: 4,
3889
- arrow: false
3883
+ smartRadius: false,
3884
+ smooth: 0,
3885
+ feather: 0,
3886
+ contrast: 0,
3887
+ shiftEdge: 0
3890
3888
  };
3891
- var SHAPE_KIND_LABELS = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
3892
- var DEFAULT_TYPE_TOOL_OPTIONS = { fontFamily: "sans-serif", fontSize: 32, fontWeight: 400, italic: false, color: "#111827", align: "left" };
3893
- function liveShapeForDrag(o, a, b, mods = {}) {
3894
- if (o.kind === "line") {
3895
- let x2 = b.x;
3896
- let y2 = b.y;
3897
- if (mods.shift) {
3898
- const ang = Math.round(Math.atan2(b.y - a.y, b.x - a.x) / (Math.PI / 4)) * (Math.PI / 4);
3899
- const len = Math.hypot(b.x - a.x, b.y - a.y);
3900
- x2 = a.x + Math.cos(ang) * len;
3901
- y2 = a.y + Math.sin(ang) * len;
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
- let x = a.x;
3915
- let y = a.y;
3916
- if (mods.alt) {
3917
- x = a.x - w;
3918
- y = a.y - h;
3919
- w *= 2;
3920
- h *= 2;
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
- const box = { x: Math.min(x, x + w), y: Math.min(y, y + h), width: Math.abs(w), height: Math.abs(h) };
3923
- if (box.width < 1 || box.height < 1) return null;
3924
- switch (o.kind) {
3925
- case "rect":
3926
- return { kind: "rect", ...box, ...o.radius > 0 ? { radius: o.radius } : {} };
3927
- case "ellipse":
3928
- return { kind: "ellipse", ...box };
3929
- case "polygon":
3930
- return { kind: "polygon", ...box, sides: o.sides };
3931
- case "star":
3932
- return { kind: "star", ...box, points: o.points, innerRatio: o.innerRatio };
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
- return null;
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
- var ShapeDrawGesture = class {
3937
- constructor(controller, options) {
3938
- this.controller = controller;
3939
- this.options = options;
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
- controller;
3942
- options;
3943
- start = null;
3944
- layerId = null;
3945
- error = null;
3946
- get active() {
3947
- return this.start !== null;
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
- /** The shape layer drawn by the last drag (until the next one starts). */
3950
- get drawnLayerId() {
3951
- return this.layerId;
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
- down(p) {
3954
- this.error = null;
3955
- this.start = { x: p.x, y: p.y };
3956
- this.layerId = null;
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
- move(p, mods = {}) {
3959
- if (!this.start) return;
3960
- const o = this.options();
3961
- const shape = liveShapeForDrag(o, this.start, p, mods);
3962
- if (!shape) return;
3963
- try {
3964
- if (!this.layerId) {
3965
- const isLine = o.kind === "line";
3966
- this.layerId = this.controller.addShapeLayer({
3967
- shape,
3968
- fill: isLine ? o.stroke ?? o.fill ?? "#000000" : o.fill,
3969
- stroke: !isLine && o.stroke ? { width: o.strokeWidth, paint: o.stroke } : null
3970
- });
3971
- } else this.controller.setShapeComponent(this.layerId, 0, { shape }, false);
3972
- } catch (e) {
3973
- this.error = e instanceof Error ? e.message : String(e);
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
- if (bx1 < 0) return createSelectionMask(w, h);
4580
- if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);
4581
- return createSelectionMask(
4582
- w,
4583
- h,
4584
- antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))
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
- return [c[0], c[1], c[2]];
4595
- }
4596
- function colorRange(pixels, samples, options = {}) {
4597
- const n = checkPixels(pixels);
4598
- const { width: w, height: h, data } = pixels;
4599
- const f = Math.max(0, options.fuzziness ?? 40) + 1;
4600
- const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));
4601
- const radius = options.localized !== void 0 ? Math.max(1, options.localized) : void 0;
4602
- const out = new Uint8Array(n);
4603
- if (!parsed.length) return createSelectionMask(w, h, out);
4604
- for (let i = 0; i < n; i++) {
4605
- const p = i * 4;
4606
- if (data[p + 3] <= 0) continue;
4607
- const x = i % w;
4608
- const y = (i - x) / w;
4609
- let best = 0;
4610
- for (const s of parsed) {
4611
- 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]));
4612
- let k = Math.min(1, Math.max(0, 1 - d / f));
4613
- if (k > 0 && radius !== void 0 && s.at) {
4614
- const dist = Math.hypot(x - s.at.x, y - s.at.y);
4615
- k *= Math.min(1, Math.max(0, 1 - dist / radius));
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
- if (k > best) best = k;
4682
+ return false;
4618
4683
  }
4619
- out[i] = coverageToByte(best);
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 toneRange(pixels, range, falloff = 20) {
4629
- const n = checkPixels(pixels);
4630
- const [lo, hi] = typeof range === "string" ? TONE_RANGE_PRESETS[range] : range;
4631
- const ramp2 = Math.max(1e-3, falloff);
4632
- const out = new Uint8Array(n);
4633
- const d = pixels.data;
4634
- for (let i = 0; i < n; i++) {
4635
- const p = i * 4;
4636
- if (d[p + 3] <= 0) continue;
4637
- const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));
4638
- const outside = l < lo ? lo - l : l > hi ? l - hi : 0;
4639
- out[i] = coverageToByte(1 - outside / ramp2);
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 createSelectionMask(pixels.width, pixels.height, out);
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
- 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;
4680
- function skinToneRange(pixels) {
4681
- const n = checkPixels(pixels);
4682
- const out = new Uint8Array(n);
4683
- const d = pixels.data;
4684
- for (let i = 0; i < n; i++) {
4685
- const p = i * 4;
4686
- if (d[p + 3] <= 0) continue;
4687
- const r = d[p] / 255;
4688
- const g = d[p + 1] / 255;
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 colorRangePreset(pixels, preset, options = {}) {
4706
- if (preset === "skinTones") return skinToneRange(pixels);
4707
- if (preset === "highlights" || preset === "midtones" || preset === "shadows") return toneRange(pixels, preset, options.falloff ?? 20);
4708
- if (preset in HUE_RANGE_PRESETS) return hueRange(pixels, HUE_RANGE_PRESETS[preset]);
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
- function selectedColorBins(pixels, sel, tolerance) {
4712
- const S = 64;
4713
- let bins = new Uint8Array(S * S * S);
4714
- const d = pixels.data;
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
- const r = Math.round(Math.max(0, tolerance) / 4);
4721
- if (r === 0) return bins;
4722
- for (const stride of [1, S, S * S]) {
4723
- const out = new Uint8Array(bins.length);
4724
- for (let i = 0; i < bins.length; i++) {
4725
- if (!bins[i]) continue;
4726
- const c = Math.floor(i / stride) % S;
4727
- const lo = Math.max(0, c - r);
4728
- const hi = Math.min(S - 1, c + r);
4729
- const base = i - c * stride;
4730
- for (let k = lo; k <= hi; k++) out[base + k * stride] = 1;
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
- bins = out;
4750
+ this.dragStart = { x: p.x, y: p.y };
4733
4751
  }
4734
- return bins;
4735
- }
4736
- function matcher(pixels, sel, options) {
4737
- if (sel.width !== pixels.width || sel.height !== pixels.height) throw new Error("The selection and the pixels differ in size");
4738
- const bins = selectedColorBins(pixels, sel, options.tolerance ?? 32);
4739
- const d = pixels.data;
4740
- 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;
4741
- }
4742
- function growSelection(pixels, sel, options = {}) {
4743
- checkPixels(pixels);
4744
- const match = matcher(pixels, sel, options);
4745
- const { width: w, height: h } = pixels;
4746
- const out = new Uint8Array(sel.data);
4747
- const stack = [];
4748
- for (let i = 0; i < out.length; i++) if (out[i] >= 128) stack.push(i);
4749
- while (stack.length) {
4750
- const i = stack.pop();
4751
- const x = i % w;
4752
- const y = (i - x) / w;
4753
- const visit = (j) => {
4754
- if (out[j] < 255 && match(j)) {
4755
- out[j] = 255;
4756
- stack.push(j);
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
- return createSelectionMask(w, h, out);
4765
- }
4766
- function selectSimilar(pixels, sel, options = {}) {
4767
- const n = checkPixels(pixels);
4768
- const match = matcher(pixels, sel, options);
4769
- const out = new Uint8Array(sel.data);
4770
- for (let i = 0; i < n; i++) if (match(i)) out[i] = 255;
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/segmentation.ts
4775
- var isPoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
4776
- function segmentationOutlines(result) {
4777
- const p = result.polygon;
4778
- if (!Array.isArray(p) || !p.length) return [];
4779
- const rings = Array.isArray(p[0]) ? p : [p];
4780
- return rings.map((ring) => Array.isArray(ring) ? ring.filter(isPoint) : []).filter((ring) => ring.length >= 3);
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 segmentToSelection(width, height, result) {
4783
- let out = null;
4784
- for (const ring of segmentationOutlines(result)) {
4785
- out = combineSelections(out, polygonSelection(width, height, ring), "add");
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
- if (result.mask && result.mask.width > 0 && result.mask.height > 0) {
4788
- const raw = selectionFromPixels(result.mask, { channel: "luminance" });
4789
- const sized = raw.width === width && raw.height === height ? raw : resizeSelection(raw, width, height);
4790
- out = combineSelections(out, sized, "add");
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
- return out?.bounds ? out : null;
4793
- }
4794
- function segmentsToSelection(width, height, results) {
4795
- let out = null;
4796
- for (const r of results) {
4797
- const sel = segmentToSelection(width, height, r);
4798
- if (sel) out = combineSelections(out, sel, "add");
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
- return out;
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
- // src/image-editor/selection/refine.ts
4804
- var DEFAULT_REFINE_EDGE_PARAMS = {
4805
- radius: 0,
4806
- smartRadius: false,
4807
- smooth: 0,
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
- var gfRadius = (p) => Math.min(64, Math.max(1, Math.ceil(p.radius)));
4825
- var smoothSigma = (p) => p.smooth / 100 * 4;
4826
- var featherSigma = (p) => p.feather / 2;
4827
- function shiftAmount(p) {
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 refineEdgeHalo(params = {}) {
4832
- const p = normalizeRefineEdgeParams(params);
4833
- const g = gfRadius(p);
4834
- const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;
4835
- return edge + Math.ceil(3 * smoothSigma(p)) + Math.ceil(3 * featherSigma(p)) + Math.ceil(Math.abs(shiftAmount(p))) + 2;
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 boxMean(src, w, h, r) {
4838
- const tmp = new Float32Array(w * h);
4839
- const pre = new Float64Array(Math.max(w, h) + 1);
4840
- for (let y = 0; y < h; y++) {
4841
- const row = y * w;
4842
- for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];
4843
- for (let x = 0; x < w; x++) {
4844
- const lo = Math.max(0, x - r);
4845
- const hi = Math.min(w, x + r + 1);
4846
- tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);
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
- const out = new Float32Array(w * h);
4850
- for (let x = 0; x < w; x++) {
4851
- for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];
4852
- for (let y = 0; y < h; y++) {
4853
- const lo = Math.max(0, y - r);
4854
- const hi = Math.min(h, y + r + 1);
4855
- out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);
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
- return out;
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
- function guidedFilterColor(guide, p, w, h, r, eps = GUIDED_FILTER_EPS) {
4861
- const n = w * h;
4862
- const [c0, c1, c2] = guide;
4863
- const prod = (a, b2) => {
4864
- const o = new Float32Array(n);
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
- const ma0 = boxMean(a0, w, h, r);
4915
- const ma1 = boxMean(a1, w, h, r);
4916
- const ma2 = boxMean(a2, w, h, r);
4917
- const mb = boxMean(b, w, h, r);
4918
- const out = new Float32Array(n);
4919
- for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];
4920
- return out;
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 edgeWidth(guide, w, h, r) {
4923
- const n = w * h;
4924
- const y = new Float32Array(n);
4925
- for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];
4926
- const g = new Float32Array(n);
4927
- for (let yy = 0; yy < h; yy++) {
4928
- for (let xx = 0; xx < w; xx++) {
4929
- const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;
4930
- const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;
4931
- g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);
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 refineCoverage(guide, m, w, h, params = {}) {
4942
- const p = normalizeRefineEdgeParams(params);
4943
- let a = Float32Array.from(m);
4944
- if (p.radius > 0 && guide) {
4945
- const n = w * h;
4946
- const boundary = new Uint8Array(n);
4947
- let any = false;
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
- const i = y * w + x;
4951
- const v = m[i] >= 0.5;
4952
- 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;
4953
- if (diff) {
4954
- boundary[i] = 1;
4955
- any = true;
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
- if (any) {
4960
- const dist = distanceTransform(boundary, w, h);
4961
- const r = gfRadius(p);
4962
- const q = guidedFilterColor(guide, m, w, h, r);
4963
- const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;
4964
- for (let i = 0; i < n; i++) {
4965
- const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;
4966
- const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));
4967
- if (wgt > 0) {
4968
- const qi = Math.min(1, Math.max(0, q[i]));
4969
- a[i] = m[i] + (qi - m[i]) * wgt;
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
- const ss = smoothSigma(p);
4975
- if (ss >= 0.3) {
4976
- a = gaussianBlur(a, w, h, ss, "clamp");
4977
- const gain = 1 + ss;
4978
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
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
- a = gaussianBlur(a, w, h, featherSigma(p), "clamp");
4981
- if (p.contrast > 0) {
4982
- const gain = 1 / (1 - 0.99 * p.contrast / 100);
4983
- for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));
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
- const amount = shiftAmount(p);
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
- function guidePlanes(pixels, win) {
4990
- const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };
4991
- const n = box.width * box.height;
4992
- const r = new Float32Array(n);
4993
- const g = new Float32Array(n);
4994
- const b = new Float32Array(n);
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 y = 0; y < box.height; y++) {
4997
- for (let x = 0; x < box.width; x++) {
4998
- const i = y * box.width + x;
4999
- const p = ((box.y + y) * pixels.width + box.x + x) * 4;
5000
- r[i] = d[p] / 255;
5001
- g[i] = d[p + 1] / 255;
5002
- b[i] = d[p + 2] / 255;
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 [r, g, b];
5193
+ return bins;
5006
5194
  }
5007
- function refineSelection(pixels, mask, params = {}) {
5008
- const p = normalizeRefineEdgeParams(params);
5009
- if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {
5010
- throw new Error("Refine Edge needs pixels the size of the selection");
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
- if (!mask.bounds) return mask;
5013
- const halo = refineEdgeHalo(p);
5014
- const x0 = Math.max(0, mask.bounds.x - halo);
5015
- const y0 = Math.max(0, mask.bounds.y - halo);
5016
- const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);
5017
- const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);
5018
- const ww = x1 - x0;
5019
- const wh = y1 - y0;
5020
- const m = new Float32Array(ww * wh);
5021
- for (let y = 0; y < wh; y++) {
5022
- const src = (y0 + y) * mask.width + x0;
5023
- for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;
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
- const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;
5026
- const refined = refineCoverage(guide, m, ww, wh, p);
5027
- const out = new Uint8Array(mask.width * mask.height);
5028
- for (let y = 0; y < wh; y++) {
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 createSelectionMask(mask.width, mask.height, out);
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 lab = new Uint8Array(n * 4);
5840
- t.convert8(rgba, 4, lab, 4, false);
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] = lab[i * 4 + c];
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 lab = [Math.round(l[0] * 1e4) / 100, Math.round((l[1] * 255 - 128) * 100) / 100, Math.round((l[2] * 255 - 128) * 100) / 100];
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,