How to do photoshop-like effects in SVG
Tuesday, 5. February 2008, 21:10:00
Read on for my take on the tutorial named How To Create a Stunning Vista Inspired Menu. To better follow the steps in my article I recommend reading these side by side.
Step 1
Creating the svg canvas.
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 600 335">
</svg>
By adding a 'viewBox' with the dimensions (600x335) we can get the SVG to scale to whatever size we want later on.
Step 2
Creating some rectangles and a couple of gradients.
<linearGradient id="lowergrad" x1="0" y1="1" x2="0" y2="0"> <stop stop-color="#000000" offset="0"/> <stop stop-color="#0c0c0c" offset="1"/> </linearGradient> <linearGradient id="uppergrad" x1="0" y1="1" x2="0" y2="0"> <stop stop-color="#35393d" offset="0"/> <stop stop-color="#787b7d" offset="1"/> </linearGradient> <g id="bar" fill-opacity="0.9" shape-rendering="optimizeSpeed"> <rect id="upperbar" y="285" width="600" height="25" fill="url(#uppergrad)"/> <rect id="lowerbar" y="310" width="600" height="25" fill="url(#lowergrad)"/> </g>
We are instructed to create some linear gradients between color A and color B at a 90 degrees angle. Straight angles are easily translated into a vector from point (x1,y1) to point (x2,y2), where the value '1' maps to 100% of the boundingbox of the shape that gets painted with the gradient, and the value '0' maps of course to 0% of the same. We add the gradient stops with the colors we want, and the set the offsets. The offset is where the color will be mapped onto the gradient vector which we specified with x1, y1, x2 and y2.
The 'g' element groups the rects together and provides a 'shape-rendering' property to disable anti-aliasing so that the edges stay crisp. We also add the 90% opacity to the 'g' layer. Then for each of the rects we assign a fill. Now for better performance we realise we can use 'fill-opacity' instead of 'opacity'.
Step 3
Create a few straight lines. We add these in the 'g' element from the previous step.
<line stroke-width="2" stroke="#9fa2a4" x1="0" y1="287" x2="600" y2="287"/> <line stroke-width="2" stroke="#484b4d" x1="0" y1="285" x2="600" y2="285"/>
One thing to note here is that 'line' elements have no fill, only stroke. Everything else should be fairly easy to understand, we draw the line between (x1,y1) and (x2,y2).
Step 4
Drawing the divider line.
<linearGradient id="upperdivider" x1="0" y1="1" x2="0" y2="0"> <stop stop-color="#676a6d" offset="0"/> <stop stop-color="#afb1b2" offset="1"/> </linearGradient>
First we add the gradient that goes on the bottom half of the divider.
<g id="divider"> <line y1="290" y2="310" stroke-width="2" stroke="url(#upperdivider)"/> <line y1="310" y2="330" stroke-width="2" stroke="#43474b"/> </g> ... <use xlink:href="#divider" x="100"/> <use xlink:href="#divider" x="200"/> <use xlink:href="#divider" x="300"/> <use xlink:href="#divider" x="400"/> <use xlink:href="#divider" x="500"/>
We define the divider group once, then re-use it multiple times with the 'use' element. If we change the look of the divider all of the 'use' instances will be updated too. It's possible to optimize here by creating filled rects instead of lines, and using only one gradient. In this particular example that's hardly the most timeconsuming part of the svg anyway, so I've opted to leave it as close to the original tutorial as possible.
Step 5
Adding the text, styling it with common properties and aligning it.
<style>
.links { font-family: Arial, sans-serif;
font-size: 16px;
text-anchor: middle;
fill: white;
text-rendering: optimizeLegibility; }
</style>
...
<text class="links" x="150" y="316.5">Blog</text>
<text class="links" x="250" y="316.5">About</text>
<text class="links" x="350" y="316.5">Tutorials</text>
<text class="links" x="450" y="316.5">Contact</text>
Instead of rewriting stuff multiple times we assign a 'class' attribute and then use CSS for styling all the text. We use Arial with a sans-serif fallback, and set the size and align the text using the 'text-anchor' property. The text will be aligned around the 'x' coordinate we specify. I've added a 'text-rendering' property to make the text look more readable on viewers that support that. Usually it means that the text renders a bit more crisp.
Step 6
To add the background image we simply drop in an 'image' element.
<image xlink:href="background.jpg" width="100%" height="100%" preserveAspectRatio="xMinYMid slice"/>
The image should cover the entire canvas so we specify 'width' and 'height' to 100%. Note that we can also write width="600" height=335" since we have defined the coordinate system by using the 'viewBox' attribute on the root 'svg' element (in step #1). Finally we add a 'preserveAspectRatio' attribute so that if we decide to add a graphic that can't be scaled to fit exactly it will still fill the canvas without being stretched. Now, when we want we can simply make the image point at an svg instead, meaning that we get a fully scalable result.
Step 7
To get the blurred rounded rect we have to use SVG filters and clipping. Let's start with defining the clip region, which is a rounded rect.
<clipPath id="clip"> <rect id="blurrect" x="-10%" y="25%" width="55%" height="60%" rx="20"/> </clipPath>
As you see this is as simple as drawing any other SVG graphic, the only difference is that we wrap it inside a 'clipPath' element. This defines a clipping region that we can use on other elements.
Next let's create the blur filter.
<filter id="blurpane"> <feImage xlink:href="#blurrect" result="clip"/> <feGaussianBlur stdDeviation="2" in="SourceGraphic" result="blur"/> <feComposite operator="in" in="blur" in2="clip"/> <feComposite mode="over" in="blur" in2="SourceGraphic" result="final"/> </filter>
Steps in the filter:
- Take rounded-rect and define it as input to the filter, name it 'clip'
- Blur the background image (using keyword 'SourceGraphic'), name it 'blur'
- Composite the two together effectively clipping the result
- Composite the result of the previous step with the original background image
Here's how to use the clip-path and filter:
<g filter="url(#dropshadow)">
<g clip-path="url(#clip)">
<image image-rendering="optimizeSpeed" xlink:href="background.jpg"
width="100%" height="100%" preserveAspectRatio="xMidYMid slice"
filter="url(#blurpane)"/>
</g>
</g>
We have added a drop-shadow filter on the blurred region. The drop-shadow filter has its filter region limited so that we don't spend time filtering regions that aren't interesting, here's what it looks like:
<filter id="dropshadow" x="0" y="30%" width="60%" height="54%">
<feGaussianBlur in="SourceAlpha" stdDeviation="5"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.5"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
Steps in the drop-shadow filter:
- Take alpha-channel of the graphic that the filter is applied on and blur it
- Make it more transparent by using a component transfer on the alpha-channel
- Merge the blurred slightly more transparent shadow with the original graphic (SourceGraphic)
A tip for visualising the filter region is to make a simple filter and use an feFlood element to fill the region. It should furthermore be noted that I make no claims that the provided filter-chains are optimal - in fact I'm fairly sure they could be improved.
Step 8
Some text for the blurred rect, plus a gradient fill and a drop-shadow.
<text id="header" font-family="Arial" font-weight="900" font-size="40" x="20" y="55%" fill="url(#textgrad)" filter="url(#smallblur)">SVG Example</text> <text id="subheader" font-family="Arial" font-size="20" font-style="italic" x="20" y="75%" fill="url(#textgrad)" filter="url(#smallblur)">Shiny new web standards for all!</text>
The gradient we use is nothing special, but I've included it here anyway along with a drop-shadow filter that we also apply on the text.
<linearGradient id="textgrad" x1="0" y1="1" x2="0" y2="0">
<stop stop-color="#afb1b2" offset="0"/>
<stop stop-color="white" offset="0.5"/>
</linearGradient>
<filter id="smallblur" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1.5"/>
<feOffset dx="2" dy="2"/>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
Step 9
The hover effect, a blurred ellipse with clipping. This is re-using some of the code we already have.
<ellipse id="hover" cx="250" cy="340" rx="100" ry="30" style="display:none"/>
A bit of style on the element itself here, but most of it has been put in the 'style' element:
#hover { fill:#5c94c5;
filter:url(#bigblur);
clip-path:url(#cliphover); }
The ellipse is clipped to the rect shape of the buttons.
Step 10
Since this is SVG we can play with the result easily, and add dynamic effects. So let's add a hover effect.
<script>
function setHover(val)
{
document.getElementById("hover").style.display="inline";
document.getElementById("hover").cx.baseVal.value=val+50;
document.getElementById("hoverrect").x.baseVal.value = val-1;
}
function hideHover()
{
document.getElementById("hover").style.display="none";
}
</script>
We call these functions on mouseover on a few interesting areas. These areas can be defined separately from the actual graphic elements used (in this case the text labels). I've defined a few rectangles for regions that looked reasonable for the mouseover effects. The rects are invisible and are just there for listening to the events.
Final result
Note: if your browser doesn't support the SVG Basic 1.1 filters the above will show a static jpeg image instead. You may click here to see the source of the SVG.
Conclusion
Using modern web standards it's easy to create nice effects that were previously only possible in photo editing applications, such as Photoshop. By using stylesheets we can change the look of the graphic without needing to go back to an image editing application to re-color and re-export and then fix all links to point to the new images. Instead we might decide that blue was a sucky color, and just add a style-rule for changing that to a fresh lime green. No other changes required! And if you're still stuck doing all that Photoshopping I'll just grab a beer in the mean time while the browser does the job for me instead.










dmarks # 19. February 2008, 01:45
Just slightly disappointed that the background image is a raster - it looks crappy when zoomed in... Any chance of getting it completely vectorized?
MacDev_ed # 19. February 2008, 08:44
In Opera 9.5 replacing the background image for any svg graphic is as simple as changing the xlink:href on the <image> element to point to an SVG file. Perhaps try that yourself, or wait for a follow-up article.
Anonymous # 5. March 2008, 02:39
Erik, your example works great in the latest Firefox 3 build, if you replace the SVG
Anonymous # 5. March 2008, 02:41
OOps repost...
Erik, your example works great in the latest Firefox 3 build, if you replace the SVG
Anonymous # 6. March 2008, 21:49
I figured out what the problem was with the style element. We do support it, but in SVG the "type" attribute on
MacDev_ed # 13. March 2008, 08:09
Thanks, I'll fix the svg.
Note that this is finding its way into the SVG 1.1 errata though, to align <svg:style> with <html:style> (which defaults 'type' to 'text/css').
Anonymous # 28. October 2009, 21:39
Many thanks for good article!
In Linux all it normally works in Opera 9.x and Opera 10.
But unfortunately this SVG does not work in Firefox 3.0.14 in Linux Fedora 11. Firefox shows a picture at the choice the 'switch'.