Custom Stencil
#
IdeaThis 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 structureA 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 PropsFirst 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 coordinatesThe 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> )}
#
TransitionsTransitions 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 coordinatesYou 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 ratioThe 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}
#
OverlayThe 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%; }}
#
HandlerLet'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#
PreparingYou 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 handlerWe'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:
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 stepsIt's almost ready. We should add only the last callbacks:
- Callback
onDragEnd
forDraggableElement
- Callbacks
onMove
andonMoveEnd
forDraggableArea
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> )}
#
ResultThe full ready-to-use source code of this example is here.