@michaelyagi/kiri 0.1.0-alpha.2
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/batch.d.ts +34 -0
- package/dist/exif.d.ts +17 -0
- package/dist/export.d.ts +16 -0
- package/dist/filters.d.ts +11 -0
- package/dist/gestures.d.ts +35 -0
- package/dist/index.d.ts +4 -0
- package/dist/kiri.css +72 -0
- package/dist/kiri.d.ts +88 -0
- package/dist/kiri.js +779 -0
- package/dist/kiri.js.map +1 -0
- package/dist/kiri.min.css +1 -0
- package/dist/kiri.min.js +10 -0
- package/dist/kiri.min.js.map +1 -0
- package/dist/kiri.mjs +613 -0
- package/dist/kiri.mjs.map +1 -0
- package/dist/stage.d.ts +24 -0
- package/dist/types.d.ts +117 -0
- package/dist/upload.d.ts +10 -0
- package/dist/validate.d.ts +7 -0
- package/package.json +57 -0
package/dist/kiri.mjs
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
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 = {
|
|
5
|
+
brightness: 1,
|
|
6
|
+
contrast: 1,
|
|
7
|
+
saturation: 1,
|
|
8
|
+
grayscale: !1,
|
|
9
|
+
sepia: !1
|
|
10
|
+
};
|
|
11
|
+
function M(i, t) {
|
|
12
|
+
return {
|
|
13
|
+
brightness: Math.max(0, t.brightness ?? i.brightness),
|
|
14
|
+
contrast: Math.max(0, t.contrast ?? i.contrast),
|
|
15
|
+
saturation: Math.max(0, t.saturation ?? i.saturation),
|
|
16
|
+
grayscale: t.grayscale ?? i.grayscale,
|
|
17
|
+
sepia: t.sepia ?? i.sepia
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function C(i) {
|
|
21
|
+
const t = [
|
|
22
|
+
`brightness(${i.brightness})`,
|
|
23
|
+
`contrast(${i.contrast})`,
|
|
24
|
+
`saturate(${i.saturation})`
|
|
25
|
+
];
|
|
26
|
+
return i.grayscale && t.push("grayscale(1)"), i.sepia && t.push("sepia(1)"), t.join(" ");
|
|
27
|
+
}
|
|
28
|
+
function $(i, t, e, s, o) {
|
|
29
|
+
i.innerHTML = "";
|
|
30
|
+
const n = document.createElement("div");
|
|
31
|
+
n.className = "kiri-stage";
|
|
32
|
+
const r = document.createElement("div");
|
|
33
|
+
r.className = "kiri-image-layer";
|
|
34
|
+
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
|
+
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
|
+
} else
|
|
43
|
+
i.appendChild(n);
|
|
44
|
+
return { stageEl: n, frameEl: d, imageLayerEl: r, imgEl: c, zoomerEl: h };
|
|
45
|
+
}
|
|
46
|
+
function H(i, t, e) {
|
|
47
|
+
i.style.width = `${t}px`, i.style.height = `${e}px`;
|
|
48
|
+
}
|
|
49
|
+
const F = 20;
|
|
50
|
+
function R(i, t, e) {
|
|
51
|
+
i.style.width = `${t}px`, i.style.height = `${e}px`;
|
|
52
|
+
}
|
|
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
|
+
}
|
|
57
|
+
function Z(i, t) {
|
|
58
|
+
i.style.filter = C(t);
|
|
59
|
+
}
|
|
60
|
+
function D(i, t) {
|
|
61
|
+
return (t / 90 % 2 + 2) % 2 === 1 ? { width: i.height, height: i.width } : { width: i.width, height: i.height };
|
|
62
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
71
|
+
function E(i, t, e) {
|
|
72
|
+
return Math.min(Math.max(i, t), e);
|
|
73
|
+
}
|
|
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);
|
|
76
|
+
return {
|
|
77
|
+
x: Math.min(Math.max(i.x, -s), s) || 0,
|
|
78
|
+
y: Math.min(Math.max(i.y, -o), o) || 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function S(i) {
|
|
82
|
+
return (i % 360 + 360) % 360;
|
|
83
|
+
}
|
|
84
|
+
function I(i, t) {
|
|
85
|
+
return Math.hypot(i.clientX - t.clientX, i.clientY - t.clientY);
|
|
86
|
+
}
|
|
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(
|
|
92
|
+
t.getNaturalSize(),
|
|
93
|
+
t.getFrameSize(),
|
|
94
|
+
a.rotation,
|
|
95
|
+
g
|
|
96
|
+
), x = w(a.offset, v, t.getFrameSize());
|
|
97
|
+
t.setState({
|
|
98
|
+
zoom: g,
|
|
99
|
+
offset: x,
|
|
100
|
+
rotation: S(a.rotation),
|
|
101
|
+
flip: a.flip,
|
|
102
|
+
filters: a.filters
|
|
103
|
+
});
|
|
104
|
+
}
|
|
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;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
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
|
+
if (n > 0) {
|
|
120
|
+
const v = g / n, x = t.getState();
|
|
121
|
+
c({ ...x, zoom: r * v });
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (o) {
|
|
126
|
+
const l = a.clientX - o.x, f = a.clientY - o.y, g = t.getState();
|
|
127
|
+
c({
|
|
128
|
+
...g,
|
|
129
|
+
offset: { x: o.offset.x + l, y: o.offset.y + f }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
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) });
|
|
142
|
+
}
|
|
143
|
+
return i.addEventListener("pointerdown", d), i.addEventListener("pointermove", h), i.addEventListener("pointerup", m), i.addEventListener("pointercancel", m), i.addEventListener("wheel", u, { passive: !1 }), {
|
|
144
|
+
destroy() {
|
|
145
|
+
i.removeEventListener("pointerdown", d), i.removeEventListener("pointermove", h), i.removeEventListener("pointerup", m), i.removeEventListener("pointercancel", m), i.removeEventListener("wheel", u);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function A(i) {
|
|
150
|
+
const t = new DataView(i);
|
|
151
|
+
if (t.byteLength < 4 || t.getUint16(0, !1) !== 65496) return 1;
|
|
152
|
+
let e = 2;
|
|
153
|
+
for (; e + 4 <= t.byteLength; ) {
|
|
154
|
+
const s = t.getUint16(e, !1), o = t.getUint16(e + 2, !1);
|
|
155
|
+
if (s === 65505) {
|
|
156
|
+
const n = e + 4;
|
|
157
|
+
return t.getUint32(n, !1) !== 1165519206 ? 1 : K(t, n + 6);
|
|
158
|
+
}
|
|
159
|
+
if ((s & 65280) !== 65280) break;
|
|
160
|
+
e += 2 + o;
|
|
161
|
+
}
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
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;
|
|
170
|
+
if (c + 12 > i.byteLength) break;
|
|
171
|
+
if (i.getUint16(c, e) === 274) {
|
|
172
|
+
const h = i.getUint16(c + 8, e);
|
|
173
|
+
return h >= 1 && h <= 8 ? h : 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
function j(i) {
|
|
179
|
+
switch (i) {
|
|
180
|
+
case 2:
|
|
181
|
+
return { rotation: 0, flipHorizontal: !0 };
|
|
182
|
+
case 3:
|
|
183
|
+
return { rotation: 180, flipHorizontal: !1 };
|
|
184
|
+
case 4:
|
|
185
|
+
return { rotation: 180, flipHorizontal: !0 };
|
|
186
|
+
case 5:
|
|
187
|
+
return { rotation: 90, flipHorizontal: !0 };
|
|
188
|
+
case 6:
|
|
189
|
+
return { rotation: 90, flipHorizontal: !1 };
|
|
190
|
+
case 7:
|
|
191
|
+
return { rotation: 270, flipHorizontal: !0 };
|
|
192
|
+
case 8:
|
|
193
|
+
return { rotation: 270, flipHorizontal: !1 };
|
|
194
|
+
default:
|
|
195
|
+
return { rotation: 0, flipHorizontal: !1 };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function y(i, t, e, s) {
|
|
199
|
+
return i === void 0 ? e : t.includes(i) ? i : (console.warn(
|
|
200
|
+
`Kiri: invalid ${s} "${i}" — defaulting to "${e}". Valid values: ${t.join(", ")}.`
|
|
201
|
+
), e);
|
|
202
|
+
}
|
|
203
|
+
const X = ["base64", "blob", "canvas"], Y = ["image/jpeg", "image/png", "image/webp"];
|
|
204
|
+
function V(i, t, e) {
|
|
205
|
+
return {
|
|
206
|
+
left: i.width / 2 - t.x - e.width / 2,
|
|
207
|
+
top: i.height / 2 - t.y - e.height / 2
|
|
208
|
+
};
|
|
209
|
+
}
|
|
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,
|
|
218
|
+
t.offset,
|
|
219
|
+
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,
|
|
226
|
+
o / 2,
|
|
227
|
+
s / 2,
|
|
228
|
+
o / 2,
|
|
229
|
+
0,
|
|
230
|
+
0,
|
|
231
|
+
Math.PI * 2
|
|
232
|
+
), f.clip()), f.drawImage(
|
|
233
|
+
h,
|
|
234
|
+
u,
|
|
235
|
+
a,
|
|
236
|
+
e.width,
|
|
237
|
+
e.height,
|
|
238
|
+
0,
|
|
239
|
+
0,
|
|
240
|
+
s,
|
|
241
|
+
o
|
|
242
|
+
), n === "circle" && f.restore(), l;
|
|
243
|
+
}
|
|
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(
|
|
247
|
+
"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
|
+
);
|
|
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,
|
|
254
|
+
c
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
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);
|
|
263
|
+
return fetch(t.url, {
|
|
264
|
+
...t.fetchOptions,
|
|
265
|
+
method: "POST",
|
|
266
|
+
body: o
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function G(i) {
|
|
270
|
+
switch (i) {
|
|
271
|
+
case "image/jpeg":
|
|
272
|
+
return "jpg";
|
|
273
|
+
case "image/webp":
|
|
274
|
+
return "webp";
|
|
275
|
+
default:
|
|
276
|
+
return "png";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const P = 200, O = 20, q = ["rectangle", "circle"], J = ["top", "bottom", "left", "right"];
|
|
280
|
+
function Q(i) {
|
|
281
|
+
return i === void 0 ? !0 : typeof i == "boolean" || i === "ctrl" ? i : (console.warn(
|
|
282
|
+
`Kiri: invalid mouseWheelZoom "${String(i)}" — defaulting to true. Valid values: true, false, "ctrl".`
|
|
283
|
+
), !0);
|
|
284
|
+
}
|
|
285
|
+
class tt {
|
|
286
|
+
/**
|
|
287
|
+
* @param container An element already present in the DOM. Passing
|
|
288
|
+
* `null`/`undefined`, or an element that isn't in the DOM yet, throws.
|
|
289
|
+
* @param options See the {@link KiriOptions} fields for defaults.
|
|
290
|
+
*/
|
|
291
|
+
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", {
|
|
300
|
+
zoom: 1,
|
|
301
|
+
offset: { x: 0, y: 0 },
|
|
302
|
+
rotation: 0,
|
|
303
|
+
flip: { horizontal: !1, vertical: !1 },
|
|
304
|
+
filters: L
|
|
305
|
+
});
|
|
306
|
+
p(this, "listeners", { change: [] });
|
|
307
|
+
var s, o, n;
|
|
308
|
+
if (!t || typeof t.appendChild != "function")
|
|
309
|
+
throw new Error(
|
|
310
|
+
"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
|
+
);
|
|
312
|
+
this.container = t, this.opts = {
|
|
313
|
+
frame: {
|
|
314
|
+
shape: y(
|
|
315
|
+
(s = e.frame) == null ? void 0 : s.shape,
|
|
316
|
+
q,
|
|
317
|
+
"rectangle",
|
|
318
|
+
"frame.shape"
|
|
319
|
+
),
|
|
320
|
+
width: ((o = e.frame) == null ? void 0 : o.width) ?? P,
|
|
321
|
+
height: ((n = e.frame) == null ? void 0 : n.height) ?? P
|
|
322
|
+
},
|
|
323
|
+
minZoom: e.minZoom ?? 1,
|
|
324
|
+
maxZoom: e.maxZoom ?? 4,
|
|
325
|
+
rotatable: e.rotatable ?? !0,
|
|
326
|
+
flippable: e.flippable ?? !0,
|
|
327
|
+
resizableFrame: e.resizableFrame ?? !1,
|
|
328
|
+
mouseWheelZoom: Q(e.mouseWheelZoom),
|
|
329
|
+
useExifOrientation: e.useExifOrientation ?? !0,
|
|
330
|
+
uploader: e.uploader,
|
|
331
|
+
autoSizeStage: e.autoSizeStage ?? !0,
|
|
332
|
+
showZoomer: e.showZoomer ?? !1,
|
|
333
|
+
zoomerPosition: y(
|
|
334
|
+
e.zoomerPosition,
|
|
335
|
+
J,
|
|
336
|
+
"bottom",
|
|
337
|
+
"zoomerPosition"
|
|
338
|
+
)
|
|
339
|
+
}, this.state.filters = M(L, e.filters ?? {}), this.stage = $(
|
|
340
|
+
this.container,
|
|
341
|
+
this.opts.frame.shape,
|
|
342
|
+
this.opts.frame.width,
|
|
343
|
+
this.opts.frame.height,
|
|
344
|
+
{
|
|
345
|
+
show: this.opts.showZoomer,
|
|
346
|
+
position: this.opts.zoomerPosition,
|
|
347
|
+
min: this.opts.minZoom,
|
|
348
|
+
max: this.opts.maxZoom,
|
|
349
|
+
value: this.state.zoom
|
|
350
|
+
}
|
|
351
|
+
), this.opts.autoSizeStage && this.syncStageSize(), Z(this.stage.imgEl, this.state.filters), this.stage.zoomerEl && this.enableZoomer(this.stage.zoomerEl), this.gestureHandle = _(
|
|
352
|
+
this.stage.stageEl,
|
|
353
|
+
{
|
|
354
|
+
getNaturalSize: () => this.naturalSize,
|
|
355
|
+
getFrameSize: () => this.getFrameSize(),
|
|
356
|
+
getState: () => this.state,
|
|
357
|
+
getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),
|
|
358
|
+
setState: (r) => this.commitState(r)
|
|
359
|
+
},
|
|
360
|
+
{ mouseWheelZoom: this.opts.mouseWheelZoom }
|
|
361
|
+
), this.opts.resizableFrame && this.enableFrameResize();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Loads an image, replacing whatever was loaded before. EXIF orientation
|
|
365
|
+
* (rotation + horizontal flip) is corrected automatically unless
|
|
366
|
+
* `useExifOrientation: false` was passed to the constructor — only for
|
|
367
|
+
* `File`/`Blob` sources, since a plain URL string can't be read for EXIF
|
|
368
|
+
* data without an extra fetch.
|
|
369
|
+
* @param source A `File` (e.g. from a file input), a `Blob`, or a URL string.
|
|
370
|
+
* @param loadOptions Initial `zoom`/`offset`/`rotation`/`flip`.
|
|
371
|
+
*/
|
|
372
|
+
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;
|
|
377
|
+
if (typeof t == "string")
|
|
378
|
+
c = t;
|
|
379
|
+
else {
|
|
380
|
+
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;
|
|
383
|
+
}
|
|
384
|
+
r = URL.createObjectURL(t), c = r;
|
|
385
|
+
}
|
|
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 = {
|
|
389
|
+
width: this.stage.imgEl.naturalWidth,
|
|
390
|
+
height: this.stage.imgEl.naturalHeight
|
|
391
|
+
};
|
|
392
|
+
const d = E(
|
|
393
|
+
e.zoom ?? this.opts.minZoom,
|
|
394
|
+
this.opts.minZoom,
|
|
395
|
+
this.opts.maxZoom
|
|
396
|
+
), h = z(this.naturalSize, this.getFrameSize(), s, d), m = w(e.offset ?? { x: 0, y: 0 }, h, this.getFrameSize());
|
|
397
|
+
this.commitState({
|
|
398
|
+
zoom: d,
|
|
399
|
+
offset: m,
|
|
400
|
+
rotation: s,
|
|
401
|
+
flip: { horizontal: o, vertical: n },
|
|
402
|
+
filters: this.state.filters
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
/** A snapshot of the current state — mutating the returned object has no effect. */
|
|
406
|
+
getState() {
|
|
407
|
+
return {
|
|
408
|
+
zoom: this.state.zoom,
|
|
409
|
+
offset: { ...this.state.offset },
|
|
410
|
+
rotation: this.state.rotation,
|
|
411
|
+
flip: { ...this.state.flip },
|
|
412
|
+
filters: { ...this.state.filters }
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */
|
|
416
|
+
setZoom(t) {
|
|
417
|
+
const e = E(t, this.opts.minZoom, this.opts.maxZoom), s = z(
|
|
418
|
+
this.naturalSize,
|
|
419
|
+
this.getFrameSize(),
|
|
420
|
+
this.state.rotation,
|
|
421
|
+
e
|
|
422
|
+
), o = w(this.state.offset, s, this.getFrameSize());
|
|
423
|
+
this.commitState({ ...this.state, zoom: e, offset: o });
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Rotates relative to the current rotation, snapped to the nearest 90°.
|
|
427
|
+
* No-op if `rotatable: false` was passed to the constructor.
|
|
428
|
+
*/
|
|
429
|
+
rotate(t) {
|
|
430
|
+
if (!this.opts.rotatable) return;
|
|
431
|
+
const e = Math.round(t / 90) * 90, s = S(this.state.rotation + e), o = z(
|
|
432
|
+
this.naturalSize,
|
|
433
|
+
this.getFrameSize(),
|
|
434
|
+
s,
|
|
435
|
+
this.state.zoom
|
|
436
|
+
), n = w(this.state.offset, o, this.getFrameSize());
|
|
437
|
+
this.commitState({ ...this.state, rotation: s, offset: n });
|
|
438
|
+
}
|
|
439
|
+
/** Toggles horizontal flip, independent of rotation. No-op if `flippable: false`. */
|
|
440
|
+
flipHorizontal() {
|
|
441
|
+
if (!this.opts.flippable) return;
|
|
442
|
+
const t = { ...this.state.flip, horizontal: !this.state.flip.horizontal };
|
|
443
|
+
this.commitState({ ...this.state, flip: t });
|
|
444
|
+
}
|
|
445
|
+
/** Toggles vertical flip, independent of rotation. No-op if `flippable: false`. */
|
|
446
|
+
flipVertical() {
|
|
447
|
+
if (!this.opts.flippable) return;
|
|
448
|
+
const t = { ...this.state.flip, vertical: !this.state.flip.vertical };
|
|
449
|
+
this.commitState({ ...this.state, flip: t });
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Resizes the frame. Also resizes the stage to match, if
|
|
453
|
+
* `autoSizeStage: true` (the default). Each axis is clamped to a 20px
|
|
454
|
+
* minimum.
|
|
455
|
+
*/
|
|
456
|
+
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(
|
|
459
|
+
this.naturalSize,
|
|
460
|
+
this.getFrameSize(),
|
|
461
|
+
this.state.rotation,
|
|
462
|
+
this.state.zoom
|
|
463
|
+
), o = w(this.state.offset, s, this.getFrameSize());
|
|
464
|
+
this.commitState({ ...this.state, offset: o });
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Merges a partial update into the current filters (omitted fields are
|
|
468
|
+
* left as they are). Numeric values are clamped to `>= 0`.
|
|
469
|
+
*/
|
|
470
|
+
setFilters(t) {
|
|
471
|
+
this.commitState({ ...this.state, filters: M(this.state.filters, t) });
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Renders the current crop. A circle frame is a real clip in the output
|
|
475
|
+
* (transparent corners on PNG/WebP); a circle exported as JPEG warns and
|
|
476
|
+
* renders solid black corners instead, since JPEG has no alpha channel.
|
|
477
|
+
* @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
|
|
478
|
+
*/
|
|
479
|
+
async export(t = {}) {
|
|
480
|
+
return k(
|
|
481
|
+
this.stage.imgEl,
|
|
482
|
+
this.state,
|
|
483
|
+
this.getFrameSize(),
|
|
484
|
+
this.opts.frame.shape,
|
|
485
|
+
t
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Exports the current crop as a blob, then uploads it — a default
|
|
490
|
+
* FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`
|
|
491
|
+
* wins over the constructor's, which wins over the built-in default).
|
|
492
|
+
*/
|
|
493
|
+
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 });
|
|
496
|
+
}
|
|
497
|
+
/** Subscribes to `"change"` — fires on every state update (drag/zoom/rotate/flip/filters), and once after `load()` resolves. */
|
|
498
|
+
on(t, e) {
|
|
499
|
+
this.listeners[t].push(e);
|
|
500
|
+
}
|
|
501
|
+
/** Unsubscribes a callback previously passed to {@link on}. */
|
|
502
|
+
off(t, e) {
|
|
503
|
+
this.listeners[t] = this.listeners[t].filter((s) => s !== e);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Tears the instance down: removes all pointer/wheel event listeners
|
|
507
|
+
* (drag/zoom gestures), the resize-handle listener (if `resizableFrame`),
|
|
508
|
+
* and the zoom-slider listener (if `showZoomer`); clears the container's
|
|
509
|
+
* `innerHTML`, leaving an empty container element; and clears all
|
|
510
|
+
* `"change"` listeners. Call this when you're done with an instance (e.g.
|
|
511
|
+
* unmounting) to avoid leaking listeners.
|
|
512
|
+
*/
|
|
513
|
+
destroy() {
|
|
514
|
+
var t, e;
|
|
515
|
+
this.gestureHandle.destroy(), (t = this.resizeHandle) == null || t.destroy(), (e = this.zoomerHandle) == null || e.destroy(), this.container.innerHTML = "", this.listeners.change = [];
|
|
516
|
+
}
|
|
517
|
+
getFrameSize() {
|
|
518
|
+
return { width: this.opts.frame.width, height: this.opts.frame.height };
|
|
519
|
+
}
|
|
520
|
+
syncStageSize() {
|
|
521
|
+
R(
|
|
522
|
+
this.stage.stageEl,
|
|
523
|
+
this.opts.frame.width + F * 2,
|
|
524
|
+
this.opts.frame.height + F * 2
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
commitState(t) {
|
|
528
|
+
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());
|
|
532
|
+
}
|
|
533
|
+
enableZoomer(t) {
|
|
534
|
+
const e = () => this.setZoom(Number(t.value));
|
|
535
|
+
t.addEventListener("input", e), this.zoomerHandle = {
|
|
536
|
+
destroy() {
|
|
537
|
+
t.removeEventListener("input", e);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
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
|
|
551
|
+
};
|
|
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 = {
|
|
560
|
+
destroy() {
|
|
561
|
+
t.removeEventListener("pointerdown", s), t.removeEventListener("pointermove", o), t.removeEventListener("pointerup", n), t.removeEventListener("pointercancel", n), t.remove();
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
class it {
|
|
567
|
+
/** @param items An initial queue; more can be added later via `add()`. */
|
|
568
|
+
constructor(t, e = {}, s = []) {
|
|
569
|
+
/** 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];
|
|
575
|
+
}
|
|
576
|
+
/** Appends an item to the queue. */
|
|
577
|
+
add(t) {
|
|
578
|
+
this.items.push(t);
|
|
579
|
+
}
|
|
580
|
+
/** Total number of queued items. */
|
|
581
|
+
get length() {
|
|
582
|
+
return this.items.length;
|
|
583
|
+
}
|
|
584
|
+
/** Loads the next queued image into `cropper`. Returns false once the queue is exhausted. */
|
|
585
|
+
async next() {
|
|
586
|
+
if (this.index + 1 >= this.items.length) return !1;
|
|
587
|
+
this.index += 1;
|
|
588
|
+
const t = this.items[this.index];
|
|
589
|
+
return await this.cropper.load(t.source, t.loadOptions), !0;
|
|
590
|
+
}
|
|
591
|
+
/** Metadata for the currently loaded item, or null before the first next() / after exhaustion. */
|
|
592
|
+
current() {
|
|
593
|
+
return this.items[this.index] ?? null;
|
|
594
|
+
}
|
|
595
|
+
/** Exports the current crop and stores it, indexed by item order. */
|
|
596
|
+
async capture(t) {
|
|
597
|
+
const e = await this.cropper.export(t);
|
|
598
|
+
return this.captures[this.index] = e, e;
|
|
599
|
+
}
|
|
600
|
+
/** All captures made so far, in item order (sparse where an item hasn't been captured yet). */
|
|
601
|
+
results() {
|
|
602
|
+
return [...this.captures];
|
|
603
|
+
}
|
|
604
|
+
/** Tears down the shared `Kiri` instance. */
|
|
605
|
+
destroy() {
|
|
606
|
+
this.cropper.destroy();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
export {
|
|
610
|
+
tt as Kiri,
|
|
611
|
+
it as KiriBatch
|
|
612
|
+
};
|
|
613
|
+
//# sourceMappingURL=kiri.mjs.map
|