A crop window component for images and videos that supports touch gestures (pinch zoom, rotate, pan), as well as mousewheel zoom, mouse-dragging the image, and rotating on right mouse button.
Currently looking for contributors / feature requests / feedback to help improve this component.
If you can do code-review, that's very welcome.
Here's a demo page.
And here's a minimal REPL where you can play with the code and a fancier REPL.
npm install svelte-crop-window
You must wrap the CropWindow
component with an Element that determines the height.
<script>
import { CropWindow, defaultValue } from 'svelte-crop-window';
let media = {
content_type: 'image',
url: '/svelte-crop-window/hintersee-3601004.jpg'
};
let value = { ...defaultValue };
</script>
<div style="height:20em">
<CropWindow bind:value {media} />
</div>
name | type | required | purpose |
---|---|---|---|
media |
Media |
✓ | image or video to be cropped |
value |
CropValue |
value that describes how to crop - will be initialized if undefined | |
options |
Options |
options for the crop window and overlay, see below |
type Media = {
content_type: 'image' | 'video';
url: string;
}
type CropValue = {
position: { x: number; y: number };
aspect: number;
rotation: number;
scale: number; }
}
const defaultValue: CropValue = {
position: { x: 0, y: 0 },
aspect: 1.0,
rotation: 0,
scale: 0
};
name | type | purpose |
---|---|---|
shape |
'rect' | 'round' |
shape of the crop area (yes, an ellipse will work) |
crop_window_margin |
number |
Margin of the crop window, in pixels. The crop window will always scale to the maximum possible size in its containing element. |
overlay |
a Svelte component | The overlay component which visually highlights the crop area. You can pass your own Svelte component with props options: T, gesture_in_progress: boolean, shape: 'rect' | 'round' here, or use the included Overlay.svelte. |
overlay_options |
T |
Options for your overlay component. See below for the options of the included overlay component. |
const defaultOptions: Options<OverlayOptions> = {
shape: 'rect',
crop_window_margin: 10,
overlay: Overlay,
overlay_options: defaultOverlayOptions
};
name | type | purpose |
---|---|---|
overlay_color |
string |
the color of the overlay that covers everything except the crop area |
line_color |
string |
the color of the lines |
show_third_lines |
boolean |
whether to show third lines or not when a gesture is in progress |
const defaultOverlayOptions: OverlayOptions = {
overlay_color: 'rgb(11, 11, 11, 0.7)',
line_color: 'rgba(167, 167, 167, 0.5)',
show_third_lines: true
};
<div
style="
position:relative; overflow:hidden;
height:{HEIGHT}px; width:{value.aspect * HEIGHT}px;
border-radius: {options.shape == 'round' ? '50%' : '0'}"
>
<video
style="
transform: translateX(-50%) translateY(-50%) rotate({value.rotation}deg);
height: {value.scale * HEIGHT}px;
margin-left: {HEIGHT * value.aspect / 2 + value.position.x * HEIGHT}px;
margin-top: {HEIGHT / 2 + value.position.y * HEIGHT}px;
max-width:none"
src="{url}"
/>
</div>
Note: You must choose a HEIGHT
, because the crop value is normalized against the target height.
- Choose a
target_height
and calculate thetarget_width
for the cropped image:
let target_width = value.aspect * target_height;
- Calculate factor
s
by which to scale:
let s = (value.scale * target_height) / media.height;
- Scale media by
s
:
let resized_media = scale(media, s);
- Rotate media by
value.rotation
:
let resized_and_rotated_media = rotate(resized_media, value.rotation);
- Calculate top left position of the area to extract:
let left = (resized_and_rotated_media.width - target_width) / 2.0
- value.x * target_height;
let top = (resized_and_rotated_media.height - target_height) / 2.0
- value.y * target_height;
- Extract area:
let cropped_media =
extract_area(resized_and_rotated_media,
left, top, target_width, target_height);
- Does not modify/crop the image, you have to do that by whatever means make sense for your application. Doesn't (yet) provide usable controls. Currently, you need to implement your own.
- Similar to the overlay, it would be nice to include some controls to make this more usable out of the box. Contributions are very welcome.
Once you've cloned the project and installed dependencies with npm install
(or pnpm install
or yarn
), start a development server:
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
One big inspiration for this component was the Android library uCrop by Yalantis. What is particularly valuable is that the developers shared their thought process in this blog post.
Another very helpful resource was svelte-easy-crop which gave me a basic understanding of how to implement a crop window component in Svelte (and HTML/JS in general).
There's no code reuse between either of these components and this one. All calculations had to be recreated from textbook math.