w3c / svgwg

SVG Working Group specifications
Other
705 stars 132 forks source link

Add getScreenBBox() method to SVGGraphicsElement interface #76

Open jarek-foksa opened 8 years ago

jarek-foksa commented 8 years ago

There is no way to determine the bounding box of an SVG element in the client space.

The generic element.getBoundingClientRect() method, unlike element.getBBox(), does not allow you to specify whether to include stroke and markers in the result. It also does not return a geometric bbox, but rather a bbox of a user bbox that has been transformed to the client space, which is not what you would expect or want in most scenarios.

I would propose to add getScreenBBox() method to the SVGGraphicsElement interface. The method would take the same arguments as the existing getBBox() method. Note that "screen" in the method name would actually mean "client", for the sake of consistency with getCTM()/getScreenCTM().

BigBadaboom commented 7 years ago

Or alternatively, perhaps, add a new property to SVGBoundingBoxOptions?

dictionary SVGBoundingBoxOptions {
  boolean fill = true;
  boolean stroke = false;
  boolean markers = false;
  boolean clipped = false;
  boolean applyTransforms = false;
};

If applyTransforms is true, then the bounding box that is calculated takes into account all transforms on the current and ancestor elements.

fsoder commented 7 years ago

This would be something like the below (modulo terrible indentation) using currently speced primitives?

function getScreenBBox(element) { let ctm = element.getScreenCTM(); let localBBox = DOMQuad.fromRect(element.getBBox({fill: true, stroke: true, markers: true})); let clientBBox = new DOMQuad(localBBox.p1.matrixTransform(ctm), localBBox.p2.matrixTransform(ctm), localBBox.p3.matrixTransform(ctm), localBBox.p4.matrixTransform(ctm)); return clientBBox.getBounds(); }

jarek-foksa commented 7 years ago

@foolip: In order to determine the geometric bbox of a path in the client space you can't just transform paths' user (local) bbox to the client space, this might produce wrong results when the path or its ancestors are transformed.

Instead, you have to determine the coordinates of each path node point, then transform each point to the client space and finally use that to determine the bbox.

Doing this in JS is very slow, I suspect browsers could make it much faster by querying the render tree.

fsoder commented 7 years ago

Ah, yes, I missed that part of the issue. Computing the "precise" bounding box in "screen-space" will be costly even if "querying the render tree" though - unless this happen to be stored already (it isn't in Blink for example. There's a cost associated with maintaining this kind of "absolute" state - if there weren't, gBCR et al would likely already return these tight fitting bboxes.)

AmeliaBR commented 7 years ago

Yes, we already use the simplified approach in other cases when combining transforms and BBox calculations, precisely for the issue of computational complexity.

Currently, if you compute the BBox of a rotated path, or of a <g> that contains a rotated path, you get the bounds in the parent's coordinate space of the path's rotated BBox, not tight bounds for the rotated path.

This CodePen by Sarah Drasner demonstrates the effect clearly.

Therefore, for compatibility, if a getScreenBBox() method was introduced, it would probably follow the steps in @fsoder's comment, although I'd recommend letting the method take the options object as a parameter, rather than assuming certain values.

jarek-foksa commented 7 years ago

@AmeliaBR

From my testing pathElement.getBBox() returns a proper geometric minimum bounding box in the user space. I.e. the smallest possible rectangle inside which the path fits in, computed using raw path data (without applying any transforms).

That gist is demonstrating the problem I was talking about earlier - you can't just take a bounding box in the user space and transform its coordinates to some other space - this will produce a bounding box of another bounding box which is not useful and wrong if you stick to the mathematical definition.

Making getScreenBBox() behave this way would in my opinion be inconsistent with how getBBox() works and somehow redundant because this is what element.getBoundingClientRect() already does (without the advanced options though).

BTW, In Blink (and probably WebKit) pathElement.getBBox() seems to be taking control points of bezier segments into account when computing the bbox. Hopefully this is a bug rather than a standard behavior.

BigBadaboom commented 6 years ago

There is a very common request on Stack Overflow to get the bounds of an element including its own transform. IOW returning {x:50, y:0, width:100, height:100} for

<rect width="100" height="100" transform="translate(50,0)"/>

So it would be good to consider this use case in any solution.

So, using my previous suggestion, for example:

dictionary SVGBoundingBoxOptions {
  boolean fill = true;
  boolean stroke = false;
  boolean markers = false;
  boolean clipped = false;
  boolean selfTransform = false;
  boolean ancestorTransforms = false;
};
fsoder commented 6 years ago

Having selfTransform and ancestorTransforms means you have the possibility of { selfTransform: false, ancestorTransforms: true } which is a bit of an oddity. So an enumeration (enum ApplyTransforms { "none", "self-only", "self-and-ancestors" }; ... ApplyTransforms applyTransforms = "none" ...) or even a (nullable) Element might be a better choice.

paradisaeidae commented 6 years ago

Blake/ OSUblake has a great writeup of this issue here: https://greensock.com/forums/topic/13681-svg-gotchas/?page=2&tab=comments#comment-72060

His codepen of a workaround is excellent!