WebGL meets Turtle Geometry.
Based upon ideas in Turtle Geometry by Harold Abelson and Andrea diSessa.
const FLOATS_PER_VERTEX = 3; | |
const BYTES_PER_FLOAT = 4; | |
const STRIDE = BYTES_PER_FLOAT * FLOATS_PER_VERTEX; | |
function primitive(): EIGHT.Primitive { | |
const aPosition: EIGHT.Attribute = { | |
values: [ | |
[0,+0.0,0, 0,-0.3,0, -0.3,0,0, +0.3,0,0], // center | |
[0,+1,0, -1,-1,0], // LHS | |
[0,+1,0, +1,-1,0], // RHS | |
[-1,-1,0, +1,-1,0] // BASE | |
].reduce(function(a,b){return a.concat(b)}), | |
size: 3, | |
type: EIGHT.DataType.FLOAT | |
}; | |
const aColor: EIGHT.Attribute = { | |
values: [ | |
[1,1,1, 1,1,1, 1,1,1, 1,1,1], // center | |
[0,1,0, 0,1,0], // LHS | |
[1,0,0, 1,0,0], // RHS | |
[0,1,1, 0,1,1] // BASE | |
].reduce(function(a,b){return a.concat(b)}), | |
size: 3, | |
type: EIGHT.DataType.FLOAT | |
}; | |
const result: EIGHT.Primitive = { | |
mode: EIGHT.BeginMode.LINES, | |
attributes: { | |
} | |
}; | |
result.attributes['aPosition'] = aPosition; | |
result.attributes['aColor'] = aColor; | |
return result; | |
} | |
/** | |
* The geometry of the Bug is static so we use the conventional | |
* approach based upon GeometryArrays | |
*/ | |
class BugGeometry extends EIGHT.GeometryArrays { | |
private w = 1 | |
private h = 1 | |
private d = 1 | |
constructor(private contextManager: EIGHT.ContextManager) { | |
super(primitive(), contextManager); | |
} | |
getPrincipalScale(name: string): number { | |
switch (name) { | |
case 'width': { | |
return this.w | |
} | |
case 'height': { | |
return this.h | |
} | |
case 'depth': { | |
return this.d | |
} | |
default: { | |
throw new Error(`getPrincipalScale(${name}): name is not a principal scale property.`) | |
} | |
} | |
} | |
setPrincipalScale(name: string, value: number): void { | |
switch (name) { | |
case 'width': { | |
this.w = value | |
} | |
break | |
case 'height': { | |
this.h = value | |
} | |
break | |
case 'depth': { | |
this.d = value | |
} | |
break | |
default: { | |
throw new Error(`setPrinciplaScale(${name}): name is not a principal scale property.`) | |
} | |
} | |
this.setScale(this.w, this.h, this.d) | |
} | |
} | |
const mops: EIGHT.LineMaterialOptions = { | |
attributes: {}, | |
uniforms: {}, | |
}; | |
mops.attributes['aPosition'] = 3; | |
mops.attributes['aColor'] = 3; | |
mops.uniforms['uModel'] = 'mat4'; | |
mops.uniforms['uProjection'] = 'mat4'; | |
mops.uniforms['uView'] = 'mat4'; | |
export default class Bug extends EIGHT.RigidBody { | |
constructor(contextManager: EIGHT.ContextManager) { | |
super(new BugGeometry(contextManager), new EIGHT.LineMaterial(mops, contextManager), contextManager, {x:0,y:0,z:1}) | |
this.height = 1; | |
this.width = 1; | |
} | |
get width() { | |
return this.getPrincipalScale('width'); | |
} | |
set width(width: number) { | |
this.setPrincipalScale('width', width); | |
} | |
/** | |
* | |
*/ | |
get height() { | |
return this.getPrincipalScale('height'); | |
} | |
set height(height: number) { | |
this.setPrincipalScale('height', height); | |
} | |
} |
import Steppable from './Steppable'; | |
import Stepper from './Stepper'; | |
export default class Forward implements Steppable { | |
constructor(private body: EIGHT.RigidBody, private distance: number) { | |
} | |
stepper(): Stepper { | |
return new ForwardStepper(this.body, this.distance); | |
} | |
} | |
class ForwardStepper implements Stepper { | |
private todo = true; | |
constructor(private body: EIGHT.RigidBody, private distance: number) { | |
} | |
hasNext(): boolean { | |
return this.todo; | |
} | |
next(): void { | |
if (this.todo) { | |
// We're assuming that P is a heading unit vector. | |
// We need to change out the model representation. | |
this.body.X.add(this.body.P, this.distance); | |
this.todo = false; | |
} | |
} | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<!-- STYLES-MARKER --> | |
<style> | |
/* STYLE-MARKER */ | |
</style> | |
<script src='https://jspm.io/system.js'></script> | |
<!-- SHADERS-MARKER --> | |
<!-- SCRIPTS-MARKER --> | |
</head> | |
<body> | |
<canvas id='my-canvas'></canvas> | |
<script> | |
// CODE-MARKER | |
</script> | |
<script> | |
System.import('./index.js') | |
</script> | |
</body> | |
</html> |
import Bug from './Bug'; | |
import Forward from './Forward' | |
import Repeat from './Repeat' | |
import Rotate from './Rotate' | |
import Steppable from './Steppable'; | |
import SteppableList from './SteppableList'; | |
import Track from './Track'; | |
import World from './World' | |
const e1 = EIGHT.Geometric3.e1() | |
const e2 = EIGHT.Geometric3.e2() | |
const e3 = EIGHT.Geometric3.e3() | |
/** | |
* The space will have a natural scale of [-SIZE, SIZE]. | |
*/ | |
const SIZE = 2; | |
/** | |
* A brave new World. | |
*/ | |
const world = new World(SIZE) | |
const body = new Bug(world.engine) | |
body.height = 0.1 | |
body.width = 0.0618 | |
// body.color = new EIGHT.Color(0.8984, 0.1133, 0.3711) | |
const track = new Track(world.engine); | |
// const gridXY = world.createGridXY({segments: 20, min: -SIZE/2, max: +SIZE/2}) | |
// gridXY.color = new EIGHT.Color(0.4, 0.4, 0.4) | |
world.reset() | |
world.planView() | |
/** | |
* Use to control the speed of the animation. | |
* Roughly the number of frames used to execute a motion step. | |
*/ | |
const N = 1; | |
function FORWARD(distance: number) { | |
return new Repeat(new Forward(body, distance / N), N); | |
} | |
/** | |
* θ: The angle of heading change counter-clockwise in degrees. | |
*/ | |
function LEFT(θ: number) { | |
const angle = θ * Math.PI / 180; | |
return new Repeat(new Rotate(body, e1 ^ e2, angle / N), N) | |
} | |
const list = new SteppableList() | |
list.add(FORWARD(0.1)) | |
list.add(LEFT(10)) | |
const program = new Repeat(list, 36) | |
let stepper = program.stepper(); | |
const animate = function() { | |
world.beginFrame() | |
if (world.time === 0) { | |
// Initialize | |
body.X.scale(0) | |
body.P.copy(e2).normalize() | |
track.erase() | |
track.addPoint(body.X.x, body.X.y, body.X.z) | |
// We also need a new stepper. | |
stepper = program.stepper() | |
} | |
if (world.running && stepper.hasNext()) { | |
// We'll count steps. | |
// All that matters is that time moves on from zero so we can reset. | |
world.time = world.time + 1; | |
stepper.next(); | |
track.addPoint(body.X.x, body.X.y, body.X.z) | |
} | |
body.render(world.ambients) | |
track.render(world.ambients) | |
world.draw() | |
// This call keeps the animation going. | |
requestAnimationFrame(animate) | |
} | |
// This call starts the animation. | |
requestAnimationFrame(animate) |
{ | |
"description": "Turtle Geometry", | |
"dependencies": { | |
"DomReady": "1.0.0", | |
"jasmine": "2.4.1", | |
"davinci-eight": "2.245.0", | |
"dat-gui": "0.5.0", | |
"stats.js": "0.16.0" | |
}, | |
"name": "copy-of-copy-of-Vector Modeling Problem Framework", | |
"version": "0.1.0", | |
"keywords": [ | |
"EIGHT", | |
"project", | |
"Getting", | |
"Started", | |
"WebGL" | |
], | |
"operatorOverloading": true | |
} |
import Steppable from './Steppable'; | |
import Stepper from './Stepper'; | |
export default class Repeat implements Steppable { | |
constructor(private steppable: Steppable, private N: number) { | |
} | |
stepper(): Stepper { | |
return new RepeatStepper(this.steppable, this.N); | |
} | |
} | |
class RepeatStepper implements Stepper { | |
private i = 0; | |
private stepper: Stepper; | |
constructor(private steppable: Steppable, private N: number) { | |
this.stepper = steppable.stepper(); | |
} | |
hasNext() { | |
if (this.stepper.hasNext()) { | |
return true; | |
} | |
else { | |
if (this.i < this.N - 1) { | |
this.i++; | |
this.stepper = this.steppable.stepper(); | |
return this.hasNext(); | |
} | |
else { | |
return false; | |
} | |
} | |
} | |
next(): void { | |
if (this.hasNext()) { | |
this.stepper.next() | |
} | |
} | |
} |
import Steppable from './Steppable' | |
import Stepper from './Stepper' | |
/** | |
* Scratch variable to avoid creating temporary objects. | |
*/ | |
const R = EIGHT.Geometric3.one() | |
export default class Rotate implements Steppable { | |
constructor(private body: EIGHT.RigidBody, private B: EIGHT.Geometric3, private θ: number) { | |
} | |
stepper(): Stepper { | |
return new RotateStepper(this.body, this.B, this.θ); | |
} | |
} | |
class RotateStepper implements Stepper { | |
private todo = true; | |
constructor(private body: EIGHT.RigidBody, private B: EIGHT.Geometric3, private θ: number) { | |
} | |
hasNext() { | |
return this.todo; | |
} | |
next(): void { | |
if (this.todo) { | |
this.todo = false; | |
R.rotorFromGeneratorAngle(this.B, this.θ); | |
this.body.P.rotate(R); | |
this.body.R = R * this.body.R; | |
} | |
} | |
} |
import Stepper from './Stepper'; | |
/** | |
* Capable of being traversed or controlled by steps. | |
*/ | |
interface Steppable { | |
stepper(): Stepper; | |
} | |
export default Steppable; |
import Steppable from './Steppable'; | |
import Stepper from './Stepper'; | |
export default class SteppableList implements Steppable { | |
private steppables: Steppable[] = []; | |
constructor() { | |
} | |
stepper(): Stepper { | |
return new StepperList(this.steppables.map(function(steppable){return steppable.stepper();})); | |
} | |
add(steppable: Steppable): void { | |
this.steppables.push(steppable); | |
} | |
} | |
class StepperList implements Stepper { | |
private i = 0; | |
private N: number; | |
constructor(private steppers: Stepper[]) { | |
this.N = steppers.length; | |
} | |
hasNext(): boolean { | |
if (this.i < this.N) { | |
if (this.steppers[this.i].hasNext()) { | |
return true; | |
} | |
else { | |
this.i++; | |
return this.hasNext(); | |
} | |
} | |
else { | |
return false; | |
} | |
} | |
next(): void { | |
if (this.hasNext()) { | |
this.steppers[this.i].next(); | |
} | |
} | |
} | |
/** | |
* A device that moves or rotates in a series of small discrete steps. | |
*/ | |
interface Stepper { | |
hasNext(): boolean; | |
next(): void; | |
} | |
export default Stepper; |
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 Vector3 from './Vector3.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("Vector3", Vector3) | |
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 | |
} |
const FLOATS_PER_VERTEX = 3; | |
const BYTES_PER_FLOAT = 4; | |
const STRIDE = BYTES_PER_FLOAT * FLOATS_PER_VERTEX; | |
/** | |
* | |
*/ | |
class LineGeometry implements EIGHT.Geometry { | |
scaling = EIGHT.Matrix4.one(); | |
private data: Float32Array; | |
private count = 0; | |
private N = 2; | |
private dirty = true; | |
private vbo: EIGHT.VertexBuffer; | |
private refCount = 1; | |
private contextProvider: EIGHT.ContextProvider; | |
constructor(private contextManager: EIGHT.ContextManager) { | |
this.data = new Float32Array(this.N * FLOATS_PER_VERTEX); | |
this.vbo = new EIGHT.VertexBuffer(contextManager); | |
} | |
bind(material: EIGHT.Material): LineGeometry { | |
if (this.dirty) { | |
this.vbo.bufferData(this.data, EIGHT.Usage.DYNAMIC_DRAW); | |
this.dirty = false; | |
} | |
this.vbo.bind(); | |
const aPosition = material.getAttrib('aPosition'); | |
aPosition.config(FLOATS_PER_VERTEX, EIGHT.DataType.FLOAT, true, STRIDE, 0); | |
aPosition.enable(); | |
return this; | |
} | |
unbind(material: EIGHT.Material): LineGeometry { | |
const aPosition = material.getAttrib('aPosition'); | |
aPosition.disable(); | |
this.vbo.unbind() | |
return this; | |
} | |
draw(material: EIGHT.Material): LineGeometry { | |
// console.log(`LineGeometry.draw(${this.i})`) | |
this.contextProvider.gl.drawArrays(EIGHT.BeginMode.LINE_STRIP, 0, this.count); | |
return this; | |
} | |
getPrincipalScale(name: string): number { | |
throw new Error("LineGeometry.getPrincipalScale"); | |
} | |
hasPrincipalScale(name: string): boolean { | |
throw new Error("LineGeometry.hasPrincipalScale"); | |
} | |
setPrincipalScale(name: string, value: number): void { | |
throw new Error("LineGeometry.setPrincipalScale"); | |
} | |
contextFree(contextProvider: EIGHT.ContextProvider): void { | |
this.vbo.contextFree(contextProvider); | |
} | |
contextGain(contextProvider: EIGHT.ContextProvider): void { | |
this.contextProvider = contextProvider; | |
this.vbo.contextGain(contextProvider); | |
} | |
contextLost(): void { | |
this.vbo.contextLost(); | |
} | |
addRef(): number { | |
this.refCount++; | |
return this.refCount; | |
} | |
release(): number { | |
this.refCount--; | |
if (this.refCount === 0) { | |
// Clean Up | |
} | |
return this.refCount; | |
} | |
addPoint(x: number, y: number, z: number): void { | |
if (this.count === this.N) { | |
this.N = this.N * 2; | |
const temp = new Float32Array(this.N * FLOATS_PER_VERTEX); | |
temp.set(this.data) | |
this.data = temp; | |
} | |
const offset = this.count * FLOATS_PER_VERTEX; | |
this.data[offset + 0] = x; | |
this.data[offset + 1] = y; | |
this.data[offset + 2] = z; | |
this.count++; | |
this.dirty = true; | |
} | |
erase(): void { | |
this.count = 0; | |
} | |
} | |
export default class Track extends EIGHT.Mesh { | |
constructor(contextManager: EIGHT.ContextManager) { | |
super(new LineGeometry(contextManager), new EIGHT.LineMaterial(void 0, contextManager), contextManager) | |
} | |
addPoint(x: number, y: number, z: number): void { | |
const geometry = <LineGeometry>this.geometry; | |
geometry.addPoint(x, y, z); | |
geometry.release(); | |
} | |
erase(): void { | |
const geometry = <LineGeometry>this.geometry; | |
geometry.erase(); | |
geometry.release(); | |
} | |
} |
import Vector3 from './Vector3'; | |
export default function() { | |
describe("constructor", function() { | |
const x = Math.random(); | |
const y = Math.random(); | |
const z = Math.random(); | |
const v = new Vector3(x, y, z); | |
it("should initialize x-coordinate", function() { | |
expect(v.x).toBe(x) | |
}) | |
}) | |
} |
export default class Vector3 { | |
constructor(public x = 0, public y = 0, public z = 0) { | |
} | |
} |
const origin = EIGHT.Geometric3.vector(0, 0, 0) | |
const e1 = EIGHT.Geometric3.vector(1, 0, 0) | |
const e2 = EIGHT.Geometric3.vector(0, 1, 0) | |
const e3 = EIGHT.Geometric3.vector(0, 0, 1) | |
export default class World { | |
public engine: EIGHT.Engine; | |
private scene: EIGHT.Scene; | |
public ambients: EIGHT.Facet[] = []; | |
private camera: EIGHT.PerspectiveCamera; | |
private trackball: EIGHT.TrackballControls; | |
private dirLight: EIGHT.DirectionalLight; | |
private gui: dat.GUI; | |
/** | |
* An flag that determines whether the simulation should move forward. | |
*/ | |
public running = false; | |
/** | |
* Universal Newtonian Time. | |
*/ | |
public time = 0; | |
/** | |
* Creates a new Worls containg a WebGL canvas, a camera, lighting, | |
* and controllers. | |
*/ | |
constructor(private size: number) { | |
this.engine = new EIGHT.Engine('my-canvas') | |
.size(500, 500) | |
.clearColor(0.1, 0.1, 0.1, 1.0) | |
.enable(EIGHT.Capability.DEPTH_TEST); | |
this.engine.gl.lineWidth(1) | |
this.scene = new EIGHT.Scene(this.engine); | |
this.camera = new EIGHT.PerspectiveCamera(); | |
this.ambients.push(this.camera) | |
this.dirLight = new EIGHT.DirectionalLight(); | |
this.ambients.push(this.dirLight) | |
this.trackball = new EIGHT.TrackballControls(this.camera, window) | |
// Subscribe to mouse events from the canvas. | |
this.trackball.subscribe(this.engine.canvas) | |
this.gui = new dat.GUI({name: 'Yahoo'}); | |
const simFolder = this.gui.addFolder("Simulation") | |
simFolder.add(this, 'start'); | |
simFolder.add(this, 'stop'); | |
simFolder.add(this, 'reset'); | |
simFolder.open(); | |
const cameraFolder = this.gui.addFolder("Camera") | |
cameraFolder.add(this, 'planView'); | |
cameraFolder.add(this, 'sideView'); | |
cameraFolder.open(); | |
this.sideView(); | |
} | |
/** | |
* This method should be called at the beginning of an animation frame. | |
* It performs the following tasks: | |
* 1. Clears the graphics output. | |
* 2. Updates the camera based upon movements of the mouse. | |
* 3. Aligns the directional light with the viewing direction. | |
*/ | |
beginFrame(): void { | |
this.engine.clear(); | |
// Update the camera based upon mouse events received. | |
this.trackball.update(); | |
// Keep the directional light pointing in the same direction as the camera. | |
this.dirLight.direction.copy(this.camera.look).sub(this.camera.eye) | |
} | |
/** | |
* This method should be called after objects have been moved. | |
*/ | |
draw(): void { | |
this.scene.draw(this.ambients); | |
} | |
/** | |
* Puts the simulation into the running state. | |
*/ | |
start(): void { | |
this.running = true | |
} | |
stop(): void { | |
this.running = false | |
} | |
/** | |
* Resets the universal time property back to zero. | |
*/ | |
reset(): void { | |
this.running = false | |
this.time = 0 | |
} | |
planView(): void { | |
this.camera.eye.copy(e3).normalize().scale(this.size * 1.4) | |
this.camera.look.copy(origin) | |
this.camera.up.copy(e2) | |
} | |
sideView(): void { | |
this.camera.eye.sub2(e3, e2).normalize().scale(this.size * 1.6) | |
this.camera.look.copy(origin) | |
this.camera.up.copy(e3) | |
} | |
/** | |
* Convenience method for creating a grid in the xy-plane. | |
*/ | |
createGridXY(options: { | |
segments?: number; | |
min?: number; | |
max?: number; | |
} = {}): EIGHT.Grid { | |
const grid = new EIGHT.Grid({ | |
uSegments: options.segments, | |
uMin: options.min, | |
uMax: options.max, | |
vSegments: options.segments, | |
vMin: options.min, | |
vMax: options.max | |
}); | |
this.scene.add(grid); | |
return grid; | |
} | |
createBox(): EIGHT.Box { | |
const box = new EIGHT.Box(); | |
this.scene.add(box); | |
return box; | |
} | |
/** | |
* Convenience method for creating a sphere. | |
*/ | |
createSphere(options: {radius?: number} = {}): EIGHT.Sphere { | |
const sphere = new EIGHT.Sphere({radius: options.radius}); | |
this.scene.add(sphere); | |
return sphere; | |
} | |
} |