Flash 3D User interface Web Animation Graphics

This was the front page of the CubeLogix website. The logo cracks, breaks open, and three cubes come out through the hole to become the navigation. There's one light in the scene, and everything casts a shadow: the pieces as they fall, the cubes on the background and on each other, and the windows that open when you click a cube.

Flash 8 has no 3D of its own, but it does have a drawing API, bitmaps you can draw into, and masks, and that turns out to be enough.

What happens

Everything is driven by one onEnterFrame handler, which looks at how long it's been since the page started and does whatever that moment needs:

TimeWhat happens
0 sThe logo band, still in one piece
1.05 sThe camera jolts forward, and the logo cracks
1.5 sThe view starts to turn towards where the eye in the corner is, and gets there at 9 s
2.05 sA bigger jolt, and the pieces break out, tumbling towards you and falling
2 s to 6.5 sThe three cubes come out of the hole, one after another, and spread out
6 sThe labels and the map fade in
What happens when

The jolts come from a function called bump. For a tenth of a second around the moment it's given, it moves the camera forward and wobbles it, as if something has hit the logo from behind:

function bump(time, bump, bump_w) {
  var half = bump_w / 2;
  var b = 0;
  if (time > bump - half && time < bump + half) {
    b = 1 - 0.5 * Math.sin(3.14159 * (time - bump + half) / half);
  }
  return b;
}
The jolt: 1, then 0.5, 1, 1.5 and back to 1 over a tenth of a second, and 0 the rest of the time

The camera moves forward by 8 times this for the first jolt, and 15 times for the second.

The logo cracked by the first jolt
The logo cracked by the first jolt
The pieces breaking out at the second jolt
The pieces breaking out at the second jolt

The 3D engine

Every object has a Transform: a position, and a rotation kept as a quaternion. A quaternion is four numbers rather than the nine of a rotation matrix, and it's easy to combine with another one and to turn a little at a time. Setting one from an axis and an angle:

Transform.prototype.setRotation = function(x, y, z, r) {
  var s = Math.sin(r / 2);
  var n = s / Math.sqrt(x * x + y * y + z * z);
  this.q = [x * n, y * n, z * n, Math.cos(r / 2)];
  this.qi = false;
};
A rotation of r radians about the axis (x, y, z), as a quaternion

Turning a point uses the rotation matrix that the quaternion stands for, written out in full, and then adds the position:

Transform.prototype.transformPoint = function(s, d) {
  var x = s[0];
  var y = s[1];
  var z = s[2];
  var qx = this.q[0];
  var qy = this.q[1];
  var qz = this.q[2];
  var qw = this.q[3];
  d[0] = this.p[0] + x * (1 - 2 * (qy * qy + qz * qz)) + y * (2 * qx * qy - 2 * qw * qz) + z * (2 * qw * qy + 2 * qx * qz);
  d[1] = this.p[1] + x * (2 * qx * qy + 2 * qw * qz) + y * (1 - 2 * (qx * qx + qz * qz)) + z * (-2 * qw * qx + 2 * qy * qz);
  d[2] = this.p[2] + x * (-2 * qw * qy + 2 * qx * qz) + y * (2 * qw * qx + 2 * qy * qz) + z * (1 - 2 * (qx * qx + qy * qy));
};
Turning a point by the quaternion and moving it by the position

combine multiplies two quaternions and moves one position by the other, so combining an object's transform with the camera's gives the transform from the object straight to the camera.

One quirk: a new Transform starts with q = [0, 0, 1, 0], which is half a turn about z rather than no turn at all. Everything in the scene starts out that way, so it all lines up, but it's why some positions below are negated.

The camera, ori, is another Transform, 350 units in front of the background. The screen is a movie clip with a perspective setting p, which is the distance from the eye to the screen in pixels, so projecting a point is a divide by its depth:

MovieClip.prototype.screenPoint = function(s, d) {
  var p = this.p;
  d[0] = p * s[0] / s[2];
  d[1] = -p * s[1] / s[2];
};
Perspective: x and y divided by depth

p is 350 too, so anything at the depth of the background is drawn at its actual size. Each clip that's drawn into has _yscale set to -100, so y goes up, and is cleared and drawn again every frame with the drawing API. They're stacked like this:

DepthClipWhat's drawn in it
100sbufEvery shadow, as a mask (see Shadows)
150hbufThe background: the logo, the hole and the two lines
200shadA sheet of black at 30%, which only shows through sbuf
501 to 503cbuf1 to cbuf3The cubes, one in each
1000bufThe falling pieces
The movie clips the scene is drawn into, from the back

A Scene is a list of objects. Drawing one works out how far each object's centre is from the camera, sorts them furthest first with sortOn, and draws them in that order, so nearer things are painted over further ones. These are the objects:

ObjectWhat it is
PlaneThe background: a grid of triangles textured with the logo
LineThe top and bottom edges of the logo band
PolygonA falling piece, textured with the logo
HolePolygonThe same piece in black, to make the hole
NavCubeOne of the three cubes
Scene, CubeSceneLists of objects, drawn back to front
The kinds of object in the scene

The background

The logo is a movie clip, and at the start it's drawn into a 500x180 BitmapData. The background is a Plane: a grid of 7x7 points across those 500x180 units, with 72 triangles between them, and the texture co-ordinates of each point.

Flash 8 can't map a texture onto a triangle, but beginBitmapFill takes a matrix that says how the bitmap lies on the screen, and a matrix can turn any triangle into any other. drawTri makes one matrix that takes a reference triangle to the triangle in the texture, and one that takes it to the triangle on the screen. Undoing the first and then doing the second takes the texture straight to the screen:

function drawTri(g, w_vert, texture) {
  var a = w_vert[0];
  var b = w_vert[1];
  var c = w_vert[2];
  if (a.sx == null || b.sx == null || c.sx == null) {
    return;
  }
  var w = texture.width;
  var h = texture.height;
  var u = a.u;
  var v = a.v;
  tMat.tx = u;
  tMat.ty = v;
  tMat.a = (b.u - u) / w;
  tMat.b = (b.v - v) / w;
  tMat.c = (c.u - u) / h;
  tMat.d = (c.v - v) / h;
  var x0 = a.sx;
  var y0 = a.sy;
  var x1 = b.sx;
  var y1 = b.sy;
  var x2 = c.sx;
  var y2 = c.sy;
  sMat.a = (x1 - x0) / w;
  sMat.b = (y1 - y0) / w;
  sMat.c = (x2 - x0) / h;
  sMat.d = (y2 - y0) / h;
  sMat.tx = x0;
  sMat.ty = y0;
  tMat.invert();
  tMat.concat(sMat);
  g.beginBitmapFill(texture, tMat, false, false);
  g.moveTo(x0, y0);
  g.lineTo(x1, y1);
  g.lineTo(x2, y2);
  g.lineTo(x0, y0);
  g.endFill();
}
Drawing a textured triangle with one bitmap fill and two matrices

A matrix can stretch and skew a triangle but can't bend it, so this is affine mapping, with no correction for perspective across each triangle. With 72 small triangles, it doesn't show.

The lines along the top and bottom of the band are Line objects 8,000 units long. Each is drawn as 20 lengths, each as thick as 1000 divided by its depth, kept between 1 and 10 pixels, so the lines are thicker where they're closer and thin out into the distance when the view is turned.

The breaking pattern

The pattern is made before anything happens, and it's different every time. It starts with some lines out from the middle, 7, 8 or 9 of them. Each line goes in its own slice of the circle, at a random angle within it, and each pair of lines next to each other makes a triangle:

  for (i = 0; i < n; i++) {
    angles[i] = i * 3.14159 * 2 / n + (0.1 + 0.8 * Math.random() * 3.14159 * 2 / n);
  }

  ...

  for (i = 0; i < n; i++) {
    var a1 = angles[i];
    var a2 = angles[(i + 1) % n];
    var s1 = Math.sin(a1);
    var c1 = Math.cos(a1);
    var s2 = Math.sin(a2);
    var c2 = Math.cos(a2);
    var r1 = Math.random() * 0.5 + 0.75;
    var r2 = Math.random() * 0.5 + 0.75;
    var f = addFace();
    addPointToFace(f, 0, 0);
    x1 = s1 * shatter_w * r1;
    x2 = s2 * shatter_w * r2;
    y1 = -c1 * shatter_h * r1;
    y2 = -c2 * shatter_h * r2;
    if (y1 < -limit) {
      x1 = x1 * -limit / y1;
      y1 = -limit;
    }

    ...

    addPointToFace(f, x1, y1);
    addPointToFace(f, x2, y2);
  }
The radial lines and the triangles between them (trimmed)

Each corner is between 75% and 125% of 120 units out along its line. A corner past the top or bottom of the band is pulled back along its line to 89 units, so it stays inside. Neighbouring triangles choose their own lengths, so they don't share their outer corners, and that's a lot of what makes the edge of the hole so jagged.

Then the pattern is cut, between 8 and 16 more times. Each cut goes across the longest edge in the whole pattern, which stops the pieces getting long and thin:

One breaking pattern: nine radial triangles, then nine cuts
One breaking pattern: nine radial triangles, then nine cuts

Cutting a face

A cut starts at a random point between 25% and 75% of the way along the longest edge, and goes into the face at a random angle to it:

Cutting a face across its longest edge
Cutting a face across its longest edge

Finding where the cut comes out is a ray against each of the other edges. For an edge going in direction d, (-d.y, d.x) is at right angles to it. The distance from the edge in that direction changes as you go along the ray, by n·(-d.y, d.x) for every unit, so dividing the distance by that gives how far along the ray the edge is. The nearest edge that's ahead of the ray is where the cut ends:

  for (i = 0; i < faces[f].length; i++) {
    if (i != e) {
      var d = getLineDirection(f, i);
      var a = faces[f][i];
      var nx = -d[1];
      var ny = d[0];
      var dist = (p[0] - a[0]) * nx + (p[1] - a[1]) * ny;
      var speed = n[0] * nx + n[1] * ny;
      var t = -dist / speed;
      if (t >= 0 && t < nearest) {
        best = i;
        nearest = t;
      }
    }
  }
How far along the ray from p, in direction n, each edge of the face is (trimmed)

Every piece starts as a triangle and is only ever cut in a straight line, so every piece is convex, and a ray from a point on its edge always comes out through exactly one other edge. Then the face is split into two lists of corners, by walking round it from the start of the cut to the end, and then on round from the end back to the start:

function breakApartRandomFace() {
  var longest = findLongestEdge();
  var f = longest[0];
  var e = longest[1];
  var p = getRandomPointOnLine(f, e);
  var d = getLineDirection(f, e);
  var n = rotateDirection(d, Math.random() * 3.141592653589793);
  var hit = findIntersectingEdge(f, e, p, n);
  var e2 = hit[0];
  var q = hit[1];
  var a = new Array();
  var b = new Array();
  var i;
  a[0] = p;
  i = (e + 1) % faces[f].length;
  while (true) {
    a[a.length] = faces[f][i];
    if (i == e2) {
      break;
    }
    i = (i + 1) % faces[f].length;
  }
  a[a.length] = q;
  b[0] = q;
  i = (e2 + 1) % faces[f].length;
  while (true) {
    b[b.length] = faces[f][i];
    if (i == e) {
      break;
    }
    i = (i + 1) % faces[f].length;
  }
  b[b.length] = p;
  faces[f] = a;
  faces[faces.length] = b;
}
One cut: a ray across the face from its longest edge, and the two faces either side of it

The hole

Only some of the pieces fall: the ones with at least one corner inside a 100x100 box in the middle. Each of those becomes two objects, a Polygon, which is the piece of the logo that falls, and a HolePolygon, which is the same shape in black.

On the first frame after the crack, the HolePolygons are drawn, and the drawing is copied into the background's texture:

  } else if (!cracked) {
    ori.setPosition(0, 0, cam_dist);
    hbuf.clear();
    t = new flash.geom.Matrix();
    t.scale(tex_scale, tex_scale);
    t.tx = tex_scale * 500 / 2;
    t.ty = tex_scale * 180 / 2;
    t.b = -t.b;
    t.d = -t.d;
    hscene.draw(hbuf, ori);
    hole_tex.draw(hbuf, t);

    ...
Drawing the hole into the background's texture (trimmed)

So from then on the hole is part of the background picture, and turns with it when the view does. The pieces still cover it exactly at first, and the cracks you see before they fall are the black edges of the hole showing between them.

Falling

Each falling piece is made from its face by moving its corners so its middle is at its own origin, and putting the piece where that middle was. Each corner keeps its position in the logo as its texture co-ordinates:

function createMeshFromFace(f, sbuf, type) {
  f = faces[f];
  var i;
  var cx = 0;
  var cy = 0;
  for (i = 0; i < f.length; i++) {
    cx += f[i][0];
    cy += f[i][1];
  }
  cx /= f.length;
  cy /= f.length;
  var points = new Array();

  ...

  for (i = 0; i < f.length; i++) {
    points[i] = [f[i][0] - cx, f[i][1] - cy, 0, tex_scale * (f[i][0] + 250), tex_scale * (f[i][1] + 90)];
  }
  cx *= -1;
  cy *= -1;
  var piece = new type(points, sbuf, logo_tex);
  piece.t.setPosition(cx, cy, 0);
  var near = 0.015 * Math.sqrt(cx * cx + cy * cy);
  near = 1 - near;
  if (near < 0) {
    near = 0;
  }
  piece.dr = [-cy, cx, 0, 4 * (near * (Math.random() * 1.8121 + 0.212)) + Math.random() * 1.6772];
  piece.dp = [cx * 0.13, cy * 0.13, -40 - near * 140];
  return piece;
}
A piece: its corners around its own middle, its spin and its speed (trimmed)

near is 1 in the middle and 0 from about 67 units out, so the pieces nearest the middle get the hardest push. A piece spins about an axis at right angles to the line out from the middle to it, so it tips over outwards, like a door opening away from the hole. It moves outwards at 0.13 times its distance from the middle each second, and towards you at 40 units a second plus up to 140 more.

Each frame it turns a little more about its axis and moves on, and then its speed changes: multiplying by 0.99 each frame across and towards you is drag, and taking off 29.8 units a second, every second, going up, is gravity:

Polygon.prototype.move = function(f, dt) {
  var spin = new Transform();
  spin.setRotation(this.dr[0], this.dr[1], this.dr[2], this.dr[3] * dt);
  this.t.combine(spin);
  this.t.p[0] += this.dp[0] * dt;
  this.t.p[1] += this.dp[1] * dt;
  this.t.p[2] += this.dp[2] * dt;
  this.dp[0] *= 0.99;
  this.dp[1] -= dt * 29.8;
  this.dp[2] *= 0.99;
};
A piece's move for one frame: spin, move, drag and gravity
The pieces falling, with their shadows on the background
The pieces falling, with their shadows on the background

Shadows

There's one light, at (400, 300, -500) in the scene. With the half turn that everything starts with, that puts it up and to the left of the viewer, and a little way behind them. It never moves in the scene, so when the view turns, the light turns with everything else.

Projecting onto a plane

Every shadow here is made the same way: a point's shadow is where the ray from the light through the point meets the surface the shadow falls on.

Projecting a point onto a plane from the light
Projecting a point onto a plane from the light

Every point on the ray is L + t (P − L) for some t. It's on the plane when its distance from Q in the direction of n is zero, which is when t = n·(Q − L) / n·(P − L):

function vector_projectToPlane(light, point, planePoint, planeNormal, result) {
  var ray;
  ray = vector_subtract(point, light);
  var t = vector_dot(planeNormal, vector_subtract(planePoint, light)) / vector_dot(planeNormal, ray);
  result[0] = light[0] + t * ray[0];
  result[1] = light[1] + t * ray[1];
  result[2] = light[2] + t * ray[2];

  ...
}
Where the ray from the light through a point meets a plane (trimmed)

The pieces' shadows

The background is the plane z = 0 in the scene, and for that plane the sum comes down to a single division. As each piece works out where its corners go on the screen, it also works out where their shadows go:

  for (i = 0; i < this.p.length; i++) {
    pt = this.p[i];
    t.transformPoint(pt, c);
    if (c[2] < 0) {
      return;
    }
    screen[i] = [0, 0];
    b.screenPoint(c, screen[i]);
    this.t.transformPoint(pt, c);
    f = -c[2] / (light[2] - c[2]);
    pt = [c[0] + f * (light[0] - c[0]), c[1] + f * (light[1] - c[1]), 0];
    tt.transformPoint(pt, c);
    shadow[i] = [0, 0];
    b.screenPoint(c, shadow[i]);
  }
Each corner of a piece on the screen, and its shadow on the background

this.t takes the corner into the scene without the camera, the shadow is worked out there on z = 0, and then tt, the camera, takes the shadow to the screen. The shadow's outline is then filled into sbuf.

One layer of shadow

Shadows that overlap shouldn't get darker, because the light is either blocked or it isn't. If each shadow were drawn in black at 30%, the places where two overlap would be 51%:

Overlapping shadows, drawn one on top of the other and drawn into one mask
Overlapping shadows, drawn one on top of the other and drawn into one mask

So no shadow is ever drawn as a shadow. They're all filled into sbuf, which is never seen, because it's the mask for shad, a big black rectangle at 30%:

createEmptyMovieClip("shad", 200);
shad.moveTo(-1000, -1000);
shad.beginFill(0, 100);
shad.lineTo(1000, -1000);
shad.lineTo(1000, 1000);
shad.lineTo(-1000, 1000);
shad.endFill();
shad.setMask(sbuf);
shad._alpha = 30;
The one sheet of shadow, showing only where sbuf has been filled in

A mask is a shape, not a colour, so however many shadows are filled into sbuf and however they overlap, the result is the same 30% black wherever any of them are. And shad is in front of the background but behind the cubes, so these shadows only ever fall on the background.

Cubes on cubes

A cube's shadow on another cube has to fall on that cube's faces, which are in front of shad. So each face of each cube has a shadow layer of its own: a movie clip filled with the face at half brightness, and another clip that's its mask.

  for (i = 0; i < 6; i++) {
    cb.createEmptyMovieClip("sm" + i, 2000 + i);
    cb.createEmptyMovieClip("sf" + i, 1000 + i);
    cb.m[i] = eval(cn + ".sm" + i);
    cb.f[i] = eval(cn + ".sf" + i);
    cb.f[i].setMask(cb.m[i]);
  }
A darker copy and a mask for each of a cube's six faces

To cast a cube's shadow onto a face, all 8 of its corners are projected onto the face's plane. Each of its 6 faces is then filled into the mask if it still faces the right way after projection. Together they cover exactly the cube's shadow, without having to work out its outline:

NavCube.prototype.drawShadow = function(b, c, l, p, n) {
  var s = [];
  for (j = 0; j < 8; j++) {
    s[j] = [0, 0, 0];
    vector_projectToPlane(l, this.trp[j], p, n, s[j]);
    this.b.screenPoint(s[j], s[j]);
  }
  for (j = 0; j < 6; j++) {
    this.drawFaceIfFacing(j, b, c, s);
  }
};
A cube's shadow on a plane: its corners projected, and its faces filled in

The darker copy has the face's shape, and the mask has the shadow's, so what shows is where they overlap: the part of the face that's in shadow.

Which cube can shade which face depends on where the light is, so the pairs are written out one by one. The face of the second cube that's towards the viewer can be in the first cube's shadow, and so on, for the front and one side of each cube:

  if (this.cube2.v[5]) {
    this.cube2.tt.transformNormal(this.cube2.n[5], n);
    this.cube1.drawShadow(this.cube2.b.m[5], this.cube2.col >> 1 & 8355711, light, this.cube2.trp[this.cube2.f[5][0]], n);
  }
The first cube's shadow on the front of the second, if the front can be seen

The windows' shadows

The windows are ordinary movie clips that lie flat on the screen. For their shadows, though, each one is treated as a rectangle in the scene, facing the screen at a depth of 160, which is about halfway between the viewer and the background. Its position on the screen is turned back into 3D at that depth by undoing the perspective:

MovieClip.prototype.unScreenPoint = function(s, d) {
  var p = this.p;
  d[0] = s[0] * s[2] / p;
  d[1] = -s[1] * s[2] / p;
  d[2] = s[2];
};

function getWindowPoints() {
  var x = win._x;
  var y = win._y;
  var w = win.getWidth();
  var h = win.getHeight();
  var tl = [0, 0, 0];
  var br = [0, 0, 0];
  buf.unScreenPoint([x, -y, window_z], tl);
  buf.unScreenPoint([x + w, -y - h, window_z], br);
  return [window_z, tl[0], tl[1], br[0], br[1]];
}
The window as a rectangle in the scene, 160 units from the viewer

Its shadow on the background is its four corners projected onto the background's plane. As the camera sees it, that plane goes through the middle of the scene, 350 units away, facing whichever way the view has turned it, and the light, buf.tl, is turned the same way:

function drawWindowShadow() {
  var w = getWindowPoints();
  var corner;
  var normal = [0, 0, -1];
  ori.transformNormal(normal, normal);
  var s = [[0, 0], [0, 0], [0, 0], [0, 0]];
  corner = [w[1], w[2], w[0]];
  vector_projectToPlane(buf.tl, corner, [0, 0, cam_dist], normal, s[0]);

  ...

  sbuf.moveTo(s[0][0], s[0][1]);
  sbuf.beginFill(6750156, 100);
  sbuf.lineStyle(0, 0, 0);
  sbuf.lineTo(s[1][0], s[1][1]);
  sbuf.lineTo(s[2][0], s[2][1]);
  sbuf.lineTo(s[3][0], s[3][1]);
  sbuf.endFill();
}
The window's shadow on the background, filled into the shadow mask (trimmed)

The light is fixed in the scene, but the window is fixed on the screen. So when you turn the view, the window stays where it is, and its shadow moves across the background:

The window's shadow, looking straight on and with the view turned
The window's shadow, looking straight on and with the view turned
A window, with its shadow on the background and on the cube behind it
A window, with its shadow on the background and on the cube behind it
The same window with the view turned, and its shadow moved
The same window with the view turned, and its shadow moved

A window's shadow on a cube is made face by face, like a cube's, with one extra step. A window is much bigger than a cube, and its corners projected onto a face that's almost edge-on to the light would land a long way off. So first the cube's 8 corners are projected the other way, onto the window's plane, which gives the part of the window that can shade the cube at all. Only that rectangle is projected onto each of the cube's faces that face the light, and filled into their masks.

The cubes

The three cubes are NavCubes, 60 units across, in pink (FFBBEE), blue (BBDDFF) and lilac (CCBBDD). The cubes never turn and the light never moves, so the brightness of each face only needs working out once, and it's kept as a number after the face's four corners:

NavCube.prototype.f = [[2, 3, 7, 6, 0.5], [6, 7, 5, 4, 0.5], [2, 6, 4, 0, 0.5], [1, 0, 4, 5, 0.9], [3, 2, 0, 1, 0.8], [3, 1, 5, 7, 1.1]];
Each face's corners, and its brightness

The face towards the viewer is 1.1, the two faces towards the light are 0.9 and 0.8, and the three faces away from it are 0.5. While a cube is still behind the background it's drawn darker, down to half brightness the further back it is, as if it's coming out of the dark:

      if (this.t.p[2] > -10) {
        var c = this.fc[i];
        var f = 90 - this.t.p[2] * 0.5;
        if (f < 50) {
          f = 50;
        }
        f *= 0.01;
        c = 65536 * Math.floor((c >> 16) * f) + 256 * Math.floor((c >> 8 & 255) * f) + Math.floor((c & 255) * f);
        b.beginFill(c);
      } else {
        b.beginFill(this.fc[i]);
      }
A face's colour, darkened while the cube is behind the background

The cubes are never clipped against the hole. They come out along the middle, starting far enough back to be small enough to fit inside it.

Each cube follows a path of keyframes, one a second, with the second and third cubes starting a quarter of a second after the one before. The pairs of numbers are across and depth, and each path ends with its last keyframe repeated:

  var path1 = [[0, 100], [0, 0], [0, -100], [-100, -100], [-100, -100], [-100, -100], [-100, -100]];
  var path2 = [[0, 200], [0, 100], [0, 0], [0, -100], [100, -100], [100, -100], [100, -100], [100, -100]];
  var path3 = [[0, 300], [0, 200], [0, 100], [0, 0], [0, -100], [0, -100], [0, -100], [0, -100]];
The keyframes for the three cubes
The cubes' paths, seen from above
The cubes' paths, seen from above

Between keyframes a cube doesn't go in a straight line. a carries on the way the cube was already going, b goes straight from this keyframe to the next, and blending from a to b as r goes from 0 to 1 gives a curve that sets off the way the cube was heading and arrives at the next keyframe:

  for (i = 0; i < 3; i++) {
    t = this.time - i * 0.25;
    var k = Math.floor(t);
    var r = t - k;
    if (k > paths[i].length - 3) {
      k = paths[i].length - 3;
    }
    if (k < 0) {
      k = 0;
    }
    ax = paths[i][k][0] * -r + paths[i][k + 1][0] * (r + 1);
    az = paths[i][k][1] * -r + paths[i][k + 1][1] * (r + 1);
    bx = paths[i][k + 1][0] * (1 - r) + paths[i][k + 2][0] * r;
    bz = paths[i][k + 1][1] * (1 - r) + paths[i][k + 2][1] * r;
    ox = 0;
    oy = 0;
    oz = 0;
    this.objects[i].tp = [ox + ax * (1 - r) + bx * r, oy, oz + az * (1 - r) + bz * r + 30];

    ...
  }
Where each cube should be, between two keyframes (trimmed)

That's a quadratic Bézier curve, with its control point half a step on from the keyframe it leaves. The last step goes from the last keyframe back to itself, so the cube swings past it and comes back.

That gives each cube a target, and the cube follows it on a spring. Each frame its speed moves 10% of the way towards the target and then loses 25% of itself, which damps it. Cubes that get too close push each other apart, and the cube under the mouse aims 40 units closer to you:

NavCube.prototype.move = function(f, dt) {
  var d = this.trad - this.rad;
  var r;
  this.rv += d * 0.1;
  this.rv *= 0.85;
  this.rad += this.rv;
  var i;
  f = [0, 0, 0];
  for (i = 0; i < 2; i++) {
    var o = this.cs[i].t.p;
    var dx = o[0] - this.t.p[0];
    var dy = o[1] - this.t.p[1];
    var dz = o[2] - this.t.p[2];
    d = Math.sqrt(dx * dx + dy * dy + dz * dz);
    r = this.rad + this.cs[i].rad * (1 + (this.cs[i].rad - 30) * 0.04632199) + 40;
    if (d < r) {
      r -= d;
      f[0] += 0.1 * r * dx / d;
      f[1] += 0.1 * r * dy / d;
      f[2] += 0.1 * r * dz / d;
    }
  }
  for (i = 0; i < 3; i++) {
    d = this.tp[i] - this.t.p[i];
    if (i == 2 && this.over) {
      d -= 40;
    }
    this.tpv[i] += d * 0.1;
    this.tpv[i] -= f[i];
    this.tpv[i] *= 0.75;
    this.t.p[i] += this.tpv[i];
  }
};
A cube's size and position, each on a spring, pushed away from the other two

The size works the same way. Moving the mouse over a cube makes its target size 35 instead of 30, and its size springs towards that, overshooting a little. Once it's over 32, it gets an outline, which fades in as it grows:

A cube under the mouse, come forward, with its shadow on the cube beside it
A cube under the mouse, come forward, with its shadow on the cube beside it

Each cube is drawn into its own movie clip, so that it can have its own rollover and clicks. Each frame the cubes are sorted by distance and given cbuf1 to cbuf3 in that order, nearest in front, and each clip keeps a note of which cube it has, so its mouse events go to the right one:

  order.sortOn("d");
  for (i = 0; i < 3; i++) {
    this.bufs[i].clear();
    order[i].o.draw(this.bufs[i], t);
    this.bufs[i].cube = order[i].o;
  }

  ...

  cb.onRollOver = function() {
    this.cube.onRollOver();
  };
The cubes given their clips by distance, and a clip passing a rollover on to its cube (trimmed)

Turning the view

The box at the bottom right is a little map of the scene from the front, with an eye in it that you can drag around. The eye's position decides which way the view turns. The axis is at right angles to the direction from the middle of the box to the eye, and the angle grows with the eye's distance from the middle:

  var ex = 1.753462 * (overlay.overlay_bottom.mini.eye._x - 50.5) / 101;
  var ey = 1.2891191822 * (overlay.overlay_bottom.mini.eye._y - 28.5) / 57;
  var angle = Math.sqrt(ex * ex + ey * ey) * 0.371268761982;
  if (time > 1.5 && time < 9) {
    var r = (time - 1.5) / 7.5;
    ori.setRotation(-ey, ex, 0, (0.5 - 0.5 * Math.cos(r * 3.14159)) * angle);
    redraw_hole = true;
  } else if (time >= 9 && (lex != ex || ley != ey)) {
    if (angle == 0) {
      ori.setRotation(1.1786218762, 7.172619, 0, 0);
    } else {
      ori.setRotation(-ey, ex, 0, angle);
    }
    last_redraw_hole = true;
  }
  lex = ex;
  ley = ey;
Turning the view towards the eye: eased in to start with, then whenever the eye moves

From 1.5 to 9 seconds the view eases from straight on to where the eye starts, which is why the band tilts as the logo breaks. After that it only changes when the eye moves, and the background, which is 72 textured triangles, is only drawn again when it has.

Windows

Clicking a cube opens its window: contact details, services or case studies. The window grows out from where you clicked, easing to its full size over 0.7 seconds, and then its contents fade in over another 0.4:

    var duration = 700;
    var elapsed = getTimer() - anim_start;
    if (elapsed > duration) {
      elapsed = duration;
    }
    var k = 0.5 - 0.5 * Math.cos(elapsed * 3.1415926 / duration);
    var w = Math.round(sw * (1 - k) + tw * k);
    var h = Math.round(sh * (1 - k) + th * k);
    _x = Math.round(sx * (1 - k) + tx * k);
    _y = Math.round(sy * (1 - k) + ty * k);
Growing the window, with a cosine ease (trimmed)

The frame is made of pieces for its corners and edges, moved and stretched to fit each size, and dragging the title bar moves the window, which is kept just inside the edges of the stage.

The glass is frosted. Every frame, the whole scene behind the window is drawn into a bitmap the size of the window, blurred, and put behind its contents:

function drawWindowTransparent() {
  var bmp = new flash.display.BitmapData(win.getWidth() - 4, win.getHeight() - 4, false, 16777215);
  bmp.draw(this, new flash.geom.Matrix(1, 0, 0, 1, -win._x + 4, -win._y + 4));
  bmp.applyFilter(bmp, bmp.rectangle, new flash.geom.Point(0, 0), new flash.filters.BlurFilter(8, 8, 1));
  win.blur.attachBitmap(bmp, 500);
  win.blur._x = 2;
  win.blur._y = 2;
}
Frosted glass: the scene behind the window, blurred

The labels

The labels at the top right are joined to the cubes by lines that follow the cubes as they move. Each line goes straight up from the cube's dot and then at 45 degrees to its label, and the bend is placed so the diagonal is at exactly 45 degrees wherever the cube is:

  b.lineStyle(4, 5556725, 100);
  b.moveTo(dot1._x, dot1._y);
  if (dot4._y + (dot4._x - dot1._x) < dot1._y) {
    b.lineTo(dot1._x, dot4._y + (dot4._x - dot1._x));
  } else {
    b.lineTo(dot4._x + (dot4._y - dot1._y), dot1._y);
  }
  b.lineTo(dot4._x - 3, dot4._y + 3);
The line from a cube's dot to its label, with one 45 degree bend

The stage isn't scaled, and when the browser window changes size, a listener on Stage moves the logo, the labels and the map so they stay the same distance from its edges.

Source code

The ActionScript was recovered from the movie with the SWF disassembler. Local variables aren't named in a SWF, so they come out as r1, r2 and so on, and in the excerpts above they've been given names.

Scripts

  • engine.as ActionScript 2 · 1950 lines · the 3D engine, the breaking pattern, the shadows and the cubes
  • window.as ActionScript 2 · 143 lines · opening, sizing and dragging a window
  • labels.as ActionScript 2 · 73 lines · the labels and the lines to the cubes
  • eye.as ActionScript 2 · 9 lines · dragging the eye in the map
  • contents.as ActionScript 2 · 161 lines · the tabs in the windows, the contact form and the main timeline

Download