Wednesday, October 20, 2021

Creating a Pencil Effect in SVG (Part 2)

Quite some time ago, I wrote a post about creating a pencil effect in SVG.  It's challenging to do a pencil effect in SVG because SVG is fundamentally a vector graphics format, and pencils are fundamentally not.  I ended up applying an SVG filter that added some texture and other effects to create a line that looks at least somewhat like a pencil:

At the time, I left it at that and skipped over a couple of other characteristics of pencil-drawn lines that I didn't have a good way to replicate.  One of those characteristics is that pencil doesn't completely cover whatever is underneath it.  So when two pencil lines overlap, they build up to a darker color.  You can see an example in the drawing below, where a single pencil makes a spectrum of shades, in part by building up graphite in areas where lines are drawn repeatedly:


Admittedly, artists control darkness by pressure and density of line as well, and the color of the graphite limits how dark you can go.  But in general if we want to have a good “hand-drawn pencil" look, we'd like to get some darker textures where lines overlap.

In SVG, when one line crosses another line, the top line complete covers the bottom line.  So when you have two lines of the same color that overlap, they just seamlessly blend into each other.  Here's an example of a map where I've filled in a forest area with a bunch of scribbled lines in a pencil color:
It has a little bit of texture thanks to the pencil effect, but all the lines just blend into one flat area of color.  That's pretty unsatisfactory.  So how do we get the colors to “build up" where lines cross?

You might think (I certainly did) that opacity is the answer.  If we set the opacity of our lines to (say) 50%, then 50% of the underlying color will show through, and that will let us build up a darker shade where lines cross.  But unfortunately, it doesn't work that way.  If the colors and opacities are the same, then the overlap area looks just like the non-overlapped areas.  No matter how many layers you build up, it always looks the same.

There are other work-arounds that I might consider, but if you have any experience with computer graphics you might have heard of blend modes.  Blend modes control how two colors interact when they overlap.  The default is alpha compositing, which is what SVG does where the top color completely obscures the bottom color.  But there's another mode called multiply that blends two colors when they overlap in a way that would be useful for pencil strokes.

The bad news is that SVG doesn't support multiply blend mode except in filters.  There's no way to tell regular SVG elements to multiply with colors below.  (Except perhaps by abusing filters in horrible ways that I don't want to contemplate.)  The good news is that CSS does support a multiply blend mode!

CSS is a styling mechanism for browser pages that provides a flexible way to control the appearance of page elements.  CSS was invented around the same time as SVG, but unlike SVG has continued to grow and evolve and has added capabilities like blend modes.  In modern browsers you can apply CSS styles even to individual SVG elements, which will enable us to borrow the multiply blend mode from CSS and use it in SVG.  

Here's what the flat fill example from above looks like when I turn on CSS multiply blend mode on all the individual pencil strokes:

Needs some tuning, but you can see that the color now gets darker where strokes overlap.

(There are some reasons not to mix CSS and SVG, the primary one being that the CSS effects will be lost when you isolate the SVG -- say to import it into Inkscape for some manual editing.  But in this case, there's no real work-around that would allow me to create this effect in “pure" SVG.)

There are a number of improvements I can make to the first effort above.  One is to break up the long fill strokes into shorter segments.  An amateur human artist likely wouldn't draw single lines all the way across the area.  He'd fill in using shorter strokes and work in patches across the area.  Splitting up the fill area into realistic patches is a little too challenging, but it isn't too hard to break up the lines going across the fill area into smaller segments:

I've circled an area where you can (faintly) see where long lines have been broken up into consecutive line segments.  Because both ends of each segment have rounded ends, and one segment ends on the start point of the next segment, you get a sort of circle where they overlap.  That's a little too regular to be pleasing, so let I'll make the start and end points a bit more random:

This goes a long way towards making the strokes more evident.  In the same area as above, you can clearly see a stroke ending and an overlap just above.  This example is a bit too obvious for my tastes, so I'll tone it down and adjust it to be a little more subtle:

Another aspect of hand-drawn pencil strokes that I didn't include the first time around is gradient.  Pencil strokes are often darker at the beginning, or where they change direction.  This is partly because humans aren't very precise and don't always hit exactly the right shade when they make a pencil stroke.  If you start too light you can just go over it again, but if you start too dark all you can do is ease up quickly, leaving a darker start to the line.  And it's partly because physiologically it's easier to trail off a line than to trail in a line.  At any rate, you often see strokes like on the right side of this image:
where some strokes start dark and end lighter.  Compared to the pencil strokes on the left side, these seem more realistic.  (Although somewhat exaggerated here.)  

The bad news is that, like blend modes, SVG doesn't implement gradient colors on lines.  The good news is that I already tackled this problem back when I was making river borders work better.  So it's just a matter of reusing that code to occasionally mix in a stroke that starts dark and ends lighter.

But in the process of doing that, I discovered some problems in the code that fills an area with lines.  It was drawing some lines in one direction and some in the other.  And the code itself was complex enough that I didn't really understand how it worked anymore.  So I stopped and reimplemented the code to “sweep" a polygon in a more straightforward way.  There are some efficient algorithms for doing this, but I was content with a simpler implementation based on the description here.  (The basic idea of drawing lines across a polygon at an arbitrary angle by first rotating the polygon and then drawing vertical lines is very clever!)

This change means re-tuning the various parameters, but eventually I have this:
Which you can see now contains the occasional line that starts dark and gets lighter.  I've kept this fairly infrequent.  In this example I've used a fairly wide “pencil" and kept a fair amount of erratic placement.  I think this looks good, but you might prefer an artist with a better “hand" who is closer to that flat machine fill ideal:
Or perhaps a narrow pencil:
I prefer the first version, but all the styles seem pretty realistic to my eye.  Here's a look at the entire map with these changes in place (click through for a hopefully full-sized image):
One area that could still be improved is the line direction.  Especially when seen on a full map, the very consistent line direction doesn't look realistic.  I've done some simple experiments to try to address this and haven't been entirely happy with the results, but I'll continue to think about it.  Maybe that will be in (Part 3) in a year or so!

5 comments:

  1. Great work!
    In a project I was working on recently, I faced the issue of having to export an SVG, and while your concern about not mixing CSS and SVG is absolutely justified, I don't think it's as bad as you present it:
    - The "style loss" only occurs when the style is specified in an external CSS file, any inline style (e.g. <circle style="color: red") should work fine when exporting the SVG. Since you're using d3, any CSS property applied using selection.style(...) falls into this category. This should also be true if you set it as an attribute (e.g. <circle color="red") using selection.attribute(...).
    - Should you rely on external CSS files, you can still parse the computed style before exporting to integrate it into the exported SVG. For reference, you can use or take a look at svg-export (main repo: https://github.com/sharonchoong/svg-exportJS, or my own fork with added filtering options: https://github.com/VRaveneau/svg-exportJS), but be aware that this will greatly increase the size of the generated SVG file due to a lot of unnecessary CSS rules being added to it.

    ReplyDelete
    Replies
    1. Thanks for that reply! Yes, I'm probably overstating the problem with CSS. As I recall, early on I tried to bring some maps into Inkscape and had this problem; that's probably the source of my concern. Thanks very much for the pointer to svg-exportJS. I've been using saveSvgAsPng for export, but this looks much better. I'll put it on my list to look at more closely!

      Delete
    2. Before using svg-exportJS, I had a quick look at saveSvgAsPng, but it felt more complicated to modify to suit my needs. Also, the main repo is read-only, so that's why I prefered svg-exportJS. Should you have any questions when looking at it, don't hesitate to contact me on GitHub!

      Delete