Hey there! If you're building Figma plugins, you probably know that moving things around on the canvas is super important for making useful tools. Some things are pretty easy - like moving stuff around or making it bigger. But when you try to rotate or flip objects... well, that's where things can get tricky!
I learned this the hard way while building my plugin "project animate", which helps automate smart component animations. At first, I thought "Oh, this should be simple!" but I quickly found myself diving deep into coordinate systems and transformation matrices (fancy words for "how things move and change on screen"). It took some time and a lot of testing, but I finally figured out some really solid ways to make it all work.
Now I want to share these solutions with you! Whether you're just starting with plugin development or looking to add cool features to your existing plugin, I think you'll find these code snippets really helpful.
The Basics of Transformation Matrices
In Figma, transformations are represented using 2x3 matrices, which allow us to perform operations like rotation, scaling, and translation. Let's start with a simple rotation operation:
function getTransformMatrix(rotation: number): Transform {
const angle = (rotation * Math.PI) / 180;
return [
[Math.cos(angle), -Math.sin(angle), 0],
[Math.sin(angle), Math.cos(angle), 0]
];
}
This function converts a rotation angle from degrees to radians and creates a transformation matrix. The matrix uses sine and cosine functions to handle the rotation. The resulting matrix can be directly applied to a Figma node's relativeTransform property.
Normalizing Rotations
When working with rotations, it's important to keep angles within a manageable range. Here's how we normalize rotation angles to stay between -180 and 180 degrees:
function normalizeRotation(rotation: number): number {
rotation = rotation % 360;
if (rotation > 180) {
rotation -= 360;
} else if (rotation < -180) {
rotation += 360;
}
return rotation;
}
This normalization ensures consistent behavior when rotating objects, preventing issues with extremely large rotation values.
Rotating Around a Center Point
One of the more complex operations is rotating a node around its center point. Here's a simplified version that maintains the object's center position:
function rotateNodeAroundCenter(node: SceneNode, angle: number): void {
if (!('relativeTransform' in node)) return;
// Calculate the center point
const centerX = node.x + (node.width / 2);
const centerY = node.y + (node.height / 2);
// Convert angle to radians and calculate transformation
const theta = (angle * Math.PI) / 180;
const cos = Math.cos(theta);
const sin = Math.sin(theta);
// Create transformation matrix
const transformMatrix = [
[cos, sin, 0],
[-sin, cos, 0]
];
// Apply transformation while maintaining center
node.relativeTransform = transformMatrix;
// Adjust position to keep center point stable
const newCenterX = node.x + (node.width / 2);
const newCenterY = node.y + (node.height / 2);
node.x += centerX - newCenterX;
node.y += centerY - newCenterY;
}
This function demonstrates several important concepts:
- Finding the center point of an object
- Converting angles from degrees to radians
- Creating a rotation matrix
- Maintaining the center point during rotation
Understanding Flipping Operations
One of the most complex transformations in Figma is flipping an object, either horizontally or vertically. This complexity arises because we need to consider both the object's own transformation and its parent's transformation to maintain correct positioning. Let's break down how flipping works:
function calculateFlipMatrix(
node: SceneNode,
direction: 'horizontal' | 'vertical'
): Transform {
// Get both transforms
const currentTransform = node.relativeTransform;
const absoluteTransform = node.absoluteTransform;
// Get parent transform (or identity matrix if no parent)
const parentTransform = node.parent && 'absoluteTransform' in node.parent
? node.parent.absoluteTransform
: [[1, 0, 0], [0, 1, 0]];
// Calculate parent inverse transform
const parentDet = parentTransform[0][0] * parentTransform[1][1] -
parentTransform[0][1] * parentTransform[1][0];
const parentInverse = [
[
parentTransform[1][1] / parentDet,
-parentTransform[0][1] / parentDet,
(parentTransform[0][1] * parentTransform[1][2] -
parentTransform[1][1] * parentTransform[0][2]) / parentDet
],
[
-parentTransform[1][0] / parentDet,
parentTransform[0][0] / parentDet,
(parentTransform[1][0] * parentTransform[0][2] -
parentTransform[0][0] * parentTransform[1][2]) / parentDet
]
];
// Get bounds and calculate center
const bounds = node.absoluteBoundingBox;
if (!bounds) {
throw new Error('Node has no bounding box');
}
// Calculate center in absolute coordinates
const absoluteCenterX = bounds.x + bounds.width / 2;
const absoluteCenterY = bounds.y + bounds.height / 2;
// Convert to parent-relative coordinates
const relativeCenterX = parentInverse[0][0] * absoluteCenterX +
parentInverse[0][1] * absoluteCenterY +
parentInverse[0][2];
const relativeCenterY = parentInverse[1][0] * absoluteCenterX +
parentInverse[1][1] * absoluteCenterY +
parentInverse[1][2];
// Create flip matrix
const flipMatrix = direction === 'horizontal'
? [
[-1, 0, 2 * relativeCenterX],
[0, 1, 0]
]
: [
[1, 0, 0],
[0, -1, 2 * relativeCenterY]
];
// Combine with current transform
return multiplyTransformMatrices(flipMatrix, currentTransform);
}
Let's break down why this operation is so complex:
Coordinate Spaces: When flipping an object, we need to work with three different coordinate spaces: 1. The object's local space, 2. The parent's coordinate space, 3. The absolute (screen) coordinate space
Transform Matrices: We need to calculate the inverse of the parent's transform matrix to convert between coordinate spaces. This is necessary because we want to flip the object relative to its visual center, not its coordinate origin.
Center Point Calculation: The flip needs to happen around the object's visual center. We calculate this in absolute coordinates, then convert it back to the parent's coordinate space for the actual flip operation. Matrix Multiplication: Finally, we need to combine the flip transformation with the object's existing transformation to preserve any previous rotations or scales.
Here's a simpler way to think about what's happening:
- We find the object's center point in screen coordinates
- We convert this center point to the parent's coordinate system
- We create a flip matrix that will mirror the object around this center point
- We combine this with any existing transformations
To use this in practice, you would call it like this:
function flipNode(node: SceneNode, direction: 'horizontal' | 'vertical') {
const newTransform = calculateFlipMatrix(node, direction);
node.relativeTransform = newTransform;
}
The complexity of this operation comes from Figma's hierarchical coordinate system. When you flip an object, you want it to flip visually around its center point, but you need to express this flip in terms of the parent's coordinate system while preserving all existing transformations. This is why we need to do all these coordinate space conversions.
The Critical Order of Transformations
One of the trickiest parts of working with transformations is handling multiple operations at once. The order in which you apply transformations matters a lot - get it wrong, and objects might end up in unexpected positions or with strange behaviors. Let me share snippets from my plugin and discuss about the right order:
function applyStateProperties(node: SceneNode, properties: StateProperties) {
// Store original properties
const originalWidth = node.width;
const originalHeight = node.height;
const originalCenter = getNodeCenter(node);
// 1. Handle position first
if (properties.x !== undefined || properties.y !== undefined) {
node.x = properties.x ?? node.x;
node.y = properties.y ?? node.y;
}
// 2. Apply scaling (before rotation)
if (properties.scale !== undefined) {
const newWidth = originalWidth * properties.scale;
const newHeight = originalHeight * properties.scale;
if (hasResize(node)) {
// Scale from center point
const widthDiff = newWidth - node.width;
const heightDiff = newHeight - node.height;
node.resize(newWidth, newHeight);
node.x -= widthDiff / 2;
node.y -= heightDiff / 2;
}
}
// 3. Apply rotation last
if (properties.rotation !== undefined) {
applyRotation(node, properties.rotation);
}
}
Here's why this order matters:
Position First: We start with basic position changes because they're the simplest. They don't affect the object's internal properties or how other transformations will work.
Scale Second: Scaling comes before rotation to directly adjust the object's dimensions and ensure it scales around the center point. This keeps the math simpler and avoids distortions, as scaling after rotation could align with rotated axes instead of the natural x/y axes. Scaling first maintains consistent and predictable proportions.
Rotation Last: Rotation is applied last because it’s the most complex transformation. It ensures that the final position and size are accurate, as applying rotation after scaling and positioning would cause issues with alignment and calculations.
Common Pitfalls to Avoid
When working with multiple transformations, watch out for these common issues:
Center Point Changes: Every transformation can affect the object's center point. Keep track of the original center if you need it:
function getNodeCenter(node: SceneNode): { x: number; y: number } {
return {
x: node.x + node.width / 2,
y: node.y + node.height / 2
};
}
const originalCenter = getNodeCenter(node);
Scale Before Position: If you need to both scale and position precisely, consider scaling first, then applying the final position. This avoids having to calculate position offsets based on scaled dimensions.
Rotation Complications: If you need to rotate and then position, you might need to: Store the original center point then apply the rotation and calculate the new position based on the rotated dimensions and then apply the final position adjustment
Conclusion
When implementing transformations in your Figma plugin, keep these points in mind:
- Always check if a node supports transformations before applying them:
if (!('relativeTransform' in node)) return;
-
Use relative transformations when possible, as they're easier to reason about than absolute transformations.
-
Remember to convert between degrees and radians when working with angles:
const radians = (degrees * Math.PI) / 180;
-
When dealing with rotations, always normalize angles to prevent unexpected behavior.
-
Keep track of the center point when performing transformations to maintain expected positioning.
-
Remember that Figma's coordinate system uses a top-left origin point, and all transformations should take this into account. When in doubt, test your transformations with simple shapes first before applying them to more complex objects.
This lil devlog covered the basics of rotations and scaling, but there's much more to explore in the world of Figma transformations. Check out the figma documentation.
WIP of the Project animate plugin
Here is Demo You can signup herestay hungry, stay foolish
-Steve Jobs
©realvjy✦vijay verma