import {meter, kilogram, second, unitless, newton, coulomb} from './units'; // Change the lables used for the basis to i, j, k. // These lables were borrowed from Hamilton's quaternions. UNITS.G3.BASIS_LABELS = UNITS.G3.BASIS_LABELS_HAMILTON ///////////////////////////////////////////////////////////////////////////// // Lighting /** * Ambient Lighting for the World. */ const ambLight = new EIGHT.AmbientLight(EIGHT.Color.white.scale(0.4)) /** * Directional Lighting for the World. */ const dirLight = new EIGHT.DirectionalLight() ///////////////////////////////////////////////////////////////////////////// // Standard Colors /** * The standard colors. */ export const color = { red: EIGHT.Color.red, green: EIGHT.Color.green, blue: EIGHT.Color.blue, yellow: EIGHT.Color.yellow, magenta: EIGHT.Color.magenta, cyan: EIGHT.Color.cyan, orange: EIGHT.Color.fromRGB(1, 102 / 255, 0), black: EIGHT.Color.black, white: EIGHT.Color.white } ///////////////////////////////////////////////////////////////////////////// // Physical Constants /** * */ export const ε0 = 8.854E-12 * (coulomb * coulomb) / (meter * meter * newton) ///////////////////////////////////////////////////////////////////////////// // Validation /** * Determines whether a multivector is admissable as a vector. */ function isVector(mv: EIGHT.GeometricE3): boolean { if (mv.a !== 0 || mv.b !== 0 || mv.yz !== 0 || mv.zx !== 0 || mv.xy !== 0) { return false; } else { return true; } } /** * Determines whether a multivector is admissable as a scalar. */ function isScalar(mv: EIGHT.GeometricE3): boolean { if (mv.x !== 0 || mv.y !== 0 || mv.z !== 0 || mv.yz !== 0 || mv.zx !== 0 || mv.xy !== 0 || mv.b !== 0) { return false; } else { return true; } } /** * A camera is a frame of reference from which the scene is viewed. */ export interface Camera { /** * The position of the camera, a position vector, measured in meters. */ eye: EIGHT.Vector3; /** * The point that the camera is looking at, a position vector, measured in meters. */ look: UNITS.G3; /** * The desired up direction, a dimensionless vector. */ up: UNITS.G3; } /** * A ready-to-go composite for EIGHT animations. */ export class World { /** * The scale factor for converting world units to dimensionless units. */ public scaleFactor: UNITS.G3 = meter; /** * The frame of reference from which the world is viewed. */ public camera: Camera; public engine:EIGHT.Engine; public scene: EIGHT.Scene; public ambients: EIGHT.Facet[] = []; private trackball:EIGHT.TrackballControls; private dimlessCamera: EIGHT.PerspectiveCamera; public framecounter: number = 0; public overlay: EIGHT.Diagram3D; constructor() { // Notice that the canvas is "burned in". this.engine = new EIGHT.Engine('canvas3D') .clearColor(0.2, 0.2, 0.2, 1.0) .enable(EIGHT.Capability.DEPTH_TEST) // .enable(EIGHT.Capability.BLEND) // .blendFunc(EIGHT.BlendingFactorSrc.SRC_ALPHA, EIGHT.BlendingFactorDest.ONE); this.scene = new EIGHT.Scene(this.engine) this.dimlessCamera = new EIGHT.PerspectiveCamera() this.dimlessCamera.eye.x = 0 this.dimlessCamera.eye.y = 0 this.dimlessCamera.eye.z = 3 this.ambients.push(this.dimlessCamera) this.camera = worldCamera(this, this.dimlessCamera) this.ambients.push(ambLight) this.ambients.push(dirLight) this.trackball = new EIGHT.TrackballControls(this.dimlessCamera, window) // Workaround because Trackball no longer supports context menu for panning. this.trackball.noPan = true this.trackball.subscribe(this.engine.canvas) this.overlay = new EIGHT.Diagram3D('canvas2D', this.dimlessCamera) windowResize(this.engine, this.overlay, this.dimlessCamera).resize() } /** * The underlying HTML5 Canvas. */ get canvas(): HTMLCanvasElement { return this.engine.canvas; } /** * The origin is fixed to be zero. */ get origin(): EIGHT.Vector3 { return EIGHT.Vector3.e3().scale(0) // return 0 * this.scaleFactor } /** * Adds a drawable object to the world. */ add(drawable: EIGHT.Renderable): void { if (drawable){ this.scene.add(drawable) } else { // Throw Error } } /** * Clears the WebGL canvas and keeps the directional light pointing in the camera direction. */ clear(): void { this.engine.clear() this.overlay.clear() this.trackball.update() dirLight.direction.copy(this.dimlessCamera.look).sub(this.dimlessCamera.eye) } /** * Draws the objects that have been added to the world. */ draw(): void { this.scene.draw(this.ambients) } drawText(text: string, X: EIGHT.Vector3): void { const where = {x: 0, y: 0, z: 0} // scale(text, X, this.scaleFactor, where) this.overlay.fillText(text, X) } } /** * Divides the measure by the scaleFactor to produce a dimensionless quantity. */ function scale(name: string, measure: UNITS.G3, scaleFactor: UNITS.G3, out: EIGHT.VectorE3): void { if (!isScalar(scaleFactor)) { throw new Error(`scaleFactor must be a scalar. scale(${name}, ${measure}, ${scaleFactor})`) } // We are expecting the result of scaling to produce a dimensionless quantity. const dimless = measure / scaleFactor; const uom = dimless.uom if (!uom || uom.isOne()) { out.x = dimless.x out.y = dimless.y out.z = dimless.z // return EIGHT.Geometric3.copy(dimless) } else { throw new Error(`Units of ${name}, ${scaleFactor}, is not consistent with units of quantity, ${measure}.`) } } function scaleToNumber(name: string, measure: UNITS.G3, scaleFactor: UNITS.G3): number { if (!isScalar(scaleFactor)) { throw new Error(`scaleFactor must be a scalar. scale(${name}, ${measure}, ${scaleFactor})`) } // We are expecting the result of scaling to produce a dimensionless quantity. const dimless = measure / scaleFactor; const uom = dimless.uom if (!uom || uom.isOne()) { return dimless.a } else { throw new Error(`Units of ${name}, ${scaleFactor}, is not consistent with units of quantity, ${measure}.`) } } ////////////////////////////////////////////////////////////////////////////// /** * A type that is useful for representing vectors. */ export class Arrow { private _scaleFactor: UNITS.G3 = meter; private _label: string; private inner: EIGHT.Arrow; constructor(private world: World) { this.inner = new EIGHT.Arrow() world.add(this.inner) } get color() { return this.inner.color; } set color(color: EIGHT.Color) { this.inner.color = color; } get label() { return this._label; } set label(value: string) { if (typeof value === 'string') { this._label = value } else { throw new Error(`Arrow.label property must be a string.`); } } get scaleFactor() { return this._scaleFactor; } set scaleFactor(value: UNITS.G3) { if (isScalar(value)) { this._scaleFactor = value; } else { throw new Error(`Arrow.scaleFactor property must be a scalar.`); } } get model() { return EIGHT.Vector3.copy(this.inner.h) // return UNITS.G3.copy(this.inner.h).mul(this.scaleFactor) } set model(value: EIGHT.Vector3) { this.inner.h.copyVector(value) /* if (isVector(value)) { scale('axis', value, this.scaleFactor, this.inner.h) } else { throw new Error(`Arrow.axis property must be a vector.`); } */ } get position() { return EIGHT.Vector3.copy(this.inner.X) // return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor); } set position(value: EIGHT.Vector3) { this.inner.X.copyVector(value) /* if (isVector(value)) { scale('pos', value, this.world.scaleFactor, this.inner.X) } else { throw new Error(`Arrow.pos property must be a vector.`); } */ } draw() { this.inner.render(this.world.ambients) if (typeof this._label === 'string' && this._label.length > 0) { this.world.drawText(this._label, this.position + (this.model / 2)) // this.world.drawText(this._label, this.position + (this.model / 2) * (this.world.scaleFactor / this.scaleFactor)) } } } /** * Constructor function for an Arrow. */ export function createArrow(world: World, options: {scaleFactor?: UNITS.G3; color?: EIGHT.Color} = {}): Arrow { const that = new Arrow(world) if (options.scaleFactor) { if (options.scaleFactor instanceof UNITS.G3) { that.scaleFactor = options.scaleFactor; } else { throw new Error("pos option must have type UNITS.G3"); } } if (options.color) { if (options.color instanceof EIGHT.Color) { that.color = options.color; } else { throw new Error("color property must have type EIGHT.Color"); } } return that; } /** * */ export class Box { public scaleFactor: UNITS.G3 = meter; private inner: EIGHT.Box; constructor(private world: World) { this.inner = new EIGHT.Box({k: 1}) this.scaleFactor = world.scaleFactor world.add(this.inner) } get color() { return this.inner.color; } set color(color: EIGHT.Color) { this.inner.color = color; } get width() { return UNITS.G3.scalar(this.inner.width, this.scaleFactor.uom) } set width(value: UNITS.G3) { if (isScalar(value)) { this.inner.width = scaleToNumber('width', value, this.scaleFactor) } else { throw new Error(`Box.width property must be a scalar.`); } } get height() { return UNITS.G3.scalar(this.inner.height, this.scaleFactor.uom) } set height(value: UNITS.G3) { if (isScalar(value)) { this.inner.height = scaleToNumber('height', value, this.scaleFactor) } else { throw new Error(`Box.height property must be a scalar.`); } } get depth() { return UNITS.G3.scalar(this.inner.depth, this.scaleFactor.uom) } set depth(value: UNITS.G3) { if (isScalar(value)) { this.inner.depth = scaleToNumber('depth', value, this.scaleFactor) } else { throw new Error(`Box.depth property must be a scalar.`); } } get pos() { return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor); } set pos(value: UNITS.G3) { if (isVector(value)) { scale('X', value, this.world.scaleFactor, this.inner.X) } else { throw new Error(`Box.pos property must be a vector.`); } } get visible() { return this.inner.visible } set visible(value: boolean) { this.inner.visible = false } draw() { this.inner.render(this.world.ambients) } } /** * Constructor function for a Box. */ export function createBox(world: World, options: {pos?: UNITS.G3; color?: EIGHT.Color} = {}): Box { if (world) { const that = new Box(world); if (options.pos) { if (options.pos instanceof UNITS.G3) { that.pos = options.pos; } else { throw new Error("pos option must have type UNITS.G3"); } } if (options.color) { if (options.color instanceof EIGHT.Color) { that.color = options.color; } else { throw new Error("color property must have type EIGHT.Color"); } } return that; } else { throw new Error("World has not yet been initialized.") } } /** * TODO */ export class Cylinder { private inner: EIGHT.Cylinder; public scaleFactor: UNITS.G3 = meter; constructor(private world: World) { this.inner = new EIGHT.Cylinder(); world.add(this.inner) } get color() { return this.inner.color; } set color(color: EIGHT.Color) { this.inner.color = color; } get length() { return UNITS.G3.copy(this.inner.length, void 0).mul(this.scaleFactor); } set length(length: UNITS.G3) { scale('length', length, this.scaleFactor, this.inner.length) } get radius() { return UNITS.G3.copy(this.inner.radius, void 0).mul(this.scaleFactor); } set radius(radius: UNITS.G3) { scale('radius', radius, this.scaleFactor, this.inner.radius) } get axis() { return UNITS.G3.copy(this.inner.axis, void 0) } set axis(axis: UNITS.G3) { scale('axis', axis, unitless, this.inner.axis) } get transparent() { return this.inner.transparent; } set transparent(transparent: boolean) { this.inner.transparent = transparent; } get X() { return UNITS.G3.copy(this.inner.X, void 0).mul(this.world.scaleFactor); } set X(X: UNITS.G3) { scale('X', X, this.world.scaleFactor, this.inner.X) } } /** * */ export class Grid { constructor(private world: World, private inner: EIGHT.Grid) { world.add(inner) } get color() { return this.inner.color; } set color(color: EIGHT.Color) { this.inner.color = color; } draw() { this.inner.render(this.world.ambients) } } export function createGridXY(world: World) { const that = new Grid(world, new EIGHT.GridXY()) return that; } export function createGridZX(world: World) { const that = new Grid(world, new EIGHT.GridZX()) return that; } /** * */ export class Sphere { public scaleFactor: UNITS.G3 = meter; public trail: Curve; private inner: EIGHT.Sphere; private _velocity: UNITS.G3 = 0 * meter / second; constructor(private world: World) { this.inner = new EIGHT.Sphere() this.scaleFactor = world.scaleFactor; this.inner.transparent = false this.inner.opacity = 1 world.add(this.inner) } get color() { return this.inner.color; } set color(color: EIGHT.Color) { this.inner.color = color; } get radius() { return UNITS.G3.scalar(this.inner.radius, this.scaleFactor.uom) } set radius(value: UNITS.G3) { this.inner.radius = scaleToNumber('radius', value, this.scaleFactor) } get velocity() { return this._velocity; } set velocity(value: UNITS.G3) { if (isVector(value)) { this._velocity = value; } else { throw new Error(`Sphere.velocity property must be a vector.`); } } get pos() { return UNITS.G3.copy(this.inner.X).mul(this.world.scaleFactor); } set pos(value: UNITS.G3) { if (isVector(value)) { scale('X', value, this.world.scaleFactor, this.inner.X) } else { throw new Error(`Sphere.pos property must be a vector.`); } } } /** * Constructor function for a Sphere. */ export function sphere(world: World, options: {pos?: UNITS.G3; radius?: UNITS.G3; color?: EIGHT.Color} = {}): Sphere { if (world) { const that = new Sphere(world); if (options.pos) { if (options.pos instanceof UNITS.G3) { that.pos = options.pos; } else { throw new Error("pos option must have type UNITS.G3"); } } if (options.radius) { if (options.radius instanceof UNITS.G3) { that.radius = options.radius; } else { throw new Error("radius option must have type UNITS.G3"); } } if (options.color) { if (options.color instanceof EIGHT.Color) { that.color = options.color; } else { throw new Error("color option must have type EIGHT.Color"); } } return that; } else { throw new Error("World has not yet been initialized.") } } export interface Curve { append(point: UNITS.G3): void } /** * */ export function curve(world: World, options: {color?: EIGHT.Color} = {}): Curve { const track = new EIGHT.Track({engine: world.engine, color: options.color}) world.scene.add(track) const that: Curve = { append(point: UNITS.G3): void { track.addPoint(point) } } return that; } /////////////////////////////////////////////////////////////////////// /** * Wrapper object for the PerspectiveCamera so that the eye, look (vector) * properties use the units of the World (usually meters). */ function worldCamera(world: World, camera: EIGHT.PerspectiveCamera): Camera { const that: Camera = { get eye() { return EIGHT.Vector3.copy(camera.eye) // return UNITS.G3.copy(camera.eye).mul(world.scaleFactor); }, set eye(value: EIGHT.Vector3) { camera.eye.copyVector(value) /* if (isVector(value)) { scale('eye', value, world.scaleFactor, camera.eye) } else { throw new Error(`Camera.eye property must be a vector.`); } */ }, get look() { return UNITS.G3.copy(camera.look).mul(world.scaleFactor); }, set look(value: UNITS.G3) { if (isVector(value)) { scale('look', value, world.scaleFactor, camera.look) } else { throw new Error(`Camera.look property must be a vector.`); } }, get up() { return UNITS.G3.copy(camera.up).mul(unitless); }, set up(value: UNITS.G3) { if (isVector(value)) { scale('up', value, unitless, camera.up) } else { throw new Error(`Camera.up property must be a vector.`); } } } return that; } /////////////////////////////////////////////////////////////////////////////// /** * Displays an exception by writing it to a <pre> element. */ function displayError(e: any) { const stderr = <HTMLPreElement>document.getElementById('error') stderr.style.color = "#FF0000" stderr.innerHTML = `${e}` } /** * Calls the callback argument when the Document Object Model (DOM) has been loaded. * Exceptions thrown by the callback function are caught and displayed. */ export function domReady(callback: () => any): void { DomReady.ready(function() { try { callback() } catch(e) { displayError(e) } }) } /** * Catches exceptions thrown in the animation callback and displays them. * This function will have a slight performance impact owing to the try...catch statement. * This function may be bypassed for production use by using window.requestAnimationFrame directly. */ export function requestFrame(callback: FrameRequestCallback): number { const wrapper: FrameRequestCallback = function(time: number) { try { callback(time) } catch(e) { displayError(e) } } return window.requestAnimationFrame(wrapper) } /** * Creates an object that manages resizing of the output to fit the window. */ function windowResize(engine: EIGHT.Engine, overlay: EIGHT.Diagram3D, camera: EIGHT.PerspectiveCamera){ const callback = function() { engine.size(window.innerWidth, window.innerHeight); // engine.viewport(0, 0, window.innerWidth, window.innerHeight) // engine.canvas.width = window.innerWidth // engine.canvas.height = window.innerHeight engine.canvas.style.width = `${window.innerWidth}px` engine.canvas.style.height = `${window.innerHeight}px` camera.aspect = window.innerWidth / window.innerHeight; overlay.canvas.width = window.innerWidth overlay.canvas.height = window.innerHeight overlay.canvas.style.width = `${window.innerWidth}px` overlay.canvas.style.height = `${window.innerHeight}px` const ctxt = overlay.canvas.getContext('2d') ctxt.font = '24px Helvetica' ctxt.fillStyle = '#FFFFFF' } window.addEventListener('resize', callback, false); const that = { /** * */ resize: function() { callback(); return that; }, /** * Stop watching window resize */ stop : function() { window.removeEventListener('resize', callback); return that; } }; return that; }