Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active August 8, 2016 03:51
Show Gist options
  • Save mathdoodle/c93218468a2cdb8a7b727a7be4d9881e to your computer and use it in GitHub Desktop.
Save mathdoodle/c93218468a2cdb8a7b727a7be4d9881e to your computer and use it in GitHub Desktop.
Turtle Geometry

Turtle Geometry

Overview

WebGL meets Turtle Geometry.

Credits

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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment