Polotno Docs
Demos

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:

  1. Extract all leaf elements (non-group children) from your selection
  2. Calculate the combined bounding box
  3. 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 rotation
  • getCenter(rect) — returns the center point of a rectangle
  • rotateAroundPoint(shape, angleDelta, center) — rotates a shape around a point
  • forEveryChild(group, callback) — iterates through all children of a group (including nested)

Why use getTotalClientRect for the center? A simple calculation like x + width/2 only works for non-rotated elements. When an element is already rotated, its visual center shifts. getTotalClientRect returns the correct bounding box accounting for rotation, so getCenter gives 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:

  1. Initializes to the common rotation of all shapes (or 0 if mixed)
  2. Resets when selection changes
  3. 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]);

Live demo