Search code examples
svg

why does an inner viewBox changes my scale?


In a nutshell:

In this simplified example, I don't understand why my third cross is smaller than the others, I would have expected all cross to be the same

<svg width="512" height="128" xmlns="http://www.w3.org/2000/svg">
<!-- My baseline: no viewbox, and I use percentages -->
<svg x="0%" width="25%" height="100%">
        <line x1="50%" y1="0" x2="50%" y2="100%" stroke="black" stroke-width="5%"/>
        <line x1="0" y1="50%" x2="100%" y2="50%" stroke="black" stroke-width="5%"/>
</svg>

<!-- With a viewbox, to be able to not use percentages but still have something independant from my viewport -->
<svg x="25%" width="25%" height="100%" viewBox="0 0 100 100">
        <line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="5"/>
        <line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="5"/>
</svg>

<!-- with a viewbox one level below. Why is this cross smaller? -->
<svg x="50%" width="25%" height="100%">
    <g viewBox="0 0 100 100">
        <line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="5"/>
        <line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="5"/>
    </g>
</svg>

<!-- with a viewbox inside a viewbox. The size is restored by I have no clue why... -->
<svg x="75%" width="25%" height="100%" viewBox="0 0 100 100">
    <g viewBox="0 0 100 100">
        <line x1="50" y1="0" x2="50" y2="100" stroke="black" stroke-width="5"/>
        <line x1="0" y1="50" x2="100" y2="50" stroke="black" stroke-width="5"/>
    </g>
</svg>
</svg>


More context

  • Why I expect all crosses to be the same:

    • The first one is my baseline
    • The second one uses non percent coordinates, but given the dimensions of the viewBox it should be the same (which is the case)
    • The third one is the same as the second one, except that the viewBox is defined one level below. But the viewport and the viewBox are still the same so I don't expect any change (but I'm wrong)
    • I don't understand the fourth one. I've noticed that I get back my cross when I repeat my viewBox directive, but I have no clue why.
  • The full context of my question is that I'm trying to create a tilesheet. It contains an element with different rotations, so I define this elements in a <defs> and then apply some transform on it, in successive sub-svg. I define this element with a viewBox because it's the only way I'm aware of, to be able to use a <path> while still being independent of the viewPort. But I've noticed this behavior where some of my tiles have an unexpected scale. (my example above has no <defs> and no <path> because I managed to get a simpler example than with my original svg)


Solution

  • That viewBox on you <g> Group element is ignored

    https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox

    viewBox is only valid on elements:

    • SVG
    • MARKER
    • PATTERN
    • SYMBOL
    • VIEW

    For a tile game a native Web Component with SVG only for one tile might be easier.
    All positioning is done with HTML and CSS

    <tiles style="display:grid;grid:repeat(3,1fr)/repeat(3,1fr);background:teal;width:240px">
      <svg-tile></svg-tile>  <svg-tile></svg-tile>            <svg-tile></svg-tile>
      <svg-tile></svg-tile>  <svg-tile rotate=0></svg-tile>   <svg-tile></svg-tile>
      <svg-tile></svg-tile>  <svg-tile></svg-tile>            <svg-tile></svg-tile>
    </tiles>
    
    <script>
      customElements.define("svg-tile", class extends HTMLElement {
        constructor() {
          const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
          super().attachShadow({ mode: "open" })
            .append(
              createElement("style", {
                innerHTML: `svg {display:inline-block;vertical-align:top}` +
                           `path {stroke:black}`
              }),
              this.rotation = createElement("style"), // set by rotate(x)
              createElement("div", {
                innerHTML: `<svg viewBox="0 0 6 6"><path d="M3 0v6M0 3h6"/></svg>`
              })
            );
        }
        connectedCallback() {
          this.rotate = ~~(this.getAttribute("rotate") || 45);
          this.onclick = (evt) => this.rotate = this._rotate + 45;
        }
        set rotate(value) {
          this.rotation.innerHTML = `svg{transform:rotate(${this._rotate=value}deg)}`;
        }
      })
    </script>