I've been doing some playing around with SVG effects that can be achieved without filters. I started playing because the build of Firefox (Deer Park Alpha 2) that I'm using doesn't have them built-in. There's work under way and a patch is available if you build from source.
There are better reasons for faking lighting though. Filters are resource intensive and sometimes you don't care about realistic lighting so much as just a way to make something shiny (or shadowed). The comparable filter to what I'm talking about today is fePointLight.
My simple example is lighting a circle to make it look more like a sphere. I have a few goals:
Next if I imagine the difference between this circle and a picture of a shiny ball, a marble maybe, the most obvious missing feature is the transition of light from the top to bottom. If a light is bright enough, it will appear white (assuming that the light source itself is white) at the point closest to the light source and there will be some shadow on the bottom where the light is blocked.
I'm going to start by assuming that the light source is white and that it is above an a little to the right of the ball. I'm also going to ignore the shadowing underneath for now. If the ball is blue then what I want is to make a white spot on top that fades gradually in to blue. This is basically the definition of a gradient. Specifically a radial gradient changes gradually from one colour to the next as the distance from the focal point increases. Some basic examples of radial gradients can be seen here.
There are two differences in the source code. The first is inside the defs. A radialGradient is defined with the id="light". The stop elements in the radialGradient define what colour the gradient will be at different points. Between these points the colour is interpolated. This gradient starts out white - that is rgb(255,255,255) at its focal point and gradually changes to the shade of blue that the circle is in Step 1 of this example - rgb(0,0,180). I've also put in a midtone of rgb(200,200,240) at 15% of the gradient's radius. This is just my way of shaping the gradient, it's arbitrary and subjective. If you know more about light and want to do the math you could do something more realistic. The second difference from Step 1 is that I've replaced the fill colour of the circle with a reference to the new gradient. The gradient fill is specified as fill="url(#light)".
I've set the focal point of the gradient (representing the point closest to the light) with fx and fy at (70%,15%). I use percentages so that the gradient scales easily and I don't have to worry too much about coordinates. A radius of 50% covers the entire width and height of space that the gradient will fill. That is to say that the outer circle of the radialGradient (a stop offset of 100%) should just touch the bounding box of any shape that uses the radialGradient for a fill. If you want to get the gradient to cover a box corner to corner (so the radialGradient circumscribes the box) then you'd need a radius of half the square root of 2, expressed as a percentage. Approximately 70.7107%. This shape is a circle though, and I just want the gradient fill to be a little bigger than the area it's filling, so I've set the radius to 55%.
Above I said that I don't want the effect to depend on the colours we're using. So if I want to make an orange ball instead of a blue one, how do I do that from this point? The colours are embedded in #light, so that's where the values have to be changed. If I want to have both a blue and an orange ball, then I'll need another gradient. I can avoid this by re-evaluating the approach for the gradient.
The stops for linearGradient and radialGradient have a colour and an opacity. Instead of blending two colours on one shape, controlling the opacity allows me to defer the blending and remove it from the #light gradient. I'll draw the shape twice, once in the colour I want at 100% opacity then draw the light on top of that. Here's the new image.
In the third step I've changed the colour for all the stops of #light to white - rgb(255,255,255). I've also added a stop-opacity attribute for each stop of the radialGradient. The stop at the focal point, 0%, is 1. An opacity of 1.0 corresponds to being solid white and none of the image underneath will show. I want to simulate a bright light on a shiny surface, so 1.0 makes sense. If you want a more subtle effect, try lowering this initial value. The outside edge of the radialGradient has a stop-opacity of 0 so that the shape under the one being filled will show through completely. To flood the shape with light, you could turn that outer edge opacity up a little bit. I think that flooding effect won't work with the shadow effect that I'll add in later though. Note that the outside edge of the radialGradient actually falls past the edge of the shape it's applied to because the radius is over 50%.
I did the same kind of interpolation that the SVG viewer will do in order to transform the midtone that I used in Step 2. I came up with an opacity value of 0.8, and my example was only slightly contrived. How do I arrive at 0.8? Easy: 255-240=15, 255-180=75. 75-15=60. 60/75=0.8. That's for the 180 in that the midtone colour of rgb(200,200,180). Similar math holds for the red and green components.
In addition to the changes to #light, I've changed the way it's used. There are now two references to #shape. I've labelled them for convenience as #flat-layer and #hilight-layer. #flat-layer is drawn first then #hilight-layer is drawn on top. The SVG viewer takes care of alpha blending since using the opacity values I specified earlier. Notice that the x,y position is repeated on both layers. You can avoid this repetition by specifying them in the earlier #shape definition and maybe I should have too, but it's too late now.
How are we doing compared to the goals I set out above? Let's try that orange ball.
All that's changed here is the colour. Pretty easy.
This has gotten to be rather long, so I think I'll add the shadow in my next post.
[...] I’m going to continue from the last example I posted with the shading under the sphere. To see how I got this far, read yesterday’s post: Fake Lighting Without Filters in SVG. Dark is the opposite of light, so as a first approximation, I’ll try just applying a gradient that’s conceptually opposite to the one I made for the light effect. The size of the shading gradient will be the same as the light, but it will be black instead of white. The focal point will also be moved; to the exact opposite side. So the (fx,fy) of (70%,15%) maps to (30%,85%). Here’s what it looks like. <defs> <g id = “shape“> <circle cx = “0” cy = “0” r = “3“/> </g> <radialGradient id = “light” cx = “50%” cy = “50%” fx = “70%” fy = “15%” r = “55%“> <stop stop-color = “rgb(255,255,255)” stop-opacity = “1” offset = “0%“/> <stop stop-color = “rgb(255,255,255)” stop-opacity = “.8” offset = “15%“/> <stop stop-color = “rgb(255,255,255)” stop-opacity = “0” offset = “100%“/> </radialGradient> <radialGradient id = “shade” cx = “50%” cy = “50%” fx = “30%” fy = “85%” r = “55%“> <stop stop-color = “rgb(0,0,0)” stop-opacity = “1” offset = “0%“/> <stop stop-color = “rgb(0,0,0)” stop-opacity = “.8” offset = “15%“/> <stop stop-color = “rgb(0,0,0)” stop-opacity = “0” offset = “100%“/> </radialGradient> </defs> <use id = “flat-layer” xlink:href = “#shape” fill = “rgb(0,0,180)” x = “5” y = “5“/> <use id = “shade-layer” xlink:href = “#shape” fill = “url(#shade)” x = “5” y = “5“/> <use id = “hilight-layer” xlink:href = “#shape” fill = “url(#light)” x = “5” y = “5“/> [...]
[...] I was thinking over the SVG lighting examples that I presented over the weekend and I wanted a clean way to produce something more complex that utilizes the lighting effect that I came up with. Examples are easiest to learn from when the information can be presented in small chunks. I think that logic carries through to code analysis. If there’s no good reason to make big long functions then I stick to what fits on my screen. That’s very developer-centric of me, but I am a developer. So the way that I know to keep things small when it comes to XML is XSLT. That might sound backwards since XSLT and XML can be very verbose, but what I mean is that units can be independently analyzed. Large and complicated functionality can be elegantly composed from many small blocks. Chunks of code that I’ve produced with XSLT tend to be quite digestable - so far. [...]