Turning to the Dark Side

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"/>

All that I've done in the source code here is to copy #light as #dark with the new focal point and colour - rgb(0,0,0) - as I described above. To apply #shade, I've added #shade-layer between #flat-layer and #hilight-layer.

I had hoped that the symmetry would allow most of #light to be used for #shade, but the shading in this case looks too dark and too focused to me. There might be places where it's suitabe, but I'm not satisfied. I'd like to widen the umbra of the shadow (the darkest part, represented by the middle stop) and fade it a little more. Here's what I get.

<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 = "85%"/>
<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" opacity = "0.2"/>
<use id = "hilight-layer" xlink:href = "#shape" fill = "url(#light)" x = "5" y = "5"/>

I've altered the overall transparency of the fill in #shade-layer instead of adjusting the stop-opacity repeatedly. This is just my subjective opinion. If I were making many shapes with all the layers then I'd multiply each stop-opacity by 0.8 to simplify processing. An opacity of 0.2 makes the shading very subtle. Try the sample code with an opacity of 0 for comparison and you'll see that there is a difference, however subtle it may be.

Now that I have the shading effect that I'm satisfied with, I have to consider my original goals. I haven't specified any new colours, so I'm okay on that point. I still haven't tested with any shapes but my circle-cum-sphere, so that goal mayt not be met. It sounds the toughest and you may guess that I'm putting it off until last. As for adjusting position and intensity, I should be pretty close. The obvious problem interoduced by the shading is that the position of the focal point is meant to be oppositi to the focal point of the light, but there's nothing really enforcing that logic - there are two independent spots that a focal point has to be set. I know a way to remedy that, though it's not perfect. A gradientTransform applies a transform to a gradient such as #light or #shade within it's own coordinate space. The syntax should be the same as it is for the usual transform attribute. Here's what I get when I make #shade a rotated version of #light.

<defs>
<g id = "shape">
<circle cx = "0" cy = "0" r = "3"/>
</g>
<radialGradient id = "lightingBase" cx = "50%" cy = "50%" fx = "70%" fy = "15%" r = "55%"/>
<radialGradient id = "light" xlink:href = "#lightingBase">
<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" xlink:href = "#lightingBase" gradientTransform = "rotate(180,0.5,0.5)">
<stop stop-color = "rgb(0,0,0)" stop-opacity = "1" offset = "0%"/>
<stop stop-color = "rgb(0,0,0)" stop-opacity = ".8" offset = "85%"/>
<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" opacity = "0.2"/>
<use id = "hilight-layer" xlink:href = "#shape" fill = "url(#light)" x = "5" y = "5"/>

As you can see in the new source code, I decided to compose both #light and #shade from a common ancestor that I called #lightingBase. I took the attributes of #light that I want to propogate and moved them to #lightingBase, then I made a reference to #lightingBase in #light using the xlink:href attribute of radialGradient. Next I did the same to #shade, but I accomodated for the missing fx and fy attributes by applying a transform of rotate(180,0.5,0.5) with the gradientTransform as described above. The rotation syntax is straightforward: the first parameter is in degrees and second and third give the point (cx,cy) to rotate about. The confusion here is that the coordinate system is objectBoundingBox, set by the absent gradientUnits attribute. Simply put, the objectBoundingBox coordinate system goes from (0,0) to (1,1). Percentages don't seem to work for (cx,cy).

Now I'm going to try the final product with a variety of colours. Here's an example.

<defs>
<g id = "shape">
<circle cx = "0" cy = "0" r = ".75"/>
</g>
<radialGradient id = "lightingBase" cx = "50%" cy = "50%" fx = "70%" fy = "15%" r = "55%"/>
<radialGradient id = "light" xlink:href = "#lightingBase">
<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" xlink:href = "#lightingBase" gradientTransform = "rotate(180,0.5,0.5)">
<stop stop-color = "rgb(0,0,0)" stop-opacity = "1" offset = "0%"/>
<stop stop-color = "rgb(0,0,0)" stop-opacity = ".8" offset = "85%"/>
<stop stop-color = "rgb(0,0,0)" stop-opacity = "0" offset = "100%"/>
</radialGradient>
<g id = "litShape">
<use id = "flat-layer" xlink:href = "#shape" fill = "currentColor"/>
<use id = "shade-layer" xlink:href = "#shape" fill = "url(#shade)" opacity = "0.2"/>
<use id = "hilight-layer" xlink:href = "#shape" fill = "url(#light)"/>
</g>
</defs>
<g>
<use xlink:href = "#litShape" x = "0.9" y = "1.2" color = "rgb(255,220,180)"/>
<use xlink:href = "#litShape" x = "2.6" y = "3.7" color = "rgb(220,210,180)"/>
<use xlink:href = "#litShape" x = "5.0" y = "6.0" color = "rgb(195,200,180)"/>
<use xlink:href = "#litShape" x = "7.5" y = "6.5" color = "rgb(160,190,180)"/>
<use xlink:href = "#litShape" x = "8.6" y = "4.8" color = "rgb(140,180,180)"/>
<use xlink:href = "#litShape" x = "7.2" y = "3.0" color = "rgb(120,170,180)"/>
<use xlink:href = "#litShape" x = "5.1" y = "3.8" color = "rgb(100,160,180)"/>
<use xlink:href = "#litShape" x = "3.1" y = "5.8" color = "rgb(080,150,180)"/>
<use xlink:href = "#litShape" x = "2.5" y = "7.5" color = "rgb(050,130,180)"/>
<use xlink:href = "#litShape" x = "2.4" y = "9.1" color = "rgb(035,105,180)"/>
</g>

For this example I shrank the radius of the circle in #shape to get many in the viewBox. I could have done the same with a scale transform, but that might confuse me more about my coordinates. It also means more computation for the client. The other, more interesting, change that I made is to the colour specification. To avoid having to repeat all three layers for each sphere, I grouped the layers into a g elelment called #litShape and moved #litShape into the defs section. When I made this change, I also set the fill colour for #flat-layer to the special value currentColor. The value currentColor indicates that the paint comes from the color attribute in effect when the shape is drawn. The actual spheres are drawn by the last set of use tags. In each case a color attribute is specified. The color attribute is applied substituted for the currentColor in #litShape. I could also have done the specification in the CSS syntax - e.g. style="color:rgb(120,160,180)".

It's clear that this isn't a real light (or a realistic lighting algorithm), even though the effect does put a nice gloss on the shapes. Some of the shortcomings are obvious. The light looks like a point, but it's hitting all the instances of #shape in the exact same spot. Without filters or multiple gradients, the spot can't be moved easily on each ball as far as I can tell. This could be ammeliorated by making the imaginary light source farther away, indicated by a larger solid bright splotch. That would also reduce the implication of the illuminated object being perfectly round. Let's see if I can make this work with a slightly different shape.

<defs>
<g id = "shape" transform = "scale(2)">
<path d = "M 0,-.2 l .4,0 a .1,0.09 0 0 1 .1,.1 l -.1,.3 a 0.13,0.13 0 0 1 -.1,.1 l -.2,0 a 0.13,0.13 0 0 1 -.1,-.1 l -.1,-.3 a .1,0.09 0 0 1 .1,-.1 "/>
</g>
<radialGradient id = "lightingBase" cx = "50%" cy = "50%" fx = "60%" fy = "15%" r = "65%"/>
<radialGradient id = "light" xlink:href = "#lightingBase">
<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" xlink:href = "#lightingBase" gradientTransform = "rotate(180,0.5,0.5)">
<stop stop-color = "rgb(0,0,0)" stop-opacity = "1" offset = "0%"/>
<stop stop-color = "rgb(0,0,0)" stop-opacity = ".8" offset = "85%"/>
<stop stop-color = "rgb(0,0,0)" stop-opacity = "0" offset = "100%"/>
</radialGradient>
<g id = "litShape">
<use id = "flat-layer" xlink:href = "#shape" fill = "currentColor"/>
<use id = "shade-layer" xlink:href = "#shape" fill = "url(#shade)" opacity = "0.2"/>
<use id = "hilight-layer" xlink:href = "#shape" fill = "url(#light)"/>
</g>
</defs>
<g>
<use xlink:href = "#litShape" x = "0.9" y = "1.2" color = "rgb(255,220,180)"/>
<use xlink:href = "#litShape" x = "2.6" y = "3.7" color = "rgb(220,210,180)"/>
<use xlink:href = "#litShape" x = "5.0" y = "6.0" color = "rgb(195,200,180)"/>
<use xlink:href = "#litShape" x = "7.5" y = "6.5" color = "rgb(160,190,180)"/>
<use xlink:href = "#litShape" x = "8.6" y = "4.8" color = "rgb(140,180,180)"/>
<use xlink:href = "#litShape" x = "7.2" y = "3.0" color = "rgb(120,170,180)"/>
<use xlink:href = "#litShape" x = "5.1" y = "3.8" color = "rgb(100,160,180)"/>
<use xlink:href = "#litShape" x = "3.1" y = "5.8" color = "rgb(080,150,180)"/>
<use xlink:href = "#litShape" x = "2.5" y = "7.5" color = "rgb(050,130,180)"/>
<use xlink:href = "#litShape" x = "2.4" y = "9.1" color = "rgb(035,105,180)"/>
</g>

I'm on the fence about this one. I chose a pretty arbitrary shape, but I thought that the rounded corners would help. I don't really see the object as having any depth, though. I think what I've come up with is a pretty good starting point, but I don't think it's a general solution for lighting any object. Notice also that with the simple lighting I've made here, the shape has to be specified deep inside. There are a lot of parts of the SVG source code that would have to be repeated in order to light a couple of shapes this way. Filters are the way to avoid this. Filters are written so that the effect is independent of the shape it's applied to.

0
Your rating: None

[...] or maybe something uplifting&#8230; &#187; Turning to the Dark Side Says: August 21st, 2005 at 2:11 am [...]

Yep, this is what I was expressing to you Friday. Unfortunately, I think radial gradients are only good for effectively lighting round shapes, not arbitrary ones.

[...] 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&#8217;s no good reason to make big long functions then I stick to what fits on my screen. That&#8217;s very developer-centric of me, but I am a developer. [...]