SVG mask is clipping stroke on masked element - svg

I have a completely vertical <path> with a thick stroke-width applied. I would like to add a mask (i.e. mask="url(#...)") to it, however when I do, (how do i put this?) the stroke is ignored when computing the visible area. Here's a code snippet:
function toggleMask() {
var path = $('path');
if (path.attr('mask')) {
path.removeAttr('mask');
} else {
path.attr('mask',"url(#test)");
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div><button onclick="toggleMask()">toggle mask</button></div>
<svg width="400" height="400">
<defs>
<mask id="test">
<rect
width="100%"
height="100%"
x="0"
y="0"
fill="white">
</rect>
<circle r="20" cx="35" cy="80" fill="black"></circle>
</mask>
</defs>
<path
d="M30,30L30,300"
stroke-width="40"
stroke="black"></path>
<path
d="M50,30L100,300"
stroke-width="40"
stroke="black"></path>
</svg>
I expected the applied mask to look something like this:
Thanks in advance!

Add maskUnits="userSpaceOnUse"to your mask and it'll work as you want.
https://codepen.io/nerdmanship/pen/XazJVR
The reason is that when the maskUnits attribute is unspecified it defaults to the objectBoundingBox value, which means that the mask is applied to the area inside a target element's bounding box. The bounding box of a completely vertical or horizontal path has the width or height of 0. The result is that the mask is applied to a total area of 0 pixels of the target element.
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/maskUnits

Related

How to simulate stroke-align (stroke-alignment) in SVG

I am trying to mimic the behavior of "stroke alignment" in an SVG object. While there is a working draft for stroke-alignment in the spec, this has not actually been implemented (despite being drafted in 2015).
Example of non-working stroke-alignment:
The blue square should have stroke inside, the red outside, but they're both the same
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="100">
<g id="myGroup" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 50,0 50,50 0,50" style="fill:blue;stroke:black;stroke-width:4;stroke-alignment:inner"></polygon>
<polygon id="myPoly1" transform="translate(75 0)" points="0,0 50,0 50,50 0,50" style="fill:red;stroke:black;stroke-width:4;stroke-alignment:outer"></polygon>
</g>
</svg>
My approach to mimicking this behavior is to create a duplicate SVG object using the <use> element, setting a stroke property on the copy and scaling it slightly up or down depending on whether it's an inner or outer stroke alignment (default is center)
For example:
The scale and transform for the <use> element gets worse the farther from origin
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="400">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" transform="translate(110 110)" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" transform="translate(220 220)" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2" style="fill:none;stroke:black;stroke-width:4;" transform="translate(-2 -2) scale(1.04 1.04)"></use>
</g>
</svg>
As you can see from the above example, the relative positioning of the <use> element goes awry, and gets worse the farther away from the origin it gets.
Naively, I assume that the transform property of the <use> element acts upon the SVG shape referenced in its href, but that seems not to be the case.
In my example, I'm scaling a 100x100 square by a factor of 1.04, which should result in a 104x104 square (4px width of the stroke). I'm then translating back by -2px to position the stroke on the outside of the source shape, thereby mimicking an outer stroke alignment.
This works as expected if the source shape is at origin (relative to the container group), but goes bonkers if the source shape is translated away from the origin.
My brain says this should work, but my browser says no bueno.
Anyone got any clues?
So, it turns out that the transform applied to the <use> element will be applied to the existing transform of the source element.
This means that the scale transform applied to the <use> element will also scale its translate matrix.
For example:
If the source element has translate(100 100), applying a scale(1.1 1.1) on the <use> copy will cause it to have a translate with the value (110,110)
This means to move the copy with the stroke value back to the correct location, you need to move the copy far enough back to overcome this "scaled translation".
This may well be expected behavior, but it was not intuitive to me (I may need to RTFM). Overall this approach "works", but feels complicated and hacky.
Working sample:
const strokeWidth = 8;
const poly1Translate = {
x: 150,
y: 20
};
const poly2Translate = {
x: 300,
y: 40
};
const poly1 = document.getElementById("myPoly1");
const poly2 = document.getElementById("myPoly2");
const polyCopy0 = document.getElementById("myPolyCopy0");
const polyCopy1 = document.getElementById("myPolyCopy1");
const polyCopy2 = document.getElementById("myPolyCopy2");
const styleString = `fill:none;stroke:red;stroke-opacity:0.5;stroke-width:${strokeWidth};`;
poly1.setAttribute(
"transform",
`translate(${poly1Translate.x} ${poly1Translate.y})`
);
poly2.setAttribute(
"transform",
`translate(${poly2Translate.x} ${poly2Translate.y})`
);
polyCopy0.setAttribute("style", styleString);
polyCopy1.setAttribute("style", styleString);
polyCopy2.setAttribute("style", styleString);
// Use the boundingbox to get the dimensions
const poly1BBox = poly1.getBBox();
const poly2BBox = poly2.getBBox();
let halfStrokeWidth = strokeWidth / 2;
// stroke-alignment:outside
// Scale the copy to be strokeWidth pixels larger
let scaleOutsideX = 1+strokeWidth/poly1BBox.width;
let scaleOutsideY = 1+strokeWidth/poly1BBox.height;
// Move the copy to the same scale property based on the current translation
// This will position the stroke at the correct origin point, and we need to
// deduct a further half of the stroke width to position it fully on the outside
let translateOutsideX = -((poly1Translate.x * scaleOutsideX - poly1Translate.x) + halfStrokeWidth);
let translateOutsideY = -((poly1Translate.y * scaleOutsideY - poly1Translate.y) + halfStrokeWidth);
polyCopy1.setAttribute('transform', `translate(${translateOutsideX} ${translateOutsideY}) scale(${scaleOutsideX} ${scaleOutsideY})`);
// stroke-alignment:inside
let scaleInsideX = 1-strokeWidth/poly2BBox.width;
let scaleInsideY = 1-strokeWidth/poly2BBox.height;
let translateInsideX = poly2Translate.x * scaleOutsideX - poly2Translate.x + halfStrokeWidth;
let translateInsideY = poly2Translate.y * scaleOutsideY - poly2Translate.y + halfStrokeWidth;
polyCopy2.setAttribute('transform', `translate(${translateInsideX} ${translateInsideY}) scale(${scaleInsideX} ${scaleInsideY})`);
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20 20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100"></polygon>
<use id="myPolyCopy0" vector-effect="non-scaling-stroke" href="#myPoly0"></use>
<use id="myPolyCopy1" vector-effect="non-scaling-stroke" href="#myPoly1"></use>
<use id="myPolyCopy2" vector-effect="non-scaling-stroke" href="#myPoly2"></use>
</g>
</svg>
UPDATE
After noticing the following comment in the Figma website:
Inside and outside stroke are actually implemented by doubling the stroke weight and masking the stroke by the fill. This means inside-aligned stroke will never draw strokes outside the fill and outside-aligned stroke will never draw strokes inside the fill.
I implemented a similar method using a combination of <clipPath> and <mask>
.stroke {
fill:none;
stroke:red;
stroke-opacity:0.5;
}
.stroke-center {
stroke-width:8;
}
.stroke-inout {
stroke-width:16;
}
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="160">
<defs>
<rect id="stroke-mask" width="500" height="160" fill="white"/>
</defs>
<g id="myGroup" style="fill:rgb(45, 130, 255);" transform="translate(20,20)">
<polygon id="myPoly0" points="0,0 100,0 100,100 0,100" transform="translate(0,20)"></polygon>
<polygon id="myPoly1" points="0,0 100,0 100,100 0,100" transform="translate(150,20)"></polygon>
<polygon id="myPoly2" points="0,0 100,0 100,100 0,100" transform="translate(300,20)"></polygon>
<mask id="mask">
<use href="#stroke-mask"/>
<use href="#myPoly1" fill="black"/>
</mask>
<clipPath id="clip">
<use href="#myPoly2"/>
</clipPath>
<use id="myPolyCopy0" class="stroke stroke-center" href="#myPoly0"></use>
<use id="myPolyCopy1" class="stroke stroke-inout" href="#myPoly1" mask="url(#mask)"></use>
<use id="myPolyCopy2" class="stroke stroke-inout" href="#myPoly2" clip-path="url(#clip)"></use>
</g>
</svg>
The idea here is, to achieve the equivalent of:
stroke-align:center: is the default behavior, do nothing
stroke-align:inner: Create a clipPath using the source object to which you want to apply the inner stroke, then with the <use> element, create a copy of this with a stroke twice the width you actually want, and set the clip-path of the copy to be the clipPath created from the source object. This will effectively clip everything outside the clipPath, thereby clipping the "outside half" of the double-width stroke
stroke-align:outer: There isn't an equivalent of clipPath which will clip everything inside the path (sadly), so the way to achieve this is to use a <mask>, but the same principle applies as for inner. Create a <mask> based on the source object, create a copy with a double-width stroke, then use the mask to clip everything inside the mask, thereby clipping the "inside half" of the double-width stroke

Reversing the flip and rotation of an image fill in a path that is flipped and rotated

I am needing some help understanding how to "unflip/unrotate" and image fill in SVG for a path. When I fill a path with an image and then rotate and fill the path with an image, the image also flips and rotates. But I'd like to keep the image upright and non-flipped, regardless of the rotation and flipping. The size of the picture is the bounding box of the rotated shape.
So, for example, say I have this path and this picture:
If the path is only rotated (in this case, 315 degrees), it's easy to unrotate the image by just reversing the angle in the pattern that is used for a fill (i.e. 45 degrees).
<svg name="rotate only" x="0" y="0" width="100.08" height="200" overflow="visible" fill="url(#fillImages0sp15)" stroke="#4472C4" stroke-miterlimit="8" stroke-width="2.25">
<defs>
<image id="bgImage" preserveAspectRatio="none" width="159.113" height="159.113" xlink:href="THE IMAGE URL"></image>
<pattern id="fillImages0sp15" x="-38.362" y="11.598" width="159.113" height="159.113" patternTransform="rotate(45,50.04,100)" patternUnits="userSpaceOnUse">
<use xlink:href="#bgImages0sp15"></use>
</pattern>
</defs>
<path d="M0,149.96 25.02,149.96 25.02,0 75.06,0 75.06,149.96 100.08,149.96 50.04,200 Z " transform="rotate(315,50.04,100)"></path>
</svg>
But if there any kind of flip on the path (horizontal, vertical, or both), it doesn't work by just reversing the transformation on the pattern used for the image fill. For example, if the image is rotated 315 degrees and flipped vertical, the path has transform="rotate(45,50.04,100) translate(0,200), scale(1,-1)" for flipping vertically. That works. But the image fill needs to get reset back to be upright and not flipped. So the patternTransform should just be the same transformation. But this isn't working. This is the result I get.
<svg name="flipV" x="0" y="0" width="100.08" height="200" overflow="visible" fill="url(#fillImages0sp14)" stroke="#4472C4" stroke-miterlimit="8" stroke-width="2.25">
<defs>
<image id="bgImages" preserveAspectRatio="none" width="159.113" height="159.113" xlink:href="THE IMAGE URL"></image>
<pattern id="fillImages0sp14" x="-20.671" y="-370.711" width="159.113" height="159.113" patternTransform="rotate(45,50.04,100) translate(0,200) scale(1,-1)" patternUnits="userSpaceOnUse">
<use xlink:href="#bgImages"></use>
</pattern>
</defs>
<path d="M0,149.96 25.02,149.96 25.02,0 75.06,0 75.06,149.96 100.08,149.96 50.04,200 Z " transform="rotate(45,50.04,100) translate(0,200) scale(1,-1)"></path>
</svg>
Notice the path has transform="rotate(45,50.04,100) translate(0,200) scale(1,-1) and the fill pattern with the image has patternTransform="rotate(45,50.04,100) translate(0,200) scale(1,-1). This produces the wrong results.
In fact, here's all of it. This is what I'm hoping to achieve:
Does anyone know how to set the patternTransform so that it can "unflip/unrotate" the filled image? Is it that the translate in the patternTransform needs to be calculated differently?
Rather than filling the arrow, <svg ... fill="url(#fillImages0sp14)", transforming it and then trying to somehow separate it from its fill, untransform that, and then refill it, I'd just display the image, but masked by the transformed arrow.
I don't understand why the orange border still shows up. I've made the black rectangle overly large (which helped), and I've changed the overflow="...", but neither made it disappear.
Edit: Your global stroke attributes were messing up the mask. Moving them to the displayed arrow (the only thing using that stroke) fixed the orange border issue.
P.S. xlink is deprecated. Just use href.
P.P.S. I had to add a final translate to center the transformed arrow over the image. It's easier and more accurate to move the center of the arrow to the center of the image first, and then do your transformations about that center.
<svg name="transformed" x="0" y="0" width="159.113" height="159.113" overflow="visible" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="Arrow" d="M0,149.96 25.02,149.96 25.02,0 75.06,0 75.06,149.96 100.08,149.96 50.04,200 Z" />
<use id="TransformedArrow" href="#Arrow" transform="translate(38.3625,-29.2893) rotate(45,50.04,100) translate(0,200) scale(1,-1)" />
<mask id="ArrowMask">
<!-- Everything under black will be invisible -->
<rect x="0" y="0" width="100%" height="100%" fill="black" />
<!-- Everything under white will be visible -->
<use href="#TransformedArrow" fill="white" />
</mask>
</defs>
<image width="159.113" height="159.113" mask="url(#ArrowMask)" href="https://i.stack.imgur.com/yDcGi.png" />
<use href="#TransformedArrow" fill="none" stroke="#4472C4" stroke-miterlimit="8" stroke-width="2.25" />
</svg>

SVG | Erase part of another path

I'm working on an SVG image and I can't figure out how to erase a certain part of a path.
This is the current situation: https://gyazo.com/db59fcaf9f122e7e2c0bba5833db9ec5
There are two green letters which overlap and a red bar which does basically represent the area I want to erase so the letters don't stick directly on each other. It works fine when I have a set background colour since I can then easily overwrite lower paths, but with transparent background, this method no longer works, since it appears to make the path transparent, not the entire pixel itself.
TL;DR: How do I make a path actually render the pixel transparent, not just the path element?
You can mask the J with a white rect and a black N with an extra stroke. Next you use again the N. Please play with the stroke width of the mask <use>
svg{border:1px solid; width:90vh}
text{font-family:arial;dominant-baseline:middle}
<svg viewBox="0 0 24 24">
<defs>
<text id="n" x="7" y="14" >N</text>
<mask id="mascara">
<rect width="24" height="24" fill="white" />
<use xlink:href="#n" fill="black" stroke="black" />
</mask>
</defs>
<text x="5" y="10" style="mask: url(#mascara)">J</text>
<use xlink:href="#n" fill="black" />
</svg>

How to automatically create the minimal size of the viewbox which fits for the complete content?

I have a simple or complex SVG graphic. For example a rotated rectangle.
Without calculating you cannot know the minimal size of the viewbox, where the graphic fits into completely.
<svg viewBox="0 0 30 30">
<rect x="20" y="0" width="100" height="20" transform="rotate(45)" fill="black" />
</svg>
The result is, that the graphic does not fit into the viewbox.
Is there any method, how to get an the minimal size of the viewbox, where the graphic is shown completely?
Ideally I do not want to declare a size/ratio of a viewbox. I just want that the minimal size is a result of the content of the SVG graphics.
Is there any disadvantage, when I do not declare the viewBox attribute at all?
Thanks for your help.
One way to do it is wrapping the transformed rectangle in a <g> element and then get the value of the bounding box for theG. Next you use the values of the bounding box (BB) to reset the viewBox of theSVG. I hope it helps.
// the bounding box for the wrapping g
let BB = theG.getBBox();
theSVG.setAttributeNS(null, "viewBox", `${BB.x} ${BB.y} ${BB.width} ${BB.height}`)
svg{border:1px solid}
<svg id="theSVG" viewBox="0 0 30 30" width="300">
<g id="theG">
<rect x="20" y="0" width="100" height="20" transform="rotate(45)" fill="black" />
</g>
</svg>

Why does adding a positive viewbox min-x, min-y value have a negative translate effect?

I was just going through THIS fiddle and the code looks like below:
<svg width=200 height=200 viewbox="0 0 225 225" >
<path d="M220, 220
A200, 200, 0, 0, 0, 20, 20
L 20, 220
Z"
fill = "lightskyblue">
</path>
</svg>
Now when i play around with the viewbox and change the value to viewbox="100 100 225 225" it has the effect of doing something like:
transform:translate(-100px, -100px);
Well i believe when i specify 100 as the min-x, min-y the values of viewbox the effect should have been something like
transform:translate(100px, 100px);
But instead the effect is something similar to:
transform:translate(-100px, -100px);
Why so ? can somebody explain ?
By setting minX and minY to 100, what you are doing is telling the SVG renderer that the top left of your SVG starts at (100,100). And that point should be at the top left of the SVG viewport.
It is the same as if you decided your ruler started at the 10cm mark. The 12cm mark would appear to be at 2cm instead of 12cm. In other words 10cm further left (lower).
Have a look at the following sample SVG. I've marked out an area which we will make set the viewport and viewBox to in a later example.
<svg width="600" height="600">
<!-- mark the area that will become the viewport -->
<rect x="100" y="100" width="300" height="200" fill="linen"/>
<!-- add some other content -->
<circle cx="120" cy="120" r="20" fill="red"/>
<circle cx="200" cy="200" r="50" fill="red"/>
<circle cx="380" cy="270" r="50" fill="red" fill-opacity="0.3"/>
</svg>
If we now set the viewBox to the cream coloured area and set the viewport (SVG width and height) correspondingly, you will see what happens.
<svg width="300" height="200" viewBox="100 100 300 200">
<!-- mark the area that will become the viewport -->
<rect x="100" y="100" width="300" height="200" fill="linen"/>
<!-- add some other content -->
<circle cx="120" cy="120" r="20" fill="red"/>
<circle cx="200" cy="200" r="50" fill="red"/>
<circle cx="380" cy="270" r="50" fill="red" fill-opacity="0.3"/>
</svg>
You can see that the small red circle which is roughly at 100,100, is now at the top left of the viewport.
Hope this makes it clearer for you.
Imagine you have a sheet of paper with your drawing on it and you overlay a piece of cellulite (or anything transparent) on top.
Draw a box on the cellulite and colour in everything outside the box.
Move the cellulite to the right.
Your drawing (the part you can still see within the cellulite box) appears to have moved to the left.
the viewBox is the cellulite box in this example.

Resources