DEV Community

Cover image for Transform System with the new Snap.svg (Basics - part 3)
Orlin Vakarelov
Orlin Vakarelov

Posted on

Transform System with the new Snap.svg (Basics - part 3)

This is the third tutorial in our Snap.svg series. In the previous parts we learned how to build hierarchical SVG structures and style them beautifully. Now we'll position those shapes precisely with the enhanced transform system.

In this tutorial we'll learn to:

  • use direct transform methods like translate(), scale(), and rotate() instead of string syntax,
  • understand relative vs absolute transformations with the prev_trans parameter,
  • access the current transformation state with getLocalMatrix(),
  • build and compose complex transformations step-by-step with addTransform(),
  • animate transformations smoothly with animateTransform() and translateAnimate(),

The underlying theme is that Snap.svg's direct transform methods make positioning and animating shapes far more intuitive than traditional string-based transforms.

Previous:

  1. Building SVGs with the new Snap.svg

  2. Styling and Attributes with the new Snap.svg


1. Why Direct Transform Methods Matter

Standard SVG uses string-based transforms:

// Old way: string syntax
element.attr({ transform: "translate(100,50) rotate(45)" });
Enter fullscreen mode Exit fullscreen mode

This approach has limitations:

  • Hard to read: What's the rotation center?
  • Hard to modify: How do you change just the translation?
  • Hard to animate: String parsing for every frame

Snap.svg's enhanced transform system provides direct methods that are chainable, readable, and easy to compose:

// New way: direct methods
element
  .translate(100, 50)
  .rotate(45, centerX, centerY);
Enter fullscreen mode Exit fullscreen mode

Marketing Note: These direct transform methods are a major enhancement over standard Snap.svg, making transforms as intuitive as method chaining.


2. Basic Translation: Moving Elements

The translate(x, y) method moves elements along the X and Y axes.

<div id="container">
  <svg id="mySvg" width="500" height="300" style="border: 1px solid #ccc;"></svg>
</div>

<script src="https://iaesth.ca/dist/snap.svg_ia.min.js"></script>
<script src="basic-translate.js"></script>
Enter fullscreen mode Exit fullscreen mode

Example: Moving Rectangles

// basic-translate.js
var s = Snap("#mySvg");
var group = s.g();

// Rectangle 1: No translation (origin position)
group
  .rect(0, 0, 80, 60)
  .attr({
    fill: "#3498db",
    stroke: "#2c3e50",
    strokeWidth: 2
  });

// Rectangle 2: Translate right 100px
group
  .rect(0, 0, 80, 60)
  .attr({
    fill: "#e74c3c",
    stroke: "#c0392b",
    strokeWidth: 2
  })
  .translate(100, 0);  // Move 100px right, 0px down

// Rectangle 3: Translate right 200px, down 50px
group
  .rect(0, 0, 80, 60)
  .attr({
    fill: "#2ecc71",
    stroke: "#27ae60",
    strokeWidth: 2
  })
  .translate(200, 50);  // Move 200px right, 50px down

// Rectangle 4: Multiple translations (they compose!)
group
  .rect(0, 0, 80, 60)
  .attr({
    fill: "#f39c12",
    stroke: "#e67e22",
    strokeWidth: 2
  })
  .translate(100, 0)   // First move right 100px
  .translate(200, 100); // Then move right 200px more, down 100px
                        // Final position: (300, 100)
Enter fullscreen mode Exit fullscreen mode

Output: Four rectangles at different positions, with the fourth showing how translations compose (add together).

Key Insight: By default, translate() is relative - it adds to the existing transformation. This is why the fourth rectangle ends up at (300, 100), not (200, 100).


3. Understanding prev_trans: Relative vs Absolute

All transformation methods accept an optional prev_trans parameter that controls how the new transform composes with existing ones:

  • Omitted (default): Uses current element's local matrix (relative transformation)
  • true or "id": Uses identity matrix (absolute transformation from origin)
  • Snap.matrix(): Uses specified matrix as base
// prev_trans-demo.js
var s = Snap("#mySvg");
var group = s.g();

// Rectangle 1: Multiple relative translations
var rect1 = group
  .rect(0, 0, 60, 60)
  .attr({ fill: "#3498db" })
  .translate(50, 50)    // Move to (50, 50)
  .translate(100, 0);   // Move 100 more right → final: (150, 50)

// Label for rect1
group.text(150, 130, "Relative")
  .attr({ textAnchor: "middle", fill: "#2c3e50" });

// Rectangle 2: Absolute translation (ignores previous)
var rect2 = group
  .rect(0, 0, 60, 60)
  .attr({ fill: "#e74c3c" })
  .translate(50, 50)           // Move to (50, 50)
  .translate(100, 0, true);    // ABSOLUTE to (100, 0)
                                // prev_trans = true resets matrix

// Label for rect2
group.text(100, 80, "Absolute")
  .attr({ textAnchor: "middle", fill: "#c0392b" });

// Rectangle 3: Custom base matrix
var customMatrix = Snap.matrix().translate(300, 150);
var rect3 = group
  .rect(0, 0, 60, 60)
  .attr({ fill: "#2ecc71" })
  .translate(50, 50, customMatrix);  // Apply translation to custom matrix
                                     // Final: (350, 200)

// Label for rect3 (moved lower so it doesn't overlap)
group.text(350, 270, "Custom Base")
  .attr({ textAnchor: "middle", fill: "#27ae60" });
Enter fullscreen mode Exit fullscreen mode

Output: Three rectangles demonstrating different prev_trans behaviors.


4. Rotation: Spinning Around a Center

The rotate(angle, cx, cy) method rotates elements. The angle is in degrees, and you can specify a center point.

Example: Rotating Rectangles

// basic-rotate.js
var s = Snap("#mySvg");
var group = s.g();

// Rectangle 1: No rotation
var rect1 = group
  .rect(50, 50, 100, 60)
  .attr({
    fill: "#3498db",
    stroke: "#2c3e50",
    strokeWidth: 2
  });

// Center point marker
group.circle(100, 80, 4).attr({ fill: "#c0392b" });

// Rectangle 2: Rotate 30° around its center
var rect2 = group
  .rect(200, 50, 100, 60)
  .attr({
    fill: "#e74c3c",
    stroke: "#c0392b",
    strokeWidth: 2
  })
  .rotate(30, 250, 80);  // 30° around (250, 80) - the rect center

// Center point marker
group.circle(250, 80, 4).attr({ fill: "#c0392b" });

// Rectangle 3: Rotate 45° around top-left corner
var rect3 = group
  .rect(350, 50, 100, 60)
  .attr({
    fill: "#2ecc71",
    stroke: "#27ae60",
    strokeWidth: 2
  })
  .rotate(45, 350, 50);  // 45° around top-left corner (350, 50)

// Corner point marker
group.circle(350, 50, 4).attr({ fill: "#c0392b" });

// Rectangle 4: Multiple rotations compose
var rect4 = group
  .rect(150, 180, 100, 60)
  .attr({
    fill: "#f39c12",
    stroke: "#e67e22",
    strokeWidth: 2
  })
  .rotate(15, 200, 210)   // First rotate 15°
  .rotate(30, 200, 210);  // Then rotate 30° more
                          // Total: 45° rotation

// Center point marker
group.circle(200, 210, 4).attr({ fill: "#c0392b" });
Enter fullscreen mode Exit fullscreen mode

Output: Four rectangles showing different rotation behaviors, with red dots marking rotation centers.


5. Scaling: Changing Size and Reflection

The scale(sx, sy, cx, cy) method scales elements. Use negative values for reflection (mirroring).

Example: Scaling and Reflecting

// basic-scale.js
var s = Snap("#mySvg");
var group = s.g();

// Original rectangle (reference)
group
  .rect(50, 50, 80, 60)
  .attr({
    fill: "none",
    stroke: "#95a5a6",
    strokeWidth: 1,
    strokeDasharray: "5,3"
  });

// Rectangle 1: Scale up 1.5x uniformly
group
  .rect(50, 50, 80, 60)
  .attr({
    fill: "#3498db",
    opacity: 0.7
  })
  .scale(1.5, 1.5, 50, 50);  // Scale from top-left corner

// Rectangle 2: Scale horizontally only
group
  .rect(200, 50, 80, 60)
  .attr({
    fill: "#e74c3c",
    opacity: 0.7
  })
  .scale(2, 1, 200, 80);  // 2x width, 1x height, from left center

// Rectangle 3: Reflect horizontally (negative scale) - moved below red to avoid overlap
group
  .rect(350, 130, 80, 60)
  .attr({
    fill: "#2ecc71",
    opacity: 0.7
  })
  .scale(-1, 1, 390, 160);  // Flip horizontally around new center

// Add text to show reflection
group.text(390, 165, "ABC")
  .attr({
    fontSize: 24,
    textAnchor: "middle",
    fill: "#fff",
    fontFamily: "Arial, sans-serif"
  })
  .scale(-1, 1, 390, 165);

// Rectangle 4: Reflect vertically
group
  .rect(150, 180, 80, 60)
  .attr({
    fill: "#f39c12",
    opacity: 0.7
  })
  .scale(1, -1, 190, 210);  // Flip vertically around center
Enter fullscreen mode Exit fullscreen mode

Output: Four rectangles showing uniform scaling, horizontal scaling, horizontal reflection, and vertical reflection.


6. Combining Transformations: Order Matters

Transformations are applied in sequence, and order matters. translate().rotate() produces different results than rotate().translate().

Example: Transform Order

// transform-order.js
var s = Snap("#mySvg");
var group = s.g();

// Reference: Small circle at origin
group.circle(250, 150, 5).attr({ fill: "#c0392b" });

// Path 1: Translate THEN Rotate
// The rect moves right, then rotates around the origin
var rect1 = group
  .rect(-30, -20, 60, 40)
  .attr({
    fill: "#3498db",
    opacity: 0.7,
    stroke: "#2c3e50",
    strokeWidth: 2
  })
  .translate(250, 150)   // Move to (250, 150)
  .translate(80, 0)      // Move 80px right
  .rotate(45, 250, 150); // Rotate 45° around origin point

// Label
group.text(330, 115, "Translate → Rotate")
  .attr({ fontSize: 12, fill: "#2c3e50" });

// Path 2: Rotate THEN Translate
// The rect rotates first, then moves along the rotated axis
var rect2 = group
  .rect(-30, -20, 60, 40)
  .attr({
    fill: "#e74c3c",
    opacity: 0.7,
    stroke: "#c0392b",
    strokeWidth: 2
  })
  .translate(250, 150)   // Move to (250, 150)
  .rotate(45, 250, 150)  // Rotate 45° around origin point
  .translate(80, 0);     // Move 80px along rotated X axis

// Label (moved lower/right to avoid overlap)
group.text(305, 235, "Rotate → Translate")
  .attr({ fontSize: 12, fill: "#c0392b" });

// Arrow showing the difference
group.line(330, 150, 285, 195)
  .attr({
    stroke: "#7f8c8d",
    strokeWidth: 1,
    strokeDasharray: "3,3",
    markerEnd: "url(#arrowhead)"
  });

// Define arrowhead marker
var defs = s.select("defs") || s.el("defs");
defs.append(
  s.path("M 0 0 L 10 5 L 0 10 z")
    .attr({ id: "arrowhead", fill: "#7f8c8d" })
);
Enter fullscreen mode Exit fullscreen mode

Output: Two rectangles at different positions, demonstrating how transformation order affects the final result.

Key Principle: Each transform method applies relative to the coordinate system established by previous transforms.


7. Reading Current Transforms with getLocalMatrix()

The getLocalMatrix() method returns the element's current transformation matrix, allowing you to read and build upon existing transforms.

Example: Inspecting and Building Transforms

// get-local-matrix.js
var s = Snap("#mySvg");
var group = s.g();

// Create a transformed rectangle
var rect = group
  .rect(100, 100, 80, 60)
  .attr({
    fill: "#3498db",
    stroke: "#2c3e50",
    strokeWidth: 2
  })
  .translate(150, 50)
  .rotate(30, 190, 130);

// Get the current transformation matrix
var matrix = rect.getLocalMatrix();

// Display matrix values
var infoText = group.text(20, 18, 
  "Current Matrix: " + matrix.toTransformString())
  .attr({
    fontSize: 14,
    fontFamily: "monospace",
    fill: "#2c3e50"
  });

// Create another rectangle using the same matrix
var rect2 = group
  .rect(100, 100, 80, 60)
  .attr({
    fill: "#e74c3c",
    opacity: 0.5,
    stroke: "#c0392b",
    strokeWidth: 2
  })
  .transform(matrix);  // Apply the exact same transform

// Label
group.text(190, 220, "Same transform applied")
  .attr({
    fontSize: 12,
    textAnchor: "middle",
    fill: "#7f8c8d"
  });

// Button to add more rotation
var button = group
  .rect(350, 20, 120, 40)
  .attr({
    fill: "#2ecc71",
    rx: 5,
    ry: 5,
    cursor: "pointer"
  });

group.text(410, 45, "Rotate More")
  .attr({
    textAnchor: "middle",
    fill: "#fff",
    fontSize: 14,
    pointerEvents: "none"
  });

button.click(function() {
  // Get current matrix and add 15° rotation
  var currentMatrix = rect.getLocalMatrix();
  rect.rotate(15, 190, 130);

  // Update info text
  var newMatrix = rect.getLocalMatrix();
  infoText.attr({
    text: "Current Matrix: " + newMatrix.toTransformString()
  });
});
Enter fullscreen mode Exit fullscreen mode

Output: Two overlapping rectangles with identical transforms, plus a button to add rotation and see the matrix update.

Key Methods:

  • getLocalMatrix(): Returns the element's current transformation matrix
  • matrix.toTransformString(): Converts matrix to readable string
  • transform(matrix): Applies a matrix directly


8. Building Matrices with addTransform()

The addTransform(matrix) method lets you add a transformation matrix to an element's existing transform, giving you full control over matrix composition.

Example: Composing Matrices Programmatically

// add-transform.js
var s = Snap("#mySvg");
var group = s.g();

// Create a shape with initial transform
var triangle = group
  .polygon([0, -30, 25, 15, -25, 15])
  .attr({
    fill: "#3498db",
    stroke: "#2c3e50",
    strokeWidth: 2
  })
  .translate(250, 150);

// Create transformation matrices
var scaleMatrix = Snap.matrix().scale(1.5, 1.5, 250, 150);
var rotateMatrix = Snap.matrix().rotate(45, 250, 150);
var translateMatrix = Snap.matrix().translate(50, 0);

// UI buttons to add transforms
var btnY = 20;
var createButton = function(label, x, callback) {
  var btn = group
    .rect(x, btnY, 100, 35)
    .attr({
      fill: "#2ecc71",
      rx: 5,
      cursor: "pointer"
    });

  group.text(x + 50, btnY + 22, label)
    .attr({
      textAnchor: "middle",
      fill: "#fff",
      fontSize: 12,
      pointerEvents: "none"
    });

  btn.click(callback);
};

createButton("+ Scale", 20, function() {
  triangle.addTransform(scaleMatrix);
});

createButton("+ Rotate", 130, function() {
  triangle.addTransform(rotateMatrix);
});

createButton("+ Translate", 240, function() {
  triangle.addTransform(translateMatrix);
});

createButton("Reset", 350, function() {
  triangle.transform(Snap.matrix().translate(250, 150));
});

// Instructions
group.text(250, 280, "Click buttons to compose transforms")
  .attr({
    textAnchor: "middle",
    fontSize: 14,
    fill: "#7f8c8d"
  });
Enter fullscreen mode Exit fullscreen mode

Output: An interactive demo where you can build complex transforms by adding matrices incrementally.

Key Insight: addTransform() is the programmatic way to build transforms. It's especially useful when:

  • Calculating transforms based on data
  • Building physics simulations
  • Creating procedural animations


9. Animated Transforms

The animateTransform() and translateAnimate() methods enable smooth animated transitions between transformations.

Example: Spinning Windmill

// animated-windmill.js
var s = Snap("#mySvg");
var group = s.g();

console.log("Windmill")

// Windmill base (pole)
var pole = group
        .rect(245, 200, 10, 100)
        .attr({
          fill: "#7f8c8d",
          stroke: "#2c3e50",
          strokeWidth: 2
        });

// Windmill top (housing)
var housing = group
        .polygon([250, 190, 230, 210, 270, 210])
        .attr({
          fill: "#34495e",
          stroke: "#2c3e50",
          strokeWidth: 2
        });

// Center hub
var hub = group
        .circle(250, 150, 12)
        .attr({
          fill: "#c0392b",
          stroke: "#2c3e50",
          strokeWidth: 2
        });

// Blade function
var createBlade = function (angle) {
  let blade_group = group
          .g()
          .attr({id: "blade" + angle});
  blade_group
          .path("M 0,-10 L 5,-10 L 8,-60 L 4,-80 L -4,-80 L -8,-60 L -5,-10 Z")
          .attr({
            fill: "#ecf0f1",
            stroke: "#2c3e50",
            strokeWidth: 2
          })
          .translate(250, 150)
          .rotate(angle, 250, 150);
  return blade_group;
};

// Create 4 blades at 90° intervals
var blades = [
  createBlade(0),
  createBlade(90),
  createBlade(180),
  createBlade(270)
];

// Animate continuous rotation
var rotationSpeed = 3000; // 3 seconds per rotation

// Use animateTransform with a MATRIX-EASING generator.
// The easing function receives (startMatrix, endMatrix) and must
// return a function t -> matrix at time t in [0,1].
// We spin around the hub (250,150) by mapping t to a rotation matrix.
function spinEasingFactory(startMatrix /*, endMatrix */) {
  var cx = 250, cy = 150;
  var fullTurn = 360; // degrees per cycle
  return function (t) {
    // Return startMatrix rotated by fullTurn * t around (cx, cy)
    let rotate = startMatrix.clone().rotate(fullTurn * t, cx, cy);
    return rotate;

  };
}

var run = true;
var animateBlade = function (blade) {
  var start = blade.getLocalMatrix();
  // Target can be start (360° brings us back). We pass easing_direct_matrix=true
  // so our easing function drives the matrices each frame.
  blade.animateTransform(
          start,                // target matrix (same as start for seamless loop)
          rotationSpeed,
          spinEasingFactory,    // matrix-easing generator
          function () {         // on complete, immediately loop
            if (run) animateBlade(blade);
          },
          true                  // easing_direct_matrix: interpret easing as matrix generator
  );
};

// Start animation for all blades
blades.forEach(function (blade) {
  animateBlade(blade);
});

// Stop the animation at the end
hub.mouseenter(() => {
  run = false
})

// Restart Animation
hub.mouseleave(() => {
  if (!run) blades.forEach(function (blade) {
    run = true;
    animateBlade(blade);
  });
})

// Sun in background
group.circle(80, 80, 30)
        .attr({
          fill: "#f39c12",
          opacity: 0.8
        });

// Ground
group.rect(0, 280, 500, 40)
        .attr({
          fill: "#27ae60"
        });

Enter fullscreen mode Exit fullscreen mode

Output: An animated windmill with four rotating blades using animateTransform() for smooth continuous rotation.

Animation Methods:

  • animateTransform(matrix, duration, easing, callback): Animate to target matrix
  • translateAnimate(duration, x, y, prev_trans): Shortcut for animated translation
  • For rotations and complex motions, pass a matrix-easing generator as the easing argument and set the 5th parameter to true (easing_direct_matrix). The generator receives (startMatrix, endMatrix) and returns a function t => matrix.
    • Example: el.animateTransform(start, 3000, spinEasingFactory, onDone, true)
    • This avoids linear interpolation of matrix coefficients (which can shear) and handles 360° loops cleanly (since 360° = identity).


10. The reflect() Helper Method

The reflect() method provides a convenient way to mirror elements along an axis.

Example: Reflections and Symmetry

// reflect-demo.js
var s = Snap("#mySvg");
var group = s.g();

// Original shape (triangle pointing right)
var originalTriangle = group
  .polygon([0, 0, 40, 20, 0, 40])
  .attr({
    fill: "#3498db",
    stroke: "#2c3e50",
    strokeWidth: 2
  })
  .translate(100, 100);

// Label
group.text(120, 95, "Original")
  .attr({ fontSize: 12, fill: "#2c3e50", textAnchor: "middle" });

// Reflect horizontally
var reflectX = group
  .polygon([0, 0, 40, 20, 0, 40])
  .attr({
    fill: "#e74c3c",
    stroke: "#c0392b",
    strokeWidth: 2
  })
  .translate(250, 100)
  .reflect("x", 270, 120);  // Reflect around vertical axis at x=270

// Label
group.text(270, 95, "reflect('x')")
  .attr({ fontSize: 12, fill: "#c0392b", textAnchor: "middle" });

// Reflect vertically
var reflectY = group
  .polygon([0, 0, 40, 20, 0, 40])
  .attr({
    fill: "#2ecc71",
    stroke: "#27ae60",
    strokeWidth: 2
  })
  .translate(100, 200)
  .reflect("y", 120, 220);  // Reflect around horizontal axis at y=220

// Label
group.text(120, 195, "reflect('y')")
  .attr({ fontSize: 12, fill: "#27ae60", textAnchor: "middle" });

// Reflect at 45° angle
var reflect45 = group
  .polygon([0, 0, 40, 20, 0, 40])
  .attr({
    fill: "#f39c12",
    stroke: "#e67e22",
    strokeWidth: 2
  })
  .translate(370, 100)
  .reflect(45, 390, 120);  // Reflect around 45° axis

// Label
group.text(390, 95, "reflect(45°)")
  .attr({ fontSize: 12, fill: "#e67e22", textAnchor: "middle" });

// Draw reflection axes
group.line(270, 80, 270, 160)
  .attr({ stroke: "#95a5a6", strokeWidth: 1, strokeDasharray: "5,3" });

group.line(80, 220, 160, 220)
  .attr({ stroke: "#95a5a6", strokeWidth: 1, strokeDasharray: "5,3" });
Enter fullscreen mode Exit fullscreen mode

Output: Four triangles showing original and three reflection types.

reflect() Parameters:

  • reflect("x") or reflect("vertical"): Mirror across vertical axis
  • reflect("y") or reflect("horizontal"): Mirror across horizontal axis
  • reflect(angle): Mirror across axis at specified angle (degrees)
  • reflect(lineElement): Mirror across a line element


11. Best Practices Summary

When to use direct transform methods:

  • Default choice: Use translate(), rotate(), scale() for all positioning
  • Readable code: Method chaining is self-documenting
  • Easy modification: Change specific transforms without reparsing strings
  • Animation-ready: Works seamlessly with animateTransform()

When to use prev_trans parameter:

  • Relative transforms (default): Compose with existing transform
  • Absolute positioning: Use prev_trans = true to reset matrix
  • Custom bases: Use prev_trans = Snap.matrix() for specific starting point

When to use getLocalMatrix():

  • Read current state: Inspect existing transforms
  • Clone transforms: Apply same transform to multiple elements
  • Build upon transforms: Get matrix, modify it, apply with addTransform()

When to use addTransform():

  • Programmatic transforms: Calculate matrices from data
  • Physics engines: Apply forces as matrix operations
  • Complex compositions: Build transforms step-by-step

Key points:

  • Transform order matters: translate().rotate()rotate().translate()
  • Transforms compose: Multiple method calls add together
  • Center points: Specify (cx, cy) for rotation and scale centers
  • Reflection: Use reflect() for mirroring or negative scale values
  • Animation: Use animateTransform() for smooth transitions

12. What's Next

In this tutorial we learned:

  • Direct transform methods: translate(), rotate(), scale(), reflect()
  • Understanding prev_trans for relative vs absolute transforms
  • Reading transforms with getLocalMatrix()
  • Building transforms with addTransform()
  • Animating transforms with animateTransform() and translateAnimate()
  • Transform composition and ordering

In the next tutorial (Loading and Managing External SVG) we'll learn how to:

  • Load external SVG files with Snap.load()
  • Query elements with select() and selectAll()
  • Manage IDs and references with getId() and setIdFollowRefs()
  • Navigate hierarchy with closest() and selectParent()
  • Track references with getReferringToMe()

The foundation of structure (Tutorial 1), styling (Tutorial 2), and transforms (Tutorial 3) gives us complete control over SVG creation. Next we'll learn to load and reuse existing SVG assets.

Top comments (0)