Search code examples
svgmatrixrotationtransformscale

Background image fill of grouped shapes. Can do un-flip, can do un-rotate. Can't do un-flip AND un-rotate


I have a nested set of SVG shapes that need to display the same image fill, based on the BB of the rotated/flipped parent SVG. But while the shapes are flipped/rotated, I don't want their image fills to be be. This is similar to Reversing the flip and rotation of an image fill in a path that is flipped and rotated, except it is for a group of nested shapes rather than just a single shape.

My image to be used as a fill is this:

enter image description here

And my shapes - not rotated or flipped are like this:

<svg name="Group - not flipped or rotated" x="10" y="10" width="400" height="400">
        <g>
          <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" stroke="red" stroke-miterlimit="8" stroke-width="1" fill="none">
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="none" stroke="green" stroke-miterlimit="8" stroke-width="1">
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="none" stroke="blue" stroke-miterlimit="8" stroke-width="1">
             <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z "></path>
          </svg>
        </g>
      </svg>

Together, they look like this: enter image description here

Now I can rotate the shape group (in this case, 285°) and "unrotate" the image fills , like this.

<svg name="Group - rotated 285, not flipped." x="10" y="10" overflow="visible" width="228" height="217">
        <g transform="rotate(285,112.389,122.117)">
          <defs>
            <pattern width="239.3" height="219.212" id="groupFillImages0sp10" patternUnits="userSpaceOnUse">
              <image preserveAspectRatio="none" width="239.3" height="219.212" href="https://i.sstatic.net/jhG6H.png"></image>
            </pattern>
          </defs>
          <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp11)" stroke="red" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp10" id="groupFillImages0sp11" patternTransform="rotate(75,50,100) translate(-53.063, 36.687)"></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp12)" stroke="green" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp10" id="groupFillImages0sp12" patternTransform="rotate(75, 50, 100) translate(-73.493, -57.975)"></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="url(#groupFillImages0sp13)" stroke="blue" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp10" id="groupFillImages0sp13" patternTransform="rotate(75, 55.262, 42.545) translate(-139.65, -10.834)"></pattern>
            </defs>
            <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z "></path>
          </svg>
        </g>
      </svg>

And I can flip it (in this case, horizontally), like so:

<svg name="Group - flipped horizontal, not rotated." x="54.258" y="247.007" width="219" height="238" overflow="visible">
        <g transform="translate(224.779,0) scale(-1,1)">
          <defs>
            <pattern width="224.779" height="244.234" id="groupFillImages0sp6" patternUnits="userSpaceOnUse">
              <image preserveAspectRatio="none" width="224.779" height="244.234" href="https://i.sstatic.net/jhG6H.png"></image>
            </pattern>
          </defs>
          <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp7)" stroke="red" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp6" id="groupFillImages0sp7" patternTransform="translate(-86.148, 0) translate(224.779,0) scale(-1,1)"></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp8)" stroke="green" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern x="0" href="#groupFillImages0sp6" id="groupFillImages0sp8" patternTransform="translate(0, -44.234) translate(224.779,0) scale(-1,1) "></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="url(#groupFillImages0sp9)" stroke="blue" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp6" id="groupFillImages0sp9" patternTransform="translate(-114.254, -143.603) translate(224.779,0) scale(-1,1)"></pattern>
            </defs>
            <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z"></path>
          </svg>
        </g>
      </svg>

HOWEVER, I can't figure out how to both unflip and unrotate the fill image at the same time. This is the best I came up with, but it's not working:

<svg name="Group - rotated 285 AND flipped horizontal" x="689.198" y="247.007" overflow="visible" width="286" height="273">
        <g transform="rotate(75,112.389,122.117) translate(224.779,0), scale(-1,1)">
          <defs>
            <pattern width="239.3" height="219.212" id="groupFillImages0sp2" patternUnits="userSpaceOnUse">
              <image preserveAspectRatio="none" width="239.3" height="219.212" href="https://i.sstatic.net/jhG6H.png"></image>
            </pattern>
          </defs>
          <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp3)" stroke="red" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp2" id="groupFillImages0sp3" patternTransform="rotate(75, 50, 100) translate(224.779,0), scale(-1,1) translate(-111.616, -21.794)"></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp4)" stroke="green" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp2" id="groupFillImages0sp4" patternTransform="rotate(75, 50, 100) translate(224.779,0), scale(-1,1) translate(-46.593, 49.97)"></pattern>
            </defs>
            <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
          </svg>
          <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="url(#groupFillImages0sp5)" stroke="blue" stroke-miterlimit="8" stroke-width="1">
            <defs>
              <pattern href="#groupFillImages0sp2" id="groupFillImages0sp5" patternTransform="rotate(75, 55.262, 42.545) translate(224.779,0), scale(-1,1) translate(-31.776, -133.777)"></pattern>
            </defs>
            <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z "></path>
          </svg>
        </g>
      </svg>

I've tried almost all positions of the different transforms in patternTransform, but it never works. So I'm thinking it's not related to prepending/appending to the matrix, but rather some other kind of calculation. Might anyone know what this kind of calculation should be.

Also, here is what it should look like:

enter image description here


Solution

  • If you'd like to cancel all the sequential group-transforms, you need to put inversion of each transform in reverse order inside the patternTransform. Then all can vanish.

    translate(224.779,0)               // group transform #1
        scale(-1,1)                    // group transform #2
           rotate(285,112.389,122.117) // group transform #3
    
              translate(114.254, 143.603)  // inner svg's offset x="114.254" y="143.603"
    
    
              translate(-114.254, -143.603) // patternTransform #1
           rotate(75, 112.389,122.117)      // patternTransform #2
         scale(-1,1)                        // patternTransform #3
     translate(-224.779,0)                  // patternTransform #4
    

    So, if both rotation and flip are to be applied, it should be;

    <svg name="Group - rotated 285 AND flipped horizontal" x="10" y="10" overflow="visible" width="228" height="248">
      <path d="M0,0 h224.779 v244.234 h-224.779 z M0,122.117 h224.79 M112.3895,0 v244.234" fill="none" stroke="gray" stroke-width="1" stroke-dasharray="2,2"/>
      <g transform="translate(224.779,0) scale(-1,1) rotate(285,112.389,122.117)">
        <defs>
          <pattern width="224.779" height="244.234" id="groupFillImages0sp14" patternUnits="userSpaceOnUse">
            <image preserveAspectRatio="none" width="224.779" height="244.234" href="https://i.sstatic.net/jhG6H.png"></image>
          </pattern>
        </defs>
        <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp15)" stroke="red" stroke-miterlimit="8" stroke-width="1">
          <defs>
            <pattern href="#groupFillImages0sp14" id="groupFillImages0sp15" patternTransform="translate(-86.148, 0.0) rotate(75, 112.389,122.117) scale(-1,1) translate(-224.779,0)"></pattern>
          </defs>
          <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
        </svg>
        <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp16)" stroke="green" stroke-miterlimit="8" stroke-width="1">
          <defs>
            <pattern href="#groupFillImages0sp14" id="groupFillImages0sp16" patternTransform="translate(0, -44.234) rotate(75, 112.389,122.117) scale(-1,1) translate(-224.779,0)"></pattern>
          </defs>
          <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
        </svg>
        <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="url(#groupFillImages0sp17)" stroke="blue" stroke-miterlimit="8" stroke-width="1">
          <defs>
            <pattern href="#groupFillImages0sp14" id="groupFillImages0sp17" patternTransform="translate(-114.254, -143.603) rotate(75, 112.389,122.117) scale(-1,1) translate(-224.779,0)"></pattern>
          </defs>
          <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z "></path>
        </svg>
      </g>
    </svg>

    Some additional fine adjustments on pattern-size/offset may be required though. (to be continued to the Appendix B)

    Appendix A: How it works briefly

    According to the documents,

    • The group's transform is for mapping local coordinates xy to global coordinates XY.

    • The patternTransform is for mapping pattern coordinates uv to local coordinates xy.

    Writing transformations in matrix-form like as CSS Transform Rendering Model,

    TG = translate(224.779,0) scale(-1,1) rotate(285,112.389,122.117) translate(114.254, 143.603) 
    TP = translate(-114.254, -143.603) rotate(75, 112.389,122.117) scale(-1,1) translate(-224.779,0)
    

    we can express actual transform operations as follows;

    XY = TG xy  
    xy = TP uv 
    

    By combining these, uv to XY mapping is to be;

    XY = TG TP uv = uv 
    

    Here, because TG TP vanishes, pattern-pixels are sampled based on global coordinates XY as expected.

    Appendix B: How to adjust mapping programmatically

    In case the entire pattern need to fit to bounding box perfectly, we need to adjust transformations programmatically.

    All what to do is to calculate bounding box of transformed shapes in following steps.

    1. Parse all the path-segments included.
    2. Map them to global coordinate system by applying group-transform
    3. And measure actual boundaries.

    Then, the bounding box gives

    • x and y -> additional translation required for patternTransform.
    • width and height -> pattern size to be set.

    Minimal implementation specific to this sample would be something as follows;

    function calculateMapping()
    {
      let svg = document.querySelector('svg');
      let relevantGroup = document.getElementById('relevantGroup');
      let groupMatrix = relevantGroup.transform.baseVal.consolidate().matrix;
    
      let pattern18 = document.getElementById('groupFillImages0sp18');
    
      let container1 = relevantGroup.children["1"]; // inner svg 1
      let patternMatrix1 = composePatternMatrix(container1, groupMatrix);
      let bounds1 = getActualBounds(container1, patternMatrix1.inverse());
    
      let container2 = relevantGroup.children["2"]; // inner svg 2
      let patternMatrix2 = composePatternMatrix(container2, groupMatrix);
      let bounds2 = getActualBounds(container2, patternMatrix2.inverse());
    
      let container3 = relevantGroup.children["3"]; // inner svg 3
      let patternMatrix3 = composePatternMatrix(container3, groupMatrix);
      let bounds3 = getActualBounds(container3, patternMatrix3.inverse());
    
      let boundsAll = bounds1;
    
      boundsUnionPoint(boundsAll, bounds2);
      boundsUnionPoint(boundsAll, bounds2.slice(2));
      boundsUnionPoint(boundsAll, bounds3);
      boundsUnionPoint(boundsAll, bounds3.slice(2));
      let patternSize = [boundsAll[2] - boundsAll[0], boundsAll[3] - boundsAll[1]];
    
      console.log("x,y", boundsAll.slice(0, 2));
      console.log("w,h", patternSize);
      
      // Adjust pattern size
      pattern18.width.baseVal.value = patternSize[0];  // pattern width=".."
      pattern18.height.baseVal.value = patternSize[1]; // pattern height=".."
      pattern18.firstElementChild.width.baseVal.value = patternSize[0]; // image width=".."
      pattern18.firstElementChild.height.baseVal.value = patternSize[1]; // image height=".."
    
      // Adjust UV offsets
      let shiftUV = svg.createSVGTransform();
      shiftUV.setTranslate(boundsAll[0], boundsAll[1]);
      setPatternTransform(container1, patternMatrix1, shiftUV);  
      setPatternTransform(container2, patternMatrix2, shiftUV);
      setPatternTransform(container3, patternMatrix3, shiftUV);
     
      // Adjust XY offsets
      let shiftXY = svg.createSVGTransform();
      shiftXY.setTranslate(-boundsAll[0], -boundsAll[1])
      relevantGroup.transform.baseVal.insertItemBefore(shiftXY, 0)
    
    }
    
    function composePatternMatrix(svgContainer, groupMatrix) {
      let patternMatrix = groupMatrix.inverse();
      patternMatrix.e -= svgContainer.x.baseVal.value;
      patternMatrix.f -= svgContainer.y.baseVal.value;
      return patternMatrix;
    }  
    function setPatternTransform(svgContainer, patternMatrix, offsetTransform) {
      let patternTransform = svgContainer.getElementsByTagName('pattern')[0].patternTransform;
      let svg = document.querySelector('svg');
      patternTransform.baseVal.initialize(svg.createSVGTransformFromMatrix(patternMatrix));
      patternTransform.baseVal.appendItem(offsetTransform);
    }
    
    function getActualBounds(svgContainer, mapMatrix) {
      let pathElement = svgContainer.getElementsByTagName('path')[0];
      let pathData = parsePathDataMinimal(pathElement.getAttribute('d')); // [[x,y], ...]
    
      // Map local x,y to global X,Y
      pathData = pathData.map(element => {
    return [ 
      mapMatrix.a * element[0] + mapMatrix.c * element[1] + mapMatrix.e,
      mapMatrix.b * element[0] + mapMatrix.d * element[1] + mapMatrix.f
    ]
      })
      // Calculate bounding box.
      let bounds = [ pathData[0][0], pathData[0][1], pathData[0][0], pathData[0][1] ]; // [left, top, right, bottom]
      pathData.slice(1).forEach(element => { boundsUnionPoint(bounds, element); });
      return bounds;
    }
    
    
    function boundsUnionPoint(bounds, point) {
      bounds[0] = Math.min(bounds[0], point[0]);
      bounds[1] = Math.min(bounds[1], point[1]);
      bounds[2] = Math.max(bounds[2], point[0]);
      bounds[3] = Math.max(bounds[3], point[1]);
    }
    
    // TODO: Replace this path-data parsing function with more serious one.
    function parsePathDataMinimal(pathData) {
      const regexXY = /(-?\s*\d+(?:\.\d*)?)\s*,\s*(-?\s*\d+(?:\.\d*)?)/g; // Well-formed M and L only.
      return [...pathData.matchAll(regexXY)].map(element => {
    return [parseFloat(element[1]), parseFloat(element[2])];
      });
    }
    <svg name="Group - rotated 285 AND flipped horizontal" x="10" y="10" overflow="visible" width="228" height="248">
      <path d="M0,0 h239.3 v219.213 h-239.3 Z M0,109.607 h239.3 Z M119.65,0 v219.213 Z" fill="none" stroke="gray" stroke-width="1" stroke-dasharray="2,2"/>
      <g id="relevantGroup" transform="translate(224.779,0) scale(-1,1) rotate(285,112.389,122.117)">
    <defs>
      <pattern width="224.779" height="244.234" id="groupFillImages0sp18" patternUnits="userSpaceOnUse">
        <image preserveAspectRatio="none" width="224.779" height="244.234" href="https://i.sstatic.net/jhG6H.png"></image>
      </pattern>
    </defs>
    <svg name="1" x="86.148" y="0" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp19)" stroke="red" stroke-miterlimit="8" stroke-width="1">
      <defs><pattern href="#groupFillImages0sp18" id="groupFillImages0sp19"></pattern></defs>
      <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
    </svg>
    <svg name="2" x="0" y="44.234" width="100" height="200" overflow="visible" fill="url(#groupFillImages0sp20)" stroke="green" stroke-miterlimit="8" stroke-width="1">
      <defs><pattern href="#groupFillImages0sp18" id="groupFillImages0sp20"></pattern></defs>
      <path d="M0,150 25,150 25,0 75,0 75,150 100,150 50,200 Z "></path>
    </svg>
    <svg name="3" x="114.254" y="143.603" width="110.525" height="85.091" overflow="visible" fill="url(#groupFillImages0sp21)" stroke="blue" stroke-miterlimit="8" stroke-width="1">
      <defs><pattern href="#groupFillImages0sp18" id="groupFillImages0sp21"></pattern></defs>
      <path d="M0,21.273 67.979,21.273 67.979,0 110.525,42.545 67.979,85.091 67.979,63.818 0,63.818 Z "></path>
    </svg>
      </g>
    </svg>
    
    <button onclick="calculateMapping()">Calculate mapping</button>

    Note: In the code above, a simple X,Y regex matcher was used to parse path-data, because the shapes here are all consist of straight lines with absolute coordinates only. If more complex path (including curves, relative coordinates or …) is used, full-featured parser is to be required. How to parse svg path is another controversial big theme itself though.