Skip to content

WebGL Morph Targets and Ginger: Modernizing for Today's Web

WebGL Morph Targets and Ginger: Modernizing for Today's Web

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

WebGL Morph Targets and Ginger: Modernizing for Today's Web

Ginger is a morph target demo that was initially written in 2011 for chrome experiments, and then rewritten to work as progressive web app in early 2016. In essence, Ginger is a customizable head that uses a slider to tweak various facial features, much like a character customization system one would find in a video game. It also supports taking screenshots, and generating share links that can be used to share your creation with others.


The rewrite in 2016 focused on improving performance, and supporting modern web features. Notable improvements were made in the following areas:

  1. Utilizing HTTP/2 multiplexing to load model data concurrently, and efficiently.
  2. Utilize manifest.json for "Add to Homescreen" capability.
  3. Offering a full offline experience with service workers.
  4. Performance improvements from the previous version.

We inherited the project with our recent acquisition of Stickman Ventures, and thought it appropriate to showcase. Since it was rewritten in 2016, Ginger hasn't been touched, and has fallen into neglect, though it still worked fine in modern browsers. It was using an old version of Polymer 2 which has long since fallen out of use.

The Refactor

The first order of business was replacing Polymer 2 usage with something more modern. Polymer 2 was bleeding edge at the time, and used many proposed browser features that have since been adopted or dropped in favor for other standards (modules vs HTML imports).

Replacing Polymer with lit-html

To reduce friction, we opted to refactor Ginger's web components to use lit-html from the Polymer Project. Lit isn't a framework necessarily, it just makes it easier to write standard web components that are efficient, and work across all major browsers.

Transitioning from Polymer to lit was not a very complicated process, as they both use the same element lifecycle. The most notable difference was the transition from HTML imports to modules. With Polymer 2, elements are defined inside of HTML files, and are imported directly using HTML imports; however lit-html allows defining elements using a modern class-based approach.

class GingerApp extends LitElement {
  static get properties() {

  static get styles() {
    ...css here

  render() {
    ...html here


With lit-html, it's very easy to define custom elements by extending off of the LitElement class, and registering it using either customElements.define or the decorator. We then use rollup.js and babel to create a bundle that works well across browsers.

Using Workbox to Support Offline Usage

Ginger is able to work completely offline thanks to its use of service workers. The previous iteration of Ginger used a service worker that came from platinum elements. Platinum elements made it possible to add offline capabilities to an application with very little effort. However, they've been deprecated in favor of workbox and sw-toolbox. We chose to go with workbox as it supports all of the features that we need, and I was already familiar with it.

The service worker uses caching strategies to cache images, fonts, code and model data. Data that won't change often or at all uses the "cache first" strategy, while code uses the "stale while revalidate" strategy.

The cache first strategy will grab the requested file from the cache if it exists, and do nothing else. The stale while revalidate strategy will return the cached data similar to the cache first strategy; however, it will also download a newer version from the server in the background.

Here's some basic examples taken directly from Ginger.

// Here we use 'StaleWhileRevalidate' for caching scripts and HTML documents. We
// check the destination to find out what kind of resource is being processed.
// Destinations are similar to mime types but aren't quite the same.
  ({ request }) =>
    request.destination === 'script' || request.destination == 'document',
  new StaleWhileRevalidate({
    cacheName: 'script-cache',

// The image cache definition looks very similar. You'll see the cache class
// being used has changed to 'CacheFirst' and that 'ExpirationPlugin' is used to
// control how long we'll keep the images.
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 20,
        maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week

You can find out more about other caching strategies that workbox offers in their docs.


You can find Ginger's source code on Github, and it's available under an Apache 2 license.

The Ginger model was created, and is copyright of David Steele. This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.