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:
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.
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
aspectRatioproperty 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 imagewidth- the image widthheight- the image heighttransforms- the transforms applied to the imagerevoke- flag that indicates should be the image revoked (bywindow/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: MoveDirectionsinterface 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:
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
onDragEndforDraggableElement - Callbacks
onMoveandonMoveEndforDraggableArea
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.