Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robertpenner/dec8a5c16f71eb88c3ab to your computer and use it in GitHub Desktop.
Save robertpenner/dec8a5c16f71eb88c3ab to your computer and use it in GitHub Desktop.
3D TypeScript cubes, wireframe and shading

3D TypeScript cubes, wireframe and shading ('-' * 42) 3D cubes with wireframe and shading in TypeScript.

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!

Forked from Bas Groothedde's Pen 3D TypeScript cubes, wireframe and shading.

A Pen by Robert Penner on CodePen.

License.

'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);
body, html {
margin: 0;
padding: 0;
}
body {
background: #000;
canvas {
display: block;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment