Programmatic Transform
Control position, size, and rotation of elements via API including groups and multi-selections
When building custom UI for Polotno, you may need to transform elements programmatically — changing position, size, or rotation through input fields or buttons rather than the canvas transformer.
While Polotno provides an on-canvas transformer that handles grouped and multi-selected elements automatically, there's no direct API to perform the same bulk operations programmatically. This guide shows how to implement your own transform logic.
Understanding Polotno Limitations
Polotno groups don't have their own x, y, width, height, or rotation properties. A group is just a container — only its children have transform properties. When you transform a group on canvas, Polotno internally transforms each child element.
To replicate this behavior via API, you need to:
- Extract all leaf elements (non-group children) from your selection
- Calculate the combined bounding box
- Apply transforms to each element individually
Required Utilities
Polotno provides math utilities that make this easier:
import {
getTotalClientRect,
getCenter,
rotateAroundPoint,
} from 'polotno/utils/math';
import { forEveryChild } from 'polotno/model/group-model';getTotalClientRect(shapes)— returns the axis-aligned bounding box of elements, accounting for rotationgetCenter(rect)— returns the center point of a rectanglerotateAroundPoint(shape, angleDelta, center)— rotates a shape around a pointforEveryChild(group, callback)— iterates through all children of a group (including nested)
Why use
getTotalClientRectfor the center? A simple calculation likex + width/2only works for non-rotated elements. When an element is already rotated, its visual center shifts.getTotalClientRectreturns the correct bounding box accounting for rotation, sogetCentergives you the true visual center.
Extracting Shapes from Selection
First, get all leaf elements from your selection, handling groups properly:
function getShapes(elements) {
const shapes = [];
elements.forEach((el) => {
if (el.type === 'group') {
forEveryChild(el, (child) => {
if (child.type !== 'group') {
shapes.push(child);
}
});
} else {
shapes.push(el);
}
});
return shapes;
}
// Usage
const shapes = getShapes(store.selectedElements);Transforming a Single Element
For a single non-group element, you can set properties directly:
const element = store.selectedElements[0];
// Position
element.set({ x: 100, y: 200 });
// Size (with locked aspect ratio)
const ratio = element.height / element.width;
element.set({ width: 300, height: 300 * ratio });
// Rotation around center (use bounding box for correct center even when rotated)
const bbox = getTotalClientRect([element]);
const center = getCenter(bbox);
const targetRotation = 45;
const delta = targetRotation - (element.rotation || 0);
const newShape = rotateAroundPoint(
{
x: element.x,
y: element.y,
width: element.width,
height: element.height,
rotation: element.rotation || 0,
},
delta,
center
);
element.set(newShape);Transforming Multiple Elements
For multiple selected elements, calculate the bounding box and transform each element relative to it:
const shapes = getShapes(store.selectedElements);
const bbox = getTotalClientRect(shapes);
// Move all elements by delta
function moveElements(deltaX, deltaY) {
shapes.forEach((shape) => {
shape.set({ x: shape.x + deltaX, y: shape.y + deltaY });
});
}
// Scale all elements proportionally from top-left
function scaleElements(newWidth) {
const scale = newWidth / bbox.width;
const originX = bbox.x;
const originY = bbox.y;
shapes.forEach((shape) => {
shape.set({
x: originX + (shape.x - originX) * scale,
y: originY + (shape.y - originY) * scale,
width: shape.width * scale,
height: shape.height * scale,
});
});
}
// Rotate all elements around bounding box center
function rotateElements(angleDelta) {
const center = getCenter(bbox);
shapes.forEach((shape) => {
const newShape = rotateAroundPoint(
{
x: shape.x,
y: shape.y,
width: shape.width,
height: shape.height,
rotation: shape.rotation || 0,
},
angleDelta,
center
);
shape.set(newShape);
});
}Transforming Groups
Groups work the same as multiple selections — extract the children and transform them:
const group = store.selectedElements[0]; // assuming it's a group
const shapes = [];
forEveryChild(group, (child) => {
if (child.type !== 'group') {
shapes.push(child);
}
});
const bbox = getTotalClientRect(shapes);
const center = getCenter(bbox);
// Rotate group children by 15 degrees
shapes.forEach((shape) => {
const newShape = rotateAroundPoint(
{
x: shape.x,
y: shape.y,
width: shape.width,
height: shape.height,
rotation: shape.rotation || 0,
},
15, // angle delta
center
);
shape.set(newShape);
});Handling Rotation State
When building UI controls for rotation, you need to track the "current rotation" of a group or multi-selection. Since there's no single rotation value, use a local state that:
- Initializes to the common rotation of all shapes (or 0 if mixed)
- Resets when selection changes
- Syncs when rotation changes externally (e.g., via canvas handles)
const [localRotation, setLocalRotation] = React.useState(0);
const prevSelectionRef = React.useRef(null);
// Reset on selection change
React.useEffect(() => {
const selectionKey = elements.map((e) => e.id).join(',');
if (selectionKey !== prevSelectionRef.current) {
prevSelectionRef.current = selectionKey;
// Initialize to current rotation
const rotations = shapes.map((el) => el.rotation || 0);
const allSame = rotations.every((r) => r === rotations[0]);
setLocalRotation(allSame ? rotations[0] : 0);
}
}, [elements]);