This post is to introduce a new project I’ve been working on, alphapun.ch, which I made as an entry for the 10K Apart contest.
Graphics on the web are based on boxes. This is naturally limiting, but luckily we are able to circumvent this limitation by applying transparent backgrounds to our images. Although any image object we display will still be a square or a rectangle, these transparent backgrounds can give the illusion of an irregular shape.
Most of the time this is a very good system, but there are edge cases where it may not be flexible enough. Sometimes we don’t want to just be able to see through a transparent background, we want to be able to click through it as well.
The problem of clicking through transparent images is something that first started to bother me back in 2009. At the time I was creating what was basically a paper doll site, where users could drag and drop different articles of clothing onto a doll. The clothing and accessories were all PNGs with transparent backgrounds. The site used jQuery UI to allow users to click and drag individual images.
The problem occurred in a situation like Figure 1, where two images didn’t appear to overlap, but actually did because of the transparent backgrounds. Assume that in Figure 1 the hat and sunglasses are two separate images with transparent backgrounds, and that the stacking order on the page has the hat in front of the sunglasses.
A user might want to click and drag the glasses, and would rightfully think that they could. However a click on what appears to be the glasses image would actually register a click on the transparent part of the hat image. This means a user would unexpectedly start dragging the wrong article of clothing. I thought this unexpected behaviour was unacceptable and looked for a solution.
Initial Solution, or “The Hard Way”
Solving the problem involved three steps. First, I needed to be able to identify and define the opaque and transparent parts of the image, so that these could be treated differently. To do this, you can use Adobe Fireworks to generate a set of coordinates defining the opaque parts of the image. In Fireworks CS5:
import an image with a transparent background
select the opaque bits using the magic wand or other tools
convert the selection to a path by choosing Select > Convert Marquee to Path
convert the path to a hotspot by right-clicking it and choosing Insert Hotspot
choose File > Export… and export the HTML
the coordinates defining the shape/path/hotspot will be inside the generated HTML document
This is tedious, and becomes complicated when you have multiple shapes in a single image, or when you have shapes with holes in them. But, it works.
Armed with a set of coordinates to define the opaque shape, the second step was to figure out what to do with them. Drawing the shape on a canvas was not really an option at the time, and even if I could do that, there was no obvious way it could solve my problem. I needed to draw the shape as a real DOM object that could register click events, and that wasn’t another rectangular box overlapping my content.
I ended up stumbling onto a solution in Walter Zorn’s wz_jsgraphics library (which I’ve made available on GitHub). This library was created as an early polyfill for canvas, and included a function called fillPolygon that, when given a set of coordinates, would draw the shape defined by those coordinates using a large number of small div elements. The result has the desired shape, is a set of real DOM objects, and doesn’t have any problematic transparent backgrounds to confuse users.
The third and final step was to tie this new wz_jsgraphics mask to the original images somehow. The solution to this was to make the divs in the mask transparent, overlay them directly on top of their source image, and to use jQuery UI’s super-useful handle option to define the mask as a handle for the image. Some messing about with z-index and position to insure proper stacking orders (with masks always on top of images), and everything worked beautifully.
With this solution, when a user appeared to be clicking on the sunglasses, they would actually be clicking not on the glasses image, and not on the hat image, but on a set of transparent divs that act as a mask and handle for the sunglasses. To the user, this would be seamless, and they would be able to drag the glasses onto the doll without being blocked by the hat.
alphaPun.ch, or “The Easy Way”
In updating my 2009 solution and making alphaPun.ch, I wanted to update and simplify each part of the three step process outlined above.
First, I wanted to be able to automatically generate the set of coordinates defining the opaque part of the shape. AlphaPun.ch traces shapes using an alphaPunchPencil (APP) object.
Once an image is uploaded, the tool draws it onto a canvas. The opaque parts of the image are traced using a Moore-Neighbor tracing algorithm, based in part on Abeer Ghuneim’s tutorial. This eliminates the need to launch Fireworks and manually generate coordinates, and it works when there are multiple objects in an image and when there are objects with holes.
Next, I wanted to replace wz_jsgraphics for drawing the masks. It worked very well, but it had far too many features for my needs, and I didn’t want to rely on any external libraries other than jQuery. An alphaPunchFist (APF) object is used to create and place the masks.
I wrote my own scanline-based replacement for the fillPolygon() function (f.f() in Figure 6), based in part on the algorithms in the Polygon Fill Teaching Tool (which, unfortunately, doesn’t list an author). This new function draws spans rather than divs, which means they can be properly embedded in inline elements, such as a.
The HTML simply places your image and the links in a container element with the class alphapunch. The links have the class aptarget, and the images and links all have IDs. The JS will find instances of the alphapunch class, create masks for the images by matching them with a set of coordinates (based on ID), and create masks for links with the aptarget class.
The CSS sets up the stacking order of the image, the links, and the masks. The order from back to front is: links, image, link masks, image mask. Note that the masks must have a background for them to register click events in IE.
The JS includes the coordinates for your image, the APF object, and some code telling the APF to create the masks.
As mentioned above, this code will allow you to click through to links behind a single image, but alphaPun.ch also allows for some flexibility. For example, you could add more images within the .alphapunch container, you could adjust the CSS to change around z-indexes, or you could set .aptargets that aren’t links.
To create the paper doll functionality that started all of this, we just need to add extra images into our .alphapunchdiv, add coordinates for the extra images into our var coords object, and call the jQuery UIdraggable() function with appropriate handles set. Everything else is standard output from alphaPunch.
As it stands, alphaPun.ch has a few problems:
Alphapun.ch generates a metric craptonne of non-semantic spans when drawing the masks. These are not just bad for the page semantics, but all the extra elements and DOM changes can also slow down the page, especially on older browsers.
When tracing shapes, alphapun.ch considers anything other than opacity 1.0 to be transparent. I really wanted to allow the user to select the opacity threshold, but this caused major slowdowns in processing and often led alphaPun.ch to crash. It’s on the TODO list.
When drawing on a canvas, browsers antialias lines. This can cause problems when pixel-perfect accuracy is needed, as it is here. In order to overcome antialiasing artifacts, I needed to take some steps that lower the accuracy of the masks that get created. As a result, masks don’t perfectly match the original image, and very thin lines may not get masked at all. (Antialiasing can actually be turned off in Firefox, but I opted for relative consistancy between browsers.)
Despite the parenthetical remark above, not all browsers trace shapes and create masks in exactly the same way. Chrome and Firefox seem to have the same behaviour, but IE10 will come up with different coordinates. I haven’t figured out exactly why this is, but I think IE10 is reporting pixel opacity values differently. It’s something to investigate further.
Alphapun.ch uses canvas, Drag & Drop and FileReader, so any browsers that don’t support these (e.g. Safari 5.x and lower) are left out of the fun (at least for now).
Processing complex images (e.g. that are very large, have lots of separate shapes, or with very complex polygons) can be processor intensive and slow. IE10 seems especially slow, but that might just be my VM.
I had a heck of a time getting everything in under 10K for the contest, so some of the first things I’d like to do for the non-contest version are organize the code a bit differently, make it more readable, and do some cleanup.
Other than that, and the problems listed above, I’m open. Go play with it! Let me know if there’s anything you want to see, or anything you want a more detailed explanation for. Comment, or email, or tweet. It’s on GitHub, so take a look and/or fork it.