Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active August 30, 2016 12:32
Show Gist options
  • Save mathdoodle/7af7c1675440ceaeedd78d27cfb5268c to your computer and use it in GitHub Desktop.
Save mathdoodle/7af7c1675440ceaeedd78d27cfb5268c to your computer and use it in GitHub Desktop.
3D on HTML Canvas

3D on HTML Canvas

export default function() {
describe("...", function() {
it("should ...", function() {
expect(true).toBeTruthy()
})
})
}
<!DOCTYPE html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src='https://jspm.io/system.js'></script>
<!-- SHADERS-MARKER -->
<!-- SCRIPTS-MARKER -->
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.import('./index.js')
</script>
</body>
</html>
/**
* Demonstrates the vanishing points from 3D space projected onto a 2D canvas.
*/
// Workaround to prevent TS2082 and TS2087.
// I don't know why this works.
var unused: Window = window;
function vectorE3(x: number, y: number, z: number) {
return EIGHT.Geometric3.vector(x, y, z);
}
function scalarE3(a: number) {
return EIGHT.Geometric3.scalar(a);
}
var WINDOW_HEIGHT = 800;
var WINDOW_WIDTH = 800;
var WINDOW_HALF_HEIGHT = WINDOW_HEIGHT / 2;
var WINDOW_HALF_WIDTH = WINDOW_WIDTH / 2;
var CANVAS_HEIGHT = 800;
var CANVAS_WIDTH = 800;
var CANVAS_HALF_HEIGHT = CANVAS_HEIGHT / 2;
var CANVAS_HALF_WIDTH = CANVAS_WIDTH / 2;
var CANVAS_DISTANCE = 100;
// Global Variables.
const popUp: Window = window.open("", "", "width=" + WINDOW_WIDTH + ", height=" + WINDOW_HEIGHT, false);
var context: CanvasRenderingContext2D;
var printer: Printer3D;
var e1 = vectorE3(1,0,0);
var e2 = vectorE3(0,1,0);
var e3 = vectorE3(0,0,1);
var arcBall: ArcBall;
class Printer3D {
private context2D: CanvasRenderingContext2D;
private d: number;
constructor(context2D: CanvasRenderingContext2D, d: number) {
this.context2D = context2D;
this.d = d;
}
beginPath(): void {
this.context2D.beginPath();
}
stroke(): void {
this.context2D.stroke();
}
moveTo(x: number, y: number, z: number): void {
var point = perspective(x, y, z, this.d);
this.context2D.moveTo(point.x + CANVAS_HALF_WIDTH, point.y + CANVAS_HALF_HEIGHT);
}
lineTo(x: number, y: number, z: number): void {
var point = perspective(x, y, z, this.d);
this.context2D.lineTo(point.x + CANVAS_HALF_WIDTH, point.y + CANVAS_HALF_HEIGHT);
}
}
class ArcBall {
private start: EIGHT.Geometric3;
public rotor: EIGHT.Geometric3 = scalarE3(1);
private win: Window;
private down: boolean = false;
private a: EIGHT.Geometric3;
private b: EIGHT.Geometric3;
constructor(win: Window) {
this.win = win;
}
private static vectorFromMouse(clientX: number, clientY: number): EIGHT.Geometric3 {
var x = (clientX - CANVAS_HALF_WIDTH) / CANVAS_HALF_WIDTH;
var y = (clientY - CANVAS_HALF_HEIGHT) / CANVAS_HALF_HEIGHT;
// The negative sign for z arises because the arc ball is a hemisphere in the
// directin of the user, which is negative z.
var z = -Math.sqrt(1 - x * x - y * y);
return vectorE3(x, y, z);
}
private static computeRotor(a: EIGHT.Geometric3, b: EIGHT.Geometric3) {
var one = scalarE3(1);
var rotor = one.add(b.mul(a)).div(a.clone().add(b).norm());
return rotor;
}
setUp(): void {
var self = this;
this.win.addEventListener('mousedown', function(ev: MouseEvent) {
self.down = true;
self.a = ArcBall.vectorFromMouse(ev.clientX, ev.clientY);
self.start = self.rotor;
});
this.win.addEventListener('mouseup', function(ev: MouseEvent) {
self.down = false;
self.b = ArcBall.vectorFromMouse(ev.clientX, ev.clientY);
self.rotor = ArcBall.computeRotor(self.a, self.b).mul(self.start);
});
this.win.addEventListener('mousemove', function(ev: MouseEvent) {
if (self.down) {
self.b = ArcBall.vectorFromMouse(ev.clientX, ev.clientY)
self.rotor = ArcBall.computeRotor(self.a, self.b).mul(self.start);
}
});
}
tearDown(): void {
}
}
class Cube {
public position: EIGHT.Geometric3;
public attitude: EIGHT.Geometric3;
public size: number = 100;
private corners: EIGHT.Geometric3[];
constructor(position: EIGHT.Geometric3, attitude: EIGHT.Geometric3) {
this.position = position;
this.attitude = attitude;
this.corners = [];
var sz = this.size;
this.corners.push(vectorE3(-1 * sz, +1 * sz, -1 * sz));
this.corners.push(vectorE3(-1 * sz, -1 * sz, -1 * sz));
this.corners.push(vectorE3(+1 * sz, -1 * sz, -1 * sz));
this.corners.push(vectorE3(+1 * sz, +1 * sz, -1 * sz));
this.corners.push(vectorE3(-1 * sz, +1 * sz, +1 * sz));
this.corners.push(vectorE3(-1 * sz, -1 * sz, +1 * sz));
this.corners.push(vectorE3(+1 * sz, -1 * sz, +1 * sz));
this.corners.push(vectorE3(+1 * sz, +1 * sz, +1 * sz));
}
draw()
{
var R = this.attitude;
var T = reverse(R);
var corners = this.corners.map(function(value) {return R.mul(value).mul(T);});
// front face
printer.beginPath();
context.strokeStyle = "#00FF00";
printer.moveTo(this.position.x + corners[0].x, this.position.y + corners[0].y, this.position.z + corners[0].z);
printer.lineTo(this.position.x + corners[1].x, this.position.y + corners[1].y, this.position.z + corners[1].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#FF0000";
printer.lineTo(this.position.x + corners[1].x, this.position.y + corners[1].y, this.position.z + corners[1].z);
printer.lineTo(this.position.x + corners[2].x, this.position.y + corners[2].y, this.position.z + corners[2].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#00FF00";
printer.lineTo(this.position.x + corners[2].x, this.position.y + corners[2].y, this.position.z + corners[2].z);
printer.lineTo(this.position.x + corners[3].x, this.position.y + corners[3].y, this.position.z + corners[3].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#FF0000";
printer.lineTo(this.position.x + corners[3].x, this.position.y + corners[3].y, this.position.z + corners[3].z);
printer.lineTo(this.position.x + corners[0].x, this.position.y + corners[0].y, this.position.z + corners[0].z);
printer.stroke();
// back face
printer.beginPath();
context.strokeStyle = "#00FF00";
printer.moveTo(this.position.x + corners[4].x, this.position.y + corners[4].y, this.position.z + corners[4].z);
printer.lineTo(this.position.x + corners[5].x, this.position.y + corners[5].y, this.position.z + corners[5].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#FF0000";
printer.lineTo(this.position.x + corners[5].x, this.position.y + corners[5].y, this.position.z + corners[5].z);
printer.lineTo(this.position.x + corners[6].x, this.position.y + corners[6].y, this.position.z + corners[6].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#00FF00";
printer.lineTo(this.position.x + corners[6].x, this.position.y + corners[6].y, this.position.z + corners[6].z);
printer.lineTo(this.position.x + corners[7].x, this.position.y + corners[7].y, this.position.z + corners[7].z);
printer.stroke();
printer.beginPath();
context.strokeStyle = "#FF0000";
printer.lineTo(this.position.x + corners[7].x, this.position.y + corners[7].y, this.position.z + corners[7].z);
printer.lineTo(this.position.x + corners[4].x, this.position.y + corners[4].y, this.position.z + corners[4].z);
printer.stroke();
// LHS face
printer.beginPath();
context.strokeStyle = "#0000FF";
printer.moveTo(this.position.x + corners[0].x, this.position.y + corners[0].y, this.position.z + corners[0].z);
printer.lineTo(this.position.x + corners[4].x, this.position.y + corners[4].y, this.position.z + corners[4].z);
printer.moveTo(this.position.x + corners[1].x, this.position.y + corners[1].y, this.position.z + corners[1].z);
printer.lineTo(this.position.x + corners[5].x, this.position.y + corners[5].y, this.position.z + corners[5].z);
printer.stroke();
// RHS face
printer.beginPath();
context.strokeStyle = "#0000FF";
printer.moveTo(this.position.x + corners[2].x, this.position.y + corners[2].y, this.position.z + corners[2].z);
printer.lineTo(this.position.x + corners[6].x, this.position.y + corners[6].y, this.position.z + corners[6].z);
printer.moveTo(this.position.x + corners[3].x, this.position.y + corners[3].y, this.position.z + corners[3].z);
printer.lineTo(this.position.x + corners[7].x, this.position.y + corners[7].y, this.position.z + corners[7].z);
printer.stroke();
// top face
// bottom face
}
}
var cube = new Cube(vectorE3(0, 0, 200), scalarE3(1));
const perspective = function(X: number, Y: number, Z: number, d: number): {x:number; y:number} {
/**
* The distance factor determines how much the X and Y components are reduced by the distance (Z + d) from the viewer.
*/
var distanceFactor = d / (Z + d);
return {'x': distanceFactor * X, 'y': distanceFactor * Y};
// var m = Math.sqrt(X * X + Y * Y + Z * Z);
// var x = d * (1 + X / m);
// var y = d * (1 + Y / m);
// return {'x':x, 'y':y};
}
function reverse(m: EIGHT.Geometric3) {
return m.clone().rev();
}
const vanishingPoint = function(v: EIGHT.Geometric3) : {x: number; y: number} {
var norm = v.norm();
var normalized = v.div(norm);
var x = CANVAS_DISTANCE * v.x / v.z;
var y = CANVAS_DISTANCE * v.y / v.z;
return {'x': x,'y': y};
}
const drawVanishingPoint = function(point: {x:number;y:number}, strokeStyle: string) {
context.beginPath();
context.strokeStyle = strokeStyle;
context.moveTo(point.x-10 + CANVAS_HALF_WIDTH, point.y + CANVAS_HALF_HEIGHT);
context.lineTo(point.x+10 + CANVAS_HALF_WIDTH, point.y + CANVAS_HALF_HEIGHT);
context.moveTo(point.x + CANVAS_HALF_WIDTH, point.y-10 + CANVAS_HALF_HEIGHT);
context.lineTo(point.x + CANVAS_HALF_WIDTH, point.y+10 + CANVAS_HALF_HEIGHT);
context.stroke();
}
/**
* Called for each animation tick.
*/
const tick = function(): void {
// Set the background color to gray.
context.fillStyle = "#555555";
context.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
var R = arcBall.rotor;
// Draw the cube at the appropriate attitude.
cube.attitude = R;
cube.position.y = 0;
cube.draw();
// Draw the vanishing points.
var T = reverse(R);
var p1 = vanishingPoint(R.mul(e1).mul(T));
drawVanishingPoint(p1, "#FF0000");
var p2 = vanishingPoint(R.mul(e2).mul(T));
drawVanishingPoint(p2, "#00FF00");
var p3 = vanishingPoint(R.mul(e3).mul(T));
drawVanishingPoint(p3, "#0000FF");
context.strokeStyle = "#FFFFFF";
// Draw symmetric two-vanishing point locus.
context.beginPath();
context.arc(CANVAS_HALF_WIDTH, CANVAS_HALF_HEIGHT, CANVAS_DISTANCE, 0, 2 * Math.PI);
context.closePath();
context.stroke();
// Draw symmetric three-vanishing point locus.
context.beginPath();
context.arc(CANVAS_HALF_WIDTH, CANVAS_HALF_HEIGHT, CANVAS_DISTANCE * Math.SQRT2, 0, 2 * Math.PI);
context.closePath();
context.stroke();
context.strokeRect(CANVAS_HALF_WIDTH - CANVAS_DISTANCE, CANVAS_HALF_HEIGHT - CANVAS_DISTANCE, CANVAS_DISTANCE * 2, CANVAS_DISTANCE * 2);
// requestAnimationFrame(tick)
}
/**
* Called to determine whether to end the animation.
*/
function terminate(time: number): boolean {
return false;
}
/**
* Called once at the start of the animation.
*/
const setUp = function() {
arcBall = new ArcBall(popUp);
arcBall.setUp();
var popDoc = popUp.document;
var canvas = popDoc.createElement("canvas");
canvas.setAttribute("id", "graph");
canvas.setAttribute("width", CANVAS_WIDTH.toString());
canvas.setAttribute("height", CANVAS_HEIGHT.toString());
popDoc.body.appendChild(canvas);
// Remove the margin that pushes the canvas.
popDoc.body.style.margin = "0";
context = canvas.getContext("2d");
printer = new Printer3D(context, CANVAS_DISTANCE);
}
/**
* Called once at the end of the animation.
*/
function tearDown(e: Error) {
arcBall.tearDown();
popUp.close();
if (e) {
alert(e.message);
}
}
setUp();
requestAnimationFrame(tick);
// eight.animationRunner(tick, terminate, setUp, tearDown, popUp).start();
{
"description": "3D on HTML Canvas",
"dependencies": {
"DomReady": "1.0.0",
"jasmine": "2.4.1",
"davinci-eight": "2.304.0"
},
"name": "",
"version": ""
}
body {
background-color: white;
}
<!DOCTYPE html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src='https://jspm.io/system.js'></script>
<!-- SCRIPTS-MARKER -->
</head>
<body>
<script>
// CODE-MARKER
</script>
<script>
System.import('./tests.js')
</script>
</body>
</html>
import Example from './Example.spec'
window['jasmine'] = jasmineRequire.core(jasmineRequire)
jasmineRequire.html(window['jasmine'])
const env = jasmine.getEnv()
const jasmineInterface = jasmineRequire.interface(window['jasmine'], env)
extend(window, jasmineInterface)
const htmlReporter = new jasmine.HtmlReporter({
env: env,
getContainer: function() { return document.body },
createElement: function() { return document.createElement.apply(document, arguments) },
createTextNode: function() { return document.createTextNode.apply(document, arguments) },
timer: new jasmine.Timer()
})
env.addReporter(htmlReporter)
DomReady.ready(function() {
htmlReporter.initialize()
describe("Example", Example)
env.execute()
})
/*
* Helper function for extending the properties on objects.
*/
export default function extend<T>(destination: T, source: any): T {
for (let property in source) {
destination[property] = source[property]
}
return destination
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment