6

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.)

Community
  • 1
  • 1
Miral
  • 12,637
  • 4
  • 53
  • 93
  • The viewBox is not a window, it's not something you "move" or "remove" or "change or "override". It's a kind of coordinate system, which is fixed and absolute for the SVG element. –  Sep 30 '14 at 07:37
  • The **viewBox attribute** is something that could be moved or removed, and could have been a part of a possible answer, except that I can't do that without breaking other things that I want to work the way they are. That's why I said that. – Miral Sep 30 '14 at 23:51
  • Thanks for adding the detailed code to help others. Very nicely organized into logical functions, too. – AmeliaBR Oct 01 '14 at 15:54
  • I should probably add that if anyone comes across this later and wants to use it, I left out the adjustment for the current page's scroll position because I knew that in my case the page couldn't scroll. Have a look at [AmeliaBR's CodePen](http://codepen.io/AmeliaBR/pen/kFDvH) if that's needed; there's an example there (search for `window.pageXOffset`). It should be pretty trivial to add, but I didn't want to put it in myself as I wasn't going to test it. – Miral Oct 08 '14 at 01:15

2 Answers2

5

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.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • `getScreenCTM` requires that the SVG already be added to the "real" document model. While I can do that, currently I'm trying to do any script-based changes to the SVG model while it is out of tree, in order to make everything just appear at once when I finally add it to the tree instead of being updated piecemeal. So is there something similar to `getScreenCTM` that works out of tree? – Miral Sep 30 '14 at 23:49
  • Nothing similar that will factor in a viewBox -- you can calculate the net transformation between the parent SVG and any element with the regular `getCTM` method, but you'd need to figure out the viewBox scaling to translate to the parent HTML coordinate system. I'm not sure if you have to worry too much about repaint performance of multiple layout tasks, though -- you can still add the SVG in one batch, and then the absolutely positioned divs don't create any layout recalculation. – AmeliaBR Oct 01 '14 at 02:55
  • And keep in mind, you'll need to re-call the Javascript function that sets the position and size of the divs when your window (and therefore your responsive SVG) resizes, so you're not going to be able to pre-calculate everything. – AmeliaBR Oct 01 '14 at 02:58
  • 1
    Thanks; based on your previous answer I amended my example code to use `getScreenCTM` and it appears to behave as desired now. It sounds like doing it without that might be a bit too much work. Now I just need to make sure that it still behaves when applied to my real page instead of just a toy example. :) – Miral Oct 01 '14 at 03:10
1

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.

Alvin K.
  • 4,329
  • 20
  • 25
  • That's the alternative I suggested in my question, however just saying that is useless without knowing how to calculate what the position should be. It cannot be static CSS, it would have to be calculated somehow from the SVG content by script. – Miral Sep 30 '14 at 23:46