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:
| Time | What happens |
|---|---|
| 0 s | The logo band, still in one piece |
| 1.05 s | The camera jolts forward, and the logo cracks |
| 1.5 s | The view starts to turn towards where the eye in the corner is, and gets there at 9 s |
| 2.05 s | A bigger jolt, and the pieces break out, tumbling towards you and falling |
| 2 s to 6.5 s | The three cubes come out of the hole, one after another, and spread out |
| 6 s | The labels and the map fade in |
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 camera moves forward by 8 times this for the first jolt, and 15 times for the second.


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;
};
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));
};
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];
};
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:
| Depth | Clip | What's drawn in it |
|---|---|---|
| 100 | sbuf | Every shadow, as a mask (see Shadows) |
| 150 | hbuf | The background: the logo, the hole and the two lines |
| 200 | shad | A sheet of black at 30%, which only shows through sbuf |
| 501 to 503 | cbuf1 to cbuf3 | The cubes, one in each |
| 1000 | buf | The falling pieces |
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:
| Object | What it is |
|---|---|
Plane | The background: a grid of triangles textured with the logo |
Line | The top and bottom edges of the logo band |
Polygon | A falling piece, textured with the logo |
HolePolygon | The same piece in black, to make the hole |
NavCube | One of the three cubes |
Scene, CubeScene | Lists of objects, drawn back to front |
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();
}
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);
}
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:
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:
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;
}
}
}
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;
}
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);
...
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;
}
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;
};

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.
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];
...
}
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]);
}
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%:
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;
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]);
}
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);
}
};
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 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]];
}
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 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:


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]];
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]);
}
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]];
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];
...
}
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];
}
};
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:

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();
};
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;
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);
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;
}
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 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
- index.swf Flash 8 movie · the whole site