/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0
});
/****
* Game Code
****/
// Game constants for the screen
var SELECTED_RESOLUTION = null;
var resolutionSelected = false;
// Cache children array reference
var screen = game.children;
// Pre-allocate reusable arrays to avoid garbage collection
var tempRotatedTriangle = [0, 0, 0, 0, 0, 0, 0, 0, 0];
var tempCenteredTriangle = [0, 0, 0, 0, 0, 0, 0, 0, 0];
var tempProjectedTriangle = [0, 0, 0, 0, 0, 0];
var tempVertex1 = [0, 0, 0];
var tempVertex2 = [0, 0, 0];
var tempVertex3 = [0, 0, 0];
// Optimized screen clearing - only clear visible pixels
var visiblePixels = [];
function clearScreen() {
var len = visiblePixels.length;
for (var i = 0; i < len; i++) {
visiblePixels[i].tint = 0;
}
visiblePixels.length = 0;
}
function putPixel(x, y, color) {
if (x >= 0 && x < SCREEN_SIZE && y >= 0 && y < SCREEN_SIZE) {
var pixel = screen[x + y * SCREEN_SIZE];
pixel.tint = color;
visiblePixels.push(pixel);
}
}
// Line drawing function using Bresenham's line algorithm
function drawLine(x0, y0, x1, y1, color) {
var dx = x1 - x0;
var dy = y1 - y0;
var adx = dx < 0 ? -dx : dx;
var ady = dy < 0 ? -dy : dy;
var sx = dx < 0 ? -1 : 1;
var sy = dy < 0 ? -1 : 1;
var err = adx - ady;
var x = x0;
var y = y0;
while (true) {
putPixel(x, y, color);
if (x === x1 && y === y1) {
break;
}
var e2 = 2 * err;
if (e2 > -ady) {
err -= ady;
x += sx;
}
if (e2 < adx) {
err += adx;
y += sy;
}
}
}
function drawTriangle(triangle, color, wireframe) {
if (wireframe === undefined) {
wireframe = false;
}
// Extract triangle vertices
var x1 = triangle[0];
var y1 = triangle[1];
var x2 = triangle[2];
var y2 = triangle[3];
var x3 = triangle[4];
var y3 = triangle[5];
// Draw the three sides of the triangle only if wireframe is true
if (wireframe) {
drawLine(x1, y1, x2, y2, color); // Side 1-2
drawLine(x2, y2, x3, y3, color); // Side 2-3
drawLine(x3, y3, x1, y1, color); // Side 3-1
} else {
// Helper to interpolate x between two points at a given y
// Sort vertices by y (ascending) - reuse pre-allocated array
if (!drawTriangle.verts) {
drawTriangle.verts = [{
x: 0,
y: 0
}, {
x: 0,
y: 0
}, {
x: 0,
y: 0
}];
}
var verts = drawTriangle.verts;
verts[0].x = x1;
verts[0].y = y1;
verts[1].x = x2;
verts[1].y = y2;
verts[2].x = x3;
verts[2].y = y3;
var interpX = function interpX(y, x0, y0, x1, y1) {
if (y1 === y0) {
return x0;
}
return x0 + (x1 - x0) * (y - y0) / (y1 - y0);
}; // Fill bottom flat triangle (v0, v1, v2) if v1.y != v0.y
verts.sort(function (a, b) {
return a.y - b.y;
});
var v0 = verts[0];
var v1 = verts[1];
var v2 = verts[2];
if (v1.y !== v0.y) {
var yStart = Math.ceil(v0.y);
var yEnd = Math.ceil(v1.y);
for (var y = yStart; y < yEnd; y++) {
var xA = interpX(y, v0.x, v0.y, v2.x, v2.y);
var xB = interpX(y, v0.x, v0.y, v1.x, v1.y);
var xStart = xA < xB ? Math.ceil(xA) : Math.ceil(xB);
var xEnd = xA > xB ? Math.floor(xA) : Math.floor(xB);
for (var x = xStart; x <= xEnd; x++) {
putPixel(x, y, color);
}
}
}
// Fill top flat triangle (v1, v2, v0) if v2.y != v1.y
if (v2.y !== v1.y) {
var yStart = Math.ceil(v1.y);
var yEnd = Math.ceil(v2.y);
for (var y = yStart; y < yEnd; y++) {
var xA = interpX(y, v0.x, v0.y, v2.x, v2.y);
var xB = interpX(y, v1.x, v1.y, v2.x, v2.y);
var xStart = xA < xB ? Math.ceil(xA) : Math.ceil(xB);
var xEnd = xA > xB ? Math.floor(xA) : Math.floor(xB);
for (var x = xStart; x <= xEnd; x++) {
putPixel(x, y, color);
}
}
}
}
}
var colorIndex = 0;
// Resolution selection UI
var resolutionTitle = new Text2('Choose Resolution', {
size: 120,
fill: 0xFFFFFF
});
resolutionTitle.anchor.set(0.5, 0.5);
resolutionTitle.x = 2048 / 2;
resolutionTitle.y = 2732 / 2 - 300;
game.addChild(resolutionTitle);
var lowResText = new Text2('Low Resolution (64x64)', {
size: 80,
fill: 0x00FF00
});
lowResText.anchor.set(0.5, 0.5);
lowResText.x = 2048 / 2;
lowResText.y = 2732 / 2 - 100;
game.addChild(lowResText);
var highResText = new Text2('High Resolution (96x96)', {
size: 80,
fill: 0x0066FF
});
highResText.anchor.set(0.5, 0.5);
highResText.x = 2048 / 2;
highResText.y = 2732 / 2 + 100;
game.addChild(highResText);
// Add touch handlers for resolution selection
lowResText.down = function (x, y, obj) {
SELECTED_RESOLUTION = 'low';
SCREEN_SIZE = 64;
PIXEL_SIZE = 2048 / SCREEN_SIZE;
var offset = 2732 / 2 - SCREEN_SIZE * PIXEL_SIZE / 2;
resolutionSelected = true;
// Remove UI elements
game.removeChild(resolutionTitle);
game.removeChild(lowResText);
game.removeChild(highResText);
// Create the screen of pixels
for (var y = 0; y < SCREEN_SIZE; y++) {
for (var x = 0; x < SCREEN_SIZE; x++) {
// Get an instance of our pixel asset
var pixel = LK.getAsset('pixel', {
x: x * PIXEL_SIZE,
y: y * PIXEL_SIZE + offset,
tint: 0
});
game.addChild(pixel);
}
}
};
highResText.down = function (x, y, obj) {
SELECTED_RESOLUTION = 'high';
SCREEN_SIZE = 96;
PIXEL_SIZE = 2048 / SCREEN_SIZE;
var offset = 2732 / 2 - SCREEN_SIZE * PIXEL_SIZE / 2;
resolutionSelected = true;
// Remove UI elements
game.removeChild(resolutionTitle);
game.removeChild(lowResText);
game.removeChild(highResText);
// Create the screen of pixels using high res asset
for (var y = 0; y < SCREEN_SIZE; y++) {
for (var x = 0; x < SCREEN_SIZE; x++) {
// Get an instance of our pixel asset
var pixel = LK.getAsset('pixelHighRes', {
x: x * PIXEL_SIZE,
y: y * PIXEL_SIZE + offset,
tint: 0
});
game.addChild(pixel);
}
}
};
function translateTriangle(triangle, dx, dy, dz, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// Translate each vertex by dx, dy, dz - use provided result array
for (var i = 0; i < 9; i += 3) {
result[i] = triangle[i] + dx;
result[i + 1] = triangle[i + 1] + dy;
result[i + 2] = triangle[i + 2] + dz;
}
return result;
}
function rotateTriangle(triangle, angleX, angleY, angleZ, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// angleX, angleY, angleZ in radians
// Precompute sines and cosines
var cosX = Math.cos(angleX),
sinX = Math.sin(angleX);
var cosY = Math.cos(angleY),
sinY = Math.sin(angleY);
var cosZ = Math.cos(angleZ),
sinZ = Math.sin(angleZ);
// Inline rotation for better performance - rotate each vertex directly
for (var i = 0; i < 9; i += 3) {
var x = triangle[i];
var y = triangle[i + 1];
var z = triangle[i + 2];
// Rotate around Y axis (Yaw)
var x1 = x * cosY + z * sinY;
var z1 = -x * sinY + z * cosY;
// Rotate around X axis (Pitch)
var y2 = y * cosX - z1 * sinX;
var z2 = y * sinX + z1 * cosX;
// Rotate around Z axis (Roll)
var x3 = x1 * cosZ - y2 * sinZ;
var y3 = x1 * sinZ + y2 * cosZ;
result[i] = x3;
result[i + 1] = y3;
result[i + 2] = z2;
}
return result;
}
function projectTriangle(triangle, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// Perspective projection parameters
var fov = 80; // Focal length (distance from camera to projection plane)
var cx = SCREEN_SIZE / 2;
var cy = SCREEN_SIZE / 2;
// Inline projection for better performance
for (var i = 0; i < 9; i += 3) {
var x = triangle[i];
var y = triangle[i + 1];
var z = triangle[i + 2];
// Avoid division by zero
var zz = z === 0 ? 0.0001 : z;
var projIdx = i / 3 * 2;
result[projIdx] = Math.round(cx + (x - cx) * (fov / (fov + zz)));
result[projIdx + 1] = Math.round(cy + (y - cy) * (fov / (fov + zz)));
}
return result;
}
function shadeTriangleColor(triangle, baseColor) {
// Calculate face normal in view space (after rotation, before projection)
// Get the three vertices in view space
var v1 = triangle.slice(0, 3);
var v2 = triangle.slice(3, 6);
var v3 = triangle.slice(6, 9);
// Compute vectors
var ax = v2[0] - v1[0],
ay = v2[1] - v1[1],
az = v2[2] - v1[2];
var bx = v3[0] - v1[0],
by = v3[1] - v1[1],
bz = v3[2] - v1[2];
// Cross product (normal)
var nx = ay * bz - az * by;
var ny = az * bx - ax * bz;
var nz = ax * by - ay * bx;
// Normalize normal
var nlen = Math.sqrt(nx * nx + ny * ny + nz * nz);
if (nlen > 0) {
nx /= nlen;
ny /= nlen;
nz /= nlen;
}
// Camera looks down -Z, so light direction is (0,0,-1)
var dot = nz; // dot(normal, [0,0,-1]) = nz
// Clamp dot to [0,1] for brightness (facing camera = 1, edge = 0)
var brightness = dot + 0.25;
if (brightness < 0) {
brightness = 0;
}
if (brightness > 1) {
brightness = 1;
}
// Darken color by brightness (simple shading)
var r = (baseColor >> 16 & 0xFF) * brightness;
var g = (baseColor >> 8 & 0xFF) * brightness;
var b = (baseColor & 0xFF) * brightness;
return Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
}
function isBackfacing(projectedTriangle) {
// Check winding order using cross product of projected triangle
// projectedTriangle: [x1, y1, x2, y2, x3, y3]
var x1 = projectedTriangle[0];
var y1 = projectedTriangle[1];
var x2 = projectedTriangle[2];
var y2 = projectedTriangle[3];
var x3 = projectedTriangle[4];
var y3 = projectedTriangle[5];
// Calculate cross product to determine winding order
var crossProduct = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
// If cross product is negative, triangle is clockwise (backfacing)
return crossProduct < 0;
}
// Drag variables
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var dragStartAngleX = 0;
var dragStartAngleY = 0;
var angleX = Math.PI / 4;
var angleY = Math.PI / 4;
var momentumX = 0;
var momentumY = 0;
var lastAngleX = 0;
var lastAngleY = 0;
// Initialize cube rendering after resolution is selected
function initializeCubeRendering() {
// Touch/mouse down: start drag
game.down = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
dragStartX = x;
dragStartAngleY = angleY;
dragStartY = y;
dragStartAngleX = angleX;
isDragging = true;
};
// Touch/mouse up: end drag
game.up = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
isDragging = false;
};
// Touch/mouse move: update angles if dragging
game.move = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
if (isDragging) {
// Horizontal drag for Y-axis rotation
var dx = x - dragStartX;
angleY = dragStartAngleY - dx / 2048 * (2 * Math.PI);
// Vertical drag for X-axis rotation
var dy = y - dragStartY;
angleX = dragStartAngleX + dy / 2732 * (2 * Math.PI);
}
};
game.update = function () {
if (!resolutionSelected) {
return;
}
// Center the cube by translating to the middle of the screen
var centerX = SCREEN_SIZE / 2;
var centerY = SCREEN_SIZE / 2;
var centerZ = 16; // Move slightly back for better perspective
if (SELECTED_RESOLUTION == 'high') {
var triangles = [[-24, -24, -24, 24, -24, -24, -24, 24, -24], [-24, 24, -24, 24, -24, -24, 24, 24, -24], [24, -24, -24, 24, -24, 24, 24, 24, -24], [24, 24, -24, 24, -24, 24, 24, 24, 24], [-24, -24, 24, -24, 24, 24, 24, -24, 24], [-24, 24, 24, 24, 24, 24, 24, -24, 24], [-24, -24, 24, -24, -24, -24, -24, 24, 24], [-24, 24, 24, -24, -24, -24, -24, 24, -24], [24, -24, 24, 24, -24, -24, -24, -24, -24], [-24, -24, -24, -24, -24, 24, 24, -24, 24], [-24, 24, -24, 24, 24, 24, -24, 24, 24], [24, 24, 24, -24, 24, -24, 24, 24, -24]];
} else {
var triangles = [[-16, -16, -16, 16, -16, -16, -16, 16, -16], [-16, 16, -16, 16, -16, -16, 16, 16, -16], [16, -16, -16, 16, -16, 16, 16, 16, -16], [16, 16, -16, 16, -16, 16, 16, 16, 16], [-16, -16, 16, -16, 16, 16, 16, -16, 16], [-16, 16, 16, 16, 16, 16, 16, -16, 16], [-16, -16, 16, -16, -16, -16, -16, 16, 16], [-16, 16, 16, -16, -16, -16, -16, 16, -16], [16, -16, 16, 16, -16, -16, -16, -16, -16], [-16, -16, -16, -16, -16, 16, 16, -16, 16], [-16, 16, -16, 16, 16, 16, -16, 16, 16], [16, 16, 16, -16, 16, -16, 16, 16, -16]];
}
var COLOR_ARRAY = [0xFF0000,
// Red
0xFF7F00,
// Orange
0xFFFF00,
// Yellow
0x00FF00,
// Green
0x0000FF,
// Blue
0x4B0082
// Indigo
];
// If not dragging, apply momentum to angles
if (!isDragging) {
angleX += momentumX;
angleY += momentumY;
// Apply friction to slow down over time
momentumX *= 0.96;
momentumY *= 0.96;
// Clamp very small values to zero to avoid drift
if (Math.abs(momentumX) < 0.00001) {
momentumX = 0;
}
if (Math.abs(momentumY) < 0.00001) {
momentumY = 0;
}
}
//Start of the 3d renderer
clearScreen();
for (var i = 0; i < triangles.length; i++) {
var triangle = triangles[i];
rotateTriangle(triangle, angleX, angleY, 0, tempRotatedTriangle);
translateTriangle(tempRotatedTriangle, centerX, centerY, centerZ, tempCenteredTriangle);
projectTriangle(tempCenteredTriangle, tempProjectedTriangle);
// Only draw triangle if it's not backfacing
if (!isBackfacing(tempProjectedTriangle)) {
var shadedColor = shadeTriangleColor(tempRotatedTriangle, COLOR_ARRAY[Math.floor(i / 2)]);
drawTriangle(tempProjectedTriangle, shadedColor);
drawTriangle(tempProjectedTriangle, 0, true);
}
}
// Update momentum based on last frame's angle change
if (isDragging) {
var deltaX = angleX - lastAngleX;
var deltaY = angleY - lastAngleY;
// Smooth momentum using an exponential moving average for a more natural feel.
momentumX = momentumX * 0.5 + deltaX * 0.5;
momentumY = momentumY * 0.5 + deltaY * 0.5;
}
lastAngleX = angleX;
lastAngleY = angleY;
};
}
// Initialize cube rendering immediately
initializeCubeRendering(); /****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0
});
/****
* Game Code
****/
// Game constants for the screen
var SELECTED_RESOLUTION = null;
var resolutionSelected = false;
// Cache children array reference
var screen = game.children;
// Pre-allocate reusable arrays to avoid garbage collection
var tempRotatedTriangle = [0, 0, 0, 0, 0, 0, 0, 0, 0];
var tempCenteredTriangle = [0, 0, 0, 0, 0, 0, 0, 0, 0];
var tempProjectedTriangle = [0, 0, 0, 0, 0, 0];
var tempVertex1 = [0, 0, 0];
var tempVertex2 = [0, 0, 0];
var tempVertex3 = [0, 0, 0];
// Optimized screen clearing - only clear visible pixels
var visiblePixels = [];
function clearScreen() {
var len = visiblePixels.length;
for (var i = 0; i < len; i++) {
visiblePixels[i].tint = 0;
}
visiblePixels.length = 0;
}
function putPixel(x, y, color) {
if (x >= 0 && x < SCREEN_SIZE && y >= 0 && y < SCREEN_SIZE) {
var pixel = screen[x + y * SCREEN_SIZE];
pixel.tint = color;
visiblePixels.push(pixel);
}
}
// Line drawing function using Bresenham's line algorithm
function drawLine(x0, y0, x1, y1, color) {
var dx = x1 - x0;
var dy = y1 - y0;
var adx = dx < 0 ? -dx : dx;
var ady = dy < 0 ? -dy : dy;
var sx = dx < 0 ? -1 : 1;
var sy = dy < 0 ? -1 : 1;
var err = adx - ady;
var x = x0;
var y = y0;
while (true) {
putPixel(x, y, color);
if (x === x1 && y === y1) {
break;
}
var e2 = 2 * err;
if (e2 > -ady) {
err -= ady;
x += sx;
}
if (e2 < adx) {
err += adx;
y += sy;
}
}
}
function drawTriangle(triangle, color, wireframe) {
if (wireframe === undefined) {
wireframe = false;
}
// Extract triangle vertices
var x1 = triangle[0];
var y1 = triangle[1];
var x2 = triangle[2];
var y2 = triangle[3];
var x3 = triangle[4];
var y3 = triangle[5];
// Draw the three sides of the triangle only if wireframe is true
if (wireframe) {
drawLine(x1, y1, x2, y2, color); // Side 1-2
drawLine(x2, y2, x3, y3, color); // Side 2-3
drawLine(x3, y3, x1, y1, color); // Side 3-1
} else {
// Helper to interpolate x between two points at a given y
// Sort vertices by y (ascending) - reuse pre-allocated array
if (!drawTriangle.verts) {
drawTriangle.verts = [{
x: 0,
y: 0
}, {
x: 0,
y: 0
}, {
x: 0,
y: 0
}];
}
var verts = drawTriangle.verts;
verts[0].x = x1;
verts[0].y = y1;
verts[1].x = x2;
verts[1].y = y2;
verts[2].x = x3;
verts[2].y = y3;
var interpX = function interpX(y, x0, y0, x1, y1) {
if (y1 === y0) {
return x0;
}
return x0 + (x1 - x0) * (y - y0) / (y1 - y0);
}; // Fill bottom flat triangle (v0, v1, v2) if v1.y != v0.y
verts.sort(function (a, b) {
return a.y - b.y;
});
var v0 = verts[0];
var v1 = verts[1];
var v2 = verts[2];
if (v1.y !== v0.y) {
var yStart = Math.ceil(v0.y);
var yEnd = Math.ceil(v1.y);
for (var y = yStart; y < yEnd; y++) {
var xA = interpX(y, v0.x, v0.y, v2.x, v2.y);
var xB = interpX(y, v0.x, v0.y, v1.x, v1.y);
var xStart = xA < xB ? Math.ceil(xA) : Math.ceil(xB);
var xEnd = xA > xB ? Math.floor(xA) : Math.floor(xB);
for (var x = xStart; x <= xEnd; x++) {
putPixel(x, y, color);
}
}
}
// Fill top flat triangle (v1, v2, v0) if v2.y != v1.y
if (v2.y !== v1.y) {
var yStart = Math.ceil(v1.y);
var yEnd = Math.ceil(v2.y);
for (var y = yStart; y < yEnd; y++) {
var xA = interpX(y, v0.x, v0.y, v2.x, v2.y);
var xB = interpX(y, v1.x, v1.y, v2.x, v2.y);
var xStart = xA < xB ? Math.ceil(xA) : Math.ceil(xB);
var xEnd = xA > xB ? Math.floor(xA) : Math.floor(xB);
for (var x = xStart; x <= xEnd; x++) {
putPixel(x, y, color);
}
}
}
}
}
var colorIndex = 0;
// Resolution selection UI
var resolutionTitle = new Text2('Choose Resolution', {
size: 120,
fill: 0xFFFFFF
});
resolutionTitle.anchor.set(0.5, 0.5);
resolutionTitle.x = 2048 / 2;
resolutionTitle.y = 2732 / 2 - 300;
game.addChild(resolutionTitle);
var lowResText = new Text2('Low Resolution (64x64)', {
size: 80,
fill: 0x00FF00
});
lowResText.anchor.set(0.5, 0.5);
lowResText.x = 2048 / 2;
lowResText.y = 2732 / 2 - 100;
game.addChild(lowResText);
var highResText = new Text2('High Resolution (96x96)', {
size: 80,
fill: 0x0066FF
});
highResText.anchor.set(0.5, 0.5);
highResText.x = 2048 / 2;
highResText.y = 2732 / 2 + 100;
game.addChild(highResText);
// Add touch handlers for resolution selection
lowResText.down = function (x, y, obj) {
SELECTED_RESOLUTION = 'low';
SCREEN_SIZE = 64;
PIXEL_SIZE = 2048 / SCREEN_SIZE;
var offset = 2732 / 2 - SCREEN_SIZE * PIXEL_SIZE / 2;
resolutionSelected = true;
// Remove UI elements
game.removeChild(resolutionTitle);
game.removeChild(lowResText);
game.removeChild(highResText);
// Create the screen of pixels
for (var y = 0; y < SCREEN_SIZE; y++) {
for (var x = 0; x < SCREEN_SIZE; x++) {
// Get an instance of our pixel asset
var pixel = LK.getAsset('pixel', {
x: x * PIXEL_SIZE,
y: y * PIXEL_SIZE + offset,
tint: 0
});
game.addChild(pixel);
}
}
};
highResText.down = function (x, y, obj) {
SELECTED_RESOLUTION = 'high';
SCREEN_SIZE = 96;
PIXEL_SIZE = 2048 / SCREEN_SIZE;
var offset = 2732 / 2 - SCREEN_SIZE * PIXEL_SIZE / 2;
resolutionSelected = true;
// Remove UI elements
game.removeChild(resolutionTitle);
game.removeChild(lowResText);
game.removeChild(highResText);
// Create the screen of pixels using high res asset
for (var y = 0; y < SCREEN_SIZE; y++) {
for (var x = 0; x < SCREEN_SIZE; x++) {
// Get an instance of our pixel asset
var pixel = LK.getAsset('pixelHighRes', {
x: x * PIXEL_SIZE,
y: y * PIXEL_SIZE + offset,
tint: 0
});
game.addChild(pixel);
}
}
};
function translateTriangle(triangle, dx, dy, dz, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// Translate each vertex by dx, dy, dz - use provided result array
for (var i = 0; i < 9; i += 3) {
result[i] = triangle[i] + dx;
result[i + 1] = triangle[i + 1] + dy;
result[i + 2] = triangle[i + 2] + dz;
}
return result;
}
function rotateTriangle(triangle, angleX, angleY, angleZ, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// angleX, angleY, angleZ in radians
// Precompute sines and cosines
var cosX = Math.cos(angleX),
sinX = Math.sin(angleX);
var cosY = Math.cos(angleY),
sinY = Math.sin(angleY);
var cosZ = Math.cos(angleZ),
sinZ = Math.sin(angleZ);
// Inline rotation for better performance - rotate each vertex directly
for (var i = 0; i < 9; i += 3) {
var x = triangle[i];
var y = triangle[i + 1];
var z = triangle[i + 2];
// Rotate around Y axis (Yaw)
var x1 = x * cosY + z * sinY;
var z1 = -x * sinY + z * cosY;
// Rotate around X axis (Pitch)
var y2 = y * cosX - z1 * sinX;
var z2 = y * sinX + z1 * cosX;
// Rotate around Z axis (Roll)
var x3 = x1 * cosZ - y2 * sinZ;
var y3 = x1 * sinZ + y2 * cosZ;
result[i] = x3;
result[i + 1] = y3;
result[i + 2] = z2;
}
return result;
}
function projectTriangle(triangle, result) {
// triangle: [x1, y1, z1, x2, y2, z2, x3, y3, z3]
// Perspective projection parameters
var fov = 80; // Focal length (distance from camera to projection plane)
var cx = SCREEN_SIZE / 2;
var cy = SCREEN_SIZE / 2;
// Inline projection for better performance
for (var i = 0; i < 9; i += 3) {
var x = triangle[i];
var y = triangle[i + 1];
var z = triangle[i + 2];
// Avoid division by zero
var zz = z === 0 ? 0.0001 : z;
var projIdx = i / 3 * 2;
result[projIdx] = Math.round(cx + (x - cx) * (fov / (fov + zz)));
result[projIdx + 1] = Math.round(cy + (y - cy) * (fov / (fov + zz)));
}
return result;
}
function shadeTriangleColor(triangle, baseColor) {
// Calculate face normal in view space (after rotation, before projection)
// Get the three vertices in view space
var v1 = triangle.slice(0, 3);
var v2 = triangle.slice(3, 6);
var v3 = triangle.slice(6, 9);
// Compute vectors
var ax = v2[0] - v1[0],
ay = v2[1] - v1[1],
az = v2[2] - v1[2];
var bx = v3[0] - v1[0],
by = v3[1] - v1[1],
bz = v3[2] - v1[2];
// Cross product (normal)
var nx = ay * bz - az * by;
var ny = az * bx - ax * bz;
var nz = ax * by - ay * bx;
// Normalize normal
var nlen = Math.sqrt(nx * nx + ny * ny + nz * nz);
if (nlen > 0) {
nx /= nlen;
ny /= nlen;
nz /= nlen;
}
// Camera looks down -Z, so light direction is (0,0,-1)
var dot = nz; // dot(normal, [0,0,-1]) = nz
// Clamp dot to [0,1] for brightness (facing camera = 1, edge = 0)
var brightness = dot + 0.25;
if (brightness < 0) {
brightness = 0;
}
if (brightness > 1) {
brightness = 1;
}
// Darken color by brightness (simple shading)
var r = (baseColor >> 16 & 0xFF) * brightness;
var g = (baseColor >> 8 & 0xFF) * brightness;
var b = (baseColor & 0xFF) * brightness;
return Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
}
function isBackfacing(projectedTriangle) {
// Check winding order using cross product of projected triangle
// projectedTriangle: [x1, y1, x2, y2, x3, y3]
var x1 = projectedTriangle[0];
var y1 = projectedTriangle[1];
var x2 = projectedTriangle[2];
var y2 = projectedTriangle[3];
var x3 = projectedTriangle[4];
var y3 = projectedTriangle[5];
// Calculate cross product to determine winding order
var crossProduct = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1);
// If cross product is negative, triangle is clockwise (backfacing)
return crossProduct < 0;
}
// Drag variables
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var dragStartAngleX = 0;
var dragStartAngleY = 0;
var angleX = Math.PI / 4;
var angleY = Math.PI / 4;
var momentumX = 0;
var momentumY = 0;
var lastAngleX = 0;
var lastAngleY = 0;
// Initialize cube rendering after resolution is selected
function initializeCubeRendering() {
// Touch/mouse down: start drag
game.down = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
dragStartX = x;
dragStartAngleY = angleY;
dragStartY = y;
dragStartAngleX = angleX;
isDragging = true;
};
// Touch/mouse up: end drag
game.up = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
isDragging = false;
};
// Touch/mouse move: update angles if dragging
game.move = function (x, y, obj) {
if (!resolutionSelected) {
return;
}
if (isDragging) {
// Horizontal drag for Y-axis rotation
var dx = x - dragStartX;
angleY = dragStartAngleY - dx / 2048 * (2 * Math.PI);
// Vertical drag for X-axis rotation
var dy = y - dragStartY;
angleX = dragStartAngleX + dy / 2732 * (2 * Math.PI);
}
};
game.update = function () {
if (!resolutionSelected) {
return;
}
// Center the cube by translating to the middle of the screen
var centerX = SCREEN_SIZE / 2;
var centerY = SCREEN_SIZE / 2;
var centerZ = 16; // Move slightly back for better perspective
if (SELECTED_RESOLUTION == 'high') {
var triangles = [[-24, -24, -24, 24, -24, -24, -24, 24, -24], [-24, 24, -24, 24, -24, -24, 24, 24, -24], [24, -24, -24, 24, -24, 24, 24, 24, -24], [24, 24, -24, 24, -24, 24, 24, 24, 24], [-24, -24, 24, -24, 24, 24, 24, -24, 24], [-24, 24, 24, 24, 24, 24, 24, -24, 24], [-24, -24, 24, -24, -24, -24, -24, 24, 24], [-24, 24, 24, -24, -24, -24, -24, 24, -24], [24, -24, 24, 24, -24, -24, -24, -24, -24], [-24, -24, -24, -24, -24, 24, 24, -24, 24], [-24, 24, -24, 24, 24, 24, -24, 24, 24], [24, 24, 24, -24, 24, -24, 24, 24, -24]];
} else {
var triangles = [[-16, -16, -16, 16, -16, -16, -16, 16, -16], [-16, 16, -16, 16, -16, -16, 16, 16, -16], [16, -16, -16, 16, -16, 16, 16, 16, -16], [16, 16, -16, 16, -16, 16, 16, 16, 16], [-16, -16, 16, -16, 16, 16, 16, -16, 16], [-16, 16, 16, 16, 16, 16, 16, -16, 16], [-16, -16, 16, -16, -16, -16, -16, 16, 16], [-16, 16, 16, -16, -16, -16, -16, 16, -16], [16, -16, 16, 16, -16, -16, -16, -16, -16], [-16, -16, -16, -16, -16, 16, 16, -16, 16], [-16, 16, -16, 16, 16, 16, -16, 16, 16], [16, 16, 16, -16, 16, -16, 16, 16, -16]];
}
var COLOR_ARRAY = [0xFF0000,
// Red
0xFF7F00,
// Orange
0xFFFF00,
// Yellow
0x00FF00,
// Green
0x0000FF,
// Blue
0x4B0082
// Indigo
];
// If not dragging, apply momentum to angles
if (!isDragging) {
angleX += momentumX;
angleY += momentumY;
// Apply friction to slow down over time
momentumX *= 0.96;
momentumY *= 0.96;
// Clamp very small values to zero to avoid drift
if (Math.abs(momentumX) < 0.00001) {
momentumX = 0;
}
if (Math.abs(momentumY) < 0.00001) {
momentumY = 0;
}
}
//Start of the 3d renderer
clearScreen();
for (var i = 0; i < triangles.length; i++) {
var triangle = triangles[i];
rotateTriangle(triangle, angleX, angleY, 0, tempRotatedTriangle);
translateTriangle(tempRotatedTriangle, centerX, centerY, centerZ, tempCenteredTriangle);
projectTriangle(tempCenteredTriangle, tempProjectedTriangle);
// Only draw triangle if it's not backfacing
if (!isBackfacing(tempProjectedTriangle)) {
var shadedColor = shadeTriangleColor(tempRotatedTriangle, COLOR_ARRAY[Math.floor(i / 2)]);
drawTriangle(tempProjectedTriangle, shadedColor);
drawTriangle(tempProjectedTriangle, 0, true);
}
}
// Update momentum based on last frame's angle change
if (isDragging) {
var deltaX = angleX - lastAngleX;
var deltaY = angleY - lastAngleY;
// Smooth momentum using an exponential moving average for a more natural feel.
momentumX = momentumX * 0.5 + deltaX * 0.5;
momentumY = momentumY * 0.5 + deltaY * 0.5;
}
lastAngleX = angleX;
lastAngleY = angleY;
};
}
// Initialize cube rendering immediately
initializeCubeRendering();