What do Beer Cans, Image Masks, and Angular JS have in common?

Written by in Technology on

Recently our team worked on a really exciting project for LEGO® to create an in-browser poster builder. Functionally, it is a typical light image editor that allows you to compose images from a library of backgrounds and characters by placing, moving, rotating and scaling the characters on top of the background. The client’s requirements were that we utilized HTML5, and recommended an Angular and Less stack to match their internal practices moving forward.

While we could have utilized a webgl library to accomplish the project, we felt that modern browsers were ready to do it in straight HTML with less code overhead. With touch libraries like hammer.js and a two-way bound framework like Angular, we were set up for success. Indeed, the majority of the app was straightforward with the only challenges being exporting an image and edge cases around an image-size saving technique. It is the latter that we found interesting enough to share in this article.

Beer Can Compression

Having decided on a pure HTML/CSS implementation, we moved on to thinking about how to reduce the size of the image assets that would be delivered to the browser. In particular, since we can scale a given character to more than fill the background (for close up shots of ships and faces for example), we wanted to use larger assets that could be scaled up and down without too much loss in quality.

A while ago a technique for reducing filesize of images with transparency made its rounds through the internet via a blog post by Pete Hrynkow. I like to call it the Beer Can Compression technique since it was applied to a site for Sapporo beer to an image of the can. I say compression even though it isn’t technically compression for lack of a better term to describe it. The method detailed the use of an SVG as a wrapper for two images; one a JPEG, with all the free compression that entails, and a PNG transparency mask to be applied to that JPEG. This results in approximately 80% reduction in image size versus the pure png version of the image for images with a lot of detail. This matched our use case perfectly.

Peter had already done the grunt work for refining the technique’s cross-browser compatibility. Our IE9+ requirement for the project perfectly aligned with the spectrum of support he reported. After extensive testing to confirm his results, we decided to move forward with the technique. A few requirements for cross-browser compatibility are immediately known from this:

  • The SVG image needs to be inline.
  • The JPG and PNG images need to be data-urls instead of external assets.

Applying the Technique

The project requires that we be able to move, rotate, and scale a character on a given background in response to user interaction. To implement this, we chose the transform attribute with its translate(), rotate() and scale() values as the most performant and natural method. Immediately it was known that IE would not allow us to apply CSS transforms to an inline SVG, so we applied the transform to the containing div instead. Everything seemed to work perfectly until we noticed that in Safari (desktop and mobile), the transparency mask would not rotate with the image!

In Safari and iOS, SVG masks do not rotate with the parent SVG.

It turns out Googling “Safari transform rotate svg mask doesn’t rotate with image” is fruitless, landing you with a lot about SVG transforms (as opposed to CSS transforms on SVGs). It appears as if no-one ever blogged about trying to rotate an SVG image with a mask on it. Unfulfilled and exhausted, we nervously searched for an alternative implementation. Would we have to scrap the Beer Can Compression altogether? Had we wasted our time?

An Alternative in CSS Masks

The first thing we looked into were CSS masks. A relatively new feature to browsers, they looked full of promise. So we tested it and several things immediately jumped out at us:

  • Safari only supports the webkit prefixed version.
  • Firefox supports the feature only partially.
  • Internet Explorer (even 11) doesn’t support it at all.

Good news, then! We didn’t waste effort on the SVG solution, as it’s the only viable method for IE and Firefox. But what about the Safari bug?

Since Safari does support the prefixed version, we implemented a feature detect to swap the implementation for a simple img tag containing the JPEG with a CSS mask defined by the PNG. Time for another wall to insert itself in our path, but before we get to that, a quick detour.

Interlude: Canvas and Masks

In the design phase, we had decided to use an offscreen HTML5 canvas element to compose the poster image into an exportable form. The implementation is fairly simple, but one difficulty surfaced involving replication of the transparency masking with a canvas equivalent. Canvas does provide a method to apply a host of different compositing effects when layering images over one another, but none of them enabled the use of a black and white transparency mask. In order to make it work, we had to implement a hack that, pixel by pixel, inspected the mask for color value and applied that to the alpha channel of the underlying image. This was messy, inefficient and left us with a bad taste, but we set it aside while we dealt with other issues.

Luminance and Alpha

It turns out the cause of both the canvas masking and webkit css masking difficulties is that Peter’s original implementation of the Beer Can Compression technique specified the use of a luminance mask. That is, a mask that uses white to indicate bleed through and grey through black to determine underlying transparency. There is another kind of mask called an alpha mask, where the bleed through is specified by transparency and the underlying transparency by a color.

On the left is an example alpha mask where the underlying image shows through the transparent area. On the right a luminance mask, where the underlying image shows wherever the mask is white.

The current mask specification in CSS includes a mask-type property with the alpha and luminance type as values, but it is not implemented in the prefixed version in Safari. Indeed both implementations default to the alpha type mask. Additionally, canvas is perfectly capable of dealing with alpha masks via the globalCompositeOperation property. So the solution, if it isn’t obvious by now, is to switch to alpha masks. There is a minor change that needs to be made to the SVG markup, as it expects a luminance mask by default: simply add mask-type=”alpha” to the element and we’re good to go.

Implementing with Angular

If that were the end of the story this would already be a lengthy article, but we’re using Angular to implement all of the above learnings. That comes with its own caveats, and since it’s related to the above, we’re including them here. If Angular is not applicable to you, you can skip to the end.

Namespaced SVG in Templates

When we implemented the directive for a character, we included the inline SVG markup in the template for the directive. To properly create DOM nodes in JavaScript for an inline SVG element, you need to use the namespaced versions of JavaScript’s document.createElement et al. Failure to do so means browsers that are strict about the spec (eg. Firefox) will not display inline SVG namespaced as HTML.

What came as a surprise to us is that Angular does not use those namespaced versions when processing templates. Through an obscure github issue, we were able to determine there is an undocumented solution to this. You can define a type key on the directive definition object and set it to SVG and it will use a workaround to put the inline SVG into the appropriate namespace.

var directive = {
    restrict: 'E',
    template: require('../templates/figure.template.html'),
    controller: 'LpbFigureController',
    controllerAs: 'figureController',
    scope: {
        figure: '=',
        index: '@'
    },
    type: 'svg',
    replace: true,
    link: link
};

An undocumented feature, the type property can be used to properly process svgs in angular templates.

Ng-attr and SVG

When we started to populate the SVG with the data-urls for the image and mask dynamically, we discovered a minor snag. Normally, one uses ng-attr- to dynamically set an attribute based on a model. When you do this for the xlink:href attribute, used to specify the data-url image and mask assets, it fails. The reason is that the attribute is not applied until after the SVG is already in the DOM, and browsers for some reason will not respect the attribute if it was not already present when the DOM was first constructed.

The first part of the solution is to enter an empty xlink:href attribute in the template. Second, as the xlink:href attribute itself is namespaced, we need to use the link function in the directive to set the xlink:href attribute ourselves using the appropriate setAttributeNS method. This is no big deal, as we are already using the link function to replace the SVG with the webkit mask technique for browsers that don’t behave.


An empty xlink:href attribute is needed before it is set dynamically.

if (supportsWebkitMask) {
    element.css({
        '-webkit-mask-box-image': 'url(' + maskDataUrl + ')'
    });
    element[0].innerHTML = '';
} else {
    images[0].setAttributeNS(
        'http://www.w3.org/1999/xlink',
        'xlink:href',
        maskDataUrl
    );
    images[1].setAttributeNS(
        'http://www.w3.org/1999/xlink', 'xlink:href',
        imageDataUrl
    );
}

Setting the xlink:href attribute requires the namsepaced version of setAttribute.

<base> and SVG

One last final word of warning, if you intend to use HTML5 pushState urls with Angular alongside this technique, you won’t be able to due to the requirement of the presence of a tag, which breaks SVG masks. At root, this is because the SVG 1.1 spec does not define how the url(#mask-id) construction in the mask attribute should behave when a base element is present in the parent document. Thus, different browsers interpret this differently. Specifically, Firefox attempts to apply the base url to the url() construction, resulting in a link to a url rather than an element and breaking the transparency mask. Avoid use of the base element if possible.

The Result

Aside from being a client and a franchise that many of us are really excited about, this project was an interesting challenge. We have shared with you a slice of the most interesting things we learned on this project in the hopes that you will save time in one or more of the edge cases we ran into. Pushing the web forward like this excites us and we hope that, at the very least, it tickled your inner techie.

If this sort of challenge excites you too, take a look at our career opportunities or if you'd like to leverage modern technologies like Angular JS and HTML5 for your web applications give us a shout

Discuss on Twitter