Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to escape an SVG viewbox? (or how to position HTML DIV over scaled/transformed SVG?)

Tags:

html

svg

viewbox

In this document:

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden
}
#content {
  width: 100%;
  height: 100%
}
<div id="content">
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="90%" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid meet">
    <rect style="fill:none;stroke:black" width="540" height="157" x="690" y="26"></rect>
    <text style="font-size:148px;text-align:center;text-anchor:middle;fill:black;stroke:none" x="960" y="145">Test</text>
    <text style="font-size:112px;text-align:center;text-anchor:middle;fill:black;stroke:none" x="960" y="340">Lorem ipsum etc etc</text>
    <foreignObject x="10" y="726" width="1901" height="347">
      <div xmlns="http://www.w3.org/1999/xhtml" style="width:99.6%;height:97.7%;border:4px solid blue">
        <p style="text-align:center">Hello World, from HTML inside SVG.</p>
      </div>
    </foreignObject>
  </svg>
  <div style="width:99%;border:4px solid blue">
    <p style="text-align:center">Hello World, from HTML outside SVG.</p>
  </div>
</div>

I'm using a viewBox to rescale an arbitrary-size SVG so that it fills the browser window (normally the 90% height on the svg is 100%; I've reduced it here for illustrative purposes). Within that SVG is a foreignObject containing some HTML to be rendered.

As it stands, the rendered HTML output is being rescaled to fit the viewBox coordinates as well; notice how the border width and text size for the inside-svg content is different from that outside, and that it changes when the window is resized.

I want the location and size of the foreignObject to define the bounding box of the internal div, but I don't want it to actually rescale the contents, just to reflow them. Is there a way to do this?

(The text at the top also rescales with the window, but this is desired in that case.)

Note that I can't move or remove the viewBox. I can put the internal div outside of the svg and use JavaScript to size it, but I don't know how to set its bounding box (but not scale) to where it would be if it were inside.

(An unrelated thing that I don't understand is that I have to specify the width/height of the divs as less than 100% or it crops the border, both inside and outside. This might just be a Chrome thing though and isn't really important; I'm just curious.)


Edit After AmeliaBR's answer, this is the code I've come up with:

function svgTransform(x, y, matrix, svg) {
  var p = svg.createSVGPoint();
  p.x = x;
  p.y = y;
  return p.matrixTransform(matrix);
}

function svgScreenBounds(svgElement) {
  var matrix = svgElement.getScreenCTM();
  var r = svgElement.getBBox();
  var leftTop = svgTransform(r.x, r.y, matrix, svgElement.ownerSVGElement);
  var rightBottom = svgTransform(r.x + r.width, r.y + r.height, matrix, svgElement.ownerSVGElement);
  return {
    x: leftTop.x,
    y: leftTop.y,
    width: rightBottom.x - leftTop.x,
    height: rightBottom.y - leftTop.y
  };
}

function adjustOverlay() {
  var placeholder = document.getElementById('placeholder');
  var overlay = document.getElementById('overlay');
  var bounds = svgScreenBounds(placeholder);
  overlay.style.left = bounds.x + 'px';
  overlay.style.top = bounds.y + 'px';
  overlay.style.width = bounds.width + 'px';
  overlay.style.height = bounds.height + 'px';
  overlay.style.display = 'block';
}
html,body { height: 100%; margin: 0; padding: 0; overflow: hidden }
#content { width: 100%; height: 100% }
#overlay {
  display: none;
  position: absolute;
  border:4px solid blue;
  box-sizing: border-box;
}
<body onload="adjustOverlay()" onresize="adjustOverlay()">
  <div id="content">
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid meet">
      <rect style="fill:none;stroke:black" width="540" height="157" x="690" y="26"></rect>
      <text style="font-size:148px;text-align:center;text-anchor:middle;fill:black;stroke:none" x="960" y="145">Test</text>
      <text style="font-size:112px;text-align:center;text-anchor:middle;fill:black;stroke:none" x="960" y="340">Lorem ipsum etc etc</text>
      <rect style="fill:yellow;stroke:black" width="1901" height="100" x="10" y="618" rx="20" ry="20"></rect>
      <text style="font-size:64px;text-align:center;text-anchor:middle;fill:black;stroke:none" x="960" y="685">Bottom banner text</text>
      <rect style="visibility:hidden" x="10" y="726" width="1901" height="347" id="placeholder"></rect>
    </svg>
  </div>
  <div id="overlay">
    <p style="text-align:center">Hello World, from HTML outside SVG.</p>
  </div>
</body>

It seems to behave as expected. (And the reason for the less-than-100% widths was because I needed box-sizing: border-box -- which I had tried on the html element's style before asking the question, but it had no effect there; apparently it needs to be applied directly rather than inherited.)

like image 377
Miral Avatar asked Sep 30 '14 07:09

Miral


2 Answers

Unfortunately, the answer to your question is "no"; the viewBox changes the entire coordinate system, including the definition of a px or pt unit, for all its children.

The content in <foreignObject> is rendered to a screen buffer (temporary image) the same width and height in px as the foreignObject's width and height is in the local SVG coordinate system, and then the result is transformed to fit on the screen, the same as if you'd drawn a (JPEG or PNG) image in a transformed SVG coordinate system.

If using Javascript is an option, the best solution to have readable text and a responsive graphic is to have absolutely positioned <div> elements superimposed over the SVG. This CodePen I put together about tooltips should help get you started on how to convert between the SVG coordinate system and the page coordinate system. In particular, you'll want to read up on the SVGPoint object and the getScreenCTM() function.

Although more work, this approach will also circumvent the cross-browser problems with foreignObject, which are not supported at all in IE, and which are rather erratic (e.g., not always getting scrollbars you need) in other browsers.

like image 100
AmeliaBR Avatar answered Oct 21 '22 03:10

AmeliaBR


Alternative solution is to position the lower DIV (outside SVG) over the SVG area.

<div style="width:99%;border:4px solid blue; position:relative;top:-100px;">

The scaling of foreignObject inside SVG is correct. It still doesn't make sense to have DIV inside SVG inside HTML (convoluted way of coding IMHO) when you can simplify it. Or if you want control text size inside SVG, use the <text...> tags.

like image 36
Alvin K. Avatar answered Oct 21 '22 04:10

Alvin K.