@michaelyagi/kiri 0.1.0-alpha.7 → 0.1.0-alpha.9
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 +6 -5
- package/dist/export.d.ts +2 -2
- package/dist/filters.d.ts +36 -2
- package/dist/kiri.css +3 -0
- package/dist/kiri.d.ts +4 -3
- package/dist/kiri.js +83 -23
- package/dist/kiri.js.map +1 -1
- package/dist/kiri.min.js +1 -1
- package/dist/kiri.min.js.map +1 -1
- package/dist/kiri.mjs +308 -269
- package/dist/kiri.mjs.map +1 -1
- package/dist/stage.d.ts +5 -2
- package/dist/types.d.ts +13 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,11 +9,12 @@ A dependency-free TypeScript library for interactive image cropping in the brows
|
|
|
9
9
|
- **Drag, zoom, rotate, flip** — wheel/trackpad pinch zoom, an optional
|
|
10
10
|
built-in zoom slider (`showZoomer`, placeable on any of the four sides),
|
|
11
11
|
90° rotation, independent horizontal/vertical flip
|
|
12
|
-
- **Rectangle or
|
|
13
|
-
corners on PNG/WebP), not just a
|
|
12
|
+
- **Rectangle, circle, or rounded-rectangle frame** — circle/rounded-rectangle
|
|
13
|
+
are real clips on export (transparent corners on PNG/WebP), not just a
|
|
14
|
+
visual overlay
|
|
14
15
|
- **Zero-CSS sizing** — the stage auto-sizes itself to the frame's dimensions
|
|
15
|
-
- **Filters** — brightness/contrast/saturation/grayscale/sepia,
|
|
16
|
-
identically to the live preview and the export
|
|
16
|
+
- **Filters** — brightness/contrast/saturation/sharpness/grayscale/sepia,
|
|
17
|
+
applied identically to the live preview and the export
|
|
17
18
|
- **Automatic EXIF orientation correction** on load
|
|
18
19
|
- **Resizable frame**, four independent corner handles, with an optional
|
|
19
20
|
`lockAspectRatio` to keep a fixed ratio while resizing
|
|
@@ -78,7 +79,7 @@ an interactive playground, and the full API reference.
|
|
|
78
79
|
|
|
79
80
|
## Status
|
|
80
81
|
|
|
81
|
-
**`0.1.0-alpha.
|
|
82
|
+
**`0.1.0-alpha.9`** — see [CHANGELOG.md](./CHANGELOG.md)
|
|
82
83
|
for what's in it. Core publishes to npm automatically on version tags via
|
|
83
84
|
GitHub Actions; `kiri-react`/`kiri-vue` aren't published yet. The public API
|
|
84
85
|
is expected to be mostly stable but may still change before a `0.1.0`
|
package/dist/export.d.ts
CHANGED
|
@@ -23,5 +23,5 @@ export declare function computeFrameSourceRect(rendered: Size, offset: Offset, f
|
|
|
23
23
|
* carried through untouched in the result for the caller to apply.
|
|
24
24
|
*/
|
|
25
25
|
export declare function computeCropRegion(natural: Size, frame: Size, state: KiriState): CropRegion;
|
|
26
|
-
export declare function renderCropToCanvas(img: HTMLImageElement, state: KiriState, frame: Size, outputWidth: number, outputHeight: number, frameShape: FrameShape): HTMLCanvasElement;
|
|
27
|
-
export declare function exportCrop(img: HTMLImageElement, state: KiriState, frame: Size, frameShape: FrameShape, options: ExportOptions): Promise<ExportResult>;
|
|
26
|
+
export declare function renderCropToCanvas(img: HTMLImageElement, state: KiriState, frame: Size, outputWidth: number, outputHeight: number, frameShape: FrameShape, cornerRadius: number, sharpenFilterId: string): HTMLCanvasElement;
|
|
27
|
+
export declare function exportCrop(img: HTMLImageElement, state: KiriState, frame: Size, frameShape: FrameShape, cornerRadius: number, sharpenFilterId: string, options: ExportOptions): Promise<ExportResult>;
|
package/dist/filters.d.ts
CHANGED
|
@@ -2,10 +2,44 @@ import { Filters } from './types';
|
|
|
2
2
|
export declare const DEFAULT_FILTERS: Filters;
|
|
3
3
|
/** Merges a partial filter update into the current filters, clamping numeric values to >= 0. */
|
|
4
4
|
export declare function mergeFilters(current: Filters, partial: Partial<Filters>): Filters;
|
|
5
|
+
/**
|
|
6
|
+
* The 3x3 unsharp-mask kernel for `sharpness`, as a `feConvolveMatrix`
|
|
7
|
+
* `kernelMatrix` attribute value (row-major, space-separated):
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* 0 -k 0
|
|
11
|
+
* -k 1+4k -k
|
|
12
|
+
* 0 -k 0
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The matrix always sums to `1`, so the convolution needs no separate
|
|
16
|
+
* `divisor`. `sharpness <= 1` collapses to the identity kernel (center `1`,
|
|
17
|
+
* neighbors `0`) — no blur/sharpen either way, matching the `1` = unchanged
|
|
18
|
+
* convention used by the other numeric filters. `4.5` is chosen so a
|
|
19
|
+
* `sharpness` of `2`–`3` (a reasonable slider range) lands in a visually
|
|
20
|
+
* subtle-to-strong sharpening range, not an immediately harsh one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sharpenKernelMatrix(sharpness: number): string;
|
|
5
23
|
/**
|
|
6
24
|
* Builds a CSS `filter` value from the current filter state. Used verbatim as
|
|
7
25
|
* both the live-preview `img.style.filter` and a canvas 2D context's
|
|
8
26
|
* `ctx.filter` before drawing, so preview and export always match exactly —
|
|
9
|
-
* no separately hand-rolled brightness/contrast/saturation pixel
|
|
27
|
+
* no separately hand-rolled brightness/contrast/saturation/sharpness pixel
|
|
28
|
+
* math. Brightness/contrast/saturation/grayscale/sepia are native CSS filter
|
|
29
|
+
* functions; CSS has no `sharpen()`, so `sharpness` instead references an
|
|
30
|
+
* SVG `feConvolveMatrix` filter (by `sharpenFilterId`) that `stage.ts` keeps
|
|
31
|
+
* in sync with the current `sharpness` value — `ctx.filter` and
|
|
32
|
+
* `img.style.filter` both support `url(#id)` references to it identically.
|
|
33
|
+
*
|
|
34
|
+
* The `url(#id)` reference goes **first**, before the native CSS filter
|
|
35
|
+
* functions — not last, despite sharpening-as-a-final-step being the more
|
|
36
|
+
* common photo-editing order. Verified directly in a real browser (Chromium)
|
|
37
|
+
* that a `url(#id)` SVG filter reference placed *after* one or more native
|
|
38
|
+
* CSS filter functions in the same chain silently produces a fully blank
|
|
39
|
+
* (transparent) result, while the identical chain with `url(#id)` first
|
|
40
|
+
* renders correctly. Sharpening a source that hasn't had brightness/contrast/
|
|
41
|
+
* saturation applied yet is a legitimate (if less common) ordering rather
|
|
42
|
+
* than a compromise — but the ordering here is load-bearing either way, not
|
|
43
|
+
* arbitrary; don't reorder this without re-verifying in a real browser.
|
|
10
44
|
*/
|
|
11
|
-
export declare function buildFilterString(filters: Filters): string;
|
|
45
|
+
export declare function buildFilterString(filters: Filters, sharpenFilterId: string): string;
|
package/dist/kiri.css
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
.kiri-frame--circle {
|
|
35
35
|
border-radius: 50%;
|
|
36
36
|
}
|
|
37
|
+
/* .kiri-frame--rounded-rectangle's border-radius is a per-instance pixel
|
|
38
|
+
value (frame.cornerRadius), so it's set inline by stage.ts instead of
|
|
39
|
+
here — this class exists as a CSS hook for consumers, not styling. */
|
|
37
40
|
.kiri-frame-handle {
|
|
38
41
|
position: absolute;
|
|
39
42
|
width: 10px;
|
package/dist/kiri.d.ts
CHANGED
|
@@ -69,9 +69,10 @@ export declare class Kiri {
|
|
|
69
69
|
*/
|
|
70
70
|
setFilters(filters: Partial<Filters>): void;
|
|
71
71
|
/**
|
|
72
|
-
* Renders the current crop. A circle frame is a real
|
|
73
|
-
* (transparent corners on PNG/WebP);
|
|
74
|
-
* renders solid black corners instead, since
|
|
72
|
+
* Renders the current crop. A circle or rounded-rectangle frame is a real
|
|
73
|
+
* clip in the output (transparent corners on PNG/WebP); either shape
|
|
74
|
+
* exported as JPEG warns and renders solid black corners instead, since
|
|
75
|
+
* JPEG has no alpha channel.
|
|
75
76
|
* @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
|
|
76
77
|
*/
|
|
77
78
|
export(options?: ExportOptions): Promise<ExportResult>;
|
package/dist/kiri.js
CHANGED
|
@@ -9,6 +9,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
9
9
|
brightness: 1,
|
|
10
10
|
contrast: 1,
|
|
11
11
|
saturation: 1,
|
|
12
|
+
sharpness: 1,
|
|
12
13
|
grayscale: false,
|
|
13
14
|
sepia: false
|
|
14
15
|
};
|
|
@@ -17,21 +18,52 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
17
18
|
brightness: Math.max(0, partial.brightness ?? current.brightness),
|
|
18
19
|
contrast: Math.max(0, partial.contrast ?? current.contrast),
|
|
19
20
|
saturation: Math.max(0, partial.saturation ?? current.saturation),
|
|
21
|
+
sharpness: Math.max(0, partial.sharpness ?? current.sharpness),
|
|
20
22
|
grayscale: partial.grayscale ?? current.grayscale,
|
|
21
23
|
sepia: partial.sepia ?? current.sepia
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
+
function sharpenKernelMatrix(sharpness) {
|
|
27
|
+
const weight = sharpness > 1 ? (sharpness - 1) / 4.5 : 0;
|
|
28
|
+
const center = 1 + 4 * weight;
|
|
29
|
+
const neighbor = -weight;
|
|
30
|
+
return `0 ${neighbor} 0 ${neighbor} ${center} ${neighbor} 0 ${neighbor} 0`;
|
|
31
|
+
}
|
|
32
|
+
function buildFilterString(filters, sharpenFilterId) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (filters.sharpness > 1) parts.push(`url(#${sharpenFilterId})`);
|
|
35
|
+
parts.push(
|
|
26
36
|
`brightness(${filters.brightness})`,
|
|
27
37
|
`contrast(${filters.contrast})`,
|
|
28
38
|
`saturate(${filters.saturation})`
|
|
29
|
-
|
|
39
|
+
);
|
|
30
40
|
if (filters.grayscale) parts.push("grayscale(1)");
|
|
31
41
|
if (filters.sepia) parts.push("sepia(1)");
|
|
32
42
|
return parts.join(" ");
|
|
33
43
|
}
|
|
34
|
-
|
|
44
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
45
|
+
let sharpenFilterCounter = 0;
|
|
46
|
+
function createSharpenFilter() {
|
|
47
|
+
sharpenFilterCounter += 1;
|
|
48
|
+
const filterId = `kiri-sharpen-${sharpenFilterCounter}`;
|
|
49
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
50
|
+
svg.setAttribute("width", "0");
|
|
51
|
+
svg.setAttribute("height", "0");
|
|
52
|
+
svg.style.position = "absolute";
|
|
53
|
+
const filterEl = document.createElementNS(SVG_NS, "filter");
|
|
54
|
+
filterEl.setAttribute("id", filterId);
|
|
55
|
+
filterEl.setAttribute("color-interpolation-filters", "sRGB");
|
|
56
|
+
const kernelEl = document.createElementNS(SVG_NS, "feConvolveMatrix");
|
|
57
|
+
kernelEl.setAttribute("order", "3");
|
|
58
|
+
kernelEl.setAttribute("divisor", "1");
|
|
59
|
+
kernelEl.setAttribute("edgeMode", "duplicate");
|
|
60
|
+
kernelEl.setAttribute("preserveAlpha", "true");
|
|
61
|
+
kernelEl.setAttribute("kernelMatrix", sharpenKernelMatrix(1));
|
|
62
|
+
filterEl.appendChild(kernelEl);
|
|
63
|
+
svg.appendChild(filterEl);
|
|
64
|
+
return { svg, filterId, kernelEl };
|
|
65
|
+
}
|
|
66
|
+
function createStage(container, frameShape, frameWidth, frameHeight, cornerRadius, zoomer) {
|
|
35
67
|
container.innerHTML = "";
|
|
36
68
|
const stageEl = document.createElement("div");
|
|
37
69
|
stageEl.className = "kiri-stage";
|
|
@@ -47,10 +79,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
47
79
|
imgEl.draggable = false;
|
|
48
80
|
imageLayerEl.appendChild(imgEl);
|
|
49
81
|
const frameEl = document.createElement("div");
|
|
50
|
-
frameEl.className = frameShape === "circle" ? "kiri-frame kiri-frame--circle" : "kiri-frame";
|
|
82
|
+
frameEl.className = frameShape === "circle" ? "kiri-frame kiri-frame--circle" : frameShape === "rounded-rectangle" ? "kiri-frame kiri-frame--rounded-rectangle" : "kiri-frame";
|
|
51
83
|
setFrameSize(frameEl, frameWidth, frameHeight);
|
|
84
|
+
if (frameShape === "rounded-rectangle") {
|
|
85
|
+
frameEl.style.borderRadius = `${cornerRadius}px`;
|
|
86
|
+
}
|
|
87
|
+
const { svg: sharpenSvg, filterId: sharpenFilterId, kernelEl: sharpenKernelEl } = createSharpenFilter();
|
|
52
88
|
stageEl.appendChild(imageLayerEl);
|
|
53
89
|
stageEl.appendChild(frameEl);
|
|
90
|
+
stageEl.appendChild(sharpenSvg);
|
|
54
91
|
let zoomerEl = null;
|
|
55
92
|
if (zoomer.show) {
|
|
56
93
|
const rootEl = document.createElement("div");
|
|
@@ -68,7 +105,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
68
105
|
} else {
|
|
69
106
|
container.appendChild(stageEl);
|
|
70
107
|
}
|
|
71
|
-
return { stageEl, frameEl, imageLayerEl, imgEl, zoomerEl };
|
|
108
|
+
return { stageEl, frameEl, imageLayerEl, imgEl, zoomerEl, sharpenFilterId, sharpenKernelEl };
|
|
72
109
|
}
|
|
73
110
|
function setFrameSize(frameEl, width, height) {
|
|
74
111
|
frameEl.style.width = `${width}px`;
|
|
@@ -84,8 +121,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
84
121
|
const scaleY = renderedScale * (state.flip.vertical ? -1 : 1);
|
|
85
122
|
imageLayerEl.style.transform = `translate(-50%, -50%) translate(${state.offset.x}px, ${state.offset.y}px) rotate(${state.rotation}deg) scale(${scaleX}, ${scaleY})`;
|
|
86
123
|
}
|
|
87
|
-
function applyFilters(imgEl, filters) {
|
|
88
|
-
|
|
124
|
+
function applyFilters(imgEl, filters, sharpenKernelEl, sharpenFilterId) {
|
|
125
|
+
sharpenKernelEl.setAttribute("kernelMatrix", sharpenKernelMatrix(filters.sharpness));
|
|
126
|
+
imgEl.style.filter = buildFilterString(filters, sharpenFilterId);
|
|
89
127
|
}
|
|
90
128
|
function effectiveNaturalSize(natural, rotationDeg) {
|
|
91
129
|
const swapped = (rotationDeg / 90 % 2 + 2) % 2 === 1;
|
|
@@ -368,7 +406,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
368
406
|
flip: { ...state.flip }
|
|
369
407
|
};
|
|
370
408
|
}
|
|
371
|
-
function renderCropToCanvas(img, state, frame, outputWidth, outputHeight, frameShape) {
|
|
409
|
+
function renderCropToCanvas(img, state, frame, outputWidth, outputHeight, frameShape, cornerRadius, sharpenFilterId) {
|
|
372
410
|
const natural = { width: img.naturalWidth, height: img.naturalHeight };
|
|
373
411
|
const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;
|
|
374
412
|
const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);
|
|
@@ -380,7 +418,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
380
418
|
sctx.translate(sourceCanvas.width / 2, sourceCanvas.height / 2);
|
|
381
419
|
sctx.rotate(state.rotation * Math.PI / 180);
|
|
382
420
|
sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));
|
|
383
|
-
sctx.filter = buildFilterString(state.filters);
|
|
421
|
+
sctx.filter = buildFilterString(state.filters, sharpenFilterId);
|
|
384
422
|
sctx.drawImage(img, -natural.width / 2, -natural.height / 2, natural.width, natural.height);
|
|
385
423
|
const { left: frameLeft, top: frameTop } = computeFrameSourceRect(
|
|
386
424
|
rendered,
|
|
@@ -405,6 +443,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
405
443
|
Math.PI * 2
|
|
406
444
|
);
|
|
407
445
|
octx.clip();
|
|
446
|
+
} else if (frameShape === "rounded-rectangle") {
|
|
447
|
+
const scale2 = outputWidth / frame.width;
|
|
448
|
+
const radius = clampNum(cornerRadius * scale2, 0, Math.min(outputWidth, outputHeight) / 2);
|
|
449
|
+
octx.save();
|
|
450
|
+
octx.beginPath();
|
|
451
|
+
octx.roundRect(0, 0, outputWidth, outputHeight, radius);
|
|
452
|
+
octx.clip();
|
|
408
453
|
}
|
|
409
454
|
octx.drawImage(
|
|
410
455
|
sourceCanvas,
|
|
@@ -417,21 +462,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
417
462
|
outputWidth,
|
|
418
463
|
outputHeight
|
|
419
464
|
);
|
|
420
|
-
if (frameShape === "circle") octx.restore();
|
|
465
|
+
if (frameShape === "circle" || frameShape === "rounded-rectangle") octx.restore();
|
|
421
466
|
return outCanvas;
|
|
422
467
|
}
|
|
423
|
-
async function exportCrop(img, state, frame, frameShape, options) {
|
|
468
|
+
async function exportCrop(img, state, frame, frameShape, cornerRadius, sharpenFilterId, options) {
|
|
424
469
|
const type = resolveEnumOption(options.type, VALID_EXPORT_TYPES, "base64", "export type");
|
|
425
470
|
const format = resolveEnumOption(options.format, VALID_EXPORT_FORMATS, "image/png", "export format");
|
|
426
471
|
const quality = options.quality;
|
|
427
472
|
const width = options.width ?? frame.width;
|
|
428
473
|
const height = options.height ?? frame.height;
|
|
429
|
-
if (frameShape === "circle" && format === "image/jpeg") {
|
|
474
|
+
if ((frameShape === "circle" || frameShape === "rounded-rectangle") && format === "image/jpeg") {
|
|
430
475
|
console.warn(
|
|
431
|
-
|
|
476
|
+
`Kiri: exporting a ${frameShape}-shaped frame as image/jpeg — JPEG has no alpha channel, so the area outside the shape will render as solid black instead of transparent. Use image/png or image/webp instead.`
|
|
432
477
|
);
|
|
433
478
|
}
|
|
434
|
-
const canvas = renderCropToCanvas(
|
|
479
|
+
const canvas = renderCropToCanvas(
|
|
480
|
+
img,
|
|
481
|
+
state,
|
|
482
|
+
frame,
|
|
483
|
+
width,
|
|
484
|
+
height,
|
|
485
|
+
frameShape,
|
|
486
|
+
cornerRadius,
|
|
487
|
+
sharpenFilterId
|
|
488
|
+
);
|
|
435
489
|
if (type === "canvas") return canvas;
|
|
436
490
|
if (type === "base64") return canvas.toDataURL(format, quality);
|
|
437
491
|
return new Promise((resolve, reject) => {
|
|
@@ -468,7 +522,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
468
522
|
}
|
|
469
523
|
const DEFAULT_FRAME_SIZE = 200;
|
|
470
524
|
const MIN_FRAME_SIZE = 20;
|
|
471
|
-
const
|
|
525
|
+
const DEFAULT_CORNER_RADIUS = 20;
|
|
526
|
+
const VALID_FRAME_SHAPES = ["rectangle", "circle", "rounded-rectangle"];
|
|
472
527
|
const VALID_ZOOMER_POSITIONS = ["top", "bottom", "left", "right"];
|
|
473
528
|
function resolveMouseWheelZoom(value) {
|
|
474
529
|
if (value === void 0) return true;
|
|
@@ -502,7 +557,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
502
557
|
/** Snapshot taken right after `load()` resolves, so `reset()` has something to revert to. */
|
|
503
558
|
__publicField(this, "initialState", null);
|
|
504
559
|
__publicField(this, "listeners", { change: [] });
|
|
505
|
-
var _a, _b, _c;
|
|
560
|
+
var _a, _b, _c, _d;
|
|
506
561
|
if (!container || typeof container.appendChild !== "function") {
|
|
507
562
|
throw new Error(
|
|
508
563
|
"Kiri: container element is null/undefined or not a DOM element. This usually means the element wasn't in the DOM yet when `new Kiri(...)` ran — e.g. document.getElementById() was called before the element existed. Place the <script> after the element, or construct inside a DOMContentLoaded listener."
|
|
@@ -518,7 +573,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
518
573
|
"frame.shape"
|
|
519
574
|
),
|
|
520
575
|
width: ((_b = options.frame) == null ? void 0 : _b.width) ?? DEFAULT_FRAME_SIZE,
|
|
521
|
-
height: ((_c = options.frame) == null ? void 0 : _c.height) ?? DEFAULT_FRAME_SIZE
|
|
576
|
+
height: ((_c = options.frame) == null ? void 0 : _c.height) ?? DEFAULT_FRAME_SIZE,
|
|
577
|
+
cornerRadius: ((_d = options.frame) == null ? void 0 : _d.cornerRadius) ?? DEFAULT_CORNER_RADIUS
|
|
522
578
|
},
|
|
523
579
|
minZoom: options.minZoom ?? 1,
|
|
524
580
|
maxZoom: options.maxZoom ?? 4,
|
|
@@ -544,6 +600,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
544
600
|
this.opts.frame.shape,
|
|
545
601
|
this.opts.frame.width,
|
|
546
602
|
this.opts.frame.height,
|
|
603
|
+
this.opts.frame.cornerRadius,
|
|
547
604
|
{
|
|
548
605
|
show: this.opts.showZoomer,
|
|
549
606
|
position: this.opts.zoomerPosition,
|
|
@@ -553,7 +610,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
553
610
|
}
|
|
554
611
|
);
|
|
555
612
|
if (this.opts.autoSizeStage) this.syncStageSize();
|
|
556
|
-
applyFilters(this.stage.imgEl, this.state.filters);
|
|
613
|
+
applyFilters(this.stage.imgEl, this.state.filters, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);
|
|
557
614
|
if (this.stage.zoomerEl) this.enableZoomer(this.stage.zoomerEl);
|
|
558
615
|
this.gestureHandle = attachGestures(
|
|
559
616
|
this.stage.stageEl,
|
|
@@ -735,9 +792,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
735
792
|
this.commitState({ ...this.state, filters: mergeFilters(this.state.filters, filters) });
|
|
736
793
|
}
|
|
737
794
|
/**
|
|
738
|
-
* Renders the current crop. A circle frame is a real
|
|
739
|
-
* (transparent corners on PNG/WebP);
|
|
740
|
-
* renders solid black corners instead, since
|
|
795
|
+
* Renders the current crop. A circle or rounded-rectangle frame is a real
|
|
796
|
+
* clip in the output (transparent corners on PNG/WebP); either shape
|
|
797
|
+
* exported as JPEG warns and renders solid black corners instead, since
|
|
798
|
+
* JPEG has no alpha channel.
|
|
741
799
|
* @returns A data URL string (`type: "base64"`, the default), a `Blob`, or an `HTMLCanvasElement`.
|
|
742
800
|
*/
|
|
743
801
|
async export(options = {}) {
|
|
@@ -746,6 +804,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
746
804
|
this.state,
|
|
747
805
|
this.getFrameSize(),
|
|
748
806
|
this.opts.frame.shape,
|
|
807
|
+
this.opts.frame.cornerRadius,
|
|
808
|
+
this.stage.sharpenFilterId,
|
|
749
809
|
options
|
|
750
810
|
);
|
|
751
811
|
}
|
|
@@ -806,7 +866,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
806
866
|
this.state = next;
|
|
807
867
|
const scale = computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;
|
|
808
868
|
applyTransform(this.stage.imageLayerEl, next, scale);
|
|
809
|
-
applyFilters(this.stage.imgEl, next.filters);
|
|
869
|
+
applyFilters(this.stage.imgEl, next.filters, this.stage.sharpenKernelEl, this.stage.sharpenFilterId);
|
|
810
870
|
if (this.stage.zoomerEl) this.stage.zoomerEl.value = String(next.zoom);
|
|
811
871
|
for (const cb of this.listeners.change) cb(this.getState());
|
|
812
872
|
}
|