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(), androtate()instead of string syntax, - understand relative vs absolute transformations with the
prev_transparameter, - access the current transformation state with
getLocalMatrix(), - build and compose complex transformations step-by-step with
addTransform(), - animate transformations smoothly with
animateTransform()andtranslateAnimate(),
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:
- Some Cool Demos: Some fun 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)" });
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);
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>
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)
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)
-
trueor"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" });
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" });
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
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" })
);
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()
});
});
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"
});
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"
});
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
easingargument and set the 5th parameter totrue(easing_direct_matrix). The generator receives(startMatrix, endMatrix)and returns a functiont => 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).
- Example:
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" });
Output: Four triangles showing original and three reflection types.
reflect() Parameters:
-
reflect("x")orreflect("vertical"): Mirror across vertical axis -
reflect("y")orreflect("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 = trueto 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_transfor relative vs absolute transforms - Reading transforms with
getLocalMatrix() - Building transforms with
addTransform() - Animating transforms with
animateTransform()andtranslateAnimate() - 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()andselectAll() - Manage IDs and references with
getId()andsetIdFollowRefs() - Navigate hierarchy with
closest()andselectParent() - 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)