@streamscloud/kit 0.1.9 → 0.1.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/dist/core/utils/color-helper.d.ts +13 -0
- package/dist/core/utils/color-helper.js +39 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/utils/index.js +1 -0
- package/dist/ui/color-picker/cmp.color-picker.svelte +150 -0
- package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +33 -0
- package/dist/ui/color-picker/cmp.input-stub.svelte +98 -0
- package/dist/ui/color-picker/cmp.input-stub.svelte.d.ts +40 -0
- package/dist/ui/color-picker/color-picker-localization.d.ts +3 -0
- package/dist/ui/color-picker/color-picker-localization.js +12 -0
- package/dist/ui/color-picker/index.d.ts +1 -0
- package/dist/ui/color-picker/index.js +1 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +109 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte.d.ts +9 -0
- package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.d.ts +6 -0
- package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.js +33 -0
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +21 -0
- package/dist/ui/cropper/image-editor-dialog/index.js +25 -0
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +25 -0
- package/dist/ui/cropper/image-editor-dialog/types.js +1 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +67 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +21 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +228 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +28 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +198 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +58 -0
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +33 -0
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +40 -0
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +159 -0
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +40 -0
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +163 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +6 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.js +33 -0
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +11 -0
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +68 -0
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +32 -0
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +138 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +39 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +1 -0
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +81 -0
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +160 -0
- package/dist/ui/cropper/img-cropper/index.d.ts +4 -0
- package/dist/ui/cropper/img-cropper/index.js +4 -0
- package/dist/ui/icon-text/cmp.icon-text.svelte +90 -0
- package/dist/ui/icon-text/cmp.icon-text.svelte.d.ts +39 -0
- package/dist/ui/icon-text/index.d.ts +1 -0
- package/dist/ui/icon-text/index.js +1 -0
- package/package.json +27 -5
|
@@ -0,0 +1,159 @@
|
|
|
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;
|
|
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
|
+
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();
|
|
36
|
+
}
|
|
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();
|
|
49
|
+
}
|
|
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;
|
|
85
|
+
}
|
|
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
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const canvasRect = this._canvas.getBoundingClientRect();
|
|
151
|
+
const fitsNaturally = visualW <= canvasRect.width && visualH <= canvasRect.height;
|
|
152
|
+
if (fitsNaturally) {
|
|
153
|
+
this._image.$center();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this._image.$center('contain');
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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;
|
|
15
|
+
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;
|
|
40
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
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;
|
|
17
|
+
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
|
+
};
|
|
30
|
+
// Dedup: handleCoverCheck calls $setTransform(clamped) which synchronously
|
|
31
|
+
// fires another transform event with a matrix that differs by ~0.0001px
|
|
32
|
+
// due to floating point. Without this guard CropperJS re-fires the same
|
|
33
|
+
// clamped transform endlessly, causing a stack overflow.
|
|
34
|
+
let prevMatrix = null;
|
|
35
|
+
const onCoverCheck = (e) => {
|
|
36
|
+
const { matrix } = e.detail;
|
|
37
|
+
if (prevMatrix && matrix.every((v, i) => Math.abs(v - prevMatrix[i]) < 0.01)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
prevMatrix = matrix;
|
|
41
|
+
handleCoverCheck({ event: e, image: this._image, canvas: this._canvas });
|
|
42
|
+
prevMatrix = null;
|
|
43
|
+
};
|
|
44
|
+
this._selection.addEventListener('change', onSelectionChange);
|
|
45
|
+
this._selection.addEventListener('change', onBoundaryCheck);
|
|
46
|
+
this._image.addEventListener('transform', onCoverCheck);
|
|
47
|
+
this._withCoverCheckSuspended = (fn) => {
|
|
48
|
+
this._image.removeEventListener('transform', onCoverCheck);
|
|
49
|
+
fn();
|
|
50
|
+
this._image.addEventListener('transform', onCoverCheck);
|
|
51
|
+
};
|
|
52
|
+
this.destroy = () => {
|
|
53
|
+
this._selection.removeEventListener('change', onSelectionChange);
|
|
54
|
+
this._selection.removeEventListener('change', onBoundaryCheck);
|
|
55
|
+
this._image.removeEventListener('transform', onCoverCheck);
|
|
56
|
+
};
|
|
57
|
+
this._applyMoveMode();
|
|
58
|
+
}
|
|
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);
|
|
126
|
+
}
|
|
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));
|
|
130
|
+
}
|
|
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');
|
|
142
|
+
}
|
|
143
|
+
this.dragMode = 'crop';
|
|
144
|
+
};
|
|
145
|
+
_applyMoveMode = () => {
|
|
146
|
+
this._image.translatable = true;
|
|
147
|
+
if (this._selectHandle) {
|
|
148
|
+
this._selectHandle.setAttribute('action', 'move');
|
|
149
|
+
}
|
|
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
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppLocale } from '../../../core/locale';
|
|
2
|
+
const loc = {
|
|
3
|
+
apply: {
|
|
4
|
+
en: 'Apply',
|
|
5
|
+
no: 'Bruk'
|
|
6
|
+
},
|
|
7
|
+
cancel: {
|
|
8
|
+
en: 'Cancel',
|
|
9
|
+
no: 'Avbryt'
|
|
10
|
+
},
|
|
11
|
+
imageLoadError: {
|
|
12
|
+
en: 'Failed to load image',
|
|
13
|
+
no: 'Kunne ikke laste bildet'
|
|
14
|
+
},
|
|
15
|
+
initCropperError: {
|
|
16
|
+
en: 'Failed to initialize cropper',
|
|
17
|
+
no: 'Kunne ikke initialisere cropper'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export class ImgCropperLocalization {
|
|
21
|
+
get apply() {
|
|
22
|
+
return loc.apply[AppLocale.current];
|
|
23
|
+
}
|
|
24
|
+
get cancel() {
|
|
25
|
+
return loc.cancel[AppLocale.current];
|
|
26
|
+
}
|
|
27
|
+
get imageLoadError() {
|
|
28
|
+
return loc.imageLoadError[AppLocale.current];
|
|
29
|
+
}
|
|
30
|
+
get initCropperError() {
|
|
31
|
+
return loc.initCropperError[AppLocale.current];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class ImgCropperToolbarLocalization {
|
|
2
|
+
get move(): string;
|
|
3
|
+
get crop(): string;
|
|
4
|
+
get zoomIn(): string;
|
|
5
|
+
get zoomOut(): string;
|
|
6
|
+
get rotate(): string;
|
|
7
|
+
get reset(): string;
|
|
8
|
+
get free(): string;
|
|
9
|
+
get ratio(): string;
|
|
10
|
+
get fill(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AppLocale } from '../../../core/locale';
|
|
2
|
+
const loc = {
|
|
3
|
+
move: {
|
|
4
|
+
en: 'Move',
|
|
5
|
+
no: 'Flytt'
|
|
6
|
+
},
|
|
7
|
+
crop: {
|
|
8
|
+
en: 'Crop',
|
|
9
|
+
no: 'Beskjær'
|
|
10
|
+
},
|
|
11
|
+
zoomIn: {
|
|
12
|
+
en: 'Zoom In',
|
|
13
|
+
no: 'Zoom inn'
|
|
14
|
+
},
|
|
15
|
+
zoomOut: {
|
|
16
|
+
en: 'Zoom Out',
|
|
17
|
+
no: 'Zoom ut'
|
|
18
|
+
},
|
|
19
|
+
rotate: {
|
|
20
|
+
en: 'Rotate',
|
|
21
|
+
no: 'Roter'
|
|
22
|
+
},
|
|
23
|
+
reset: {
|
|
24
|
+
en: 'Reset',
|
|
25
|
+
no: 'Tilbakestill'
|
|
26
|
+
},
|
|
27
|
+
free: {
|
|
28
|
+
en: 'Free',
|
|
29
|
+
no: 'Fri'
|
|
30
|
+
},
|
|
31
|
+
ratio: {
|
|
32
|
+
en: 'Ratio',
|
|
33
|
+
no: 'Forhold'
|
|
34
|
+
},
|
|
35
|
+
fill: {
|
|
36
|
+
en: 'Fill',
|
|
37
|
+
no: 'Fyll'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export class ImgCropperToolbarLocalization {
|
|
41
|
+
get move() {
|
|
42
|
+
return loc.move[AppLocale.current];
|
|
43
|
+
}
|
|
44
|
+
get crop() {
|
|
45
|
+
return loc.crop[AppLocale.current];
|
|
46
|
+
}
|
|
47
|
+
get zoomIn() {
|
|
48
|
+
return loc.zoomIn[AppLocale.current];
|
|
49
|
+
}
|
|
50
|
+
get zoomOut() {
|
|
51
|
+
return loc.zoomOut[AppLocale.current];
|
|
52
|
+
}
|
|
53
|
+
get rotate() {
|
|
54
|
+
return loc.rotate[AppLocale.current];
|
|
55
|
+
}
|
|
56
|
+
get reset() {
|
|
57
|
+
return loc.reset[AppLocale.current];
|
|
58
|
+
}
|
|
59
|
+
get free() {
|
|
60
|
+
return loc.free[AppLocale.current];
|
|
61
|
+
}
|
|
62
|
+
get ratio() {
|
|
63
|
+
return loc.ratio[AppLocale.current];
|
|
64
|
+
}
|
|
65
|
+
get fill() {
|
|
66
|
+
return loc.fill[AppLocale.current];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CropperCanvas, CropperImage, CropperSelection } from 'cropperjs';
|
|
2
|
+
export type Rect = {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
right: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const computeImageScale: (transform: number[]) => number;
|
|
9
|
+
export declare const computeNaturalOutputSize: (params: {
|
|
10
|
+
displayWidth: number;
|
|
11
|
+
displayHeight: number;
|
|
12
|
+
imageScale: number;
|
|
13
|
+
}) => {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
export declare const computeLocalAABB: (params: {
|
|
18
|
+
matrix: number[];
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
}) => Rect;
|
|
22
|
+
export declare const rectCoversRect: (inner: Rect, outer: Rect, tolerance?: number) => boolean;
|
|
23
|
+
export declare const handleCoverCheck: (params: {
|
|
24
|
+
event: Event;
|
|
25
|
+
image: CropperImage;
|
|
26
|
+
canvas: CropperCanvas;
|
|
27
|
+
}) => void;
|
|
28
|
+
export declare const handleSelectionBoundary: (params: {
|
|
29
|
+
event: Event;
|
|
30
|
+
selection: CropperSelection;
|
|
31
|
+
canvas: CropperCanvas;
|
|
32
|
+
}) => void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Extract the uniform scale factor from a CSS transform matrix [a, b, c, d, e, f].
|
|
2
|
+
// Works for any combination of scale + rotation: scale = sqrt(a² + b²).
|
|
3
|
+
export const computeImageScale = (transform) => {
|
|
4
|
+
return Math.sqrt(transform[0] ** 2 + transform[1] ** 2) || 1;
|
|
5
|
+
};
|
|
6
|
+
// Compute the natural-resolution output size for a displayed region.
|
|
7
|
+
// Divides the display dimensions by the image's transform scale so that
|
|
8
|
+
// the exported image matches the source pixel density.
|
|
9
|
+
export const computeNaturalOutputSize = (params) => {
|
|
10
|
+
const { displayWidth, displayHeight, imageScale } = params;
|
|
11
|
+
return {
|
|
12
|
+
width: Math.round(displayWidth / imageScale),
|
|
13
|
+
height: Math.round(displayHeight / imageScale)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
// Compute proposed bounding rect mathematically (no DOM style manipulation).
|
|
17
|
+
// CSS default transform-origin is 50% 50%, so AABB of a w×h element under matrix(a,b,c,d,e,f):
|
|
18
|
+
// left = e + w/2 - |a|·w/2 - |c|·h/2
|
|
19
|
+
// right = e + w/2 + |a|·w/2 + |c|·h/2 (and similarly for top/bottom with b,d,f)
|
|
20
|
+
// We only need the DIFFERENCE between old and new AABB to predict the proposed viewport rect.
|
|
21
|
+
export const computeLocalAABB = (params) => {
|
|
22
|
+
const { matrix, width, height } = params;
|
|
23
|
+
const halfWidth = width / 2;
|
|
24
|
+
const halfHeight = height / 2;
|
|
25
|
+
return {
|
|
26
|
+
left: matrix[4] + halfWidth - Math.abs(matrix[0]) * halfWidth - Math.abs(matrix[2]) * halfHeight,
|
|
27
|
+
top: matrix[5] + halfHeight - Math.abs(matrix[1]) * halfWidth - Math.abs(matrix[3]) * halfHeight,
|
|
28
|
+
right: matrix[4] + halfWidth + Math.abs(matrix[0]) * halfWidth + Math.abs(matrix[2]) * halfHeight,
|
|
29
|
+
bottom: matrix[5] + halfHeight + Math.abs(matrix[1]) * halfWidth + Math.abs(matrix[3]) * halfHeight
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export const rectCoversRect = (inner, outer, tolerance = 1) => inner.left <= outer.left + tolerance &&
|
|
33
|
+
inner.top <= outer.top + tolerance &&
|
|
34
|
+
inner.right >= outer.right - tolerance &&
|
|
35
|
+
inner.bottom >= outer.bottom - tolerance;
|
|
36
|
+
export const handleCoverCheck = (params) => {
|
|
37
|
+
const { event, image, canvas } = params;
|
|
38
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
39
|
+
const currentRect = image.getBoundingClientRect();
|
|
40
|
+
// Only restrict when image currently covers canvas.
|
|
41
|
+
// During internal ops ($resetTransform, $center, $rotate) it doesn't — handler skips.
|
|
42
|
+
if (!rectCoversRect(currentRect, canvasRect)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { matrix, oldMatrix } = event.detail;
|
|
46
|
+
// Allow rotations — always followed by $center() which restores coverage.
|
|
47
|
+
// Compare angles (atan2(b,a)) instead of raw b/c values because zoom also
|
|
48
|
+
// changes b and c when the image is already rotated.
|
|
49
|
+
const oldAngle = Math.atan2(oldMatrix[1], oldMatrix[0]);
|
|
50
|
+
const newAngle = Math.atan2(matrix[1], matrix[0]);
|
|
51
|
+
if (Math.abs(newAngle - oldAngle) > 0.001) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const imageWidth = image.offsetWidth;
|
|
55
|
+
const imageHeight = image.offsetHeight;
|
|
56
|
+
const oldAABB = computeLocalAABB({ matrix: oldMatrix, width: imageWidth, height: imageHeight });
|
|
57
|
+
const newAABB = computeLocalAABB({ matrix, width: imageWidth, height: imageHeight });
|
|
58
|
+
const proposedRect = {
|
|
59
|
+
left: currentRect.left + (newAABB.left - oldAABB.left),
|
|
60
|
+
top: currentRect.top + (newAABB.top - oldAABB.top),
|
|
61
|
+
right: currentRect.right + (newAABB.right - oldAABB.right),
|
|
62
|
+
bottom: currentRect.bottom + (newAABB.bottom - oldAABB.bottom)
|
|
63
|
+
};
|
|
64
|
+
// Strict check (tolerance 0) — any gap triggers clamping
|
|
65
|
+
if (rectCoversRect(proposedRect, canvasRect, 0)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
// If the proposed image is still large enough to cover the canvas, clamp
|
|
70
|
+
// translation to maintain coverage. Otherwise block the transform entirely.
|
|
71
|
+
const proposedWidth = proposedRect.right - proposedRect.left;
|
|
72
|
+
const proposedHeight = proposedRect.bottom - proposedRect.top;
|
|
73
|
+
const canvasWidth = canvasRect.right - canvasRect.left;
|
|
74
|
+
const canvasHeight = canvasRect.bottom - canvasRect.top;
|
|
75
|
+
if (proposedWidth < canvasWidth - 0.5 || proposedHeight < canvasHeight - 0.5) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
let clampedTx = matrix[4];
|
|
79
|
+
let clampedTy = matrix[5];
|
|
80
|
+
if (proposedRect.left > canvasRect.left) {
|
|
81
|
+
clampedTx -= proposedRect.left - canvasRect.left;
|
|
82
|
+
}
|
|
83
|
+
else if (proposedRect.right < canvasRect.right) {
|
|
84
|
+
clampedTx += canvasRect.right - proposedRect.right;
|
|
85
|
+
}
|
|
86
|
+
if (proposedRect.top > canvasRect.top) {
|
|
87
|
+
clampedTy -= proposedRect.top - canvasRect.top;
|
|
88
|
+
}
|
|
89
|
+
else if (proposedRect.bottom < canvasRect.bottom) {
|
|
90
|
+
clampedTy += canvasRect.bottom - proposedRect.bottom;
|
|
91
|
+
}
|
|
92
|
+
image.$setTransform(matrix[0], matrix[1], matrix[2], matrix[3], clampedTx, clampedTy);
|
|
93
|
+
};
|
|
94
|
+
export const handleSelectionBoundary = (params) => {
|
|
95
|
+
const { event, selection, canvas } = params;
|
|
96
|
+
const { x, y, width, height } = event.detail;
|
|
97
|
+
const { width: canvasWidth, height: canvasHeight } = canvas.getBoundingClientRect();
|
|
98
|
+
if (x >= 0 && y >= 0 && x + width <= canvasWidth && y + height <= canvasHeight) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
const isMove = Math.abs(width - selection.width) < 1 && Math.abs(height - selection.height) < 1;
|
|
103
|
+
if (isMove) {
|
|
104
|
+
selection.$change(Math.max(0, Math.min(x, canvasWidth - width)), Math.max(0, Math.min(y, canvasHeight - height)), width, height);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (selection.aspectRatio) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const overLeft = x < 0;
|
|
111
|
+
const overTop = y < 0;
|
|
112
|
+
const overRight = x + width > canvasWidth;
|
|
113
|
+
const overBottom = y + height > canvasHeight;
|
|
114
|
+
if (+overLeft + +overTop + +overRight + +overBottom !== 1) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let clampedX = x;
|
|
118
|
+
let clampedY = y;
|
|
119
|
+
let clampedWidth = width;
|
|
120
|
+
let clampedHeight = height;
|
|
121
|
+
if (overLeft) {
|
|
122
|
+
clampedX = 0;
|
|
123
|
+
clampedWidth = x + width;
|
|
124
|
+
}
|
|
125
|
+
else if (overTop) {
|
|
126
|
+
clampedY = 0;
|
|
127
|
+
clampedHeight = y + height;
|
|
128
|
+
}
|
|
129
|
+
else if (overRight) {
|
|
130
|
+
clampedWidth = canvasWidth - x;
|
|
131
|
+
}
|
|
132
|
+
else if (overBottom) {
|
|
133
|
+
clampedHeight = canvasHeight - y;
|
|
134
|
+
}
|
|
135
|
+
if (clampedWidth > 0 && clampedHeight > 0) {
|
|
136
|
+
selection.$change(clampedX, clampedY, clampedWidth, clampedHeight);
|
|
137
|
+
}
|
|
138
|
+
};
|