Skip to main content
Version: 0.x (Reanimated 3)

Transformer

The Transformer is an interactive selection box with resize and rotate handles. Point it at a node and it draws a border plus anchors that let the user scale and rotate that node directly.

import { useState } from 'react';
import {
Transformer,
Rect,
type TransformEvent,
} from 'react-native-canvas-kit';

function Editor() {
const [selected, setSelected] = useState<string | null>(null);
const [box, setBox] = useState({
x: 60,
y: 60,
scaleX: 1,
scaleY: 1,
rotation: 0,
});

return (
<Layer onTap={() => setSelected(null)} width={width} height={height}>
<Rect
id="box"
x={box.x}
y={box.y}
scaleX={box.scaleX}
scaleY={box.scaleY}
rotation={box.rotation}
width={160}
height={110}
fill="#22d3ee"
draggable
onTap={(e) => {
setSelected('#box');
e.cancelBubble = true;
}}
/>

<Transformer
node={selected}
onTransformEnd={(e: TransformEvent) =>
setBox({
x: e.x,
y: e.y,
scaleX: e.scaleX,
scaleY: e.scaleY,
rotation: e.rotation,
})
}
/>
</Layer>
);
}
Transformer editor

Attaching to a node

The node prop is a selector string for the target: its id prefixed with # (or its name prefixed with .). Set it to null to hide the transformer. Track the current selection in state and set it from each shape's onTap.

Persisting transforms

While the user drags an anchor, the target updates live on the UI thread. When they release, onTransformEnd fires with the final transform; write it back to your state so it persists:

<Transformer node={selected} onTransformEnd={(e) => commit(selected, e)} />
Persisting transforms

onTransformEnd is a node event, so the same transform is delivered to both the Transformer and the selected node. Put the handler wherever fits your code: on the Transformer, or on the node itself alongside its onDragEnd. Both receive the identical TransformEvent shown below.

The event carries the resolved transform:

interface TransformEvent {
x: number;
y: number;
scaleX: number;
scaleY: number;
rotation: number; // degrees
targetId: number | null;
anchor: AnchorId; // which handle was dragged
}
The TransformEvent

onTransformStart and onTransform fire at the start of and during the gesture, respectively.

Props

Behavior

PropTypeDefaultDescription
nodestring | nullNoneSelector of the target node ("#id" or ".name"); null disables.
enabledAnchorsAnchorId[]allWhich handles to show (see below).
keepRatiobooleanfalseLock aspect ratio while scaling.
centeredScalingbooleanfalseScale from the center instead of the opposite anchor.
paddingnumber0Extra space between the target's bounds and the border.
ignoreStrokebooleanfalseExclude stroke width from the measured bounds.
rotationSnapsnumber[]NoneSnap rotation to these angles (degrees).
rotationSnapTolerancenumber5How close (degrees) before snapping engages.
rotateAnchorOffsetnumber50Distance of the rotate handle from the shape.

Appearance

PropTypeDefaultDescription
anchorFillstring'#ffffff'Anchor handle fill.
anchorStrokestring'rgb(0, 161, 255)'Anchor handle border.
anchorStrokeWidthnumber1Anchor border width.
anchorSizenumber10Anchor handle size.
borderStrokestring'rgb(0, 161, 255)'Selection border color.
borderStrokeWidthnumber1Selection border width.

Events

PropTypeDescription
onTransformStart(e: TransformEvent) => voidGesture begins.
onTransform(e: TransformEvent) => voidFires continuously during the gesture.
onTransformEnd(e: TransformEvent) => voidGesture ends; persist here.

Anchors

AnchorId values:

'top-left' 'top-center' 'top-right'
'middle-left' 'middle-right'
'bottom-left' 'bottom-center' 'bottom-right'
'rotater' // the rotation handle

Show only some handles, for example corners plus rotation for a uniform-scaling image:

<Transformer
node={selected}
keepRatio
enabledAnchors={[
'top-left',
'top-right',
'bottom-left',
'bottom-right',
'rotater',
]}
/>
Limiting anchors

Snapping rotation

Snap to 45° increments:

<Transformer
node={selected}
rotationSnaps={[0, 45, 90, 135, 180, 225, 270, 315]}
rotationSnapTolerance={8}
/>
Rotation snapping