Carl

SVG Gotchas!

Recommended Posts

Masking vs Clipping

 

I'm not sure this is a gotcha, but it's definitely a confusing matter that has been very time-consuming for me. Dropping it here to maybe save someone else.

 

Here's a pen using the same path for both masking and clipping to compare the two. I expected the exact same result, but the mask clearly includes all the attributes and the clipPath is ignoring the stroke attributes and just renders the inside of the path (like a fill would).

 

"For the clipping, every path inside the clipPath is inspected and evaluated together with its stroke properties and transformation."

Mozilla Docs: Clipping and Masking

 

I don't think the doc is wrong, but it's definitely confusing. I understand this as the stroke properties will be included.

 

"A clipping path is conceptually equivalent to a custom viewport for the referencing element."

Mozilla Docs <clipPath>

 

This, to me, means never do anything fancy with clipPath. Just use it to define a "viewport".

  • Like 3

Share this post


Link to post
Share on other sites

On my recent handwriting demo (http://codepen.io/PointC/pen/MpLVvO), I hit a little oddity in Chrome involving the bounding box of a SVG group and incorrect data.

 

To demonstrate this issue, I’ve created a 100x100 SVG and added a 100x100 rectangle for reference as a background. I then dropped in  a circle and added 2 points to make it into a blob. These two elements are grouped. Both elements are within the 100x100 size of the viewBox and the group. You would expect a group size of 100x100 and x/y positions of 0, but Chrome reports back odd size and position data. Firefox, IE and Edge all calculate everything as you’d expect.

 

u46pxTD.png

 

Chrome seems to be calculating the group BBox based on the child elements plus the Bezier handle positions of the path(s).

 

wpaXl9e.png

 

You can check it for yourself:

 

http://codepen.io/PointC/pen/vxqgrr

 

This can cause problems when trying to align the group to a motion path or animate it with precision to a new x/y position. Just something to watch for when creating your vector artwork.

 

Happy tweening

  • Like 7

Share this post


Link to post
Share on other sites

Wow, great find Craig. Thanks for sharing the details. 

  • Like 2

Share this post


Link to post
Share on other sites

Hello Craig (PointC),

 

I believe this to be the webkit getBBox() svg bug report for what you are describing:

It looks like it affects SVG <path> elements with cubic bezier curves that calculate more than what the actual getBBox() is.

 

It shows as RESOLVED FIXED in the above bug report, but with webkit that doesn't mean anything since webkit is a groundhogs day of fun feelings!

 

UPDATE:

 

Here is a new Chrome webkit bug report ticket regarding this webkit bug, - ticket status is STARTED

:)

  • Like 3

Share this post


Link to post
Share on other sites

Thanks Jonathan. :)

 

If they ever did have that fixed, they must have brought it back for Throwback Thursday.

 

Strangely, the BBox of the path itself is accurate. It's only when it's calculated in a group or as part of the entire SVG that it's wrong.  :blink:

  • Like 1

Share this post


Link to post
Share on other sites

Everything about getBBox is a gotcha. For those that don't know what it is, it's a method on SVG elements that will return its position and size...

 

var bounds = someElement.getBBox(); // => { x: 0, y: 0, width: 100, height: 100 }

 

Sounds pretty useful as positioning is an important part of animation. The problem is that it reports its bounding box without any transforms applied. What does that mean? If you have an element at 0,0 and tween it to 100,100 using x and y, and then get it's bounding box, it will say it's still at 0,0. This goes for all transforms, x, y, scale, rotation, etc. Not very useful considering most animations use transforms.

 

Things get even more confusing with group <g> elements, which don't have a fixed bounds like other elements. They are calculated based on the size and position of their children. If you call getBBox on a group, and it has transformed children inside of it, it will take into account those transforms. 

 

Look at this example. The green box is the bounds of the group. The red box is the bounds of the blue square. As you can see, the blue square is nowhere near the red box. It uses transforms to calculate some things, but not others, which can be really confusing. 

 

VAyQF7s.png

 

Check it out for yourself....

 

 

 

 

 

Another confusing thing about bounds is that they are axis-aligned (AABB), meaning they are parallel to the x and y axis. Take a look at what happens when you rotate an element. The blue boxes are the original bounds of the element, but they are no longer aligned, so a new bounds is created around them to make them aligned. This causes the bounds to separate and move away from the element. Look at how the green boxes don't even touch the elements on the left and right. 

 

 

6DmO3hL.png

 

 

You really need to see it in action to fully understand how it works. The red boxes are the bounds getBBox reports. The blue box is the transform being applied to the red box. And the green box is the transformed bounds, which is calculated by drawing an axis aligned box around the blue box. Notice how large the transformed bounds become at the end. That can cause stuff like hit testing with Draggable to fail.

 

 

 

So there's a little info about bounds and why getBBox isn't that useful. Well, unless you want to find the original position and size of your element. But it's not a total loss. It just needs to be improved, so I made a little getBBox helper function that will calculate transformed bounds, which is what's needed to get correct positioning.  

 

 

/**
 * @param {SVGElement} element - Element to get the bounding box for
 * @param {boolean} [withoutTransforms=false] - If true, transforms will not be calculated
 * @param {SVGElement} [toElement] - Element to calculate bounding box relative to
 * @returns {SVGRect} Coordinates and dimensions of the real bounding box
 */
function getBBox(element, withoutTransforms, toElement) {
  
  var svg = element.ownerSVGElement;
  
  if (!svg) {
    return { x: 0, y: 0, cx: 0, cy: 0, width: 0, height: 0 };
  }
  
  var r = element.getBBox(); 
  
  if (withoutTransforms) {
    return {
      x: r.x,
      y: r.y,
      width: r.width,
      height: r.height,        
      cx: r.x + r.width / 2,
      cy: r.y + r.height / 2
    };
  }
  
  var p = svg.createSVGPoint(); 
      
  var matrix = (toElement || svg).getScreenCTM().inverse().multiply(element.getScreenCTM()); 

  p.x = r.x;
  p.y = r.y;
  var a = p.matrixTransform(matrix);

  p.x = r.x + r.width;
  p.y = r.y;
  var b = p.matrixTransform(matrix);

  p.x = r.x + r.width;
  p.y = r.y + r.height;
  var c = p.matrixTransform(matrix);

  p.x = r.x;
  p.y = r.y + r.height;
  var d = p.matrixTransform(matrix);

  var minX = Math.min(a.x, b.x, c.x, d.x);
  var maxX = Math.max(a.x, b.x, c.x, d.x);
  var minY = Math.min(a.y, b.y, c.y, d.y);
  var maxY = Math.max(a.y, b.y, c.y, d.y);
    
  var width = maxX - minX;
  var height = maxY - minY;
  
  return {
    x: minX,
    y: minY,
    width: width,
    height: height,        
    cx: minX + width / 2,
    cy: minY + height / 2
  };
}

 

The function takes 3 parameters. The first one is required, which is the element you want to get the bounds for. The second one, calculateWithoutTransforms, will calculate the bounds without any transforms if set to true. And the third parameter is an element to calculate the bounds relative to. By default it uses the SVG document the element is in, which is usually what you want, as it's kind of like the global space.

 

I also have another version that will work around the Chrome bug that PointC came across. I was able to get around the bug by manually calculating the bounds of all the children in a group element instead of calling getBBox.

 

So here's a pen of the original version...

 

 

 

And the one with the Chrome workaround...

(EDIT - looks like Chrome fixed the bug so this isn't necessary anymore)

 

 

 

And one last tip. You can set outlines on SVG elements in your CSS in Chrome. This is really helpful as it will let you see where stuff is, like a group element, which is usually invisible. That makes positioning so much easier. You can even see it incorrectly apply an outline on paths inside a group, just like with PointC issue.

 

g {
  outline: 1px dashed red;
}

path {
  outline: 1px solid blue;
}

 

 

  • Like 7

Share this post


Link to post
Share on other sites

Amazing stuff, Blake. Love the demos and clear explanation. My vote is that you create a CodePen Blog Post (you can easily embed the demos) with this info. More people need to see this stuff. 

  • Like 3

Share this post


Link to post
Share on other sites

Excellent info Blake. :)

 

I'm with Carl - please write that up for a CodePen blog post. The more people over there that realize how smart you are and that you hang out here in GreenSock Land, the better it could be for the GS community.

 

Bring on the new Club Members. Just think how full your $1/answer tip jar could be with a bunch of new GSAP users.  ;)

  • Like 2

Share this post


Link to post
Share on other sites

Carl's video tutorial on the first page of this thread is brilliant. I found the copy and paste from Illustrator artwork to SVG code works in Adobe Brackets as well as Sublime text.

  • Like 1

Share this post


Link to post
Share on other sites

Yay! I get to contribute here :D

 

But not without a hat tip to @OSUblake

 

You can nest an SVG into another SVG - That's quite handy for a number of reasons,  one quick example would be: You have a super complex drawing with some text inside it. You can have an inline SVG that holds the text and then, nest the other SVG in with an <image>. That'll keep the text reachable with CSS and JS without having a wall of code for the rest of the drawing.

 

However - If you end up nesting an SVG inside another one you will not be able to transform the nested root SVG element (for now).

 

https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform#Elements

 

 

  • Like 3

Share this post


Link to post
Share on other sites

While we wait for SVG 2 and the ability to transform nested SVGs, you can currently fake a scale() for a nested SVG. By animating the width/height/x/y attributes we can create the illusion of animating scale(). If you don't have those attributes on the nested SVG, you can add them via a TweenMax.set(). You can then gain some control and fake the scale along with changing the point of origin.

 

Not a perfect solution, but if you only want to scale a nested SVG from a specific point, it works.

:)

 

  • Like 2

Share this post


Link to post
Share on other sites

The last example in @Dipscom's demo is a good work around. Wrap the nested svg in a <g> and scale that.

 

<svg viewBox="0 0 500 500">
  <g class="circle">
    <svg viewBox="0 0 500 500">
      <circle cx="50%" cy="50%" r="50%" />
    </svg>
  </g>
</svg>

 

 Works...

TweenMax.to('.circle', 1, { scale: 0 });

 

  • Like 3

Share this post


Link to post
Share on other sites

Oh, I wasn't questioning the solutions.  ;)

 

Grouping is probably the best/easiest way to go in most cases. I was just curious if I could push some attributes on it and get any sort of control. 

  • Like 2

Share this post


Link to post
Share on other sites
26 minutes ago, PointC said:

Oh, I wasn't questioning the solutions.  ;)

 

Hehe. I was thinking of the same work around as you. I didn't even think of doing what  @Dipscom did with the group

  • Like 1

Share this post


Link to post
Share on other sites

Hi,

 

Already a Greek mathematician said: Noli turbare circulos meos!

 

Chapeau! 

Share this post


Link to post
Share on other sites

If one is not as smart as @PointC or @OSUblake to come up with nice calculations and/or is lazy enough, one just cheats with <g>

's...

  • Like 3

Share this post


Link to post
Share on other sites

Having watched Chris Gannon's video I'd like to suggest a slightly different workflow.

After some trial and error I find the easiest is to keep all the graphic editing in Illustrator, do export as an SVG file, that leave it unchanged. Then in the html page I load the svg file using ajax, with a recipe found here: http://plnkr.co/edit/LneUEK?p=preview .

I did a CodePen to illustrate the method, with a "this works / this does not work" example, to show the utility of those lines of code:

 

I've got rid of "_1" ids by attributing names to the layers in the Illustrator's layer panel. Or at least, to any object I will need to animate or manipulate later.

 

The advantage is that you don't have to do any of the copy-pasting described by Chris. You can keep the exported SVG file in the text editor for reference. Each time you do another export the text editor asks you whether you want to load the new version, and all you have to do is click OK.

 

The only drawback I see is that it doesn't work in CodePen. If you want to share your code, you have to do the copy-paste the SVG into the html section of Codepen. But you would typically do that once or twice, not all the time as you would when you are trying to get things to work.

  • Like 1

Share this post


Link to post
Share on other sites

SVG Mask Animations and a slight rotation

 

This has popped up several times in the forum so I thought I'd drop it into this thread. Some of the browsers are getting fussy with SVG mask animations. This is especially true of DrawSVG stroke masks. You should add a slight rotation to the mask element as it animates to force the browser to redraw the mask. Right now Edge is the biggest offender, but the way Chrome has been adding bugs with each release, it may not be long before they call it a feature.

 

If you look at this pen in all the browsers, you'll see that most of them handle both mask animations just fine, but in Edge the right one will not work correctly. Try resizing the window in Edge and then using  DevTools to scrub the timeline. You'll see that right mask get rather funky.

 

 

I've also seen this in Edge with a rectangular gradient mask so my recommendation is to add that slight rotation to any and all mask animations just to be safe. Happy tweening everyone.

 

 

 

  • Like 2

Share this post


Link to post
Share on other sites

Nice thread! I'm gonna read this one on my phone after work, lying in the park :-D

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.