@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 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
  [![npm](https://img.shields.io/npm/v/@we-are-singular/svelte-chop-chop)](https://www.npmjs.com/package/@we-are-singular/svelte-chop-chop)
7
- [![license](https://img.shields.io/github/license/we-are-singular/svelte-chop-chop)](LICENSE)
7
+ [![license](https://img.shields.io/github/license/we-are-singular/svelte-chop-chop?cache=0)](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 | 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
- | `grid` | `"none" \| "rule-of-thirds" \| "grid" \| "golden-ratio"` | `"rule-of-thirds"` | Grid overlay |
87
- | `transitions` | `boolean` | `true` | CSS transitions |
88
- | `stencil` | `Component` | `CropStencil` | Custom stencil component |
89
- | `readOnly` | `boolean` | `false` | Disable interaction |
90
- | `class` / `style` | `string` | `''` | Root element styling |
91
-
92
- **Bindable:** `coordinates` (`CropCoordinates`), `transforms` (`TransformState`)
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 '@we-are-singular/svelte-chop-chop/headless';
101
- import { createImageEditor } from '@we-are-singular/svelte-chop-chop';
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('/photo.jpg');
106
- const result = await cropper.export({ format: 'image/webp', quality: 0.9 });
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('/photo.jpg');
111
- editor.applyFilter('clarendon');
111
+ await editor.loadImage("/photo.jpg");
112
+ editor.applyFilter("clarendon");
112
113
  editor.rotate(90);
113
- const result = await editor.export({ format: 'image/webp', quality: 0.9 });
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 | Description |
122
- |--------|-------------|
123
- | `@we-are-singular/svelte-chop-chop/plugins/filters` | 16 Instagram-style color filter presets |
124
- | `@we-are-singular/svelte-chop-chop/plugins/finetune` | Brightness, contrast, saturation, exposure, etc. |
125
- | `@we-are-singular/svelte-chop-chop/plugins/frame` | Decorative frame at export (solid, line, hook) |
126
- | `@we-are-singular/svelte-chop-chop/plugins/watermark` | Text watermark at export |
127
- | `@we-are-singular/svelte-chop-chop/plugins/resize` | Output width/height controls |
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 '@we-are-singular/svelte-chop-chop';
133
+ import type { ChopPlugin } from "@we-are-singular/svelte-chop-chop";
133
134
 
134
135
  const myPlugin: ChopPlugin = {
135
- name: 'my-plugin',
136
+ name: "my-plugin",
136
137
  setup(ctx) {
137
138
  ctx.registerAction({
138
- id: 'my-action',
139
- label: 'My Action',
140
- group: 'tabs',
141
- execute: () => ctx.showPanel('my-panel'),
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 () => { /* cleanup */ };
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 | Aspect | Max Size | Format |
165
- |--------|--------|----------|--------|
166
- | `profilePicture` | 1:1 | 512px | JPEG |
167
- | `coverPhoto` | 16:9 | min 1200px wide | JPEG |
168
- | `productImage` | 1:1 | — | PNG |
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 '@we-are-singular/svelte-chop-chop/themes/default'; /* or dark, minimal */
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 | Description |
195
- |------|-------------|
196
- | `@we-are-singular/svelte-chop-chop` | Components, composables, types |
197
- | `@we-are-singular/svelte-chop-chop/headless` | `createCropper` composable |
198
- | `@we-are-singular/svelte-chop-chop/plugins` | All plugin factories |
199
- | `@we-are-singular/svelte-chop-chop/presets` | Preset bundles |
200
- | `@we-are-singular/svelte-chop-chop/themes/default` | Default CSS theme |
201
- | `@we-are-singular/svelte-chop-chop/themes/dark` | Dark CSS theme |
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 | Action |
209
- |-----|--------|
210
- | `R` / `Shift+R` | Rotate 90° CW / CCW |
211
- | `H` / `V` | Flip horizontal / vertical |
212
- | `Ctrl+Z` / `Ctrl+Shift+Z` | Undo / Redo |
213
- | `+` / `-` | Zoom in / out |
214
- | `0` | Reset crop to image bounds |
215
- | `Escape` | Reset all transforms |
216
- | `Ctrl+F` | Toggle filters panel |
217
- | Arrow keys | Move crop 1px (10px with Shift) |
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 '../core/types.js';
7
- import { createDragHandler } from '../core/interactions.js';
8
- import DragHandle from './DragHandle.svelte';
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 = $derived(rect.width / 2);
36
- const cy = $derived(rect.height / 2);
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="chop-circle-mask">
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(#chop-circle-mask)"
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 position="nw" {onresize} {onresizestart} {onresizeend} size={16} />
86
- <DragHandle position="ne" {onresize} {onresizestart} {onresizeend} size={16} />
87
- <DragHandle position="sw" {onresize} {onresizestart} {onresizeend} size={16} />
88
- <DragHandle position="se" {onresize} {onresizestart} {onresizeend} size={16} />
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 '../core/types.js';
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 '../core/types.js';
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="chop-crop-mask">
25
+ <mask id={maskId}>
25
26
  <rect x="0" y="0" width="100%" height="100%" fill="white" />
26
27
  <rect
27
- x="{maskX}"
28
- y="{maskY}"
29
- width="{rect.width}"
30
- height="{rect.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(#chop-crop-mask)"
42
+ mask="url(#{maskId})"
42
43
  />
43
44
  </svg>
44
45
 
@@ -1,4 +1,4 @@
1
- import type { Rect } from '../core/types.js';
1
+ import type { Rect } from "../core/types.js";
2
2
  type $$ComponentProps = {
3
3
  rect: Rect;
4
4
  imageBounds: Rect;
@@ -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;