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:
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:
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:
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)
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.
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.
Then, the bounding box gives
patternTransform
.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.