The idea for this program comes from the book:
Turtle Geometry
The Computer as a medium for Exploring Mathematics
Harold Abelson and Andrea diSessa.
/** | |
* Heading | |
*/ | |
const INDEX_H = 0; | |
/** | |
* Left | |
*/ | |
const INDEX_L = 1; | |
/** | |
* Up. | |
*/ | |
const INDEX_X = 2; | |
export default class Bug { | |
public X: EIGHT.Geometric3 = EIGHT.Geometric3.zero() | |
public _frame: EIGHT.Geometric3[] = []; | |
constructor() { | |
this._frame[INDEX_H] = EIGHT.Geometric3.zero(); | |
this._frame[INDEX_L] = EIGHT.Geometric3.zero() | |
this._frame[INDEX_X] = EIGHT.Geometric3.zero(); | |
} | |
get frame(): EIGHT.VectorE3[] { | |
// Return a copy. | |
return this._frame.map(function(e){ return e.clone(); }); | |
} | |
set frame(frame: EIGHT.VectorE3[]) { | |
// Copy the frame parameter. | |
for(let i = 0; i < 3; i++) { | |
this._frame[i] = EIGHT.Geometric3.fromVector(frame[i]); | |
} | |
} | |
forward(distance: number): EIGHT.Geometric3 { | |
const H = this._frame[INDEX_H]; | |
const realWalk = H * distance; | |
this.X = this.X + realWalk; | |
return realWalk; | |
} | |
rotate(R: EIGHT.Geometric3): void { | |
this._frame[INDEX_H].rotate(R); | |
this._frame[INDEX_L].rotate(R); | |
this._frame[INDEX_X].rotate(R); | |
} | |
} |
<!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 World from './World' | |
import {gapRatio,e_r,e_θ,turningPerAngle,r} from './surface' | |
const origin = EIGHT.Geometric3.zero() | |
const e1 = EIGHT.Geometric3.e1() | |
const e2 = EIGHT.Geometric3.e2() | |
const e3 = EIGHT.Geometric3.e3() | |
/** | |
* The initial frame sets the orientation. | |
*/ | |
const INITIAL_FRAME = [e2, -e1, e3] | |
/** | |
* | |
*/ | |
const world = new World() | |
const engine = world.engine; | |
const bug = new Bug() | |
const viz = new EIGHT.Turtle({engine}) | |
const track = new EIGHT.Track({engine}) | |
track.color = EIGHT.Color.white | |
const arrow = new EIGHT.Arrow({engine}) | |
const grid = new EIGHT.GridXY({engine}) | |
world.reset() | |
world.sideView() | |
const animate = function() { | |
world.beginFrame() | |
if (world.time === 0) { | |
bug.X = e1 | |
bug.frame = INITIAL_FRAME | |
// Erasing and adding the start point means we only have one point upon reset. | |
track.clear() | |
track.addPoint(bug.X) | |
} | |
if (world.running) { | |
// We'll count steps. | |
// All that matters is that time moves on from zero so we can reset. | |
world.time = world.time + 1 | |
const realWalk = bug.forward(0.01) | |
// bug.rotate((e1^e2).scale(-0.005).exp()) | |
const s = e_θ(bug.X); | |
const leap = gapRatio(bug.X) * (realWalk | s) * s; | |
const walk = realWalk + leap | |
const angle = (1 / r(bug.X) * (walk|s).a); | |
const demonTurningAngle = angle * turningPerAngle(r(bug.X)) | |
const demonRotor = (e1 ^ e2).scale(-demonTurningAngle/2).exp() | |
bug.X = bug.X + leap | |
bug.rotate(demonRotor) | |
track.addPoint(bug.X) | |
} | |
// Render the bug as a triangle. | |
viz.X.copyVector(bug.X) | |
viz.R.rotorFromFrameToFrame(INITIAL_FRAME, bug.frame) | |
viz.render(world.ambients) | |
// Render the track of the bug. | |
track.render(world.ambients) | |
grid.render(world.ambients) | |
arrow.X.copy(bug.X) | |
arrow.h.copy(e_r(bug.X)) | |
arrow.color = EIGHT.Color.red | |
arrow.render(world.ambients) | |
arrow.h.copy(e_θ(bug.X)) | |
arrow.color = EIGHT.Color.blue | |
arrow.render(world.ambients) | |
// This call keeps the animation going. | |
requestAnimationFrame(animate) | |
} | |
// This call starts the animation. | |
requestAnimationFrame(animate) |
{ | |
"description": "Demon Turning", | |
"dependencies": { | |
"DomReady": "1.0.0", | |
"jasmine": "2.4.1", | |
"davinci-eight": "2.302.0", | |
"dat-gui": "0.5.0", | |
"stats.js": "0.16.0" | |
}, | |
"name": "copy-of-copy-of-copy-of-local-geometry-on-a-sphere", | |
"version": "0.1.0", | |
"keywords": [ | |
"EIGHT", | |
"project", | |
"Getting", | |
"Started", | |
"WebGL", | |
"Local", | |
"Geometric", | |
"Physics" | |
], | |
"operatorOverloading": true, | |
"author": "David Geo Holmes" | |
} |
body { | |
background-color: white; | |
} |
const e1 = EIGHT.Geometric3.e1(); | |
const e2 = EIGHT.Geometric3.e2(); | |
const rotor = EIGHT.Geometric3.one(); | |
/** | |
* The radis of the sphere. | |
*/ | |
const R = 2; | |
export function gapRatio(X: EIGHT.Geometric3): number { | |
const d = r(X); | |
return (2 * Math.PI * d / C(d)) - 1; | |
} | |
export function e_r(X: EIGHT.Geometric3): EIGHT.Geometric3 { | |
rotor.rotorFromDirections(e1, X); | |
return e1.clone().rotate(rotor); | |
} | |
export function e_θ(X: EIGHT.Geometric3): EIGHT.Geometric3 { | |
return e_r(X).rotate((e1^e2).scale(-Math.PI/4).exp()); | |
} | |
export function r(X: EIGHT.Geometric3): number { | |
return Math.sqrt(X.x * X.x + X.y * X.y); | |
} | |
function C(r: number) { | |
return 2 * Math.PI * Math.sin(r / R); | |
} | |
export function turningPerAngle(r: number) { | |
return 1 - Math.cos(r / R); | |
} |
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() { | |
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.trackball.noPan = true | |
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(e2).scale(3) | |
this.camera.look.copy(origin) | |
this.camera.up.copy(-e3) | |
} | |
sideView(): void { | |
this.camera.eye.copy(e3).scale(3) | |
this.camera.look.copy(origin) | |
this.camera.up.copy(e2) | |
} | |
} |