Building a real-time frosted glass effect on mobile web

tl;dr

You can do it on iOS (at least iPhone 4S+).

https://vimeo.com/79659941

Why?

Mobile is eating the world, and the frosted glass effect is popular in iOS 7. If mobile web can’t match most native interactions then it will be relegated to glue between native apps. And I really like the web.

What makes this interesting?

This capability isn’t built into the browser. You can’t just do background: frosted-glass. You need to create the effect yourself (frame-by-frame) in JavaScript.

I needed to do this when I made some touch demos for the React JavaScript library (which you should totally check out because it’s awesome!), but I’ll keep this blog post framework-agnostic.

Getting into the mindset

I’m going to assume you know CSS and what requestAnimationFrame() is. It’s also helpful to have a high-level idea of how GPUs work and to understand why hitting 60 frames per second is important.

In order to create new effects in the browser you need to shift your thinking. You’re no longer describing a semantic document to the browser and letting it render it. You’re imperatively telling it exactly what to render. You’re effectively taking the mindset of native development and bringing it to web technologies.

Methodology

Since we want to build performant mobile interactions, we need to figure out which set of primitives we can use that are fast enough to be acceptable and expressive enough to do what we want.

The best way to do this is to test on-device with an FPS counter like stats.js. I did a lot of exploration on the iPhone 5 and spot-checked the iPhone 4S, iPad 3, Nexus 4 and ZTE Open. I deliberately didn’t look at the most modern phones since I want to build apps today, not next year, so it should work on phones that have been out for a while.

Performant, expressive mobile web primitives

Concretely, you need to start thinking of certain block elements as GPU surfaces. You create the texture for the surface once using HTML and CSS and can manipulate the surface using the CSS3 transform or filter properties. These properties map directly to fast GPU primitives. On modern mobile devices they’re generally fast enough to hit 60 frames per second.

The caveat is that changing the texture (reflowing the HTML content by changing markup or certain style properties) is very expensive. Additionally you need to be careful about compositing and how it affects performance. If you use overflow: hidden to clip a container or opacity to overlay two containers the GPU needs to do extra compositing work. On modern mobile devices you can use these, but use them sparingly as they can eventually add up to dropped frames on some devices.

Additionally, we need to have absolute control over animations and direct touch manipulation rather than relying on the built-in CSS toolkit. Fortunately you can manipulate the transform property in JavaScript every requestAnimationFrame() at 60 frames per second and still have plenty of time to do some math. So much time, in fact, that this technique works just fine without the JIT, so you can get great performance in an iOS WebView using something like PhoneGap.

Note that I haven’t included CSS animations here. They just aren’t flexible enough to describe all interactions we need. I’ve also observed some latency in CSS animation startup time which precludes us from compiling CSS animations on-the-fly. If you can use a CSS animation they do tend to be quite performant though as they seem to run entirely off of the main thread.

The basic effect

We want to have a vertically scrolling content area with arbitrary HTML content. It’ll have a semi-transparent overlay that blurs the background with a white tint and has some content on it.

Think about the primitives we have. There’s only one way I’ve thought of to do this:

  • Two “viewport” divs stacked vertically (but not overlayed)
  • Copy the same content into both divs and keep them in sync (this is where having a framework is helpful)
  • Add a filter: blur(5px) CSS transform to the content in the top div
  • Overlay (with position: absolute) a div with a semi-transparent white background and some content over the blurred top viewport
  • Use margin-top and overflow: hidden to make the content in the two viewports look like a single piece of content

Here’s the HTML:

<div class="container">
<div class="overlayViewport">
<div class="content">
<h1>Hello world</h1>
<h1>Goodbye world</h1>
</div>
<div class="overlayContent">
Overlay content
</div>
</div>
<div class="contentViewport">
<div class="content">
<h1>Hello world</h1>
<h1>Goodbye world</h1>
</div>
</div>
</div>

And the CSS:

.container {
border: 1px solid gray;
height: 240px;
position: relative;
width: 320px;
}
.overlayViewport {
border-bottom: 1px solid rgba(50, 50, 50, 0.1);
height: 40px;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.contentViewport {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 39px; /* compensate for 1px border */
}
.overlayViewport,
.contentViewport {
overflow: hidden;
}
.contentViewport .content {
margin-top: -40px;
}
.overlayViewport .content {
-webkit-filter: blur(5px);
}
.overlayContent {
background: rgba(255, 255, 255, 0.5);
bottom: 0;
left: 0;
line-height: 40px;
position: absolute;
right: 0;
top: 0;
}

See this in JSFiddle.

Synthetic scrolling

So now we have a static representation of the effect. How do we integrate scrolling? We can’t use the built-in browser scroll because it’s not a single scrollable area as the content is duplicated and it only looks like a single scrollable area.

Instead we must use synthetic scrolling to achieve this effect. This means:

  • Listen to touch events on the bottom content area.
  • Based on the touch events and inertia formulae, compute the desired scroll position every requestAnimationFrame() tick (just like the native UI toolkit does).
  • Update the transform: translate3d() of the content in each div every requestAnimationFrame() based on this information.

This may sound like a lot of work, but the good news is that JavaScript is definitely fast enough to do this on modern mobile devices at 60 frames per second. And Zynga Scroller has already done all of this math for you.

When you combine this with the basic frosted glass effect above you get something that looks, feels and performs pretty close to native.

The final demo is here, the code is here, and a demo video is here. They’re part of the React Touch demo suite.

What browser and runtime vendors need to fix

If browsers drop a single frame (that is, if it takes longer than 16ms to execute the next requestAnimationFrame() or respond to a user touch) then the synthetic scrolling will feel choppy or laggy.

Android needs to fix touch latency on mobile web. It’s really bad and I consider Android a non-starter for real-time interactions on mobile web until this is fixed.

Image decoding on the main thread. I believe this is fixed in Blink and improving in WebKit, but it’s still an issue. If you load images (JPEGs in particular) with a simple <img> tag or background-image CSS property the browser will start dropping frames. This means that dynamically loading images with synthetic scrolling will be janky.

I was going to put garbage collector performance in here, but despite popular rumors I haven’t seen it as a major problem with these sorts of interactions on modern devices.

Show your support

Clapping shows how much you appreciated Pete Hunt’s story.