@we-are-singular/svelte-chop-chop 0.1.0 → 1.0.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/README.md +64 -61
- package/dist/components/CircleStencil.svelte +38 -17
- package/dist/components/CircleStencil.svelte.d.ts +1 -1
- package/dist/components/CropOverlay.svelte +8 -7
- package/dist/components/CropOverlay.svelte.d.ts +1 -1
- package/dist/components/Cropper.svelte +9 -0
- package/dist/components/Cropper.svelte.d.ts +2 -0
- package/dist/components/ImageEditor.svelte +651 -260
- package/dist/components/ImageEditor.svelte.d.ts +2 -0
- package/dist/composables/create-cropper.svelte.d.ts +8 -4
- package/dist/composables/create-cropper.svelte.js +81 -32
- package/dist/composables/create-image-editor.svelte.d.ts +2 -0
- package/dist/composables/create-image-editor.svelte.js +7 -1
- package/dist/core/crop-engine.svelte.d.ts +4 -1
- package/dist/core/crop-engine.svelte.js +73 -9
- package/dist/core/export.d.ts +1 -1
- package/dist/core/export.js +31 -22
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# svelte-chop-chop
|
|
2
2
|
|
|
3
|
-
> Headless-first image cropping and editing SDK for Svelte 5.
|
|
3
|
+
> Headless-first image cropping and editing SDK for Svelte 5.
|
|
4
4
|
> Zero dependencies beyond Svelte.
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/@we-are-singular/svelte-chop-chop)
|
|
7
|
-
[](LICENSE)
|
|
7
|
+
[](LICENSE)
|
|
8
8
|
|
|
9
9
|
**[Documentation & Demo →](https://svelte-chop-chop.pages.dev)**
|
|
10
10
|
|
|
@@ -78,18 +78,19 @@ npm install @we-are-singular/svelte-chop-chop
|
|
|
78
78
|
|
|
79
79
|
## Cropper Props
|
|
80
80
|
|
|
81
|
-
| Prop
|
|
82
|
-
|
|
83
|
-
| `src`
|
|
84
|
-
| `aspectRatio`
|
|
85
|
-
| `sizeConstraints`
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
| Prop | Type | Default | Description |
|
|
82
|
+
| ------------------ | -------------------------------------------------------- | ------------------ | ----------------------------------------------------------- |
|
|
83
|
+
| `src` | `ImageSource` | — | URL, data URL, `File`, `Blob`, or `HTMLImageElement` |
|
|
84
|
+
| `aspectRatio` | `number \| { min?: number; max?: number } \| null` | `null` | Aspect ratio constraint |
|
|
85
|
+
| `sizeConstraints` | `SizeConstraints` | — | Min/max width/height in pixels |
|
|
86
|
+
| `initialCropScale` | `number` | `1` | Initial crop as fraction of image (0–1). 0.8 = 80% centered |
|
|
87
|
+
| `grid` | `"none" \| "rule-of-thirds" \| "grid" \| "golden-ratio"` | `"rule-of-thirds"` | Grid overlay |
|
|
88
|
+
| `transitions` | `boolean` | `true` | CSS transitions |
|
|
89
|
+
| `stencil` | `Component` | `CropStencil` | Custom stencil component |
|
|
90
|
+
| `readOnly` | `boolean` | `false` | Disable interaction |
|
|
91
|
+
| `class` / `style` | `string` | `''` | Root element styling |
|
|
92
|
+
|
|
93
|
+
**Bindable:** `coordinates` (`CropCoordinates`), `transforms` (`TransformState`)
|
|
93
94
|
**Events:** `onchange`, `onready`, `onerror`
|
|
94
95
|
|
|
95
96
|
---
|
|
@@ -97,20 +98,20 @@ npm install @we-are-singular/svelte-chop-chop
|
|
|
97
98
|
## Headless Usage
|
|
98
99
|
|
|
99
100
|
```typescript
|
|
100
|
-
import { createCropper } from
|
|
101
|
-
import { createImageEditor } from
|
|
101
|
+
import { createCropper } from "@we-are-singular/svelte-chop-chop/headless";
|
|
102
|
+
import { createImageEditor } from "@we-are-singular/svelte-chop-chop";
|
|
102
103
|
|
|
103
|
-
// Lightweight cropper
|
|
104
|
+
// Lightweight cropper (export uses full resolution — original image pixels)
|
|
104
105
|
const cropper = createCropper({ aspectRatio: 16 / 9 });
|
|
105
|
-
await cropper.loadImage(
|
|
106
|
-
const result = await cropper.export({ format:
|
|
106
|
+
await cropper.loadImage("/photo.jpg");
|
|
107
|
+
const result = await cropper.export({ format: "image/webp", quality: 0.9 });
|
|
107
108
|
|
|
108
109
|
// Full editor
|
|
109
110
|
const editor = createImageEditor({ plugins: [pluginFilters()] });
|
|
110
|
-
await editor.loadImage(
|
|
111
|
-
editor.applyFilter(
|
|
111
|
+
await editor.loadImage("/photo.jpg");
|
|
112
|
+
editor.applyFilter("clarendon");
|
|
112
113
|
editor.rotate(90);
|
|
113
|
-
const result = await editor.export({ format:
|
|
114
|
+
const result = await editor.export({ format: "image/webp", quality: 0.9 });
|
|
114
115
|
// result.blob, result.dataURL, result.canvas, result.coordinates
|
|
115
116
|
```
|
|
116
117
|
|
|
@@ -118,34 +119,36 @@ const result = await editor.export({ format: 'image/webp', quality: 0.9 });
|
|
|
118
119
|
|
|
119
120
|
## Plugins
|
|
120
121
|
|
|
121
|
-
| Import
|
|
122
|
-
|
|
123
|
-
| `@we-are-singular/svelte-chop-chop/plugins/filters`
|
|
124
|
-
| `@we-are-singular/svelte-chop-chop/plugins/finetune`
|
|
125
|
-
| `@we-are-singular/svelte-chop-chop/plugins/frame`
|
|
126
|
-
| `@we-are-singular/svelte-chop-chop/plugins/watermark` | Text watermark at export
|
|
127
|
-
| `@we-are-singular/svelte-chop-chop/plugins/resize`
|
|
122
|
+
| Import | Description |
|
|
123
|
+
| ----------------------------------------------------- | ------------------------------------------------ |
|
|
124
|
+
| `@we-are-singular/svelte-chop-chop/plugins/filters` | 16 Instagram-style color filter presets |
|
|
125
|
+
| `@we-are-singular/svelte-chop-chop/plugins/finetune` | Brightness, contrast, saturation, exposure, etc. |
|
|
126
|
+
| `@we-are-singular/svelte-chop-chop/plugins/frame` | Decorative frame at export (solid, line, hook) |
|
|
127
|
+
| `@we-are-singular/svelte-chop-chop/plugins/watermark` | Text watermark at export |
|
|
128
|
+
| `@we-are-singular/svelte-chop-chop/plugins/resize` | Output width/height controls |
|
|
128
129
|
|
|
129
130
|
### Custom Plugin
|
|
130
131
|
|
|
131
132
|
```typescript
|
|
132
|
-
import type { ChopPlugin } from
|
|
133
|
+
import type { ChopPlugin } from "@we-are-singular/svelte-chop-chop";
|
|
133
134
|
|
|
134
135
|
const myPlugin: ChopPlugin = {
|
|
135
|
-
name:
|
|
136
|
+
name: "my-plugin",
|
|
136
137
|
setup(ctx) {
|
|
137
138
|
ctx.registerAction({
|
|
138
|
-
id:
|
|
139
|
-
label:
|
|
140
|
-
group:
|
|
141
|
-
execute: () => ctx.showPanel(
|
|
139
|
+
id: "my-action",
|
|
140
|
+
label: "My Action",
|
|
141
|
+
group: "tabs",
|
|
142
|
+
execute: () => ctx.showPanel("my-panel"),
|
|
142
143
|
});
|
|
143
144
|
|
|
144
145
|
ctx.registerPostProcessor(async (drawCtx, canvas) => {
|
|
145
146
|
// draw on canvas at export time
|
|
146
147
|
});
|
|
147
148
|
|
|
148
|
-
return () => {
|
|
149
|
+
return () => {
|
|
150
|
+
/* cleanup */
|
|
151
|
+
};
|
|
149
152
|
},
|
|
150
153
|
};
|
|
151
154
|
```
|
|
@@ -161,18 +164,18 @@ import { profilePicture, coverPhoto, productImage } from '@we-are-singular/svelt
|
|
|
161
164
|
<Cropper src="/photo.jpg" {...profilePicture} />
|
|
162
165
|
```
|
|
163
166
|
|
|
164
|
-
| Preset
|
|
165
|
-
|
|
166
|
-
| `profilePicture` | 1:1
|
|
167
|
-
| `coverPhoto`
|
|
168
|
-
| `productImage`
|
|
167
|
+
| Preset | Aspect | Max Size | Format |
|
|
168
|
+
| ---------------- | ------ | --------------- | ------ |
|
|
169
|
+
| `profilePicture` | 1:1 | 512px | JPEG |
|
|
170
|
+
| `coverPhoto` | 16:9 | min 1200px wide | JPEG |
|
|
171
|
+
| `productImage` | 1:1 | — | PNG |
|
|
169
172
|
|
|
170
173
|
---
|
|
171
174
|
|
|
172
175
|
## Theming
|
|
173
176
|
|
|
174
177
|
```css
|
|
175
|
-
@import
|
|
178
|
+
@import "@we-are-singular/svelte-chop-chop/themes/default"; /* or dark, minimal */
|
|
176
179
|
```
|
|
177
180
|
|
|
178
181
|
Override any CSS custom property:
|
|
@@ -191,30 +194,30 @@ Override any CSS custom property:
|
|
|
191
194
|
|
|
192
195
|
## Package Exports
|
|
193
196
|
|
|
194
|
-
| Path
|
|
195
|
-
|
|
196
|
-
| `@we-are-singular/svelte-chop-chop`
|
|
197
|
-
| `@we-are-singular/svelte-chop-chop/headless`
|
|
198
|
-
| `@we-are-singular/svelte-chop-chop/plugins`
|
|
199
|
-
| `@we-are-singular/svelte-chop-chop/presets`
|
|
200
|
-
| `@we-are-singular/svelte-chop-chop/themes/default` | Default CSS theme
|
|
201
|
-
| `@we-are-singular/svelte-chop-chop/themes/dark`
|
|
202
|
-
| `@we-are-singular/svelte-chop-chop/themes/minimal` | Minimal CSS theme
|
|
197
|
+
| Path | Description |
|
|
198
|
+
| -------------------------------------------------- | ------------------------------ |
|
|
199
|
+
| `@we-are-singular/svelte-chop-chop` | Components, composables, types |
|
|
200
|
+
| `@we-are-singular/svelte-chop-chop/headless` | `createCropper` composable |
|
|
201
|
+
| `@we-are-singular/svelte-chop-chop/plugins` | All plugin factories |
|
|
202
|
+
| `@we-are-singular/svelte-chop-chop/presets` | Preset bundles |
|
|
203
|
+
| `@we-are-singular/svelte-chop-chop/themes/default` | Default CSS theme |
|
|
204
|
+
| `@we-are-singular/svelte-chop-chop/themes/dark` | Dark CSS theme |
|
|
205
|
+
| `@we-are-singular/svelte-chop-chop/themes/minimal` | Minimal CSS theme |
|
|
203
206
|
|
|
204
207
|
---
|
|
205
208
|
|
|
206
209
|
## Keyboard Shortcuts
|
|
207
210
|
|
|
208
|
-
| Key
|
|
209
|
-
|
|
210
|
-
| `R` / `Shift+R`
|
|
211
|
-
| `H` / `V`
|
|
212
|
-
| `Ctrl+Z` / `Ctrl+Shift+Z` | Undo / Redo
|
|
213
|
-
| `+` / `-`
|
|
214
|
-
| `0`
|
|
215
|
-
| `Escape`
|
|
216
|
-
| `Ctrl+F`
|
|
217
|
-
| Arrow keys
|
|
211
|
+
| Key | Action |
|
|
212
|
+
| ------------------------- | ------------------------------- |
|
|
213
|
+
| `R` / `Shift+R` | Rotate 90° CW / CCW |
|
|
214
|
+
| `H` / `V` | Flip horizontal / vertical |
|
|
215
|
+
| `Ctrl+Z` / `Ctrl+Shift+Z` | Undo / Redo |
|
|
216
|
+
| `+` / `-` | Zoom in / out |
|
|
217
|
+
| `0` | Reset crop to image bounds |
|
|
218
|
+
| `Escape` | Reset all transforms |
|
|
219
|
+
| `Ctrl+F` | Toggle filters panel |
|
|
220
|
+
| Arrow keys | Move crop 1px (10px with Shift) |
|
|
218
221
|
|
|
219
222
|
---
|
|
220
223
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
Same contract as CropStencil, but with circular mask.
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import type { HandlePosition, Point, StencilProps } from
|
|
7
|
-
import { createDragHandler } from
|
|
8
|
-
import DragHandle from
|
|
6
|
+
import type { HandlePosition, Point, StencilProps } from "../core/types.js";
|
|
7
|
+
import { createDragHandler } from "../core/interactions.js";
|
|
8
|
+
import DragHandle from "./DragHandle.svelte";
|
|
9
9
|
|
|
10
10
|
let {
|
|
11
11
|
rect,
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
onMove: (delta) => onmove(delta),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
const maskId = $state(`chop-circle-mask-${crypto.randomUUID()}`);
|
|
33
|
+
|
|
32
34
|
// The circle is inscribed in a 1:1 square bounding rect.
|
|
33
35
|
// cx/cy are used for the circle border positioning only.
|
|
34
36
|
const size = $derived(Math.min(rect.width, rect.height));
|
|
35
|
-
const cx
|
|
36
|
-
const cy
|
|
37
|
+
const cx = $derived(rect.width / 2);
|
|
38
|
+
const cy = $derived(rect.height / 2);
|
|
37
39
|
</script>
|
|
38
40
|
|
|
39
41
|
<div
|
|
@@ -51,14 +53,9 @@
|
|
|
51
53
|
>
|
|
52
54
|
<svg class="chop-circle-overlay" aria-hidden="true">
|
|
53
55
|
<defs>
|
|
54
|
-
<mask id=
|
|
56
|
+
<mask id={maskId}>
|
|
55
57
|
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
|
56
|
-
<circle
|
|
57
|
-
cx="{cx}"
|
|
58
|
-
cy="{cy}"
|
|
59
|
-
r="{size / 2}"
|
|
60
|
-
fill="black"
|
|
61
|
-
/>
|
|
58
|
+
<circle {cx} {cy} r={size / 2} fill="black" />
|
|
62
59
|
</mask>
|
|
63
60
|
</defs>
|
|
64
61
|
<rect
|
|
@@ -67,7 +64,7 @@
|
|
|
67
64
|
width="100%"
|
|
68
65
|
height="100%"
|
|
69
66
|
fill="var(--chop-overlay, rgba(0, 0, 0, 0.55))"
|
|
70
|
-
mask="url(#
|
|
67
|
+
mask="url(#{maskId})"
|
|
71
68
|
/>
|
|
72
69
|
</svg>
|
|
73
70
|
|
|
@@ -82,10 +79,34 @@
|
|
|
82
79
|
></div>
|
|
83
80
|
|
|
84
81
|
<!-- Handles at the 4 bounding-box corners — correct for a 1:1 square crop with circular mask -->
|
|
85
|
-
<DragHandle
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
<DragHandle
|
|
83
|
+
position="nw"
|
|
84
|
+
{onresize}
|
|
85
|
+
{onresizestart}
|
|
86
|
+
{onresizeend}
|
|
87
|
+
size={16}
|
|
88
|
+
/>
|
|
89
|
+
<DragHandle
|
|
90
|
+
position="ne"
|
|
91
|
+
{onresize}
|
|
92
|
+
{onresizestart}
|
|
93
|
+
{onresizeend}
|
|
94
|
+
size={16}
|
|
95
|
+
/>
|
|
96
|
+
<DragHandle
|
|
97
|
+
position="sw"
|
|
98
|
+
{onresize}
|
|
99
|
+
{onresizestart}
|
|
100
|
+
{onresizeend}
|
|
101
|
+
size={16}
|
|
102
|
+
/>
|
|
103
|
+
<DragHandle
|
|
104
|
+
position="se"
|
|
105
|
+
{onresize}
|
|
106
|
+
{onresizestart}
|
|
107
|
+
{onresizeend}
|
|
108
|
+
size={16}
|
|
109
|
+
/>
|
|
89
110
|
</div>
|
|
90
111
|
|
|
91
112
|
<style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HandlePosition, Point, StencilProps } from
|
|
1
|
+
import type { HandlePosition, Point, StencilProps } from "../core/types.js";
|
|
2
2
|
type $$ComponentProps = StencilProps & {
|
|
3
3
|
onmove: (delta: Point) => void;
|
|
4
4
|
onresize: (handle: HandlePosition, delta: Point) => void;
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
Positioned to cover imageBounds; cutout at rect.
|
|
5
5
|
-->
|
|
6
6
|
<script lang="ts">
|
|
7
|
-
import type { Rect } from
|
|
7
|
+
import type { Rect } from "../core/types.js";
|
|
8
8
|
|
|
9
9
|
let { rect, imageBounds }: { rect: Rect; imageBounds: Rect } = $props();
|
|
10
10
|
|
|
11
|
+
const maskId = $state(`chop-crop-mask-${crypto.randomUUID()}`);
|
|
11
12
|
const maskX = $derived(rect.x - imageBounds.x);
|
|
12
13
|
const maskY = $derived(rect.y - imageBounds.y);
|
|
13
14
|
</script>
|
|
@@ -21,13 +22,13 @@
|
|
|
21
22
|
style:height="{imageBounds.height}px"
|
|
22
23
|
>
|
|
23
24
|
<defs>
|
|
24
|
-
<mask id=
|
|
25
|
+
<mask id={maskId}>
|
|
25
26
|
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
|
26
27
|
<rect
|
|
27
|
-
x=
|
|
28
|
-
y=
|
|
29
|
-
width=
|
|
30
|
-
height=
|
|
28
|
+
x={maskX}
|
|
29
|
+
y={maskY}
|
|
30
|
+
width={rect.width}
|
|
31
|
+
height={rect.height}
|
|
31
32
|
fill="black"
|
|
32
33
|
/>
|
|
33
34
|
</mask>
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
width="100%"
|
|
39
40
|
height="100%"
|
|
40
41
|
fill="var(--chop-overlay, rgba(0, 0, 0, 0.55))"
|
|
41
|
-
mask="url(#
|
|
42
|
+
mask="url(#{maskId})"
|
|
42
43
|
/>
|
|
43
44
|
</svg>
|
|
44
45
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
sizeConstraints,
|
|
24
24
|
cropOutsideImage = false,
|
|
25
25
|
initialCrop,
|
|
26
|
+
initialCropScale = 1,
|
|
26
27
|
grid = "rule-of-thirds",
|
|
27
28
|
gridOnlyActive = true,
|
|
28
29
|
transitions = true,
|
|
@@ -48,6 +49,8 @@
|
|
|
48
49
|
sizeConstraints?: import("../core/types.js").SizeConstraints;
|
|
49
50
|
cropOutsideImage?: boolean;
|
|
50
51
|
initialCrop?: Partial<Rect>;
|
|
52
|
+
/** Initial crop as fraction of image (0–1). 1 = full image, 0.8 = 80% centered. Default 1. */
|
|
53
|
+
initialCropScale?: number;
|
|
51
54
|
grid?: import("../core/types.js").GridType;
|
|
52
55
|
gridOnlyActive?: boolean;
|
|
53
56
|
transitions?: boolean;
|
|
@@ -90,6 +93,9 @@
|
|
|
90
93
|
get initialCrop() {
|
|
91
94
|
return initialCrop;
|
|
92
95
|
},
|
|
96
|
+
get initialCropScale() {
|
|
97
|
+
return initialCropScale;
|
|
98
|
+
},
|
|
93
99
|
get grid() {
|
|
94
100
|
return grid;
|
|
95
101
|
},
|
|
@@ -99,6 +105,9 @@
|
|
|
99
105
|
get readOnly() {
|
|
100
106
|
return readOnly;
|
|
101
107
|
},
|
|
108
|
+
get shape() {
|
|
109
|
+
return StencilComponent === CircleStencil ? "circle" : "rect";
|
|
110
|
+
},
|
|
102
111
|
});
|
|
103
112
|
|
|
104
113
|
/** Only show grid lines while the user is actively interacting (dragging/resizing). */
|
|
@@ -7,6 +7,8 @@ type $$ComponentProps = {
|
|
|
7
7
|
sizeConstraints?: import("../core/types.js").SizeConstraints;
|
|
8
8
|
cropOutsideImage?: boolean;
|
|
9
9
|
initialCrop?: Partial<Rect>;
|
|
10
|
+
/** Initial crop as fraction of image (0–1). 1 = full image, 0.8 = 80% centered. Default 1. */
|
|
11
|
+
initialCropScale?: number;
|
|
10
12
|
grid?: import("../core/types.js").GridType;
|
|
11
13
|
gridOnlyActive?: boolean;
|
|
12
14
|
transitions?: boolean;
|