Last active
August 30, 2016 12:32
-
-
Save mathdoodle/7af7c1675440ceaeedd78d27cfb5268c to your computer and use it in GitHub Desktop.
3D on HTML Canvas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default function() { | |
describe("...", function() { | |
it("should ...", function() { | |
expect(true).toBeTruthy() | |
}) | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"description": "3D on HTML Canvas", | |
"dependencies": { | |
"DomReady": "1.0.0", | |
"jasmine": "2.4.1", | |
"davinci-eight": "2.304.0" | |
}, | |
"name": "", | |
"version": "" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background-color: white; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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