|
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; |
|
} |