Elliptical light source using SVG filters - svg

I am trying to apply a Vignette effect on images with SVG filters. I am trying to implement this with the <fePointLight> primitive, but I find this a bit limited in that it is always has a circular shape. Is it possible to change the width such that the lighting effect takes on a wide elliptical shape? This is currently the filter I am using:
<filter id="vignette">
<feFlood id="flood-5" result="blackfield-6" x="0%" y="0%" width="100%" height="100%" flood-color="#000000" flood-opacity="1"/>
<feSpecularLighting id="specular-5" result="Spotlight-6" lighting-color="#FFFFFF" surfaceScale="1" specularConstant="1" specularExponent="100">
<fePointLight id="pointlight-5" x="720" y="450" z="1200"/>
</feSpecularLighting>
<feBlend id="svg-7" result="A-6" in="blackfield-6" in2="Spotlight-6" mode="lighten"/>
<feBlend id="blend-5" result="B-6" in="A-6" in2="SourceGraphic" mode="multiply"/>
</filter>
I am aware that it is possible to do this with a radial gradient effect on a separate shape, but I have a requirement that it must be done purely using SVG filters.

The values of the x=350 and y=240 attributes of the fePointLight filter are chosen so that the point is in the center of the image.
Different values for the z attribute of the fePointLight filter render the light source at different depths in relation to the drawing. The nearest position corresponds to the largest size.
Please watch in full screen
Hover over image
#img {
width:700px;
height:481px;
}
#img:hover {
filter:url(#spotlight);
}
<img id="img" src="https://i.stack.imgur.com/mBuDo.jpg" >
<svg width="700" height="481" viewBox="0 0 700 481" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="spotlight">
<feSpecularLighting result="spotlight" specularConstant="3.5"
specularExponent="70" lighting-color="grey">
<fePointLight x="350" y="240" z="520"/>
</feSpecularLighting>
<feComposite in="SourceGraphic" in2="spotlight" operator="in"/>
</filter>
</defs>
</svg>
Other image
#img {
width:700px;
height:481px;
}
#img:hover {
filter:url(#spotlight);
}
<img id="img" src="https://i.stack.imgur.com/GlhkD.jpg" >
<svg width="700" height="481" viewBox="0 0 700 481" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="spotlight">
<feSpecularLighting result="spotlight" specularConstant="3.5"
specularExponent="70" lighting-color="grey">
<fePointLight x="350" y="240" z="520"/>
</feSpecularLighting>
<feComposite in="SourceGraphic" in2="spotlight" operator="in"/>
</filter>
</defs>

If you want that sort of effect (elliptical light), you probably want to be using a spotlight rather than a point light.
If you offset the light position to one side and shine the cone down at a shallow-ish angle, you will get an elliptical spot.
<svg width="600" height="529">
<defs>
<filter id="spotlight">
<feSpecularLighting result="spotlight" specularConstant="1.5"
specularExponent="4" lighting-color="#FFF">
<feSpotLight x="-200" y="265" z="400" limitingConeAngle="10" pointsAtX="300" pointsAtY="265" />
</feSpecularLighting>
<feComposite in="SourceGraphic" in2="spotlight" operator="out" k1="0" k2="1" k3="1" k4="0"/>
</filter>
</defs>
<rect x="0" y="0" width="600" height="529" style="fill: skyblue; filter:url(#spotlight);"/>
</svg>
Note that the lighting filter components can be unreliable to use. Each browser has differences in interpretation of the standard. Not to mention some bugs.
A point in case is the above example, which looks different in Firefox and Chrome.
But good luck with your project.

Related

SVG feGaussianBlur producing noticeable banding & edges

I'm working on some generative art using SVG. The one thing I noticed that SVG feGaussianBlur filter produces ugly result for nearest colors. E.g.:
<svg viewBox="0 0 512 512" width="512" height="512">
<defs>
<filter id="blur-1" filterUnits="userSpaceOnUse">
<feGaussianBlur edgeMode="none" in="SourceGraphic" stdDeviation="40" />
</filter>
</defs>
<rect x="0" y="0" width="512" height="512" fill="#333" />
<circle cx="256" cy="256" r="128" fill="#444" filter="url(#blur-1)" />
</svg>
Produces strange results around the edges of blurred area. Looking for a way to fix this.
<svg viewBox="0 0 512 512" width="512" height="512">
<defs>
<filter id="blur-1" filterUnits="userSpaceOnUse">
<feGaussianBlur edgeMode="none" in="SourceGraphic" stdDeviation="40" />
</filter>
</defs>
<rect x="0" y="0" width="512" height="512" fill="#111" />
<circle cx="256" cy="256" r="128" fill="#222" filter="url(#blur-1)" />
</svg>
See rendered result
The banding that you're seeing (on Windows only probably) is due to Windows graphics API's doing a crappy job of dithering for larger blurs when the color space is linearRGB. If you add
color-interpolation-filters="sRGB" to your filter element you'll get a smooth blur.

how to make a svg scatter filter?

I try to turn a precise shapes into a series of scatted dots that have a higher density in the middle of the source image.
Example: White Circle on Black Background → Messier 92 (sorry i hive not enough reputation to embed the image)
Consequently a shape other then a circle should still be recognizable. Here is the best I was able to do:
I would call such an effect a scatter filter. Please tell me if you have a better name.
<svg height='100' width='100'>
<filter id="cluster" filterUnits="userSpaceOnUse">
<feGaussianBlur in="SourceGraphic" result='blurred' stdDeviation="10" />
<feTurbulence type="turbulence" baseFrequency="9" numOctaves="4" result="turbulence" />
<feDisplacementMap in2="turbulence" in="blurred" scale="10" xChannelSelector="R" yChannelSelector="G" />
</filter>
<rect x='0' y='0' height='100' width='100' fill='black' />
<circle cx='50' cy='50' fill='white' r='30' filter='url(#cluster)' />
</svg>
It is basically a fancy blur. The results look better with increased scale in feDisplacementMap.
<svg height='100' width='100'>
<filter id="cluster" filterUnits="userSpaceOnUse">
<feGaussianBlur in="SourceGraphic" result='blurred' stdDeviation="10" />
<feTurbulence type="turbulence" baseFrequency="9" numOctaves="4" result="turbulence" />
<feDisplacementMap in2="turbulence" in="blurred" scale="50" xChannelSelector="R" yChannelSelector="G" />
</filter>
<rect x='0' y='0' height='100' width='100' fill='black' />
<circle cx='50' cy='50' fill='white' r='30' filter='url(#cluster)' />
</svg>
But that also displaces the image. Can I undo this? Or not do it in the first place? Maybe by using something other than feDisplacementMap?
The main problem with your filter is its very high baseFrequency - you need to dial that waaay down. And fractalNoise is better than turbulence as a feTurbulence type for a scatter filter. As long as your displacement map is centered at 0.5 of the channel, your image shouldn't be displaced in x/y on average. It's only a problem if you've got a bright or dark displacement map (which is not the case with feTurbulence). Finally - a higher displacement scale will give you the pointy look you're looking for - like so.
It can take a bunch of trial and error to get the exact look that you want. This is a link to a filter editor for shadows that might give you some ideas https://codepen.io/mullany/pen/sJopz
<svg height="220px" width="260px" viewBox="0 0 800 600">
<defs>
<filter id="scatter">
<feTurbulence baseFrequency=".2" type="fractalNoise" numOctaves="3"/>
<feDisplacementMap in="SourceGraphic" xChannelSelector="G" yChannelSelector="B" scale="300"/>
<feComposite operator="in" in2="finalMask"/>
</filter>
</defs>
<g filter="url(#scatter)">
<polyline points="10,10 10,300, 300,400" transform="translate(60 60)" fill="blue"/>
<polyline points="500,10 110,300, 300,400" transform="translate(260 60)" fill="red"/>
</g>
</svg>

SVG filter: different result depending on browser

I am trying to add a point light effect to an SVG rectangle. The problem is that i got different results depending on the browser I am using. For example in Chrome and Safari i got the following:
How could I get a consistent result using svg filters on different browsers?
*, *:before, *:after {
box-sizing: border-box;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" rel="stylesheet"/>
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="customPointLight">
<feSpecularLighting result="lightBuffer" specularConstant="1.5"
specularExponent="80" lighting-color="#fff">
<fePointLight x="100" y="100" z="80"/>
</feSpecularLighting>
<feComposite in="SourceGraphic" in2="lightBuffer" operator="out"
k1="0" k2="1" k3="1" k4="0"/>
</filter>
</defs>
<rect x="50" y="50" width="100" height="100" fill="blue" filter="url(#customPointLight)"></rect>
</svg>
Safari is choosing an incorrect filter resolution automatically, probably because no-one has bothered to update the code for retina displays. You can kick Safari to do "mostly" the right thing by adding filterRes="200" to the filter element because it hasn't dropped support for filterRes yet.
That said, today, the right thing to do cross-browser, is to punt on light sources completely and just use a rectangle filled with a black/white radial gradient imported as a data:URI with feImage (for Firefox & Edge compatibility). A screen blend will add the white high light to the original as I think you were intending. Like so:
svg {
background: red;
}
<svg width="400px" height="200px" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="lightHack">
<stop offset="35%" stop-color="white"/>
<stop offset="80%" stop-color="black"/>
</radialGradient>
<filter id="customPointLight">
<feSpecularLighting result="lightBuffer" specularConstant="1.5"
specularExponent="80" lighting-color="#fff">
<fePointLight x="100" y="100" z="80"/>
</feSpecularLighting>
<feComposite in="SourceGraphic" in2="lightBuffer" operator="out"
k1="0" k2="1" k3="1" k4="0"/>
</filter>
<filter id="pointLightHack" x="0%" y="0%" width="100%" height="100%">
<feImage width="100" height="100" xlink:href="data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiDQogICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KDQogIDxkZWZzPg0KICAgIDxyYWRpYWxHcmFkaWVudCBpZD0iZXhhbXBsZUdyYWRpZW50Ij4NCiAgICAgIDxzdG9wIG9mZnNldD0iNDAlIiBzdG9wLWNvbG9yPSJ3aGl0ZSIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSI3NSUiIHN0b3AtY29sb3I9ImJsYWNrIi8+DQogICAgPC9yYWRpYWxHcmFkaWVudD4NCiAgPC9kZWZzPg0KICA8Y2lyY2xlIGZpbGw9InVybCgjZXhhbXBsZUdyYWRpZW50KSIgY3g9IjUwIiBjeT0iNTAiIHI9IjUwIi8+DQo8L3N2Zz4="/>
<feBlend mode="screen" in="SourceGraphic"/>
</filter>
</defs>
<rect x="50" y="50" width="100" height="100" fill="blue" filter="url(#customPointLight)"/>
<rect x="250" y="50" height="100" width="100" fill="blue" filter="url(#pointLightHack)"/>
</svg>
<!-- SVG source of the base64 encoded feImage -->
<svg width="100" height="100" viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="exampleGradient">
<stop offset="40%" stop-color="white"/>
<stop offset="75%" stop-color="black"/>
</radialGradient>
</defs>
<circle fill="url(#exampleGradient)" cx="50" cy="50" r="50"/>
</svg>
As an aside, you're not using the lighting effect correctly, specular lighting is supposed to add "shiny" highlights, so the proper usage is to composite the result on top of the source. Diffuse lighting is supposed to add "regular" light and it should be multiplied with the original graphic. In neither case should you be using an "out" composite operation - which is punching a transparent hole in your rectangle, as you can see when you add the red background above.
For all filters that use neighboring pixels in their computation, like feSpecularLighting, feGaussianBlur or feConvolveMatrix, the result is influenced by attribute filterRes, which defines the resolution for computing the filter effect. Unlike other attributes, the specification defines no default:
If not provided, then the user agent will use reasonable values to produce a high-quality result on the output device.
That leaves room for alot of differences between UAs.
feSpecularLighting itself has an attribute kernelUnitLength that explicitly references filter resolution:
For some level of consistency (sic!) across display media and user agents, it is necessary that a value be provided for at least one of ‘filterRes’ and ‘kernelUnitLength’.
Solved for now adding a circle with blur primitive which gives a similar effect and seems to be rendered correctly across browsers.
*, *:before, *:after {
box-sizing: border-box;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" rel="stylesheet"/>
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<filter id="blur">
<feGaussianBlur in="SourceGraphic" stdDeviation="4"></feGaussianBlur>
</filter>
</defs>
<rect x="50" y="50" width="100" height="100" fill="blue""></rect>
<circle cx="100" cy="100" r="20" fill="#fff" filter="url(#blur)"></circle>
</svg>

Matching SVG fill pattern with repeated background image on containing element?

I am trying to create some ornate page dividers using SVGs with a fill pattern using the same image as the containing element's background image but I am having difficulty getting the SVG pattern to match the repeated background image of the containing element. I found a few similar questions on StackOverflow which mentioned using preserveAspectRatio but none of the solutions I've found are creating the desired effect.
Here's what my SVG code looks like:
<svg id="Layer_1" class="divider" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 150" >
<defs>
<pattern id="imgpattern" patternUnits="userSpaceOnUse" width="576" height="576" preserveAspectRatio="xMinYMin slice">
<image width="576" height="576" xlink:href="http://s12.postimg.org/730a258rx/pattern2.jpg"/>
</pattern>
<filter id="dropshadow" height="130%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="2" result="offsetblur"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<path d="M-394.09,367.9V295.34l1400,0v70s-3.39.9-4.92,1.38c-56.68,17.79-114.85,25.72-174.22,26.17-36.9.27-73.6-2.7-110.25-6.28q-67.4-6.59-134.7-14.08c-49.65-5.51-99.28-8.74-149-1.64-35.12,5-68.54,15.05-99.24,33.08-6,3.53-12.09,7-18.13,10.48-8.94,5.18-17.93,5.51-27,.36-3.47-2-7.14-3.6-10.51-5.71-48.22-30.22-101-43.79-157.83-41.84-29.13,1-58.27,2.45-87.3,4.94-40.49,3.48-80.88,8.18-121.32,12.27-24.37,2.46-48.71,5.26-73.14,7a714.52,714.52,0,0,1-96.42.34,605.73,605.73,0,0,1-96.81-13.9C-368.07,374.88-394.09,367.9-394.09,367.9Z" transform="translate(394.09 -295.32)" stroke="none" fill="url(#imgpattern)" filter="url(#dropshadow)" />
</svg>
And here's a JSFiddle which recreates my issue. Notice how the background pattern of the SVG appears more stretched and because of that does not completely match the repeated background image of the container.
One option is just to move the pattern fill into the filter like so - although there is a small problem that your texture has a one pixel black border, so the tiling isn't perfect.
<filter id="dropshadowandfill" height="130%" >
<feImage x="0" y="0" width="575" height="575" xlink:href="http://s12.postimg.org/730a258rx/pattern2.jpg"></feImage>
<feTile/>
<feComposite operator="in" in2="SourceGraphic" result="filledOriginal"/>
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="2" result="offsetblur"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="filledOriginal"/>
</feMerge>
</filter>
</defs>
<path d="M-394.09,367.9V295.34l1400,0v70s-3.39.9-4.92,1.38c-56.68,17.79-114.85,25.72-174.22,26.17-36.9.27-73.6-2.7-110.25-6.28q-67.4-6.59-134.7-14.08c-49.65-5.51-99.28-8.74-149-1.64-35.12,5-68.54,15.05-99.24,33.08-6,3.53-12.09,7-18.13,10.48-8.94,5.18-17.93,5.51-27,.36-3.47-2-7.14-3.6-10.51-5.71-48.22-30.22-101-43.79-157.83-41.84-29.13,1-58.27,2.45-87.3,4.94-40.49,3.48-80.88,8.18-121.32,12.27-24.37,2.46-48.71,5.26-73.14,7a714.52,714.52,0,0,1-96.42.34,605.73,605.73,0,0,1-96.81-13.9C-368.07,374.88-394.09,367.9-394.09,367.9Z" transform="translate(394.09 -295.32)" stroke="none" filter="url(#dropshadowandfill)" />

Why does a filter break the half-pixel trick for crisp 1px strokes?

I'm using the "offset everything by half a pixel" trick discussed in the second answer to this question to get a crisp 1px stroke on a shape.
(There are rounded edges involved, so the shape-rendering: crispEdges solution isn't viable here -- it makes the curved parts of strokes look terrible.)
How come adding a filter (I'm using a gaussian blur + offset filter to implement a drop shadow) breaks the half-pixel offset hack?
You can play with the jsfiddle.
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<filter id="f1" x="-20%" y="-20%" width="160%" height="160%">
<feOffset result="offOut" in="SourceAlpha" dx="10" dy="10" />
<feGaussianBlur result="blurOut" in="offOut" stdDeviation="2" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
</defs>
<g transform="translate(5.5,5.5)">
<!-- This one has a blurry stroke -->
<rect width="50" height="50" rx="5" ry="5" stroke="black" stroke-width="1" fill="steelblue" filter="url(#f1)" />
<!-- This one has a crisp stroke -->
<rect x="150" width="50" height="50" rx="5" ry="5" stroke="black" stroke-width="1" fill="steelblue" />
</g>
</svg>
Apparently I can't post inline images as a new user, but you can this is an image of how the svg looks for me.
If you don't like the blurry edges, you can always experiment with adding an feComponentTransfer with feFuncA to manually manipulate the edge blur. Aka:
<filter id="fee" x="-20%" y="-20%" width="160%" height="160%">
<feComponentTransfer>
<feFuncA type="table" tableValues="0 0 1" />
</feComponentTransfer>
</filter>

Resources