|
'use strict'; // TS should do this! |
|
|
|
/** |
|
TypeScript in CodePen |
|
|
|
I wanted to see how well this preprocessor works. |
|
The editor doesn't cope with it that well quite yet. |
|
The indentation doesn't quite work, nor does the syntax |
|
highlighting. |
|
|
|
Also, when an error occurs, it always points to line 1. |
|
No matter what the error is. |
|
|
|
It also is rather difficult to access external symbols |
|
from secondary script files, for example, new Stats() isn't possible, |
|
but that's just a TypeScript annoyance. |
|
|
|
Aside from that; it works beautifully! |
|
*/ |
|
|
|
class Size2D { |
|
constructor(public width: number, public height: number) { |
|
} |
|
} |
|
|
|
class Point2D { |
|
constructor(public x: number, public y: number) { |
|
} |
|
} |
|
|
|
class CanvasException { |
|
message: string; |
|
name: string = "CanvasException"; |
|
constructor(message: string) { |
|
this.message = message; |
|
} |
|
} |
|
|
|
class RGBAColor { |
|
constructor(public r: number, public g: number, public b:number, public a: number = 255) { |
|
} |
|
|
|
/** |
|
Instead of setting the alpha (0-255), set |
|
the opacity (0-1). |
|
*/ |
|
setOpacity(opacity: number): void { |
|
this.a = Math.floor(opacity * 255); |
|
} |
|
|
|
toString(): string { |
|
if(this.a === 255) { |
|
return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; |
|
} else { |
|
var o: number = (this.a / 255); |
|
return 'rgba(' + this.r + ', ' + this.g + ', ' + this.b + ', ' + o + ')'; |
|
} |
|
} |
|
} |
|
|
|
class RenderLoop { |
|
function: Function; |
|
userdata: any; |
|
|
|
constructor(f: Function, udata: any = null) { |
|
this.function = f; |
|
this.userdata = udata; |
|
|
|
var that = this; |
|
var wrap = function() { |
|
window.requestAnimationFrame(wrap); |
|
that.function.call(that, that.userdata); |
|
}; wrap(); |
|
} |
|
} |
|
|
|
class FullSizeCanvas { |
|
element: any; |
|
context: any; |
|
size: Size2D; |
|
fcol: RGBAColor; |
|
|
|
constructor(parent: any = document.body) { |
|
this.element = document.createElement('canvas'); |
|
if(!this.element) { |
|
throw new CanvasException("Canvas not supported"); |
|
} |
|
|
|
this.context = this.element.getContext('2d'); |
|
if(!this.context) { |
|
throw new CanvasException("2D context error"); |
|
} |
|
|
|
this.size = new Size2D(window.innerWidth, window.innerHeight); |
|
this.element.width = this.size.width; |
|
this.element.height = this.size.height; |
|
|
|
this.fcol = new RGBAColor(0, 0, 0); |
|
|
|
parent.appendChild(this.element); |
|
} |
|
|
|
fit() { |
|
this.size = new Size2D(window.innerWidth, window.innerHeight); |
|
this.element.width = this.size.width; |
|
this.element.height = this.size.height; |
|
} |
|
|
|
setFadeColor(color: RGBAColor): void { |
|
this.fcol = color; |
|
} |
|
|
|
clear(fade: boolean = false, opacity: number = 0.3): void { |
|
if(fade) { |
|
this.fcol.setOpacity(opacity); |
|
this.context.fillStyle = this.fcol.toString(); |
|
this.context.fillRect(0, 0, this.size.width, this.size.height); |
|
} else { |
|
this.context.clearRect(0, 0, this.size.width, this.size.height); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
3D vectors are used to build a vertex of points to |
|
render lines in between. |
|
*/ |
|
class Vector3D { |
|
x: number; |
|
y: number; |
|
z: number; |
|
|
|
constructor(x: number, y: number, z: number) { |
|
this.x = x; |
|
this.y = y; |
|
this.z = z; |
|
} |
|
|
|
rotateX(angle: number): Vector3D { |
|
var rad, cosa, sina, y, z; |
|
rad = angle * Math.PI / 180; |
|
cosa = Math.cos(rad); |
|
sina = Math.sin(rad); |
|
y = this.y * cosa - this.z * sina; |
|
z = this.y * sina + this.z * cosa; |
|
|
|
return new Vector3D(this.x, y, z); |
|
} |
|
|
|
rotateY(angle: number): Vector3D { |
|
var rad, cosa, sina, x, z; |
|
rad = angle * Math.PI / 180; |
|
cosa = Math.cos(rad); |
|
sina = Math.sin(rad); |
|
x = this.z * sina + this.x * cosa; |
|
z = this.z * cosa - this.x * sina; |
|
|
|
return new Vector3D(x, this.y, z); |
|
} |
|
|
|
rotateZ(angle: number): Vector3D { |
|
var rad, cosa, sina, x, y; |
|
rad = angle * Math.PI / 180; |
|
cosa = Math.cos(rad); |
|
sina = Math.sin(rad); |
|
x = this.x * cosa - this.y * sina; |
|
y = this.x * sina + this.y * cosa; |
|
|
|
return new Vector3D(x, y, this.z); |
|
} |
|
|
|
rotate(x: number, y: number, z:number): Vector3D { |
|
return this.rotateX(x).rotateY(y).rotateZ(z); |
|
} |
|
|
|
project(cx: number, cy: number, fov: number, viewDistance: number): Vector3D { |
|
var factor, x, y; |
|
factor = fov / (viewDistance + this.z); |
|
x = this.x * factor + cx; |
|
y = this.y * factor + cy; |
|
return new Vector3D(x, y, this.z); |
|
} |
|
} |
|
|
|
class RotationAngle3D { |
|
x: number; |
|
y: number; |
|
z: number; |
|
|
|
constructor(x: number, y: number, z: number) { |
|
this.x = x; |
|
this.y = y; |
|
this.z = z; |
|
} |
|
|
|
increaseX(value: number) { |
|
this.x = ((this.x + value) % 360); |
|
} |
|
|
|
increaseY(value: number) { |
|
this.y = ((this.y + value) % 360); |
|
} |
|
|
|
increaseZ(value: number) { |
|
this.z = ((this.z + value) % 360); |
|
} |
|
} |
|
|
|
class Cube { |
|
vertices: Array<Vector3D>; |
|
state: Array<Vector3D>; |
|
faces: Array<Array<number>>; |
|
angle: RotationAngle3D = new RotationAngle3D(0, 0, 0); // start at an angle of 0 by default |
|
astep: RotationAngle3D = new RotationAngle3D(2, 2, 2); // rotate all dimensions by 2 degrees each time. |
|
pos: Point2D; |
|
stroke: RGBAColor; |
|
fill: RGBAColor = null; |
|
|
|
constructor(position: Point2D, color: RGBAColor) { |
|
this.vertices = [ |
|
new Vector3D(-1,1,-1), |
|
new Vector3D(1,1,-1), |
|
new Vector3D(1,-1,-1), |
|
new Vector3D(-1,-1,-1), |
|
new Vector3D(-1,1,1), |
|
new Vector3D(1,1,1), |
|
new Vector3D(1,-1,1), |
|
new Vector3D(-1,-1,1) |
|
]; |
|
|
|
/* |
|
Each integer in this matrix is a reference to a vector in the vertices list. |
|
A cube has 6 sides, the faces define which vertices make up each side. |
|
*/ |
|
this.faces = [ |
|
[0,1,2,3], |
|
[1,5,6,2], |
|
[5,4,7,6], |
|
[4,0,3,7], |
|
[0,4,5,1], |
|
[3,2,6,7] |
|
]; |
|
|
|
this.pos = position; |
|
this.stroke = color; |
|
} |
|
|
|
setFill(c: RGBAColor) { |
|
this.fill = c; |
|
} |
|
|
|
setStroke(c: RGBAColor) { |
|
this.stroke = c; |
|
} |
|
|
|
setStep(s: RotationAngle3D) { |
|
this.astep = s; |
|
} |
|
|
|
step() { |
|
var r: Vector3D; |
|
|
|
// generate the state that will be rendered, preserve the original vertices. |
|
this.state = new Array<Vector3D>(); |
|
for(var i = 0; i < this.vertices.length; i++) { |
|
r = this.vertices[i].rotate(this.angle.x, this.angle.y, this.angle.z).project(this.pos.x, this.pos.y, 128, 3.5); |
|
|
|
this.state.push(r); |
|
} |
|
|
|
// increase rotation |
|
this.angle.increaseX(this.astep.x); |
|
this.angle.increaseY(this.astep.y); |
|
this.angle.increaseZ(this.astep.z); |
|
} |
|
|
|
render(context: any) { |
|
context.strokeStyle = this.stroke.toString(); |
|
|
|
for(var i = 0; i < this.faces.length; i++) { |
|
var f = this.faces[i]; |
|
context.beginPath(); |
|
context.moveTo(this.state[f[0]].x, this.state[f[0]].y); |
|
context.lineTo(this.state[f[1]].x, this.state[f[1]].y); |
|
context.lineTo(this.state[f[2]].x, this.state[f[2]].y); |
|
context.lineTo(this.state[f[3]].x, this.state[f[3]].y); |
|
context.closePath(); |
|
context.stroke(); |
|
|
|
if(this.fill !== null) { |
|
context.fillStyle = this.fill.toString(); |
|
context.fill(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
var w: number = window.innerWidth, |
|
h: number = window.innerHeight, |
|
c: FullSizeCanvas, |
|
l: RenderLoop, |
|
cubes: Array<Cube>; |
|
|
|
var stats = new window['Stats']; // we'll get a "Cannot find name: Stats" if we do new Stats(); |
|
stats.setMode(0); // FPS mode |
|
|
|
// Place the statistics at the bottom right. |
|
stats.domElement.style.position = 'absolute'; |
|
stats.domElement.style.right = '5px'; |
|
stats.domElement.style.bottom = '5px'; |
|
document.body.appendChild(stats.domElement); |
|
|
|
// create a canvas |
|
c = new FullSizeCanvas(); |
|
c.setFadeColor(new RGBAColor(38, 38, 38)); |
|
c.clear(true, 1); |
|
|
|
// Array of cubes |
|
cubes = new Array<Cube>(); |
|
|
|
// center green cube. |
|
cubes.push(new Cube(new Point2D(w / 2, h / 2), new RGBAColor(30, 255, 30, 255))); |
|
cubes[0].setFill(new RGBAColor(50, 220, 50, 0.4 * 255)); |
|
|
|
// red cube |
|
cubes.push(new Cube(new Point2D((w / 3) / 2, h / 2), new RGBAColor(255, 30, 30, 255))); |
|
cubes[1].setFill(new RGBAColor(220, 50, 50, 0.4 * 255)); |
|
cubes[1].setStep(new RotationAngle3D(-1, 2, 2)); |
|
|
|
// blue cube |
|
cubes.push(new Cube(new Point2D(w - (w / 3) / 2, h / 2), new RGBAColor(30, 30, 255, 255))); |
|
cubes[2].setFill(new RGBAColor(50, 50, 220, 0.4 * 255)); |
|
cubes[2].setStep(new RotationAngle3D(-2, -2, 0)); |
|
|
|
window.addEventListener('resize', function(event) { |
|
c.fit(); |
|
|
|
var w: number = c.size.width, |
|
h: number = c.size.height; |
|
|
|
cubes[0].pos.x = (w / 2); |
|
cubes[1].pos.x = ((w / 3) / 2); |
|
cubes[2].pos.x = w - ((w / 3) / 2); |
|
for(var i in cubes) cubes[i].pos.y = (h / 2); |
|
}); |
|
|
|
l = new RenderLoop(function(context) { |
|
stats.begin(); |
|
c.clear(true, 0.4); |
|
|
|
for(var i in cubes) { |
|
cubes[i].step(); |
|
cubes[i].render(context); |
|
} |
|
stats.end(); |
|
}, c.context); |