Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active March 8, 2017 18:37
Show Gist options
  • Save mathdoodle/f1ddd640cb40fbffbb2ba09d441fe35c to your computer and use it in GitHub Desktop.
Save mathdoodle/f1ddd640cb40fbffbb2ba09d441fe35c to your computer and use it in GitHub Desktop.
Vectors and Mathematics
table {
background: #FFFFFF;
border: solid 1px #DDDDDD;
margin-bottom: 1.25rem;
table-layout: auto; }
table caption {
background: transparent;
color: #222222;
font-size: 1rem;
font-weight: bold; }
table thead {
background: #F5F5F5; }
table thead tr th,
table thead tr td {
color: #222222;
font-size: 0.875rem;
font-weight: bold;
padding: 0.5rem 0.625rem 0.625rem; }
table tfoot {
background: #F5F5F5; }
table tfoot tr th,
table tfoot tr td {
color: #222222;
font-size: 0.875rem;
font-weight: bold;
padding: 0.5rem 0.625rem 0.625rem; }
table tr th,
table tr td {
color: #222222;
font-size: 0.875rem;
padding: 0.5625rem 0.625rem;
text-align: left; }
table tr.even, table tr.alt, table tr:nth-of-type(even) {
background: #F9F9F9; }
table thead tr th,
table tfoot tr th,
table tfoot tr td,
table tbody tr th,
table tbody tr td,
table tr td {
display: table-cell;
line-height: 1.125rem; }

DEPRECATED Please use the STEMCstudio version from the Examples.

Vectors and Mathematics

Student Learning Objectives

  • To understand the basic properties of vectors and scalars.
  • To understand the meaning of scalar multiplication on vectors.
  • To add and subtract vectors graphically, mathematically, and computationally, either geometrically (coordinate-free) or by using coordinates.
  • To decompose a vector into components.
  • To assemble a vector from a magnitude and direction.
  • To recognize and use the standard unit vectors.
  • To work with tilted coordinate systems.
  • To understand the meaning of a scalar and a vector field.
  • To understand the definition of a position vector.
  • To integrate units of measure into vector computation and mathematics.

Informal Approach

Launch the Program! Temporarily Hide the Code, if you need too. Use your mouse to rotate the scene.

You will see a red arrow that has been labeled $A$. A grid has also been drawn to make it easier to visualize the direction of the arrow. The arrow is perpendicular to the grid. Other than visualization, the grid has no significance. The origin or reference point has been labelled and coincides with the tail of the red arrow.

Physics loves Geometry!

In Physical Science we encounter quantities that, informally speaking, have a direction and magnitude. We call these quantities vectors. We also find quantities that have magnitude and orientation (sign), but no obvious direction. Such quantities we call scalars.

(I'm speaking very roughly here. That's what we have to do when we encounter new ideas. Our task will be to make these ideas more precise and useful. We'll also have to become acquainted with the formal terms that have been established if we want to communicate with others in the field. Finally, as we develop the mathematics and prove things we will become more rigorous).

The following table contains examples of some common vectors and scalars.

| quantity | type | |============|:=========== | |displacement|vector | |velocity |vector | |force |vector | |momentum |vector | |speed |scalar | |mass |scalar | |temperature |scalar |

We want to learn the precise mathematical rules for working with these quantities. By doing so we will be able describe Physical Laws and also perform computations. We will also find that many equations have a geometric interpretation, that is, there is a spatial picture that accompanies the mathematical quantities. By working with both the mathematical notation and the geometric interpretation we can have our intuition help us make advances in learning with the mathematics ensuring rigor so that we don't make mistakes.

Onto the Code!

The file index.ts contains a function called update which draws visual representations of vectors as arrows.

The following code is used to draw a visual representation of the vector $\vec{a}$.

  arrow.model = A
  arrow.label = "A"
  arrow.position = origin
  arrow.color = color.red
  arrow.draw()

Take a quick look at the contents of the index.ts. Don't worry if you don't understand how it all works.

Direction

Our arrow, labelled by A, is a visual representation of the mathematical vector quantity called A. The direction of the vector is given by the direction of the arrow with the arrowhead pointing in the positive direction. The arrowtail points in the negative direction. The imaginary line along the axis of the arrow is called the aspect. We could say that for a given aspect, a vector has two possible orientations.

Magnitude

The magnitude of the vector is given by the length of the arrow. Since the length can only be positive, the magnitude property is always a positive number.

Vector Algebra

In this unit you are going to learn the mathematical rules for manipulating equations involving vectors and scalars.

The precise definition of algebra won't concern us yet. For now let's stick with the informal idea that we are learning the rules for doing addition, subtraction, and multiplication with mathematical equations when scalars and vectors are involved.

Scalar Multiplication with a vector

The code representation of a scalar quantity in our program is simply going to be a JavaScript number. You probably know that you can add, subtract, multiply and divide these number quantities. But what we are going to do is to multiply a scalar and a vector together.

The code representation of a vector in our program is going to be a Vector3 from the EIGHT library. Later on we will learn how to design and code our own mathematical quantities.

Now suppose we multiply a number by a Vector3. Can you guess what the outcome is going to be? Try to guess what multiplying $A$ by each of the scalars $-1,-0.5,-2, 2, 0$ will produce as a result before attempting the following exercise.

Now try adding the following lines to the update function to visualize $-1 A$ as a blue arrow.

  arrow.model = -1 * A
  arrow.label = "-1 * A"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

Multiplication of a vector by $-1$ appears to result in a new vector which has the opposite direction to the original vector. More precisely, we would say that the new vector has the same magnitude (length), the same aspect (line), but an opposite orientation (sign).

Try multiplying by the other scalar values. What is the result of multiplying by a positive scalar? What is the result of multiplying by zero?

Uncomment the line that draws the gridXY to see the results more clearly.

We can write a shorthand for -1 * A using the unary minus operator -. A unary operator is a symbol where there is no operand on the left-hand-side. By contrast, in the expression -1 * A, the * operator is called a binary operator because it has an operand on both sides.

Let's try that shorthand in the code:

  arrow.model = -A
  arrow.label = "-A"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

Thinking about Scalars and Multiplication

So far we have described a vector as having the properties magnitude, aspect, and orientation. While a vector and scalar have been seen to be quite different objects, we know that a scalar, $\alpha$, does have both a magnitude, $|\alpha|$, and an orientation (sign).

Could a scalar have an aspect property too? Since a scalar has no obvious direction in the common sense usage, what kind of object might it be? Consider the following options:

  • point
  • line
  • plane
  • volume

Which one do you think is a good geometric interpretation?

It can't be a line because we have already assigned that to a vector and it clearly has a direction. It doesn't seem right to be a plane. We don't yet have a way to mathematically describe planes, but in any case, these seem to be associated with the concept of direction (a plane looks different if you tilt your head). We're left with point and volume. Let's just assign the simplest case, a point, and see if this geometric interpretation makes sense.

When a scalar and a vector are multiplied together, we can think of it in two ways (don't take these too literally, but consider them seriously because they will be the key to enlightenment!)

  1. The vector acts on the scalar "extending it" and making another vector.
  2. The scalar acts on the vector simply changing its magnitude and orientation, but not its aspect.

We complete the picture by adding the following two observations.

  • The magnitudes get multiplied together.
  • The orientations combine as expected for multiplying $+1$ and $-1$.

Does this geometric interpretation make sense when multiplying a scalar by a scalar?

We won't get to multiplying a vector by a vector yet but you might like to think about what kind of object (or objects) would be created.

Scalar Multiplication with a Vector Commutes.

Instead of computing, say 2 * A, compute and display A * 2.

Try it!

Do you get the same result?

You should, which leads to a new mathematical rule:

$$ \alpha A = A \alpha $$, where $\alpha$ is any scalar, and $A$ is any vector.

We say that multiplication involving a scalar and a vector is commutative, which is to say that we are allowed to swap the operands in such a case and nothing changes.

Vector Addition

Before we can add two vectors, we'll need another vector.

Let us add some similar code to draw the vector $B$.

  arrow.model = B
  arrow.label = "B"
  arrow.position = origin
  arrow.color = color.green
  arrow.draw()

Your diagram should now have two arrows, one red and one green, representing the vectors $A$ and $B$, respectively.

So far, we have stated that vectors are quantities with magnitude and direction. This has enabled us to represent our vectors in space as arrows.

We now investigate what happens when we add two vectors together.

Let's add some code to draw the sum of the two vectors and color it blue.

  arrow.model = A + B
  arrow.label = "A + B"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

Amazingly, we have been able to add two vectors together as easily as if they were ordinary scalar numbers (which they are not!). We'll learn how that works later. For now, let's study the diagram and try to deduce the rule that allows us to add two vectors together. Hint: Imagine sliding a copy each of $A$ and $B$ (always keeping them parallel to their original direction), so that a parallelogram is formed with $A + B$ as the diagonal.

Do you see it? We can parallel-translate (slide) a copy of $A$ so that its tail coincides with the point of $B$. We can also parallel-translate a copy of ${B}$ so that its tail is at the head of $A$.

Let's make it a bit easier on ourselves by actually drawing in these copies by adding the following code:

arrow.model = A
arrow.label = "A"
arrow.position = origin + B
arrow.color = color.red
arrow.draw()

arrow.model = B
arrow.label = "B"
arrow.position = origin + A
arrow.color = color.green
arrow.draw()

You should be looking at a nice parallelogram made of red and green vectors with blue diagonal. Got it? If not, your update code should look something like this:

function update() {
  /**
   * A shortcut to the origin defined on our World.
   */
  const origin = world.origin

  // Draw the vector A and label it.
  arrow.model = A
  arrow.label = "A"
  arrow.position = origin
  arrow.color = color.red
  arrow.draw()
  
  ////////////////////////////////////////////////////////////
  // Add more visualizations here...
  arrow.model = B
  arrow.label = "B"
  arrow.position = origin
  arrow.color = color.green
  arrow.draw()

  arrow.model = A + B
  arrow.label = "A + B"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

  arrow.model = A
  arrow.label = "A"
  arrow.position = origin + B
  arrow.color = color.red
  arrow.draw()

  arrow.model = B
  arrow.label = "B"
  arrow.position = origin + A
  arrow.color = color.green
  arrow.draw()

  // Draw the grid in the XY plane to get a sense of the magnitude and direction of the vectors.
  gridXY.draw()

  // Draw the grid normal to A to avoid becoming disorientated!
  gridZX.draw()
  
  // Label the origin.
  world.drawText("origin", origin)
}

Addition of vectors is commutative

We interpret $A + B$ as being resultant vector formed by sliding $B$ so that its tail coincides with the point of $A$.

To be consistent, we must interpret $B + A$ as being resultant vector formed by sliding $A$ so that its tail coincides with the point of $B$.

But the diagram demonstrates that we get the same result either way.

We can check that this is true by making a small change to our code to reverse the order of operands for the addition operator. Remeber to relabel the vector too!

  arrow.model = B + A
  arrow.label = "B + A"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

We can express this mathematically by writing

$$ A + B = B + A $$, for any two vectors, $A$ and $B$.

We say that addition of vectors is commutative.

Vector Subtraction

We now know the rules for adding vectors. Let's take a look at vector subtraction. Change the code involving the two vectors to compute and display $A - B$.

  arrow.model = A - B
  arrow.label = "A - B"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

Now take a look at the diagram.

What do we have to do with the arrows representing $A$ and $B$ in order to compute $A - B$?

It appears that we need to form the arrow that points in the opposite direction to $B$ and then add that to $A$.

But we know how to create a vector pointing in the opposite direction - multiply it by $-1$!

Let's change the code to see it work.

  arrow.model = A + (-B)
  arrow.label = "A + (-B)"
  arrow.position = origin
  arrow.color = color.blue
  arrow.draw()

And to make the picture complete, instead of drawing a copy of $B$ at the head of $A$, draw a copy of $-B$ at the head of $A$.

Congratulations! You now know how to subtract vectors. It all hinges upon the fact that every vector $V$ has an additive identity $-V$ such that $V + (-V) = 0$.

This allows us to rewrite $A - B$:

$$ \begin{eqnarray} A - B & = & A + 0 - B \\\ & = & A + (B + (-B)) - B \\\ & = & A + B + (-B) - B \\\ & = & A + (-B) + B - B \\\ & = & A + (-B) \end{eqnarray} $$

Decomposing a Vector

Start with code that displays only $A$ and ${B} in red and green respectively:

  arrow.model = A
  arrow.label = "A"
  arrow.position = origin
  arrow.color = color.red
  arrow.draw()

  arrow.model = B
  arrow.label = "B"
  arrow.position = origin
  arrow.color = color.green
  arrow.draw()
<!doctype html>
<html>
<head>
<!-- STYLES-MARKER -->
<style>
/* STYLE-MARKER */
</style>
<script src="https://jspm.io/system.js"></script>
<!-- SCRIPTS-MARKER -->
</head>
<body>
<canvas id='canvas3D'></canvas>
<canvas id='canvas2D'></canvas>
<div id='error'></div>
<script>
// CODE-MARKER
</script>
<script>
System.import('./index.js')
</script>
</body>
</html>
import {domReady, requestFrame} from './visual'
import {meter, kilogram, second, newton} from './units'
import {createArrow, Arrow} from './visual'
import {createBox, Box} from './visual'
import {createGridXY, createGridZX, Grid} from './visual'
import {sphere, Sphere} from './visual'
import {color} from './visual'
import {curve} from './visual'
import {World} from './visual'
const i = EIGHT.Vector3.e1()
const j = EIGHT.Vector3.e2()
const k = EIGHT.Vector3.e3()
/**
* A composite containing a camera, the canvas, a scene, etc.
*/
let world: World
/**
* A graphical arrow that will be used to visualize all vectors.
* This is done by moving it around (translation), by changing its color,
* and by changing the vector property that it is model that it represents.
*/
let arrow: Arrow
/**
* A grid in the xy-plane.
*/
let gridXY: Grid
/**
* A grid in the zx-plane.
*/
let gridZX: Grid
/**
* Vector A
*/
const A = 1.0 * j
/**
* Vector B
*/
const B = 1.2 * i + 0.4 * j + 0.0 * k
/**
* Vector C, a random vector.
*/
const C = Math.random() * i + Math.random() * j + Math.random() * k
/**
* Vector D, a random vector.
*/
const D = Math.random() * i + Math.random() * j + Math.random() * k
/*
const gui = new dat.GUI()
const folderA = gui.addFolder("A (vector)")
folderA.add(A, 'x', -2, +2)
folderA.add(A, 'y', -2, +2)
folderA.add(A, 'z', -2, +2)
folderA.open()
const folderB = gui.addFolder("B (vector)")
folderB.add(B, 'x', -2, +2)
folderB.add(B, 'y', -2, +2)
folderB.add(B, 'z', -2, +2)
folderB.open()
*/
/**
* The initialization function is called once, when the DOM has been loaded.
*/
function init(): void {
EIGHT.refChange('start')
world = new World()
// world.scaleFactor = meter
// world.camera.eye = 12 * world.scaleFactor * k
world.camera.eye = 6 * k
arrow = createArrow(world)
gridXY = createGridXY(world)
gridZX = createGridZX(world)
}
/**
* The update function is called repeatedly, for each animation frame.
*/
function update() {
/**
* A shortcut to the origin defined on our World.
*/
const origin = world.origin
// Draw the vector A and label it.
arrow.model = A
arrow.label = "A"
arrow.position = origin
arrow.color = color.red
arrow.draw()
// Draw the grid in the XY plane to get a sense of the magnitude and direction of the vectors.
// gridXY.draw()
// Draw the grid normal to A to avoid becoming disorientated!
gridZX.draw()
// Label the origin.
world.drawText("origin", origin)
}
/**
* The animation function is called repeatedly, as a callback for the browser.
*/
function animate() {
world.clear()
update()
requestFrame(animate)
}
/**
* When the DOM has been loaded, initialize this program.
* Then request a frame to get the animation going.
*/
domReady(function() {
init()
requestFrame(animate)
})
window.onunload = (e) => {
arrow.release()
gridXY.release()
gridZX.release()
world.release()
EIGHT.refChange('stop')
EIGHT.refChange('dump')
}
{
"description": "Vectors and Mathematics",
"dependencies": {
"davinci-eight": "2.317.0",
"davinci-units": "1.5.3",
"DomReady": "1.0.0",
"jquery": "2.1.4",
"stats.js": "0.16.0",
"dat-gui": "0.5.0"
},
"operatorOverloading": true,
"name": "copy-of-a-ball-in-a-box-with-units",
"version": "0.1.0",
"keywords": [
"visual",
"UNITS",
"G3",
"deprecated",
"mathdoodle"
],
"author": "David Geo Holmes"
}
body {
margin: 0;
overflow: hidden;
font-family: "Arial";
}
#canvas3D {
position: absolute;
left: 0px;
top: 0px;
z-index: 0;
width: 500px;
height: 500px;
}
#canvas2D {
position: absolute;
left: 0px;
top: 0px;
z-index: 10;
width: 500px;
height: 500px;
/**
* Allow events to go to the other elements
*/
pointer-events: none;
}
#error {
position: absolute;
left: 10px;
top: 10px;
z-index: 20;
}
{
"rules": {
"array-type": [
true,
"array"
],
"curly": false,
"comment-format": [
true,
"check-space"
],
"eofline": true,
"forin": true,
"jsdoc-format": true,
"no-conditional-assignment": false,
"no-consecutive-blank-lines": true,
"no-construct": true,
"no-for-in-array": true,
"no-magic-numbers": false,
"no-shadowed-variable": true,
"no-string-throw": true,
"no-trailing-whitespace": [
true,
"ignore-jsdoc"
],
"no-var-keyword": true,
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"prefer-const": true,
"prefer-for-of": true,
"prefer-function-over-method": false,
"radix": true,
"semicolon": [
true,
"never"
],
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": true,
"use-isnan": true
}
}
import {G3} from 'davinci-units'
export const unitless = G3.one
export const i = G3.e1
export const j = G3.e2
export const k = G3.e3
export const meter = G3.meter
export const kilogram = G3.kilogram
export const second = G3.second
export const newton = kilogram * meter / (second * second)
export const coulomb = G3.coulomb
/**
*
*/
export default class Vector {
constructor(public x: number, public y: number, public z: number, public uom: UNITS.Unit) {
}
}
import {Geometric3, GeometricE3, Color, Vector3} from 'davinci-eight'
import {G3} from 'davinci-units'
import {ShareableBase} from 'davinci-eight'
import {Engine, Capability, Renderable, Scene} from 'davinci-eight'
import {Facet, PerspectiveCamera, AmbientLight, DirectionalLight} from 'davinci-eight'
import {TrackballControls} from 'davinci-eight'
import {Diagram3D} from 'davinci-eight'
import {Arrow as EightArrow} from 'davinci-eight'
import {Box as EightBox} from 'davinci-eight'
import {meter, 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.
G3.BASIS_LABELS = G3.BASIS_LABELS_HAMILTON
/////////////////////////////////////////////////////////////////////////////
// Lighting
/**
* Ambient Lighting for the World.
*/
const ambLight = new AmbientLight(Color.white.scale(0.4))
/**
* Directional Lighting for the World.
*/
const dirLight = new DirectionalLight()
/////////////////////////////////////////////////////////////////////////////
// Standard Colors
/**
* The standard colors.
*/
export const color = {
red: Color.red,
green: Color.green,
blue: Color.blue,
yellow: Color.yellow,
magenta: Color.magenta,
cyan: Color.cyan,
orange: Color.fromRGB(1, 102 / 255, 0),
black: Color.black,
white: 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: 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: 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: Vector3;
/**
* The point that the camera is looking at, a position vector, measured in meters.
*/
look: G3;
/**
* The desired up direction, a dimensionless vector.
*/
up: G3;
}
/**
* A ready-to-go composite for EIGHT animations.
*/
export class World extends ShareableBase {
/**
* The scale factor for converting world units to dimensionless units.
*/
public scaleFactor: G3 = meter;
/**
* The frame of reference from which the world is viewed.
*/
public camera: Camera;
public engine: Engine;
public scene: Scene;
public ambients: Facet[] = [];
private trackball: TrackballControls;
private dimlessCamera: PerspectiveCamera;
public framecounter: number = 0;
public overlay: Diagram3D;
constructor() {
super()
this.setLoggingName('World')
// Notice that the canvas is "burned in".
this.engine = new Engine('canvas3D')
.clearColor(0.2, 0.2, 0.2, 1.0)
.enable(Capability.DEPTH_TEST)
// .enable(EIGHT.Capability.BLEND)
// .blendFunc(EIGHT.BlendingFactorSrc.SRC_ALPHA, EIGHT.BlendingFactorDest.ONE);
this.scene = new Scene(this.engine)
this.dimlessCamera = new 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 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 Diagram3D('canvas2D', this.dimlessCamera)
windowResize(this.engine, this.overlay, this.dimlessCamera).resize()
}
protected destructor(levelUp: number) {
this.trackball.release()
this.scene.release()
this.engine.release()
super.destructor(levelUp + 1)
}
/**
* The underlying HTML5 Canvas.
*/
get canvas(): HTMLCanvasElement {
return this.engine.canvas;
}
/**
* The origin is fixed to be zero.
*/
get origin(): Vector3 {
return Vector3.e3().scale(0)
// return 0 * this.scaleFactor
}
/**
* Adds a drawable object to the world.
*/
add(drawable: 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 extends ShareableBase {
private _scaleFactor: UNITS.G3 = meter;
private _label: string;
private inner: EIGHT.Arrow;
constructor(private world: World) {
super()
this.setLoggingName('Arrow')
this.inner = new EightArrow(world.engine)
world.add(this.inner)
}
protected destructor(levelUp: number) {
this.inner.release()
super.destructor(levelUp + 1)
}
get color() {
return this.inner.color;
}
set color(color: 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: G3) {
if (isScalar(value)) {
this._scaleFactor = value;
}
else {
throw new Error(`Arrow.scaleFactor property must be a scalar.`);
}
}
get model() {
return Vector3.copy(this.inner.vector)
// return UNITS.G3.copy(this.inner.h).mul(this.scaleFactor)
}
set model(value: Vector3) {
this.inner.vector = 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 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 G3) {
that.scaleFactor = options.scaleFactor;
}
else {
throw new Error("pos option must have type UNITS.G3");
}
}
if (options.color) {
if (options.color instanceof Color) {
that.color = options.color;
}
else {
throw new Error("color property must have type EIGHT.Color");
}
}
return that;
}
/**
*
*/
export class Box {
public scaleFactor: G3 = meter;
private inner: EightBox;
constructor(private world: World) {
this.inner = new EightBox(world.engine, {mode: 'wire'})
this.scaleFactor = world.scaleFactor
world.add(this.inner)
}
get color() {
return this.inner.color;
}
set color(color: Color) {
this.inner.color = color;
}
get width() {
return G3.scalar(this.inner.width, this.scaleFactor.uom)
}
set width(value: 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 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 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 G3.copy(this.inner.X).mul(this.world.scaleFactor);
}
set pos(value: 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 = value
}
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 G3) {
that.pos = options.pos;
}
else {
throw new Error("pos option must have type UNITS.G3");
}
}
if (options.color) {
if (options.color instanceof 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.engine);
world.add(this.inner)
}
get color() {
return this.inner.color;
}
set color(color: EIGHT.Color) {
this.inner.color = color;
}
get axis() {
return UNITS.G3.fromVector(this.inner.axis)
}
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 extends EIGHT.ShareableBase {
constructor(private world: World, private inner: EIGHT.Grid) {
super()
this.setLoggingName('Grid')
world.add(inner)
}
protected destructor(levelUp: number) {
super.destructor(levelUp + 1)
}
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 g = new EIGHT.GridXY(world.engine);
const that = new Grid(world, g);
g.release();
return that;
}
export function createGridZX(world: World) {
const g = new EIGHT.GridZX(world.engine);
const that = new Grid(world, g);
g.release();
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(world.engine)
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(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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment