Coding Transformations for Figma Plugins

A practical guide to rotation, scaling, and the object flipping

Jan 27, 2025 · 8 min read

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 here

stay hungry, stay foolish

-Steve Jobs

©realvjyvijay verma