Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active July 8, 2020 04:27
Show Gist options
  • Save mathdoodle/1fef109875fac0bd77da086727d6832f to your computer and use it in GitHub Desktop.
Save mathdoodle/1fef109875fac0bd77da086727d6832f to your computer and use it in GitHub Desktop.
Local Geometry on a Sphere

Local 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.

import { Geometric3, VectorE3, BivectorE3 } from 'davinci-eight'
/**
* Scratch variable for implementing the rotate method.
*/
const R = 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 = Geometric3.zero()
public _frame: Geometric3[] = []
constructor() {
this._frame[INDEX_H] = Geometric3.zero()
this._frame[INDEX_L] = Geometric3.zero()
this._frame[INDEX_X] = Geometric3.zero()
}
get X(): Geometric3 {
return this._frame[INDEX_X]
}
get frame(): VectorE3[] {
// Return a copy.
return this._frame.map(function(e) { return e.clone() })
}
set frame(frame: VectorE3[]) {
// Copy the frame parameter.
for (let i = 0; i < 3; i++) {
this._frame[i] = 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: 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>
<base href='/'>
<script src='https://jspm.io/[email protected]'></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id='my-canvas'></canvas>
<script>
System.defaultJSExtensions = true
System.import('./index')
</script>
</body>
</html>
import { Geometric3 } from 'davinci-eight'
import { Color } from 'davinci-eight'
import { Arrow, Sphere, Track, Turtle } from 'davinci-eight'
import { GeometryMode } from 'davinci-eight'
import { windowResizer } from './windowResizer'
import Bug from './Bug'
import Forward from './Forward'
import Repeat from './Repeat'
import Left from './Left'
// import Right from './Right'
// import Rotate from './Rotate'
// import Steppable from './Steppable'
import SteppableList from './SteppableList'
import World from './World'
const e1 = Geometric3.e1()
const e2 = Geometric3.e2()
const e3 = 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]
/**
* The initial direction of the pointer.
* This is represented by the green arrow.
*/
const INITIAL_POINTER = e2
/**
* A brave new World.
*/
const world = new World()
const engine = world.engine
const bug = new Bug()
const turtle = new Turtle(engine, { color: Color.blueviolet })
const coords = new Sphere(engine, { mode: GeometryMode.WIRE, radius: 1 })
const track = new Track(engine)
track.color = Color.white
const arrow = new Arrow(engine)
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.clear()
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.
turtle.X.copyVector(bug.X)
turtle.R.rotorFromFrameToFrame(INITIAL_FRAME, bug.frame)
turtle.render(world.ambients)
// Render the track of the bug.
track.render(world.ambients)
// Render the pointer of the bug as a blue arrow.
arrow.vector = bug.pointer.clone().normalize().scale(0.3)
arrow.X.copy(bug.X)
arrow.color = Color.blue
arrow.render(world.ambients)
// Render the initial pointer as a green arrow at the initial location.
arrow.vector = INITIAL_POINTER.clone().normalize().scale(0.3)
arrow.X.copy(e3)
arrow.color = Color.green
arrow.render(world.ambients)
// Render the Spherical Polar Coordinate curves.
coords.render(world.ambients)
// This call keeps the animation going.
requestAnimationFrame(animate)
}
windowResizer(engine, world.camera).resize()
// This call starts the animation.
requestAnimationFrame(animate)
import Bug from './Bug'
import Steppable from './Steppable'
import Stepper from './Stepper'
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": "Local Geometry on a Sphere",
"dependencies": {
"DomReady": "1.0.0",
"stats.js": "0.16.0",
"davinci-eight": "7.4.4",
"dat.gui": "0.7.7"
},
"name": "copy-of-local-geometry-on-a-sphere",
"version": "0.1.0",
"keywords": [
"EIGHT",
"project",
"Getting",
"Started",
"WebGL",
"Local",
"Geometric",
"Physics",
"mathdoodle"
],
"operatorOverloading": true,
"author": "David Geo Holmes",
"linting": true,
"hideConfigFiles": 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(): boolean {
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'
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'
import { Geometric3 } from 'davinci-eight'
export default class Rotate implements Steppable {
constructor(private bug: Bug, private B: 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: Geometric3, private θ: number) {
}
hasNext() {
return this.todo
}
next(): void {
if (this.todo) {
this.todo = false
this.bug.rotate(this.B, this.θ)
}
}
}
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: blue;
}
{
"allowJs": false,
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"jsx": "react",
"module": "system",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"sourceMap": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"target": "es5",
"traceResolution": true
}
{
"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 { Engine } from 'davinci-eight'
import { PerspectiveCamera } from 'davinci-eight'
export interface Resizer {
resize(): this
stop(): this
}
/**
* Creates an object that manages resizing of the output to fit the window.
*/
export function windowResizer(engine: Engine, camera: PerspectiveCamera): Resizer {
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
}
window.addEventListener('resize', callback, false)
const that: Resizer = {
/**
*
*/
resize: function() {
callback()
return that
},
/**
* Stop watching window resize
*/
stop: function() {
window.removeEventListener('resize', callback)
return that
}
}
return that
}
import { Geometric3 } from 'davinci-eight'
import { Engine, Capability, Scene } from 'davinci-eight'
import { Facet, DirectionalLight, PerspectiveCamera } from 'davinci-eight'
import { TrackballControls } from 'davinci-eight'
const origin = Geometric3.vector(0, 0, 0)
// const e1 = Geometric3.vector(1, 0, 0)
const e2 = Geometric3.vector(0, 1, 0)
const e3 = Geometric3.vector(0, 0, 1)
export default class World {
public engine: Engine
private scene: Scene
public ambients: Facet[] = []
public camera: PerspectiveCamera
private trackball: TrackballControls
private dirLight: 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 Engine('my-canvas')
.size(500, 500)
.clearColor(0.1, 0.1, 0.1, 1.0)
.enable(Capability.DEPTH_TEST)
this.engine.gl.lineWidth(1)
this.scene = new Scene(this.engine)
this.camera = new PerspectiveCamera()
this.ambients.push(this.camera)
this.dirLight = new DirectionalLight()
this.ambients.push(this.dirLight)
this.trackball = new 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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment