27

What I'm trying to do is simple: scale some SVG dots from scale(0) to scale(1) when a sibling element is hovered using vanilla js. They are the red ones in the demo

Here's the basic SVG setup

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
      x="0px" y="0px" viewBox="0 0 720 576" style="enable-background:new 0 0 720 576;" xml:space="preserve">
    <style type="text/css">
        .st3 {
            fill:red;
        }
        * {
            -webkit-transition:.3s;
            transition:.3s;
        }
    </style>
    <g id="Layer_4">
        <!-- Shield -->
        <path class="st8" d="M601,304.7c-32.4-15.4-68.6-24-106.8-24c-40.4,0-78.5,9.6-112.3,26.6c4.9,79.7,41.9,146.7,109.5,187.6
            C559.8,454.1,597,385.6,601,304.7z" />
        <path class="st9" d="M420.1,328.7c2.1-4.7,32.5-23.9,72.5-23.9c39.9,0,73.1,20,75.5,24.3c2.4,4.3,5.7,40-12.7,74.6
                c-19.7,36.9-53.5,50.1-61.8,50.4c-6.4,0.2-41.8-14.3-62.5-51.6C411.5,367.4,418,333.4,420.1,328.7z" />
        <circle class="st10" cx="494.9" cy="373.3" r="35.5" />
    </g>
    <g id="Layer_8">
        <!-- Dots on shield -->
        <circle class="st3" cx="578.8" cy="316.2" r="4.6" />
        <circle class="st3" cx="543.4" cy="346.2" r="4.6" />
        <circle class="st3" cx="505" cy="375.5" r="4.6" />
    </g>
</svg>

The issue is that SVG scales based on the origin location, not the current location, thus when a transform is applied it moves the element in addition to scaling it. I am attempting to fix this situation by translating by the BBox() offset, scaling, then translating back but that only seemed to help and not entirely fix the issue.

var shield = document.getElementById("Layer_4"),
    dots = document.querySelectorAll("#Layer_8 .st3");

toggleTransform(false);

shield.onmouseover = function () { toggleTransform(true); }
shield.onmouseout = function () { toggleTransform(false); }

function toggleTransform(bool) {
    if (!bool) {
        for (var i = 0; i < dots.length; i++) {
            var box = dots[i].getBBox(),
                cx = box.x + box.width / 10,
                cy = box.y + box.height / 10;
            //dots[i].setAttribute("transform", "translate(" + cx + " " + cy + ") scale(0) translate(" + cx + " " + cy + ")");
            dots[i].style.WebkitTransform = "translate(" + cx + "px, " + cy + "px) scale(0) translate(" + -cx + "px, " + -cy + "px)";
        }
    } else {
        for (var i = 0; i < dots.length; i++) {
            var box = dots[i].getBBox(),
                cx = box.x + box.width / 2,
                cy = box.y + box.height / 2;
            //dots[i].setAttribute("transform", "translate(0 0) scale(1) translate(0 0)");
            dots[i].style.WebkitTransform = "translate(0, 0) scale(1) translate(0, 0)";
        }
    }
}

I tried using both setAttribute and CSS's transform (I couldn't get setAttribute to transition, presumably because it's not animatable by CSS) but couldn't get it with either. I've only been testing in Chrome

Anyone have an idea how I can scale, while not moving, red dots?

Here's the demo again if you missed it

Edit

I made a function based on RashFlash's answer to make it quite simple to use and also takes into account offsets and different transform origins

function scaleMe(elem, scaleX, scaleY, newOffsetX, newOffsetY, originX, originY) {
    newOffsetX = null ? 0 : newOffsetX;
    newOffsetY = null ? 0 : newOffsetY;
    originX = null ? "center" : originX;
    originY = null ? "center" : originY;

    var bbox = elem.getBBox(),
        cx = bbox.x + (bbox.width / 2),
        cy = bbox.y + (bbox.height / 2),        
        tx = -cx * (scaleX - 1) + newOffsetX,
        ty = -cy * (scaleY - 1) + newOffsetY;        

    if(originX === "left" || originX === "right") {
        tx = newOffsetX;
    }
    if(originY === "top" || originY === "bottom") {
        ty = newOffsetY;
    }

    var scalestr = scaleX + ',' + scaleY,
        translatestr = tx + 'px,' + ty + 'px';

    elem.style.WebkitTransformOrigin = originX + " " + originY;
    elem.style.MozTransformOrigin = originX + " " + originY;
    elem.style.msTransformOrigin = originX + " " + originY;
    elem.style.transformOrigin = originX + " " + originY;

    elem.style.WebkitTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
    elem.style.MozTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
    elem.style.msTransform = "translate(" + translatestr + ") scale(" + scalestr + ")";
    elem.style.transform = "translate(" + translatestr + ") scale(" + scalestr + ")";
}
Zach Saucier
  • 24,871
  • 12
  • 85
  • 147
  • It's not really clear what you want to happen. Do you want each of the red dots to get bigger on mouseover? – Paul LeBeau Jun 12 '14 at 00:38
  • @BigBadaboom Yes, I want to scale the red dots on mouse over without having them move in from anywhere - just get bigger – Zach Saucier Jun 12 '14 at 01:41

4 Answers4

36

Updated to work with modern browsers that support transform-box Previously, this approach worked only in Chrome. But spec changes to how transform-origin works, and the addition of transform-box now means that this works in more browsers (currently Chrome, FF, and Opera).

You can actually achieve this effect without JS.

.st3 {
    fill: red;
    -webkit-transform: scale(1);
    -webkit-transform-origin: 50% 50%;
    -webkit-transition:.3s;
    transform: scale(1);
    transform-origin: 50% 50%;
    transition:.3s;
    transform-box: fill-box;
}

#Layer_4:hover + g .st3 {
    -webkit-transform: scale(2);
    -webkit-transform-origin: 50% 50%;
    -webkit-transition:.3s;
    transform: scale(2);
    transform-origin: 50% 50%;
    transition:.3s;
}

Demo here

Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • Note that transform-origin when used in SVG (as opposed to CSS) doesn't seem to play nice with certain browsers, especially mobile. – V. Rubinetti Sep 03 '18 at 01:36
  • Chrome fixed a "bug" which meant this didn't work any more. I've now updated the example so that it now works again, and also works in other browsers. – Paul LeBeau Sep 03 '18 at 05:38
  • I had a similar issue for a project I'm now making - the use of `transform-box: fill-box` made what I was doing so much easier! – Zach Saucier Jan 02 '19 at 23:35
  • Awesome thing, thank you! Unfortunately, it is not fully supported yet: https://www.w3cschool.cn/doc_css/css-transform-box.html – nad_rom Feb 17 '19 at 20:40
  • The info on that page is out of date. I would suggest you use MDN as a more reliable resource: https://developer.mozilla.org/en-US/docs/Web/CSS/transform-box – Paul LeBeau Feb 18 '19 at 12:19
11

if i am not wrong, you want to scale the dots along their center, dots remain their current position and just gets bigger.

if this you want, then following code will help you

var bbox=elementNode.getBBox();
var cx=bbox.x+(bbox.width/2),
    cy=bbox.y+(bbox.height/2);   // finding center of element
var scalex=1.5, scaley=1.5;    // your desired scale
var saclestr=scalex+','+scaley;
var tx=-cx*(scalex-1);
var ty=-cy*(scaley-1);                        
var translatestr=tx+','+ty;

elementNode.setAttribute('transform','translate('+translatestr+') scale('+saclestr+')');

So what i did, i first translate the dot and than scale it. i use following formula as described in Transforming Coordinate system

translate(-centerX*(factor-1), -centerY*(factor-1))
scale(factor)
Zach Saucier
  • 24,871
  • 12
  • 85
  • 147
RashFlash
  • 992
  • 2
  • 20
  • 40
  • 1
    Thanks for your help! Using that math and using a CSS transform instead retained the transition. [**Working demo**](http://jsbin.com/vefahide/2/edit) (webkit only) – Zach Saucier Jun 12 '14 at 14:06
3

An easier way to do this that does not involve a bunch of geometry is to put the item to be scaled and translated into a parent group ('g').

Then, you apply the translation to the parent group and the scale to the element itself.

var trasnstr = x + ',' + y;
var scalestr = scaleX + ',' + scaleY;

parentElement.setAttribute('transform', 'translate(' + trasnstr + ')');
element.setAttribute('transform', 'scale(' + scalestr + ')');
  • No idea why you got the down vote, as this approach worked for me. I am however working with the xml code and wrapped my path into a ... instead of inserting the transform command into the path itself. Moving gone. Scaling as intended. Thanks ! – 1813222 Jan 31 '21 at 19:48
0

This will automatically calculate and set transform-origin for any SVG element.

// mainSvgElement is SVG element itself
// svgChildElement is any path, rect, circle etc. inside SVG element
var setTransformOrigin = function(mainSvgElement, svgChildElement) {
  var mainRect = mainSvgElement.getBoundingClientRect();
  var childRect = svgChildElement.getBoundingClientRect();
  var originX = (((childRect.left - mainRect.left) + (childRect.width * 0.5)) / mainRect.width) * 100;
  var originY = (((childRect.top - mainRect.top) + (childRect.height * 0.5)) / mainRect.height) * 100;
  svgChildElement.style.transformOrigin = originX + "% " + originY + "%";
};

setTransformOrigin(mainSvgElement, svgChildElement);
// set scale now / or you can set in css too
svgChildElement.style.transform = "scale(1.5)";

(function() {
  var mainSvgElement = document.querySelector("svg");
  var svgChildElement = mainSvgElement.querySelector("path");
  // mainSvgElement is SVG element itself
  // svgChildElement is any path, rect, circle etc. inside SVG element
  var setTransformOrigin = function(mainSvgElement, svgChildElement) {
    var mainRect = mainSvgElement.getBoundingClientRect();
    var childRect = svgChildElement.getBoundingClientRect();
    var originX = (((childRect.left - mainRect.left) + (childRect.width * 0.5)) / mainRect.width) * 100;
    var originY = (((childRect.top - mainRect.top) + (childRect.height * 0.5)) / mainRect.height) * 100;
    svgChildElement.style.transformOrigin = originX + "% " + originY + "%";
  };
  
  setTransformOrigin(mainSvgElement, svgChildElement);
  // set scale now / or you can set in css too
  svgChildElement.addEventListener("mouseenter", function() {
    svgChildElement.style.transform = "scale(1.5)";
  });
  svgChildElement.addEventListener("mouseleave", function() {
    svgChildElement.style.transform = "scale(1)";
  });
})();
svg {
  width: 100%;
  border: 2px solid red;
}
path {
  cursor: pointer;
  transition: transform 1s;
}
Bring your mouse over on the shape:
<svg class="tb-graph" viewBox="0 0 1006 684" fill="none" xmlns="http://www.w3.org/2000/svg">
  <path d="M574.618 66.3322C575.377 63.1066 573.378 59.87 570.137 59.1784C512.776 46.9376 452.991 52.4821 398.793 75.1494C344.594 97.8167 298.663 136.486 267.096 185.919C265.312 188.711 266.213 192.407 269.042 194.132L357.225 247.895C360.055 249.62 363.738 248.718 365.56 245.95C384.451 217.254 411.493 194.793 443.273 181.502C475.052 168.211 510.032 164.732 543.728 171.435C546.978 172.082 550.207 170.093 550.966 166.867L574.618 66.3322Z" fill="#005453" stroke="#012020" stroke-width="2" />

https://codepen.io/animatedcreativity/pen/qBKXQKZ

Rehmat
  • 2,121
  • 2
  • 24
  • 28