Skip to main content

Custom Stencil

Idea#

This guide describes the process of creating an one of possible stencils and you can apply this ideas to implement arbitrary stencil.

Let's consider the following stencil:

Internal structure

It doesn't look like the default stencils, at least because it has only one handler which is arranged not on the corner of a bounding box.

In addition, this stencil has the different resize logic. Unlike the default stencils it expands in all directions simultaneously.

Internal structure

Basic structure#

A custom stencil is always a React component.

import React from 'react';
export const CircleStencil = () => {    return (        <div className="circle-stencil"></div>    )}

Let's define basic requirements to a typical stencil:

  • it should be inscribed to box is represented by coordinates (width, height, left, top)
  • if stencil has aspect ratios it should provide aspectRatio property to inform the cropper resize algorithm about it (this method should return object with minimum and maximum aspect ratio values)
  • it should call the corresponding cropper methods when user interact with the stencil (moveCoordinates, moveCoordinatesEnd, resizeCoordinates, resizeCoordinatesEnd, )
  • it should display the cropped part of a image
  • it should display the overlay around the stencil

Stencil Props#

First of all, it's need to describe service props that come from the cropper.

cropper#

It's the ref to the cropper instance.

image#

This prop is the object, that describes the properties of image and has following properties:

  • src - the link to the image
  • width - the image width
  • height - the image height
  • transforms - the transforms applied to the image
  • revoke - flag that indicates should be the image revoked (by window/revokeObjectURL)
  • arrayBuffer - the content of the image in bytes

Stencil coordinates#

The stencil coordinates can be easily calculated from state, so you should set the coordinates of your stencil himself. It was done on purpose, to give you possibility to create the custom stencil more flexible.

That's how it can be done:

import React from 'react';import { StencilProps } from 'react-advanced-cropper';
export const CircleStencil = ({ cropper }: StencilProps) => {    const coordinates = cropper.getStencilCoordinates();    return (        <div className="circle-stencil"></div>    )}

Transitions#

Transitions can be received in the similar way:

import React from 'react';import { StencilProps } from 'react-advanced-cropper';
export const CircleStencil = ({ cropper }: StencilProps) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();    return (        <div className="circle-stencil"></div>    )}

Applying the stencil coordinates#

You can compute the style himself, but it's recommended to use StencilWrapper component.

import React from 'react';import {    StencilProps,    StencilWrapper} from 'react-advanced-cropper';
export const CircleStencil = ({ cropper }: StencilProps) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>        </StencilWrapper>    )}

Aspect ratio#

The stencil should inform the cropper that he has aspect ratio limitation. You should pass the object with the following type as a ref:

{    aspectRatio: number | {        minimum?: number;        maximum?: number;    }}

Therefore the functional component should be wrapped in forwardRef method.

import React, { useImperativeHandle, forwardRef } from 'react';import {    StencilRef,    StencilProps,    StencilWrapper} from 'react-advanced-cropper';
export const CircleStencil = forwardRef<StencilRef, StencilProps> (({ cropper }: StencilProps, ref) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();
    useImperativeHandle(ref, () => ({        aspectRatio: 1,    }));
    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>        </StencilWrapper>    )}
tip

Notice, that in this example aspectRatio is equal to 1. That's because it should be always a circle. It will be converted lately into:

{    minimum: 1,    maximum: 1}

Overlay#

The preferable way to create the overlay (i.e. black background around the stencil) is the using StencilOverlay. There are other techniques, but they can produce some glitches during the transitions.

import React, { useImperativeHandle, forwardRef } from 'react';import {    StencilRef,    StencilProps,    StencilWrapper,    StencilOverlay} from 'react-advanced-cropper';
export const CircleStencil = forwardRef<StencilRef, StencilProps> (({ cropper }: StencilProps, ref) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();
    useImperativeHandle(ref, () => ({        aspectRatio: 1,    }));
    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>            <StencilOverlay className="circle-stencil__overlay" />        </StencilWrapper>    )}

It's time to add some styling now to make the stencil overlay round and add the little border to it:

.circle-stencil {    &__overlay {        cursor: move;    border: dashed 2px white;    box-sizing: border-box;        border-radius: 50%;    }}

Handler#

Let's add the handler to resize the stencil:

import React, { useImperativeHandle, forwardRef } from 'react';import {    StencilRef,    StencilProps,    StencilWrapper,  StencilOverlay} from 'react-advanced-cropper';
const handlerIcon = require('./handler.svg');
export const CircleStencil = forwardRef<StencilRef, StencilProps> (    ({ cropper }: StencilProps, ref) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();
    useImperativeHandle(ref, () => ({        aspectRatio: 1,    }));
    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>            <img src={handlerIcon} className="circle-stencil__handler" />            <StencilOverlay className="circle-stencil__overlay" />        </StencilWrapper>    )}
.circle-stencil {    &__handler {        position: absolute;        right: 15%;        top: 14%;        z-index: 1;        cursor: ne-resize;        width: 30px;        height: 30px;        display: flex;        align-items: center;        justify-content: center;        transform: translate(50%, -50%);    }    &__overlay {        cursor: move;    border: dashed 2px white;    box-sizing: border-box;        border-radius: 50%;    }    &__draggable-area {        width: 100%;        height: 100%;    }}

Events handling#

Preparing#

You may handle drag events himself, but this library provides two very useful components for this goal: DraggableElement and DraggableArea. The first one is used for different handlers, lines and etc, the second one is used for dragging the stencil itself.

import React, { useImperativeHandle, forwardRef } from 'react';import {    StencilRef,    StencilProps,    StencilWrapper,  StencilOverlay} from 'react-advanced-cropper';
const handlerIcon = require('./handler.svg');
export const CircleStencil = forwardRef<StencilRef, StencilProps>(    ({ cropper }: StencilProps, ref) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();
    useImperativeHandle(ref, () => ({        aspectRatio: 1,    }));
    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>            <DraggableElement className="circle-stencil__handler">            <img src={handlerIcon} />        </DraggableElement>            <DraggableArea className="circle-stencil__draggable-area">                <StencilOverlay className="circle-stencil__overlay" />            </DraggableArea>        </StencilWrapper>    )}

Notice, that we still didn't callbacks for the used components. It's time to do it.

Moving handler#

We've' used DraggableElement before to wrap the handler. It has onDrag handler, that called on handler drag.

This callback has only two arguments:

  • shift: MoveDirections
    interface MoveDirections {  left: number;  top: number;}
  • nativeEvent: MouseEvent | TouchEvent

The first one is the most important. It tells the current shift of the handler, i.e. how much pixels the handler was moved away from its current position.

We should transform it to resize directions:

Resize directions

The draft of onResize method is represented below

const onResizeHandler = (shift: MoveDirections) => {    cropper.resizeCoordinates(        {            left: shift.left,            right: shift.left,            top: shift.top,            bottom: shift.top,        },        {            compensate: true,        },    );}
tip

All values has the same sign. It's because the resize directions tells the cropper how much each of the stencil edges should be moved. For example, a positive value for left will move left edge to left, a positive value for right will move right edge to right.

tip

The compensate property tells the cropper that if the stencil can't be resized due to intersection of the cropper boundary, the stencil should try to compensate in another directions.

Last steps#

It's almost ready. We should add only the last callbacks:

  • Callback onDragEnd for DraggableElement
  • Callbacks onMove and onMoveEnd for DraggableArea

Fortunately, they are trivial. We just pass the default cropper methods to them.

import React, { useImperativeHandle, forwardRef } from 'react';import {    StencilRef,    StencilProps,    StencilWrapper,  StencilOverlay,  DraggableElement,  DraggableArea,    MoveDirections,} from 'react-advanced-cropper';
const handlerIcon = require('./handler.svg');
export const CircleStencil = forwardRef<StencilRef, StencilProps>(    ({ cropper }: StencilProps, ref) => {    const coordinates = cropper.getStencilCoordinates();    const transitions = cropper.getTransitions();
    useImperativeHandle(ref, () => ({        aspectRatio: 1,    }));
    const onResize = (shift: MoveDirections) => {        cropper.resizeCoordinates('center', {            left: shift.left,            top: shift.left,        });    };
    const onMove = (directions: MoveDirections) => {        cropper.moveCoordinates(directions);    };
    return (        <StencilWrapper className="circle-stencil" transitions={transitions} {...coordinates}>            <DraggableElement                className="circle-stencil__handler"                onMove={onResize}                onMoveEnd={cropper.resizeCoordinatesEnd}            >            <img src={handlerIcon} />        </DraggableElement>            <DraggableArea                className="circle-stencil__draggable-area"                onMove={onMove}                onMoveEnd={cropper.moveCoordinatesEnd}            >                <StencilOverlay className="circle-stencil__overlay" />            </DraggableArea>        </StencilWrapper>    )}

Result#

The full ready-to-use source code of this example is here.