Skip to content

Instantly share code, notes, and snippets.

@stemcstudio
Last active August 10, 2016 11:31
Show Gist options
  • Save stemcstudio/f4f595ede86a4797e34ec6c840cbb535 to your computer and use it in GitHub Desktop.
Save stemcstudio/f4f595ede86a4797e34ec6c840cbb535 to your computer and use it in GitHub Desktop.
Bug Geometry on a Sphere

Bug Geometry on a Sphere

Overview

The bug lives on the surface of a unit sphere and can walk forward and rotate in the plane tangent to the sphere at the bugs' location.

The gray triangle represents the bug. The green arrow is a reference pointer that the bug leaves behind at the start of the trip. The blue arrow is a pointer that the bug pushes around its path without turning.

The program can be used to explore the consequences of curvature on a sphere.

Credits

The idea for this program comes from the book:

Turtle Geometry

The Computer as a medium for Exploring Mathematics

Harold Abelson and Andrea diSessa.

/**
* Scratch variable for implementing the rotate method.
*/
const R = EIGHT.Geometric3.zero();
/**
* Heading
*/
const INDEX_H = 0;
/**
* Left
*/
const INDEX_L = 1;
/**
* Position (on unit sphere) and Up.
*/
const INDEX_X = 2;
export default class Bug {
public pointer = 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 X(): EIGHT.Geometric3 {
return this._frame[INDEX_X];
}
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]);
}
}
/**
* Moves the bug forward by the specified (angular) distance.
*/
public forward(θ: number): Bug {
const X = this._frame[INDEX_X];
const H = this._frame[INDEX_H];
R.copy(X).mul(H).scale(-θ / 2).exp();
X.rotate(R);
H.rotate(R);
this.pointer.rotate(R);
return this;
}
/**
* Rotate towards left.
*/
public left(θ: number): Bug {
const H = this._frame[INDEX_H];
const L = this._frame[INDEX_L];
R.copy(H).mul(L).scale(-θ / 2).exp();
H.rotate(R);
L.rotate(R);
// The pointer does not move when we rotate.
return this;
}
/**
* Rotate towards right.
*/
public right(θ: number): Bug {
return this.left(-θ);
}
public rotate(B: EIGHT.BivectorE3, θ: number): Bug {
R.rotorFromGeneratorAngle(B, θ);
// Rotate all of the frame vectors.
for(let i = 0; i < 3; i++) {
this._frame[i].rotate(R);
}
return this;
}
}
import Bug from './Bug';
import Steppable from './Steppable';
import Stepper from './Stepper';
export default class Forward implements Steppable {
constructor(private bug: Bug, private distance: number) {
}
stepper(): Stepper {
return new ForwardStepper(this.bug, this.distance);
}
}
class ForwardStepper implements Stepper {
private todo = true;
constructor(private bug: Bug, private distance: number) {
}
hasNext(): boolean {
return this.todo;
}
next(): void {
if (this.todo) {
this.todo = false;
this.bug.forward(this.distance);
}
}
}
<!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 Triangle from './Triangle'
import Forward from './Forward'
import Repeat from './Repeat'
import Left from './Left'
import Right from './Right'
import Rotate from './Rotate'
import SphericalPolars from './SphericalPolars'
import Steppable from './Steppable'
import SteppableList from './SteppableList'
import Track from './Track'
import World from './World'
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, but since the position and
* up direction are the same on a sphere, this also sets the position.
*/
const INITIAL_FRAME = [e2, -e1, e3]
const INITIAL_POINTER = e2
/**
* A brave new World.
*/
const world = new World()
const bug = new Bug()
const viz = new Triangle(world.engine)
viz.height = 0.1
viz.width = 0.0618
viz.color = EIGHT.Color.white
const coords = new SphericalPolars(world.engine)
coords.color = EIGHT.Color.gray
const track = new Track(world.engine)
track.color = EIGHT.Color.gray
track.color = EIGHT.Color.white
const arrow = new EIGHT.Arrow()
arrow.subscribe(world.engine)
arrow.synchUp()
world.reset()
world.sideView()
/**
* Use to control the speed of the animation.
* Roughly the number of frames used to execute a motion step.
*/
const FRAMES_PER_STEP = 50
const ONE_TURN = 2 * Math.PI
const QUARTER_TURN = ONE_TURN / 4
/**
* Divisor for the movement and turning angle.
*/
const N = 1 // must be an integer
/**
* Ratio of forward movement to turn angle.
*/
const ρ = 1
/**
* Distance that we move forward.
*/
const s = ρ * QUARTER_TURN / N
/**
* Angle that we turn through.
*/
const θ = QUARTER_TURN / N
/**
* The number of repetitions.
*/
const REPETITIONS = 3 * N
const list = new SteppableList()
list.add(new Repeat(new Forward(bug, s / FRAMES_PER_STEP), FRAMES_PER_STEP))
list.add(new Repeat(new Left(bug, -θ / FRAMES_PER_STEP), FRAMES_PER_STEP))
const program = new Repeat(list, REPETITIONS)
let stepper = program.stepper()
const animate = function() {
world.beginFrame()
if (world.time === 0) {
// Initialize or Reset.
bug.pointer.copy(INITIAL_POINTER)
bug.X.copy(e3)
bug.frame = INITIAL_FRAME
// Erasing and adding the start point means we only have one point upon reset.
track.erase()
track.addPoint(bug.X)
// 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(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)
// Render the pointer of the bug as a blue arrow.
arrow.h.copy(bug.pointer).normalize().scale(0.3)
arrow.X.copy(bug.X)
arrow.color = EIGHT.Color.blue
arrow.render(world.ambients)
// Render the initial pointer as a green arrow at the initial location.
arrow.h.copy(INITIAL_POINTER).normalize().scale(0.3)
arrow.X.copy(e3)
arrow.color = EIGHT.Color.green
arrow.render(world.ambients)
// Render the Spherical Polar Coordinate curves.
coords.render(world.ambients)
// This call keeps the animation going.
requestAnimationFrame(animate)
}
// This call starts the animation.
requestAnimationFrame(animate)
import Bug from './Bug';
import Steppable from './Steppable'
import Stepper from './Stepper'
/**
* Scratch variable to avoid creating temporary objects.
*/
const R = EIGHT.Geometric3.one()
export default class Left implements Steppable {
constructor(private bug: Bug, private θ: number) {
}
stepper(): Stepper {
return new LeftStepper(this.bug, this.θ);
}
}
class LeftStepper implements Stepper {
private todo = true;
constructor(private bug: Bug, private θ: number) {
}
hasNext() {
return this.todo;
}
next(): void {
if (this.todo) {
this.todo = false;
this.bug.left(this.θ);
}
}
}
{
"description": "Bug Geometry on a Sphere",
"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": "bug-geometry-on-a-sphere",
"version": "1.0.0",
"keywords": [
"EIGHT",
"project",
"Getting",
"Started",
"WebGL",
"Local",
"Geometric",
"Physics"
],
"operatorOverloading": true,
"author": "David Geo Holmes"
}
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 Bug from './Bug';
import Steppable from './Steppable'
import Stepper from './Stepper'
/**
* Scratch variable to avoid creating temporary objects.
*/
const R = EIGHT.Geometric3.one()
export default class Right implements Steppable {
constructor(private bug: Bug, private θ: number) {
}
stepper(): Stepper {
return new RightStepper(this.bug, this.θ);
}
}
class RightStepper implements Stepper {
private todo = true;
constructor(private bug: Bug, private θ: number) {
}
hasNext() {
return this.todo;
}
next(): void {
if (this.todo) {
this.todo = false;
this.bug.right(this.θ);
}
}
}
import Bug from './Bug';
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 bug: Bug, private B: EIGHT.Geometric3, private θ: number) {
}
stepper(): Stepper {
return new RotateStepper(this.bug, this.B, this.θ);
}
}
class RotateStepper implements Stepper {
private todo = true;
constructor(private bug: Bug, private B: EIGHT.Geometric3, private θ: number) {
}
hasNext() {
return this.todo;
}
next(): void {
if (this.todo) {
this.todo = false;
this.bug.rotate(this.B, this.θ);
}
}
}
const FLOATS_PER_VERTEX = 3;
const BYTES_PER_FLOAT = 4;
const STRIDE = BYTES_PER_FLOAT * FLOATS_PER_VERTEX;
/**
* Computes the LINES for Spherical Polar Coordinates.
*
*/
function lines(): number[] {
const values: number[] = [];
// Odd number gives an equator.
const uSegments = 17;
const uLength = uSegments + 1;
/**
* Determines the smoothness of the latitude polygons.
*/
const vSegments = 64;
const vLength = vSegments + 1;
// Lines of Latitude.
// The poles are omitted.
for (let i = 1; i < uLength; i++) {
const θ = i * Math.PI / uLength;
const cosθ = Math.cos(θ);
const sinθ = Math.sin(θ);
values.push(sinθ);
values.push(cosθ);
values.push(0);
for (let j = 1; j < vLength; j++) {
const φ = j * 2 * Math.PI / vLength;
const cosφ = Math.cos(φ);
const sinφ = Math.sin(φ);
const x = sinθ * cosφ;
const z = sinθ * sinφ;
const y = cosθ;
values.push(x);
values.push(y);
values.push(z);
values.push(x);
values.push(y);
values.push(z);
}
values.push(sinθ);
values.push(cosθ);
values.push(0);
}
return values;
}
function primitive(): EIGHT.Primitive {
const aPosition: EIGHT.Attribute = {
values: lines(),
size: 3,
type: EIGHT.DataType.FLOAT
};
const result: EIGHT.Primitive = {
mode: EIGHT.BeginMode.LINES,
attributes: {
}
};
result.attributes['aPosition'] = aPosition;
return result;
}
/**
* The geometry is static so we use the conventional approach based upon GeometryArrays.
*/
class WireframeGeometry extends EIGHT.GeometryArrays {
constructor(private contextManager: EIGHT.ContextManager) {
super(primitive(), contextManager);
}
getPrincipalScale(name: string): number {
switch (name) {
default: {
throw new Error(`getPrincipalScale(${name}): name is not a principal scale property.`)
}
}
}
setPrincipalScale(name: string, value: number): void {
switch (name) {
default: {
throw new Error(`setPrinciplaScale(${name}): name is not a principal scale property.`)
}
}
// this.setScale(this.w, this.h, this.d)
}
}
export default class SphericalPolars extends EIGHT.Mesh {
constructor(contextManager: EIGHT.ContextManager) {
super(new WireframeGeometry(contextManager), new EIGHT.LineMaterial(void 0, contextManager), contextManager)
}
}
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: EIGHT.VectorE3): void {
const geometry = <LineGeometry>this.geometry;
geometry.addPoint(X.x, X.y, X.z);
geometry.release();
}
erase(): void {
const geometry = <LineGeometry>this.geometry;
geometry.erase();
geometry.release();
}
}
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.1,0, 0,-0.1,0, -0.1,0,0, +0.1,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 result: EIGHT.Primitive = {
mode: EIGHT.BeginMode.LINES,
attributes: {
}
};
result.attributes['aPosition'] = aPosition;
return result;
}
/**
* The geometry of the Bug is static so we use the conventional
* approach based upon GeometryArrays
*/
class TriangleGeometry 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)
}
}
export default class Triangle extends EIGHT.RigidBody {
constructor(contextManager: EIGHT.ContextManager) {
super(new TriangleGeometry(contextManager), new EIGHT.LineMaterial(void 0, 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 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() {
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(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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment