I'm working on a new responsive site and decided to finally delve into the responsive image quagmire. I've been following the goings-on for awhile, but not in any great detail. After doing some research, I wanted to focus on two approaches: first, using inline SVGs whenever possible, so the images will scale and look great at any size; second, when SVGs aren't an option, conditionally loading different size images based on media queries.
With <video>, the browser will load the first <source> with a compatible type. A type attribute on <picture> would allow me to load an SVG if it was supported, and fall back on JPEGs with media queries if it wasn't, as in Figure 2. This has been discussed at the Responsive Images Community Group in a blog post by Brett Jankord.
With this strategy in mind, it was relatively simple to modify picturefill to accomodate a type attribute. I included a function to detect whether support for SVG was available, with the actual detection based on Modernizr's inlinesvg test. (I also threw in a test for WebP, again based on Modernizr.) Now when looping through the source elements, the script will match on media with no type, image/svg+xml if supported, and image/webp if supported. (Additional types for JPEG, PNG, and GIF could easily be added, but I chose to just leave out the type attribute for graphics with [near-] universal support.)
In the example in Figure 3, we can expect the following results:
If SVG is supported: the SVG will be loaded and all other sources will be ignored.
If SVG is not supported but WebP is: the SVG will be ignored, the WebP images will be loaded based on their media queries, and the JPEGs will be ignored.
If neither SVG or WebP are supported: the JPEGs will be loaded based on their media queries.
Hey guys. alphaPun.ch has been officially entered into the 10K Apart contest and is now live on their site. To vote for it, please go to the alphPun.ch entry page and tweet, like and comment! I need your support! Thank you!
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.
Although I wasn’t able to attend, I was very excited to follow the recent An Event Apart conference in Minneapolis via Twitter and notes posted by attendees. One thing that caught my eye was Richard Rutter’s talk, ‘Detail in Web Typography’, and a couple of tweets (1, 2) that mentioned CSS-based hyphenation. It turns out that the latest versions of some browsers support hyphenation with the following CSS:
Update : Firefox 6 got released as I was writing this yesterday, so there are now two browsers available supporting CSShyphens, Firefox 6 and Safari 5.1.
Testing for support
For many, the broken find-on-page might not be a big enough reason to avoid using Hyphenator.js, but others may not want to lose that functionality. Luckily, not all browsers break in this way, and we can test for proper full support before applying Hyphenator.js.
The test_wordbreak() function above takes a string as an argument and uses that string as a delimiter between two characters. With this function we can test ­ (or Á) and zero-width space delimiters to see if browsers acknowledge them and properly wrap text to a new line. We test for this by measuring the height of the container without the delimiter to get the height of a single line of text, then by measuring the height of the container with the delimiter. If the second measurement is larger, we can be reasonably sure that the text has been wrapped to a second line. It’s a little hacky, but it works.
This function works well in most browsers. However, some browsers that I’ve tested (specifically on BlackBerry devices, including the PlayBook) will recognize the soft hyphen and wrap the text properly, but won’t display the hyphen itself. For this reason, we need to modify the function above to also test for the width of the container.
As before, if the width of the container with the soft hyphen is larger than the width without, we can be reasonably sure there is an extra visible hyphen being displayed.
These tests tell us whether the browser recognizes and uses the soft hyphen properly, but not whether they break the find-on-page functionality. To test that, we’ll need another function that injects some text with a delimiter, and then searches for that text without the delimiter. If the text is found, we know that find-on-page is not broken; if not, it is broken.
By combining the functions in Figures 3 and 4, we can get a pretty good idea if it’s safe to use Hyphenator.js in a browser.
Problems and Browser Support
Naturally, life for a web developer is never that easy.
First, there's the issue of Chrome's support for the hyphensCSS. Unfortunately, Chrome claims that it supports this hyphenation, but in actual fact no hyphenation occurs. This is a problem if we want to test for CSS support before applying Hyphenator.js.
The solution to this is even more hacky than the functions above, but it should work.
Basically, this throws a huge wad of text into an element and sees if the element changes size when hyphenation is applied. Like I said, hacky.
This kind of false positive means that Hyphenator.js will be applied even though using it will break find-on-page. Options for dealing with this are unappealing:
Accept that find-on-page is broken on these devices.
Do browser sniffing in the test to make sure Android browsers don’t have Hyphenator.js
Give up on the whole thing entirely.
Wrapping it all in Modernizr
Modernizr is amazing and should be part of every web dev’s toolkit. Not only does it have a great built-in battery of tests for feature support, it also allows us to add our own. We can use Modernizr’s addTest() API to get very robust support for hyphenation on the web, without breaking anything in older browsers.
These tests will check for both CSS hyphenation support and Hyphenator.js soft hyphen/zero-width space support. The results of these tests will allow us to dynamically apply different styles and JS libraries based on what the user’s browser supports.
If CSS hyphenation is supported, it will be applied; browsers that don’t recognize the CSS hyphenation rules will simply ignore them.
If CSS hyphenation is not supported, but soft hyphens and the zero width space are, we’ll load and apply Hyphenator.js.
If neither are supported, the page remains unhyphenated but functional.
If you’d like to test this out for yourself in your own browser, feel free to check out this demo page and let me know your results, either in a comment, by email or via Twitter.
This is pretty basic right now, and is more of a proof-of-concept. I definitely welcome feedback and improvements. I’ve forked Modernizr on GitHub and have added this as a feature-detect, so feel free to fork it yourself and make it better! (Especially if you have a fix for the Android problem!) [Note: my feature test has now been pulled into the main Modernizr repo, so you can also mess around with it there].
Updated to mention Firefox 6 release
Updated to fix a syntax error in Figure 6
Updated to add window.scroll(0,0) in Figure 6 and update GitHub note
Updated to fix soft hyphen and ZWS characters that weren't appearing correctly in the code