The Amiga boing ball, as seen here http://www.youtube.com/watch?v=-ga41edXw3A, although this time in actual 3D: a camera rotation instead of a palette rotation.
_=Math;X=Y=Z=100;V=3;W=a=r=0;d=1;
function R(t,l,j,u,C,v,w,m,D,x,y,g,z,E,h,k,c,b,e){function n(f,i){f=f*E+r;i=_.PI/2-i*E;return[j*l(i)*l(f)+X,j*l(i)*t(f)+Y,j*t(i)+Z]}function A(f,i,o,B,p,q,s){s=l(a);q=t(-a);for(e=0;e<4;e++){p=f[e][0]-u;o=f[e][1]-u;B=f[e][2]-u;f[e]=[p*s*y-o*q*y-B*x,p*q+o*s,p*s*x-o*q*x+B*y]}k.push(f)}h=z.getContext("2d");z.width=z.height=v;if(Y<=j||Y>=w-j){V=-V;d=-d}Z<=j?W=-W:W-=1.5;a-=0.04;r-=d*0.1;k=[];for(c=0;c<14;c++)for(b=0;b<7;b++)A([n(c,b),n(c+1,b),n(c+1,b-1),n(c,b-1),(c+b)%2?"red":"#fff"]);Y+=V;Z+=W;h[D]="#999";
h.fillRect(0,0,v,v);for(c=0;c<=w;c+=g)for(b=0;b<=w;b+=g){A([[c,b,0],[c+g,b,0],[c+g,b+g,0],[c+g,b,0],C]);A([[0,c,b],[0,c+g,b],[0,c+g,b+g],[0,c+g,b],C])}k.sort(function(f,i){return i[0][2]-f[0][2]});for(c in k){e=k[c];h.strokeStyle=h[D]=e[4];h.beginPath();h.moveTo(e[0][1]+m,-e[0][0]+m);for(b=1;b<=4;b++)h.lineTo(e[b%4][1]+m,-e[b%4][0]+m);h.stroke();h.fill()}}setInterval('R(_.sin,_.cos,50,200,"#90C",550,300,250,"fillStyle",-1,-.3,15,document.getElementById("c"),_.PI/7)',33);
Xz1NYXRoO1g9WT1aPTEwMDtWPTM7Vz1hPXI9MDtkPTE7DQpmdW5jdGlvbiBSKHQsbCxqLHUsQyx2LHcsbSxELHgseSxnLHosRSxoLGssYyxiLGUpe2Z1bmN0aW9uIG4oZixpKXtmPWYqRStyO2k9Xy5QSS8yLWkqRTtyZXR1cm5baipsKGkpKmwoZikrWCxqKmwoaSkqdChmKStZLGoqdChpKStaXX1mdW5jdGlvbiBBKGYsaSxvLEIscCxxLHMpe3M9bChhKTtxPXQoLWEpO2ZvcihlPTA7ZTw0O2UrKyl7cD1mW2VdWzBdLXU7bz1mW2VdWzFdLXU7Qj1mW2VdWzJdLXU7ZltlXT1bcCpzKnktbypxKnktQip4LHAqcStvKnMscCpzKngtbypxKngrQip5XX1rLnB1c2goZil9aD16LmdldENvbnRleHQoIjJkIik7ei53aWR0aD16LmhlaWdodD12O2lmKFk8PWp8fFk+PXctail7Vj0tVjtkPS1kfVo8PWo/Vz0tVzpXLT0xLjU7YS09MC4wNDtyLT1kKjAuMTtrPVtdO2ZvcihjPTA7YzwxNDtjKyspZm9yKGI9MDtiPDc7YisrKUEoW24oYyxiKSxuKGMrMSxiKSxuKGMrMSxiLTEpLG4oYyxiLTEpLChjK2IpJTI/InJlZCI6IiNmZmYiXSk7WSs9VjtaKz1XO2hbRF09IiM5OTkiOw0KaC5maWxsUmVjdCgwLDAsdix2KTtmb3IoYz0wO2M8PXc7Yys9Zylmb3IoYj0wO2I8PXc7Yis9Zyl7QShbW2MsYiwwXSxbYytnLGIsMF0sW2MrZyxiK2csMF0sW2MrZyxiLDBdLENdKTtBKFtbMCxjLGJdLFswLGMrZyxiXSxbMCxjK2csYitnXSxbMCxjK2csYl0sQ10pfWsuc29ydChmdW5jdGlvbihmLGkpe3JldHVybiBpWzBdWzJdLWZbMF1bMl19KTtmb3IoYyBpbiBrKXtlPWtbY107aC5zdHJva2VTdHlsZT1oW0RdPWVbNF07aC5iZWdpblBhdGgoKTtoLm1vdmVUbyhlWzBdWzFdK20sLWVbMF1bMF0rbSk7Zm9yKGI9MTtiPD00O2IrKyloLmxpbmVUbyhlW2IlNF1bMV0rbSwtZVtiJTRdWzBdK20pO2guc3Ryb2tlKCk7aC5maWxsKCl9fXNldEludGVydmFsKCdSKF8uc2luLF8uY29zLDUwLDIwMCwiIzkwQyIsNTUwLDMwMCwyNTAsImZpbGxTdHlsZSIsLTEsLS4zLDE1LGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJjIiksXy5QSS83KScsMzMpOw0K
// X=P0,Y=P1,Z=P2,V=V1,W=V2,a=yaw,r=rot, d=rotation direction
// X, Y, Z are the centre of the ball
// V and W are the velocity in y and z directions repsectively
// a is the yaw, r the rotation and d the rotation direction
_ = Math,X=Y=Z=100,V=3,W=a=r=0,d=1;
function R(sin, cos, radius, Q, lineColour, w, gridSize, dw, fillStyle, sinPitch, cosPitch, fifteen,
canvas, fact, context, sphere, i, j, tmp)
{
context = canvas.getContext('2d'), // just assume it exists!
canvas.width = canvas.height = w;
// Performs a translation from spherical to cartesion coordinates, together with an origin shift.
function T(theta, phi) {
theta = theta * fact + r;
phi = _.PI / 2 - phi * fact;
return [radius * cos(phi) * cos(theta) + X, radius * cos(phi) * sin(theta) + Y, radius * sin(phi) + Z]
};
// Handle bounce off walls etc.
if (Y <= radius || Y >= gridSize - radius)
V = -V, d = -d;
Z <= radius ? W = -W : W -= 1.5;
// Update camera yaw, and rotation of ball.
a -= .04;
r -= d * .1;
// Create a model of our sphere. No space in 1k to make it smooth, can be done by increasing the facet count
// but requires dividing i and j in the colour calculation which adds bytes etc.
sphere = [];
for(i = 0; i < 14; i++)
for(j = 0; j < 7; j++)
cameraTranslateAndPush(
[T(i, j), T(i + 1, j), T(i + 1, j - 1), T(i, j - 1),
(i+j) % 2 ? 'red' : '#fff']);
Y += V; Z += W;
// Clear and color background.
context[fillStyle] = '#999';
context.fillRect(0,0,w,w);
// Draws the grid, sticks to 4 points avoiding array length queries later on.
for(i = 0; i <= gridSize; i += fifteen)
for(j = 0; j <= gridSize; j += fifteen)
cameraTranslateAndPush([[i,j,0],[i + fifteen, j,0],[i+fifteen,j+fifteen,0],[i+fifteen,j,0], lineColour]),
cameraTranslateAndPush([[0,i,j],[0, i + fifteen, j],[0,i+fifteen,j+fifteen],[0,i+fifteen,j], lineColour])
// Painter's algorithm: sort on distance from camera, for brevity, pick first z coordinate.
sphere.sort(function(a, b) {
return b[0][2] - a[0][2];
}
);
// Draw, starting with objects furthest away.
for(i in sphere)
{
tmp = sphere[i];
context.strokeStyle = context[fillStyle] = tmp[4];
context.beginPath();
context.moveTo(tmp[0][1] + dw, -tmp[0][0] + dw);
for(j = 1; j <= 4; j++)
context.lineTo(tmp[j % 4][1] + dw, -tmp[j % 4][0] + dw); // uses % trick to draw back to starting point
context.stroke();
context.fill();
}
// Performs the camera translation given yaw and pitch (for brevity, I had to hardcode the pitch - it is precalculated).
function cameraTranslateAndPush(poly, x, y, z, x0, sinYaw, cosYaw) {
cosYaw = cos(a);
sinYaw = sin(-a);
for(tmp = 0; tmp < 4; tmp++) // reuse tmp variable
{
x0 = poly[tmp][0] - Q;
y = poly[tmp][1] - Q;
z = poly[tmp][2] - Q;
poly[tmp] = [x0 * cosYaw * cosPitch - y * sinYaw * cosPitch - z * sinPitch, x0 * sinYaw + y * cosYaw, x0 * cosYaw * sinPitch - y * sinYaw * sinPitch + z * cosPitch];
}
sphere.push(poly);
}
}
setInterval('R(_.sin,_.cos,50,200,"#90C",550,300,250,"fillStyle",-1,-.3,15,document.getElementById("c"),_.PI/7)',33); // note: heavy rounding of pitch