@streamscloud/kit 0.63.0 → 0.64.0
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 +4 -0
- package/dist/core/utils/color-helper.js +10 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +17 -51
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.d.ts +4 -16
- package/dist/ui/cropper/image-editor-dialog/crop-and-resize.js +1 -14
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +8 -7
- package/dist/ui/cropper/image-editor-dialog/index.js +8 -7
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +1 -1
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +149 -181
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +4 -13
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +389 -147
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +13 -39
- package/dist/ui/cropper/img-cropper/frame-geometry.d.ts +82 -0
- package/dist/ui/cropper/img-cropper/frame-geometry.js +165 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +2 -3
- package/dist/ui/cropper/img-cropper/img-cropper-localization.js +12 -19
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +14 -5
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +127 -33
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +4 -51
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +35 -190
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +99 -34
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +405 -104
- package/dist/ui/cropper/img-cropper/index.d.ts +1 -2
- package/dist/ui/cropper/img-cropper/index.js +0 -1
- package/package.json +1 -3
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +0 -68
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +0 -21
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +0 -35
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.d.ts +0 -40
- package/dist/ui/cropper/img-cropper/img-cropper-base-worker.svelte.js +0 -193
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +0 -51
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +0 -7
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +0 -89
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +0 -42
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +0 -1
|
@@ -5,23 +5,14 @@ type Props = {
|
|
|
5
5
|
children?: Snippet;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* ### Props
|
|
13
|
-
* | Prop | Type | Default | Description |
|
|
14
|
-
* |---|---|---|---|
|
|
15
|
-
* | `cropper` | `ImgCropper` | — | Cropper controller instance |
|
|
16
|
-
* | `children` | `Snippet` | — | Extra controls rendered at the end |
|
|
8
|
+
* Floating toolbar for `ImgCropperView` — aspect ratio, zoom, free position, alignment, rotate, reset, and the frame's
|
|
9
|
+
* fill color (in `contain` mode, whenever free position is on, or when the picture has its own transparency). Pass it
|
|
10
|
+
* to `ImgCropperView`'s `toolbar` snippet prop.
|
|
17
11
|
*
|
|
18
12
|
* ### CSS Custom Properties
|
|
19
13
|
* | Property | Description | Default |
|
|
20
14
|
* |---|---|---|
|
|
21
|
-
* | `--sc-kit--img-cropper-toolbar--
|
|
22
|
-
* | `--sc-kit--img-cropper-toolbar--background` | Button background | `light-dark(white, dark-700)` |
|
|
23
|
-
* | `--sc-kit--img-cropper-toolbar--border-color` | Button border color | `light-dark(neutral-200, dark-600)` |
|
|
24
|
-
* | `--sc-kit--img-cropper-toolbar--active-color` | Active button accent color | `light-dark(primary-500, primary-400)` |
|
|
15
|
+
* | `--sc-kit--img-cropper-toolbar--background` | Panel background | `var(--sc-kit--color--bg--panel)` |
|
|
25
16
|
*/
|
|
26
17
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
27
18
|
type Cmp = ReturnType<typeof Cmp>;
|
|
@@ -1,183 +1,425 @@
|
|
|
1
1
|
<script lang="ts">import { Spinner } from '../../spinner';
|
|
2
|
-
import {
|
|
3
|
-
import { default as ImgCropperControls } from './cmp.img-cropper-controls.svelte';
|
|
2
|
+
import { Text } from '../../typography';
|
|
4
3
|
import { ImgCropperLocalization } from './img-cropper-localization';
|
|
5
|
-
|
|
4
|
+
import { ImgCropper } from './img-cropper.svelte';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
const { src, cropper, toolbar } = $props();
|
|
7
|
+
const CORNERS = ['nw', 'ne', 'sw', 'se'];
|
|
6
8
|
const loc = new ImgCropperLocalization();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
void loadCropper();
|
|
19
|
-
});
|
|
20
|
-
const initCanvas = (canvasEl) => {
|
|
21
|
-
const imageEl = canvasEl.querySelector('cropper-image');
|
|
22
|
-
const selectionEl = canvasEl.querySelector('cropper-selection');
|
|
23
|
-
const selectHandle = canvasEl.querySelector('cropper-handle[action="select"]');
|
|
24
|
-
if (imageEl && selectionEl) {
|
|
25
|
-
const initCropper = async () => {
|
|
26
|
-
try {
|
|
27
|
-
await cropper.init({
|
|
28
|
-
src,
|
|
29
|
-
cropperImage: imageEl,
|
|
30
|
-
cropperSelection: selectionEl,
|
|
31
|
-
cropperCanvas: canvasEl,
|
|
32
|
-
selectHandle
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
Toastr.error(loc.imageLoadError);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
void initCropper();
|
|
9
|
+
const WHEEL_ZOOM_RATE = 0.0016;
|
|
10
|
+
const DRAW_START_PX = 4;
|
|
11
|
+
let stageEl = $state();
|
|
12
|
+
let drag = $state.raw(null);
|
|
13
|
+
let dragPointer = null;
|
|
14
|
+
let dragStart = { box: null, selection: null };
|
|
15
|
+
$effect(() => untrack(() => {
|
|
16
|
+
const mountedCropper = cropper;
|
|
17
|
+
if (stageEl) {
|
|
18
|
+
mountedCropper.setStage({ w: stageEl.clientWidth, h: stageEl.clientHeight });
|
|
40
19
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
void mountedCropper.init(src);
|
|
21
|
+
return () => mountedCropper.destroy();
|
|
22
|
+
}));
|
|
23
|
+
const beginDrag = (event, next) => {
|
|
24
|
+
stageEl?.setPointerCapture(event.pointerId);
|
|
25
|
+
dragPointer = event.pointerId;
|
|
26
|
+
dragStart = { box: cropper.box, selection: cropper.selection };
|
|
27
|
+
drag = next;
|
|
46
28
|
};
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
const ignoreWhileDragging = (event) => {
|
|
30
|
+
if (drag) {
|
|
31
|
+
event.stopPropagation();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const observeStage = (node) => {
|
|
35
|
+
const observer = new ResizeObserver((entries) => {
|
|
36
|
+
const entry = entries.at(0);
|
|
37
|
+
if (entry) {
|
|
38
|
+
cropper.setStage({ w: entry.contentRect.width, h: entry.contentRect.height });
|
|
51
39
|
}
|
|
52
40
|
});
|
|
53
|
-
observer.observe(
|
|
41
|
+
observer.observe(node);
|
|
54
42
|
return { destroy: () => observer.disconnect() };
|
|
55
43
|
};
|
|
44
|
+
const stagePoint = (event) => {
|
|
45
|
+
if (!stageEl) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const stageRect = stageEl.getBoundingClientRect();
|
|
49
|
+
return { x: event.clientX - stageRect.left, y: event.clientY - stageRect.top };
|
|
50
|
+
};
|
|
51
|
+
const framePoint = (event) => {
|
|
52
|
+
const frame = cropper.frame;
|
|
53
|
+
const point = stagePoint(event);
|
|
54
|
+
return frame && point ? { x: point.x - frame.x, y: point.y - frame.y } : null;
|
|
55
|
+
};
|
|
56
|
+
const cornerPoint = (rect, corner) => ({
|
|
57
|
+
x: corner === 'ne' || corner === 'se' ? rect.x + rect.w : rect.x,
|
|
58
|
+
y: corner === 'sw' || corner === 'se' ? rect.y + rect.h : rect.y
|
|
59
|
+
});
|
|
60
|
+
const oppositeCorner = (corner) => (corner === 'nw' ? 'se' : corner === 'se' ? 'nw' : corner === 'ne' ? 'sw' : 'ne');
|
|
61
|
+
const startPan = (event) => {
|
|
62
|
+
if (event.button !== 0 || !cropper.ready) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
beginDrag(event, { kind: 'pan', last: { x: event.clientX, y: event.clientY } });
|
|
67
|
+
};
|
|
68
|
+
const stagePointerDown = (event) => {
|
|
69
|
+
if (!cropper.cropMode || drag) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (event.button !== 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const frame = cropper.frame;
|
|
76
|
+
const anchor = framePoint(event);
|
|
77
|
+
if (!frame || !anchor) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
beginDrag(event, {
|
|
81
|
+
kind: 'selection',
|
|
82
|
+
anchor: { x: Math.min(Math.max(anchor.x, 0), frame.w), y: Math.min(Math.max(anchor.y, 0), frame.h) },
|
|
83
|
+
pressedAt: { x: event.clientX, y: event.clientY }
|
|
84
|
+
});
|
|
85
|
+
cropper.clearSelection();
|
|
86
|
+
};
|
|
87
|
+
const startScale = (event, corner) => {
|
|
88
|
+
const box = cropper.box;
|
|
89
|
+
const start = framePoint(event);
|
|
90
|
+
if (event.button !== 0 || !box || !start) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
const draggedCorner = cornerPoint(box, corner);
|
|
95
|
+
const anchor = cornerPoint(box, oppositeCorner(corner));
|
|
96
|
+
beginDrag(event, { kind: 'scale', startBox: box, anchor, corner: draggedCorner, start });
|
|
97
|
+
};
|
|
98
|
+
const startSelectionHandle = (event, corner) => {
|
|
99
|
+
const selection = cropper.selection;
|
|
100
|
+
if (event.button !== 0 || !selection) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
beginDrag(event, { kind: 'selection', anchor: cornerPoint(selection, oppositeCorner(corner)), pressedAt: null });
|
|
105
|
+
};
|
|
106
|
+
const startMoveSelection = (event) => {
|
|
107
|
+
if (event.button !== 0 || !cropper.selection) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
event.stopPropagation();
|
|
111
|
+
beginDrag(event, { kind: 'move-selection', last: { x: event.clientX, y: event.clientY } });
|
|
112
|
+
};
|
|
113
|
+
const pointerMove = (event) => {
|
|
114
|
+
if (!drag || event.pointerId !== dragPointer) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (drag.kind === 'pan') {
|
|
118
|
+
cropper.pan({ x: event.clientX - drag.last.x, y: event.clientY - drag.last.y });
|
|
119
|
+
drag = { kind: 'pan', last: { x: event.clientX, y: event.clientY } };
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (drag.kind === 'selection') {
|
|
123
|
+
const { pressedAt } = drag;
|
|
124
|
+
if (pressedAt) {
|
|
125
|
+
if (Math.hypot(event.clientX - pressedAt.x, event.clientY - pressedAt.y) < DRAW_START_PX) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
drag = { ...drag, pressedAt: null };
|
|
129
|
+
}
|
|
130
|
+
const point = framePoint(event);
|
|
131
|
+
if (!point) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
cropper.updateSelection(drag.anchor, point);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (drag.kind === 'move-selection') {
|
|
138
|
+
cropper.moveSelection({ x: event.clientX - drag.last.x, y: event.clientY - drag.last.y });
|
|
139
|
+
drag = { kind: 'move-selection', last: { x: event.clientX, y: event.clientY } };
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const point = framePoint(event);
|
|
143
|
+
if (!point) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const diagonal = { x: drag.corner.x - drag.anchor.x, y: drag.corner.y - drag.anchor.y };
|
|
147
|
+
const lengthSquared = diagonal.x * diagonal.x + diagonal.y * diagonal.y || 1;
|
|
148
|
+
const traveled = { x: point.x - drag.start.x, y: point.y - drag.start.y };
|
|
149
|
+
const factor = Math.max(0.05, 1 + (traveled.x * diagonal.x + traveled.y * diagonal.y) / lengthSquared);
|
|
150
|
+
cropper.box = drag.startBox;
|
|
151
|
+
cropper.scaleAbout(factor, drag.anchor);
|
|
152
|
+
};
|
|
153
|
+
const endDrag = (event) => {
|
|
154
|
+
if (event.pointerId !== dragPointer) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (drag?.kind === 'selection') {
|
|
158
|
+
cropper.finishSelection();
|
|
159
|
+
}
|
|
160
|
+
drag = null;
|
|
161
|
+
dragPointer = null;
|
|
162
|
+
};
|
|
163
|
+
const cancelDrag = (event) => {
|
|
164
|
+
if (event.pointerId !== dragPointer) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
cropper.box = dragStart.box;
|
|
168
|
+
cropper.selection = dragStart.selection;
|
|
169
|
+
drag = null;
|
|
170
|
+
dragPointer = null;
|
|
171
|
+
};
|
|
172
|
+
const wheelZoom = (event) => {
|
|
173
|
+
if (!cropper.ready) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
if (drag) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const anchor = framePoint(event);
|
|
181
|
+
if (anchor) {
|
|
182
|
+
cropper.scaleAbout(Math.exp(-event.deltaY * WHEEL_ZOOM_RATE), anchor);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const placement = (rect) => `left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px`;
|
|
186
|
+
const pictureStyle = (box) => {
|
|
187
|
+
const placement = cropper.sourcePlacement({ x: 0, y: 0, w: box.w, h: box.h });
|
|
188
|
+
if (!placement) {
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
const { center, size } = placement;
|
|
192
|
+
const turned = cropper.rotation === 90 || cropper.rotation === 270;
|
|
193
|
+
return `left:${center.x}px;top:${center.y}px;width:${turned ? size.h : size.w}px;height:${turned ? size.w : size.h}px;rotate:${cropper.rotation}deg`;
|
|
194
|
+
};
|
|
56
195
|
</script>
|
|
57
196
|
|
|
58
|
-
<div class="img-cropper"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
197
|
+
<div class="img-cropper">
|
|
198
|
+
<div
|
|
199
|
+
class="img-cropper__stage"
|
|
200
|
+
class:img-cropper__stage--dragging={drag !== null && !cropper.cropMode}
|
|
201
|
+
class:img-cropper__stage--crop-mode={cropper.cropMode}
|
|
202
|
+
role="presentation"
|
|
203
|
+
bind:this={stageEl}
|
|
204
|
+
use:observeStage
|
|
205
|
+
onpointerdowncapture={ignoreWhileDragging}
|
|
206
|
+
onpointerdown={stagePointerDown}
|
|
207
|
+
onpointermove={pointerMove}
|
|
208
|
+
onpointerup={endDrag}
|
|
209
|
+
onpointercancel={cancelDrag}
|
|
210
|
+
onwheel={wheelZoom}>
|
|
211
|
+
{#if cropper.failed}
|
|
212
|
+
<div class="img-cropper__state"><Text size="sm" tone="muted">{loc.imageLoadError}</Text></div>
|
|
213
|
+
{:else if !cropper.ready || !cropper.box || !cropper.frame}
|
|
214
|
+
<div class="img-cropper__state"><Spinner /></div>
|
|
215
|
+
{:else}
|
|
216
|
+
{@const box = cropper.box}
|
|
217
|
+
{@const frame = cropper.frame}
|
|
218
|
+
<div class="img-cropper__picture-box img-cropper__picture-box--outside" style={placement({ x: frame.x + box.x, y: frame.y + box.y, w: box.w, h: box.h })}>
|
|
219
|
+
<img class="img-cropper__picture" src={cropper.source} alt="" draggable="false" style={pictureStyle(box)} />
|
|
220
|
+
</div>
|
|
221
|
+
<div class="img-cropper__frame" style={placement(frame)} style:--_--img-cropper--fill-color={cropper.fillColor || undefined}>
|
|
222
|
+
<div class="img-cropper__picture-box" style={placement(box)}>
|
|
223
|
+
<img class="img-cropper__picture" src={cropper.source} alt="" draggable="false" style={pictureStyle(box)} />
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="img-cropper__outline" style={placement(frame)}></div>
|
|
227
|
+
{#if cropper.cropMode}
|
|
228
|
+
{#if cropper.selection}
|
|
229
|
+
{@const selection = cropper.selection}
|
|
230
|
+
<div
|
|
231
|
+
class="img-cropper__selection"
|
|
232
|
+
class:img-cropper__selection--dragging={drag !== null}
|
|
233
|
+
role="presentation"
|
|
234
|
+
style={placement({ x: frame.x + selection.x, y: frame.y + selection.y, w: selection.w, h: selection.h })}
|
|
235
|
+
onpointerdown={startMoveSelection}>
|
|
236
|
+
</div>
|
|
237
|
+
{#each CORNERS as corner (corner)}
|
|
238
|
+
{@const point = cornerPoint(selection, corner)}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
class="img-cropper__handle img-cropper__handle--{corner}"
|
|
242
|
+
aria-label={loc.resizeSelection}
|
|
243
|
+
tabindex="-1"
|
|
244
|
+
style:left="{frame.x + point.x}px"
|
|
245
|
+
style:top="{frame.y + point.y}px"
|
|
246
|
+
onpointerdown={(event) => startSelectionHandle(event, corner)}>
|
|
247
|
+
</button>
|
|
248
|
+
{/each}
|
|
249
|
+
{/if}
|
|
250
|
+
{:else}
|
|
251
|
+
<div
|
|
252
|
+
class="img-cropper__hit"
|
|
253
|
+
class:img-cropper__hit--dragging={drag !== null}
|
|
254
|
+
role="presentation"
|
|
255
|
+
style={placement({ x: frame.x + box.x, y: frame.y + box.y, w: box.w, h: box.h })}
|
|
256
|
+
onpointerdown={startPan}>
|
|
257
|
+
<div class="img-cropper__picture-outline"></div>
|
|
258
|
+
{#each CORNERS as corner (corner)}
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
class="img-cropper__handle img-cropper__handle--{corner} img-cropper__handle--picture"
|
|
262
|
+
aria-label={loc.resizeHandle}
|
|
263
|
+
tabindex="-1"
|
|
264
|
+
style:left={corner === 'ne' || corner === 'se' ? '100%' : '0'}
|
|
265
|
+
style:top={corner === 'sw' || corner === 'se' ? '100%' : '0'}
|
|
266
|
+
onpointerdown={(event) => startScale(event, corner)}>
|
|
267
|
+
</button>
|
|
268
|
+
{/each}
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
92
271
|
{/if}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<ImgCropperControls cropper={cropper} />
|
|
272
|
+
</div>
|
|
273
|
+
{#if toolbar}
|
|
274
|
+
<div class="img-cropper__toolbar" inert={drag !== null}>
|
|
275
|
+
{@render toolbar()}
|
|
98
276
|
</div>
|
|
99
277
|
{/if}
|
|
100
278
|
</div>
|
|
101
279
|
|
|
102
280
|
<!--
|
|
103
281
|
@component
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`overflow: hidden` to clip the shadow overlay at its boundaries.
|
|
110
|
-
|
|
111
|
-
#### Modes
|
|
112
|
-
|
|
113
|
-
The behavior is determined by the `ImgCropper` instance passed via the `cropper` prop:
|
|
114
|
-
|
|
115
|
-
- **`cover`** — the image always fills the canvas completely. The crop selection
|
|
116
|
-
defines the visible area. After save, the result is the selected region at
|
|
117
|
-
natural image resolution.
|
|
118
|
-
- **`contain`** — the image is fitted inside the canvas with optional fill color
|
|
119
|
-
for empty space. After crop, the canvas shrinks to match the cropped area's
|
|
120
|
-
aspect ratio. After save, the result includes the fill color background.
|
|
121
|
-
|
|
122
|
-
#### Aspect ratio
|
|
123
|
-
|
|
124
|
-
Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
|
|
125
|
-
|
|
126
|
-
- **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
|
|
127
|
-
- **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
|
|
128
|
-
ratios and an optional `initial` value. The toolbar renders a ratio popover when
|
|
129
|
-
multiple options are available. `allowFreeCrop: true` adds a free-ratio option
|
|
130
|
-
that lets the user draw an unconstrained selection.
|
|
131
|
-
- **Omitted** — defaults to free crop (no ratio constraint on the selection).
|
|
132
|
-
|
|
133
|
-
#### Shadow overlay
|
|
134
|
-
|
|
135
|
-
When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
|
|
136
|
-
`box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
|
|
137
|
-
the canvas bounds so the surrounding area is dimmed. The parent element should set
|
|
138
|
-
`overflow: hidden` to clip the shadow at its boundaries.
|
|
282
|
+
Fixed-frame picture cropper — the frame stays put on the stage; drag pans the picture, the corner handles (shown while
|
|
283
|
+
the pointer is over the picture, always on touch) scale it about the opposite corner, and the wheel zooms about the
|
|
284
|
+
pointer. The part of the picture spilling past the frame stays visible but dimmed. Pass a `toolbar` snippet to mount
|
|
285
|
+
`ImgCropperToolbar` underneath the picture area. `src` and `cropper` are read once, on mount — to change either, remount
|
|
286
|
+
the view (e.g. `{#key}`) with a new `ImgCropper`.
|
|
139
287
|
|
|
140
288
|
### CSS Custom Properties
|
|
141
|
-
|
|
289
|
+
| Property | Description | Default |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `--sc-kit--img-cropper--handle-color` | Corner handle fill | `#ffffff` |
|
|
292
|
+
| `--sc-kit--img-cropper--outline-color` | Frame outline color | `#4a4a4f` |
|
|
142
293
|
-->
|
|
143
294
|
|
|
144
295
|
<style>.img-cropper {
|
|
145
|
-
--
|
|
296
|
+
--_img-cropper--handle-color: var(--sc-kit--img-cropper--handle-color, #ffffff);
|
|
297
|
+
--_img-cropper--outline-color: var(--sc-kit--img-cropper--outline-color, #4a4a4f);
|
|
298
|
+
--_img-cropper--background: var(--sc-kit--img-cropper--background, #1e1e20);
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-direction: column;
|
|
301
|
+
inline-size: 100%;
|
|
302
|
+
block-size: 100%;
|
|
303
|
+
overflow: hidden;
|
|
304
|
+
background: var(--_img-cropper--background);
|
|
305
|
+
}
|
|
306
|
+
.img-cropper__stage {
|
|
146
307
|
position: relative;
|
|
147
|
-
|
|
148
|
-
|
|
308
|
+
flex: 1;
|
|
309
|
+
min-block-size: 0;
|
|
310
|
+
touch-action: none;
|
|
311
|
+
user-select: none;
|
|
312
|
+
}
|
|
313
|
+
.img-cropper__stage--dragging {
|
|
314
|
+
cursor: grabbing;
|
|
315
|
+
}
|
|
316
|
+
.img-cropper__stage--crop-mode {
|
|
317
|
+
cursor: crosshair;
|
|
318
|
+
}
|
|
319
|
+
.img-cropper__toolbar {
|
|
149
320
|
display: flex;
|
|
321
|
+
flex-shrink: 0;
|
|
150
322
|
justify-content: center;
|
|
323
|
+
padding-inline: var(--sc-kit--space--4);
|
|
324
|
+
padding-block-end: var(--sc-kit--space--4);
|
|
325
|
+
}
|
|
326
|
+
.img-cropper__state {
|
|
327
|
+
position: absolute;
|
|
328
|
+
inset: 0;
|
|
329
|
+
display: flex;
|
|
151
330
|
align-items: center;
|
|
152
|
-
|
|
331
|
+
justify-content: center;
|
|
153
332
|
}
|
|
154
|
-
.img-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
min-height: 0;
|
|
159
|
-
background-color: var(--_fill-color);
|
|
160
|
-
visibility: hidden;
|
|
333
|
+
.img-cropper__hit {
|
|
334
|
+
--_img-cropper--picture-controls: hidden;
|
|
335
|
+
position: absolute;
|
|
336
|
+
cursor: grab;
|
|
161
337
|
}
|
|
162
|
-
.img-
|
|
163
|
-
|
|
338
|
+
.img-cropper__hit::before {
|
|
339
|
+
content: "";
|
|
340
|
+
position: absolute;
|
|
341
|
+
inset: -0.5rem;
|
|
342
|
+
}
|
|
343
|
+
.img-cropper__hit:hover {
|
|
344
|
+
--_img-cropper--picture-controls: visible;
|
|
345
|
+
}
|
|
346
|
+
@media (any-pointer: coarse) {
|
|
347
|
+
.img-cropper__hit {
|
|
348
|
+
--_img-cropper--picture-controls: visible;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
.img-cropper__hit--dragging {
|
|
352
|
+
--_img-cropper--picture-controls: visible;
|
|
353
|
+
cursor: grabbing;
|
|
354
|
+
}
|
|
355
|
+
.img-cropper__picture-box {
|
|
356
|
+
position: absolute;
|
|
357
|
+
pointer-events: none;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
}
|
|
360
|
+
.img-cropper__picture-box--outside {
|
|
361
|
+
opacity: 0.35;
|
|
362
|
+
}
|
|
363
|
+
.img-cropper__picture {
|
|
364
|
+
position: absolute;
|
|
365
|
+
max-inline-size: none;
|
|
366
|
+
translate: -50% -50%;
|
|
367
|
+
}
|
|
368
|
+
.img-cropper__frame {
|
|
369
|
+
--_img-cropper--fill-color: var(--_--img-cropper--fill-color, transparent);
|
|
370
|
+
--_check: rgb(255 255 255 / 10%);
|
|
371
|
+
position: absolute;
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
background-image: linear-gradient(var(--_img-cropper--fill-color), var(--_img-cropper--fill-color)), linear-gradient(45deg, var(--_check) 25%, transparent 25%, transparent 75%, var(--_check) 75%), linear-gradient(45deg, var(--_check) 25%, transparent 25%, transparent 75%, var(--_check) 75%);
|
|
374
|
+
background-size: 100% 100%, 1rem 1rem, 1rem 1rem;
|
|
375
|
+
background-position: 0 0, 0 0, 0.5rem 0.5rem;
|
|
164
376
|
}
|
|
165
|
-
.img-
|
|
166
|
-
|
|
377
|
+
.img-cropper__outline {
|
|
378
|
+
position: absolute;
|
|
379
|
+
border: 1px solid var(--_img-cropper--outline-color);
|
|
380
|
+
pointer-events: none;
|
|
167
381
|
}
|
|
168
|
-
.img-
|
|
382
|
+
.img-cropper__picture-outline {
|
|
169
383
|
position: absolute;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
transform: translate(-50%, -50%);
|
|
173
|
-
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
|
|
384
|
+
inset: 0;
|
|
385
|
+
border: 1px dashed rgba(255, 255, 255, 0.55);
|
|
174
386
|
pointer-events: none;
|
|
175
|
-
|
|
387
|
+
visibility: var(--_img-cropper--picture-controls);
|
|
388
|
+
}
|
|
389
|
+
.img-cropper__selection {
|
|
390
|
+
position: absolute;
|
|
391
|
+
border: 1px dashed rgba(255, 255, 255, 0.55);
|
|
392
|
+
box-shadow: 0 0 0 999rem rgba(0, 0, 0, 0.55);
|
|
393
|
+
cursor: grab;
|
|
394
|
+
}
|
|
395
|
+
.img-cropper__selection--dragging {
|
|
396
|
+
cursor: grabbing;
|
|
176
397
|
}
|
|
177
|
-
.img-
|
|
398
|
+
.img-cropper__handle {
|
|
178
399
|
position: absolute;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
400
|
+
inline-size: 0.75rem;
|
|
401
|
+
block-size: 0.75rem;
|
|
402
|
+
padding: 0;
|
|
403
|
+
border: 0;
|
|
404
|
+
background: transparent;
|
|
405
|
+
translate: -50% -50%;
|
|
406
|
+
cursor: nwse-resize;
|
|
407
|
+
}
|
|
408
|
+
.img-cropper__handle::before {
|
|
409
|
+
content: "";
|
|
410
|
+
position: absolute;
|
|
411
|
+
inset: 0.1875rem;
|
|
412
|
+
border-radius: 50%;
|
|
413
|
+
background: var(--_img-cropper--handle-color);
|
|
414
|
+
box-shadow: var(--sc-kit--shadow--sm);
|
|
415
|
+
}
|
|
416
|
+
.img-cropper__handle--ne, .img-cropper__handle--sw {
|
|
417
|
+
cursor: nesw-resize;
|
|
418
|
+
}
|
|
419
|
+
.img-cropper__handle--picture {
|
|
420
|
+
visibility: var(--_img-cropper--picture-controls);
|
|
421
|
+
}
|
|
422
|
+
.img-cropper__handle:focus-visible {
|
|
423
|
+
outline: 2px solid var(--sc-kit--color--border--focus);
|
|
424
|
+
outline-offset: 1px;
|
|
183
425
|
}</style>
|
|
@@ -1,49 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ImgCropper } from './img-cropper.svelte';
|
|
2
|
+
import { type Snippet } from 'svelte';
|
|
2
3
|
type Props = {
|
|
4
|
+
/** The picture — same-origin, a `blob:` / `data:` URL, or a server that sends CORS headers; anything else fails to load. */
|
|
3
5
|
src: string;
|
|
4
6
|
cropper: ImgCropper;
|
|
5
|
-
|
|
6
|
-
showControls?: boolean;
|
|
7
|
+
toolbar?: Snippet;
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `overflow: hidden` to clip the shadow overlay at its boundaries.
|
|
15
|
-
*
|
|
16
|
-
* #### Modes
|
|
17
|
-
*
|
|
18
|
-
* The behavior is determined by the `ImgCropper` instance passed via the `cropper` prop:
|
|
19
|
-
*
|
|
20
|
-
* - **`cover`** — the image always fills the canvas completely. The crop selection
|
|
21
|
-
* defines the visible area. After save, the result is the selected region at
|
|
22
|
-
* natural image resolution.
|
|
23
|
-
* - **`contain`** — the image is fitted inside the canvas with optional fill color
|
|
24
|
-
* for empty space. After crop, the canvas shrinks to match the cropped area's
|
|
25
|
-
* aspect ratio. After save, the result includes the fill color background.
|
|
26
|
-
*
|
|
27
|
-
* #### Aspect ratio
|
|
28
|
-
*
|
|
29
|
-
* Aspect ratio is configured on the `ImgCropper` instance via the `aspectRatio` option:
|
|
30
|
-
*
|
|
31
|
-
* - **Fixed number** (e.g. `16/9`) — locks the crop selection to this ratio.
|
|
32
|
-
* - **Dynamic object** (`ImgCropperDynamicAspectRatio`) — provides a list of `supported`
|
|
33
|
-
* ratios and an optional `initial` value. The toolbar renders a ratio popover when
|
|
34
|
-
* multiple options are available. `allowFreeCrop: true` adds a free-ratio option
|
|
35
|
-
* that lets the user draw an unconstrained selection.
|
|
36
|
-
* - **Omitted** — defaults to free crop (no ratio constraint on the selection).
|
|
37
|
-
*
|
|
38
|
-
* #### Shadow overlay
|
|
39
|
-
*
|
|
40
|
-
* When `showImageShadow` is enabled on the cropper instance, a dark semi-transparent
|
|
41
|
-
* `box-shadow` is applied directly to the `<cropper-canvas>` element, extending beyond
|
|
42
|
-
* the canvas bounds so the surrounding area is dimmed. The parent element should set
|
|
43
|
-
* `overflow: hidden` to clip the shadow at its boundaries.
|
|
10
|
+
* Fixed-frame picture cropper — the frame stays put on the stage; drag pans the picture, the corner handles (shown while
|
|
11
|
+
* the pointer is over the picture, always on touch) scale it about the opposite corner, and the wheel zooms about the
|
|
12
|
+
* pointer. The part of the picture spilling past the frame stays visible but dimmed. Pass a `toolbar` snippet to mount
|
|
13
|
+
* `ImgCropperToolbar` underneath the picture area. `src` and `cropper` are read once, on mount — to change either, remount
|
|
14
|
+
* the view (e.g. `{#key}`) with a new `ImgCropper`.
|
|
44
15
|
*
|
|
45
16
|
* ### CSS Custom Properties
|
|
46
|
-
*
|
|
17
|
+
* | Property | Description | Default |
|
|
18
|
+
* |---|---|---|
|
|
19
|
+
* | `--sc-kit--img-cropper--handle-color` | Corner handle fill | `#ffffff` |
|
|
20
|
+
* | `--sc-kit--img-cropper--outline-color` | Frame outline color | `#4a4a4f` |
|
|
47
21
|
*/
|
|
48
22
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
49
23
|
type Cmp = ReturnType<typeof Cmp>;
|