@streamscloud/kit 0.1.10 → 0.1.11

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.
@@ -76,7 +76,7 @@ $effect(() => untrack(() => {
76
76
  .image-editor-dialog__body {
77
77
  flex: 1;
78
78
  position: relative;
79
- background: #DADADA;
79
+ background: #dadada;
80
80
  overflow: hidden;
81
81
  }
82
82
  .image-editor-dialog__cropper-wrapper {
@@ -9,9 +9,9 @@ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types'
9
9
  *
10
10
  * ### Aspect ratio
11
11
  * - `number` — fixed ratio, no dropdown.
12
- * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
- * - `{ initial?, supported, allowFreeCrop? }` (contain) dropdown with optional free crop.
14
- * - Omitted — free crop in contain, full canvas in cover.
12
+ * - `ImgCropperDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
13
+ * adds a free-ratio option; `initial` is optional when free crop is enabled.
14
+ * - Omitted — free crop (no ratio constraint on the selection).
15
15
  *
16
16
  * ### Result
17
17
  * - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
@@ -9,9 +9,9 @@ import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
9
9
  *
10
10
  * ### Aspect ratio
11
11
  * - `number` — fixed ratio, no dropdown.
12
- * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
- * - `{ initial?, supported, allowFreeCrop? }` (contain) dropdown with optional free crop.
14
- * - Omitted — free crop in contain, full canvas in cover.
12
+ * - `ImgCropperDynamicAspectRatio` — dropdown with supported ratios. `allowFreeCrop: true`
13
+ * adds a free-ratio option; `initial` is optional when free crop is enabled.
14
+ * - Omitted — free crop (no ratio constraint on the selection).
15
15
  *
16
16
  * ### Result
17
17
  * - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
@@ -1,18 +1,11 @@
1
1
  import type { FileWithBlobUrl } from '../../../core/files';
2
- import type { ImgCropperContainAspectRatio, ImgCropperCoverAspectRatio } from '../img-cropper';
3
- export type ImageEditorDialogOptions = ImageEditorDialogCoverOptions | ImageEditorDialogContainOptions;
4
- type ImageEditorDialogOptionsBase = {
2
+ import type { CropperMode, ImgCropperDynamicAspectRatio } from '../img-cropper';
3
+ export type ImageEditorDialogOptions = {
5
4
  url: string;
6
5
  fillColor?: string;
7
6
  showImageShadow?: boolean;
8
- };
9
- type ImageEditorDialogCoverOptions = ImageEditorDialogOptionsBase & {
10
- mode?: 'cover';
11
- aspectRatio?: number | ImgCropperCoverAspectRatio;
12
- };
13
- type ImageEditorDialogContainOptions = ImageEditorDialogOptionsBase & {
14
- mode: 'contain';
15
- aspectRatio?: number | ImgCropperContainAspectRatio;
7
+ mode?: CropperMode;
8
+ aspectRatio?: number | ImgCropperDynamicAspectRatio;
16
9
  };
17
10
  export type CroppedFile = FileWithBlobUrl & {
18
11
  width: number;
@@ -22,4 +15,3 @@ export type ImageEditorDialogResult = {
22
15
  croppedFile: CroppedFile;
23
16
  selectedRatio: number | null;
24
17
  };
25
- export {};
@@ -38,16 +38,24 @@ const initCanvas = (canvasEl) => {
38
38
  };
39
39
  void initCropper();
40
40
  }
41
- return { destroy: () => cropper.destroy() };
41
+ return {
42
+ destroy: () => {
43
+ cropper.destroy();
44
+ }
45
+ };
46
+ };
47
+ const observeResize = (el) => {
48
+ const observer = new ResizeObserver(() => {
49
+ if (cropper.ready) {
50
+ cropper.refit();
51
+ }
52
+ });
53
+ observer.observe(el);
54
+ return { destroy: () => observer.disconnect() };
42
55
  };
43
56
  </script>
44
57
 
45
- <div
46
- class="img-cropper"
47
- style:--img-cropper--fill-color={cropper.fillColor || undefined}
48
- style:--img-cropper--ratio={cropper.effectiveCanvasRatio ?? undefined}
49
- style:--img-cropper--natural-w={cropper.naturalWidth ? `${cropper.naturalWidth}px` : undefined}
50
- style:--img-cropper--natural-h={cropper.naturalHeight ? `${cropper.naturalHeight}px` : undefined}>
58
+ <div class="img-cropper" use:observeResize style:--img-cropper--fill-color={cropper.fillColor || undefined}>
51
59
  {#if cropper.loading || !cropper.ready}
52
60
  <Loading positionAbsoluteCenter={true} />
53
61
  {/if}
@@ -56,8 +64,9 @@ const initCanvas = (canvasEl) => {
56
64
  <cropper-canvas
57
65
  class="img-cropper__canvas"
58
66
  class:img-cropper__canvas--visible={cropper.ready}
59
- class:img-cropper__canvas--fitted={cropper.effectiveCanvasRatio !== null && cropper.naturalWidth > 0}
60
67
  class:img-cropper__canvas--shadow={cropper.showImageShadow}
68
+ style:width={cropper.canvasGeometry.width ? `${cropper.canvasGeometry.width}px` : undefined}
69
+ style:height={cropper.canvasGeometry.height ? `${cropper.canvasGeometry.height}px` : undefined}
61
70
  use:initCanvas
62
71
  background={(cropper.showFillColor && cropper.isTransparentFill) || undefined}>
63
72
  <cropper-image crossorigin={cropper.corsMode === 'native' ? 'anonymous' : null} initial-center-size={cropper.mode} scalable translatable rotatable>
@@ -79,8 +88,8 @@ const initCanvas = (canvasEl) => {
79
88
  </cropper-selection>
80
89
  </cropper-canvas>
81
90
 
82
- {#if cropper.showImageShadow && cropper.ready}
83
- <div class="img-cropper__shadow" class:img-cropper__shadow--full={cropper.effectiveCanvasRatio === null}></div>
91
+ {#if cropper.showImageShadow && cropper.ready && cropper.canvasGeometry.width > 0}
92
+ <div class="img-cropper__shadow" style:width="{cropper.canvasGeometry.width}px" style:height="{cropper.canvasGeometry.height}px"></div>
84
93
  {/if}
85
94
  {/if}
86
95
 
@@ -116,20 +125,18 @@ The behavior is determined by the `ImgCropper` instance passed via the `cropper`
116
125
  Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
117
126
 
118
127
  - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
119
- - **Dynamic object** — provides a list of `supported` ratios and an `initial` value.
120
- The toolbar renders a ratio dropdown when multiple options are available.
121
- - In `contain` mode, `allowFreeCrop: true` adds a free-ratio option that lets
122
- the user draw an unconstrained selection. After crop, the canvas dynamically
123
- adopts the cropped area's proportions.
124
- - In `cover` mode, `initial` is required and free crop is not available.
125
- - **Omitted** — defaults to free crop in `contain` mode, or fills the canvas in `cover` mode.
128
+ - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
129
+ ratios and an optional `initial` value. The toolbar renders a ratio dropdown when
130
+ multiple options are available. `allowFreeCrop: true` adds a free-ratio option
131
+ that lets the user draw an unconstrained selection.
132
+ - **Omitted** defaults to free crop (no ratio constraint on the selection).
126
133
 
127
134
  #### Shadow overlay
128
135
 
129
136
  When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
130
- overlay (`box-shadow: 0 0 0 9999px`) extends beyond the canvas bounds so the full
131
- image remains visible outside the crop area. In free-ratio mode (no fixed aspect
132
- ratio), the shadow covers the entire container instead.
137
+ `box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
138
+ the canvas bounds so the surrounding area is dimmed. The parent element should set
139
+ `overflow: hidden` to clip the shadow at its boundaries.
133
140
 
134
141
  ### Props
135
142
  | Prop | Type | Default | Description |
@@ -144,10 +151,6 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
144
151
 
145
152
  <style>.img-cropper {
146
153
  --_fill-color: var(--img-cropper--fill-color);
147
- --_ratio: var(--img-cropper--ratio);
148
- --_natural-w: var(--img-cropper--natural-w, 100cqw);
149
- --_natural-h: var(--img-cropper--natural-h, 100cqh);
150
- container-type: size;
151
154
  position: relative;
152
155
  width: 100%;
153
156
  height: 100%;
@@ -162,11 +165,6 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
162
165
  background-color: var(--_fill-color);
163
166
  visibility: hidden;
164
167
  }
165
- .img-cropper__canvas--fitted {
166
- width: min(100cqw, 100cqh * var(--_ratio), max(var(--_natural-w), var(--_natural-h) * var(--_ratio)));
167
- height: auto;
168
- aspect-ratio: var(--_ratio);
169
- }
170
168
  .img-cropper__canvas--visible {
171
169
  visibility: visible;
172
170
  }
@@ -178,17 +176,10 @@ None — canvas background, shadow, and sizing are controlled by the `ImgCropper
178
176
  top: 50%;
179
177
  left: 50%;
180
178
  transform: translate(-50%, -50%);
181
- width: min(100cqw, 100cqh * var(--_ratio), max(var(--_natural-w), var(--_natural-h) * var(--_ratio)));
182
- aspect-ratio: var(--_ratio);
183
179
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
184
180
  pointer-events: none;
185
181
  z-index: 2;
186
182
  }
187
- .img-cropper__shadow--full {
188
- width: 100cqw;
189
- height: 100cqh;
190
- aspect-ratio: auto;
191
- }
192
183
  .img-cropper__controls {
193
184
  position: absolute;
194
185
  bottom: 0;
@@ -28,20 +28,18 @@ type Props = {
28
28
  * Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
29
29
  *
30
30
  * - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
31
- * - **Dynamic object** — provides a list of `supported` ratios and an `initial` value.
32
- * The toolbar renders a ratio dropdown when multiple options are available.
33
- * - In `contain` mode, `allowFreeCrop: true` adds a free-ratio option that lets
34
- * the user draw an unconstrained selection. After crop, the canvas dynamically
35
- * adopts the cropped area's proportions.
36
- * - In `cover` mode, `initial` is required and free crop is not available.
37
- * - **Omitted** — defaults to free crop in `contain` mode, or fills the canvas in `cover` mode.
31
+ * - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
32
+ * ratios and an optional `initial` value. The toolbar renders a ratio dropdown when
33
+ * multiple options are available. `allowFreeCrop: true` adds a free-ratio option
34
+ * that lets the user draw an unconstrained selection.
35
+ * - **Omitted** defaults to free crop (no ratio constraint on the selection).
38
36
  *
39
37
  * #### Shadow overlay
40
38
  *
41
39
  * When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
42
- * overlay (`box-shadow: 0 0 0 9999px`) extends beyond the canvas bounds so the full
43
- * image remains visible outside the crop area. In free-ratio mode (no fixed aspect
44
- * ratio), the shadow covers the entire container instead.
40
+ * `box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
41
+ * the canvas bounds so the surrounding area is dimmed. The parent element should set
42
+ * `overflow: hidden` to clip the shadow at its boundaries.
45
43
  *
46
44
  * ### Props
47
45
  * | Prop | Type | Default | Description |
@@ -0,0 +1,43 @@
1
+ import type { CanvasGeometry, DragMode, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
2
+ import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
3
+ export declare abstract class ImgCropperBaseWorker implements ImgCropperWorker {
4
+ cropBoxVisible: boolean;
5
+ dragMode: DragMode;
6
+ canvasGeometry: CanvasGeometry;
7
+ destroy: () => void;
8
+ protected _image: CropperImage;
9
+ protected _selection: CropperSelection;
10
+ protected _canvas: CropperCanvas;
11
+ protected _selectHandle: HTMLElement | null;
12
+ protected _originalSrc: string;
13
+ protected _visualWidth: number;
14
+ protected _visualHeight: number;
15
+ protected abstract readonly _centerMode: 'contain' | 'cover';
16
+ constructor(params: ImgCropperWorkerParams);
17
+ ready: () => Promise<void>;
18
+ rotate: () => Promise<void>;
19
+ zoomIn: () => void;
20
+ zoomOut: () => void;
21
+ reset: () => Promise<void>;
22
+ crop: (options?: {
23
+ fillColor?: string;
24
+ }) => Promise<{
25
+ dataUrl: string;
26
+ width: number;
27
+ height: number;
28
+ }>;
29
+ save: (options?: {
30
+ fillColor?: string;
31
+ }) => Promise<{
32
+ dataUrl: string;
33
+ width: number;
34
+ height: number;
35
+ }>;
36
+ clearSelection: () => void;
37
+ refit: () => Promise<void>;
38
+ enableCropMode: () => void;
39
+ protected _wrapTransformOp: (fn: () => void) => void;
40
+ protected _applyMoveMode: () => void;
41
+ protected _fitImage: () => Promise<void>;
42
+ protected abstract _computeCanvasSize(): void;
43
+ }
@@ -0,0 +1,163 @@
1
+ import { ColorHelper } from '../../../core/utils';
2
+ import { computeImageScale, computeNaturalOutputSize, handleSelectionBoundary } from './img-cropper-utils';
3
+ import { tick } from 'svelte';
4
+ export class ImgCropperBaseWorker {
5
+ cropBoxVisible = $state(false);
6
+ dragMode = $state('move');
7
+ canvasGeometry = $state.raw({ aspectRatio: null, width: 0, height: 0 });
8
+ destroy;
9
+ _image;
10
+ _selection;
11
+ _canvas;
12
+ _selectHandle;
13
+ _originalSrc;
14
+ _visualWidth = 0;
15
+ _visualHeight = 0;
16
+ constructor(params) {
17
+ this._originalSrc = params.originalSrc;
18
+ this._image = params.image;
19
+ this._selection = params.selection;
20
+ this._canvas = params.canvas;
21
+ this._selectHandle = params.selectHandle;
22
+ this.canvasGeometry = { aspectRatio: params.aspectRatio, width: 0, height: 0 };
23
+ const onSelectionChange = (e) => {
24
+ const { width, height } = e.detail;
25
+ this.cropBoxVisible = width > 0 && height > 0;
26
+ };
27
+ const onBoundaryCheck = (e) => {
28
+ handleSelectionBoundary({ event: e, selection: this._selection, canvas: this._canvas });
29
+ };
30
+ this._selection.addEventListener('change', onSelectionChange);
31
+ this._selection.addEventListener('change', onBoundaryCheck);
32
+ this.destroy = () => {
33
+ this._selection.removeEventListener('change', onSelectionChange);
34
+ this._selection.removeEventListener('change', onBoundaryCheck);
35
+ };
36
+ this._applyMoveMode();
37
+ }
38
+ ready = async () => {
39
+ this._image.src = this._originalSrc;
40
+ await this._fitImage();
41
+ this._selection.$clear();
42
+ };
43
+ rotate = async () => {
44
+ const prevW = this._visualWidth;
45
+ this._visualWidth = this._visualHeight;
46
+ this._visualHeight = prevW;
47
+ this._computeCanvasSize();
48
+ await tick();
49
+ this._wrapTransformOp(() => {
50
+ this._image.$rotate('90deg');
51
+ this._image.$center(this._centerMode);
52
+ });
53
+ };
54
+ zoomIn = () => {
55
+ this._image.$zoom(0.1);
56
+ };
57
+ zoomOut = () => {
58
+ this._image.$zoom(-0.1);
59
+ };
60
+ reset = async () => {
61
+ this._selection.$clear();
62
+ this._applyMoveMode();
63
+ this._image.src = this._originalSrc;
64
+ await this._fitImage();
65
+ };
66
+ crop = async (options) => {
67
+ const fillColor = options?.fillColor;
68
+ const imageScale = computeImageScale(this._image.$getTransform());
69
+ const outputSize = computeNaturalOutputSize({ displayWidth: this._selection.width, displayHeight: this._selection.height, imageScale });
70
+ const canvas = await this._selection.$toCanvas({
71
+ width: outputSize.width,
72
+ height: outputSize.height,
73
+ beforeDraw: (ctx, cvs) => {
74
+ if (fillColor && !ColorHelper.isTransparent(fillColor)) {
75
+ ctx.fillStyle = fillColor;
76
+ ctx.fillRect(0, 0, cvs.width, cvs.height);
77
+ }
78
+ }
79
+ });
80
+ const dataUrl = canvas.toDataURL('image/png', 0.9);
81
+ this._applyMoveMode();
82
+ this._image.src = dataUrl;
83
+ await this._fitImage();
84
+ this._selection.$clear();
85
+ return { dataUrl, width: canvas.width, height: canvas.height };
86
+ };
87
+ save = async (options) => {
88
+ const fillColor = options?.fillColor;
89
+ const beforeDraw = (ctx, cvs) => {
90
+ if (fillColor && !ColorHelper.isTransparent(fillColor)) {
91
+ ctx.fillStyle = fillColor;
92
+ ctx.fillRect(0, 0, cvs.width, cvs.height);
93
+ }
94
+ };
95
+ // Compute output dimensions before micro-zoom so the result matches
96
+ // the source pixel density, not the display size.
97
+ const imageScale = computeImageScale(this._image.$getTransform());
98
+ const displayWidth = this.cropBoxVisible ? this._selection.width : this._canvas.offsetWidth;
99
+ const displayHeight = this.cropBoxVisible ? this._selection.height : this._canvas.offsetHeight;
100
+ const outputSize = computeNaturalOutputSize({ displayWidth, displayHeight, imageScale });
101
+ const toCanvasOptions = { width: outputSize.width, height: outputSize.height, beforeDraw };
102
+ // CropperJS $toCanvas exports only the visible portion of the canvas.
103
+ // Sub-pixel rounding can leave a 1px transparent edge when the image
104
+ // tightly fits the canvas. A micro-zoom nudges the image slightly past
105
+ // the boundary so the export captures the full area; the zoom is
106
+ // reversed immediately after.
107
+ const needsMicroZoom = !this.cropBoxVisible;
108
+ if (needsMicroZoom) {
109
+ // Forward zoom increases coverage — safe without suspending cover check
110
+ this._image.$zoom(0.003);
111
+ }
112
+ const canvas = this.cropBoxVisible ? await this._selection.$toCanvas(toCanvasOptions) : await this._canvas.$toCanvas(toCanvasOptions);
113
+ if (needsMicroZoom) {
114
+ // Reverse zoom shrinks image — must suspend cover check to avoid clamping
115
+ this._wrapTransformOp(() => this._image.$zoom(-0.003));
116
+ }
117
+ const dataUrl = canvas.toDataURL('image/png', 0.9);
118
+ return { dataUrl, width: canvas.width, height: canvas.height };
119
+ };
120
+ clearSelection = () => {
121
+ this._selection.$clear();
122
+ this._applyMoveMode();
123
+ };
124
+ refit = async () => {
125
+ this._selection.$clear();
126
+ this._applyMoveMode();
127
+ this._computeCanvasSize();
128
+ await tick();
129
+ this._wrapTransformOp(() => {
130
+ this._image.$resetTransform();
131
+ this._image.$center(this._centerMode);
132
+ });
133
+ };
134
+ enableCropMode = () => {
135
+ this._image.translatable = false;
136
+ if (this._selectHandle) {
137
+ this._selectHandle.setAttribute('action', 'select');
138
+ }
139
+ this.dragMode = 'crop';
140
+ };
141
+ _wrapTransformOp = (fn) => {
142
+ fn();
143
+ };
144
+ _applyMoveMode = () => {
145
+ this._image.translatable = true;
146
+ if (this._selectHandle) {
147
+ this._selectHandle.setAttribute('action', 'move');
148
+ }
149
+ this.dragMode = 'move';
150
+ this.cropBoxVisible = false;
151
+ };
152
+ _fitImage = async () => {
153
+ const img = await this._image.$ready();
154
+ this._visualWidth = img.naturalWidth;
155
+ this._visualHeight = img.naturalHeight;
156
+ this._computeCanvasSize();
157
+ await tick();
158
+ this._wrapTransformOp(() => {
159
+ this._image.$resetTransform();
160
+ this._image.$center(this._centerMode);
161
+ });
162
+ };
163
+ }
@@ -1,40 +1,7 @@
1
- import type { DragMode, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
2
- export declare class ImgCropperContainWorker implements ImgCropperWorker {
3
- cropBoxVisible: boolean;
4
- dragMode: DragMode;
5
- naturalWidth: number;
6
- naturalHeight: number;
7
- canvasRatio: number | null;
8
- destroy: () => void;
9
- private _image;
10
- private _selection;
11
- private _canvas;
12
- private _selectHandle;
13
- private _originalSrc;
1
+ import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
2
+ import type { ImgCropperWorkerParams } from './img-cropper-worker.svelte';
3
+ export declare class ImgCropperContainWorker extends ImgCropperBaseWorker {
4
+ protected readonly _centerMode: "contain";
14
5
  constructor(params: ImgCropperWorkerParams);
15
- ready: () => Promise<void>;
16
- rotate: () => Promise<void>;
17
- zoomIn: () => void;
18
- zoomOut: () => void;
19
- reset: () => Promise<void>;
20
- crop: (options?: {
21
- fillColor?: string;
22
- freeRatio?: boolean;
23
- }) => Promise<{
24
- dataUrl: string;
25
- width: number;
26
- height: number;
27
- }>;
28
- save: (options?: {
29
- fillColor?: string;
30
- }) => Promise<{
31
- dataUrl: string;
32
- width: number;
33
- height: number;
34
- }>;
35
- clearSelection: () => void;
36
- enableCropMode: () => void;
37
- private _applyMoveMode;
38
- private _fitImage;
39
- private _centerForCurrentRotation;
6
+ protected _computeCanvasSize: () => void;
40
7
  }
@@ -1,159 +1,39 @@
1
- import { ColorHelper } from '../../../core/utils';
2
- import { computeImageScale, computeNaturalOutputSize, handleSelectionBoundary } from './img-cropper-utils';
3
- import { tick } from 'svelte';
4
- export class ImgCropperContainWorker {
5
- cropBoxVisible = $state(false);
6
- dragMode = $state('move');
7
- naturalWidth = $state(0);
8
- naturalHeight = $state(0);
9
- canvasRatio = $state(null);
10
- destroy;
11
- _image;
12
- _selection;
13
- _canvas;
14
- _selectHandle;
15
- _originalSrc;
1
+ import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
2
+ import { fitInContainer } from './img-cropper-utils';
3
+ export class ImgCropperContainWorker extends ImgCropperBaseWorker {
4
+ _centerMode = 'contain';
16
5
  constructor(params) {
17
- this._originalSrc = params.originalSrc;
18
- this._image = params.image;
19
- this._selection = params.selection;
20
- this._canvas = params.canvas;
21
- this._selectHandle = params.selectHandle;
22
- const onSelectionChange = (e) => {
23
- const { width, height } = e.detail;
24
- this.cropBoxVisible = width > 0 && height > 0;
25
- };
26
- const onBoundaryCheck = (e) => {
27
- handleSelectionBoundary({ event: e, selection: this._selection, canvas: this._canvas });
28
- };
29
- this._selection.addEventListener('change', onSelectionChange);
30
- this._selection.addEventListener('change', onBoundaryCheck);
31
- this.destroy = () => {
32
- this._selection.removeEventListener('change', onSelectionChange);
33
- this._selection.removeEventListener('change', onBoundaryCheck);
34
- };
35
- this._applyMoveMode();
6
+ super(params);
36
7
  }
37
- ready = async () => {
38
- this._image.src = this._originalSrc;
39
- await this._fitImage();
40
- this._selection.$clear();
41
- };
42
- rotate = async () => {
43
- if (this.canvasRatio !== null) {
44
- this.canvasRatio = 1 / this.canvasRatio;
45
- const prevW = this.naturalWidth;
46
- this.naturalWidth = this.naturalHeight;
47
- this.naturalHeight = prevW;
48
- await tick();
8
+ _computeCanvasSize = () => {
9
+ const container = this._canvas.parentElement;
10
+ if (!container) {
11
+ return;
49
12
  }
50
- this._image.$rotate('90deg');
51
- this._centerForCurrentRotation();
52
- };
53
- zoomIn = () => {
54
- this._image.$zoom(0.1);
55
- };
56
- zoomOut = () => {
57
- this._image.$zoom(-0.1);
58
- };
59
- reset = async () => {
60
- this._selection.$clear();
61
- this._applyMoveMode();
62
- this.canvasRatio = null;
63
- this._image.src = this._originalSrc;
64
- await this._fitImage();
65
- };
66
- crop = async (options) => {
67
- const fillColor = options?.fillColor;
68
- const selectionRatio = this._selection.width / this._selection.height;
69
- const imageScale = computeImageScale(this._image.$getTransform());
70
- const outputSize = computeNaturalOutputSize({ displayWidth: this._selection.width, displayHeight: this._selection.height, imageScale });
71
- const canvas = await this._selection.$toCanvas({
72
- width: outputSize.width,
73
- height: outputSize.height,
74
- beforeDraw: (ctx, cvs) => {
75
- if (fillColor && !ColorHelper.isTransparent(fillColor)) {
76
- ctx.fillStyle = fillColor;
77
- ctx.fillRect(0, 0, cvs.width, cvs.height);
78
- }
79
- }
80
- });
81
- const dataUrl = canvas.toDataURL('image/png', 0.9);
82
- this._applyMoveMode();
83
- if (options?.freeRatio) {
84
- this.canvasRatio = selectionRatio;
13
+ const containerWidth = container.clientWidth;
14
+ const containerHeight = container.clientHeight;
15
+ const visualWidth = this._visualWidth;
16
+ const visualHeight = this._visualHeight;
17
+ const aspectRatio = this.canvasGeometry.aspectRatio;
18
+ if (containerWidth <= 0 || containerHeight <= 0 || visualWidth <= 0 || visualHeight <= 0) {
19
+ this.canvasGeometry = { aspectRatio, width: 0, height: 0 };
20
+ return;
85
21
  }
86
- this._image.src = dataUrl;
87
- await this._fitImage();
88
- this._selection.$clear();
89
- return { dataUrl, width: canvas.width, height: canvas.height };
90
- };
91
- save = async (options) => {
92
- const fillColor = options?.fillColor;
93
- const beforeDraw = (ctx, cvs) => {
94
- if (fillColor && !ColorHelper.isTransparent(fillColor)) {
95
- ctx.fillStyle = fillColor;
96
- ctx.fillRect(0, 0, cvs.width, cvs.height);
97
- }
98
- };
99
- const imageScale = computeImageScale(this._image.$getTransform());
100
- const displayWidth = this.cropBoxVisible ? this._selection.width : this._canvas.offsetWidth;
101
- const displayHeight = this.cropBoxVisible ? this._selection.height : this._canvas.offsetHeight;
102
- const outputSize = computeNaturalOutputSize({ displayWidth, displayHeight, imageScale });
103
- const toCanvasOptions = { width: outputSize.width, height: outputSize.height, beforeDraw };
104
- const canvas = this.cropBoxVisible ? await this._selection.$toCanvas(toCanvasOptions) : await this._canvas.$toCanvas(toCanvasOptions);
105
- const dataUrl = canvas.toDataURL('image/png', 0.9);
106
- return { dataUrl, width: canvas.width, height: canvas.height };
107
- };
108
- clearSelection = () => {
109
- this._selection.$clear();
110
- this._applyMoveMode();
111
- };
112
- enableCropMode = () => {
113
- this._image.translatable = false;
114
- if (this._selectHandle) {
115
- this._selectHandle.setAttribute('action', 'select');
116
- }
117
- this.dragMode = 'crop';
118
- };
119
- _applyMoveMode = () => {
120
- this._image.translatable = true;
121
- if (this._selectHandle) {
122
- this._selectHandle.setAttribute('action', 'move');
123
- }
124
- this.dragMode = 'move';
125
- this.cropBoxVisible = false;
126
- };
127
- _fitImage = async () => {
128
- const img = await this._image.$ready();
129
- this.naturalWidth = img.naturalWidth;
130
- this.naturalHeight = img.naturalHeight;
131
- await tick();
132
- this._image.$resetTransform();
133
- this._centerForCurrentRotation();
134
- };
135
- // Choose $center() (natural size) or $center('contain') (scale-to-fit) based on
136
- // whether the image's visual bounds fit inside the canvas at the current rotation.
137
- // When canvasRatio is active, naturalWidth/Height are already swapped to match
138
- // the visual orientation. Otherwise, derive visual dims from the rotation angle.
139
- _centerForCurrentRotation = () => {
140
- let visualW = this.naturalWidth;
141
- let visualH = this.naturalHeight;
142
- if (this.canvasRatio === null) {
143
- const matrix = this._image.$getTransform();
144
- const isSwapped = Math.abs(Math.sin(Math.atan2(matrix[1], matrix[0]))) > 0.5;
145
- if (isSwapped) {
146
- visualW = this.naturalHeight;
147
- visualH = this.naturalWidth;
148
- }
22
+ if (aspectRatio !== null) {
23
+ // Fixed ratio: fit in container, cap at natural image bounds
24
+ const fitted = fitInContainer({ ratio: aspectRatio, containerWidth, containerHeight });
25
+ const maxWidth = Math.max(visualWidth, visualHeight * aspectRatio);
26
+ const width = Math.min(fitted.width, maxWidth);
27
+ this.canvasGeometry = { aspectRatio, width, height: width / aspectRatio };
149
28
  }
150
- const canvasRect = this._canvas.getBoundingClientRect();
151
- const fitsNaturally = visualW <= canvasRect.width && visualH <= canvasRect.height;
152
- if (fitsNaturally) {
153
- this._image.$center();
29
+ else if (visualWidth <= containerWidth && visualHeight <= containerHeight) {
30
+ // Free ratio, image fits naturally no upscale
31
+ this.canvasGeometry = { aspectRatio, width: visualWidth, height: visualHeight };
154
32
  }
155
33
  else {
156
- this._image.$center('contain');
34
+ // Free ratio, image too large — scale down to fit
35
+ const fitted = fitInContainer({ ratio: visualWidth / visualHeight, containerWidth, containerHeight });
36
+ this.canvasGeometry = { aspectRatio, width: fitted.width, height: fitted.height };
157
37
  }
158
38
  };
159
39
  }
@@ -1,40 +1,7 @@
1
- import type { DragMode, ImgCropperWorker, ImgCropperWorkerParams } from './img-cropper-worker.svelte';
2
- export declare class ImgCropperCoverWorker implements ImgCropperWorker {
3
- cropBoxVisible: boolean;
4
- dragMode: DragMode;
5
- naturalWidth: number;
6
- naturalHeight: number;
7
- canvasRatio: number | null;
8
- destroy: () => void;
9
- private _image;
10
- private _selection;
11
- private _canvas;
12
- private _selectHandle;
13
- private _originalSrc;
14
- private _withCoverCheckSuspended;
1
+ import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
2
+ import type { ImgCropperWorkerParams } from './img-cropper-worker.svelte';
3
+ export declare class ImgCropperCoverWorker extends ImgCropperBaseWorker {
4
+ protected readonly _centerMode: "cover";
15
5
  constructor(params: ImgCropperWorkerParams);
16
- ready: () => Promise<void>;
17
- rotate: () => void;
18
- zoomIn: () => void;
19
- zoomOut: () => void;
20
- reset: () => Promise<void>;
21
- crop: (options?: {
22
- fillColor?: string;
23
- freeRatio?: boolean;
24
- }) => Promise<{
25
- dataUrl: string;
26
- width: number;
27
- height: number;
28
- }>;
29
- save: (options?: {
30
- fillColor?: string;
31
- }) => Promise<{
32
- dataUrl: string;
33
- width: number;
34
- height: number;
35
- }>;
36
- clearSelection: () => void;
37
- enableCropMode: () => void;
38
- private _applyMoveMode;
39
- private _fitImage;
6
+ protected _computeCanvasSize: () => void;
40
7
  }
@@ -1,32 +1,9 @@
1
- import { ColorHelper } from '../../../core/utils';
2
- import { computeImageScale, computeNaturalOutputSize, handleCoverCheck, handleSelectionBoundary } from './img-cropper-utils';
3
- import { tick } from 'svelte';
4
- export class ImgCropperCoverWorker {
5
- cropBoxVisible = $state(false);
6
- dragMode = $state('move');
7
- naturalWidth = $state(0);
8
- naturalHeight = $state(0);
9
- canvasRatio = $state(null);
10
- destroy;
11
- _image;
12
- _selection;
13
- _canvas;
14
- _selectHandle;
15
- _originalSrc;
16
- _withCoverCheckSuspended;
1
+ import { ImgCropperBaseWorker } from './img-cropper-base-worker.svelte';
2
+ import { fitInContainer, handleCoverCheck } from './img-cropper-utils';
3
+ export class ImgCropperCoverWorker extends ImgCropperBaseWorker {
4
+ _centerMode = 'cover';
17
5
  constructor(params) {
18
- this._originalSrc = params.originalSrc;
19
- this._image = params.image;
20
- this._selection = params.selection;
21
- this._canvas = params.canvas;
22
- this._selectHandle = params.selectHandle;
23
- const onSelectionChange = (e) => {
24
- const { width, height } = e.detail;
25
- this.cropBoxVisible = width > 0 && height > 0;
26
- };
27
- const onBoundaryCheck = (e) => {
28
- handleSelectionBoundary({ event: e, selection: this._selection, canvas: this._canvas });
29
- };
6
+ super(params);
30
7
  // Dedup: handleCoverCheck calls $setTransform(clamped) which synchronously
31
8
  // fires another transform event with a matrix that differs by ~0.0001px
32
9
  // due to floating point. Without this guard CropperJS re-fires the same
@@ -41,123 +18,48 @@ export class ImgCropperCoverWorker {
41
18
  handleCoverCheck({ event: e, image: this._image, canvas: this._canvas });
42
19
  prevMatrix = null;
43
20
  };
44
- this._selection.addEventListener('change', onSelectionChange);
45
- this._selection.addEventListener('change', onBoundaryCheck);
46
21
  this._image.addEventListener('transform', onCoverCheck);
47
- this._withCoverCheckSuspended = (fn) => {
22
+ this._wrapTransformOp = (fn) => {
48
23
  this._image.removeEventListener('transform', onCoverCheck);
49
- fn();
50
- this._image.addEventListener('transform', onCoverCheck);
24
+ try {
25
+ fn();
26
+ }
27
+ finally {
28
+ this._image.addEventListener('transform', onCoverCheck);
29
+ }
51
30
  };
31
+ const baseDestroy = this.destroy;
52
32
  this.destroy = () => {
53
- this._selection.removeEventListener('change', onSelectionChange);
54
- this._selection.removeEventListener('change', onBoundaryCheck);
33
+ baseDestroy();
55
34
  this._image.removeEventListener('transform', onCoverCheck);
56
35
  };
57
- this._applyMoveMode();
58
36
  }
59
- ready = async () => {
60
- this._image.src = this._originalSrc;
61
- await this._fitImage();
62
- this._selection.$clear();
63
- };
64
- rotate = () => {
65
- this._withCoverCheckSuspended(() => {
66
- this._image.$rotate('90deg');
67
- this._image.$center('cover');
68
- });
69
- };
70
- zoomIn = () => {
71
- this._image.$zoom(0.1);
72
- };
73
- zoomOut = () => {
74
- this._image.$zoom(-0.1);
75
- };
76
- reset = async () => {
77
- this._selection.$clear();
78
- this._applyMoveMode();
79
- this._image.src = this._originalSrc;
80
- await this._fitImage();
81
- };
82
- crop = async (options) => {
83
- const fillColor = options?.fillColor;
84
- const imageScale = computeImageScale(this._image.$getTransform());
85
- const outputSize = computeNaturalOutputSize({ displayWidth: this._selection.width, displayHeight: this._selection.height, imageScale });
86
- const canvas = await this._selection.$toCanvas({
87
- width: outputSize.width,
88
- height: outputSize.height,
89
- beforeDraw: (ctx, cvs) => {
90
- if (fillColor && !ColorHelper.isTransparent(fillColor)) {
91
- ctx.fillStyle = fillColor;
92
- ctx.fillRect(0, 0, cvs.width, cvs.height);
93
- }
94
- }
95
- });
96
- const dataUrl = canvas.toDataURL('image/png', 0.9);
97
- this._applyMoveMode();
98
- this._image.src = dataUrl;
99
- await this._fitImage();
100
- this._selection.$clear();
101
- return { dataUrl, width: canvas.width, height: canvas.height };
102
- };
103
- save = async (options) => {
104
- const fillColor = options?.fillColor;
105
- const beforeDraw = (ctx, cvs) => {
106
- if (fillColor && !ColorHelper.isTransparent(fillColor)) {
107
- ctx.fillStyle = fillColor;
108
- ctx.fillRect(0, 0, cvs.width, cvs.height);
109
- }
110
- };
111
- // Compute natural output dimensions before any micro-zoom so the
112
- // output matches the source pixel density, not the display size.
113
- const imageScale = computeImageScale(this._image.$getTransform());
114
- const displayWidth = this.cropBoxVisible ? this._selection.width : this._canvas.offsetWidth;
115
- const displayHeight = this.cropBoxVisible ? this._selection.height : this._canvas.offsetHeight;
116
- const outputSize = computeNaturalOutputSize({ displayWidth, displayHeight, imageScale });
117
- const toCanvasOptions = { width: outputSize.width, height: outputSize.height, beforeDraw };
118
- // CropperJS $toCanvas exports only the visible portion of the canvas.
119
- // In cover mode the image exactly covers the canvas, so sub-pixel rounding
120
- // can leave a 1px transparent edge. A micro-zoom nudges the image slightly
121
- // past the canvas boundary so the export captures the full area; the zoom
122
- // is reversed immediately after.
123
- const needsCoverZoom = !this.cropBoxVisible;
124
- if (needsCoverZoom) {
125
- this._image.$zoom(0.003);
37
+ _computeCanvasSize = () => {
38
+ const container = this._canvas.parentElement;
39
+ if (!container) {
40
+ return;
126
41
  }
127
- const canvas = this.cropBoxVisible ? await this._selection.$toCanvas(toCanvasOptions) : await this._canvas.$toCanvas(toCanvasOptions);
128
- if (needsCoverZoom) {
129
- this._withCoverCheckSuspended(() => this._image.$zoom(-0.003));
42
+ const containerWidth = container.clientWidth;
43
+ const containerHeight = container.clientHeight;
44
+ const visualWidth = this._visualWidth;
45
+ const visualHeight = this._visualHeight;
46
+ const aspectRatio = this.canvasGeometry.aspectRatio;
47
+ if (containerWidth <= 0 || containerHeight <= 0) {
48
+ this.canvasGeometry = { aspectRatio, width: 0, height: 0 };
49
+ return;
130
50
  }
131
- const dataUrl = canvas.toDataURL('image/png', 0.9);
132
- return { dataUrl, width: canvas.width, height: canvas.height };
133
- };
134
- clearSelection = () => {
135
- this._selection.$clear();
136
- this._applyMoveMode();
137
- };
138
- enableCropMode = () => {
139
- this._image.translatable = false;
140
- if (this._selectHandle) {
141
- this._selectHandle.setAttribute('action', 'select');
51
+ if (aspectRatio !== null) {
52
+ const fitted = fitInContainer({ ratio: aspectRatio, containerWidth, containerHeight });
53
+ // Cap so the image can cover the canvas without upscaling
54
+ const maxWidth = visualWidth > 0 && visualHeight > 0 ? Math.min(visualWidth, visualHeight * aspectRatio) : fitted.width;
55
+ const width = Math.min(fitted.width, maxWidth);
56
+ this.canvasGeometry = { aspectRatio, width, height: width / aspectRatio };
142
57
  }
143
- this.dragMode = 'crop';
144
- };
145
- _applyMoveMode = () => {
146
- this._image.translatable = true;
147
- if (this._selectHandle) {
148
- this._selectHandle.setAttribute('action', 'move');
58
+ else {
59
+ // Cap each dimension at visual size to avoid upscaling small images
60
+ const width = visualWidth > 0 ? Math.min(containerWidth, visualWidth) : containerWidth;
61
+ const height = visualHeight > 0 ? Math.min(containerHeight, visualHeight) : containerHeight;
62
+ this.canvasGeometry = { aspectRatio, width, height };
149
63
  }
150
- this.dragMode = 'move';
151
- this.cropBoxVisible = false;
152
- };
153
- _fitImage = async () => {
154
- const img = await this._image.$ready();
155
- this.naturalWidth = img.naturalWidth;
156
- this.naturalHeight = img.naturalHeight;
157
- await tick();
158
- this._withCoverCheckSuspended(() => {
159
- this._image.$resetTransform();
160
- this._image.$center('cover');
161
- });
162
64
  };
163
65
  }
@@ -1,5 +1,5 @@
1
1
  import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
2
- export type Rect = {
2
+ type Rect = {
3
3
  left: number;
4
4
  top: number;
5
5
  right: number;
@@ -25,8 +25,17 @@ export declare const handleCoverCheck: (params: {
25
25
  image: CropperImage;
26
26
  canvas: CropperCanvas;
27
27
  }) => void;
28
+ export declare const fitInContainer: (params: {
29
+ ratio: number;
30
+ containerWidth: number;
31
+ containerHeight: number;
32
+ }) => {
33
+ width: number;
34
+ height: number;
35
+ };
28
36
  export declare const handleSelectionBoundary: (params: {
29
37
  event: Event;
30
38
  selection: CropperSelection;
31
39
  canvas: CropperCanvas;
32
40
  }) => void;
41
+ export {};
@@ -91,6 +91,13 @@ export const handleCoverCheck = (params) => {
91
91
  }
92
92
  image.$setTransform(matrix[0], matrix[1], matrix[2], matrix[3], clampedTx, clampedTy);
93
93
  };
94
+ export const fitInContainer = (params) => {
95
+ const { ratio, containerWidth, containerHeight } = params;
96
+ if (ratio > containerWidth / containerHeight) {
97
+ return { width: containerWidth, height: containerWidth / ratio };
98
+ }
99
+ return { width: containerHeight * ratio, height: containerHeight };
100
+ };
94
101
  export const handleSelectionBoundary = (params) => {
95
102
  const { event, selection, canvas } = params;
96
103
  const { x, y, width, height } = event.detail;
@@ -1,18 +1,22 @@
1
1
  import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
2
2
  export type DragMode = 'move' | 'crop';
3
+ export type CanvasGeometry = {
4
+ aspectRatio: number | null;
5
+ width: number;
6
+ height: number;
7
+ };
3
8
  export type ImgCropperWorkerParams = {
4
9
  originalSrc: string;
5
10
  image: CropperImage;
6
11
  selection: CropperSelection;
7
12
  canvas: CropperCanvas;
8
13
  selectHandle: HTMLElement | null;
14
+ aspectRatio: number | null;
9
15
  };
10
16
  export interface ImgCropperWorker {
11
17
  cropBoxVisible: boolean;
12
18
  dragMode: DragMode;
13
- naturalWidth: number;
14
- naturalHeight: number;
15
- canvasRatio: number | null;
19
+ canvasGeometry: CanvasGeometry;
16
20
  destroy: () => void;
17
21
  ready: () => Promise<void>;
18
22
  rotate: () => void | Promise<void>;
@@ -21,7 +25,6 @@ export interface ImgCropperWorker {
21
25
  reset: () => Promise<void>;
22
26
  crop: (options?: {
23
27
  fillColor?: string;
24
- freeRatio?: boolean;
25
28
  }) => Promise<{
26
29
  dataUrl: string;
27
30
  width: number;
@@ -36,4 +39,5 @@ export interface ImgCropperWorker {
36
39
  }>;
37
40
  clearSelection: () => void;
38
41
  enableCropMode: () => void;
42
+ refit: () => void | Promise<void>;
39
43
  }
@@ -1,5 +1,6 @@
1
- import type { DragMode } from './img-cropper-worker.svelte';
1
+ import type { CanvasGeometry, DragMode } from './img-cropper-worker.svelte';
2
2
  import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
3
+ export type { CanvasGeometry };
3
4
  export type CropperMode = 'contain' | 'cover';
4
5
  export type CorsMode = 'native' | 'fetch';
5
6
  export type { DragMode };
@@ -7,11 +8,7 @@ export type ImgCropperRatioOption = {
7
8
  label: string;
8
9
  value: number | null;
9
10
  };
10
- export type ImgCropperCoverAspectRatio = {
11
- initial: number;
12
- supported: number[];
13
- };
14
- export type ImgCropperContainAspectRatio = {
11
+ export type ImgCropperDynamicAspectRatio = {
15
12
  initial: number;
16
13
  supported: number[];
17
14
  allowFreeCrop?: false;
@@ -30,30 +27,26 @@ type ImgCropperOptionsBase = {
30
27
  fillColor?: string;
31
28
  showImageShadow?: boolean;
32
29
  };
33
- export type ImgCropperOptions = (ImgCropperOptionsBase & {
34
- mode: 'cover';
35
- aspectRatio?: number | ImgCropperCoverAspectRatio;
36
- }) | (ImgCropperOptionsBase & {
37
- mode: 'contain';
38
- aspectRatio?: number | ImgCropperContainAspectRatio;
39
- });
30
+ export type ImgCropperOptions = ImgCropperOptionsBase & {
31
+ mode: CropperMode;
32
+ aspectRatio?: number | ImgCropperDynamicAspectRatio;
33
+ };
40
34
  export declare class ImgCropper {
41
35
  loading: boolean;
42
36
  ready: boolean;
43
37
  fillColor: string;
44
- aspectRatio: number | null;
45
38
  readonly mode: CropperMode;
46
39
  readonly corsMode: CorsMode;
47
40
  readonly ratioOptions: ImgCropperRatioOption[] | null;
48
41
  readonly showImageShadow: boolean;
49
42
  private _worker;
50
43
  private _originalSrc;
44
+ private _defaultGeometry;
51
45
  constructor(options: ImgCropperOptions);
52
46
  get showFillColor(): boolean;
53
47
  get isTransparentFill(): boolean;
54
- get effectiveCanvasRatio(): number | null;
55
- get naturalWidth(): number;
56
- get naturalHeight(): number;
48
+ get canvasGeometry(): CanvasGeometry;
49
+ get aspectRatio(): number | null;
57
50
  get cropBoxVisible(): boolean;
58
51
  get dragMode(): DragMode;
59
52
  init: (params: {
@@ -78,4 +71,5 @@ export declare class ImgCropper {
78
71
  clearSelection: () => void;
79
72
  enableCropMode: () => void;
80
73
  enableMoveMode: () => void;
74
+ refit: () => void;
81
75
  }
@@ -6,13 +6,13 @@ export class ImgCropper {
6
6
  loading = $state(false);
7
7
  ready = $state(false);
8
8
  fillColor = $state('');
9
- aspectRatio = $state(null);
10
9
  mode;
11
10
  corsMode;
12
11
  ratioOptions;
13
12
  showImageShadow;
14
13
  _worker = $state.raw(null);
15
14
  _originalSrc = null;
15
+ _defaultGeometry;
16
16
  constructor(options) {
17
17
  this.mode = options.mode;
18
18
  this.corsMode = options.corsMode ?? 'native';
@@ -26,23 +26,14 @@ export class ImgCropper {
26
26
  return ldd ? { label: `${ldd.dividend}:${ldd.divisor}`, value: r } : { label: String(r), value: r };
27
27
  };
28
28
  const toRatioOptions = (items) => (items.length >= 2 ? items : null);
29
+ let initialRatio;
29
30
  if (options.aspectRatio === undefined || typeof options.aspectRatio === 'number') {
30
- this.aspectRatio = options.aspectRatio ?? null;
31
+ initialRatio = options.aspectRatio ?? null;
31
32
  this.ratioOptions = null;
32
33
  }
33
- else if (options.mode === 'cover') {
34
- const dynamic = options.aspectRatio;
35
- this.aspectRatio = dynamic.initial;
36
- const supported = [...dynamic.supported];
37
- if (!supported.includes(dynamic.initial)) {
38
- supported.unshift(dynamic.initial);
39
- }
40
- this.ratioOptions = toRatioOptions(supported.map(toOption));
41
- }
42
34
  else {
43
35
  const dynamic = options.aspectRatio;
44
- const initialRatio = dynamic.initial ?? (dynamic.allowFreeCrop ? null : (dynamic.supported[0] ?? null));
45
- this.aspectRatio = initialRatio;
36
+ initialRatio = dynamic.initial ?? (dynamic.allowFreeCrop ? null : (dynamic.supported[0] ?? null));
46
37
  const supported = [...dynamic.supported];
47
38
  if (initialRatio !== null && !supported.includes(initialRatio)) {
48
39
  supported.unshift(initialRatio);
@@ -52,6 +43,7 @@ export class ImgCropper {
52
43
  }
53
44
  this.ratioOptions = toRatioOptions(supported.map(toOption));
54
45
  }
46
+ this._defaultGeometry = { aspectRatio: initialRatio, width: 0, height: 0 };
55
47
  }
56
48
  get showFillColor() {
57
49
  return this.mode === 'contain';
@@ -59,14 +51,11 @@ export class ImgCropper {
59
51
  get isTransparentFill() {
60
52
  return ColorHelper.isTransparent(this.fillColor);
61
53
  }
62
- get effectiveCanvasRatio() {
63
- return this.aspectRatio ?? this._worker?.canvasRatio ?? null;
64
- }
65
- get naturalWidth() {
66
- return this._worker?.naturalWidth ?? 0;
54
+ get canvasGeometry() {
55
+ return this._worker?.canvasGeometry ?? this._defaultGeometry;
67
56
  }
68
- get naturalHeight() {
69
- return this._worker?.naturalHeight ?? 0;
57
+ get aspectRatio() {
58
+ return this.canvasGeometry.aspectRatio;
70
59
  }
71
60
  get cropBoxVisible() {
72
61
  return this._worker?.cropBoxVisible ?? false;
@@ -89,7 +78,8 @@ export class ImgCropper {
89
78
  image: params.cropperImage,
90
79
  selection: params.cropperSelection,
91
80
  canvas: params.cropperCanvas,
92
- selectHandle: params.selectHandle
81
+ selectHandle: params.selectHandle,
82
+ aspectRatio: this._defaultGeometry.aspectRatio
93
83
  };
94
84
  this._worker = this.mode === 'cover' ? new ImgCropperCoverWorker(workerParams) : new ImgCropperContainWorker(workerParams);
95
85
  await this._worker.ready();
@@ -121,7 +111,10 @@ export class ImgCropper {
121
111
  await this._worker?.reset();
122
112
  };
123
113
  changeAspectRatio = async (ratio) => {
124
- this.aspectRatio = ratio;
114
+ if (this._worker) {
115
+ this._worker.canvasGeometry = { ...this._worker.canvasGeometry, aspectRatio: ratio };
116
+ }
117
+ this._defaultGeometry = { ...this._defaultGeometry, aspectRatio: ratio };
125
118
  await this.reset();
126
119
  };
127
120
  crop = async () => {
@@ -130,7 +123,7 @@ export class ImgCropper {
130
123
  }
131
124
  this.loading = true;
132
125
  try {
133
- return await this._worker.crop({ fillColor: this.fillColor, freeRatio: this.aspectRatio === null });
126
+ return await this._worker.crop({ fillColor: this.fillColor });
134
127
  }
135
128
  finally {
136
129
  this.loading = false;
@@ -157,4 +150,7 @@ export class ImgCropper {
157
150
  enableMoveMode = () => {
158
151
  this._worker?.clearSelection();
159
152
  };
153
+ refit = () => {
154
+ void this._worker?.refit();
155
+ };
160
156
  }
@@ -1,4 +1,4 @@
1
1
  export { default as ImgCropperControls } from './cmp.img-cropper-controls.svelte';
2
2
  export { default as ImgCropperToolbar } from './cmp.img-cropper-toolbar.svelte';
3
3
  export { default as ImgCropperView } from './cmp.img-cropper.svelte';
4
- export { type CorsMode, type CropperMode, type DragMode, ImgCropper, type ImgCropperContainAspectRatio, type ImgCropperCoverAspectRatio, type ImgCropperOptions, type ImgCropperRatioOption, type ImgCropperSaveResult } from './img-cropper.svelte';
4
+ export { type CanvasGeometry, type CorsMode, type CropperMode, type DragMode, ImgCropper, type ImgCropperDynamicAspectRatio, type ImgCropperOptions, type ImgCropperRatioOption, type ImgCropperSaveResult } from './img-cropper.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",