@michaelyagi/kiri 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +2 -0
- package/dist/export.d.ts +12 -1
- package/dist/gestures.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/kiri.css +7 -0
- package/dist/kiri.d.ts +22 -1
- package/dist/kiri.js +200 -31
- package/dist/kiri.js.map +1 -1
- package/dist/kiri.min.css +1 -1
- package/dist/kiri.min.js +1 -1
- package/dist/kiri.min.js.map +1 -1
- package/dist/kiri.mjs +340 -222
- package/dist/kiri.mjs.map +1 -1
- package/dist/types.d.ts +24 -1
- package/package.json +1 -1
package/dist/batch.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class KiriBatch {
|
|
|
23
23
|
get length(): number;
|
|
24
24
|
/** Loads the next queued image into `cropper`. Returns false once the queue is exhausted. */
|
|
25
25
|
next(): Promise<boolean>;
|
|
26
|
+
/** Loads the previous queued image into `cropper`. Returns false when already at the first item (or nothing loaded yet). */
|
|
27
|
+
previous(): Promise<boolean>;
|
|
26
28
|
/** Metadata for the currently loaded item, or null before the first next() / after exhaustion. */
|
|
27
29
|
current(): KiriBatchItem | null;
|
|
28
30
|
/** Exports the current crop and stores it, indexed by item order. */
|
package/dist/export.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExportOptions, ExportResult, FrameShape, KiriState, Offset } from './types';
|
|
1
|
+
import { CropRegion, ExportOptions, ExportResult, FrameShape, KiriState, Offset } from './types';
|
|
2
2
|
import { Size } from './gestures';
|
|
3
3
|
/**
|
|
4
4
|
* Top-left corner of the frame, in the local pixel space of the rendered
|
|
@@ -12,5 +12,16 @@ export declare function computeFrameSourceRect(rendered: Size, offset: Offset, f
|
|
|
12
12
|
left: number;
|
|
13
13
|
top: number;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Maps the current crop selection back into the *original, unrotated,
|
|
17
|
+
* unflipped* source image's own pixel coordinates. Corner-based: takes the
|
|
18
|
+
* frame rect's four corners in rendered (rotated + scaled) space, undoes the
|
|
19
|
+
* scale, then inverse-rotates each corner (rotation is always a multiple of
|
|
20
|
+
* 90°, so this stays an axis-aligned rectangle — no interpolation needed)
|
|
21
|
+
* back into the natural image's coordinate space, then takes the bounding
|
|
22
|
+
* box. Flip doesn't move the rectangle (mirroring is content-only), so it's
|
|
23
|
+
* carried through untouched in the result for the caller to apply.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeCropRegion(natural: Size, frame: Size, state: KiriState): CropRegion;
|
|
15
26
|
export declare function renderCropToCanvas(img: HTMLImageElement, state: KiriState, frame: Size, outputWidth: number, outputHeight: number, frameShape: FrameShape): HTMLCanvasElement;
|
|
16
27
|
export declare function exportCrop(img: HTMLImageElement, state: KiriState, frame: Size, frameShape: FrameShape, options: ExportOptions): Promise<ExportResult>;
|
package/dist/gestures.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ interface GestureCallbacks {
|
|
|
25
25
|
max: number;
|
|
26
26
|
};
|
|
27
27
|
setState: (next: KiriState) => void;
|
|
28
|
+
/** Called on the "0" key — reverts to the post-`load()` state. */
|
|
29
|
+
reset: () => void;
|
|
28
30
|
}
|
|
29
31
|
export interface GestureOptions {
|
|
30
32
|
mouseWheelZoom?: boolean | "ctrl";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Kiri } from './kiri';
|
|
2
2
|
export { KiriBatch } from './batch';
|
|
3
3
|
export type { KiriBatchItem } from './batch';
|
|
4
|
-
export type { ExportFormat, ExportOptions, ExportResult, ExportType, Filters, Flip, FrameShape, FrameSize, KiriEventCallback, KiriEventName, KiriOptions, KiriState, LoadOptions, Offset, UploadOptions, Uploader, ZoomerPosition, } from './types';
|
|
4
|
+
export type { CropRegion, ExportFormat, ExportOptions, ExportResult, ExportType, Filters, Flip, FrameShape, FrameSize, KiriEventCallback, KiriEventName, KiriOptions, KiriState, LoadOptions, Offset, UploadOptions, Uploader, ZoomerPosition, } from './types';
|
package/dist/kiri.css
CHANGED
|
@@ -42,8 +42,15 @@
|
|
|
42
42
|
border: 1px solid #333;
|
|
43
43
|
border-radius: 50%;
|
|
44
44
|
pointer-events: auto;
|
|
45
|
+
}
|
|
46
|
+
.kiri-frame-handle--top-left,
|
|
47
|
+
.kiri-frame-handle--bottom-right {
|
|
45
48
|
cursor: nwse-resize;
|
|
46
49
|
}
|
|
50
|
+
.kiri-frame-handle--top-right,
|
|
51
|
+
.kiri-frame-handle--bottom-left {
|
|
52
|
+
cursor: nesw-resize;
|
|
53
|
+
}
|
|
47
54
|
.kiri-root {
|
|
48
55
|
display: inline-flex;
|
|
49
56
|
gap: 10px;
|
package/dist/kiri.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExportOptions, ExportResult, Filters, KiriEventCallback, KiriEventName, KiriOptions, KiriState, LoadOptions, UploadOptions } from './types';
|
|
1
|
+
import { CropRegion, ExportOptions, ExportResult, Filters, KiriEventCallback, KiriEventName, KiriOptions, KiriState, LoadOptions, Offset, UploadOptions } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* An interactive image cropper attached to a plain DOM element. Drag to pan,
|
|
4
4
|
* zoom via wheel/pinch/an optional built-in slider, rotate in 90° steps,
|
|
@@ -13,6 +13,8 @@ export declare class Kiri {
|
|
|
13
13
|
private zoomerHandle;
|
|
14
14
|
private naturalSize;
|
|
15
15
|
private state;
|
|
16
|
+
/** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */
|
|
17
|
+
private initialState;
|
|
16
18
|
private listeners;
|
|
17
19
|
/**
|
|
18
20
|
* @param container An element already present in the DOM. Passing
|
|
@@ -34,6 +36,18 @@ export declare class Kiri {
|
|
|
34
36
|
getState(): KiriState;
|
|
35
37
|
/** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */
|
|
36
38
|
setZoom(zoom: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Sets the pan offset to an absolute value (image-center offset from the
|
|
41
|
+
* frame center, in stage pixels), clamped so the frame stays fully covered
|
|
42
|
+
* by the rendered image.
|
|
43
|
+
*/
|
|
44
|
+
setOffset(offset: Offset): void;
|
|
45
|
+
/**
|
|
46
|
+
* Reverts zoom/offset/rotation/flip/filters to what they were right after
|
|
47
|
+
* `load()` resolved (including any `loadOptions` passed to it). No-op if
|
|
48
|
+
* nothing has been loaded yet.
|
|
49
|
+
*/
|
|
50
|
+
reset(): void;
|
|
37
51
|
/**
|
|
38
52
|
* Rotates relative to the current rotation, snapped to the nearest 90°.
|
|
39
53
|
* No-op if `rotatable: false` was passed to the constructor.
|
|
@@ -61,6 +75,13 @@ export declare class Kiri {
|
|
|
61
75
|
* @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
|
|
62
76
|
*/
|
|
63
77
|
export(options?: ExportOptions): Promise<ExportResult>;
|
|
78
|
+
/**
|
|
79
|
+
* The current crop selection as a rectangle in the original, unrotated,
|
|
80
|
+
* unflipped source image's own pixel coordinates — for sending to a server
|
|
81
|
+
* that will crop the full-resolution original itself instead of uploading
|
|
82
|
+
* a client-re-encoded image. See {@link CropRegion}.
|
|
83
|
+
*/
|
|
84
|
+
getCropRegion(): CropRegion;
|
|
64
85
|
/**
|
|
65
86
|
* Exports the current crop as a blob, then uploads it — a default
|
|
66
87
|
* FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`
|
package/dist/kiri.js
CHANGED
|
@@ -35,6 +35,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
35
35
|
container.innerHTML = "";
|
|
36
36
|
const stageEl = document.createElement("div");
|
|
37
37
|
stageEl.className = "kiri-stage";
|
|
38
|
+
stageEl.tabIndex = 0;
|
|
39
|
+
stageEl.setAttribute("role", "application");
|
|
40
|
+
stageEl.setAttribute(
|
|
41
|
+
"aria-label",
|
|
42
|
+
"Image cropper. Drag to pan. Arrow keys to pan, plus/minus to zoom, 0 to reset."
|
|
43
|
+
);
|
|
38
44
|
const imageLayerEl = document.createElement("div");
|
|
39
45
|
imageLayerEl.className = "kiri-image-layer";
|
|
40
46
|
const imgEl = document.createElement("img");
|
|
@@ -109,6 +115,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
109
115
|
function normalizeRotation(deg) {
|
|
110
116
|
return (deg % 360 + 360) % 360;
|
|
111
117
|
}
|
|
118
|
+
const KEYBOARD_PAN_STEP = 15;
|
|
119
|
+
const KEYBOARD_ZOOM_STEP = 0.1;
|
|
112
120
|
function pointerDistance(a, b) {
|
|
113
121
|
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
114
122
|
}
|
|
@@ -188,11 +196,47 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
188
196
|
const delta = -e.deltaY * 15e-4;
|
|
189
197
|
applyClampedState({ ...state, zoom: state.zoom * (1 + delta) });
|
|
190
198
|
}
|
|
199
|
+
function onKeyDown(e) {
|
|
200
|
+
const state = callbacks.getState();
|
|
201
|
+
switch (e.key) {
|
|
202
|
+
case "ArrowLeft":
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x + KEYBOARD_PAN_STEP } });
|
|
205
|
+
break;
|
|
206
|
+
case "ArrowRight":
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x - KEYBOARD_PAN_STEP } });
|
|
209
|
+
break;
|
|
210
|
+
case "ArrowUp":
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y + KEYBOARD_PAN_STEP } });
|
|
213
|
+
break;
|
|
214
|
+
case "ArrowDown":
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y - KEYBOARD_PAN_STEP } });
|
|
217
|
+
break;
|
|
218
|
+
case "+":
|
|
219
|
+
case "=":
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
applyClampedState({ ...state, zoom: state.zoom + KEYBOARD_ZOOM_STEP });
|
|
222
|
+
break;
|
|
223
|
+
case "-":
|
|
224
|
+
case "_":
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
applyClampedState({ ...state, zoom: state.zoom - KEYBOARD_ZOOM_STEP });
|
|
227
|
+
break;
|
|
228
|
+
case "0":
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
callbacks.reset();
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
191
234
|
stageEl.addEventListener("pointerdown", onPointerDown);
|
|
192
235
|
stageEl.addEventListener("pointermove", onPointerMove);
|
|
193
236
|
stageEl.addEventListener("pointerup", onPointerUp);
|
|
194
237
|
stageEl.addEventListener("pointercancel", onPointerUp);
|
|
195
238
|
stageEl.addEventListener("wheel", onWheel, { passive: false });
|
|
239
|
+
stageEl.addEventListener("keydown", onKeyDown);
|
|
196
240
|
return {
|
|
197
241
|
destroy() {
|
|
198
242
|
stageEl.removeEventListener("pointerdown", onPointerDown);
|
|
@@ -200,6 +244,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
200
244
|
stageEl.removeEventListener("pointerup", onPointerUp);
|
|
201
245
|
stageEl.removeEventListener("pointercancel", onPointerUp);
|
|
202
246
|
stageEl.removeEventListener("wheel", onWheel);
|
|
247
|
+
stageEl.removeEventListener("keydown", onKeyDown);
|
|
203
248
|
}
|
|
204
249
|
};
|
|
205
250
|
}
|
|
@@ -265,6 +310,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
265
310
|
);
|
|
266
311
|
return fallback;
|
|
267
312
|
}
|
|
313
|
+
function clampNum(value, min, max) {
|
|
314
|
+
return Math.min(Math.max(value, min), max);
|
|
315
|
+
}
|
|
268
316
|
const VALID_EXPORT_TYPES = ["base64", "blob", "canvas"];
|
|
269
317
|
const VALID_EXPORT_FORMATS = ["image/jpeg", "image/png", "image/webp"];
|
|
270
318
|
function computeFrameSourceRect(rendered, offset, frame) {
|
|
@@ -273,6 +321,53 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
273
321
|
top: rendered.height / 2 - offset.y - frame.height / 2
|
|
274
322
|
};
|
|
275
323
|
}
|
|
324
|
+
function computeCropRegion(natural, frame, state) {
|
|
325
|
+
const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;
|
|
326
|
+
const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);
|
|
327
|
+
const { left: frameLeft, top: frameTop } = computeFrameSourceRect(rendered, state.offset, frame);
|
|
328
|
+
const renderedCenter = { x: rendered.width / 2, y: rendered.height / 2 };
|
|
329
|
+
const naturalCenter = { x: natural.width / 2, y: natural.height / 2 };
|
|
330
|
+
const rotation = normalizeRotation(state.rotation);
|
|
331
|
+
const corners = [
|
|
332
|
+
{ x: frameLeft, y: frameTop },
|
|
333
|
+
{ x: frameLeft + frame.width, y: frameTop },
|
|
334
|
+
{ x: frameLeft, y: frameTop + frame.height },
|
|
335
|
+
{ x: frameLeft + frame.width, y: frameTop + frame.height }
|
|
336
|
+
].map(({ x, y }) => {
|
|
337
|
+
const cx = (x - renderedCenter.x) / scale;
|
|
338
|
+
const cy = (y - renderedCenter.y) / scale;
|
|
339
|
+
let u;
|
|
340
|
+
let v;
|
|
341
|
+
if (rotation === 90) {
|
|
342
|
+
u = cy;
|
|
343
|
+
v = -cx;
|
|
344
|
+
} else if (rotation === 180) {
|
|
345
|
+
u = -cx;
|
|
346
|
+
v = -cy;
|
|
347
|
+
} else if (rotation === 270) {
|
|
348
|
+
u = -cy;
|
|
349
|
+
v = cx;
|
|
350
|
+
} else {
|
|
351
|
+
u = cx;
|
|
352
|
+
v = cy;
|
|
353
|
+
}
|
|
354
|
+
return { x: u + naturalCenter.x, y: v + naturalCenter.y };
|
|
355
|
+
});
|
|
356
|
+
const xs = corners.map((c) => c.x);
|
|
357
|
+
const ys = corners.map((c) => c.y);
|
|
358
|
+
const left = clampNum(Math.min(...xs), 0, natural.width);
|
|
359
|
+
const top = clampNum(Math.min(...ys), 0, natural.height);
|
|
360
|
+
const right = clampNum(Math.max(...xs), 0, natural.width);
|
|
361
|
+
const bottom = clampNum(Math.max(...ys), 0, natural.height);
|
|
362
|
+
return {
|
|
363
|
+
x: Math.round(left),
|
|
364
|
+
y: Math.round(top),
|
|
365
|
+
width: Math.round(right - left),
|
|
366
|
+
height: Math.round(bottom - top),
|
|
367
|
+
rotation,
|
|
368
|
+
flip: { ...state.flip }
|
|
369
|
+
};
|
|
370
|
+
}
|
|
276
371
|
function renderCropToCanvas(img, state, frame, outputWidth, outputHeight, frameShape) {
|
|
277
372
|
const natural = { width: img.naturalWidth, height: img.naturalHeight };
|
|
278
373
|
const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;
|
|
@@ -404,6 +499,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
404
499
|
flip: { horizontal: false, vertical: false },
|
|
405
500
|
filters: DEFAULT_FILTERS
|
|
406
501
|
});
|
|
502
|
+
/** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */
|
|
503
|
+
__publicField(this, "initialState", null);
|
|
407
504
|
__publicField(this, "listeners", { change: [] });
|
|
408
505
|
var _a, _b, _c;
|
|
409
506
|
if (!container || typeof container.appendChild !== "function") {
|
|
@@ -428,6 +525,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
428
525
|
rotatable: options.rotatable ?? true,
|
|
429
526
|
flippable: options.flippable ?? true,
|
|
430
527
|
resizableFrame: options.resizableFrame ?? false,
|
|
528
|
+
lockAspectRatio: options.lockAspectRatio ?? false,
|
|
431
529
|
mouseWheelZoom: resolveMouseWheelZoom(options.mouseWheelZoom),
|
|
432
530
|
useExifOrientation: options.useExifOrientation ?? true,
|
|
433
531
|
uploader: options.uploader,
|
|
@@ -464,7 +562,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
464
562
|
getFrameSize: () => this.getFrameSize(),
|
|
465
563
|
getState: () => this.state,
|
|
466
564
|
getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),
|
|
467
|
-
setState: (next) => this.commitState(next)
|
|
565
|
+
setState: (next) => this.commitState(next),
|
|
566
|
+
reset: () => this.reset()
|
|
468
567
|
},
|
|
469
568
|
{ mouseWheelZoom: this.opts.mouseWheelZoom }
|
|
470
569
|
);
|
|
@@ -523,6 +622,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
523
622
|
flip: { horizontal: flipHorizontal, vertical: flipVertical },
|
|
524
623
|
filters: this.state.filters
|
|
525
624
|
});
|
|
625
|
+
this.initialState = this.getState();
|
|
526
626
|
}
|
|
527
627
|
/** A snapshot of the current state — mutating the returned object has no effect. */
|
|
528
628
|
getState() {
|
|
@@ -546,6 +646,36 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
546
646
|
const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());
|
|
547
647
|
this.commitState({ ...this.state, zoom: clamped, offset });
|
|
548
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* Sets the pan offset to an absolute value (image-center offset from the
|
|
651
|
+
* frame center, in stage pixels), clamped so the frame stays fully covered
|
|
652
|
+
* by the rendered image.
|
|
653
|
+
*/
|
|
654
|
+
setOffset(offset) {
|
|
655
|
+
const rendered = effectiveRenderedSize(
|
|
656
|
+
this.naturalSize,
|
|
657
|
+
this.getFrameSize(),
|
|
658
|
+
this.state.rotation,
|
|
659
|
+
this.state.zoom
|
|
660
|
+
);
|
|
661
|
+
const clamped = clampOffset(offset, rendered, this.getFrameSize());
|
|
662
|
+
this.commitState({ ...this.state, offset: clamped });
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Reverts zoom/offset/rotation/flip/filters to what they were right after
|
|
666
|
+
* `load()` resolved (including any `loadOptions` passed to it). No-op if
|
|
667
|
+
* nothing has been loaded yet.
|
|
668
|
+
*/
|
|
669
|
+
reset() {
|
|
670
|
+
if (!this.initialState) return;
|
|
671
|
+
this.commitState({
|
|
672
|
+
zoom: this.initialState.zoom,
|
|
673
|
+
offset: { ...this.initialState.offset },
|
|
674
|
+
rotation: this.initialState.rotation,
|
|
675
|
+
flip: { ...this.initialState.flip },
|
|
676
|
+
filters: { ...this.initialState.filters }
|
|
677
|
+
});
|
|
678
|
+
}
|
|
549
679
|
/**
|
|
550
680
|
* Rotates relative to the current rotation, snapped to the nearest 90°.
|
|
551
681
|
* No-op if `rotatable: false` was passed to the constructor.
|
|
@@ -616,6 +746,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
616
746
|
options
|
|
617
747
|
);
|
|
618
748
|
}
|
|
749
|
+
/**
|
|
750
|
+
* The current crop selection as a rectangle in the original, unrotated,
|
|
751
|
+
* unflipped source image's own pixel coordinates — for sending to a server
|
|
752
|
+
* that will crop the full-resolution original itself instead of uploading
|
|
753
|
+
* a client-re-encoded image. See {@link CropRegion}.
|
|
754
|
+
*/
|
|
755
|
+
getCropRegion() {
|
|
756
|
+
return computeCropRegion(this.naturalSize, this.getFrameSize(), this.state);
|
|
757
|
+
}
|
|
619
758
|
/**
|
|
620
759
|
* Exports the current crop as a blob, then uploads it — a default
|
|
621
760
|
* FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`
|
|
@@ -678,42 +817,64 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
678
817
|
};
|
|
679
818
|
}
|
|
680
819
|
enableFrameResize() {
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
handle.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
820
|
+
const corners = [
|
|
821
|
+
{ name: "top-left", edge: { left: "-5px", top: "-5px" }, signX: -1, signY: -1 },
|
|
822
|
+
{ name: "top-right", edge: { right: "-5px", top: "-5px" }, signX: 1, signY: -1 },
|
|
823
|
+
{ name: "bottom-left", edge: { left: "-5px", bottom: "-5px" }, signX: -1, signY: 1 },
|
|
824
|
+
{ name: "bottom-right", edge: { right: "-5px", bottom: "-5px" }, signX: 1, signY: 1 }
|
|
825
|
+
];
|
|
826
|
+
const cleanups = [];
|
|
827
|
+
for (const corner of corners) {
|
|
828
|
+
const handle = document.createElement("div");
|
|
829
|
+
handle.className = `kiri-frame-handle kiri-frame-handle--${corner.name}`;
|
|
830
|
+
Object.assign(handle.style, corner.edge);
|
|
831
|
+
this.stage.frameEl.appendChild(handle);
|
|
832
|
+
let start = null;
|
|
833
|
+
const onDown = (e) => {
|
|
834
|
+
e.stopPropagation();
|
|
835
|
+
handle.setPointerCapture(e.pointerId);
|
|
836
|
+
start = {
|
|
837
|
+
x: e.clientX,
|
|
838
|
+
y: e.clientY,
|
|
839
|
+
width: this.opts.frame.width,
|
|
840
|
+
height: this.opts.frame.height
|
|
841
|
+
};
|
|
695
842
|
};
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
843
|
+
const onMove = (e) => {
|
|
844
|
+
if (!start) return;
|
|
845
|
+
const dx = (e.clientX - start.x) * 2 * corner.signX;
|
|
846
|
+
const dy = (e.clientY - start.y) * 2 * corner.signY;
|
|
847
|
+
const rawWidth = start.width + dx;
|
|
848
|
+
const rawHeight = start.height + dy;
|
|
849
|
+
if (this.opts.lockAspectRatio) {
|
|
850
|
+
const aspect = start.width / start.height;
|
|
851
|
+
if (Math.abs(rawWidth - start.width) >= Math.abs(rawHeight - start.height) * aspect) {
|
|
852
|
+
this.setFrameSize(rawWidth, rawWidth / aspect);
|
|
853
|
+
} else {
|
|
854
|
+
this.setFrameSize(rawHeight * aspect, rawHeight);
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
this.setFrameSize(rawWidth, rawHeight);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
const onUp = () => {
|
|
861
|
+
start = null;
|
|
862
|
+
};
|
|
863
|
+
handle.addEventListener("pointerdown", onDown);
|
|
864
|
+
handle.addEventListener("pointermove", onMove);
|
|
865
|
+
handle.addEventListener("pointerup", onUp);
|
|
866
|
+
handle.addEventListener("pointercancel", onUp);
|
|
867
|
+
cleanups.push(() => {
|
|
712
868
|
handle.removeEventListener("pointerdown", onDown);
|
|
713
869
|
handle.removeEventListener("pointermove", onMove);
|
|
714
870
|
handle.removeEventListener("pointerup", onUp);
|
|
715
871
|
handle.removeEventListener("pointercancel", onUp);
|
|
716
872
|
handle.remove();
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
this.resizeHandle = {
|
|
876
|
+
destroy() {
|
|
877
|
+
for (const cleanup of cleanups) cleanup();
|
|
717
878
|
}
|
|
718
879
|
};
|
|
719
880
|
}
|
|
@@ -745,6 +906,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
745
906
|
await this.cropper.load(item.source, item.loadOptions);
|
|
746
907
|
return true;
|
|
747
908
|
}
|
|
909
|
+
/** Loads the previous queued image into `cropper`. Returns false when already at the first item (or nothing loaded yet). */
|
|
910
|
+
async previous() {
|
|
911
|
+
if (this.index <= 0) return false;
|
|
912
|
+
this.index -= 1;
|
|
913
|
+
const item = this.items[this.index];
|
|
914
|
+
await this.cropper.load(item.source, item.loadOptions);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
748
917
|
/** Metadata for the currently loaded item, or null before the first next() / after exhaustion. */
|
|
749
918
|
current() {
|
|
750
919
|
return this.items[this.index] ?? null;
|