@michaelyagi/kiri 0.1.0-alpha.10
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 +100 -0
- package/dist/exif.d.ts +31 -0
- package/dist/export.d.ts +27 -0
- package/dist/filters.d.ts +45 -0
- package/dist/gestures.d.ts +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/kiri.css +82 -0
- package/dist/kiri.d.ts +173 -0
- package/dist/kiri.js +1115 -0
- package/dist/kiri.js.map +1 -0
- package/dist/kiri.min.css +1 -0
- package/dist/kiri.min.js +8 -0
- package/dist/kiri.min.js.map +1 -0
- package/dist/kiri.mjs +867 -0
- package/dist/kiri.mjs.map +1 -0
- package/dist/stage.d.ts +38 -0
- package/dist/types.d.ts +179 -0
- package/dist/upload.d.ts +10 -0
- package/dist/validate.d.ts +7 -0
- package/package.json +57 -0
package/dist/kiri.js.map
ADDED
|
@@ -0,0 +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 sharpness: 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 sharpness: Math.max(0, partial.sharpness ?? current.sharpness),\n grayscale: partial.grayscale ?? current.grayscale,\n sepia: partial.sepia ?? current.sepia,\n };\n}\n\n/**\n * The 3x3 unsharp-mask kernel for `sharpness`, as a `feConvolveMatrix`\n * `kernelMatrix` attribute value (row-major, space-separated):\n *\n * ```\n * 0 -k 0\n * -k 1+4k -k\n * 0 -k 0\n * ```\n *\n * The matrix always sums to `1`, so the convolution needs no separate\n * `divisor`. `sharpness <= 1` collapses to the identity kernel (center `1`,\n * neighbors `0`) — no blur/sharpen either way, matching the `1` = unchanged\n * convention used by the other numeric filters. `4.5` is chosen so a\n * `sharpness` of `2`–`3` (a reasonable slider range) lands in a visually\n * subtle-to-strong sharpening range, not an immediately harsh one.\n */\nexport function sharpenKernelMatrix(sharpness: number): string {\n const weight = sharpness > 1 ? (sharpness - 1) / 4.5 : 0;\n const center = 1 + 4 * weight;\n const neighbor = -weight;\n return `0 ${neighbor} 0 ${neighbor} ${center} ${neighbor} 0 ${neighbor} 0`;\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/sharpness pixel\n * math. Brightness/contrast/saturation/grayscale/sepia are native CSS filter\n * functions; CSS has no `sharpen()`, so `sharpness` instead references an\n * SVG `feConvolveMatrix` filter (by `sharpenFilterId`) that `stage.ts` keeps\n * in sync with the current `sharpness` value — `ctx.filter` and\n * `img.style.filter` both support `url(#id)` references to it identically.\n *\n * The `url(#id)` reference goes **first**, before the native CSS filter\n * functions — not last, despite sharpening-as-a-final-step being the more\n * common photo-editing order. Verified directly in a real browser (Chromium)\n * that a `url(#id)` SVG filter reference placed *after* one or more native\n * CSS filter functions in the same chain silently produces a fully blank\n * (transparent) result, while the identical chain with `url(#id)` first\n * renders correctly. Sharpening a source that hasn't had brightness/contrast/\n * saturation applied yet is a legitimate (if less common) ordering rather\n * than a compromise — but the ordering here is load-bearing either way, not\n * arbitrary; don't reorder this without re-verifying in a real browser.\n */\nexport function buildFilterString(filters: Filters, sharpenFilterId: string): string {\n const parts: string[] = [];\n if (filters.sharpness > 1) parts.push(`url(#${sharpenFilterId})`);\n parts.push(\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, sharpenKernelMatrix } from \"./filters\";\n\nexport interface StageElements {\n stageEl: HTMLDivElement;\n frameEl: HTMLDivElement;\n imageLayerEl: HTMLDivElement;\n imgEl: HTMLImageElement;\n zoomerEl: HTMLInputElement | null;\n /** id of the SVG `feConvolveMatrix` filter `buildFilterString()` references for `sharpness`. */\n sharpenFilterId: string;\n sharpenKernelEl: SVGFEConvolveMatrixElement;\n}\n\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\nlet sharpenFilterCounter = 0;\n\n/**\n * CSS has no `sharpen()` filter, so `sharpness` works through an SVG\n * `feConvolveMatrix` referenced by `url(#id)` from the same filter string as\n * brightness/contrast/saturation (see `filters.ts`'s `buildFilterString()`)\n * — both `img.style.filter` and a canvas 2D context's `ctx.filter` support\n * `url(#id)` references identically, so this one definition drives both live\n * preview and export. A unique id per instance (rather than one shared id)\n * avoids collisions between multiple `Kiri` instances on the same page.\n * `color-interpolation-filters: sRGB` matters — SVG filter primitives\n * default to linearRGB, which would visibly shift brightness/contrast\n * relative to the sRGB-space CSS filters earlier in the same filter string.\n */\nfunction createSharpenFilter(): { svg: SVGSVGElement; filterId: string; kernelEl: SVGFEConvolveMatrixElement } {\n sharpenFilterCounter += 1;\n const filterId = `kiri-sharpen-${sharpenFilterCounter}`;\n\n const svg = document.createElementNS(SVG_NS, \"svg\");\n svg.setAttribute(\"width\", \"0\");\n svg.setAttribute(\"height\", \"0\");\n svg.style.position = \"absolute\";\n\n const filterEl = document.createElementNS(SVG_NS, \"filter\");\n filterEl.setAttribute(\"id\", filterId);\n filterEl.setAttribute(\"color-interpolation-filters\", \"sRGB\");\n\n const kernelEl = document.createElementNS(SVG_NS, \"feConvolveMatrix\");\n kernelEl.setAttribute(\"order\", \"3\");\n kernelEl.setAttribute(\"divisor\", \"1\");\n kernelEl.setAttribute(\"edgeMode\", \"duplicate\");\n // Without this, feConvolveMatrix convolves the alpha channel too — the\n // kernel's negative neighbor weights can drive alpha toward 0 right at a\n // sharp edge, turning an opaque photo semi-transparent exactly where\n // sharpening is strongest. Sharpening should only ever touch color.\n kernelEl.setAttribute(\"preserveAlpha\", \"true\");\n kernelEl.setAttribute(\"kernelMatrix\", sharpenKernelMatrix(1));\n\n filterEl.appendChild(kernelEl);\n svg.appendChild(filterEl);\n\n return { svg, filterId, kernelEl };\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 movableFrame: boolean,\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 movableFrame\n ? \"Image cropper. Drag or use arrow keys to move the crop frame over the fixed image. 0 resets.\"\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 const { svg: sharpenSvg, filterId: sharpenFilterId, kernelEl: sharpenKernelEl } =\n createSharpenFilter();\n\n stageEl.appendChild(imageLayerEl);\n stageEl.appendChild(frameEl);\n stageEl.appendChild(sharpenSvg);\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 // Zoom has no meaning in movableFrame mode (setZoom() is a no-op there,\n // and never reaches the commitState() call that resyncs this input's\n // value) — leaving it enabled would let it drag freely and just... stay\n // wherever it was dropped, with zero effect and no visual snap-back.\n if (movableFrame) zoomerEl.disabled = true;\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, sharpenFilterId, sharpenKernelEl };\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 // scale (flip) then rotate — flip is OUTERMOST, applied to the\n // already-rotated result, not the image's own pre-rotation axes. That\n // means \"flip horizontal\" always mirrors left-right as currently shown on\n // screen, and \"flip vertical\" always mirrors top-to-bottom as currently\n // shown, regardless of the current rotation — not a mirror of the\n // original image's intrinsic axes (which would make the visible effect of\n // \"horizontal\" flip depend on rotation, e.g. become a vertical-looking\n // flip once rotated 90°). Both apply around the layer's own center\n // (default transform-origin) either way.\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 `scale(${scaleX}, ${scaleY}) ` +\n `rotate(${state.rotation}deg)`;\n}\n\nexport function applyFilters(\n imgEl: HTMLImageElement,\n filters: Filters,\n sharpenKernelEl: SVGFEConvolveMatrixElement,\n sharpenFilterId: string\n): void {\n sharpenKernelEl.setAttribute(\"kernelMatrix\", sharpenKernelMatrix(filters.sharpness));\n imgEl.style.filter = buildFilterString(filters, sharpenFilterId);\n}\n\n/**\n * Positions the frame itself — only meaningful in `movableFrame` mode; the\n * default mode never calls this, leaving the frame at kiri.css's static\n * `.kiri-frame` rule (centered, no inline transform). This inline transform\n * overrides that rule the same way `applyTransform()` drives the image\n * layer's position, just for the frame instead of the image.\n */\nexport function applyFramePosition(frameEl: HTMLDivElement, framePosition: { x: number; y: number }): void {\n frameEl.style.transform =\n `translate(-50%, -50%) ` + `translate(${framePosition.x}px, ${framePosition.y}px)`;\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 * `movableFrame` mode only: the image's fixed on-screen size — frozen at\n * `load()` time (and recomputed on `rotate()`, since rotation swaps which\n * natural dimension maps to width/height), *not* derived from the current\n * `getFrameSize()`. Deliberately not just\n * `effectiveRenderedSize(natural, getFrameSize(), rotation, 1)` computed\n * on demand — the frame can be resized after load in this mode, and the\n * image must stay the same size regardless, not jump every time the frame\n * does.\n */\n getFixedImageSize: () => Size;\n}\n\nconst KEYBOARD_PAN_STEP = 15;\nconst KEYBOARD_ZOOM_STEP = 0.1;\n\nexport interface GestureOptions {\n mouseWheelZoom?: boolean | \"ctrl\";\n /**\n * When true, drag/wheel/pinch/arrow-keys move and (with `resizableFrame`)\n * resize the *frame* instead of panning/zooming the image — see\n * `KiriOptions.movableFrame`. The image is fixed at `zoom: 1`,\n * `offset: { x: 0, y: 0 }` in this mode, so none of the zoom/offset\n * gesture logic below runs at all.\n */\n movableFrame?: boolean;\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 framePosition: next.framePosition,\n });\n }\n\n // movableFrame mode: the image is fixed at zoom 1 / offset {0,0} (its\n // \"cover the frame\" size at load time, never changed), and the *frame*\n // moves within that fixed image's bounds instead. Clamping a smaller box\n // (the frame) within a bigger one (the fixed-size image) is the exact\n // same shape of problem `clampOffset` already solves for the opposite\n // case (image within frame) — reused as-is, no sign flip needed, since\n // its clamp range is symmetric around 0 either way.\n function applyFramePosition(candidate: Offset): void {\n const state = callbacks.getState();\n const framePosition = clampOffset(candidate, callbacks.getFixedImageSize(), callbacks.getFrameSize());\n callbacks.setState({ ...state, framePosition });\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 = {\n x: e.clientX,\n y: e.clientY,\n offset: options.movableFrame ? state.framePosition : state.offset,\n };\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 // No pinch-zoom in movableFrame mode — the image never zooms.\n if (options.movableFrame) return;\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 if (options.movableFrame) {\n // Direct manipulation: the frame follows the pointer, same as the\n // image does in the default mode (drag right -> moves right) —\n // unlike arrow keys below, which follow the opposite \"scroll\" idea.\n applyFramePosition({ x: dragStart.offset.x + dx, y: dragStart.offset.y + dy });\n return;\n }\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.movableFrame) return; // the image never zooms in this mode\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 //\n // movableFrame mode moves the *frame* instead, and deliberately uses the\n // *other* convention (arrow direction = direct movement direction, not\n // inverted) — the frame is the thing being directly manipulated here\n // (like nudging a selection box), not a viewport revealing more content,\n // so \"Right\" moving the frame right is the intuitive one. +/- zoom keys\n // are no-ops, matching wheel/pinch: there's no zoom in this mode.\n function onKeyDown(e: KeyboardEvent): void {\n const state = callbacks.getState();\n if (options.movableFrame) {\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, x: state.framePosition.x - KEYBOARD_PAN_STEP });\n return;\n case \"ArrowRight\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, x: state.framePosition.x + KEYBOARD_PAN_STEP });\n return;\n case \"ArrowUp\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, y: state.framePosition.y - KEYBOARD_PAN_STEP });\n return;\n case \"ArrowDown\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, y: state.framePosition.y + KEYBOARD_PAN_STEP });\n return;\n case \"0\":\n e.preventDefault();\n callbacks.reset();\n return;\n default:\n return;\n }\n }\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.\n *\n * Rotation and flip don't commute, so which rotation value is paired with\n * `flipHorizontal: true` depends on Kiri's actual render order: rotation is\n * applied first, then flip mirrors the already-rotated result (see\n * stage.ts's `applyTransform()`). That's why orientations 5 and 7 use `270`/\n * `90` here rather than the more \"obvious\" `90`/`270` — reflections invert\n * the effective rotation direction (`flip · rotate(θ) = rotate(-θ) · flip`),\n * so producing the correct final image under this render order needs the\n * negated angle. Orientations 2/3/4/6/8 are unaffected: 2 and 4 have no\n * rotation-order ambiguity (0° and 180° are their own negation mod 360), and\n * 3/6/8 have no flip at all. Verified directly (not just by this algebra) —\n * confirmed in a real browser that `rotate(270) + flipHorizontal` under the\n * current render order reproduces the exact same pixels as\n * `rotate(90) + flipHorizontal` did under the old (flip-before-rotate) order,\n * and vice versa.\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: 270, flipHorizontal: true };\n case 6:\n return { rotation: 90, flipHorizontal: false };\n case 7:\n return { rotation: 90, 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 sharpenFilterId: string\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 // Canvas 2D transform calls compose in call order, with the *last*-called\n // one applied first to the drawn content (same right-to-left rule as a\n // CSS transform list) — so calling scale() before rotate() here means\n // rotate is applied first, then scale (flip) second, matching stage.ts's\n // transform order. Flip is the outermost operation, applied to the\n // already-rotated result, so it always mirrors along the\n // currently-displayed horizontal/vertical axes rather than the image's\n // own pre-rotation axes. Verified empirically (not just by this reasoning)\n // with a real asymmetric-marker canvas test before trusting it.\n sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));\n sctx.rotate((state.rotation * Math.PI) / 180);\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 // — including sharpness, whose SVG feConvolveMatrix definition (see\n // stage.ts) `url(#sharpenFilterId)` references works identically here even\n // though sourceCanvas itself is never attached to the document.\n sctx.filter = buildFilterString(state.filters, sharpenFilterId);\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 sharpenFilterId: string,\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(\n img,\n state,\n frame,\n width,\n height,\n frameShape,\n cornerRadius,\n sharpenFilterId\n );\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 applyFramePosition,\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 effectiveNaturalSize,\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 movableFrame: 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. With\n * `movableFrame: true`, this inverts: the image is fixed and the frame\n * itself is what you drag/resize instead — see `KiriOptions.movableFrame`.\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 framePosition: { x: 0, y: 0 },\n };\n /**\n * `movableFrame` mode only: the image's fixed on-screen size, frozen at\n * `load()` time and recomputed on `rotate()` — see `getFixedImageSize()`.\n */\n private fixedImageSize: Size = { width: 0, height: 0 };\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 movableFrame: options.movableFrame ?? 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 this.opts.movableFrame,\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, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);\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 getFixedImageSize: () => this.fixedImageSize,\n },\n { mouseWheelZoom: this.opts.mouseWheelZoom, movableFrame: this.opts.movableFrame }\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 // In movableFrame mode there's no pan/zoom concept at all, including at\n // load — `loadOptions.zoom`/`offset` are ignored (not just clamped) so\n // the image renders exactly centered at its frozen size from the very\n // first frame. Without this, a caller-supplied zoom/offset would get\n // clamped against a `rendered` size computed at *that* zoom, while the\n // image actually always renders at `fixedImageSize`'s frozen (zoom-1)\n // scale — the same current-vs-frozen mismatch as the commitState()/\n // rotate() bugs above, just reachable through load() options instead.\n const zoom = this.opts.movableFrame\n ? 1\n : clampZoom(loadOptions.zoom ?? this.opts.minZoom, this.opts.minZoom, this.opts.maxZoom);\n const rendered = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, zoom);\n const offset = this.opts.movableFrame\n ? { x: 0, y: 0 }\n : clampOffset(loadOptions.offset ?? { x: 0, y: 0 }, rendered, this.getFrameSize());\n // Frozen here, not derived on demand — see fixedImageSize's own doc\n // comment for why (the frame can be resized after load in movableFrame\n // mode, and the image must not resize along with it).\n this.fixedImageSize = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, 1);\n if (this.opts.movableFrame && this.opts.autoSizeStage) this.syncStageSizeToFixedImage();\n\n this.commitState({\n zoom,\n offset,\n rotation,\n flip: { horizontal: flipHorizontal, vertical: flipVertical },\n filters: this.state.filters,\n framePosition: { x: 0, y: 0 },\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 framePosition: { ...this.state.framePosition },\n };\n }\n\n /**\n * Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`.\n * No-op if `movableFrame: true` — the image never zooms in that mode.\n */\n setZoom(zoom: number): void {\n if (this.opts.movableFrame) return;\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. No-op if `movableFrame: true` — the image never\n * pans in that mode; see `setFramePosition()` instead.\n */\n setOffset(offset: Offset): void {\n if (this.opts.movableFrame) return;\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 * Sets the frame's position to an absolute value (offset from stage\n * center, in stage pixels), clamped so the frame stays fully within the\n * image's fixed bounds. Only meaningful — and only takes effect — when\n * `movableFrame: true`; a no-op otherwise, since the frame always stays\n * centered in the default mode.\n */\n setFramePosition(position: Offset): void {\n if (!this.opts.movableFrame) return;\n const clamped = clampOffset(position, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, framePosition: clamped });\n }\n\n /**\n * Reverts zoom/offset/rotation/flip/filters/framePosition to what they\n * were right after `load()` resolved (including any `loadOptions` passed\n * to it). No-op if 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 framePosition: { ...this.initialState.framePosition },\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\n if (this.opts.movableFrame) {\n // Rotation swaps which natural dimension maps to width/height, so\n // fixedImageSize needs recomputing for the new orientation — but the\n // *scale* it was established at must carry forward unchanged, not be\n // re-derived from getFrameSize(). The frame may have been resized\n // since load() (setFrameSize() deliberately never touches\n // fixedImageSize — that's the whole point of this mode), and\n // recomputing cover scale from that current, possibly-shrunk frame\n // would silently re-anchor the \"fixed\" image to a different size on\n // every rotate — the same class of bug as commitState()'s scale fix\n // above, just hit through rotation instead of a live re-render.\n const frozenScale =\n this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, this.state.rotation).width;\n const newEffNatural = effectiveNaturalSize(this.naturalSize, rotation);\n this.fixedImageSize = {\n width: newEffNatural.width * frozenScale,\n height: newEffNatural.height * frozenScale,\n };\n if (this.opts.autoSizeStage) this.syncStageSizeToFixedImage();\n const framePosition = clampOffset(this.state.framePosition, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, rotation, framePosition });\n return;\n }\n\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 /**\n * Toggles horizontal flip. Always mirrors left-right as currently\n * displayed on screen, regardless of the current `rotation` — not the\n * image's own pre-rotation axes, so the visible effect stays \"horizontal\"\n * no matter how the image is rotated. No-op if `flippable: false`.\n */\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 /**\n * Toggles vertical flip. Always mirrors top-to-bottom as currently\n * displayed on screen, regardless of the current `rotation`. No-op if\n * `flippable: false`.\n */\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) — except in `movableFrame` mode,\n * where the stage stays pinned to the fixed image's own size (see\n * `fixedImageSize`'s doc comment) instead of following the frame; letting\n * the stage shrink along with the frame there would clip the (unchanged,\n * still full-size) image down to whatever's left, visually reading as the\n * picture itself shrinking even though its actual rendered size never\n * changes. Each axis is clamped to a 20px 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 if (this.opts.movableFrame) {\n // Unlike the default mode (where the image auto-zooms to always cover\n // whatever frame size is set), the image is fixed here — so growing\n // the frame past the image's own bounds has to be capped, not\n // absorbed by zooming the image further.\n this.opts.frame.width = Math.min(this.opts.frame.width, this.fixedImageSize.width);\n this.opts.frame.height = Math.min(this.opts.frame.height, this.fixedImageSize.height);\n }\n setStageFrameSize(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height);\n if (this.opts.autoSizeStage && !this.opts.movableFrame) this.syncStageSize();\n\n if (this.opts.movableFrame) {\n const framePosition = clampOffset(this.state.framePosition, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, framePosition });\n return;\n }\n\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.getEffectiveStateForCrop(),\n this.getFrameSize(),\n this.opts.frame.shape,\n this.opts.frame.cornerRadius,\n this.stage.sharpenFilterId,\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.getEffectiveStateForCrop());\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 /**\n * `computeCropRegion()`/`renderCropToCanvas()` only know one model: an\n * image shifted by `offset` under a frame fixed at center, scaled by\n * `computeCoverScale(natural, frame, rotation) * zoom` — where `frame` is\n * whatever's passed in, i.e. the *current* frame size. In `movableFrame`\n * mode neither of those assumptions holds as-is: it's the frame that\n * shifts (by `framePosition`, image fixed at center), and the image's\n * scale must stay pinned to `fixedImageSize` regardless of how the frame\n * has since been resized — not recomputed from the current frame size,\n * which is exactly what those functions would otherwise do internally.\n *\n * Both are fixed by substituting two `state` fields before calling them,\n * rather than changing either function:\n * - `offset = -framePosition` — \"frame moved right by d\" and \"image moved\n * left by d\" describe the identical relative geometry, so this alone\n * carries the frame's position into the existing math unmodified.\n * (`state.offset` is always `{0,0}` in this mode already, since\n * `setOffset()`/gesture panning are no-ops here — this substitution is\n * what actually does the work, not a redundant no-op.)\n * - `zoom` = a value chosen so that\n * `computeCoverScale(natural, currentFrame, rotation) * zoom` evaluates\n * to exactly `fixedImageSize`'s own scale, whatever the current frame\n * size is — i.e. `zoom = frozenScale / computeCoverScale(natural,\n * currentFrame, rotation)`, which cancels the current-frame-size term\n * out algebraically. Caught by an actual pixel test (a moved+resized\n * frame exported as fully transparent) before trusting the offset\n * substitution was sufficient on its own — it wasn't.\n */\n private getEffectiveStateForCrop(): KiriState {\n if (!this.opts.movableFrame) return this.state;\n const frozenScale =\n this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, this.state.rotation).width;\n const currentCoverScale = computeCoverScale(this.naturalSize, this.getFrameSize(), this.state.rotation);\n return {\n ...this.state,\n offset: { x: -this.state.framePosition.x, y: -this.state.framePosition.y },\n zoom: frozenScale / currentCoverScale,\n };\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 // movableFrame's stage-sizing counterpart to syncStageSize() above: pins\n // the stage to the fixed image's own size (so the whole image stays\n // visible/on-screen) instead of the frame's, which may since have been\n // resized independently. Called after load() and rotate() establish/\n // recompute fixedImageSize — never from setFrameSize(), which is the\n // entire point of this mode.\n private syncStageSizeToFixedImage(): void {\n setStageSize(\n this.stage.stageEl,\n this.fixedImageSize.width + STAGE_AUTO_SIZE_PADDING * 2,\n this.fixedImageSize.height + STAGE_AUTO_SIZE_PADDING * 2\n );\n }\n\n private commitState(next: KiriState): void {\n this.state = next;\n // In movableFrame mode the image's rendered scale must stay pinned to\n // fixedImageSize (frozen at load()/rotate()) regardless of the frame's\n // current size — otherwise resizing the frame past the point where the\n // *other* axis becomes the covering-scale's limiting dimension silently\n // changes computeCoverScale()'s result and the image visibly zooms,\n // even though `zoom` itself never left 1 (setZoom() is a no-op here).\n // Caught live in the browser: growing/shrinking the frame with\n // resizableFrame + movableFrame both on visibly resized the image.\n const scale = this.opts.movableFrame\n ? this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, next.rotation).width\n : computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;\n applyTransform(this.stage.imageLayerEl, next, scale);\n applyFilters(this.stage.imgEl, next.filters, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);\n if (this.opts.movableFrame) applyFramePosition(this.stage.frameEl, next.framePosition);\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":["applyFramePosition","scale","setStageFrameSize"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.kiri-stage{position:relative;overflow:hidden;width:100%;height:100%;min-height:200px;background:#333;touch-action:none;cursor:grab;user-select:none}.kiri-stage.kiri-dragging{cursor:grabbing}.kiri-image-layer{position:absolute;top:50%;left:50%;pointer-events:none}.kiri-image-layer img{display:block;max-width:none}.kiri-frame{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border:1px solid rgba(255,255,255,.85);box-shadow:0 0 0 9999px #00000080;pointer-events:none}.kiri-frame--circle{border-radius:50%}.kiri-frame-handle{position:absolute;width:10px;height:10px;background:#fff;border:1px solid #333;border-radius:50%;pointer-events:auto}.kiri-frame-handle--top-left,.kiri-frame-handle--bottom-right{cursor:nwse-resize}.kiri-frame-handle--top-right,.kiri-frame-handle--bottom-left{cursor:nesw-resize}.kiri-root{display:inline-flex;gap:10px;align-items:stretch}.kiri-root--bottom,.kiri-root--top{flex-direction:column}.kiri-root--left,.kiri-root--right{flex-direction:row}.kiri-root--top .kiri-zoomer,.kiri-root--left .kiri-zoomer{order:-1}.kiri-zoomer{cursor:pointer}.kiri-root--left .kiri-zoomer,.kiri-root--right .kiri-zoomer{writing-mode:vertical-lr;direction:rtl;width:24px}
|
package/dist/kiri.min.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
(function(w,S){typeof exports=="object"&&typeof module<"u"?S(exports):typeof define=="function"&&define.amd?define(["exports"],S):(w=typeof globalThis<"u"?globalThis:w||self,S(w.Kiri={}))})(this,function(w){"use strict";var gt=Object.defineProperty;var ut=(w,S,E)=>S in w?gt(w,S,{enumerable:!0,configurable:!0,writable:!0,value:E}):w[S]=E;var v=(w,S,E)=>ut(w,typeof S!="symbol"?S+"":S,E);const S={brightness:1,contrast:1,saturation:1,sharpness:1,grayscale:!1,sepia:!1};function E(i,t){return{brightness:Math.max(0,t.brightness??i.brightness),contrast:Math.max(0,t.contrast??i.contrast),saturation:Math.max(0,t.saturation??i.saturation),sharpness:Math.max(0,t.sharpness??i.sharpness),grayscale:t.grayscale??i.grayscale,sepia:t.sepia??i.sepia}}function N(i){const t=i>1?(i-1)/4.5:0,e=1+4*t,s=-t;return`0 ${s} 0 ${s} ${e} ${s} 0 ${s} 0`}function U(i,t){const e=[];return i.sharpness>1&&e.push(`url(#${t})`),e.push(`brightness(${i.brightness})`,`contrast(${i.contrast})`,`saturate(${i.saturation})`),i.grayscale&&e.push("grayscale(1)"),i.sepia&&e.push("sepia(1)"),e.join(" ")}const $="http://www.w3.org/2000/svg";let _=0;function G(){_+=1;const i=`kiri-sharpen-${_}`,t=document.createElementNS($,"svg");t.setAttribute("width","0"),t.setAttribute("height","0"),t.style.position="absolute";const e=document.createElementNS($,"filter");e.setAttribute("id",i),e.setAttribute("color-interpolation-filters","sRGB");const s=document.createElementNS($,"feConvolveMatrix");return s.setAttribute("order","3"),s.setAttribute("divisor","1"),s.setAttribute("edgeMode","duplicate"),s.setAttribute("preserveAlpha","true"),s.setAttribute("kernelMatrix",N(1)),e.appendChild(s),t.appendChild(e),{svg:t,filterId:i,kernelEl:s}}function W(i,t,e,s,o,n,l){i.innerHTML="";const h=document.createElement("div");h.className="kiri-stage",h.tabIndex=0,h.setAttribute("role","application"),h.setAttribute("aria-label",n?"Image cropper. Drag or use arrow keys to move the crop frame over the fixed image. 0 resets.":"Image cropper. Drag to pan. Arrow keys to pan, plus/minus to zoom, 0 to reset.");const f=document.createElement("div");f.className="kiri-image-layer";const m=document.createElement("img");m.draggable=!1,f.appendChild(m);const g=document.createElement("div");g.className=t==="circle"?"kiri-frame kiri-frame--circle":t==="rounded-rectangle"?"kiri-frame kiri-frame--rounded-rectangle":"kiri-frame",k(g,e,s),t==="rounded-rectangle"&&(g.style.borderRadius=`${o}px`);const{svg:p,filterId:c,kernelEl:u}=G();h.appendChild(f),h.appendChild(g),h.appendChild(p);let r=null;if(l.show){const a=document.createElement("div");a.className=`kiri-root kiri-root--${l.position}`,r=document.createElement("input"),r.type="range",r.className="kiri-zoomer",r.min=String(l.min),r.max=String(l.max),r.step="0.01",r.value=String(l.value),n&&(r.disabled=!0),a.appendChild(h),a.appendChild(r),i.appendChild(a)}else i.appendChild(h);return{stageEl:h,frameEl:g,imageLayerEl:f,imgEl:m,zoomerEl:r,sharpenFilterId:c,sharpenKernelEl:u}}function k(i,t,e){i.style.width=`${t}px`,i.style.height=`${e}px`}const Z=20;function H(i,t,e){i.style.width=`${t}px`,i.style.height=`${e}px`}function q(i,t,e){const s=e*(t.flip.horizontal?-1:1),o=e*(t.flip.vertical?-1:1);i.style.transform=`translate(-50%, -50%) translate(${t.offset.x}px, ${t.offset.y}px) scale(${s}, ${o}) rotate(${t.rotation}deg)`}function K(i,t,e,s){e.setAttribute("kernelMatrix",N(t.sharpness)),i.style.filter=U(t,s)}function J(i,t){i.style.transform=`translate(-50%, -50%) translate(${t.x}px, ${t.y}px)`}function P(i,t){return(t/90%2+2)%2===1?{width:i.height,height:i.width}:{width:i.width,height:i.height}}function I(i,t,e){const s=P(i,e);return s.width<=0||s.height<=0?1:Math.max(t.width/s.width,t.height/s.height)}function y(i,t,e,s){const o=P(i,e),n=I(i,t,e)*s;return{width:o.width*n,height:o.height*n}}function O(i,t,e){return Math.min(Math.max(i,t),e)}function x(i,t,e){const s=Math.max(0,(t.width-e.width)/2),o=Math.max(0,(t.height-e.height)/2);return{x:Math.min(Math.max(i.x,-s),s)||0,y:Math.min(Math.max(i.y,-o),o)||0}}function L(i){return(i%360+360)%360}const b=15,Y=.1;function X(i,t){return Math.hypot(i.clientX-t.clientX,i.clientY-t.clientY)}function Q(i,t,e){const s=new Map;let o=null,n=0,l=1;function h(r){const{min:a,max:d}=t.getMinMaxZoom(),z=O(r.zoom,a,d),M=y(t.getNaturalSize(),t.getFrameSize(),r.rotation,z),F=x(r.offset,M,t.getFrameSize());t.setState({zoom:z,offset:F,rotation:L(r.rotation),flip:r.flip,filters:r.filters,framePosition:r.framePosition})}function f(r){const a=t.getState(),d=x(r,t.getFixedImageSize(),t.getFrameSize());t.setState({...a,framePosition:d})}function m(r){if(i.setPointerCapture(r.pointerId),s.set(r.pointerId,r),s.size===1){const a=t.getState();o={x:r.clientX,y:r.clientY,offset:e.movableFrame?a.framePosition:a.offset},i.classList.add("kiri-dragging")}else if(s.size===2){o=null;const[a,d]=[...s.values()];n=X(a,d),l=t.getState().zoom}}function g(r){if(s.has(r.pointerId)){if(s.set(r.pointerId,r),s.size===2){if(e.movableFrame)return;const[a,d]=[...s.values()],z=X(a,d);if(n>0){const M=z/n,F=t.getState();h({...F,zoom:l*M})}return}if(o){const a=r.clientX-o.x,d=r.clientY-o.y;if(e.movableFrame){f({x:o.offset.x+a,y:o.offset.y+d});return}const z=t.getState();h({...z,offset:{x:o.offset.x+a,y:o.offset.y+d}})}}}function p(r){s.delete(r.pointerId),s.size<2&&(n=0),s.size===0&&(o=null,i.classList.remove("kiri-dragging"))}function c(r){if(e.movableFrame||!e.mouseWheelZoom||e.mouseWheelZoom==="ctrl"&&!r.ctrlKey)return;r.preventDefault();const a=t.getState(),d=-r.deltaY*.0015;h({...a,zoom:a.zoom*(1+d)})}function u(r){const a=t.getState();if(e.movableFrame)switch(r.key){case"ArrowLeft":r.preventDefault(),f({...a.framePosition,x:a.framePosition.x-b});return;case"ArrowRight":r.preventDefault(),f({...a.framePosition,x:a.framePosition.x+b});return;case"ArrowUp":r.preventDefault(),f({...a.framePosition,y:a.framePosition.y-b});return;case"ArrowDown":r.preventDefault(),f({...a.framePosition,y:a.framePosition.y+b});return;case"0":r.preventDefault(),t.reset();return;default:return}switch(r.key){case"ArrowLeft":r.preventDefault(),h({...a,offset:{...a.offset,x:a.offset.x+b}});break;case"ArrowRight":r.preventDefault(),h({...a,offset:{...a.offset,x:a.offset.x-b}});break;case"ArrowUp":r.preventDefault(),h({...a,offset:{...a.offset,y:a.offset.y+b}});break;case"ArrowDown":r.preventDefault(),h({...a,offset:{...a.offset,y:a.offset.y-b}});break;case"+":case"=":r.preventDefault(),h({...a,zoom:a.zoom+Y});break;case"-":case"_":r.preventDefault(),h({...a,zoom:a.zoom-Y});break;case"0":r.preventDefault(),t.reset();break}}return i.addEventListener("pointerdown",m),i.addEventListener("pointermove",g),i.addEventListener("pointerup",p),i.addEventListener("pointercancel",p),i.addEventListener("wheel",c,{passive:!1}),i.addEventListener("keydown",u),{destroy(){i.removeEventListener("pointerdown",m),i.removeEventListener("pointermove",g),i.removeEventListener("pointerup",p),i.removeEventListener("pointercancel",p),i.removeEventListener("wheel",c),i.removeEventListener("keydown",u)}}}function tt(i){const t=new DataView(i);if(t.byteLength<4||t.getUint16(0,!1)!==65496)return 1;let e=2;for(;e+4<=t.byteLength;){const s=t.getUint16(e,!1),o=t.getUint16(e+2,!1);if(s===65505){const n=e+4;return t.getUint32(n,!1)!==1165519206?1:et(t,n+6)}if((s&65280)!==65280)break;e+=2+o}return 1}function et(i,t){const e=i.getUint16(t,!1)===18761,s=i.getUint32(t+4,e),o=t+s;if(o+2>i.byteLength)return 1;const n=i.getUint16(o,e);for(let l=0;l<n;l++){const h=o+2+l*12;if(h+12>i.byteLength)break;if(i.getUint16(h,e)===274){const m=i.getUint16(h+8,e);return m>=1&&m<=8?m:1}}return 1}function it(i){switch(i){case 2:return{rotation:0,flipHorizontal:!0};case 3:return{rotation:180,flipHorizontal:!1};case 4:return{rotation:180,flipHorizontal:!0};case 5:return{rotation:270,flipHorizontal:!0};case 6:return{rotation:90,flipHorizontal:!1};case 7:return{rotation:90,flipHorizontal:!0};case 8:return{rotation:270,flipHorizontal:!1};default:return{rotation:0,flipHorizontal:!1}}}function C(i,t,e,s){return i===void 0?e:t.includes(i)?i:(console.warn(`Kiri: invalid ${s} "${i}" — defaulting to "${e}". Valid values: ${t.join(", ")}.`),e)}function A(i,t,e){return Math.min(Math.max(i,t),e)}const st=["base64","blob","canvas"],ot=["image/jpeg","image/png","image/webp"];function j(i,t,e){return{left:i.width/2-t.x-e.width/2,top:i.height/2-t.y-e.height/2}}function rt(i,t,e){const s=I(i,t,e.rotation)*e.zoom,o=y(i,t,e.rotation,e.zoom),{left:n,top:l}=j(o,e.offset,t),h={x:o.width/2,y:o.height/2},f={x:i.width/2,y:i.height/2},m=L(e.rotation),g=[{x:n,y:l},{x:n+t.width,y:l},{x:n,y:l+t.height},{x:n+t.width,y:l+t.height}].map(({x:z,y:M})=>{const F=(z-h.x)/s,T=(M-h.y)/s;let D,R;return m===90?(D=T,R=-F):m===180?(D=-F,R=-T):m===270?(D=-T,R=F):(D=F,R=T),{x:D+f.x,y:R+f.y}}),p=g.map(z=>z.x),c=g.map(z=>z.y),u=A(Math.min(...p),0,i.width),r=A(Math.min(...c),0,i.height),a=A(Math.max(...p),0,i.width),d=A(Math.max(...c),0,i.height);return{x:Math.round(u),y:Math.round(r),width:Math.round(a-u),height:Math.round(d-r),rotation:m,flip:{...e.flip}}}function at(i,t,e,s,o,n,l,h){const f={width:i.naturalWidth,height:i.naturalHeight},m=I(f,e,t.rotation)*t.zoom,g=y(f,e,t.rotation,t.zoom),p=document.createElement("canvas");p.width=Math.max(1,Math.round(g.width)),p.height=Math.max(1,Math.round(g.height));const c=p.getContext("2d");if(!c)throw new Error("Kiri: unable to get 2D canvas context");c.translate(p.width/2,p.height/2),c.scale(m*(t.flip.horizontal?-1:1),m*(t.flip.vertical?-1:1)),c.rotate(t.rotation*Math.PI/180),c.filter=U(t.filters,h),c.drawImage(i,-f.width/2,-f.height/2,f.width,f.height);const{left:u,top:r}=j(g,t.offset,e),a=document.createElement("canvas");a.width=s,a.height=o;const d=a.getContext("2d");if(!d)throw new Error("Kiri: unable to get 2D canvas context");if(n==="circle")d.save(),d.beginPath(),d.ellipse(s/2,o/2,s/2,o/2,0,0,Math.PI*2),d.clip();else if(n==="rounded-rectangle"){const z=s/e.width,M=A(l*z,0,Math.min(s,o)/2);d.save(),d.beginPath(),d.roundRect(0,0,s,o,M),d.clip()}return d.drawImage(p,u,r,e.width,e.height,0,0,s,o),(n==="circle"||n==="rounded-rectangle")&&d.restore(),a}async function nt(i,t,e,s,o,n,l){const h=C(l.type,st,"base64","export type"),f=C(l.format,ot,"image/png","export format"),m=l.quality,g=l.width??e.width,p=l.height??e.height;(s==="circle"||s==="rounded-rectangle")&&f==="image/jpeg"&&console.warn(`Kiri: exporting a ${s}-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.`);const c=at(i,t,e,g,p,s,o,n);return h==="canvas"?c:h==="base64"?c.toDataURL(f,m):new Promise((u,r)=>{c.toBlob(a=>a?u(a):r(new Error("Kiri: canvas.toBlob failed")),f,m)})}async function ht(i,t){const e=t.fieldName??"file",s=t.fileName??`crop.${lt(t.format??"image/png")}`,o=new FormData;o.append(e,i,s);for(const[n,l]of Object.entries(t.extraFields??{}))o.append(n,l);return fetch(t.url,{...t.fetchOptions,method:"POST",body:o})}function lt(i){switch(i){case"image/jpeg":return"jpg";case"image/webp":return"webp";default:return"png"}}const V=200,B=20,ft=20,mt=["rectangle","circle","rounded-rectangle"],ct=["top","bottom","left","right"];function dt(i){return i===void 0?!0:typeof i=="boolean"||i==="ctrl"?i:(console.warn(`Kiri: invalid mouseWheelZoom "${String(i)}" — defaulting to true. Valid values: true, false, "ctrl".`),!0)}class pt{constructor(t,e={}){v(this,"container");v(this,"opts");v(this,"stage");v(this,"gestureHandle");v(this,"resizeHandle",null);v(this,"zoomerHandle",null);v(this,"naturalSize",{width:0,height:0});v(this,"state",{zoom:1,offset:{x:0,y:0},rotation:0,flip:{horizontal:!1,vertical:!1},filters:S,framePosition:{x:0,y:0}});v(this,"fixedImageSize",{width:0,height:0});v(this,"initialState",null);v(this,"listeners",{change:[]});var s,o,n,l;if(!t||typeof t.appendChild!="function")throw new Error("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.");this.container=t,this.opts={frame:{shape:C((s=e.frame)==null?void 0:s.shape,mt,"rectangle","frame.shape"),width:((o=e.frame)==null?void 0:o.width)??V,height:((n=e.frame)==null?void 0:n.height)??V,cornerRadius:((l=e.frame)==null?void 0:l.cornerRadius)??ft},minZoom:e.minZoom??1,maxZoom:e.maxZoom??4,rotatable:e.rotatable??!0,flippable:e.flippable??!0,resizableFrame:e.resizableFrame??!1,lockAspectRatio:e.lockAspectRatio??!1,movableFrame:e.movableFrame??!1,mouseWheelZoom:dt(e.mouseWheelZoom),useExifOrientation:e.useExifOrientation??!0,uploader:e.uploader,autoSizeStage:e.autoSizeStage??!0,showZoomer:e.showZoomer??!1,zoomerPosition:C(e.zoomerPosition,ct,"bottom","zoomerPosition")},this.state.filters=E(S,e.filters??{}),this.stage=W(this.container,this.opts.frame.shape,this.opts.frame.width,this.opts.frame.height,this.opts.frame.cornerRadius,this.opts.movableFrame,{show:this.opts.showZoomer,position:this.opts.zoomerPosition,min:this.opts.minZoom,max:this.opts.maxZoom,value:this.state.zoom}),this.opts.autoSizeStage&&this.syncStageSize(),K(this.stage.imgEl,this.state.filters,this.stage.sharpenKernelEl,this.stage.sharpenFilterId),this.stage.zoomerEl&&this.enableZoomer(this.stage.zoomerEl),this.gestureHandle=Q(this.stage.stageEl,{getNaturalSize:()=>this.naturalSize,getFrameSize:()=>this.getFrameSize(),getState:()=>this.state,getMinMaxZoom:()=>({min:this.opts.minZoom,max:this.opts.maxZoom}),setState:h=>this.commitState(h),reset:()=>this.reset(),getFixedImageSize:()=>this.fixedImageSize},{mouseWheelZoom:this.opts.mouseWheelZoom,movableFrame:this.opts.movableFrame}),this.opts.resizableFrame&&this.enableFrameResize()}async load(t,e={}){var p,c;let s=L(e.rotation??0),o=((p=e.flip)==null?void 0:p.horizontal)??!1;const n=((c=e.flip)==null?void 0:c.vertical)??!1;let l=null,h;if(typeof t=="string")h=t;else{if(this.opts.useExifOrientation){const u=await t.arrayBuffer(),r=tt(u),a=it(r);s=L(s+a.rotation),o=o!==a.flipHorizontal}l=URL.createObjectURL(t),h=l}try{await new Promise((u,r)=>{this.stage.imgEl.onload=()=>u(),this.stage.imgEl.onerror=()=>r(new Error("Kiri: failed to load image")),this.stage.imgEl.src=h})}finally{l&&URL.revokeObjectURL(l)}this.naturalSize={width:this.stage.imgEl.naturalWidth,height:this.stage.imgEl.naturalHeight};const f=this.opts.movableFrame?1:O(e.zoom??this.opts.minZoom,this.opts.minZoom,this.opts.maxZoom),m=y(this.naturalSize,this.getFrameSize(),s,f),g=this.opts.movableFrame?{x:0,y:0}:x(e.offset??{x:0,y:0},m,this.getFrameSize());this.fixedImageSize=y(this.naturalSize,this.getFrameSize(),s,1),this.opts.movableFrame&&this.opts.autoSizeStage&&this.syncStageSizeToFixedImage(),this.commitState({zoom:f,offset:g,rotation:s,flip:{horizontal:o,vertical:n},filters:this.state.filters,framePosition:{x:0,y:0}}),this.initialState=this.getState()}getState(){return{zoom:this.state.zoom,offset:{...this.state.offset},rotation:this.state.rotation,flip:{...this.state.flip},filters:{...this.state.filters},framePosition:{...this.state.framePosition}}}setZoom(t){if(this.opts.movableFrame)return;const e=O(t,this.opts.minZoom,this.opts.maxZoom),s=y(this.naturalSize,this.getFrameSize(),this.state.rotation,e),o=x(this.state.offset,s,this.getFrameSize());this.commitState({...this.state,zoom:e,offset:o})}setOffset(t){if(this.opts.movableFrame)return;const e=y(this.naturalSize,this.getFrameSize(),this.state.rotation,this.state.zoom),s=x(t,e,this.getFrameSize());this.commitState({...this.state,offset:s})}setFramePosition(t){if(!this.opts.movableFrame)return;const e=x(t,this.fixedImageSize,this.getFrameSize());this.commitState({...this.state,framePosition:e})}reset(){this.initialState&&this.commitState({zoom:this.initialState.zoom,offset:{...this.initialState.offset},rotation:this.initialState.rotation,flip:{...this.initialState.flip},filters:{...this.initialState.filters},framePosition:{...this.initialState.framePosition}})}rotate(t){if(!this.opts.rotatable)return;const e=Math.round(t/90)*90,s=L(this.state.rotation+e);if(this.opts.movableFrame){const l=this.fixedImageSize.width/P(this.naturalSize,this.state.rotation).width,h=P(this.naturalSize,s);this.fixedImageSize={width:h.width*l,height:h.height*l},this.opts.autoSizeStage&&this.syncStageSizeToFixedImage();const f=x(this.state.framePosition,this.fixedImageSize,this.getFrameSize());this.commitState({...this.state,rotation:s,framePosition:f});return}const o=y(this.naturalSize,this.getFrameSize(),s,this.state.zoom),n=x(this.state.offset,o,this.getFrameSize());this.commitState({...this.state,rotation:s,offset:n})}flipHorizontal(){if(!this.opts.flippable)return;const t={...this.state.flip,horizontal:!this.state.flip.horizontal};this.commitState({...this.state,flip:t})}flipVertical(){if(!this.opts.flippable)return;const t={...this.state.flip,vertical:!this.state.flip.vertical};this.commitState({...this.state,flip:t})}setFrameSize(t,e){if(this.opts.frame.width=Math.max(B,t),this.opts.frame.height=Math.max(B,e),this.opts.movableFrame&&(this.opts.frame.width=Math.min(this.opts.frame.width,this.fixedImageSize.width),this.opts.frame.height=Math.min(this.opts.frame.height,this.fixedImageSize.height)),k(this.stage.frameEl,this.opts.frame.width,this.opts.frame.height),this.opts.autoSizeStage&&!this.opts.movableFrame&&this.syncStageSize(),this.opts.movableFrame){const n=x(this.state.framePosition,this.fixedImageSize,this.getFrameSize());this.commitState({...this.state,framePosition:n});return}const s=y(this.naturalSize,this.getFrameSize(),this.state.rotation,this.state.zoom),o=x(this.state.offset,s,this.getFrameSize());this.commitState({...this.state,offset:o})}setFilters(t){this.commitState({...this.state,filters:E(this.state.filters,t)})}async export(t={}){return nt(this.stage.imgEl,this.getEffectiveStateForCrop(),this.getFrameSize(),this.opts.frame.shape,this.opts.frame.cornerRadius,this.stage.sharpenFilterId,t)}getCropRegion(){return rt(this.naturalSize,this.getFrameSize(),this.getEffectiveStateForCrop())}async upload(t,e={}){const s=await this.export({...e,type:"blob"});return(e.uploader??this.opts.uploader??ht)(s,{...e,url:t})}on(t,e){this.listeners[t].push(e)}off(t,e){this.listeners[t]=this.listeners[t].filter(s=>s!==e)}destroy(){var t,e;this.gestureHandle.destroy(),(t=this.resizeHandle)==null||t.destroy(),(e=this.zoomerHandle)==null||e.destroy(),this.container.innerHTML="",this.listeners.change=[]}getFrameSize(){return{width:this.opts.frame.width,height:this.opts.frame.height}}getEffectiveStateForCrop(){if(!this.opts.movableFrame)return this.state;const t=this.fixedImageSize.width/P(this.naturalSize,this.state.rotation).width,e=I(this.naturalSize,this.getFrameSize(),this.state.rotation);return{...this.state,offset:{x:-this.state.framePosition.x,y:-this.state.framePosition.y},zoom:t/e}}syncStageSize(){H(this.stage.stageEl,this.opts.frame.width+Z*2,this.opts.frame.height+Z*2)}syncStageSizeToFixedImage(){H(this.stage.stageEl,this.fixedImageSize.width+Z*2,this.fixedImageSize.height+Z*2)}commitState(t){this.state=t;const e=this.opts.movableFrame?this.fixedImageSize.width/P(this.naturalSize,t.rotation).width:I(this.naturalSize,this.getFrameSize(),t.rotation)*t.zoom;q(this.stage.imageLayerEl,t,e),K(this.stage.imgEl,t.filters,this.stage.sharpenKernelEl,this.stage.sharpenFilterId),this.opts.movableFrame&&J(this.stage.frameEl,t.framePosition),this.stage.zoomerEl&&(this.stage.zoomerEl.value=String(t.zoom));for(const s of this.listeners.change)s(this.getState())}enableZoomer(t){const e=()=>this.setZoom(Number(t.value));t.addEventListener("input",e),this.zoomerHandle={destroy(){t.removeEventListener("input",e)}}}enableFrameResize(){const t=[{name:"top-left",edge:{left:"-5px",top:"-5px"},signX:-1,signY:-1},{name:"top-right",edge:{right:"-5px",top:"-5px"},signX:1,signY:-1},{name:"bottom-left",edge:{left:"-5px",bottom:"-5px"},signX:-1,signY:1},{name:"bottom-right",edge:{right:"-5px",bottom:"-5px"},signX:1,signY:1}],e=[];for(const s of t){const o=document.createElement("div");o.className=`kiri-frame-handle kiri-frame-handle--${s.name}`,Object.assign(o.style,s.edge),this.stage.frameEl.appendChild(o);let n=null;const l=m=>{m.stopPropagation(),o.setPointerCapture(m.pointerId),n={x:m.clientX,y:m.clientY,width:this.opts.frame.width,height:this.opts.frame.height}},h=m=>{if(!n)return;const g=(m.clientX-n.x)*2*s.signX,p=(m.clientY-n.y)*2*s.signY,c=n.width+g,u=n.height+p;if(this.opts.lockAspectRatio){const r=n.width/n.height;Math.abs(c-n.width)>=Math.abs(u-n.height)*r?this.setFrameSize(c,c/r):this.setFrameSize(u*r,u)}else this.setFrameSize(c,u)},f=()=>{n=null};o.addEventListener("pointerdown",l),o.addEventListener("pointermove",h),o.addEventListener("pointerup",f),o.addEventListener("pointercancel",f),e.push(()=>{o.removeEventListener("pointerdown",l),o.removeEventListener("pointermove",h),o.removeEventListener("pointerup",f),o.removeEventListener("pointercancel",f),o.remove()})}this.resizeHandle={destroy(){for(const s of e)s()}}}}w.Kiri=pt,Object.defineProperty(w,Symbol.toStringTag,{value:"Module"})});
|
|
2
|
+
//# sourceMappingURL=kiri.min.js.map
|
|
3
|
+
|
|
4
|
+
(function(){
|
|
5
|
+
if (typeof globalThis !== "undefined" && globalThis.Kiri && globalThis.Kiri.Kiri) {
|
|
6
|
+
globalThis.Kiri = globalThis.Kiri.Kiri;
|
|
7
|
+
}
|
|
8
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kiri.min.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 sharpness: 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 sharpness: Math.max(0, partial.sharpness ?? current.sharpness),\n grayscale: partial.grayscale ?? current.grayscale,\n sepia: partial.sepia ?? current.sepia,\n };\n}\n\n/**\n * The 3x3 unsharp-mask kernel for `sharpness`, as a `feConvolveMatrix`\n * `kernelMatrix` attribute value (row-major, space-separated):\n *\n * ```\n * 0 -k 0\n * -k 1+4k -k\n * 0 -k 0\n * ```\n *\n * The matrix always sums to `1`, so the convolution needs no separate\n * `divisor`. `sharpness <= 1` collapses to the identity kernel (center `1`,\n * neighbors `0`) — no blur/sharpen either way, matching the `1` = unchanged\n * convention used by the other numeric filters. `4.5` is chosen so a\n * `sharpness` of `2`–`3` (a reasonable slider range) lands in a visually\n * subtle-to-strong sharpening range, not an immediately harsh one.\n */\nexport function sharpenKernelMatrix(sharpness: number): string {\n const weight = sharpness > 1 ? (sharpness - 1) / 4.5 : 0;\n const center = 1 + 4 * weight;\n const neighbor = -weight;\n return `0 ${neighbor} 0 ${neighbor} ${center} ${neighbor} 0 ${neighbor} 0`;\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/sharpness pixel\n * math. Brightness/contrast/saturation/grayscale/sepia are native CSS filter\n * functions; CSS has no `sharpen()`, so `sharpness` instead references an\n * SVG `feConvolveMatrix` filter (by `sharpenFilterId`) that `stage.ts` keeps\n * in sync with the current `sharpness` value — `ctx.filter` and\n * `img.style.filter` both support `url(#id)` references to it identically.\n *\n * The `url(#id)` reference goes **first**, before the native CSS filter\n * functions — not last, despite sharpening-as-a-final-step being the more\n * common photo-editing order. Verified directly in a real browser (Chromium)\n * that a `url(#id)` SVG filter reference placed *after* one or more native\n * CSS filter functions in the same chain silently produces a fully blank\n * (transparent) result, while the identical chain with `url(#id)` first\n * renders correctly. Sharpening a source that hasn't had brightness/contrast/\n * saturation applied yet is a legitimate (if less common) ordering rather\n * than a compromise — but the ordering here is load-bearing either way, not\n * arbitrary; don't reorder this without re-verifying in a real browser.\n */\nexport function buildFilterString(filters: Filters, sharpenFilterId: string): string {\n const parts: string[] = [];\n if (filters.sharpness > 1) parts.push(`url(#${sharpenFilterId})`);\n parts.push(\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, sharpenKernelMatrix } from \"./filters\";\n\nexport interface StageElements {\n stageEl: HTMLDivElement;\n frameEl: HTMLDivElement;\n imageLayerEl: HTMLDivElement;\n imgEl: HTMLImageElement;\n zoomerEl: HTMLInputElement | null;\n /** id of the SVG `feConvolveMatrix` filter `buildFilterString()` references for `sharpness`. */\n sharpenFilterId: string;\n sharpenKernelEl: SVGFEConvolveMatrixElement;\n}\n\nconst SVG_NS = \"http://www.w3.org/2000/svg\";\nlet sharpenFilterCounter = 0;\n\n/**\n * CSS has no `sharpen()` filter, so `sharpness` works through an SVG\n * `feConvolveMatrix` referenced by `url(#id)` from the same filter string as\n * brightness/contrast/saturation (see `filters.ts`'s `buildFilterString()`)\n * — both `img.style.filter` and a canvas 2D context's `ctx.filter` support\n * `url(#id)` references identically, so this one definition drives both live\n * preview and export. A unique id per instance (rather than one shared id)\n * avoids collisions between multiple `Kiri` instances on the same page.\n * `color-interpolation-filters: sRGB` matters — SVG filter primitives\n * default to linearRGB, which would visibly shift brightness/contrast\n * relative to the sRGB-space CSS filters earlier in the same filter string.\n */\nfunction createSharpenFilter(): { svg: SVGSVGElement; filterId: string; kernelEl: SVGFEConvolveMatrixElement } {\n sharpenFilterCounter += 1;\n const filterId = `kiri-sharpen-${sharpenFilterCounter}`;\n\n const svg = document.createElementNS(SVG_NS, \"svg\");\n svg.setAttribute(\"width\", \"0\");\n svg.setAttribute(\"height\", \"0\");\n svg.style.position = \"absolute\";\n\n const filterEl = document.createElementNS(SVG_NS, \"filter\");\n filterEl.setAttribute(\"id\", filterId);\n filterEl.setAttribute(\"color-interpolation-filters\", \"sRGB\");\n\n const kernelEl = document.createElementNS(SVG_NS, \"feConvolveMatrix\");\n kernelEl.setAttribute(\"order\", \"3\");\n kernelEl.setAttribute(\"divisor\", \"1\");\n kernelEl.setAttribute(\"edgeMode\", \"duplicate\");\n // Without this, feConvolveMatrix convolves the alpha channel too — the\n // kernel's negative neighbor weights can drive alpha toward 0 right at a\n // sharp edge, turning an opaque photo semi-transparent exactly where\n // sharpening is strongest. Sharpening should only ever touch color.\n kernelEl.setAttribute(\"preserveAlpha\", \"true\");\n kernelEl.setAttribute(\"kernelMatrix\", sharpenKernelMatrix(1));\n\n filterEl.appendChild(kernelEl);\n svg.appendChild(filterEl);\n\n return { svg, filterId, kernelEl };\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 movableFrame: boolean,\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 movableFrame\n ? \"Image cropper. Drag or use arrow keys to move the crop frame over the fixed image. 0 resets.\"\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 const { svg: sharpenSvg, filterId: sharpenFilterId, kernelEl: sharpenKernelEl } =\n createSharpenFilter();\n\n stageEl.appendChild(imageLayerEl);\n stageEl.appendChild(frameEl);\n stageEl.appendChild(sharpenSvg);\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 // Zoom has no meaning in movableFrame mode (setZoom() is a no-op there,\n // and never reaches the commitState() call that resyncs this input's\n // value) — leaving it enabled would let it drag freely and just... stay\n // wherever it was dropped, with zero effect and no visual snap-back.\n if (movableFrame) zoomerEl.disabled = true;\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, sharpenFilterId, sharpenKernelEl };\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 // scale (flip) then rotate — flip is OUTERMOST, applied to the\n // already-rotated result, not the image's own pre-rotation axes. That\n // means \"flip horizontal\" always mirrors left-right as currently shown on\n // screen, and \"flip vertical\" always mirrors top-to-bottom as currently\n // shown, regardless of the current rotation — not a mirror of the\n // original image's intrinsic axes (which would make the visible effect of\n // \"horizontal\" flip depend on rotation, e.g. become a vertical-looking\n // flip once rotated 90°). Both apply around the layer's own center\n // (default transform-origin) either way.\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 `scale(${scaleX}, ${scaleY}) ` +\n `rotate(${state.rotation}deg)`;\n}\n\nexport function applyFilters(\n imgEl: HTMLImageElement,\n filters: Filters,\n sharpenKernelEl: SVGFEConvolveMatrixElement,\n sharpenFilterId: string\n): void {\n sharpenKernelEl.setAttribute(\"kernelMatrix\", sharpenKernelMatrix(filters.sharpness));\n imgEl.style.filter = buildFilterString(filters, sharpenFilterId);\n}\n\n/**\n * Positions the frame itself — only meaningful in `movableFrame` mode; the\n * default mode never calls this, leaving the frame at kiri.css's static\n * `.kiri-frame` rule (centered, no inline transform). This inline transform\n * overrides that rule the same way `applyTransform()` drives the image\n * layer's position, just for the frame instead of the image.\n */\nexport function applyFramePosition(frameEl: HTMLDivElement, framePosition: { x: number; y: number }): void {\n frameEl.style.transform =\n `translate(-50%, -50%) ` + `translate(${framePosition.x}px, ${framePosition.y}px)`;\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 * `movableFrame` mode only: the image's fixed on-screen size — frozen at\n * `load()` time (and recomputed on `rotate()`, since rotation swaps which\n * natural dimension maps to width/height), *not* derived from the current\n * `getFrameSize()`. Deliberately not just\n * `effectiveRenderedSize(natural, getFrameSize(), rotation, 1)` computed\n * on demand — the frame can be resized after load in this mode, and the\n * image must stay the same size regardless, not jump every time the frame\n * does.\n */\n getFixedImageSize: () => Size;\n}\n\nconst KEYBOARD_PAN_STEP = 15;\nconst KEYBOARD_ZOOM_STEP = 0.1;\n\nexport interface GestureOptions {\n mouseWheelZoom?: boolean | \"ctrl\";\n /**\n * When true, drag/wheel/pinch/arrow-keys move and (with `resizableFrame`)\n * resize the *frame* instead of panning/zooming the image — see\n * `KiriOptions.movableFrame`. The image is fixed at `zoom: 1`,\n * `offset: { x: 0, y: 0 }` in this mode, so none of the zoom/offset\n * gesture logic below runs at all.\n */\n movableFrame?: boolean;\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 framePosition: next.framePosition,\n });\n }\n\n // movableFrame mode: the image is fixed at zoom 1 / offset {0,0} (its\n // \"cover the frame\" size at load time, never changed), and the *frame*\n // moves within that fixed image's bounds instead. Clamping a smaller box\n // (the frame) within a bigger one (the fixed-size image) is the exact\n // same shape of problem `clampOffset` already solves for the opposite\n // case (image within frame) — reused as-is, no sign flip needed, since\n // its clamp range is symmetric around 0 either way.\n function applyFramePosition(candidate: Offset): void {\n const state = callbacks.getState();\n const framePosition = clampOffset(candidate, callbacks.getFixedImageSize(), callbacks.getFrameSize());\n callbacks.setState({ ...state, framePosition });\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 = {\n x: e.clientX,\n y: e.clientY,\n offset: options.movableFrame ? state.framePosition : state.offset,\n };\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 // No pinch-zoom in movableFrame mode — the image never zooms.\n if (options.movableFrame) return;\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 if (options.movableFrame) {\n // Direct manipulation: the frame follows the pointer, same as the\n // image does in the default mode (drag right -> moves right) —\n // unlike arrow keys below, which follow the opposite \"scroll\" idea.\n applyFramePosition({ x: dragStart.offset.x + dx, y: dragStart.offset.y + dy });\n return;\n }\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.movableFrame) return; // the image never zooms in this mode\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 //\n // movableFrame mode moves the *frame* instead, and deliberately uses the\n // *other* convention (arrow direction = direct movement direction, not\n // inverted) — the frame is the thing being directly manipulated here\n // (like nudging a selection box), not a viewport revealing more content,\n // so \"Right\" moving the frame right is the intuitive one. +/- zoom keys\n // are no-ops, matching wheel/pinch: there's no zoom in this mode.\n function onKeyDown(e: KeyboardEvent): void {\n const state = callbacks.getState();\n if (options.movableFrame) {\n switch (e.key) {\n case \"ArrowLeft\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, x: state.framePosition.x - KEYBOARD_PAN_STEP });\n return;\n case \"ArrowRight\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, x: state.framePosition.x + KEYBOARD_PAN_STEP });\n return;\n case \"ArrowUp\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, y: state.framePosition.y - KEYBOARD_PAN_STEP });\n return;\n case \"ArrowDown\":\n e.preventDefault();\n applyFramePosition({ ...state.framePosition, y: state.framePosition.y + KEYBOARD_PAN_STEP });\n return;\n case \"0\":\n e.preventDefault();\n callbacks.reset();\n return;\n default:\n return;\n }\n }\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.\n *\n * Rotation and flip don't commute, so which rotation value is paired with\n * `flipHorizontal: true` depends on Kiri's actual render order: rotation is\n * applied first, then flip mirrors the already-rotated result (see\n * stage.ts's `applyTransform()`). That's why orientations 5 and 7 use `270`/\n * `90` here rather than the more \"obvious\" `90`/`270` — reflections invert\n * the effective rotation direction (`flip · rotate(θ) = rotate(-θ) · flip`),\n * so producing the correct final image under this render order needs the\n * negated angle. Orientations 2/3/4/6/8 are unaffected: 2 and 4 have no\n * rotation-order ambiguity (0° and 180° are their own negation mod 360), and\n * 3/6/8 have no flip at all. Verified directly (not just by this algebra) —\n * confirmed in a real browser that `rotate(270) + flipHorizontal` under the\n * current render order reproduces the exact same pixels as\n * `rotate(90) + flipHorizontal` did under the old (flip-before-rotate) order,\n * and vice versa.\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: 270, flipHorizontal: true };\n case 6:\n return { rotation: 90, flipHorizontal: false };\n case 7:\n return { rotation: 90, 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 sharpenFilterId: string\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 // Canvas 2D transform calls compose in call order, with the *last*-called\n // one applied first to the drawn content (same right-to-left rule as a\n // CSS transform list) — so calling scale() before rotate() here means\n // rotate is applied first, then scale (flip) second, matching stage.ts's\n // transform order. Flip is the outermost operation, applied to the\n // already-rotated result, so it always mirrors along the\n // currently-displayed horizontal/vertical axes rather than the image's\n // own pre-rotation axes. Verified empirically (not just by this reasoning)\n // with a real asymmetric-marker canvas test before trusting it.\n sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));\n sctx.rotate((state.rotation * Math.PI) / 180);\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 // — including sharpness, whose SVG feConvolveMatrix definition (see\n // stage.ts) `url(#sharpenFilterId)` references works identically here even\n // though sourceCanvas itself is never attached to the document.\n sctx.filter = buildFilterString(state.filters, sharpenFilterId);\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 sharpenFilterId: string,\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(\n img,\n state,\n frame,\n width,\n height,\n frameShape,\n cornerRadius,\n sharpenFilterId\n );\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 applyFramePosition,\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 effectiveNaturalSize,\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 movableFrame: 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. With\n * `movableFrame: true`, this inverts: the image is fixed and the frame\n * itself is what you drag/resize instead — see `KiriOptions.movableFrame`.\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 framePosition: { x: 0, y: 0 },\n };\n /**\n * `movableFrame` mode only: the image's fixed on-screen size, frozen at\n * `load()` time and recomputed on `rotate()` — see `getFixedImageSize()`.\n */\n private fixedImageSize: Size = { width: 0, height: 0 };\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 movableFrame: options.movableFrame ?? 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 this.opts.movableFrame,\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, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);\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 getFixedImageSize: () => this.fixedImageSize,\n },\n { mouseWheelZoom: this.opts.mouseWheelZoom, movableFrame: this.opts.movableFrame }\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 // In movableFrame mode there's no pan/zoom concept at all, including at\n // load — `loadOptions.zoom`/`offset` are ignored (not just clamped) so\n // the image renders exactly centered at its frozen size from the very\n // first frame. Without this, a caller-supplied zoom/offset would get\n // clamped against a `rendered` size computed at *that* zoom, while the\n // image actually always renders at `fixedImageSize`'s frozen (zoom-1)\n // scale — the same current-vs-frozen mismatch as the commitState()/\n // rotate() bugs above, just reachable through load() options instead.\n const zoom = this.opts.movableFrame\n ? 1\n : clampZoom(loadOptions.zoom ?? this.opts.minZoom, this.opts.minZoom, this.opts.maxZoom);\n const rendered = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, zoom);\n const offset = this.opts.movableFrame\n ? { x: 0, y: 0 }\n : clampOffset(loadOptions.offset ?? { x: 0, y: 0 }, rendered, this.getFrameSize());\n // Frozen here, not derived on demand — see fixedImageSize's own doc\n // comment for why (the frame can be resized after load in movableFrame\n // mode, and the image must not resize along with it).\n this.fixedImageSize = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, 1);\n if (this.opts.movableFrame && this.opts.autoSizeStage) this.syncStageSizeToFixedImage();\n\n this.commitState({\n zoom,\n offset,\n rotation,\n flip: { horizontal: flipHorizontal, vertical: flipVertical },\n filters: this.state.filters,\n framePosition: { x: 0, y: 0 },\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 framePosition: { ...this.state.framePosition },\n };\n }\n\n /**\n * Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`.\n * No-op if `movableFrame: true` — the image never zooms in that mode.\n */\n setZoom(zoom: number): void {\n if (this.opts.movableFrame) return;\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. No-op if `movableFrame: true` — the image never\n * pans in that mode; see `setFramePosition()` instead.\n */\n setOffset(offset: Offset): void {\n if (this.opts.movableFrame) return;\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 * Sets the frame's position to an absolute value (offset from stage\n * center, in stage pixels), clamped so the frame stays fully within the\n * image's fixed bounds. Only meaningful — and only takes effect — when\n * `movableFrame: true`; a no-op otherwise, since the frame always stays\n * centered in the default mode.\n */\n setFramePosition(position: Offset): void {\n if (!this.opts.movableFrame) return;\n const clamped = clampOffset(position, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, framePosition: clamped });\n }\n\n /**\n * Reverts zoom/offset/rotation/flip/filters/framePosition to what they\n * were right after `load()` resolved (including any `loadOptions` passed\n * to it). No-op if 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 framePosition: { ...this.initialState.framePosition },\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\n if (this.opts.movableFrame) {\n // Rotation swaps which natural dimension maps to width/height, so\n // fixedImageSize needs recomputing for the new orientation — but the\n // *scale* it was established at must carry forward unchanged, not be\n // re-derived from getFrameSize(). The frame may have been resized\n // since load() (setFrameSize() deliberately never touches\n // fixedImageSize — that's the whole point of this mode), and\n // recomputing cover scale from that current, possibly-shrunk frame\n // would silently re-anchor the \"fixed\" image to a different size on\n // every rotate — the same class of bug as commitState()'s scale fix\n // above, just hit through rotation instead of a live re-render.\n const frozenScale =\n this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, this.state.rotation).width;\n const newEffNatural = effectiveNaturalSize(this.naturalSize, rotation);\n this.fixedImageSize = {\n width: newEffNatural.width * frozenScale,\n height: newEffNatural.height * frozenScale,\n };\n if (this.opts.autoSizeStage) this.syncStageSizeToFixedImage();\n const framePosition = clampOffset(this.state.framePosition, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, rotation, framePosition });\n return;\n }\n\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 /**\n * Toggles horizontal flip. Always mirrors left-right as currently\n * displayed on screen, regardless of the current `rotation` — not the\n * image's own pre-rotation axes, so the visible effect stays \"horizontal\"\n * no matter how the image is rotated. No-op if `flippable: false`.\n */\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 /**\n * Toggles vertical flip. Always mirrors top-to-bottom as currently\n * displayed on screen, regardless of the current `rotation`. No-op if\n * `flippable: false`.\n */\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) — except in `movableFrame` mode,\n * where the stage stays pinned to the fixed image's own size (see\n * `fixedImageSize`'s doc comment) instead of following the frame; letting\n * the stage shrink along with the frame there would clip the (unchanged,\n * still full-size) image down to whatever's left, visually reading as the\n * picture itself shrinking even though its actual rendered size never\n * changes. Each axis is clamped to a 20px 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 if (this.opts.movableFrame) {\n // Unlike the default mode (where the image auto-zooms to always cover\n // whatever frame size is set), the image is fixed here — so growing\n // the frame past the image's own bounds has to be capped, not\n // absorbed by zooming the image further.\n this.opts.frame.width = Math.min(this.opts.frame.width, this.fixedImageSize.width);\n this.opts.frame.height = Math.min(this.opts.frame.height, this.fixedImageSize.height);\n }\n setStageFrameSize(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height);\n if (this.opts.autoSizeStage && !this.opts.movableFrame) this.syncStageSize();\n\n if (this.opts.movableFrame) {\n const framePosition = clampOffset(this.state.framePosition, this.fixedImageSize, this.getFrameSize());\n this.commitState({ ...this.state, framePosition });\n return;\n }\n\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.getEffectiveStateForCrop(),\n this.getFrameSize(),\n this.opts.frame.shape,\n this.opts.frame.cornerRadius,\n this.stage.sharpenFilterId,\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.getEffectiveStateForCrop());\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 /**\n * `computeCropRegion()`/`renderCropToCanvas()` only know one model: an\n * image shifted by `offset` under a frame fixed at center, scaled by\n * `computeCoverScale(natural, frame, rotation) * zoom` — where `frame` is\n * whatever's passed in, i.e. the *current* frame size. In `movableFrame`\n * mode neither of those assumptions holds as-is: it's the frame that\n * shifts (by `framePosition`, image fixed at center), and the image's\n * scale must stay pinned to `fixedImageSize` regardless of how the frame\n * has since been resized — not recomputed from the current frame size,\n * which is exactly what those functions would otherwise do internally.\n *\n * Both are fixed by substituting two `state` fields before calling them,\n * rather than changing either function:\n * - `offset = -framePosition` — \"frame moved right by d\" and \"image moved\n * left by d\" describe the identical relative geometry, so this alone\n * carries the frame's position into the existing math unmodified.\n * (`state.offset` is always `{0,0}` in this mode already, since\n * `setOffset()`/gesture panning are no-ops here — this substitution is\n * what actually does the work, not a redundant no-op.)\n * - `zoom` = a value chosen so that\n * `computeCoverScale(natural, currentFrame, rotation) * zoom` evaluates\n * to exactly `fixedImageSize`'s own scale, whatever the current frame\n * size is — i.e. `zoom = frozenScale / computeCoverScale(natural,\n * currentFrame, rotation)`, which cancels the current-frame-size term\n * out algebraically. Caught by an actual pixel test (a moved+resized\n * frame exported as fully transparent) before trusting the offset\n * substitution was sufficient on its own — it wasn't.\n */\n private getEffectiveStateForCrop(): KiriState {\n if (!this.opts.movableFrame) return this.state;\n const frozenScale =\n this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, this.state.rotation).width;\n const currentCoverScale = computeCoverScale(this.naturalSize, this.getFrameSize(), this.state.rotation);\n return {\n ...this.state,\n offset: { x: -this.state.framePosition.x, y: -this.state.framePosition.y },\n zoom: frozenScale / currentCoverScale,\n };\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 // movableFrame's stage-sizing counterpart to syncStageSize() above: pins\n // the stage to the fixed image's own size (so the whole image stays\n // visible/on-screen) instead of the frame's, which may since have been\n // resized independently. Called after load() and rotate() establish/\n // recompute fixedImageSize — never from setFrameSize(), which is the\n // entire point of this mode.\n private syncStageSizeToFixedImage(): void {\n setStageSize(\n this.stage.stageEl,\n this.fixedImageSize.width + STAGE_AUTO_SIZE_PADDING * 2,\n this.fixedImageSize.height + STAGE_AUTO_SIZE_PADDING * 2\n );\n }\n\n private commitState(next: KiriState): void {\n this.state = next;\n // In movableFrame mode the image's rendered scale must stay pinned to\n // fixedImageSize (frozen at load()/rotate()) regardless of the frame's\n // current size — otherwise resizing the frame past the point where the\n // *other* axis becomes the covering-scale's limiting dimension silently\n // changes computeCoverScale()'s result and the image visibly zooms,\n // even though `zoom` itself never left 1 (setZoom() is a no-op here).\n // Caught live in the browser: growing/shrinking the frame with\n // resizableFrame + movableFrame both on visibly resized the image.\n const scale = this.opts.movableFrame\n ? this.fixedImageSize.width / effectiveNaturalSize(this.naturalSize, next.rotation).width\n : computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;\n applyTransform(this.stage.imageLayerEl, next, scale);\n applyFilters(this.stage.imgEl, next.filters, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);\n if (this.opts.movableFrame) applyFramePosition(this.stage.frameEl, next.framePosition);\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":["DEFAULT_FILTERS","mergeFilters","current","partial","sharpenKernelMatrix","sharpness","weight","center","neighbor","buildFilterString","filters","sharpenFilterId","parts","SVG_NS","sharpenFilterCounter","createSharpenFilter","filterId","svg","filterEl","kernelEl","createStage","container","frameShape","frameWidth","frameHeight","cornerRadius","movableFrame","zoomer","stageEl","imageLayerEl","imgEl","frameEl","setFrameSize","sharpenSvg","sharpenKernelEl","zoomerEl","rootEl","width","height","STAGE_AUTO_SIZE_PADDING","setStageSize","applyTransform","state","renderedScale","scaleX","scaleY","applyFilters","applyFramePosition","framePosition","effectiveNaturalSize","natural","rotationDeg","computeCoverScale","frame","eff","effectiveRenderedSize","zoom","scale","clampZoom","minZoom","maxZoom","clampOffset","offset","rendered","maxX","maxY","normalizeRotation","deg","KEYBOARD_PAN_STEP","KEYBOARD_ZOOM_STEP","pointerDistance","a","b","attachGestures","callbacks","options","activePointers","dragStart","pinchStartDistance","pinchStartZoom","applyClampedState","next","min","max","candidate","onPointerDown","e","onPointerMove","distance","ratio","dx","dy","onPointerUp","onWheel","delta","onKeyDown","readExifOrientation","buffer","view","marker","size","exifOffset","readOrientationFromTiff","tiffStart","littleEndian","firstIfdOffset","ifdStart","entryCount","i","entryOffset","value","orientationToTransform","orientation","resolveEnumOption","valid","fallback","optionName","clampNum","VALID_EXPORT_TYPES","VALID_EXPORT_FORMATS","computeFrameSourceRect","computeCropRegion","frameLeft","frameTop","renderedCenter","naturalCenter","rotation","corners","x","y","cx","cy","u","v","xs","c","ys","left","top","right","bottom","renderCropToCanvas","img","outputWidth","outputHeight","sourceCanvas","sctx","outCanvas","octx","radius","exportCrop","type","format","quality","canvas","resolve","reject","blob","uploadBlob","fieldName","fileName","extensionFor","formData","key","DEFAULT_FRAME_SIZE","MIN_FRAME_SIZE","DEFAULT_CORNER_RADIUS","VALID_FRAME_SHAPES","VALID_ZOOMER_POSITIONS","resolveMouseWheelZoom","Kiri","__publicField","_a","_b","_c","_d","source","loadOptions","flipHorizontal","flipVertical","objectUrl","url","exifTransform","clamped","position","deltaDeg","snapped","frozenScale","newEffNatural","flip","setStageFrameSize","event","callback","cb","currentCoverScale","onInput","cleanups","corner","handle","start","onDown","onMove","rawWidth","rawHeight","aspect","onUp","cleanup"],"mappings":"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"}
|