@michaelyagi/kiri 0.1.0-alpha.6 → 0.1.0-alpha.8

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/README.md ADDED
@@ -0,0 +1,98 @@
1
+ > **AI-authored.** 100% of the code was written by Claude (Anthropic); I shaped the architecture, scope, and every decision, and did all the testing.
2
+
3
+ # Kiri
4
+
5
+ A dependency-free TypeScript library for interactive image cropping in the browser.
6
+
7
+ ## Features
8
+
9
+ - **Drag, zoom, rotate, flip** — wheel/trackpad pinch zoom, an optional
10
+ built-in zoom slider (`showZoomer`, placeable on any of the four sides),
11
+ 90° rotation, independent horizontal/vertical flip
12
+ - **Rectangle, circle, or rounded-rectangle frame** — circle/rounded-rectangle
13
+ are real clips on export (transparent corners on PNG/WebP), not just a
14
+ visual overlay
15
+ - **Zero-CSS sizing** — the stage auto-sizes itself to the frame's dimensions
16
+ - **Filters** — brightness/contrast/saturation/grayscale/sepia, applied
17
+ identically to the live preview and the export
18
+ - **Automatic EXIF orientation correction** on load
19
+ - **Resizable frame**, four independent corner handles, with an optional
20
+ `lockAspectRatio` to keep a fixed ratio while resizing
21
+ - **`setOffset()`/`reset()`** for programmatic panning and reverting to the
22
+ post-`load()` state
23
+ - **`getCropRegion()`** — the crop mapped back to the original image's own
24
+ pixel coordinates, for server-side cropping of the full-resolution source
25
+ - **Keyboard-accessible** — the stage is focusable; arrow keys pan, `+`/`-`
26
+ zoom, `0` resets
27
+ - **Export** to base64, Blob, or Canvas — JPEG/PNG/WebP, custom output size
28
+ - **Upload** — a built-in FormData/fetch helper, or plug in your own
29
+ - **Batch cropping** — a documented recipe for stepping one shared cropper
30
+ through a queue of images, no separate class needed
31
+ - **React and Vue wrappers** (`kiri-react`, `kiri-vue`) — thin components, no
32
+ duplicated logic, reactive to prop changes after mount
33
+ - Runtime-validated options (a typo'd setting warns and falls back rather
34
+ than silently misbehaving), a clear error if the container element is
35
+ missing, and a real, CSP-safe stylesheet instead of injected styles
36
+ - Ships as ESM for bundlers and UMD/CJS for a plain `<script>` tag —
37
+ minified and unminified builds of both
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install @michaelyagi/kiri
43
+ ```
44
+
45
+ [npmjs.com/package/@michaelyagi/kiri](https://www.npmjs.com/package/@michaelyagi/kiri)
46
+
47
+ ## Zero-config quickstart
48
+
49
+ ```html
50
+ <link rel="stylesheet" href="node_modules/@michaelyagi/kiri/dist/kiri.min.css" />
51
+ <script src="node_modules/@michaelyagi/kiri/dist/kiri.min.js"></script>
52
+ <div id="cropper"></div>
53
+ <script>
54
+ const cropper = new Kiri(document.getElementById("cropper"), {
55
+ frame: { shape: "circle", width: 200, height: 200 },
56
+ });
57
+ await cropper.load("photo.jpg");
58
+ const blob = await cropper.export({ type: "blob" });
59
+ </script>
60
+ ```
61
+
62
+ No CSS sizing needed on the container — the stage auto-sizes itself to the
63
+ frame's dimensions. See [Getting started](https://michaelyagi.github.io/kiri/guides/getting-started.html)
64
+ for npm/ESM installation too.
65
+
66
+ ## Documentation
67
+
68
+ **[michaelyagi.github.io/kiri](https://michaelyagi.github.io/kiri)** —
69
+ getting started, every method/event/setting, guides, real runnable examples,
70
+ an interactive playground, and the full API reference.
71
+
72
+ ## Packages
73
+
74
+ | Package | Path | What it is |
75
+ |---|---|---|
76
+ | `@michaelyagi/kiri` | `packages/core` | The library itself — framework-agnostic |
77
+ | `kiri-react` | `packages/react` | `<KiriCropper>` React component (unpublished) |
78
+ | `kiri-vue` | `packages/vue` | `<KiriCropper>` Vue component (unpublished) |
79
+
80
+ ## Status
81
+
82
+ **`0.1.0-alpha.8`** — see [CHANGELOG.md](./CHANGELOG.md)
83
+ for what's in it. Core publishes to npm automatically on version tags via
84
+ GitHub Actions; `kiri-react`/`kiri-vue` aren't published yet. The public API
85
+ is expected to be mostly stable but may still change before a `0.1.0`
86
+ (non-alpha) release.
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ npm install # installs all workspace packages
92
+ npm run dev # core demo at http://localhost:5173
93
+ npm run build # builds all three packages
94
+ npm test # runs all three packages' test suites
95
+ npm run docs:build # rebuilds the docs/ site (library + API reference + asset sync)
96
+ ```
97
+
98
+ See [design.md](./design.md) for the full design document.
package/dist/export.d.ts CHANGED
@@ -23,5 +23,5 @@ export declare function computeFrameSourceRect(rendered: Size, offset: Offset, f
23
23
  * carried through untouched in the result for the caller to apply.
24
24
  */
25
25
  export declare function computeCropRegion(natural: Size, frame: Size, state: KiriState): CropRegion;
26
- export declare function renderCropToCanvas(img: HTMLImageElement, state: KiriState, frame: Size, outputWidth: number, outputHeight: number, frameShape: FrameShape): HTMLCanvasElement;
27
- export declare function exportCrop(img: HTMLImageElement, state: KiriState, frame: Size, frameShape: FrameShape, options: ExportOptions): Promise<ExportResult>;
26
+ export declare function renderCropToCanvas(img: HTMLImageElement, state: KiriState, frame: Size, outputWidth: number, outputHeight: number, frameShape: FrameShape, cornerRadius: number): HTMLCanvasElement;
27
+ export declare function exportCrop(img: HTMLImageElement, state: KiriState, frame: Size, frameShape: FrameShape, cornerRadius: number, options: ExportOptions): Promise<ExportResult>;
package/dist/kiri.css CHANGED
@@ -34,6 +34,9 @@
34
34
  .kiri-frame--circle {
35
35
  border-radius: 50%;
36
36
  }
37
+ /* .kiri-frame--rounded-rectangle's border-radius is a per-instance pixel
38
+ value (frame.cornerRadius), so it's set inline by stage.ts instead of
39
+ here — this class exists as a CSS hook for consumers, not styling. */
37
40
  .kiri-frame-handle {
38
41
  position: absolute;
39
42
  width: 10px;
package/dist/kiri.d.ts CHANGED
@@ -69,9 +69,10 @@ export declare class Kiri {
69
69
  */
70
70
  setFilters(filters: Partial<Filters>): void;
71
71
  /**
72
- * Renders the current crop. A circle frame is a real clip in the output
73
- * (transparent corners on PNG/WebP); a circle exported as JPEG warns and
74
- * renders solid black corners instead, since JPEG has no alpha channel.
72
+ * Renders the current crop. A circle or rounded-rectangle frame is a real
73
+ * clip in the output (transparent corners on PNG/WebP); either shape
74
+ * exported as JPEG warns and renders solid black corners instead, since
75
+ * JPEG has no alpha channel.
75
76
  * @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
76
77
  */
77
78
  export(options?: ExportOptions): Promise<ExportResult>;
package/dist/kiri.js CHANGED
@@ -31,7 +31,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
31
31
  if (filters.sepia) parts.push("sepia(1)");
32
32
  return parts.join(" ");
33
33
  }
34
- function createStage(container, frameShape, frameWidth, frameHeight, zoomer) {
34
+ function createStage(container, frameShape, frameWidth, frameHeight, cornerRadius, zoomer) {
35
35
  container.innerHTML = "";
36
36
  const stageEl = document.createElement("div");
37
37
  stageEl.className = "kiri-stage";
@@ -47,8 +47,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
47
47
  imgEl.draggable = false;
48
48
  imageLayerEl.appendChild(imgEl);
49
49
  const frameEl = document.createElement("div");
50
- frameEl.className = frameShape === "circle" ? "kiri-frame kiri-frame--circle" : "kiri-frame";
50
+ frameEl.className = frameShape === "circle" ? "kiri-frame kiri-frame--circle" : frameShape === "rounded-rectangle" ? "kiri-frame kiri-frame--rounded-rectangle" : "kiri-frame";
51
51
  setFrameSize(frameEl, frameWidth, frameHeight);
52
+ if (frameShape === "rounded-rectangle") {
53
+ frameEl.style.borderRadius = `${cornerRadius}px`;
54
+ }
52
55
  stageEl.appendChild(imageLayerEl);
53
56
  stageEl.appendChild(frameEl);
54
57
  let zoomerEl = null;
@@ -368,7 +371,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
368
371
  flip: { ...state.flip }
369
372
  };
370
373
  }
371
- function renderCropToCanvas(img, state, frame, outputWidth, outputHeight, frameShape) {
374
+ function renderCropToCanvas(img, state, frame, outputWidth, outputHeight, frameShape, cornerRadius) {
372
375
  const natural = { width: img.naturalWidth, height: img.naturalHeight };
373
376
  const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;
374
377
  const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);
@@ -405,6 +408,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
405
408
  Math.PI * 2
406
409
  );
407
410
  octx.clip();
411
+ } else if (frameShape === "rounded-rectangle") {
412
+ const scale2 = outputWidth / frame.width;
413
+ const radius = clampNum(cornerRadius * scale2, 0, Math.min(outputWidth, outputHeight) / 2);
414
+ octx.save();
415
+ octx.beginPath();
416
+ octx.roundRect(0, 0, outputWidth, outputHeight, radius);
417
+ octx.clip();
408
418
  }
409
419
  octx.drawImage(
410
420
  sourceCanvas,
@@ -417,21 +427,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
417
427
  outputWidth,
418
428
  outputHeight
419
429
  );
420
- if (frameShape === "circle") octx.restore();
430
+ if (frameShape === "circle" || frameShape === "rounded-rectangle") octx.restore();
421
431
  return outCanvas;
422
432
  }
423
- async function exportCrop(img, state, frame, frameShape, options) {
433
+ async function exportCrop(img, state, frame, frameShape, cornerRadius, options) {
424
434
  const type = resolveEnumOption(options.type, VALID_EXPORT_TYPES, "base64", "export type");
425
435
  const format = resolveEnumOption(options.format, VALID_EXPORT_FORMATS, "image/png", "export format");
426
436
  const quality = options.quality;
427
437
  const width = options.width ?? frame.width;
428
438
  const height = options.height ?? frame.height;
429
- if (frameShape === "circle" && format === "image/jpeg") {
439
+ if ((frameShape === "circle" || frameShape === "rounded-rectangle") && format === "image/jpeg") {
430
440
  console.warn(
431
- "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."
441
+ `Kiri: exporting a ${frameShape}-shaped frame as image/jpeg — JPEG has no alpha channel, so the area outside the shape will render as solid black instead of transparent. Use image/png or image/webp instead.`
432
442
  );
433
443
  }
434
- const canvas = renderCropToCanvas(img, state, frame, width, height, frameShape);
444
+ const canvas = renderCropToCanvas(img, state, frame, width, height, frameShape, cornerRadius);
435
445
  if (type === "canvas") return canvas;
436
446
  if (type === "base64") return canvas.toDataURL(format, quality);
437
447
  return new Promise((resolve, reject) => {
@@ -468,7 +478,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
468
478
  }
469
479
  const DEFAULT_FRAME_SIZE = 200;
470
480
  const MIN_FRAME_SIZE = 20;
471
- const VALID_FRAME_SHAPES = ["rectangle", "circle"];
481
+ const DEFAULT_CORNER_RADIUS = 20;
482
+ const VALID_FRAME_SHAPES = ["rectangle", "circle", "rounded-rectangle"];
472
483
  const VALID_ZOOMER_POSITIONS = ["top", "bottom", "left", "right"];
473
484
  function resolveMouseWheelZoom(value) {
474
485
  if (value === void 0) return true;
@@ -502,7 +513,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
502
513
  /** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */
503
514
  __publicField(this, "initialState", null);
504
515
  __publicField(this, "listeners", { change: [] });
505
- var _a, _b, _c;
516
+ var _a, _b, _c, _d;
506
517
  if (!container || typeof container.appendChild !== "function") {
507
518
  throw new Error(
508
519
  "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."
@@ -518,7 +529,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
518
529
  "frame.shape"
519
530
  ),
520
531
  width: ((_b = options.frame) == null ? void 0 : _b.width) ?? DEFAULT_FRAME_SIZE,
521
- height: ((_c = options.frame) == null ? void 0 : _c.height) ?? DEFAULT_FRAME_SIZE
532
+ height: ((_c = options.frame) == null ? void 0 : _c.height) ?? DEFAULT_FRAME_SIZE,
533
+ cornerRadius: ((_d = options.frame) == null ? void 0 : _d.cornerRadius) ?? DEFAULT_CORNER_RADIUS
522
534
  },
523
535
  minZoom: options.minZoom ?? 1,
524
536
  maxZoom: options.maxZoom ?? 4,
@@ -544,6 +556,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
544
556
  this.opts.frame.shape,
545
557
  this.opts.frame.width,
546
558
  this.opts.frame.height,
559
+ this.opts.frame.cornerRadius,
547
560
  {
548
561
  show: this.opts.showZoomer,
549
562
  position: this.opts.zoomerPosition,
@@ -735,9 +748,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
735
748
  this.commitState({ ...this.state, filters: mergeFilters(this.state.filters, filters) });
736
749
  }
737
750
  /**
738
- * Renders the current crop. A circle frame is a real clip in the output
739
- * (transparent corners on PNG/WebP); a circle exported as JPEG warns and
740
- * renders solid black corners instead, since JPEG has no alpha channel.
751
+ * Renders the current crop. A circle or rounded-rectangle frame is a real
752
+ * clip in the output (transparent corners on PNG/WebP); either shape
753
+ * exported as JPEG warns and renders solid black corners instead, since
754
+ * JPEG has no alpha channel.
741
755
  * @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
742
756
  */
743
757
  async export(options = {}) {
@@ -746,6 +760,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
746
760
  this.state,
747
761
  this.getFrameSize(),
748
762
  this.opts.frame.shape,
763
+ this.opts.frame.cornerRadius,
749
764
  options
750
765
  );
751
766
  }
package/dist/kiri.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"kiri.js","sources":["../src/filters.ts","../src/stage.ts","../src/gestures.ts","../src/exif.ts","../src/validate.ts","../src/export.ts","../src/upload.ts","../src/kiri.ts"],"sourcesContent":["import type { Filters } from \"./types\";\n\nexport const DEFAULT_FILTERS: Filters = {\n brightness: 1,\n contrast: 1,\n saturation: 1,\n grayscale: false,\n sepia: false,\n};\n\n/** Merges a partial filter update into the current filters, clamping numeric values to >= 0. */\nexport function mergeFilters(current: Filters, partial: Partial<Filters>): Filters {\n return {\n brightness: Math.max(0, partial.brightness ?? current.brightness),\n contrast: Math.max(0, partial.contrast ?? current.contrast),\n saturation: Math.max(0, partial.saturation ?? current.saturation),\n grayscale: partial.grayscale ?? current.grayscale,\n sepia: partial.sepia ?? current.sepia,\n };\n}\n\n/**\n * Builds a CSS `filter` value from the current filter state. Used verbatim as\n * both the live-preview `img.style.filter` and a canvas 2D context's\n * `ctx.filter` before drawing, so preview and export always match exactly —\n * no separately hand-rolled brightness/contrast/saturation pixel math.\n */\nexport function buildFilterString(filters: Filters): string {\n const parts = [\n `brightness(${filters.brightness})`,\n `contrast(${filters.contrast})`,\n `saturate(${filters.saturation})`,\n ];\n if (filters.grayscale) parts.push(\"grayscale(1)\");\n if (filters.sepia) parts.push(\"sepia(1)\");\n return parts.join(\" \");\n}\n","import type { Filters, FrameShape, KiriState, ZoomerPosition } from \"./types\";\nimport { buildFilterString } from \"./filters\";\n\nexport interface StageElements {\n stageEl: HTMLDivElement;\n frameEl: HTMLDivElement;\n imageLayerEl: HTMLDivElement;\n imgEl: HTMLImageElement;\n zoomerEl: HTMLInputElement | null;\n}\n\nexport interface ZoomerConfig {\n show: boolean;\n position: ZoomerPosition;\n min: number;\n max: number;\n value: number;\n}\n\nexport function createStage(\n container: HTMLElement,\n frameShape: FrameShape,\n frameWidth: number,\n frameHeight: number,\n zoomer: ZoomerConfig\n): StageElements {\n container.innerHTML = \"\";\n\n const stageEl = document.createElement(\"div\");\n stageEl.className = \"kiri-stage\";\n // Keyboard support (see gestures.ts's onKeyDown) only fires once the\n // element can receive focus; role/label make it announce sensibly to\n // screen readers instead of as an unlabeled generic div.\n stageEl.tabIndex = 0;\n stageEl.setAttribute(\"role\", \"application\");\n stageEl.setAttribute(\n \"aria-label\",\n \"Image cropper. Drag to pan. Arrow keys to pan, plus/minus to zoom, 0 to reset.\"\n );\n\n const imageLayerEl = document.createElement(\"div\");\n imageLayerEl.className = \"kiri-image-layer\";\n\n const imgEl = document.createElement(\"img\");\n imgEl.draggable = false;\n imageLayerEl.appendChild(imgEl);\n\n const frameEl = document.createElement(\"div\");\n frameEl.className =\n frameShape === \"circle\" ? \"kiri-frame kiri-frame--circle\" : \"kiri-frame\";\n setFrameSize(frameEl, frameWidth, frameHeight);\n\n stageEl.appendChild(imageLayerEl);\n stageEl.appendChild(frameEl);\n\n let zoomerEl: HTMLInputElement | null = null;\n\n if (zoomer.show) {\n const rootEl = document.createElement(\"div\");\n rootEl.className = `kiri-root kiri-root--${zoomer.position}`;\n\n zoomerEl = document.createElement(\"input\");\n zoomerEl.type = \"range\";\n zoomerEl.className = \"kiri-zoomer\";\n zoomerEl.min = String(zoomer.min);\n zoomerEl.max = String(zoomer.max);\n zoomerEl.step = \"0.01\";\n zoomerEl.value = String(zoomer.value);\n\n rootEl.appendChild(stageEl);\n rootEl.appendChild(zoomerEl);\n container.appendChild(rootEl);\n } else {\n container.appendChild(stageEl);\n }\n\n return { stageEl, frameEl, imageLayerEl, imgEl, zoomerEl };\n}\n\nexport function setFrameSize(\n frameEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n frameEl.style.width = `${width}px`;\n frameEl.style.height = `${height}px`;\n}\n\n/** Padding added around the frame when the stage auto-sizes itself — small\n * enough to avoid empty space, big enough to fit the resize handle and give\n * a visible drag margin. */\nexport const STAGE_AUTO_SIZE_PADDING = 20;\n\nexport function setStageSize(\n stageEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n stageEl.style.width = `${width}px`;\n stageEl.style.height = `${height}px`;\n}\n\nexport function applyTransform(\n imageLayerEl: HTMLDivElement,\n state: KiriState,\n renderedScale: number\n): void {\n // translate(-50%,-50%) centers the layer's own center at the stage center;\n // the pixel translate shifts it by the (rotation/scale-independent) offset;\n // rotate then scale (with flip folded into scale's sign) apply around the\n // layer's own center (default transform-origin), flip first/innermost so a\n // mirrored image still rotates the way the user expects.\n const scaleX = renderedScale * (state.flip.horizontal ? -1 : 1);\n const scaleY = renderedScale * (state.flip.vertical ? -1 : 1);\n imageLayerEl.style.transform =\n `translate(-50%, -50%) ` +\n `translate(${state.offset.x}px, ${state.offset.y}px) ` +\n `rotate(${state.rotation}deg) ` +\n `scale(${scaleX}, ${scaleY})`;\n}\n\nexport function applyFilters(imgEl: HTMLImageElement, filters: Filters): void {\n imgEl.style.filter = buildFilterString(filters);\n}\n","import type { KiriState, Offset } from \"./types\";\n\nexport interface Size {\n width: number;\n height: number;\n}\n\n/** Rotation-aware natural size: 90/270 degrees swap width and height. */\nexport function effectiveNaturalSize(natural: Size, rotationDeg: number): Size {\n const swapped = ((rotationDeg / 90) % 2 + 2) % 2 === 1;\n return swapped\n ? { width: natural.height, height: natural.width }\n : { width: natural.width, height: natural.height };\n}\n\n/** The smallest scale at which the (rotation-adjusted) image still fully covers the frame. */\nexport function computeCoverScale(\n natural: Size,\n frame: Size,\n rotationDeg: number\n): number {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n if (eff.width <= 0 || eff.height <= 0) return 1;\n return Math.max(frame.width / eff.width, frame.height / eff.height);\n}\n\n/** Rendered size of the image layer given a user zoom multiplier (relative to cover scale). */\nexport function effectiveRenderedSize(\n natural: Size,\n frame: Size,\n rotationDeg: number,\n zoom: number\n): Size {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n const scale = computeCoverScale(natural, frame, rotationDeg) * zoom;\n return { width: eff.width * scale, height: eff.height * scale };\n}\n\nexport function clampZoom(zoom: number, minZoom: number, maxZoom: number): number {\n return Math.min(Math.max(zoom, minZoom), maxZoom);\n}\n\n/**\n * Clamp the image-center offset (from stage/frame center) so the frame stays\n * fully covered by the rendered image on every axis.\n */\nexport function clampOffset(offset: Offset, rendered: Size, frame: Size): Offset {\n const maxX = Math.max(0, (rendered.width - frame.width) / 2);\n const maxY = Math.max(0, (rendered.height - frame.height) / 2);\n // `|| 0` normalizes -0 (e.g. clamping a negative offset to a zero-width\n // range) to +0, keeping state comparisons and serialized CSS values sane.\n return {\n x: Math.min(Math.max(offset.x, -maxX), maxX) || 0,\n y: Math.min(Math.max(offset.y, -maxY), maxY) || 0,\n };\n}\n\nexport function normalizeRotation(deg: number): number {\n return ((deg % 360) + 360) % 360;\n}\n\ninterface GestureCallbacks {\n getNaturalSize: () => Size;\n getFrameSize: () => Size;\n getState: () => KiriState;\n getMinMaxZoom: () => { min: number; max: number };\n setState: (next: KiriState) => void;\n /** Called on the \"0\" key — reverts to the post-`load()` state. */\n reset: () => void;\n}\n\nconst KEYBOARD_PAN_STEP = 15;\nconst KEYBOARD_ZOOM_STEP = 0.1;\n\nexport interface GestureOptions {\n mouseWheelZoom?: boolean | \"ctrl\";\n}\n\nfunction pointerDistance(a: PointerEvent, b: PointerEvent): number {\n return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);\n}\n\nexport function attachGestures(\n stageEl: HTMLElement,\n callbacks: GestureCallbacks,\n options: GestureOptions\n): { destroy: () => void } {\n const activePointers = new Map<number, PointerEvent>();\n let dragStart: { x: number; y: number; offset: Offset } | null = null;\n let pinchStartDistance = 0;\n let pinchStartZoom = 1;\n\n function applyClampedState(next: KiriState): void {\n const { min, max } = callbacks.getMinMaxZoom();\n const zoom = clampZoom(next.zoom, min, max);\n const rendered = effectiveRenderedSize(\n callbacks.getNaturalSize(),\n callbacks.getFrameSize(),\n next.rotation,\n zoom\n );\n const offset = clampOffset(next.offset, rendered, callbacks.getFrameSize());\n callbacks.setState({\n zoom,\n offset,\n rotation: normalizeRotation(next.rotation),\n flip: next.flip,\n filters: next.filters,\n });\n }\n\n function onPointerDown(e: PointerEvent): void {\n stageEl.setPointerCapture(e.pointerId);\n activePointers.set(e.pointerId, e);\n if (activePointers.size === 1) {\n const state = callbacks.getState();\n dragStart = { x: e.clientX, y: e.clientY, offset: state.offset };\n stageEl.classList.add(\"kiri-dragging\");\n } else if (activePointers.size === 2) {\n dragStart = null;\n const [a, b] = [...activePointers.values()];\n pinchStartDistance = pointerDistance(a, b);\n pinchStartZoom = callbacks.getState().zoom;\n }\n }\n\n function onPointerMove(e: PointerEvent): void {\n if (!activePointers.has(e.pointerId)) return;\n activePointers.set(e.pointerId, e);\n\n if (activePointers.size === 2) {\n const [a, b] = [...activePointers.values()];\n const distance = pointerDistance(a, b);\n if (pinchStartDistance > 0) {\n const ratio = distance / pinchStartDistance;\n const state = callbacks.getState();\n applyClampedState({ ...state, zoom: pinchStartZoom * ratio });\n }\n return;\n }\n\n if (dragStart) {\n const dx = e.clientX - dragStart.x;\n const dy = e.clientY - dragStart.y;\n const state = callbacks.getState();\n applyClampedState({\n ...state,\n offset: { x: dragStart.offset.x + dx, y: dragStart.offset.y + dy },\n });\n }\n }\n\n function onPointerUp(e: PointerEvent): void {\n activePointers.delete(e.pointerId);\n if (activePointers.size < 2) pinchStartDistance = 0;\n if (activePointers.size === 0) {\n dragStart = null;\n stageEl.classList.remove(\"kiri-dragging\");\n }\n }\n\n function onWheel(e: WheelEvent): void {\n if (!options.mouseWheelZoom) return;\n if (options.mouseWheelZoom === \"ctrl\" && !e.ctrlKey) return;\n e.preventDefault();\n const state = callbacks.getState();\n const delta = -e.deltaY * 0.0015;\n applyClampedState({ ...state, zoom: state.zoom * (1 + delta) });\n }\n\n // Arrow keys pan, +/- zoom, 0 resets — lets a keyboard-only user operate\n // the cropper once the stage is focused (it's a tabbable, labeled element;\n // see stage.ts). Pan direction follows the \"arrow moves the view\" scroll\n // convention (pressing Right reveals more of the image's right side), the\n // opposite of drag's \"content follows the pointer\" — so the sign is\n // inverted relative to a drag delta of the same direction.\n function onKeyDown(e: KeyboardEvent): void {\n const state = callbacks.getState();\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x + KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowRight\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x - KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowUp\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y + KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowDown\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y - KEYBOARD_PAN_STEP } });\n break;\n case \"+\":\n case \"=\":\n e.preventDefault();\n applyClampedState({ ...state, zoom: state.zoom + KEYBOARD_ZOOM_STEP });\n break;\n case \"-\":\n case \"_\":\n e.preventDefault();\n applyClampedState({ ...state, zoom: state.zoom - KEYBOARD_ZOOM_STEP });\n break;\n case \"0\":\n e.preventDefault();\n callbacks.reset();\n break;\n default:\n break;\n }\n }\n\n stageEl.addEventListener(\"pointerdown\", onPointerDown);\n stageEl.addEventListener(\"pointermove\", onPointerMove);\n stageEl.addEventListener(\"pointerup\", onPointerUp);\n stageEl.addEventListener(\"pointercancel\", onPointerUp);\n stageEl.addEventListener(\"wheel\", onWheel, { passive: false });\n stageEl.addEventListener(\"keydown\", onKeyDown);\n\n return {\n destroy(): void {\n stageEl.removeEventListener(\"pointerdown\", onPointerDown);\n stageEl.removeEventListener(\"pointermove\", onPointerMove);\n stageEl.removeEventListener(\"pointerup\", onPointerUp);\n stageEl.removeEventListener(\"pointercancel\", onPointerUp);\n stageEl.removeEventListener(\"wheel\", onWheel);\n stageEl.removeEventListener(\"keydown\", onKeyDown);\n },\n };\n}\n","/**\n * Reads the EXIF orientation tag (1-8) from a JPEG ArrayBuffer, defaulting to 1\n * (no adjustment) for images with no EXIF data or non-JPEG input.\n */\nexport function readExifOrientation(buffer: ArrayBuffer): number {\n const view = new DataView(buffer);\n if (view.byteLength < 4 || view.getUint16(0, false) !== 0xffd8) return 1;\n\n let offset = 2;\n while (offset + 4 <= view.byteLength) {\n const marker = view.getUint16(offset, false);\n const size = view.getUint16(offset + 2, false);\n\n if (marker === 0xffe1) {\n const exifOffset = offset + 4;\n if (view.getUint32(exifOffset, false) !== 0x45786966) return 1; // \"Exif\"\n return readOrientationFromTiff(view, exifOffset + 6);\n }\n\n if ((marker & 0xff00) !== 0xff00) break;\n offset += 2 + size;\n }\n return 1;\n}\n\nfunction readOrientationFromTiff(view: DataView, tiffStart: number): number {\n const littleEndian = view.getUint16(tiffStart, false) === 0x4949;\n const firstIfdOffset = view.getUint32(tiffStart + 4, littleEndian);\n const ifdStart = tiffStart + firstIfdOffset;\n if (ifdStart + 2 > view.byteLength) return 1;\n\n const entryCount = view.getUint16(ifdStart, littleEndian);\n for (let i = 0; i < entryCount; i++) {\n const entryOffset = ifdStart + 2 + i * 12;\n if (entryOffset + 12 > view.byteLength) break;\n const tag = view.getUint16(entryOffset, littleEndian);\n if (tag === 0x0112) {\n const value = view.getUint16(entryOffset + 8, littleEndian);\n return value >= 1 && value <= 8 ? value : 1;\n }\n }\n return 1;\n}\n\nexport interface OrientationTransform {\n rotation: number;\n flipHorizontal: boolean;\n}\n\n/**\n * Maps an EXIF orientation value to the rotation + horizontal-flip pair that\n * normalizes it. A vertical flip is never needed on its own: orientation 4\n * (mirror vertical) is expressed as rotate(180) + flip horizontal, which is\n * mathematically equivalent and matches Kiri's transform order (flip is\n * applied before rotation, so this composes correctly).\n */\nexport function orientationToTransform(orientation: number): OrientationTransform {\n switch (orientation) {\n case 2:\n return { rotation: 0, flipHorizontal: true };\n case 3:\n return { rotation: 180, flipHorizontal: false };\n case 4:\n return { rotation: 180, flipHorizontal: true };\n case 5:\n return { rotation: 90, flipHorizontal: true };\n case 6:\n return { rotation: 90, flipHorizontal: false };\n case 7:\n return { rotation: 270, flipHorizontal: true };\n case 8:\n return { rotation: 270, flipHorizontal: false };\n default:\n return { rotation: 0, flipHorizontal: false };\n }\n}\n","/**\n * Resolves an optional string-enum option: undefined silently uses the\n * default, a valid value passes through, and an invalid value warns and\n * falls back to the default rather than silently misbehaving (e.g. a\n * typo'd option matching no CSS rule, or an unsupported canvas export type).\n */\nexport function resolveEnumOption<T extends string>(\n value: T | undefined,\n valid: readonly T[],\n fallback: T,\n optionName: string\n): T {\n if (value === undefined) return fallback;\n if ((valid as readonly string[]).includes(value)) return value;\n console.warn(\n `Kiri: invalid ${optionName} \"${value}\" — defaulting to \"${fallback}\". ` +\n `Valid values: ${valid.join(\", \")}.`\n );\n return fallback;\n}\n","import type {\n CropRegion,\n ExportFormat,\n ExportOptions,\n ExportResult,\n ExportType,\n FrameShape,\n KiriState,\n Offset,\n} from \"./types\";\nimport {\n computeCoverScale,\n effectiveRenderedSize,\n normalizeRotation,\n type Size,\n} from \"./gestures\";\nimport { buildFilterString } from \"./filters\";\nimport { resolveEnumOption } from \"./validate\";\n\nfunction clampNum(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nconst VALID_EXPORT_TYPES: ExportType[] = [\"base64\", \"blob\", \"canvas\"];\nconst VALID_EXPORT_FORMATS: ExportFormat[] = [\"image/jpeg\", \"image/png\", \"image/webp\"];\n\n/**\n * Top-left corner of the frame, in the local pixel space of the rendered\n * (rotated + scaled) image. Pure so the crop-region math is testable without\n * a real canvas: the frame is centered on the stage, the image's own center\n * sits at `offset` from that same point, so the frame's top-left in the\n * rendered image's local space is the rendered center, shifted back by the\n * offset.\n */\nexport function computeFrameSourceRect(\n rendered: Size,\n offset: Offset,\n frame: Size\n): { left: number; top: number } {\n return {\n left: rendered.width / 2 - offset.x - frame.width / 2,\n top: rendered.height / 2 - offset.y - frame.height / 2,\n };\n}\n\n/**\n * Maps the current crop selection back into the *original, unrotated,\n * unflipped* source image's own pixel coordinates. Corner-based: takes the\n * frame rect's four corners in rendered (rotated + scaled) space, undoes the\n * scale, then inverse-rotates each corner (rotation is always a multiple of\n * 90°, so this stays an axis-aligned rectangle — no interpolation needed)\n * back into the natural image's coordinate space, then takes the bounding\n * box. Flip doesn't move the rectangle (mirroring is content-only), so it's\n * carried through untouched in the result for the caller to apply.\n */\nexport function computeCropRegion(natural: Size, frame: Size, state: KiriState): CropRegion {\n const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;\n const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);\n const { left: frameLeft, top: frameTop } = computeFrameSourceRect(rendered, state.offset, frame);\n\n const renderedCenter = { x: rendered.width / 2, y: rendered.height / 2 };\n const naturalCenter = { x: natural.width / 2, y: natural.height / 2 };\n const rotation = normalizeRotation(state.rotation);\n\n const corners = [\n { x: frameLeft, y: frameTop },\n { x: frameLeft + frame.width, y: frameTop },\n { x: frameLeft, y: frameTop + frame.height },\n { x: frameLeft + frame.width, y: frameTop + frame.height },\n ].map(({ x, y }) => {\n const cx = (x - renderedCenter.x) / scale;\n const cy = (y - renderedCenter.y) / scale;\n let u: number;\n let v: number;\n if (rotation === 90) {\n u = cy;\n v = -cx;\n } else if (rotation === 180) {\n u = -cx;\n v = -cy;\n } else if (rotation === 270) {\n u = -cy;\n v = cx;\n } else {\n u = cx;\n v = cy;\n }\n return { x: u + naturalCenter.x, y: v + naturalCenter.y };\n });\n\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n // Clamp each edge independently (not just the origin) — a frame that\n // overhangs the image (e.g. offset drags it past an edge) must shrink\n // width/height to match, not just get shifted while keeping its\n // unclamped size.\n const left = clampNum(Math.min(...xs), 0, natural.width);\n const top = clampNum(Math.min(...ys), 0, natural.height);\n const right = clampNum(Math.max(...xs), 0, natural.width);\n const bottom = clampNum(Math.max(...ys), 0, natural.height);\n\n return {\n x: Math.round(left),\n y: Math.round(top),\n width: Math.round(right - left),\n height: Math.round(bottom - top),\n rotation,\n flip: { ...state.flip },\n };\n}\n\nexport function renderCropToCanvas(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n outputWidth: number,\n outputHeight: number,\n frameShape: FrameShape\n): HTMLCanvasElement {\n const natural: Size = { width: img.naturalWidth, height: img.naturalHeight };\n const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;\n const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);\n\n const sourceCanvas = document.createElement(\"canvas\");\n sourceCanvas.width = Math.max(1, Math.round(rendered.width));\n sourceCanvas.height = Math.max(1, Math.round(rendered.height));\n const sctx = sourceCanvas.getContext(\"2d\");\n if (!sctx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n sctx.translate(sourceCanvas.width / 2, sourceCanvas.height / 2);\n sctx.rotate((state.rotation * Math.PI) / 180);\n // Flip is folded into scale's sign, applied before rotate (matches\n // stage.ts's transform order) so rotation happens in \"already mirrored\"\n // space, consistent with what the user sees on screen.\n sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));\n // Same CSS filter string as the live preview (see stage.ts's applyFilters),\n // so the browser's own filter implementation guarantees they match exactly.\n sctx.filter = buildFilterString(state.filters);\n sctx.drawImage(img, -natural.width / 2, -natural.height / 2, natural.width, natural.height);\n\n const { left: frameLeft, top: frameTop } = computeFrameSourceRect(\n rendered,\n state.offset,\n frame\n );\n\n const outCanvas = document.createElement(\"canvas\");\n outCanvas.width = outputWidth;\n outCanvas.height = outputHeight;\n const octx = outCanvas.getContext(\"2d\");\n if (!octx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n // The circle frame is otherwise just a visual overlay (stage.ts's\n // .kiri-frame--circle border) — without this, the export would always be\n // a plain rectangle regardless of frame.shape. Clipping to an ellipse\n // inscribed in the output canvas matches what's visible on screen even\n // when a custom output width/height changes its aspect ratio.\n if (frameShape === \"circle\") {\n octx.save();\n octx.beginPath();\n octx.ellipse(\n outputWidth / 2,\n outputHeight / 2,\n outputWidth / 2,\n outputHeight / 2,\n 0,\n 0,\n Math.PI * 2\n );\n octx.clip();\n }\n\n octx.drawImage(\n sourceCanvas,\n frameLeft,\n frameTop,\n frame.width,\n frame.height,\n 0,\n 0,\n outputWidth,\n outputHeight\n );\n\n if (frameShape === \"circle\") octx.restore();\n\n return outCanvas;\n}\n\nexport async function exportCrop(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n frameShape: FrameShape,\n options: ExportOptions\n): Promise<ExportResult> {\n const type = resolveEnumOption(options.type, VALID_EXPORT_TYPES, \"base64\", \"export type\");\n const format = resolveEnumOption(options.format, VALID_EXPORT_FORMATS, \"image/png\", \"export format\");\n const quality = options.quality;\n const width = options.width ?? frame.width;\n const height = options.height ?? frame.height;\n\n if (frameShape === \"circle\" && format === \"image/jpeg\") {\n console.warn(\n \"Kiri: exporting a circle-shaped frame as image/jpeg — JPEG has no \" +\n \"alpha channel, so the area outside the circle will render as solid \" +\n \"black instead of transparent. Use image/png or image/webp instead.\"\n );\n }\n\n const canvas = renderCropToCanvas(img, state, frame, width, height, frameShape);\n\n if (type === \"canvas\") return canvas;\n if (type === \"base64\") return canvas.toDataURL(format, quality);\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Kiri: canvas.toBlob failed\"))),\n format,\n quality\n );\n });\n}\n","import type { UploadOptions } from \"./types\";\n\n/**\n * Default uploader: builds a FormData from the crop blob and POSTs it via\n * fetch. Swappable per-call (`UploadOptions.uploader`) or per-instance\n * (`KiriOptions.uploader`) for a custom protocol (presigned URLs, GraphQL,\n * etc.) while callers keep calling the same `cropper.upload(url, options)`.\n */\nexport async function uploadBlob(\n blob: Blob,\n options: UploadOptions & { url: string }\n): Promise<Response> {\n const fieldName = options.fieldName ?? \"file\";\n const fileName = options.fileName ?? `crop.${extensionFor(options.format ?? \"image/png\")}`;\n\n const formData = new FormData();\n formData.append(fieldName, blob, fileName);\n for (const [key, value] of Object.entries(options.extraFields ?? {})) {\n formData.append(key, value);\n }\n\n return fetch(options.url, {\n ...options.fetchOptions,\n method: \"POST\",\n body: formData,\n });\n}\n\nfunction extensionFor(format: string): string {\n switch (format) {\n case \"image/jpeg\":\n return \"jpg\";\n case \"image/webp\":\n return \"webp\";\n default:\n return \"png\";\n }\n}\n","import type {\n CropRegion,\n ExportOptions,\n ExportResult,\n Filters,\n Flip,\n FrameShape,\n KiriEventCallback,\n KiriEventName,\n KiriOptions,\n KiriState,\n LoadOptions,\n Offset,\n UploadOptions,\n Uploader,\n ZoomerPosition,\n} from \"./types\";\nimport {\n applyFilters,\n applyTransform,\n createStage,\n setFrameSize as setStageFrameSize,\n setStageSize,\n STAGE_AUTO_SIZE_PADDING,\n type StageElements,\n} from \"./stage\";\nimport {\n attachGestures,\n clampOffset,\n clampZoom,\n computeCoverScale,\n effectiveRenderedSize,\n normalizeRotation,\n type Size,\n} from \"./gestures\";\nimport { orientationToTransform, readExifOrientation } from \"./exif\";\nimport { computeCropRegion, exportCrop } from \"./export\";\nimport { DEFAULT_FILTERS, mergeFilters } from \"./filters\";\nimport { uploadBlob } from \"./upload\";\nimport { resolveEnumOption } from \"./validate\";\n\nconst DEFAULT_FRAME_SIZE = 200;\nconst MIN_FRAME_SIZE = 20;\nconst VALID_FRAME_SHAPES: FrameShape[] = [\"rectangle\", \"circle\"];\nconst VALID_ZOOMER_POSITIONS: ZoomerPosition[] = [\"top\", \"bottom\", \"left\", \"right\"];\n\nfunction resolveMouseWheelZoom(value: KiriOptions[\"mouseWheelZoom\"]): boolean | \"ctrl\" {\n if (value === undefined) return true;\n if (typeof value === \"boolean\" || value === \"ctrl\") return value;\n console.warn(\n `Kiri: invalid mouseWheelZoom \"${String(value)}\" — defaulting to true. ` +\n `Valid values: true, false, \"ctrl\".`\n );\n return true;\n}\n\ninterface ResolvedOptions {\n frame: { shape: FrameShape; width: number; height: number };\n minZoom: number;\n maxZoom: number;\n rotatable: boolean;\n flippable: boolean;\n resizableFrame: boolean;\n lockAspectRatio: boolean;\n mouseWheelZoom: boolean | \"ctrl\";\n useExifOrientation: boolean;\n uploader: Uploader | undefined;\n autoSizeStage: boolean;\n showZoomer: boolean;\n zoomerPosition: ZoomerPosition;\n}\n\n/**\n * An interactive image cropper attached to a plain DOM element. Drag to pan,\n * zoom via wheel/pinch/an optional built-in slider, rotate in 90° steps,\n * flip, apply filters, then export or upload the crop.\n */\nexport class Kiri {\n private readonly container: HTMLElement;\n private readonly opts: ResolvedOptions;\n private readonly stage: StageElements;\n private readonly gestureHandle: { destroy: () => void };\n private resizeHandle: { destroy: () => void } | null = null;\n private zoomerHandle: { destroy: () => void } | null = null;\n private naturalSize: Size = { width: 0, height: 0 };\n private state: KiriState = {\n zoom: 1,\n offset: { x: 0, y: 0 },\n rotation: 0,\n flip: { horizontal: false, vertical: false },\n filters: DEFAULT_FILTERS,\n };\n /** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */\n private initialState: KiriState | null = null;\n private listeners: Record<KiriEventName, KiriEventCallback[]> = { change: [] };\n\n /**\n * @param container An element already present in the DOM. Passing\n * `null`/`undefined`, or an element that isn't in the DOM yet, throws.\n * @param options See the {@link KiriOptions} fields for defaults.\n */\n constructor(container: HTMLElement, options: KiriOptions = {}) {\n if (!container || typeof container.appendChild !== \"function\") {\n throw new Error(\n \"Kiri: container element is null/undefined or not a DOM element. \" +\n \"This usually means the element wasn't in the DOM yet when \" +\n \"`new Kiri(...)` ran — e.g. document.getElementById() was called \" +\n \"before the element existed. Place the <script> after the \" +\n \"element, or construct inside a DOMContentLoaded listener.\"\n );\n }\n this.container = container;\n this.opts = {\n frame: {\n shape: resolveEnumOption(\n options.frame?.shape,\n VALID_FRAME_SHAPES,\n \"rectangle\",\n \"frame.shape\"\n ),\n width: options.frame?.width ?? DEFAULT_FRAME_SIZE,\n height: options.frame?.height ?? DEFAULT_FRAME_SIZE,\n },\n minZoom: options.minZoom ?? 1,\n maxZoom: options.maxZoom ?? 4,\n rotatable: options.rotatable ?? true,\n flippable: options.flippable ?? true,\n resizableFrame: options.resizableFrame ?? false,\n lockAspectRatio: options.lockAspectRatio ?? false,\n mouseWheelZoom: resolveMouseWheelZoom(options.mouseWheelZoom),\n useExifOrientation: options.useExifOrientation ?? true,\n uploader: options.uploader,\n autoSizeStage: options.autoSizeStage ?? true,\n showZoomer: options.showZoomer ?? false,\n zoomerPosition: resolveEnumOption(\n options.zoomerPosition,\n VALID_ZOOMER_POSITIONS,\n \"bottom\",\n \"zoomerPosition\"\n ),\n };\n this.state.filters = mergeFilters(DEFAULT_FILTERS, options.filters ?? {});\n\n this.stage = createStage(\n this.container,\n this.opts.frame.shape,\n this.opts.frame.width,\n this.opts.frame.height,\n {\n show: this.opts.showZoomer,\n position: this.opts.zoomerPosition,\n min: this.opts.minZoom,\n max: this.opts.maxZoom,\n value: this.state.zoom,\n }\n );\n if (this.opts.autoSizeStage) this.syncStageSize();\n applyFilters(this.stage.imgEl, this.state.filters);\n if (this.stage.zoomerEl) this.enableZoomer(this.stage.zoomerEl);\n\n this.gestureHandle = attachGestures(\n this.stage.stageEl,\n {\n getNaturalSize: () => this.naturalSize,\n getFrameSize: () => this.getFrameSize(),\n getState: () => this.state,\n getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),\n setState: (next) => this.commitState(next),\n reset: () => this.reset(),\n },\n { mouseWheelZoom: this.opts.mouseWheelZoom }\n );\n\n if (this.opts.resizableFrame) this.enableFrameResize();\n }\n\n /**\n * Loads an image, replacing whatever was loaded before. EXIF orientation\n * (rotation + horizontal flip) is corrected automatically unless\n * `useExifOrientation: false` was passed to the constructor — only for\n * `File`/`Blob` sources, since a plain URL string can't be read for EXIF\n * data without an extra fetch.\n * @param source A `File` (e.g. from a file input), a `Blob`, or a URL string.\n * @param loadOptions Initial `zoom`/`offset`/`rotation`/`flip`.\n */\n async load(source: File | Blob | string, loadOptions: LoadOptions = {}): Promise<void> {\n let rotation = normalizeRotation(loadOptions.rotation ?? 0);\n let flipHorizontal = loadOptions.flip?.horizontal ?? false;\n const flipVertical = loadOptions.flip?.vertical ?? false;\n let objectUrl: string | null = null;\n let url: string;\n\n if (typeof source === \"string\") {\n url = source;\n } else {\n if (this.opts.useExifOrientation) {\n const buffer = await source.arrayBuffer();\n const orientation = readExifOrientation(buffer);\n const exifTransform = orientationToTransform(orientation);\n rotation = normalizeRotation(rotation + exifTransform.rotation);\n // XOR: two horizontal flips (EXIF + a requested one) cancel out.\n flipHorizontal = flipHorizontal !== exifTransform.flipHorizontal;\n }\n objectUrl = URL.createObjectURL(source);\n url = objectUrl;\n }\n\n try {\n await new Promise<void>((resolve, reject) => {\n this.stage.imgEl.onload = () => resolve();\n this.stage.imgEl.onerror = () => reject(new Error(\"Kiri: failed to load image\"));\n this.stage.imgEl.src = url;\n });\n } finally {\n // Must run on the error path too, not just success — otherwise a\n // rejected load() (corrupt file, unsupported format) leaks the blob\n // URL for the rest of the page's lifetime.\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n }\n\n this.naturalSize = {\n width: this.stage.imgEl.naturalWidth,\n height: this.stage.imgEl.naturalHeight,\n };\n\n const zoom = clampZoom(\n loadOptions.zoom ?? this.opts.minZoom,\n this.opts.minZoom,\n this.opts.maxZoom\n );\n const rendered = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, zoom);\n const offset = clampOffset(loadOptions.offset ?? { x: 0, y: 0 }, rendered, this.getFrameSize());\n\n this.commitState({\n zoom,\n offset,\n rotation,\n flip: { horizontal: flipHorizontal, vertical: flipVertical },\n filters: this.state.filters,\n });\n this.initialState = this.getState();\n }\n\n /** A snapshot of the current state — mutating the returned object has no effect. */\n getState(): KiriState {\n return {\n zoom: this.state.zoom,\n offset: { ...this.state.offset },\n rotation: this.state.rotation,\n flip: { ...this.state.flip },\n filters: { ...this.state.filters },\n };\n }\n\n /** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */\n setZoom(zoom: number): void {\n const clamped = clampZoom(zoom, this.opts.minZoom, this.opts.maxZoom);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n clamped\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, zoom: clamped, offset });\n }\n\n /**\n * Sets the pan offset to an absolute value (image-center offset from the\n * frame center, in stage pixels), clamped so the frame stays fully covered\n * by the rendered image.\n */\n setOffset(offset: Offset): void {\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n this.state.zoom\n );\n const clamped = clampOffset(offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, offset: clamped });\n }\n\n /**\n * Reverts zoom/offset/rotation/flip/filters to what they were right after\n * `load()` resolved (including any `loadOptions` passed to it). No-op if\n * nothing has been loaded yet.\n */\n reset(): void {\n if (!this.initialState) return;\n this.commitState({\n zoom: this.initialState.zoom,\n offset: { ...this.initialState.offset },\n rotation: this.initialState.rotation,\n flip: { ...this.initialState.flip },\n filters: { ...this.initialState.filters },\n });\n }\n\n /**\n * Rotates relative to the current rotation, snapped to the nearest 90°.\n * No-op if `rotatable: false` was passed to the constructor.\n */\n rotate(deltaDeg: number): void {\n if (!this.opts.rotatable) return;\n const snapped = Math.round(deltaDeg / 90) * 90;\n const rotation = normalizeRotation(this.state.rotation + snapped);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, rotation, offset });\n }\n\n /** Toggles horizontal flip, independent of rotation. No-op if `flippable: false`. */\n flipHorizontal(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, horizontal: !this.state.flip.horizontal };\n this.commitState({ ...this.state, flip });\n }\n\n /** Toggles vertical flip, independent of rotation. No-op if `flippable: false`. */\n flipVertical(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, vertical: !this.state.flip.vertical };\n this.commitState({ ...this.state, flip });\n }\n\n /**\n * Resizes the frame. Also resizes the stage to match, if\n * `autoSizeStage: true` (the default). Each axis is clamped to a 20px\n * minimum.\n */\n setFrameSize(width: number, height: number): void {\n this.opts.frame.width = Math.max(MIN_FRAME_SIZE, width);\n this.opts.frame.height = Math.max(MIN_FRAME_SIZE, height);\n setStageFrameSize(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height);\n if (this.opts.autoSizeStage) this.syncStageSize();\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, offset });\n }\n\n /**\n * Merges a partial update into the current filters (omitted fields are\n * left as they are). Numeric values are clamped to `>= 0`.\n */\n setFilters(filters: Partial<Filters>): void {\n this.commitState({ ...this.state, filters: mergeFilters(this.state.filters, filters) });\n }\n\n /**\n * Renders the current crop. A circle frame is a real clip in the output\n * (transparent corners on PNG/WebP); a circle exported as JPEG warns and\n * renders solid black corners instead, since JPEG has no alpha channel.\n * @returns A data URL string (`type: \"base64\"`, the default), a `Blob`, or an `HTMLCanvasElement`.\n */\n async export(options: ExportOptions = {}): Promise<ExportResult> {\n return exportCrop(\n this.stage.imgEl,\n this.state,\n this.getFrameSize(),\n this.opts.frame.shape,\n options\n );\n }\n\n /**\n * The current crop selection as a rectangle in the original, unrotated,\n * unflipped source image's own pixel coordinates — for sending to a server\n * that will crop the full-resolution original itself instead of uploading\n * a client-re-encoded image. See {@link CropRegion}.\n */\n getCropRegion(): CropRegion {\n return computeCropRegion(this.naturalSize, this.getFrameSize(), this.state);\n }\n\n /**\n * Exports the current crop as a blob, then uploads it — a default\n * FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`\n * wins over the constructor's, which wins over the built-in default).\n */\n async upload(url: string, options: UploadOptions = {}): Promise<unknown> {\n const blob = (await this.export({ ...options, type: \"blob\" })) as Blob;\n const uploader = options.uploader ?? this.opts.uploader ?? uploadBlob;\n return uploader(blob, { ...options, url });\n }\n\n /** Subscribes to `\"change\"` — fires on every state update (drag/zoom/rotate/flip/filters), and once after `load()` resolves. */\n on(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event].push(callback);\n }\n\n /** Unsubscribes a callback previously passed to {@link on}. */\n off(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);\n }\n\n /**\n * Tears the instance down: removes all pointer/wheel event listeners\n * (drag/zoom gestures), the resize-handle listener (if `resizableFrame`),\n * and the zoom-slider listener (if `showZoomer`); clears the container's\n * `innerHTML`, leaving an empty container element; and clears all\n * `\"change\"` listeners. Call this when you're done with an instance (e.g.\n * unmounting) to avoid leaking listeners.\n */\n destroy(): void {\n this.gestureHandle.destroy();\n this.resizeHandle?.destroy();\n this.zoomerHandle?.destroy();\n this.container.innerHTML = \"\";\n this.listeners.change = [];\n }\n\n private getFrameSize(): Size {\n return { width: this.opts.frame.width, height: this.opts.frame.height };\n }\n\n private syncStageSize(): void {\n setStageSize(\n this.stage.stageEl,\n this.opts.frame.width + STAGE_AUTO_SIZE_PADDING * 2,\n this.opts.frame.height + STAGE_AUTO_SIZE_PADDING * 2\n );\n }\n\n private commitState(next: KiriState): void {\n this.state = next;\n const scale =\n computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;\n applyTransform(this.stage.imageLayerEl, next, scale);\n applyFilters(this.stage.imgEl, next.filters);\n // Keeps the slider in sync regardless of what triggered the zoom change\n // (wheel, pinch, drag-clamping, or setZoom() itself) — setting .value\n // programmatically doesn't re-fire \"input\", so no feedback loop.\n if (this.stage.zoomerEl) this.stage.zoomerEl.value = String(next.zoom);\n for (const cb of this.listeners.change) cb(this.getState());\n }\n\n private enableZoomer(zoomerEl: HTMLInputElement): void {\n const onInput = (): void => this.setZoom(Number(zoomerEl.value));\n zoomerEl.addEventListener(\"input\", onInput);\n this.zoomerHandle = {\n destroy(): void {\n zoomerEl.removeEventListener(\"input\", onInput);\n },\n };\n }\n\n private enableFrameResize(): void {\n // Corners named by which edges they sit on; signX/signY say which way\n // dragging that corner should grow the frame on each axis. The frame is\n // always centered in the stage (kiri.css), so moving any one corner by\n // `d` pixels grows/shrinks that axis by `2*d` to keep the opposite edge\n // stationary and make the drag feel corner-anchored.\n const corners: { name: string; edge: { left?: string; right?: string; top?: string; bottom?: string }; signX: number; signY: number }[] = [\n { name: \"top-left\", edge: { left: \"-5px\", top: \"-5px\" }, signX: -1, signY: -1 },\n { name: \"top-right\", edge: { right: \"-5px\", top: \"-5px\" }, signX: 1, signY: -1 },\n { name: \"bottom-left\", edge: { left: \"-5px\", bottom: \"-5px\" }, signX: -1, signY: 1 },\n { name: \"bottom-right\", edge: { right: \"-5px\", bottom: \"-5px\" }, signX: 1, signY: 1 },\n ];\n\n const cleanups: (() => void)[] = [];\n\n for (const corner of corners) {\n const handle = document.createElement(\"div\");\n handle.className = `kiri-frame-handle kiri-frame-handle--${corner.name}`;\n Object.assign(handle.style, corner.edge);\n this.stage.frameEl.appendChild(handle);\n\n let start: { x: number; y: number; width: number; height: number } | null = null;\n\n const onDown = (e: PointerEvent): void => {\n e.stopPropagation();\n handle.setPointerCapture(e.pointerId);\n start = {\n x: e.clientX,\n y: e.clientY,\n width: this.opts.frame.width,\n height: this.opts.frame.height,\n };\n };\n const onMove = (e: PointerEvent): void => {\n if (!start) return;\n const dx = (e.clientX - start.x) * 2 * corner.signX;\n const dy = (e.clientY - start.y) * 2 * corner.signY;\n const rawWidth = start.width + dx;\n const rawHeight = start.height + dy;\n\n if (this.opts.lockAspectRatio) {\n const aspect = start.width / start.height;\n // Whichever axis moved more (in aspect-normalized units) drives\n // the resize; the other axis follows to preserve the ratio.\n if (Math.abs(rawWidth - start.width) >= Math.abs(rawHeight - start.height) * aspect) {\n this.setFrameSize(rawWidth, rawWidth / aspect);\n } else {\n this.setFrameSize(rawHeight * aspect, rawHeight);\n }\n } else {\n this.setFrameSize(rawWidth, rawHeight);\n }\n };\n const onUp = (): void => {\n start = null;\n };\n\n handle.addEventListener(\"pointerdown\", onDown);\n handle.addEventListener(\"pointermove\", onMove);\n handle.addEventListener(\"pointerup\", onUp);\n handle.addEventListener(\"pointercancel\", onUp);\n\n cleanups.push(() => {\n handle.removeEventListener(\"pointerdown\", onDown);\n handle.removeEventListener(\"pointermove\", onMove);\n handle.removeEventListener(\"pointerup\", onUp);\n handle.removeEventListener(\"pointercancel\", onUp);\n handle.remove();\n });\n }\n\n this.resizeHandle = {\n destroy(): void {\n for (const cleanup of cleanups) cleanup();\n },\n };\n }\n}\n"],"names":["setStageFrameSize"],"mappings":"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"}
1
+ {"version":3,"file":"kiri.js","sources":["../src/filters.ts","../src/stage.ts","../src/gestures.ts","../src/exif.ts","../src/validate.ts","../src/export.ts","../src/upload.ts","../src/kiri.ts"],"sourcesContent":["import type { Filters } from \"./types\";\n\nexport const DEFAULT_FILTERS: Filters = {\n brightness: 1,\n contrast: 1,\n saturation: 1,\n grayscale: false,\n sepia: false,\n};\n\n/** Merges a partial filter update into the current filters, clamping numeric values to >= 0. */\nexport function mergeFilters(current: Filters, partial: Partial<Filters>): Filters {\n return {\n brightness: Math.max(0, partial.brightness ?? current.brightness),\n contrast: Math.max(0, partial.contrast ?? current.contrast),\n saturation: Math.max(0, partial.saturation ?? current.saturation),\n grayscale: partial.grayscale ?? current.grayscale,\n sepia: partial.sepia ?? current.sepia,\n };\n}\n\n/**\n * Builds a CSS `filter` value from the current filter state. Used verbatim as\n * both the live-preview `img.style.filter` and a canvas 2D context's\n * `ctx.filter` before drawing, so preview and export always match exactly —\n * no separately hand-rolled brightness/contrast/saturation pixel math.\n */\nexport function buildFilterString(filters: Filters): string {\n const parts = [\n `brightness(${filters.brightness})`,\n `contrast(${filters.contrast})`,\n `saturate(${filters.saturation})`,\n ];\n if (filters.grayscale) parts.push(\"grayscale(1)\");\n if (filters.sepia) parts.push(\"sepia(1)\");\n return parts.join(\" \");\n}\n","import type { Filters, FrameShape, KiriState, ZoomerPosition } from \"./types\";\nimport { buildFilterString } from \"./filters\";\n\nexport interface StageElements {\n stageEl: HTMLDivElement;\n frameEl: HTMLDivElement;\n imageLayerEl: HTMLDivElement;\n imgEl: HTMLImageElement;\n zoomerEl: HTMLInputElement | null;\n}\n\nexport interface ZoomerConfig {\n show: boolean;\n position: ZoomerPosition;\n min: number;\n max: number;\n value: number;\n}\n\nexport function createStage(\n container: HTMLElement,\n frameShape: FrameShape,\n frameWidth: number,\n frameHeight: number,\n cornerRadius: number,\n zoomer: ZoomerConfig\n): StageElements {\n container.innerHTML = \"\";\n\n const stageEl = document.createElement(\"div\");\n stageEl.className = \"kiri-stage\";\n // Keyboard support (see gestures.ts's onKeyDown) only fires once the\n // element can receive focus; role/label make it announce sensibly to\n // screen readers instead of as an unlabeled generic div.\n stageEl.tabIndex = 0;\n stageEl.setAttribute(\"role\", \"application\");\n stageEl.setAttribute(\n \"aria-label\",\n \"Image cropper. Drag to pan. Arrow keys to pan, plus/minus to zoom, 0 to reset.\"\n );\n\n const imageLayerEl = document.createElement(\"div\");\n imageLayerEl.className = \"kiri-image-layer\";\n\n const imgEl = document.createElement(\"img\");\n imgEl.draggable = false;\n imageLayerEl.appendChild(imgEl);\n\n const frameEl = document.createElement(\"div\");\n frameEl.className =\n frameShape === \"circle\"\n ? \"kiri-frame kiri-frame--circle\"\n : frameShape === \"rounded-rectangle\"\n ? \"kiri-frame kiri-frame--rounded-rectangle\"\n : \"kiri-frame\";\n setFrameSize(frameEl, frameWidth, frameHeight);\n // Radius is a per-instance pixel value (unlike circle's fixed 50%), so it\n // can't be a static CSS rule — set inline instead.\n if (frameShape === \"rounded-rectangle\") {\n frameEl.style.borderRadius = `${cornerRadius}px`;\n }\n\n stageEl.appendChild(imageLayerEl);\n stageEl.appendChild(frameEl);\n\n let zoomerEl: HTMLInputElement | null = null;\n\n if (zoomer.show) {\n const rootEl = document.createElement(\"div\");\n rootEl.className = `kiri-root kiri-root--${zoomer.position}`;\n\n zoomerEl = document.createElement(\"input\");\n zoomerEl.type = \"range\";\n zoomerEl.className = \"kiri-zoomer\";\n zoomerEl.min = String(zoomer.min);\n zoomerEl.max = String(zoomer.max);\n zoomerEl.step = \"0.01\";\n zoomerEl.value = String(zoomer.value);\n\n rootEl.appendChild(stageEl);\n rootEl.appendChild(zoomerEl);\n container.appendChild(rootEl);\n } else {\n container.appendChild(stageEl);\n }\n\n return { stageEl, frameEl, imageLayerEl, imgEl, zoomerEl };\n}\n\nexport function setFrameSize(\n frameEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n frameEl.style.width = `${width}px`;\n frameEl.style.height = `${height}px`;\n}\n\n/** Padding added around the frame when the stage auto-sizes itself — small\n * enough to avoid empty space, big enough to fit the resize handle and give\n * a visible drag margin. */\nexport const STAGE_AUTO_SIZE_PADDING = 20;\n\nexport function setStageSize(\n stageEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n stageEl.style.width = `${width}px`;\n stageEl.style.height = `${height}px`;\n}\n\nexport function applyTransform(\n imageLayerEl: HTMLDivElement,\n state: KiriState,\n renderedScale: number\n): void {\n // translate(-50%,-50%) centers the layer's own center at the stage center;\n // the pixel translate shifts it by the (rotation/scale-independent) offset;\n // rotate then scale (with flip folded into scale's sign) apply around the\n // layer's own center (default transform-origin), flip first/innermost so a\n // mirrored image still rotates the way the user expects.\n const scaleX = renderedScale * (state.flip.horizontal ? -1 : 1);\n const scaleY = renderedScale * (state.flip.vertical ? -1 : 1);\n imageLayerEl.style.transform =\n `translate(-50%, -50%) ` +\n `translate(${state.offset.x}px, ${state.offset.y}px) ` +\n `rotate(${state.rotation}deg) ` +\n `scale(${scaleX}, ${scaleY})`;\n}\n\nexport function applyFilters(imgEl: HTMLImageElement, filters: Filters): void {\n imgEl.style.filter = buildFilterString(filters);\n}\n","import type { KiriState, Offset } from \"./types\";\n\nexport interface Size {\n width: number;\n height: number;\n}\n\n/** Rotation-aware natural size: 90/270 degrees swap width and height. */\nexport function effectiveNaturalSize(natural: Size, rotationDeg: number): Size {\n const swapped = ((rotationDeg / 90) % 2 + 2) % 2 === 1;\n return swapped\n ? { width: natural.height, height: natural.width }\n : { width: natural.width, height: natural.height };\n}\n\n/** The smallest scale at which the (rotation-adjusted) image still fully covers the frame. */\nexport function computeCoverScale(\n natural: Size,\n frame: Size,\n rotationDeg: number\n): number {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n if (eff.width <= 0 || eff.height <= 0) return 1;\n return Math.max(frame.width / eff.width, frame.height / eff.height);\n}\n\n/** Rendered size of the image layer given a user zoom multiplier (relative to cover scale). */\nexport function effectiveRenderedSize(\n natural: Size,\n frame: Size,\n rotationDeg: number,\n zoom: number\n): Size {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n const scale = computeCoverScale(natural, frame, rotationDeg) * zoom;\n return { width: eff.width * scale, height: eff.height * scale };\n}\n\nexport function clampZoom(zoom: number, minZoom: number, maxZoom: number): number {\n return Math.min(Math.max(zoom, minZoom), maxZoom);\n}\n\n/**\n * Clamp the image-center offset (from stage/frame center) so the frame stays\n * fully covered by the rendered image on every axis.\n */\nexport function clampOffset(offset: Offset, rendered: Size, frame: Size): Offset {\n const maxX = Math.max(0, (rendered.width - frame.width) / 2);\n const maxY = Math.max(0, (rendered.height - frame.height) / 2);\n // `|| 0` normalizes -0 (e.g. clamping a negative offset to a zero-width\n // range) to +0, keeping state comparisons and serialized CSS values sane.\n return {\n x: Math.min(Math.max(offset.x, -maxX), maxX) || 0,\n y: Math.min(Math.max(offset.y, -maxY), maxY) || 0,\n };\n}\n\nexport function normalizeRotation(deg: number): number {\n return ((deg % 360) + 360) % 360;\n}\n\ninterface GestureCallbacks {\n getNaturalSize: () => Size;\n getFrameSize: () => Size;\n getState: () => KiriState;\n getMinMaxZoom: () => { min: number; max: number };\n setState: (next: KiriState) => void;\n /** Called on the \"0\" key — reverts to the post-`load()` state. */\n reset: () => void;\n}\n\nconst KEYBOARD_PAN_STEP = 15;\nconst KEYBOARD_ZOOM_STEP = 0.1;\n\nexport interface GestureOptions {\n mouseWheelZoom?: boolean | \"ctrl\";\n}\n\nfunction pointerDistance(a: PointerEvent, b: PointerEvent): number {\n return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);\n}\n\nexport function attachGestures(\n stageEl: HTMLElement,\n callbacks: GestureCallbacks,\n options: GestureOptions\n): { destroy: () => void } {\n const activePointers = new Map<number, PointerEvent>();\n let dragStart: { x: number; y: number; offset: Offset } | null = null;\n let pinchStartDistance = 0;\n let pinchStartZoom = 1;\n\n function applyClampedState(next: KiriState): void {\n const { min, max } = callbacks.getMinMaxZoom();\n const zoom = clampZoom(next.zoom, min, max);\n const rendered = effectiveRenderedSize(\n callbacks.getNaturalSize(),\n callbacks.getFrameSize(),\n next.rotation,\n zoom\n );\n const offset = clampOffset(next.offset, rendered, callbacks.getFrameSize());\n callbacks.setState({\n zoom,\n offset,\n rotation: normalizeRotation(next.rotation),\n flip: next.flip,\n filters: next.filters,\n });\n }\n\n function onPointerDown(e: PointerEvent): void {\n stageEl.setPointerCapture(e.pointerId);\n activePointers.set(e.pointerId, e);\n if (activePointers.size === 1) {\n const state = callbacks.getState();\n dragStart = { x: e.clientX, y: e.clientY, offset: state.offset };\n stageEl.classList.add(\"kiri-dragging\");\n } else if (activePointers.size === 2) {\n dragStart = null;\n const [a, b] = [...activePointers.values()];\n pinchStartDistance = pointerDistance(a, b);\n pinchStartZoom = callbacks.getState().zoom;\n }\n }\n\n function onPointerMove(e: PointerEvent): void {\n if (!activePointers.has(e.pointerId)) return;\n activePointers.set(e.pointerId, e);\n\n if (activePointers.size === 2) {\n const [a, b] = [...activePointers.values()];\n const distance = pointerDistance(a, b);\n if (pinchStartDistance > 0) {\n const ratio = distance / pinchStartDistance;\n const state = callbacks.getState();\n applyClampedState({ ...state, zoom: pinchStartZoom * ratio });\n }\n return;\n }\n\n if (dragStart) {\n const dx = e.clientX - dragStart.x;\n const dy = e.clientY - dragStart.y;\n const state = callbacks.getState();\n applyClampedState({\n ...state,\n offset: { x: dragStart.offset.x + dx, y: dragStart.offset.y + dy },\n });\n }\n }\n\n function onPointerUp(e: PointerEvent): void {\n activePointers.delete(e.pointerId);\n if (activePointers.size < 2) pinchStartDistance = 0;\n if (activePointers.size === 0) {\n dragStart = null;\n stageEl.classList.remove(\"kiri-dragging\");\n }\n }\n\n function onWheel(e: WheelEvent): void {\n if (!options.mouseWheelZoom) return;\n if (options.mouseWheelZoom === \"ctrl\" && !e.ctrlKey) return;\n e.preventDefault();\n const state = callbacks.getState();\n const delta = -e.deltaY * 0.0015;\n applyClampedState({ ...state, zoom: state.zoom * (1 + delta) });\n }\n\n // Arrow keys pan, +/- zoom, 0 resets — lets a keyboard-only user operate\n // the cropper once the stage is focused (it's a tabbable, labeled element;\n // see stage.ts). Pan direction follows the \"arrow moves the view\" scroll\n // convention (pressing Right reveals more of the image's right side), the\n // opposite of drag's \"content follows the pointer\" — so the sign is\n // inverted relative to a drag delta of the same direction.\n function onKeyDown(e: KeyboardEvent): void {\n const state = callbacks.getState();\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x + KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowRight\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, x: state.offset.x - KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowUp\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y + KEYBOARD_PAN_STEP } });\n break;\n case \"ArrowDown\":\n e.preventDefault();\n applyClampedState({ ...state, offset: { ...state.offset, y: state.offset.y - KEYBOARD_PAN_STEP } });\n break;\n case \"+\":\n case \"=\":\n e.preventDefault();\n applyClampedState({ ...state, zoom: state.zoom + KEYBOARD_ZOOM_STEP });\n break;\n case \"-\":\n case \"_\":\n e.preventDefault();\n applyClampedState({ ...state, zoom: state.zoom - KEYBOARD_ZOOM_STEP });\n break;\n case \"0\":\n e.preventDefault();\n callbacks.reset();\n break;\n default:\n break;\n }\n }\n\n stageEl.addEventListener(\"pointerdown\", onPointerDown);\n stageEl.addEventListener(\"pointermove\", onPointerMove);\n stageEl.addEventListener(\"pointerup\", onPointerUp);\n stageEl.addEventListener(\"pointercancel\", onPointerUp);\n stageEl.addEventListener(\"wheel\", onWheel, { passive: false });\n stageEl.addEventListener(\"keydown\", onKeyDown);\n\n return {\n destroy(): void {\n stageEl.removeEventListener(\"pointerdown\", onPointerDown);\n stageEl.removeEventListener(\"pointermove\", onPointerMove);\n stageEl.removeEventListener(\"pointerup\", onPointerUp);\n stageEl.removeEventListener(\"pointercancel\", onPointerUp);\n stageEl.removeEventListener(\"wheel\", onWheel);\n stageEl.removeEventListener(\"keydown\", onKeyDown);\n },\n };\n}\n","/**\n * Reads the EXIF orientation tag (1-8) from a JPEG ArrayBuffer, defaulting to 1\n * (no adjustment) for images with no EXIF data or non-JPEG input.\n */\nexport function readExifOrientation(buffer: ArrayBuffer): number {\n const view = new DataView(buffer);\n if (view.byteLength < 4 || view.getUint16(0, false) !== 0xffd8) return 1;\n\n let offset = 2;\n while (offset + 4 <= view.byteLength) {\n const marker = view.getUint16(offset, false);\n const size = view.getUint16(offset + 2, false);\n\n if (marker === 0xffe1) {\n const exifOffset = offset + 4;\n if (view.getUint32(exifOffset, false) !== 0x45786966) return 1; // \"Exif\"\n return readOrientationFromTiff(view, exifOffset + 6);\n }\n\n if ((marker & 0xff00) !== 0xff00) break;\n offset += 2 + size;\n }\n return 1;\n}\n\nfunction readOrientationFromTiff(view: DataView, tiffStart: number): number {\n const littleEndian = view.getUint16(tiffStart, false) === 0x4949;\n const firstIfdOffset = view.getUint32(tiffStart + 4, littleEndian);\n const ifdStart = tiffStart + firstIfdOffset;\n if (ifdStart + 2 > view.byteLength) return 1;\n\n const entryCount = view.getUint16(ifdStart, littleEndian);\n for (let i = 0; i < entryCount; i++) {\n const entryOffset = ifdStart + 2 + i * 12;\n if (entryOffset + 12 > view.byteLength) break;\n const tag = view.getUint16(entryOffset, littleEndian);\n if (tag === 0x0112) {\n const value = view.getUint16(entryOffset + 8, littleEndian);\n return value >= 1 && value <= 8 ? value : 1;\n }\n }\n return 1;\n}\n\nexport interface OrientationTransform {\n rotation: number;\n flipHorizontal: boolean;\n}\n\n/**\n * Maps an EXIF orientation value to the rotation + horizontal-flip pair that\n * normalizes it. A vertical flip is never needed on its own: orientation 4\n * (mirror vertical) is expressed as rotate(180) + flip horizontal, which is\n * mathematically equivalent and matches Kiri's transform order (flip is\n * applied before rotation, so this composes correctly).\n */\nexport function orientationToTransform(orientation: number): OrientationTransform {\n switch (orientation) {\n case 2:\n return { rotation: 0, flipHorizontal: true };\n case 3:\n return { rotation: 180, flipHorizontal: false };\n case 4:\n return { rotation: 180, flipHorizontal: true };\n case 5:\n return { rotation: 90, flipHorizontal: true };\n case 6:\n return { rotation: 90, flipHorizontal: false };\n case 7:\n return { rotation: 270, flipHorizontal: true };\n case 8:\n return { rotation: 270, flipHorizontal: false };\n default:\n return { rotation: 0, flipHorizontal: false };\n }\n}\n","/**\n * Resolves an optional string-enum option: undefined silently uses the\n * default, a valid value passes through, and an invalid value warns and\n * falls back to the default rather than silently misbehaving (e.g. a\n * typo'd option matching no CSS rule, or an unsupported canvas export type).\n */\nexport function resolveEnumOption<T extends string>(\n value: T | undefined,\n valid: readonly T[],\n fallback: T,\n optionName: string\n): T {\n if (value === undefined) return fallback;\n if ((valid as readonly string[]).includes(value)) return value;\n console.warn(\n `Kiri: invalid ${optionName} \"${value}\" — defaulting to \"${fallback}\". ` +\n `Valid values: ${valid.join(\", \")}.`\n );\n return fallback;\n}\n","import type {\n CropRegion,\n ExportFormat,\n ExportOptions,\n ExportResult,\n ExportType,\n FrameShape,\n KiriState,\n Offset,\n} from \"./types\";\nimport {\n computeCoverScale,\n effectiveRenderedSize,\n normalizeRotation,\n type Size,\n} from \"./gestures\";\nimport { buildFilterString } from \"./filters\";\nimport { resolveEnumOption } from \"./validate\";\n\nfunction clampNum(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nconst VALID_EXPORT_TYPES: ExportType[] = [\"base64\", \"blob\", \"canvas\"];\nconst VALID_EXPORT_FORMATS: ExportFormat[] = [\"image/jpeg\", \"image/png\", \"image/webp\"];\n\n/**\n * Top-left corner of the frame, in the local pixel space of the rendered\n * (rotated + scaled) image. Pure so the crop-region math is testable without\n * a real canvas: the frame is centered on the stage, the image's own center\n * sits at `offset` from that same point, so the frame's top-left in the\n * rendered image's local space is the rendered center, shifted back by the\n * offset.\n */\nexport function computeFrameSourceRect(\n rendered: Size,\n offset: Offset,\n frame: Size\n): { left: number; top: number } {\n return {\n left: rendered.width / 2 - offset.x - frame.width / 2,\n top: rendered.height / 2 - offset.y - frame.height / 2,\n };\n}\n\n/**\n * Maps the current crop selection back into the *original, unrotated,\n * unflipped* source image's own pixel coordinates. Corner-based: takes the\n * frame rect's four corners in rendered (rotated + scaled) space, undoes the\n * scale, then inverse-rotates each corner (rotation is always a multiple of\n * 90°, so this stays an axis-aligned rectangle — no interpolation needed)\n * back into the natural image's coordinate space, then takes the bounding\n * box. Flip doesn't move the rectangle (mirroring is content-only), so it's\n * carried through untouched in the result for the caller to apply.\n */\nexport function computeCropRegion(natural: Size, frame: Size, state: KiriState): CropRegion {\n const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;\n const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);\n const { left: frameLeft, top: frameTop } = computeFrameSourceRect(rendered, state.offset, frame);\n\n const renderedCenter = { x: rendered.width / 2, y: rendered.height / 2 };\n const naturalCenter = { x: natural.width / 2, y: natural.height / 2 };\n const rotation = normalizeRotation(state.rotation);\n\n const corners = [\n { x: frameLeft, y: frameTop },\n { x: frameLeft + frame.width, y: frameTop },\n { x: frameLeft, y: frameTop + frame.height },\n { x: frameLeft + frame.width, y: frameTop + frame.height },\n ].map(({ x, y }) => {\n const cx = (x - renderedCenter.x) / scale;\n const cy = (y - renderedCenter.y) / scale;\n let u: number;\n let v: number;\n if (rotation === 90) {\n u = cy;\n v = -cx;\n } else if (rotation === 180) {\n u = -cx;\n v = -cy;\n } else if (rotation === 270) {\n u = -cy;\n v = cx;\n } else {\n u = cx;\n v = cy;\n }\n return { x: u + naturalCenter.x, y: v + naturalCenter.y };\n });\n\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n // Clamp each edge independently (not just the origin) — a frame that\n // overhangs the image (e.g. offset drags it past an edge) must shrink\n // width/height to match, not just get shifted while keeping its\n // unclamped size.\n const left = clampNum(Math.min(...xs), 0, natural.width);\n const top = clampNum(Math.min(...ys), 0, natural.height);\n const right = clampNum(Math.max(...xs), 0, natural.width);\n const bottom = clampNum(Math.max(...ys), 0, natural.height);\n\n return {\n x: Math.round(left),\n y: Math.round(top),\n width: Math.round(right - left),\n height: Math.round(bottom - top),\n rotation,\n flip: { ...state.flip },\n };\n}\n\nexport function renderCropToCanvas(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n outputWidth: number,\n outputHeight: number,\n frameShape: FrameShape,\n cornerRadius: number\n): HTMLCanvasElement {\n const natural: Size = { width: img.naturalWidth, height: img.naturalHeight };\n const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;\n const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);\n\n const sourceCanvas = document.createElement(\"canvas\");\n sourceCanvas.width = Math.max(1, Math.round(rendered.width));\n sourceCanvas.height = Math.max(1, Math.round(rendered.height));\n const sctx = sourceCanvas.getContext(\"2d\");\n if (!sctx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n sctx.translate(sourceCanvas.width / 2, sourceCanvas.height / 2);\n sctx.rotate((state.rotation * Math.PI) / 180);\n // Flip is folded into scale's sign, applied before rotate (matches\n // stage.ts's transform order) so rotation happens in \"already mirrored\"\n // space, consistent with what the user sees on screen.\n sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));\n // Same CSS filter string as the live preview (see stage.ts's applyFilters),\n // so the browser's own filter implementation guarantees they match exactly.\n sctx.filter = buildFilterString(state.filters);\n sctx.drawImage(img, -natural.width / 2, -natural.height / 2, natural.width, natural.height);\n\n const { left: frameLeft, top: frameTop } = computeFrameSourceRect(\n rendered,\n state.offset,\n frame\n );\n\n const outCanvas = document.createElement(\"canvas\");\n outCanvas.width = outputWidth;\n outCanvas.height = outputHeight;\n const octx = outCanvas.getContext(\"2d\");\n if (!octx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n // A non-rectangle frame is otherwise just a visual overlay (stage.ts's\n // .kiri-frame--circle/--rounded-rectangle) — without this, the export\n // would always be a plain rectangle regardless of frame.shape.\n if (frameShape === \"circle\") {\n // Ellipse inscribed in the output canvas matches what's visible on\n // screen even when a custom output width/height changes its aspect ratio.\n octx.save();\n octx.beginPath();\n octx.ellipse(\n outputWidth / 2,\n outputHeight / 2,\n outputWidth / 2,\n outputHeight / 2,\n 0,\n 0,\n Math.PI * 2\n );\n octx.clip();\n } else if (frameShape === \"rounded-rectangle\") {\n // cornerRadius is a fixed pixel value tied to the on-screen frame size;\n // scale it by how much bigger/smaller the output is than the frame so a\n // custom output width/height still looks proportionally the same.\n const scale = outputWidth / frame.width;\n const radius = clampNum(cornerRadius * scale, 0, Math.min(outputWidth, outputHeight) / 2);\n octx.save();\n octx.beginPath();\n octx.roundRect(0, 0, outputWidth, outputHeight, radius);\n octx.clip();\n }\n\n octx.drawImage(\n sourceCanvas,\n frameLeft,\n frameTop,\n frame.width,\n frame.height,\n 0,\n 0,\n outputWidth,\n outputHeight\n );\n\n if (frameShape === \"circle\" || frameShape === \"rounded-rectangle\") octx.restore();\n\n return outCanvas;\n}\n\nexport async function exportCrop(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n frameShape: FrameShape,\n cornerRadius: number,\n options: ExportOptions\n): Promise<ExportResult> {\n const type = resolveEnumOption(options.type, VALID_EXPORT_TYPES, \"base64\", \"export type\");\n const format = resolveEnumOption(options.format, VALID_EXPORT_FORMATS, \"image/png\", \"export format\");\n const quality = options.quality;\n const width = options.width ?? frame.width;\n const height = options.height ?? frame.height;\n\n if ((frameShape === \"circle\" || frameShape === \"rounded-rectangle\") && format === \"image/jpeg\") {\n console.warn(\n `Kiri: exporting a ${frameShape}-shaped frame as image/jpeg — JPEG has ` +\n \"no alpha channel, so the area outside the shape will render as \" +\n \"solid black instead of transparent. Use image/png or image/webp instead.\"\n );\n }\n\n const canvas = renderCropToCanvas(img, state, frame, width, height, frameShape, cornerRadius);\n\n if (type === \"canvas\") return canvas;\n if (type === \"base64\") return canvas.toDataURL(format, quality);\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Kiri: canvas.toBlob failed\"))),\n format,\n quality\n );\n });\n}\n","import type { UploadOptions } from \"./types\";\n\n/**\n * Default uploader: builds a FormData from the crop blob and POSTs it via\n * fetch. Swappable per-call (`UploadOptions.uploader`) or per-instance\n * (`KiriOptions.uploader`) for a custom protocol (presigned URLs, GraphQL,\n * etc.) while callers keep calling the same `cropper.upload(url, options)`.\n */\nexport async function uploadBlob(\n blob: Blob,\n options: UploadOptions & { url: string }\n): Promise<Response> {\n const fieldName = options.fieldName ?? \"file\";\n const fileName = options.fileName ?? `crop.${extensionFor(options.format ?? \"image/png\")}`;\n\n const formData = new FormData();\n formData.append(fieldName, blob, fileName);\n for (const [key, value] of Object.entries(options.extraFields ?? {})) {\n formData.append(key, value);\n }\n\n return fetch(options.url, {\n ...options.fetchOptions,\n method: \"POST\",\n body: formData,\n });\n}\n\nfunction extensionFor(format: string): string {\n switch (format) {\n case \"image/jpeg\":\n return \"jpg\";\n case \"image/webp\":\n return \"webp\";\n default:\n return \"png\";\n }\n}\n","import type {\n CropRegion,\n ExportOptions,\n ExportResult,\n Filters,\n Flip,\n FrameShape,\n KiriEventCallback,\n KiriEventName,\n KiriOptions,\n KiriState,\n LoadOptions,\n Offset,\n UploadOptions,\n Uploader,\n ZoomerPosition,\n} from \"./types\";\nimport {\n applyFilters,\n applyTransform,\n createStage,\n setFrameSize as setStageFrameSize,\n setStageSize,\n STAGE_AUTO_SIZE_PADDING,\n type StageElements,\n} from \"./stage\";\nimport {\n attachGestures,\n clampOffset,\n clampZoom,\n computeCoverScale,\n effectiveRenderedSize,\n normalizeRotation,\n type Size,\n} from \"./gestures\";\nimport { orientationToTransform, readExifOrientation } from \"./exif\";\nimport { computeCropRegion, exportCrop } from \"./export\";\nimport { DEFAULT_FILTERS, mergeFilters } from \"./filters\";\nimport { uploadBlob } from \"./upload\";\nimport { resolveEnumOption } from \"./validate\";\n\nconst DEFAULT_FRAME_SIZE = 200;\nconst MIN_FRAME_SIZE = 20;\nconst DEFAULT_CORNER_RADIUS = 20;\nconst VALID_FRAME_SHAPES: FrameShape[] = [\"rectangle\", \"circle\", \"rounded-rectangle\"];\nconst VALID_ZOOMER_POSITIONS: ZoomerPosition[] = [\"top\", \"bottom\", \"left\", \"right\"];\n\nfunction resolveMouseWheelZoom(value: KiriOptions[\"mouseWheelZoom\"]): boolean | \"ctrl\" {\n if (value === undefined) return true;\n if (typeof value === \"boolean\" || value === \"ctrl\") return value;\n console.warn(\n `Kiri: invalid mouseWheelZoom \"${String(value)}\" — defaulting to true. ` +\n `Valid values: true, false, \"ctrl\".`\n );\n return true;\n}\n\ninterface ResolvedOptions {\n frame: { shape: FrameShape; width: number; height: number; cornerRadius: number };\n minZoom: number;\n maxZoom: number;\n rotatable: boolean;\n flippable: boolean;\n resizableFrame: boolean;\n lockAspectRatio: boolean;\n mouseWheelZoom: boolean | \"ctrl\";\n useExifOrientation: boolean;\n uploader: Uploader | undefined;\n autoSizeStage: boolean;\n showZoomer: boolean;\n zoomerPosition: ZoomerPosition;\n}\n\n/**\n * An interactive image cropper attached to a plain DOM element. Drag to pan,\n * zoom via wheel/pinch/an optional built-in slider, rotate in 90° steps,\n * flip, apply filters, then export or upload the crop.\n */\nexport class Kiri {\n private readonly container: HTMLElement;\n private readonly opts: ResolvedOptions;\n private readonly stage: StageElements;\n private readonly gestureHandle: { destroy: () => void };\n private resizeHandle: { destroy: () => void } | null = null;\n private zoomerHandle: { destroy: () => void } | null = null;\n private naturalSize: Size = { width: 0, height: 0 };\n private state: KiriState = {\n zoom: 1,\n offset: { x: 0, y: 0 },\n rotation: 0,\n flip: { horizontal: false, vertical: false },\n filters: DEFAULT_FILTERS,\n };\n /** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */\n private initialState: KiriState | null = null;\n private listeners: Record<KiriEventName, KiriEventCallback[]> = { change: [] };\n\n /**\n * @param container An element already present in the DOM. Passing\n * `null`/`undefined`, or an element that isn't in the DOM yet, throws.\n * @param options See the {@link KiriOptions} fields for defaults.\n */\n constructor(container: HTMLElement, options: KiriOptions = {}) {\n if (!container || typeof container.appendChild !== \"function\") {\n throw new Error(\n \"Kiri: container element is null/undefined or not a DOM element. \" +\n \"This usually means the element wasn't in the DOM yet when \" +\n \"`new Kiri(...)` ran — e.g. document.getElementById() was called \" +\n \"before the element existed. Place the <script> after the \" +\n \"element, or construct inside a DOMContentLoaded listener.\"\n );\n }\n this.container = container;\n this.opts = {\n frame: {\n shape: resolveEnumOption(\n options.frame?.shape,\n VALID_FRAME_SHAPES,\n \"rectangle\",\n \"frame.shape\"\n ),\n width: options.frame?.width ?? DEFAULT_FRAME_SIZE,\n height: options.frame?.height ?? DEFAULT_FRAME_SIZE,\n cornerRadius: options.frame?.cornerRadius ?? DEFAULT_CORNER_RADIUS,\n },\n minZoom: options.minZoom ?? 1,\n maxZoom: options.maxZoom ?? 4,\n rotatable: options.rotatable ?? true,\n flippable: options.flippable ?? true,\n resizableFrame: options.resizableFrame ?? false,\n lockAspectRatio: options.lockAspectRatio ?? false,\n mouseWheelZoom: resolveMouseWheelZoom(options.mouseWheelZoom),\n useExifOrientation: options.useExifOrientation ?? true,\n uploader: options.uploader,\n autoSizeStage: options.autoSizeStage ?? true,\n showZoomer: options.showZoomer ?? false,\n zoomerPosition: resolveEnumOption(\n options.zoomerPosition,\n VALID_ZOOMER_POSITIONS,\n \"bottom\",\n \"zoomerPosition\"\n ),\n };\n this.state.filters = mergeFilters(DEFAULT_FILTERS, options.filters ?? {});\n\n this.stage = createStage(\n this.container,\n this.opts.frame.shape,\n this.opts.frame.width,\n this.opts.frame.height,\n this.opts.frame.cornerRadius,\n {\n show: this.opts.showZoomer,\n position: this.opts.zoomerPosition,\n min: this.opts.minZoom,\n max: this.opts.maxZoom,\n value: this.state.zoom,\n }\n );\n if (this.opts.autoSizeStage) this.syncStageSize();\n applyFilters(this.stage.imgEl, this.state.filters);\n if (this.stage.zoomerEl) this.enableZoomer(this.stage.zoomerEl);\n\n this.gestureHandle = attachGestures(\n this.stage.stageEl,\n {\n getNaturalSize: () => this.naturalSize,\n getFrameSize: () => this.getFrameSize(),\n getState: () => this.state,\n getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),\n setState: (next) => this.commitState(next),\n reset: () => this.reset(),\n },\n { mouseWheelZoom: this.opts.mouseWheelZoom }\n );\n\n if (this.opts.resizableFrame) this.enableFrameResize();\n }\n\n /**\n * Loads an image, replacing whatever was loaded before. EXIF orientation\n * (rotation + horizontal flip) is corrected automatically unless\n * `useExifOrientation: false` was passed to the constructor — only for\n * `File`/`Blob` sources, since a plain URL string can't be read for EXIF\n * data without an extra fetch.\n * @param source A `File` (e.g. from a file input), a `Blob`, or a URL string.\n * @param loadOptions Initial `zoom`/`offset`/`rotation`/`flip`.\n */\n async load(source: File | Blob | string, loadOptions: LoadOptions = {}): Promise<void> {\n let rotation = normalizeRotation(loadOptions.rotation ?? 0);\n let flipHorizontal = loadOptions.flip?.horizontal ?? false;\n const flipVertical = loadOptions.flip?.vertical ?? false;\n let objectUrl: string | null = null;\n let url: string;\n\n if (typeof source === \"string\") {\n url = source;\n } else {\n if (this.opts.useExifOrientation) {\n const buffer = await source.arrayBuffer();\n const orientation = readExifOrientation(buffer);\n const exifTransform = orientationToTransform(orientation);\n rotation = normalizeRotation(rotation + exifTransform.rotation);\n // XOR: two horizontal flips (EXIF + a requested one) cancel out.\n flipHorizontal = flipHorizontal !== exifTransform.flipHorizontal;\n }\n objectUrl = URL.createObjectURL(source);\n url = objectUrl;\n }\n\n try {\n await new Promise<void>((resolve, reject) => {\n this.stage.imgEl.onload = () => resolve();\n this.stage.imgEl.onerror = () => reject(new Error(\"Kiri: failed to load image\"));\n this.stage.imgEl.src = url;\n });\n } finally {\n // Must run on the error path too, not just success — otherwise a\n // rejected load() (corrupt file, unsupported format) leaks the blob\n // URL for the rest of the page's lifetime.\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n }\n\n this.naturalSize = {\n width: this.stage.imgEl.naturalWidth,\n height: this.stage.imgEl.naturalHeight,\n };\n\n const zoom = clampZoom(\n loadOptions.zoom ?? this.opts.minZoom,\n this.opts.minZoom,\n this.opts.maxZoom\n );\n const rendered = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, zoom);\n const offset = clampOffset(loadOptions.offset ?? { x: 0, y: 0 }, rendered, this.getFrameSize());\n\n this.commitState({\n zoom,\n offset,\n rotation,\n flip: { horizontal: flipHorizontal, vertical: flipVertical },\n filters: this.state.filters,\n });\n this.initialState = this.getState();\n }\n\n /** A snapshot of the current state — mutating the returned object has no effect. */\n getState(): KiriState {\n return {\n zoom: this.state.zoom,\n offset: { ...this.state.offset },\n rotation: this.state.rotation,\n flip: { ...this.state.flip },\n filters: { ...this.state.filters },\n };\n }\n\n /** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */\n setZoom(zoom: number): void {\n const clamped = clampZoom(zoom, this.opts.minZoom, this.opts.maxZoom);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n clamped\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, zoom: clamped, offset });\n }\n\n /**\n * Sets the pan offset to an absolute value (image-center offset from the\n * frame center, in stage pixels), clamped so the frame stays fully covered\n * by the rendered image.\n */\n setOffset(offset: Offset): void {\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n this.state.zoom\n );\n const clamped = clampOffset(offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, offset: clamped });\n }\n\n /**\n * Reverts zoom/offset/rotation/flip/filters to what they were right after\n * `load()` resolved (including any `loadOptions` passed to it). No-op if\n * nothing has been loaded yet.\n */\n reset(): void {\n if (!this.initialState) return;\n this.commitState({\n zoom: this.initialState.zoom,\n offset: { ...this.initialState.offset },\n rotation: this.initialState.rotation,\n flip: { ...this.initialState.flip },\n filters: { ...this.initialState.filters },\n });\n }\n\n /**\n * Rotates relative to the current rotation, snapped to the nearest 90°.\n * No-op if `rotatable: false` was passed to the constructor.\n */\n rotate(deltaDeg: number): void {\n if (!this.opts.rotatable) return;\n const snapped = Math.round(deltaDeg / 90) * 90;\n const rotation = normalizeRotation(this.state.rotation + snapped);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, rotation, offset });\n }\n\n /** Toggles horizontal flip, independent of rotation. No-op if `flippable: false`. */\n flipHorizontal(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, horizontal: !this.state.flip.horizontal };\n this.commitState({ ...this.state, flip });\n }\n\n /** Toggles vertical flip, independent of rotation. No-op if `flippable: false`. */\n flipVertical(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, vertical: !this.state.flip.vertical };\n this.commitState({ ...this.state, flip });\n }\n\n /**\n * Resizes the frame. Also resizes the stage to match, if\n * `autoSizeStage: true` (the default). Each axis is clamped to a 20px\n * minimum.\n */\n setFrameSize(width: number, height: number): void {\n this.opts.frame.width = Math.max(MIN_FRAME_SIZE, width);\n this.opts.frame.height = Math.max(MIN_FRAME_SIZE, height);\n setStageFrameSize(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height);\n if (this.opts.autoSizeStage) this.syncStageSize();\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, offset });\n }\n\n /**\n * Merges a partial update into the current filters (omitted fields are\n * left as they are). Numeric values are clamped to `>= 0`.\n */\n setFilters(filters: Partial<Filters>): void {\n this.commitState({ ...this.state, filters: mergeFilters(this.state.filters, filters) });\n }\n\n /**\n * Renders the current crop. A circle or rounded-rectangle frame is a real\n * clip in the output (transparent corners on PNG/WebP); either shape\n * exported as JPEG warns and renders solid black corners instead, since\n * JPEG has no alpha channel.\n * @returns A data URL string (`type: \"base64\"`, the default), a `Blob`, or an `HTMLCanvasElement`.\n */\n async export(options: ExportOptions = {}): Promise<ExportResult> {\n return exportCrop(\n this.stage.imgEl,\n this.state,\n this.getFrameSize(),\n this.opts.frame.shape,\n this.opts.frame.cornerRadius,\n options\n );\n }\n\n /**\n * The current crop selection as a rectangle in the original, unrotated,\n * unflipped source image's own pixel coordinates — for sending to a server\n * that will crop the full-resolution original itself instead of uploading\n * a client-re-encoded image. See {@link CropRegion}.\n */\n getCropRegion(): CropRegion {\n return computeCropRegion(this.naturalSize, this.getFrameSize(), this.state);\n }\n\n /**\n * Exports the current crop as a blob, then uploads it — a default\n * FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`\n * wins over the constructor's, which wins over the built-in default).\n */\n async upload(url: string, options: UploadOptions = {}): Promise<unknown> {\n const blob = (await this.export({ ...options, type: \"blob\" })) as Blob;\n const uploader = options.uploader ?? this.opts.uploader ?? uploadBlob;\n return uploader(blob, { ...options, url });\n }\n\n /** Subscribes to `\"change\"` — fires on every state update (drag/zoom/rotate/flip/filters), and once after `load()` resolves. */\n on(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event].push(callback);\n }\n\n /** Unsubscribes a callback previously passed to {@link on}. */\n off(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);\n }\n\n /**\n * Tears the instance down: removes all pointer/wheel event listeners\n * (drag/zoom gestures), the resize-handle listener (if `resizableFrame`),\n * and the zoom-slider listener (if `showZoomer`); clears the container's\n * `innerHTML`, leaving an empty container element; and clears all\n * `\"change\"` listeners. Call this when you're done with an instance (e.g.\n * unmounting) to avoid leaking listeners.\n */\n destroy(): void {\n this.gestureHandle.destroy();\n this.resizeHandle?.destroy();\n this.zoomerHandle?.destroy();\n this.container.innerHTML = \"\";\n this.listeners.change = [];\n }\n\n private getFrameSize(): Size {\n return { width: this.opts.frame.width, height: this.opts.frame.height };\n }\n\n private syncStageSize(): void {\n setStageSize(\n this.stage.stageEl,\n this.opts.frame.width + STAGE_AUTO_SIZE_PADDING * 2,\n this.opts.frame.height + STAGE_AUTO_SIZE_PADDING * 2\n );\n }\n\n private commitState(next: KiriState): void {\n this.state = next;\n const scale =\n computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;\n applyTransform(this.stage.imageLayerEl, next, scale);\n applyFilters(this.stage.imgEl, next.filters);\n // Keeps the slider in sync regardless of what triggered the zoom change\n // (wheel, pinch, drag-clamping, or setZoom() itself) — setting .value\n // programmatically doesn't re-fire \"input\", so no feedback loop.\n if (this.stage.zoomerEl) this.stage.zoomerEl.value = String(next.zoom);\n for (const cb of this.listeners.change) cb(this.getState());\n }\n\n private enableZoomer(zoomerEl: HTMLInputElement): void {\n const onInput = (): void => this.setZoom(Number(zoomerEl.value));\n zoomerEl.addEventListener(\"input\", onInput);\n this.zoomerHandle = {\n destroy(): void {\n zoomerEl.removeEventListener(\"input\", onInput);\n },\n };\n }\n\n private enableFrameResize(): void {\n // Corners named by which edges they sit on; signX/signY say which way\n // dragging that corner should grow the frame on each axis. The frame is\n // always centered in the stage (kiri.css), so moving any one corner by\n // `d` pixels grows/shrinks that axis by `2*d` to keep the opposite edge\n // stationary and make the drag feel corner-anchored.\n const corners: { name: string; edge: { left?: string; right?: string; top?: string; bottom?: string }; signX: number; signY: number }[] = [\n { name: \"top-left\", edge: { left: \"-5px\", top: \"-5px\" }, signX: -1, signY: -1 },\n { name: \"top-right\", edge: { right: \"-5px\", top: \"-5px\" }, signX: 1, signY: -1 },\n { name: \"bottom-left\", edge: { left: \"-5px\", bottom: \"-5px\" }, signX: -1, signY: 1 },\n { name: \"bottom-right\", edge: { right: \"-5px\", bottom: \"-5px\" }, signX: 1, signY: 1 },\n ];\n\n const cleanups: (() => void)[] = [];\n\n for (const corner of corners) {\n const handle = document.createElement(\"div\");\n handle.className = `kiri-frame-handle kiri-frame-handle--${corner.name}`;\n Object.assign(handle.style, corner.edge);\n this.stage.frameEl.appendChild(handle);\n\n let start: { x: number; y: number; width: number; height: number } | null = null;\n\n const onDown = (e: PointerEvent): void => {\n e.stopPropagation();\n handle.setPointerCapture(e.pointerId);\n start = {\n x: e.clientX,\n y: e.clientY,\n width: this.opts.frame.width,\n height: this.opts.frame.height,\n };\n };\n const onMove = (e: PointerEvent): void => {\n if (!start) return;\n const dx = (e.clientX - start.x) * 2 * corner.signX;\n const dy = (e.clientY - start.y) * 2 * corner.signY;\n const rawWidth = start.width + dx;\n const rawHeight = start.height + dy;\n\n if (this.opts.lockAspectRatio) {\n const aspect = start.width / start.height;\n // Whichever axis moved more (in aspect-normalized units) drives\n // the resize; the other axis follows to preserve the ratio.\n if (Math.abs(rawWidth - start.width) >= Math.abs(rawHeight - start.height) * aspect) {\n this.setFrameSize(rawWidth, rawWidth / aspect);\n } else {\n this.setFrameSize(rawHeight * aspect, rawHeight);\n }\n } else {\n this.setFrameSize(rawWidth, rawHeight);\n }\n };\n const onUp = (): void => {\n start = null;\n };\n\n handle.addEventListener(\"pointerdown\", onDown);\n handle.addEventListener(\"pointermove\", onMove);\n handle.addEventListener(\"pointerup\", onUp);\n handle.addEventListener(\"pointercancel\", onUp);\n\n cleanups.push(() => {\n handle.removeEventListener(\"pointerdown\", onDown);\n handle.removeEventListener(\"pointermove\", onMove);\n handle.removeEventListener(\"pointerup\", onUp);\n handle.removeEventListener(\"pointercancel\", onUp);\n handle.remove();\n });\n }\n\n this.resizeHandle = {\n destroy(): void {\n for (const cleanup of cleanups) cleanup();\n },\n };\n }\n}\n"],"names":["scale","setStageFrameSize"],"mappings":"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"}