@michaelyagi/kiri 0.1.0-alpha.2 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/kiri.mjs CHANGED
@@ -1,14 +1,14 @@
1
- var T = Object.defineProperty;
2
- var U = (i, t, e) => t in i ? T(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
3
- var p = (i, t, e) => U(i, typeof t != "symbol" ? t + "" : t, e);
4
- const L = {
1
+ var K = Object.defineProperty;
2
+ var X = (i, t, e) => t in i ? K(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
3
+ var d = (i, t, e) => X(i, typeof t != "symbol" ? t + "" : t, e);
4
+ const A = {
5
5
  brightness: 1,
6
6
  contrast: 1,
7
7
  saturation: 1,
8
8
  grayscale: !1,
9
9
  sepia: !1
10
10
  };
11
- function M(i, t) {
11
+ function I(i, t) {
12
12
  return {
13
13
  brightness: Math.max(0, t.brightness ?? i.brightness),
14
14
  contrast: Math.max(0, t.contrast ?? i.contrast),
@@ -17,7 +17,7 @@ function M(i, t) {
17
17
  sepia: t.sepia ?? i.sepia
18
18
  };
19
19
  }
20
- function C(i) {
20
+ function $(i) {
21
21
  const t = [
22
22
  `brightness(${i.brightness})`,
23
23
  `contrast(${i.contrast})`,
@@ -25,148 +25,180 @@ function C(i) {
25
25
  ];
26
26
  return i.grayscale && t.push("grayscale(1)"), i.sepia && t.push("sepia(1)"), t.join(" ");
27
27
  }
28
- function $(i, t, e, s, o) {
28
+ function j(i, t, e, o, s) {
29
29
  i.innerHTML = "";
30
30
  const n = document.createElement("div");
31
- n.className = "kiri-stage";
32
- const r = document.createElement("div");
33
- r.className = "kiri-image-layer";
31
+ n.className = "kiri-stage", n.tabIndex = 0, n.setAttribute("role", "application"), n.setAttribute(
32
+ "aria-label",
33
+ "Image cropper. Drag to pan. Arrow keys to pan, plus/minus to zoom, 0 to reset."
34
+ );
35
+ const l = document.createElement("div");
36
+ l.className = "kiri-image-layer";
34
37
  const c = document.createElement("img");
35
- c.draggable = !1, r.appendChild(c);
36
- const d = document.createElement("div");
37
- d.className = t === "circle" ? "kiri-frame kiri-frame--circle" : "kiri-frame", H(d, e, s), n.appendChild(r), n.appendChild(d);
38
+ c.draggable = !1, l.appendChild(c);
39
+ const m = document.createElement("div");
40
+ m.className = t === "circle" ? "kiri-frame kiri-frame--circle" : "kiri-frame", k(m, e, o), n.appendChild(l), n.appendChild(m);
38
41
  let h = null;
39
- if (o.show) {
40
- const m = document.createElement("div");
41
- m.className = `kiri-root kiri-root--${o.position}`, h = document.createElement("input"), h.type = "range", h.className = "kiri-zoomer", h.min = String(o.min), h.max = String(o.max), h.step = "0.01", h.value = String(o.value), m.appendChild(n), m.appendChild(h), i.appendChild(m);
42
+ if (s.show) {
43
+ const f = document.createElement("div");
44
+ f.className = `kiri-root kiri-root--${s.position}`, h = document.createElement("input"), h.type = "range", h.className = "kiri-zoomer", h.min = String(s.min), h.max = String(s.max), h.step = "0.01", h.value = String(s.value), f.appendChild(n), f.appendChild(h), i.appendChild(f);
42
45
  } else
43
46
  i.appendChild(n);
44
- return { stageEl: n, frameEl: d, imageLayerEl: r, imgEl: c, zoomerEl: h };
47
+ return { stageEl: n, frameEl: m, imageLayerEl: l, imgEl: c, zoomerEl: h };
45
48
  }
46
- function H(i, t, e) {
49
+ function k(i, t, e) {
47
50
  i.style.width = `${t}px`, i.style.height = `${e}px`;
48
51
  }
49
- const F = 20;
50
- function R(i, t, e) {
52
+ const C = 20;
53
+ function V(i, t, e) {
51
54
  i.style.width = `${t}px`, i.style.height = `${e}px`;
52
55
  }
53
- function N(i, t, e) {
54
- const s = e * (t.flip.horizontal ? -1 : 1), o = e * (t.flip.vertical ? -1 : 1);
55
- i.style.transform = `translate(-50%, -50%) translate(${t.offset.x}px, ${t.offset.y}px) rotate(${t.rotation}deg) scale(${s}, ${o})`;
56
+ function W(i, t, e) {
57
+ const o = e * (t.flip.horizontal ? -1 : 1), s = e * (t.flip.vertical ? -1 : 1);
58
+ i.style.transform = `translate(-50%, -50%) translate(${t.offset.x}px, ${t.offset.y}px) rotate(${t.rotation}deg) scale(${o}, ${s})`;
56
59
  }
57
- function Z(i, t) {
58
- i.style.filter = C(t);
60
+ function R(i, t) {
61
+ i.style.filter = $(t);
59
62
  }
60
- function D(i, t) {
63
+ function N(i, t) {
61
64
  return (t / 90 % 2 + 2) % 2 === 1 ? { width: i.height, height: i.width } : { width: i.width, height: i.height };
62
65
  }
63
- function b(i, t, e) {
64
- const s = D(i, e);
65
- return s.width <= 0 || s.height <= 0 ? 1 : Math.max(t.width / s.width, t.height / s.height);
66
+ function O(i, t, e) {
67
+ const o = N(i, e);
68
+ return o.width <= 0 || o.height <= 0 ? 1 : Math.max(t.width / o.width, t.height / o.height);
66
69
  }
67
- function z(i, t, e, s) {
68
- const o = D(i, e), n = b(i, t, e) * s;
69
- return { width: o.width * n, height: o.height * n };
70
+ function S(i, t, e, o) {
71
+ const s = N(i, e), n = O(i, t, e) * o;
72
+ return { width: s.width * n, height: s.height * n };
70
73
  }
71
- function E(i, t, e) {
74
+ function P(i, t, e) {
72
75
  return Math.min(Math.max(i, t), e);
73
76
  }
74
- function w(i, t, e) {
75
- const s = Math.max(0, (t.width - e.width) / 2), o = Math.max(0, (t.height - e.height) / 2);
77
+ function x(i, t, e) {
78
+ const o = Math.max(0, (t.width - e.width) / 2), s = Math.max(0, (t.height - e.height) / 2);
76
79
  return {
77
- x: Math.min(Math.max(i.x, -s), s) || 0,
78
- y: Math.min(Math.max(i.y, -o), o) || 0
80
+ x: Math.min(Math.max(i.x, -o), o) || 0,
81
+ y: Math.min(Math.max(i.y, -s), s) || 0
79
82
  };
80
83
  }
81
- function S(i) {
84
+ function b(i) {
82
85
  return (i % 360 + 360) % 360;
83
86
  }
84
- function I(i, t) {
87
+ const F = 15, T = 0.1;
88
+ function H(i, t) {
85
89
  return Math.hypot(i.clientX - t.clientX, i.clientY - t.clientY);
86
90
  }
87
- function _(i, t, e) {
88
- const s = /* @__PURE__ */ new Map();
89
- let o = null, n = 0, r = 1;
90
- function c(a) {
91
- const { min: l, max: f } = t.getMinMaxZoom(), g = E(a.zoom, l, f), v = z(
91
+ function B(i, t, e) {
92
+ const o = /* @__PURE__ */ new Map();
93
+ let s = null, n = 0, l = 1;
94
+ function c(r) {
95
+ const { min: a, max: g } = t.getMinMaxZoom(), w = P(r.zoom, a, g), z = S(
92
96
  t.getNaturalSize(),
93
97
  t.getFrameSize(),
94
- a.rotation,
95
- g
96
- ), x = w(a.offset, v, t.getFrameSize());
98
+ r.rotation,
99
+ w
100
+ ), y = x(r.offset, z, t.getFrameSize());
97
101
  t.setState({
98
- zoom: g,
99
- offset: x,
100
- rotation: S(a.rotation),
101
- flip: a.flip,
102
- filters: a.filters
102
+ zoom: w,
103
+ offset: y,
104
+ rotation: b(r.rotation),
105
+ flip: r.flip,
106
+ filters: r.filters
103
107
  });
104
108
  }
105
- function d(a) {
106
- if (i.setPointerCapture(a.pointerId), s.set(a.pointerId, a), s.size === 1) {
107
- const l = t.getState();
108
- o = { x: a.clientX, y: a.clientY, offset: l.offset }, i.classList.add("kiri-dragging");
109
- } else if (s.size === 2) {
110
- o = null;
111
- const [l, f] = [...s.values()];
112
- n = I(l, f), r = t.getState().zoom;
109
+ function m(r) {
110
+ if (i.setPointerCapture(r.pointerId), o.set(r.pointerId, r), o.size === 1) {
111
+ const a = t.getState();
112
+ s = { x: r.clientX, y: r.clientY, offset: a.offset }, i.classList.add("kiri-dragging");
113
+ } else if (o.size === 2) {
114
+ s = null;
115
+ const [a, g] = [...o.values()];
116
+ n = H(a, g), l = t.getState().zoom;
113
117
  }
114
118
  }
115
- function h(a) {
116
- if (s.has(a.pointerId)) {
117
- if (s.set(a.pointerId, a), s.size === 2) {
118
- const [l, f] = [...s.values()], g = I(l, f);
119
+ function h(r) {
120
+ if (o.has(r.pointerId)) {
121
+ if (o.set(r.pointerId, r), o.size === 2) {
122
+ const [a, g] = [...o.values()], w = H(a, g);
119
123
  if (n > 0) {
120
- const v = g / n, x = t.getState();
121
- c({ ...x, zoom: r * v });
124
+ const z = w / n, y = t.getState();
125
+ c({ ...y, zoom: l * z });
122
126
  }
123
127
  return;
124
128
  }
125
- if (o) {
126
- const l = a.clientX - o.x, f = a.clientY - o.y, g = t.getState();
129
+ if (s) {
130
+ const a = r.clientX - s.x, g = r.clientY - s.y, w = t.getState();
127
131
  c({
128
- ...g,
129
- offset: { x: o.offset.x + l, y: o.offset.y + f }
132
+ ...w,
133
+ offset: { x: s.offset.x + a, y: s.offset.y + g }
130
134
  });
131
135
  }
132
136
  }
133
137
  }
134
- function m(a) {
135
- s.delete(a.pointerId), s.size < 2 && (n = 0), s.size === 0 && (o = null, i.classList.remove("kiri-dragging"));
136
- }
137
- function u(a) {
138
- if (!e.mouseWheelZoom || e.mouseWheelZoom === "ctrl" && !a.ctrlKey) return;
139
- a.preventDefault();
140
- const l = t.getState(), f = -a.deltaY * 15e-4;
141
- c({ ...l, zoom: l.zoom * (1 + f) });
138
+ function f(r) {
139
+ o.delete(r.pointerId), o.size < 2 && (n = 0), o.size === 0 && (s = null, i.classList.remove("kiri-dragging"));
140
+ }
141
+ function u(r) {
142
+ if (!e.mouseWheelZoom || e.mouseWheelZoom === "ctrl" && !r.ctrlKey) return;
143
+ r.preventDefault();
144
+ const a = t.getState(), g = -r.deltaY * 15e-4;
145
+ c({ ...a, zoom: a.zoom * (1 + g) });
146
+ }
147
+ function p(r) {
148
+ const a = t.getState();
149
+ switch (r.key) {
150
+ case "ArrowLeft":
151
+ r.preventDefault(), c({ ...a, offset: { ...a.offset, x: a.offset.x - F } });
152
+ break;
153
+ case "ArrowRight":
154
+ r.preventDefault(), c({ ...a, offset: { ...a.offset, x: a.offset.x + F } });
155
+ break;
156
+ case "ArrowUp":
157
+ r.preventDefault(), c({ ...a, offset: { ...a.offset, y: a.offset.y - F } });
158
+ break;
159
+ case "ArrowDown":
160
+ r.preventDefault(), c({ ...a, offset: { ...a.offset, y: a.offset.y + F } });
161
+ break;
162
+ case "+":
163
+ case "=":
164
+ r.preventDefault(), c({ ...a, zoom: a.zoom + T });
165
+ break;
166
+ case "-":
167
+ case "_":
168
+ r.preventDefault(), c({ ...a, zoom: a.zoom - T });
169
+ break;
170
+ case "0":
171
+ r.preventDefault(), t.reset();
172
+ break;
173
+ }
142
174
  }
143
- return i.addEventListener("pointerdown", d), i.addEventListener("pointermove", h), i.addEventListener("pointerup", m), i.addEventListener("pointercancel", m), i.addEventListener("wheel", u, { passive: !1 }), {
175
+ return i.addEventListener("pointerdown", m), i.addEventListener("pointermove", h), i.addEventListener("pointerup", f), i.addEventListener("pointercancel", f), i.addEventListener("wheel", u, { passive: !1 }), i.addEventListener("keydown", p), {
144
176
  destroy() {
145
- i.removeEventListener("pointerdown", d), i.removeEventListener("pointermove", h), i.removeEventListener("pointerup", m), i.removeEventListener("pointercancel", m), i.removeEventListener("wheel", u);
177
+ i.removeEventListener("pointerdown", m), i.removeEventListener("pointermove", h), i.removeEventListener("pointerup", f), i.removeEventListener("pointercancel", f), i.removeEventListener("wheel", u), i.removeEventListener("keydown", p);
146
178
  }
147
179
  };
148
180
  }
149
- function A(i) {
181
+ function G(i) {
150
182
  const t = new DataView(i);
151
183
  if (t.byteLength < 4 || t.getUint16(0, !1) !== 65496) return 1;
152
184
  let e = 2;
153
185
  for (; e + 4 <= t.byteLength; ) {
154
- const s = t.getUint16(e, !1), o = t.getUint16(e + 2, !1);
155
- if (s === 65505) {
186
+ const o = t.getUint16(e, !1), s = t.getUint16(e + 2, !1);
187
+ if (o === 65505) {
156
188
  const n = e + 4;
157
- return t.getUint32(n, !1) !== 1165519206 ? 1 : K(t, n + 6);
189
+ return t.getUint32(n, !1) !== 1165519206 ? 1 : q(t, n + 6);
158
190
  }
159
- if ((s & 65280) !== 65280) break;
160
- e += 2 + o;
191
+ if ((o & 65280) !== 65280) break;
192
+ e += 2 + s;
161
193
  }
162
194
  return 1;
163
195
  }
164
- function K(i, t) {
165
- const e = i.getUint16(t, !1) === 18761, s = i.getUint32(t + 4, e), o = t + s;
166
- if (o + 2 > i.byteLength) return 1;
167
- const n = i.getUint16(o, e);
168
- for (let r = 0; r < n; r++) {
169
- const c = o + 2 + r * 12;
196
+ function q(i, t) {
197
+ const e = i.getUint16(t, !1) === 18761, o = i.getUint32(t + 4, e), s = t + o;
198
+ if (s + 2 > i.byteLength) return 1;
199
+ const n = i.getUint16(s, e);
200
+ for (let l = 0; l < n; l++) {
201
+ const c = s + 2 + l * 12;
170
202
  if (c + 12 > i.byteLength) break;
171
203
  if (i.getUint16(c, e) === 274) {
172
204
  const h = i.getUint16(c + 8, e);
@@ -175,7 +207,7 @@ function K(i, t) {
175
207
  }
176
208
  return 1;
177
209
  }
178
- function j(i) {
210
+ function J(i) {
179
211
  switch (i) {
180
212
  case 2:
181
213
  return { rotation: 0, flipHorizontal: !0 };
@@ -195,78 +227,101 @@ function j(i) {
195
227
  return { rotation: 0, flipHorizontal: !1 };
196
228
  }
197
229
  }
198
- function y(i, t, e, s) {
230
+ function D(i, t, e, o) {
199
231
  return i === void 0 ? e : t.includes(i) ? i : (console.warn(
200
- `Kiri: invalid ${s} "${i}" — defaulting to "${e}". Valid values: ${t.join(", ")}.`
232
+ `Kiri: invalid ${o} "${i}" — defaulting to "${e}". Valid values: ${t.join(", ")}.`
201
233
  ), e);
202
234
  }
203
- const X = ["base64", "blob", "canvas"], Y = ["image/jpeg", "image/png", "image/webp"];
204
- function V(i, t, e) {
235
+ function Z(i, t, e) {
236
+ return Math.min(Math.max(i, t), e);
237
+ }
238
+ const Q = ["base64", "blob", "canvas"], tt = ["image/jpeg", "image/png", "image/webp"];
239
+ function Y(i, t, e) {
205
240
  return {
206
241
  left: i.width / 2 - t.x - e.width / 2,
207
242
  top: i.height / 2 - t.y - e.height / 2
208
243
  };
209
244
  }
210
- function W(i, t, e, s, o, n) {
211
- const r = { width: i.naturalWidth, height: i.naturalHeight }, c = b(r, e, t.rotation) * t.zoom, d = z(r, e, t.rotation, t.zoom), h = document.createElement("canvas");
212
- h.width = Math.max(1, Math.round(d.width)), h.height = Math.max(1, Math.round(d.height));
213
- const m = h.getContext("2d");
214
- if (!m) throw new Error("Kiri: unable to get 2D canvas context");
215
- m.translate(h.width / 2, h.height / 2), m.rotate(t.rotation * Math.PI / 180), m.scale(c * (t.flip.horizontal ? -1 : 1), c * (t.flip.vertical ? -1 : 1)), m.filter = C(t.filters), m.drawImage(i, -r.width / 2, -r.height / 2, r.width, r.height);
216
- const { left: u, top: a } = V(
217
- d,
245
+ function et(i, t, e) {
246
+ const o = O(i, t, e.rotation) * e.zoom, s = S(i, t, e.rotation, e.zoom), { left: n, top: l } = Y(s, e.offset, t), c = { x: s.width / 2, y: s.height / 2 }, m = { x: i.width / 2, y: i.height / 2 }, h = b(e.rotation), f = [
247
+ { x: n, y: l },
248
+ { x: n + t.width, y: l },
249
+ { x: n, y: l + t.height },
250
+ { x: n + t.width, y: l + t.height }
251
+ ].map(({ x: z, y }) => {
252
+ const M = (z - c.x) / o, L = (y - c.y) / o;
253
+ let v, E;
254
+ return h === 90 ? (v = L, E = -M) : h === 180 ? (v = -M, E = -L) : h === 270 ? (v = -L, E = M) : (v = M, E = L), { x: v + m.x, y: E + m.y };
255
+ }), u = f.map((z) => z.x), p = f.map((z) => z.y), r = Z(Math.min(...u), 0, i.width), a = Z(Math.min(...p), 0, i.height), g = Z(Math.max(...u), 0, i.width), w = Z(Math.max(...p), 0, i.height);
256
+ return {
257
+ x: Math.round(r),
258
+ y: Math.round(a),
259
+ width: Math.round(g - r),
260
+ height: Math.round(w - a),
261
+ rotation: h,
262
+ flip: { ...e.flip }
263
+ };
264
+ }
265
+ function it(i, t, e, o, s, n) {
266
+ const l = { width: i.naturalWidth, height: i.naturalHeight }, c = O(l, e, t.rotation) * t.zoom, m = S(l, e, t.rotation, t.zoom), h = document.createElement("canvas");
267
+ h.width = Math.max(1, Math.round(m.width)), h.height = Math.max(1, Math.round(m.height));
268
+ const f = h.getContext("2d");
269
+ if (!f) throw new Error("Kiri: unable to get 2D canvas context");
270
+ f.translate(h.width / 2, h.height / 2), f.rotate(t.rotation * Math.PI / 180), f.scale(c * (t.flip.horizontal ? -1 : 1), c * (t.flip.vertical ? -1 : 1)), f.filter = $(t.filters), f.drawImage(i, -l.width / 2, -l.height / 2, l.width, l.height);
271
+ const { left: u, top: p } = Y(
272
+ m,
218
273
  t.offset,
219
274
  e
220
- ), l = document.createElement("canvas");
221
- l.width = s, l.height = o;
222
- const f = l.getContext("2d");
223
- if (!f) throw new Error("Kiri: unable to get 2D canvas context");
224
- return n === "circle" && (f.save(), f.beginPath(), f.ellipse(
225
- s / 2,
275
+ ), r = document.createElement("canvas");
276
+ r.width = o, r.height = s;
277
+ const a = r.getContext("2d");
278
+ if (!a) throw new Error("Kiri: unable to get 2D canvas context");
279
+ return n === "circle" && (a.save(), a.beginPath(), a.ellipse(
226
280
  o / 2,
227
281
  s / 2,
228
282
  o / 2,
283
+ s / 2,
229
284
  0,
230
285
  0,
231
286
  Math.PI * 2
232
- ), f.clip()), f.drawImage(
287
+ ), a.clip()), a.drawImage(
233
288
  h,
234
289
  u,
235
- a,
290
+ p,
236
291
  e.width,
237
292
  e.height,
238
293
  0,
239
294
  0,
240
- s,
241
- o
242
- ), n === "circle" && f.restore(), l;
295
+ o,
296
+ s
297
+ ), n === "circle" && a.restore(), r;
243
298
  }
244
- async function k(i, t, e, s, o) {
245
- const n = y(o.type, X, "base64", "export type"), r = y(o.format, Y, "image/png", "export format"), c = o.quality, d = o.width ?? e.width, h = o.height ?? e.height;
246
- s === "circle" && r === "image/jpeg" && console.warn(
299
+ async function ot(i, t, e, o, s) {
300
+ const n = D(s.type, Q, "base64", "export type"), l = D(s.format, tt, "image/png", "export format"), c = s.quality, m = s.width ?? e.width, h = s.height ?? e.height;
301
+ o === "circle" && l === "image/jpeg" && console.warn(
247
302
  "Kiri: exporting a circle-shaped frame as image/jpeg — JPEG has no alpha channel, so the area outside the circle will render as solid black instead of transparent. Use image/png or image/webp instead."
248
303
  );
249
- const m = W(i, t, e, d, h, s);
250
- return n === "canvas" ? m : n === "base64" ? m.toDataURL(r, c) : new Promise((u, a) => {
251
- m.toBlob(
252
- (l) => l ? u(l) : a(new Error("Kiri: canvas.toBlob failed")),
253
- r,
304
+ const f = it(i, t, e, m, h, o);
305
+ return n === "canvas" ? f : n === "base64" ? f.toDataURL(l, c) : new Promise((u, p) => {
306
+ f.toBlob(
307
+ (r) => r ? u(r) : p(new Error("Kiri: canvas.toBlob failed")),
308
+ l,
254
309
  c
255
310
  );
256
311
  });
257
312
  }
258
- async function B(i, t) {
259
- const e = t.fieldName ?? "file", s = t.fileName ?? `crop.${G(t.format ?? "image/png")}`, o = new FormData();
260
- o.append(e, i, s);
261
- for (const [n, r] of Object.entries(t.extraFields ?? {}))
262
- o.append(n, r);
313
+ async function st(i, t) {
314
+ const e = t.fieldName ?? "file", o = t.fileName ?? `crop.${nt(t.format ?? "image/png")}`, s = new FormData();
315
+ s.append(e, i, o);
316
+ for (const [n, l] of Object.entries(t.extraFields ?? {}))
317
+ s.append(n, l);
263
318
  return fetch(t.url, {
264
319
  ...t.fetchOptions,
265
320
  method: "POST",
266
- body: o
321
+ body: s
267
322
  });
268
323
  }
269
- function G(i) {
324
+ function nt(i) {
270
325
  switch (i) {
271
326
  case "image/jpeg":
272
327
  return "jpg";
@@ -276,67 +331,70 @@ function G(i) {
276
331
  return "png";
277
332
  }
278
333
  }
279
- const P = 200, O = 20, q = ["rectangle", "circle"], J = ["top", "bottom", "left", "right"];
280
- function Q(i) {
334
+ const U = 200, _ = 20, rt = ["rectangle", "circle"], at = ["top", "bottom", "left", "right"];
335
+ function ht(i) {
281
336
  return i === void 0 ? !0 : typeof i == "boolean" || i === "ctrl" ? i : (console.warn(
282
337
  `Kiri: invalid mouseWheelZoom "${String(i)}" — defaulting to true. Valid values: true, false, "ctrl".`
283
338
  ), !0);
284
339
  }
285
- class tt {
340
+ class lt {
286
341
  /**
287
342
  * @param container An element already present in the DOM. Passing
288
343
  * `null`/`undefined`, or an element that isn't in the DOM yet, throws.
289
344
  * @param options See the {@link KiriOptions} fields for defaults.
290
345
  */
291
346
  constructor(t, e = {}) {
292
- p(this, "container");
293
- p(this, "opts");
294
- p(this, "stage");
295
- p(this, "gestureHandle");
296
- p(this, "resizeHandle", null);
297
- p(this, "zoomerHandle", null);
298
- p(this, "naturalSize", { width: 0, height: 0 });
299
- p(this, "state", {
347
+ d(this, "container");
348
+ d(this, "opts");
349
+ d(this, "stage");
350
+ d(this, "gestureHandle");
351
+ d(this, "resizeHandle", null);
352
+ d(this, "zoomerHandle", null);
353
+ d(this, "naturalSize", { width: 0, height: 0 });
354
+ d(this, "state", {
300
355
  zoom: 1,
301
356
  offset: { x: 0, y: 0 },
302
357
  rotation: 0,
303
358
  flip: { horizontal: !1, vertical: !1 },
304
- filters: L
359
+ filters: A
305
360
  });
306
- p(this, "listeners", { change: [] });
307
- var s, o, n;
361
+ /** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */
362
+ d(this, "initialState", null);
363
+ d(this, "listeners", { change: [] });
364
+ var o, s, n;
308
365
  if (!t || typeof t.appendChild != "function")
309
366
  throw new Error(
310
367
  "Kiri: container element is null/undefined or not a DOM element. This usually means the element wasn't in the DOM yet when `new Kiri(...)` ran — e.g. document.getElementById() was called before the element existed. Place the <script> after the element, or construct inside a DOMContentLoaded listener."
311
368
  );
312
369
  this.container = t, this.opts = {
313
370
  frame: {
314
- shape: y(
315
- (s = e.frame) == null ? void 0 : s.shape,
316
- q,
371
+ shape: D(
372
+ (o = e.frame) == null ? void 0 : o.shape,
373
+ rt,
317
374
  "rectangle",
318
375
  "frame.shape"
319
376
  ),
320
- width: ((o = e.frame) == null ? void 0 : o.width) ?? P,
321
- height: ((n = e.frame) == null ? void 0 : n.height) ?? P
377
+ width: ((s = e.frame) == null ? void 0 : s.width) ?? U,
378
+ height: ((n = e.frame) == null ? void 0 : n.height) ?? U
322
379
  },
323
380
  minZoom: e.minZoom ?? 1,
324
381
  maxZoom: e.maxZoom ?? 4,
325
382
  rotatable: e.rotatable ?? !0,
326
383
  flippable: e.flippable ?? !0,
327
384
  resizableFrame: e.resizableFrame ?? !1,
328
- mouseWheelZoom: Q(e.mouseWheelZoom),
385
+ lockAspectRatio: e.lockAspectRatio ?? !1,
386
+ mouseWheelZoom: ht(e.mouseWheelZoom),
329
387
  useExifOrientation: e.useExifOrientation ?? !0,
330
388
  uploader: e.uploader,
331
389
  autoSizeStage: e.autoSizeStage ?? !0,
332
390
  showZoomer: e.showZoomer ?? !1,
333
- zoomerPosition: y(
391
+ zoomerPosition: D(
334
392
  e.zoomerPosition,
335
- J,
393
+ at,
336
394
  "bottom",
337
395
  "zoomerPosition"
338
396
  )
339
- }, this.state.filters = M(L, e.filters ?? {}), this.stage = $(
397
+ }, this.state.filters = I(A, e.filters ?? {}), this.stage = j(
340
398
  this.container,
341
399
  this.opts.frame.shape,
342
400
  this.opts.frame.width,
@@ -348,14 +406,15 @@ class tt {
348
406
  max: this.opts.maxZoom,
349
407
  value: this.state.zoom
350
408
  }
351
- ), this.opts.autoSizeStage && this.syncStageSize(), Z(this.stage.imgEl, this.state.filters), this.stage.zoomerEl && this.enableZoomer(this.stage.zoomerEl), this.gestureHandle = _(
409
+ ), this.opts.autoSizeStage && this.syncStageSize(), R(this.stage.imgEl, this.state.filters), this.stage.zoomerEl && this.enableZoomer(this.stage.zoomerEl), this.gestureHandle = B(
352
410
  this.stage.stageEl,
353
411
  {
354
412
  getNaturalSize: () => this.naturalSize,
355
413
  getFrameSize: () => this.getFrameSize(),
356
414
  getState: () => this.state,
357
415
  getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),
358
- setState: (r) => this.commitState(r)
416
+ setState: (l) => this.commitState(l),
417
+ reset: () => this.reset()
359
418
  },
360
419
  { mouseWheelZoom: this.opts.mouseWheelZoom }
361
420
  ), this.opts.resizableFrame && this.enableFrameResize();
@@ -370,37 +429,37 @@ class tt {
370
429
  * @param loadOptions Initial `zoom`/`offset`/`rotation`/`flip`.
371
430
  */
372
431
  async load(t, e = {}) {
373
- var u, a;
374
- let s = S(e.rotation ?? 0), o = ((u = e.flip) == null ? void 0 : u.horizontal) ?? !1;
375
- const n = ((a = e.flip) == null ? void 0 : a.vertical) ?? !1;
376
- let r = null, c;
432
+ var u, p;
433
+ let o = b(e.rotation ?? 0), s = ((u = e.flip) == null ? void 0 : u.horizontal) ?? !1;
434
+ const n = ((p = e.flip) == null ? void 0 : p.vertical) ?? !1;
435
+ let l = null, c;
377
436
  if (typeof t == "string")
378
437
  c = t;
379
438
  else {
380
439
  if (this.opts.useExifOrientation) {
381
- const l = await t.arrayBuffer(), f = A(l), g = j(f);
382
- s = S(s + g.rotation), o = o !== g.flipHorizontal;
440
+ const r = await t.arrayBuffer(), a = G(r), g = J(a);
441
+ o = b(o + g.rotation), s = s !== g.flipHorizontal;
383
442
  }
384
- r = URL.createObjectURL(t), c = r;
443
+ l = URL.createObjectURL(t), c = l;
385
444
  }
386
- await new Promise((l, f) => {
387
- this.stage.imgEl.onload = () => l(), this.stage.imgEl.onerror = () => f(new Error("Kiri: failed to load image")), this.stage.imgEl.src = c;
388
- }), r && URL.revokeObjectURL(r), this.naturalSize = {
445
+ await new Promise((r, a) => {
446
+ this.stage.imgEl.onload = () => r(), this.stage.imgEl.onerror = () => a(new Error("Kiri: failed to load image")), this.stage.imgEl.src = c;
447
+ }), l && URL.revokeObjectURL(l), this.naturalSize = {
389
448
  width: this.stage.imgEl.naturalWidth,
390
449
  height: this.stage.imgEl.naturalHeight
391
450
  };
392
- const d = E(
451
+ const m = P(
393
452
  e.zoom ?? this.opts.minZoom,
394
453
  this.opts.minZoom,
395
454
  this.opts.maxZoom
396
- ), h = z(this.naturalSize, this.getFrameSize(), s, d), m = w(e.offset ?? { x: 0, y: 0 }, h, this.getFrameSize());
455
+ ), h = S(this.naturalSize, this.getFrameSize(), o, m), f = x(e.offset ?? { x: 0, y: 0 }, h, this.getFrameSize());
397
456
  this.commitState({
398
- zoom: d,
399
- offset: m,
400
- rotation: s,
401
- flip: { horizontal: o, vertical: n },
457
+ zoom: m,
458
+ offset: f,
459
+ rotation: o,
460
+ flip: { horizontal: s, vertical: n },
402
461
  filters: this.state.filters
403
- });
462
+ }), this.initialState = this.getState();
404
463
  }
405
464
  /** A snapshot of the current state — mutating the returned object has no effect. */
406
465
  getState() {
@@ -414,13 +473,41 @@ class tt {
414
473
  }
415
474
  /** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */
416
475
  setZoom(t) {
417
- const e = E(t, this.opts.minZoom, this.opts.maxZoom), s = z(
476
+ const e = P(t, this.opts.minZoom, this.opts.maxZoom), o = S(
418
477
  this.naturalSize,
419
478
  this.getFrameSize(),
420
479
  this.state.rotation,
421
480
  e
422
- ), o = w(this.state.offset, s, this.getFrameSize());
423
- this.commitState({ ...this.state, zoom: e, offset: o });
481
+ ), s = x(this.state.offset, o, this.getFrameSize());
482
+ this.commitState({ ...this.state, zoom: e, offset: s });
483
+ }
484
+ /**
485
+ * Sets the pan offset to an absolute value (image-center offset from the
486
+ * frame center, in stage pixels), clamped so the frame stays fully covered
487
+ * by the rendered image.
488
+ */
489
+ setOffset(t) {
490
+ const e = S(
491
+ this.naturalSize,
492
+ this.getFrameSize(),
493
+ this.state.rotation,
494
+ this.state.zoom
495
+ ), o = x(t, e, this.getFrameSize());
496
+ this.commitState({ ...this.state, offset: o });
497
+ }
498
+ /**
499
+ * Reverts zoom/offset/rotation/flip/filters to what they were right after
500
+ * `load()` resolved (including any `loadOptions` passed to it). No-op if
501
+ * nothing has been loaded yet.
502
+ */
503
+ reset() {
504
+ this.initialState && this.commitState({
505
+ zoom: this.initialState.zoom,
506
+ offset: { ...this.initialState.offset },
507
+ rotation: this.initialState.rotation,
508
+ flip: { ...this.initialState.flip },
509
+ filters: { ...this.initialState.filters }
510
+ });
424
511
  }
425
512
  /**
426
513
  * Rotates relative to the current rotation, snapped to the nearest 90°.
@@ -428,13 +515,13 @@ class tt {
428
515
  */
429
516
  rotate(t) {
430
517
  if (!this.opts.rotatable) return;
431
- const e = Math.round(t / 90) * 90, s = S(this.state.rotation + e), o = z(
518
+ const e = Math.round(t / 90) * 90, o = b(this.state.rotation + e), s = S(
432
519
  this.naturalSize,
433
520
  this.getFrameSize(),
434
- s,
521
+ o,
435
522
  this.state.zoom
436
- ), n = w(this.state.offset, o, this.getFrameSize());
437
- this.commitState({ ...this.state, rotation: s, offset: n });
523
+ ), n = x(this.state.offset, s, this.getFrameSize());
524
+ this.commitState({ ...this.state, rotation: o, offset: n });
438
525
  }
439
526
  /** Toggles horizontal flip, independent of rotation. No-op if `flippable: false`. */
440
527
  flipHorizontal() {
@@ -454,21 +541,21 @@ class tt {
454
541
  * minimum.
455
542
  */
456
543
  setFrameSize(t, e) {
457
- this.opts.frame.width = Math.max(O, t), this.opts.frame.height = Math.max(O, e), H(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height), this.opts.autoSizeStage && this.syncStageSize();
458
- const s = z(
544
+ this.opts.frame.width = Math.max(_, t), this.opts.frame.height = Math.max(_, e), k(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height), this.opts.autoSizeStage && this.syncStageSize();
545
+ const o = S(
459
546
  this.naturalSize,
460
547
  this.getFrameSize(),
461
548
  this.state.rotation,
462
549
  this.state.zoom
463
- ), o = w(this.state.offset, s, this.getFrameSize());
464
- this.commitState({ ...this.state, offset: o });
550
+ ), s = x(this.state.offset, o, this.getFrameSize());
551
+ this.commitState({ ...this.state, offset: s });
465
552
  }
466
553
  /**
467
554
  * Merges a partial update into the current filters (omitted fields are
468
555
  * left as they are). Numeric values are clamped to `>= 0`.
469
556
  */
470
557
  setFilters(t) {
471
- this.commitState({ ...this.state, filters: M(this.state.filters, t) });
558
+ this.commitState({ ...this.state, filters: I(this.state.filters, t) });
472
559
  }
473
560
  /**
474
561
  * Renders the current crop. A circle frame is a real clip in the output
@@ -477,7 +564,7 @@ class tt {
477
564
  * @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
478
565
  */
479
566
  async export(t = {}) {
480
- return k(
567
+ return ot(
481
568
  this.stage.imgEl,
482
569
  this.state,
483
570
  this.getFrameSize(),
@@ -485,14 +572,23 @@ class tt {
485
572
  t
486
573
  );
487
574
  }
575
+ /**
576
+ * The current crop selection as a rectangle in the original, unrotated,
577
+ * unflipped source image's own pixel coordinates — for sending to a server
578
+ * that will crop the full-resolution original itself instead of uploading
579
+ * a client-re-encoded image. See {@link CropRegion}.
580
+ */
581
+ getCropRegion() {
582
+ return et(this.naturalSize, this.getFrameSize(), this.state);
583
+ }
488
584
  /**
489
585
  * Exports the current crop as a blob, then uploads it — a default
490
586
  * FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`
491
587
  * wins over the constructor's, which wins over the built-in default).
492
588
  */
493
589
  async upload(t, e = {}) {
494
- const s = await this.export({ ...e, type: "blob" });
495
- return (e.uploader ?? this.opts.uploader ?? B)(s, { ...e, url: t });
590
+ const o = await this.export({ ...e, type: "blob" });
591
+ return (e.uploader ?? this.opts.uploader ?? st)(o, { ...e, url: t });
496
592
  }
497
593
  /** Subscribes to `"change"` — fires on every state update (drag/zoom/rotate/flip/filters), and once after `load()` resolves. */
498
594
  on(t, e) {
@@ -500,7 +596,7 @@ class tt {
500
596
  }
501
597
  /** Unsubscribes a callback previously passed to {@link on}. */
502
598
  off(t, e) {
503
- this.listeners[t] = this.listeners[t].filter((s) => s !== e);
599
+ this.listeners[t] = this.listeners[t].filter((o) => o !== e);
504
600
  }
505
601
  /**
506
602
  * Tears the instance down: removes all pointer/wheel event listeners
@@ -518,17 +614,17 @@ class tt {
518
614
  return { width: this.opts.frame.width, height: this.opts.frame.height };
519
615
  }
520
616
  syncStageSize() {
521
- R(
617
+ V(
522
618
  this.stage.stageEl,
523
- this.opts.frame.width + F * 2,
524
- this.opts.frame.height + F * 2
619
+ this.opts.frame.width + C * 2,
620
+ this.opts.frame.height + C * 2
525
621
  );
526
622
  }
527
623
  commitState(t) {
528
624
  this.state = t;
529
- const e = b(this.naturalSize, this.getFrameSize(), t.rotation) * t.zoom;
530
- N(this.stage.imageLayerEl, t, e), Z(this.stage.imgEl, t.filters), this.stage.zoomerEl && (this.stage.zoomerEl.value = String(t.zoom));
531
- for (const s of this.listeners.change) s(this.getState());
625
+ const e = O(this.naturalSize, this.getFrameSize(), t.rotation) * t.zoom;
626
+ W(this.stage.imageLayerEl, t, e), R(this.stage.imgEl, t.filters), this.stage.zoomerEl && (this.stage.zoomerEl.value = String(t.zoom));
627
+ for (const o of this.listeners.change) o(this.getState());
532
628
  }
533
629
  enableZoomer(t) {
534
630
  const e = () => this.setZoom(Number(t.value));
@@ -539,39 +635,54 @@ class tt {
539
635
  };
540
636
  }
541
637
  enableFrameResize() {
542
- const t = document.createElement("div");
543
- t.className = "kiri-frame-handle", t.style.right = "-5px", t.style.bottom = "-5px", this.stage.frameEl.appendChild(t);
544
- let e = null;
545
- const s = (r) => {
546
- r.stopPropagation(), t.setPointerCapture(r.pointerId), e = {
547
- x: r.clientX,
548
- y: r.clientY,
549
- width: this.opts.frame.width,
550
- height: this.opts.frame.height
638
+ const t = [
639
+ { name: "top-left", edge: { left: "-5px", top: "-5px" }, signX: -1, signY: -1 },
640
+ { name: "top-right", edge: { right: "-5px", top: "-5px" }, signX: 1, signY: -1 },
641
+ { name: "bottom-left", edge: { left: "-5px", bottom: "-5px" }, signX: -1, signY: 1 },
642
+ { name: "bottom-right", edge: { right: "-5px", bottom: "-5px" }, signX: 1, signY: 1 }
643
+ ], e = [];
644
+ for (const o of t) {
645
+ const s = document.createElement("div");
646
+ s.className = `kiri-frame-handle kiri-frame-handle--${o.name}`, Object.assign(s.style, o.edge), this.stage.frameEl.appendChild(s);
647
+ let n = null;
648
+ const l = (h) => {
649
+ h.stopPropagation(), s.setPointerCapture(h.pointerId), n = {
650
+ x: h.clientX,
651
+ y: h.clientY,
652
+ width: this.opts.frame.width,
653
+ height: this.opts.frame.height
654
+ };
655
+ }, c = (h) => {
656
+ if (!n) return;
657
+ const f = (h.clientX - n.x) * 2 * o.signX, u = (h.clientY - n.y) * 2 * o.signY, p = n.width + f, r = n.height + u;
658
+ if (this.opts.lockAspectRatio) {
659
+ const a = n.width / n.height;
660
+ Math.abs(p - n.width) >= Math.abs(r - n.height) * a ? this.setFrameSize(p, p / a) : this.setFrameSize(r * a, r);
661
+ } else
662
+ this.setFrameSize(p, r);
663
+ }, m = () => {
664
+ n = null;
551
665
  };
552
- }, o = (r) => {
553
- if (!e) return;
554
- const c = (r.clientX - e.x) * 2, d = (r.clientY - e.y) * 2;
555
- this.setFrameSize(e.width + c, e.height + d);
556
- }, n = () => {
557
- e = null;
558
- };
559
- t.addEventListener("pointerdown", s), t.addEventListener("pointermove", o), t.addEventListener("pointerup", n), t.addEventListener("pointercancel", n), this.resizeHandle = {
666
+ s.addEventListener("pointerdown", l), s.addEventListener("pointermove", c), s.addEventListener("pointerup", m), s.addEventListener("pointercancel", m), e.push(() => {
667
+ s.removeEventListener("pointerdown", l), s.removeEventListener("pointermove", c), s.removeEventListener("pointerup", m), s.removeEventListener("pointercancel", m), s.remove();
668
+ });
669
+ }
670
+ this.resizeHandle = {
560
671
  destroy() {
561
- t.removeEventListener("pointerdown", s), t.removeEventListener("pointermove", o), t.removeEventListener("pointerup", n), t.removeEventListener("pointercancel", n), t.remove();
672
+ for (const o of e) o();
562
673
  }
563
674
  };
564
675
  }
565
676
  }
566
- class it {
677
+ class ft {
567
678
  /** @param items An initial queue; more can be added later via `add()`. */
568
- constructor(t, e = {}, s = []) {
679
+ constructor(t, e = {}, o = []) {
569
680
  /** The shared `Kiri` instance. Use its normal methods (drag/zoom/rotate/filters/etc.) to adjust the currently-loaded item. */
570
- p(this, "cropper");
571
- p(this, "items");
572
- p(this, "index", -1);
573
- p(this, "captures", []);
574
- this.cropper = new tt(t, e), this.items = [...s];
681
+ d(this, "cropper");
682
+ d(this, "items");
683
+ d(this, "index", -1);
684
+ d(this, "captures", []);
685
+ this.cropper = new lt(t, e), this.items = [...o];
575
686
  }
576
687
  /** Appends an item to the queue. */
577
688
  add(t) {
@@ -588,6 +699,13 @@ class it {
588
699
  const t = this.items[this.index];
589
700
  return await this.cropper.load(t.source, t.loadOptions), !0;
590
701
  }
702
+ /** Loads the previous queued image into `cropper`. Returns false when already at the first item (or nothing loaded yet). */
703
+ async previous() {
704
+ if (this.index <= 0) return !1;
705
+ this.index -= 1;
706
+ const t = this.items[this.index];
707
+ return await this.cropper.load(t.source, t.loadOptions), !0;
708
+ }
591
709
  /** Metadata for the currently loaded item, or null before the first next() / after exhaustion. */
592
710
  current() {
593
711
  return this.items[this.index] ?? null;
@@ -607,7 +725,7 @@ class it {
607
725
  }
608
726
  }
609
727
  export {
610
- tt as Kiri,
611
- it as KiriBatch
728
+ lt as Kiri,
729
+ ft as KiriBatch
612
730
  };
613
731
  //# sourceMappingURL=kiri.mjs.map