Distortion Grid using CSS 3D Transforms #

I was recently wondering if it'd be possible to re-create the hottest demo of 2000 (specifically, the Mac OS X genie effect) inside a browser. More generally, it would be neat to have a grid-based distortion system. It would certainly be possible by drawing things inside a <canvas> and then applying distortions pixel-by-pixel (e.g. in the way that The Wilderness Downtown corrected for distortion). However, I was hoping to use CSS 3D Transforms so that actual application of distortions would be hardware-accelerated in browsers that supported it (<canvas> hardware acceleration is coming soon, but isn't quite here yet).

I then came across Wonder WebKit, which reminded me that it's possible to directly specify a matrix to use via (WebKit)CSSMatrix, and also provided a port of OpenCV's getPerspectiveTransform to JavaScript.

Mandrill distored

A couple of days of hacking later, I have a demo of distortion grids in action. It requires Safari 5.0 or Chrome dev channel (or other channels with accelerated compositing enabled). There's also a short screencast showing all the features in action.

The grid control points are drawn as plain DOM nodes, and made made draggable by handling mouse events (there's also basic touch event support, but multi-touch is not supported). The grid is rendered via a <canvas> overlay. The source data is divided up into tiles, each tile being defined by the four control points at its corners. When a point is moved, the perspective matrix that transforms from the source coordinates to the distorted ones is recomputed. When holding down option/alt, nearby points are also moved (less and less, with a 1.3^(Manhattan distance) decay factor). When holding down shift, other points are moved to maintain the overall aspect ratio.

A few different datasources are supported. The most obvious is an image, which is subdivided into tiles by drawing sections of it into smaller canvases. Iframes are also supported, albeit not in a very elegant fashion. The source iframe has to be cloned (and therefore loaded) once per tile. Something like the moz-element extension would allow the iframe to be drawn into each tile without actually having to clone it. Most interestingly, movies (via the <video> tag) can also be used as a source. They are treated quite similarly to images (each frame is drawn into a canvas, and then pieces of that are drawn into tiles). Maintaining 30 frames per second doesn't seem to be a problem, since once the matrices are set up, most of the video playback and transformation can be hardware-accelerated.

Unfortunately I couldn't quite replicate the full animated genie effect. Though it is possible to snapshot transformed tiles and use CSS Transitions to animate between then, the interpolation between matrices doesn't behave quite as expected, and seams appear. It therefore seems like animation would have to be done by hand, with control point positions being interpolated and then matrices being recomputed every frame. Especially with a finer grid, maintaining 30 fps was deemed more trouble than it was worth (a.k.a. I got lazy).

Chrome Performance Puzzler #

I was recently involved* in investigating a Chrome performance issue that I thought was worth sharing.

This page has a simple CSS 2D animation, with a ball moving back and forth (taking a second to go across the screen). At the left and right endpoints, the location fragment is updated to #left and #right. The intent is to simulate a multi-section page with transitions between the section and each section having a bookmarkable URL.

The puzzling part is that the animation skips a few frames in the middle (right as the ball is crossing the thin line). This may not be noticeable depending on your setup, so here's a video of it. This only happens in Chrome, not in Safari or WebKit nightlies.

Here's some hints as to what's going on, each revealing more and more.

  1. The jerkiness always happens 500ms into the animation (which is the halfway point in the one second version, but one quarter of the way into the 2 second version.
  2. Even though the animating area stays the same, the larger the window size, the bigger the hiccup.
  3. The inspector's timeline view for the page shows regular, small, evenly-spaced repaints, but then suddenly 500ms into the animation, there's a full screen repaint, followed by a large gap before updates resume.
  4. Taking out the location fragment update fixes the jerkiness.
  5. Chrome has a few things that happen with a 500ms delay.
  6. Watching the counter on about:histograms/Renderer4.Thumbnail is helpful.

As it turns out, what's happening is that 500ms after a load finishes, Chrome captures the current page, so that it can be shown on the New Tab Page. This includes getting a thumbnail of the page (which involves repainting all of it and then scaling it down using a high quality filter). Updating the location with the fragment triggers this logic, and the larger the window, the more time is spent painting the page and then scaling it down.

In addition to fixing this on the Chrome side, the best way to avoid this is to update the location at the end of a transition, instead of at the beginning.

* Credit for figuring out what was going on goes to James Robinson.