Skip to content

sabine/svelte-crop-window

Repository files navigation

svelte-crop-window

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.

video cropper

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.

Installation

npm install svelte-crop-window

Basic use

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>

CropWindow.svelte Component

Props

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
};

Options

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
};

Overlay.svelte Component

Options

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
};

How to Crop

Display in HTML Without Actually Cropping:

<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.

Pseudo Code to Crop

  1. Choose a target_height and calculate the target_width for the cropped image:
let target_width = value.aspect * target_height;
  1. Calculate factor s by which to scale:
let s = (value.scale * target_height) / media.height;
  1. Scale media by s:
let resized_media = scale(media, s);
  1. Rotate media by value.rotation:
let resized_and_rotated_media = rotate(resized_media, value.rotation);
  1. 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;
  1. Extract area:
let cropped_media =
    extract_area(resized_and_rotated_media,
                 left, top, target_width, target_height);

What this component doesn't do

  1. 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.
  2. 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.

Developing

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

Acknowledgements

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.