@we-are-singular/svelte-chop-chop 0.1.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/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/components/.gitkeep +0 -0
- package/dist/components/CircleStencil.svelte +126 -0
- package/dist/components/CircleStencil.svelte.d.ts +10 -0
- package/dist/components/CropOverlay.svelte +51 -0
- package/dist/components/CropOverlay.svelte.d.ts +8 -0
- package/dist/components/CropStencil.svelte +84 -0
- package/dist/components/CropStencil.svelte.d.ts +10 -0
- package/dist/components/Cropper.svelte +242 -0
- package/dist/components/Cropper.svelte.d.ts +32 -0
- package/dist/components/DragHandle.svelte +129 -0
- package/dist/components/DragHandle.svelte.d.ts +13 -0
- package/dist/components/FilterStrip.svelte +58 -0
- package/dist/components/FilterStrip.svelte.d.ts +9 -0
- package/dist/components/GridOverlay.svelte +85 -0
- package/dist/components/GridOverlay.svelte.d.ts +9 -0
- package/dist/components/ImageEditor.svelte +1087 -0
- package/dist/components/ImageEditor.svelte.d.ts +16 -0
- package/dist/components/Toolbar.svelte +103 -0
- package/dist/components/Toolbar.svelte.d.ts +7 -0
- package/dist/composables/.gitkeep +0 -0
- package/dist/composables/create-cropper.svelte.d.ts +49 -0
- package/dist/composables/create-cropper.svelte.js +257 -0
- package/dist/composables/create-image-editor.svelte.d.ts +20 -0
- package/dist/composables/create-image-editor.svelte.js +596 -0
- package/dist/composables/create-transform.svelte.d.ts +28 -0
- package/dist/composables/create-transform.svelte.js +26 -0
- package/dist/core/.gitkeep +0 -0
- package/dist/core/color-matrix.d.ts +39 -0
- package/dist/core/color-matrix.js +137 -0
- package/dist/core/constraints.d.ts +46 -0
- package/dist/core/constraints.js +107 -0
- package/dist/core/coordinate-system.d.ts +65 -0
- package/dist/core/coordinate-system.js +185 -0
- package/dist/core/crop-engine.svelte.d.ts +33 -0
- package/dist/core/crop-engine.svelte.js +192 -0
- package/dist/core/export.d.ts +14 -0
- package/dist/core/export.js +99 -0
- package/dist/core/history-manager.svelte.d.ts +22 -0
- package/dist/core/history-manager.svelte.js +72 -0
- package/dist/core/image-loader.svelte.d.ts +17 -0
- package/dist/core/image-loader.svelte.js +126 -0
- package/dist/core/interactions.d.ts +52 -0
- package/dist/core/interactions.js +118 -0
- package/dist/core/keyboard.d.ts +11 -0
- package/dist/core/keyboard.js +23 -0
- package/dist/core/transform-engine.svelte.d.ts +27 -0
- package/dist/core/transform-engine.svelte.js +79 -0
- package/dist/core/types.d.ts +265 -0
- package/dist/core/types.js +5 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +35 -0
- package/dist/plugins/.gitkeep +0 -0
- package/dist/plugins/index.d.ts +8 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/plugin-filters.d.ts +14 -0
- package/dist/plugins/plugin-filters.js +100 -0
- package/dist/plugins/plugin-finetune.d.ts +10 -0
- package/dist/plugins/plugin-finetune.js +23 -0
- package/dist/plugins/plugin-frame.d.ts +11 -0
- package/dist/plugins/plugin-frame.js +81 -0
- package/dist/plugins/plugin-resize.d.ts +10 -0
- package/dist/plugins/plugin-resize.js +23 -0
- package/dist/plugins/plugin-watermark.d.ts +10 -0
- package/dist/plugins/plugin-watermark.js +86 -0
- package/dist/presets/.gitkeep +0 -0
- package/dist/presets/cover-photo.d.ts +14 -0
- package/dist/presets/cover-photo.js +14 -0
- package/dist/presets/index.d.ts +6 -0
- package/dist/presets/index.js +6 -0
- package/dist/presets/product-image.d.ts +11 -0
- package/dist/presets/product-image.js +11 -0
- package/dist/presets/profile-picture.d.ts +17 -0
- package/dist/presets/profile-picture.js +17 -0
- package/dist/themes/.gitkeep +0 -0
- package/dist/themes/dark.css +17 -0
- package/dist/themes/default.css +23 -0
- package/dist/themes/minimal.css +17 -0
- package/package.json +118 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 We Are Singular
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# svelte-chop-chop
|
|
2
|
+
|
|
3
|
+
> Headless-first image cropping and editing SDK for Svelte 5.
|
|
4
|
+
> Zero dependencies beyond Svelte.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@we-are-singular/svelte-chop-chop)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
**[Documentation & Demo →](https://svelte-chop-chop.pages.dev)**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **`<Cropper>`** — Lightweight crop component with handles, grid overlays, and transforms
|
|
16
|
+
- **`<ImageEditor>`** — Full-featured editor with plugin-driven toolbar
|
|
17
|
+
- **Headless composables** — `createCropper` and `createImageEditor` for fully custom UI
|
|
18
|
+
- **Plugin system** — filters, finetune, frame, watermark, resize (all opt-in)
|
|
19
|
+
- **Presets** — `profilePicture`, `coverPhoto`, `productImage`
|
|
20
|
+
- **Three themes** — default, dark, minimal (CSS custom properties)
|
|
21
|
+
- **Zero runtime dependencies** — peer dep: `svelte ^5.0.0` only
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @we-are-singular/svelte-chop-chop
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Cropper
|
|
36
|
+
|
|
37
|
+
```svelte
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import { Cropper } from '@we-are-singular/svelte-chop-chop';
|
|
40
|
+
import '@we-are-singular/svelte-chop-chop/themes/default';
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<Cropper src="/photo.jpg" aspectRatio={16 / 9} style="height: 400px;" />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Image Editor
|
|
47
|
+
|
|
48
|
+
```svelte
|
|
49
|
+
<script lang="ts">
|
|
50
|
+
import { ImageEditor } from '@we-are-singular/svelte-chop-chop';
|
|
51
|
+
import { pluginFilters } from '@we-are-singular/svelte-chop-chop/plugins/filters';
|
|
52
|
+
import { pluginFinetune } from '@we-are-singular/svelte-chop-chop/plugins/finetune';
|
|
53
|
+
import { pluginFrame } from '@we-are-singular/svelte-chop-chop/plugins/frame';
|
|
54
|
+
import { pluginWatermark } from '@we-are-singular/svelte-chop-chop/plugins/watermark';
|
|
55
|
+
import { pluginResize } from '@we-are-singular/svelte-chop-chop/plugins/resize';
|
|
56
|
+
import type { ExportResult } from '@we-are-singular/svelte-chop-chop';
|
|
57
|
+
import '@we-are-singular/svelte-chop-chop/themes/default';
|
|
58
|
+
|
|
59
|
+
function handleExport(result: ExportResult) {
|
|
60
|
+
const url = URL.createObjectURL(result.blob!);
|
|
61
|
+
const a = document.createElement('a');
|
|
62
|
+
a.href = url;
|
|
63
|
+
a.download = 'edited.jpg';
|
|
64
|
+
a.click();
|
|
65
|
+
URL.revokeObjectURL(url);
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<ImageEditor
|
|
70
|
+
src="/photo.jpg"
|
|
71
|
+
plugins={[pluginFilters(), pluginFinetune(), pluginFrame(), pluginWatermark(), pluginResize()]}
|
|
72
|
+
onexport={handleExport}
|
|
73
|
+
style="height: 500px;"
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Cropper Props
|
|
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`)
|
|
93
|
+
**Events:** `onchange`, `onready`, `onerror`
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Headless Usage
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { createCropper } from '@we-are-singular/svelte-chop-chop/headless';
|
|
101
|
+
import { createImageEditor } from '@we-are-singular/svelte-chop-chop';
|
|
102
|
+
|
|
103
|
+
// Lightweight cropper
|
|
104
|
+
const cropper = createCropper({ aspectRatio: 16 / 9 });
|
|
105
|
+
await cropper.loadImage('/photo.jpg');
|
|
106
|
+
const result = await cropper.export({ format: 'image/webp', quality: 0.9 });
|
|
107
|
+
|
|
108
|
+
// Full editor
|
|
109
|
+
const editor = createImageEditor({ plugins: [pluginFilters()] });
|
|
110
|
+
await editor.loadImage('/photo.jpg');
|
|
111
|
+
editor.applyFilter('clarendon');
|
|
112
|
+
editor.rotate(90);
|
|
113
|
+
const result = await editor.export({ format: 'image/webp', quality: 0.9 });
|
|
114
|
+
// result.blob, result.dataURL, result.canvas, result.coordinates
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Plugins
|
|
120
|
+
|
|
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 |
|
|
128
|
+
|
|
129
|
+
### Custom Plugin
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import type { ChopPlugin } from '@we-are-singular/svelte-chop-chop';
|
|
133
|
+
|
|
134
|
+
const myPlugin: ChopPlugin = {
|
|
135
|
+
name: 'my-plugin',
|
|
136
|
+
setup(ctx) {
|
|
137
|
+
ctx.registerAction({
|
|
138
|
+
id: 'my-action',
|
|
139
|
+
label: 'My Action',
|
|
140
|
+
group: 'tabs',
|
|
141
|
+
execute: () => ctx.showPanel('my-panel'),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
ctx.registerPostProcessor(async (drawCtx, canvas) => {
|
|
145
|
+
// draw on canvas at export time
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return () => { /* cleanup */ };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Presets
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { profilePicture, coverPhoto, productImage } from '@we-are-singular/svelte-chop-chop/presets';
|
|
159
|
+
|
|
160
|
+
// Spread into component props
|
|
161
|
+
<Cropper src="/photo.jpg" {...profilePicture} />
|
|
162
|
+
```
|
|
163
|
+
|
|
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 |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Theming
|
|
173
|
+
|
|
174
|
+
```css
|
|
175
|
+
@import '@we-are-singular/svelte-chop-chop/themes/default'; /* or dark, minimal */
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Override any CSS custom property:
|
|
179
|
+
|
|
180
|
+
```css
|
|
181
|
+
:root {
|
|
182
|
+
--chop-bg: #1a1a2e;
|
|
183
|
+
--chop-stencil-border: #e94560;
|
|
184
|
+
--chop-toolbar-active: #e94560;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Available variables:** `--chop-bg`, `--chop-canvas-bg`, `--chop-color`, `--chop-border-radius`, `--chop-stencil-border`, `--chop-stencil-border-active`, `--chop-grid-color`, `--chop-overlay`, `--chop-toolbar-bg`, `--chop-toolbar-color`, `--chop-toolbar-active`, `--chop-transition-duration`, `--chop-transition-easing`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Package Exports
|
|
193
|
+
|
|
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 |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Keyboard Shortcuts
|
|
207
|
+
|
|
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) |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT © [We Are Singular](https://github.com/we-are-singular)
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
svelte-chop-chop — Circular crop stencil
|
|
3
|
+
Same contract as CropStencil, but with circular mask.
|
|
4
|
+
-->
|
|
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';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
rect,
|
|
12
|
+
aspectRatio = 1,
|
|
13
|
+
active,
|
|
14
|
+
imageBounds,
|
|
15
|
+
grid,
|
|
16
|
+
transitions,
|
|
17
|
+
onmove,
|
|
18
|
+
onresize,
|
|
19
|
+
onresizestart,
|
|
20
|
+
onresizeend,
|
|
21
|
+
}: StencilProps & {
|
|
22
|
+
onmove: (delta: Point) => void;
|
|
23
|
+
onresize: (handle: HandlePosition, delta: Point) => void;
|
|
24
|
+
onresizestart?: () => void;
|
|
25
|
+
onresizeend?: () => void;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
const drag = createDragHandler({
|
|
29
|
+
onMove: (delta) => onmove(delta),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The circle is inscribed in a 1:1 square bounding rect.
|
|
33
|
+
// cx/cy are used for the circle border positioning only.
|
|
34
|
+
const size = $derived(Math.min(rect.width, rect.height));
|
|
35
|
+
const cx = $derived(rect.width / 2);
|
|
36
|
+
const cy = $derived(rect.height / 2);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
class="chop-stencil chop-stencil-circle"
|
|
41
|
+
class:chop-stencil-active={active}
|
|
42
|
+
style:left="{rect.x}px"
|
|
43
|
+
style:top="{rect.y}px"
|
|
44
|
+
style:width="{rect.width}px"
|
|
45
|
+
style:height="{rect.height}px"
|
|
46
|
+
role="group"
|
|
47
|
+
aria-label="Circular crop area"
|
|
48
|
+
onpointerdown={drag.onpointerdown}
|
|
49
|
+
onpointermove={drag.onpointermove}
|
|
50
|
+
onpointerup={drag.onpointerup}
|
|
51
|
+
>
|
|
52
|
+
<svg class="chop-circle-overlay" aria-hidden="true">
|
|
53
|
+
<defs>
|
|
54
|
+
<mask id="chop-circle-mask">
|
|
55
|
+
<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
|
+
/>
|
|
62
|
+
</mask>
|
|
63
|
+
</defs>
|
|
64
|
+
<rect
|
|
65
|
+
x="0"
|
|
66
|
+
y="0"
|
|
67
|
+
width="100%"
|
|
68
|
+
height="100%"
|
|
69
|
+
fill="var(--chop-overlay, rgba(0, 0, 0, 0.55))"
|
|
70
|
+
mask="url(#chop-circle-mask)"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
|
|
74
|
+
<div
|
|
75
|
+
class="chop-circle-border"
|
|
76
|
+
style:left="{cx}px"
|
|
77
|
+
style:top="{cy}px"
|
|
78
|
+
style:width="{size}px"
|
|
79
|
+
style:height="{size}px"
|
|
80
|
+
style:margin-left="-{size / 2}px"
|
|
81
|
+
style:margin-top="-{size / 2}px"
|
|
82
|
+
></div>
|
|
83
|
+
|
|
84
|
+
<!-- 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} />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.chop-stencil-circle {
|
|
93
|
+
position: absolute;
|
|
94
|
+
border: none;
|
|
95
|
+
box-sizing: border-box;
|
|
96
|
+
z-index: 5;
|
|
97
|
+
touch-action: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.chop-circle-overlay {
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: 0;
|
|
103
|
+
top: 0;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: 100%;
|
|
106
|
+
pointer-events: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.chop-circle-border {
|
|
110
|
+
position: absolute;
|
|
111
|
+
border: 2px solid var(--chop-stencil-border, rgba(255, 255, 255, 0.8));
|
|
112
|
+
border-radius: 50%;
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.chop-stencil-active .chop-circle-border {
|
|
118
|
+
border-color: var(--chop-stencil-border-active, #fff);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Give circle handles a subtle dark outline so they're visible on light images */
|
|
122
|
+
:global(.chop-stencil-circle .chop-handle) {
|
|
123
|
+
border: 2px solid rgba(0, 0, 0, 0.35);
|
|
124
|
+
box-sizing: border-box;
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HandlePosition, Point, StencilProps } from '../core/types.js';
|
|
2
|
+
type $$ComponentProps = StencilProps & {
|
|
3
|
+
onmove: (delta: Point) => void;
|
|
4
|
+
onresize: (handle: HandlePosition, delta: Point) => void;
|
|
5
|
+
onresizestart?: () => void;
|
|
6
|
+
onresizeend?: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const CircleStencil: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type CircleStencil = ReturnType<typeof CircleStencil>;
|
|
10
|
+
export default CircleStencil;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
svelte-chop-chop — Dark overlay with crop cutout
|
|
3
|
+
Shows dimmed area outside the crop rectangle.
|
|
4
|
+
Positioned to cover imageBounds; cutout at rect.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import type { Rect } from '../core/types.js';
|
|
8
|
+
|
|
9
|
+
let { rect, imageBounds }: { rect: Rect; imageBounds: Rect } = $props();
|
|
10
|
+
|
|
11
|
+
const maskX = $derived(rect.x - imageBounds.x);
|
|
12
|
+
const maskY = $derived(rect.y - imageBounds.y);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<svg
|
|
16
|
+
class="chop-overlay"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
style:left="{imageBounds.x}px"
|
|
19
|
+
style:top="{imageBounds.y}px"
|
|
20
|
+
style:width="{imageBounds.width}px"
|
|
21
|
+
style:height="{imageBounds.height}px"
|
|
22
|
+
>
|
|
23
|
+
<defs>
|
|
24
|
+
<mask id="chop-crop-mask">
|
|
25
|
+
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
|
26
|
+
<rect
|
|
27
|
+
x="{maskX}"
|
|
28
|
+
y="{maskY}"
|
|
29
|
+
width="{rect.width}"
|
|
30
|
+
height="{rect.height}"
|
|
31
|
+
fill="black"
|
|
32
|
+
/>
|
|
33
|
+
</mask>
|
|
34
|
+
</defs>
|
|
35
|
+
<rect
|
|
36
|
+
x="0"
|
|
37
|
+
y="0"
|
|
38
|
+
width="100%"
|
|
39
|
+
height="100%"
|
|
40
|
+
fill="var(--chop-overlay, rgba(0, 0, 0, 0.55))"
|
|
41
|
+
mask="url(#chop-crop-mask)"
|
|
42
|
+
/>
|
|
43
|
+
</svg>
|
|
44
|
+
|
|
45
|
+
<style>
|
|
46
|
+
.chop-overlay {
|
|
47
|
+
position: absolute;
|
|
48
|
+
pointer-events: none;
|
|
49
|
+
z-index: 2;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Rect } from '../core/types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
rect: Rect;
|
|
4
|
+
imageBounds: Rect;
|
|
5
|
+
};
|
|
6
|
+
declare const CropOverlay: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type CropOverlay = ReturnType<typeof CropOverlay>;
|
|
8
|
+
export default CropOverlay;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
svelte-chop-chop — Default rectangular stencil
|
|
3
|
+
Uses DragHandle, CropOverlay, GridOverlay.
|
|
4
|
+
-->
|
|
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';
|
|
9
|
+
import GridOverlay from './GridOverlay.svelte';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
rect,
|
|
13
|
+
aspectRatio,
|
|
14
|
+
active,
|
|
15
|
+
imageBounds,
|
|
16
|
+
grid,
|
|
17
|
+
transitions,
|
|
18
|
+
onmove,
|
|
19
|
+
onresize,
|
|
20
|
+
onresizestart,
|
|
21
|
+
onresizeend,
|
|
22
|
+
}: StencilProps & {
|
|
23
|
+
onmove: (delta: Point) => void;
|
|
24
|
+
onresize: (handle: HandlePosition, delta: Point) => void;
|
|
25
|
+
onresizestart?: () => void;
|
|
26
|
+
onresizeend?: () => void;
|
|
27
|
+
} = $props();
|
|
28
|
+
|
|
29
|
+
const drag = createDragHandler({
|
|
30
|
+
onMove: (delta) => onmove(delta),
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
class="chop-stencil chop-stencil-rect"
|
|
36
|
+
class:chop-stencil-active={active}
|
|
37
|
+
class:chop-stencil-transitions={transitions}
|
|
38
|
+
style:left="{rect.x}px"
|
|
39
|
+
style:top="{rect.y}px"
|
|
40
|
+
style:width="{rect.width}px"
|
|
41
|
+
style:height="{rect.height}px"
|
|
42
|
+
role="group"
|
|
43
|
+
aria-label="Crop area"
|
|
44
|
+
onpointerdown={drag.onpointerdown}
|
|
45
|
+
onpointermove={drag.onpointermove}
|
|
46
|
+
onpointerup={drag.onpointerup}
|
|
47
|
+
>
|
|
48
|
+
{#if !aspectRatio || aspectRatio === null}
|
|
49
|
+
<DragHandle position="nw" {onresize} {onresizestart} {onresizeend} />
|
|
50
|
+
<DragHandle position="ne" {onresize} {onresizestart} {onresizeend} />
|
|
51
|
+
<DragHandle position="sw" {onresize} {onresizestart} {onresizeend} />
|
|
52
|
+
<DragHandle position="se" {onresize} {onresizestart} {onresizeend} />
|
|
53
|
+
<DragHandle position="n" {onresize} {onresizestart} {onresizeend} />
|
|
54
|
+
<DragHandle position="s" {onresize} {onresizestart} {onresizeend} />
|
|
55
|
+
<DragHandle position="e" {onresize} {onresizestart} {onresizeend} />
|
|
56
|
+
<DragHandle position="w" {onresize} {onresizestart} {onresizeend} />
|
|
57
|
+
{:else}
|
|
58
|
+
<DragHandle position="nw" {onresize} {onresizestart} {onresizeend} />
|
|
59
|
+
<DragHandle position="ne" {onresize} {onresizestart} {onresizeend} />
|
|
60
|
+
<DragHandle position="sw" {onresize} {onresizestart} {onresizeend} />
|
|
61
|
+
<DragHandle position="se" {onresize} {onresizestart} {onresizeend} />
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
<GridOverlay {rect} {grid} {active} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<style>
|
|
68
|
+
.chop-stencil {
|
|
69
|
+
position: absolute;
|
|
70
|
+
border: 2px solid var(--chop-stencil-border, rgba(255, 255, 255, 0.8));
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
z-index: 5;
|
|
73
|
+
touch-action: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.chop-stencil-active {
|
|
77
|
+
border-color: var(--chop-stencil-border-active, #fff);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.chop-stencil-transitions {
|
|
81
|
+
transition: border-color var(--chop-transition-duration, 300ms)
|
|
82
|
+
var(--chop-transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HandlePosition, Point, StencilProps } from '../core/types.js';
|
|
2
|
+
type $$ComponentProps = StencilProps & {
|
|
3
|
+
onmove: (delta: Point) => void;
|
|
4
|
+
onresize: (handle: HandlePosition, delta: Point) => void;
|
|
5
|
+
onresizestart?: () => void;
|
|
6
|
+
onresizeend?: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const CropStencil: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type CropStencil = ReturnType<typeof CropStencil>;
|
|
10
|
+
export default CropStencil;
|