_=Math;X=Y=Z=150;V=3;U=2;d=.1;v=_.sin,s=_.cos,m=66,n=250,t=g=16,w=document.getElementById("c"),z=_.PI/g,F=" u[m*s(h=_.PI/2-h*z)*s(q=q*z+rX,l*(p=m*s*v(q)k*m*vY,l*j-k*p+Z]} y{bb<4;)]=[n-(]-n(x=3007+180-])7-(p=@]-nA-n)A)/(x7-pA-j)/nn+(xA+p7)/j,j];j>.1&&o.push}X+=X<m|XU=-U):U;Y+=Y<m|YV=-V):V;Z+=W=Z<m?-W:W-1.5;l=s(a-=.04k=v(ar-=d;o=[];t=@t%69;ccFc@)eeFe@){c<g&&B+1B+1B-1B-1(c|1(e|1)&2?'red':'#fff'][i=c*=e*,0 i++!t-c-33]>>e,0;[D+g D + +e-32+t]>>c}o.sort( q-h}with(w.getContext('2d')){b=w.width=w.height=550;'#999';Rect(0,0,b,bc E o){(f=o[c])[4]||'#90C';begEPath(movDe E f)lE[e%4]D[e%4](()}}";for(b in C="F<32;EinD,fB,eA*k@++7*leTo(fy([[1]*.3[2]=0;(j=),)+strokeg,0 i+0,iu(ch[b(h)-9)*+b*g);fill>436?(d=-d,Style= ],[g,f(h,q){return],b&&'#30C'])for([0]function(b=1&[5495,5444,3444,5396,5492][".split(""))F=F.replace(RegExp(C[b][W=a=r=0],"g"),C[b].slice(1));setInterval(F,m)
// Compiled using Google's closure compiler.
// Handedits made:
// - setInterval with a single string expression (saves a function keyword, some curly braces etc.)
// - removed extraneous spaces/new lines still left
// - removed last semicolon
// - replaced any 0.x with .x
// - used @aivopaas' JS crusher on the expression, eval replaced by setInterval of course and removed unnecessary initializations
// X, Y, Z are the centre of the ball
// U, V and W are the velocity in x, y and z directions repsectively.
// a is the yaw, r the rotation of the ball in theta direction and d the directional change in rotation
// p is the pitch, and e is the directional change in pitch (not supported any longer)
// counter causes JS1K to scroll
_ = Math,X=Y=Z=150,V=3,W=a=r=0,d=.15,U=2,counter=15; // counter is really compiled as a parameter below
function R(sin, cos, radius, dw, sixteen, canvas, fact, sinYaw, cosYaw, sphere, i, j, tmp, iGrid, jGrid, x, y, z)
{
// Performs a translation from spherical to cartesian coordinates, together with an origin shift and some rotations.
function T(phi, theta) {
// It gets rather unreadable, but the following "inline" intialization saves 4 bytes (2 variables (minified) and ; characters).
// Theta and phi are in parts 1/16th parts of PI, thus the multiplication with fact = PI / 16.
// Also performs a rotation in the y,z-plane by an angle of yaw, together with a rotation in theta direction (which makes the ball spin).
// As we start, in spherical coords, at the top of the ball, we subtract from PI/2.
return [radius * cos(phi = _.PI / 2 - phi * fact) * cos(theta = theta * fact + r) + X,
cosYaw * (y = radius * cos(phi) * sin(theta)) + sinYaw * (z = radius * sin(phi)) + Y,
cosYaw * z - sinYaw * y + Z];
}
// Performs the camera translation given yaw and pitch, after first updating the camera's position which moves according to an ellipse (of angle yaw, conveniently).
// Finally performs the shift needed to translate our Cartesian to "Canvas" coordinates.
function cameraTranslateAndPush(poly) {
// Pitch rotation values were hardcoded and both negative, this - sign has been factored in below to save the 2 bytes. To really get rid of the -, x was also negated.
// reuse tmp variable, inline asignments to x, y and z, and a nasty post increment.
for(tmp = 0; tmp < 4;)
// Performs: lookup of x,y,z with camera origin translation (camera is being rotated through an ellipse in x,y-plane, then moved to origin). Camera is rotated to match yaw.
// dw-, and dw+ are canvas translations, other use of dw is simply "convenient", the number fits, and avoids a "big" number in its place.
// sinPitch is 1, so taken out completely, .3 is the cosPitch (both negated).
// yaw rotation is negative, cos(-a) = cos(a), sin(-a) = -sin(a): negation factored through.
// Pitch values are hardcoded, but really negative, so both negations factored through.
// 1/z/250 is the perspective division which seems to work well enough and doesn't look too skewed I find.
poly[tmp] = [
dw - ((z = poly[tmp][2] - dw) /* * sinPitch */ + (x = 300 * cosYaw + 180 - poly[tmp][0]) * cosYaw * .3 - (y = poly[tmp++][1] - dw * sinYaw - dw) * sinYaw * .3) / (z = (x * cosYaw /* * sinPitch */ - y * sinYaw /* * sinPitch */ - z * .3) / dw), // < NOTE tmp++ nasty increment saving 1 byte
dw + (x * sinYaw + y * cosYaw) / z,
z
];
// Draw if in front of camera only.
z > .1 && sphere.push(poly); // Google closure minifier doesn't always seem to do the minification if(..)->&&, so do it here.
}
// Handle bounce off invisible "walls", changing rotation direction upon every bounce.
X += X < radius | X > 436 ? (d = -d, U = -U) : U;
Y += Y < radius | Y > 436 ? (d = -d, V = -V) : V;
// Approximation of a Z bounce.
Z += W = Z < radius ? -W : W - 1.5;
// Update camera yaw which equals the angle of the elipse in which the camera moves.
// Actual rotation is by -yaw, but this has been factored through.
cosYaw = cos(a -= .04);
sinYaw = sin(a);
// Update camera pitch - can't fit in 1k.
// p += p < 4 || p > 5 ? e = -e : e));
// Update ball rotation, which changes on every y-bounce.
r -= d;
// Create a model of our sphere. This went through some iterations, starting with Math.floor and then a right shift as an integer division by 2.
// (floor(i/2)+floor(j/2)) % 2 became (i>>1)+(j>>1)&1 became 0|i/2 + (j>>1) & 1 became 2 & (i|1) + (j|1). Can I do better?
sphere = [];
counter = ++counter % (64 + 5);
for(i = 0; i < 32; i++)
for(j = 0; j < 32; j++)
// Only draw over PI (divided in 16 sections)
i < sixteen && cameraTranslateAndPush([T(i, j), T(i + 1, j), T(i + 1, j - 1), T(i, j - 1), (i|1) + (j|1) & 2 ? 'red' : '#fff']), // unfortunate operator precedence for or: parentheses needed
// Piggyback on for loops for the sphere.
// iGrid and jGrid are initialized "inline", saves a few bytes
// Drawing the grid this way simplifies our later canvas painting greatly, and does not require a conditional fill.
// The below array is 'crushed' by the jscrusher, so 2 appearances have little effect on size.
cameraTranslateAndPush([[iGrid = i * sixteen, jGrid = j * sixteen, 0], [iGrid + sixteen, jGrid, 0], [iGrid + sixteen, jGrid + sixteen, 0],
[iGrid + !(tmp = 1 & [5495,5444,3444,5396,5492][counter - i - 33] >> j - 9) * sixteen, jGrid + tmp * sixteen, 0], tmp && "#30C"]),
// Draw the grid in the y,z plane, and draw JS1K text at the same time. // "ᕷᕄ൴ᔔᕴ".charCodeAt(j - 14)
cameraTranslateAndPush([[0, iGrid, jGrid + sixteen], [0, iGrid, jGrid], [0, iGrid + sixteen, jGrid],
[0, iGrid + (tmp = 1 & [5495,5444,3444,5396,5492][j - 32 + counter] >> i - 9) * sixteen, jGrid + tmp * sixteen], tmp && "#30C"]);
// Painter's algorithm (or z-sort): sort on distance from camera, for brevity, pick first z coordinate. Which is ok anyway if the polies are small.
// This ensures that objects that are further away are drawn first so will be hidden by anything in front of it.
sphere.sort(function(a, b) { return b[0][2] - a[0][2] });
// Draw, starting with objects furthest away.
with(canvas.getContext('2d')) // sorry for the with
{
// Store the width/height in order to clear the "screen".
// Doing this in every "paint loop" doesn't seem problematic.
tmp = canvas.width = canvas.height = 550;
// Clear and colour background. 999 seems the closest gray in 3 rgb digits.
fillStyle = '#999';
fillRect(0, 0, tmp, tmp);
for(i in sphere)
{
// Colour is either defined as part of the model or defaults to the line colour.
strokeStyle = fillStyle = (jGrid = sphere[i])[4] || "#90C";
beginPath();
// Actual canvas displacement calculations were moved to cameraTranslateAndPush, thus contained in model.
moveTo(jGrid[0][1], jGrid[0][0]);
for(j in jGrid)
lineTo(jGrid[j % 4][1], jGrid[j % 4][0]); // uses mod to draw back to starting point
stroke(), fill();
}
}
}
// Initialize and repeat. Fact is set to PI / 16, making the ball consist of 7 coloured "squares" per PI.
// 75 is the radius of the ball, 250 is canvas offset and radius of circle (ellipse really) in which camera moves.
// #90C is the grid line colour. 550 the width of the canvas. 16 is distance between grid lines.
setInterval('R(_.sin,_.cos,75,250,16,document.getElementById("c"),_.PI/16)',66)