A WebGL scaffold for Common Core Math and Geometric Algebra in the Plane.
Last active
July 7, 2020 20:19
-
-
Save mathdoodle/823a7918fec5da1c7b075a0b37445f80 to your computer and use it in GitHub Desktop.
2D Geometry WebGL Scaffold
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Displays an exception by writing it to a <pre> element. | |
*/ | |
export function displayError(e: any) { | |
const stderr = <HTMLPreElement> document.getElementById('my-output') | |
stderr.style.color = "#FF0000" | |
stderr.innerHTML = `${e}` | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Geometric2 } from 'davinci-eight' | |
export enum BeginMode { | |
POINTS = 0x0000, | |
LINES = 0x0001, | |
LINE_LOOP = 0x0002, | |
LINE_STRIP = 0x0003, | |
TRIANGLES = 0x0004, | |
TRIANGLE_STRIP = 0x0005, | |
TRIANGLE_FAN = 0x0006 | |
} | |
export enum Target { | |
ARRAY_BUFFER = 0x8892, | |
ELEMENT_ARRAY_BUFFER = 0x8893, | |
ARRAY_BUFFER_BINDING = 0x8894, | |
ELEMENT_ARRAY_BUFFER_BINDING = 0x8895 | |
} | |
export enum Usage { | |
STREAM_DRAW = 0x88E0, | |
STATIC_DRAW = 0x88E4, | |
DYNAMIC_DRAW = 0x88E8 | |
} | |
/** | |
* A wrapper around the WebGLRenderingContext. | |
*/ | |
export interface Engine { | |
gl: WebGLRenderingContext | |
} | |
export interface Material { | |
gl: WebGLRenderingContext | |
program: WebGLProgram | |
use(): void | |
} | |
export interface Geometry { | |
bind(): void | |
draw(material: Material): void | |
unbind(): void | |
} | |
export interface Mesh { | |
X: Geometric2 | |
R: Geometric2 | |
render(): void | |
} | |
/** | |
* Creates a WebGLProgram with compiled and linked shaders. | |
*/ | |
function createProgram(gl: WebGLRenderingContext, vertexShader: string, fragmentShader: string): WebGLProgram { | |
// TODO: Proper cleanup if we throw an error at any point. | |
const vs = gl.createShader(gl.VERTEX_SHADER) | |
gl.shaderSource(vs, vertexShader) | |
gl.compileShader(vs) | |
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { | |
const log = gl.getShaderInfoLog(vs) | |
if (log) { | |
throw new Error(log) | |
} | |
} | |
const fs = gl.createShader(gl.FRAGMENT_SHADER) | |
if (fs) { | |
gl.shaderSource(fs, fragmentShader) | |
gl.compileShader(fs) | |
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { | |
const log = gl.getShaderInfoLog(fs) | |
if (log) { | |
throw new Error(log) | |
} | |
} | |
} | |
else { | |
throw new Error() | |
} | |
const program = gl.createProgram() | |
if (program) { | |
gl.attachShader(program, vs) | |
gl.attachShader(program, fs) | |
gl.linkProgram(program) | |
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { | |
const log = gl.getProgramInfoLog(program) | |
if (log) { | |
throw new Error(log) | |
} | |
} | |
return program | |
} | |
else { | |
throw new Error() | |
} | |
} | |
export function material(gl: WebGLRenderingContext, vs: string, fs: string): Material { | |
const program = createProgram(gl, vs, fs) | |
const use = function() { | |
gl.useProgram(program) | |
} | |
const that: Material = { | |
get gl() { | |
return gl | |
}, | |
get program() { | |
return program | |
}, | |
use | |
} | |
return that | |
} | |
export function mesh(geometry: Geometry, material: Material): Mesh { | |
const X = Geometric2.vector(0, 0) | |
const R = Geometric2.one() | |
const render = function() { | |
material.use() | |
const uPosition = material.gl.getUniformLocation(material.program, 'uPosition') | |
material.gl.uniform2f(uPosition, X.x, X.y) | |
const uAttitude = material.gl.getUniformLocation(material.program, 'uAttitude') | |
material.gl.uniform2f(uAttitude, R.a, R.b) | |
geometry.bind() | |
geometry.draw(material) | |
geometry.unbind() | |
} | |
const mesh: Mesh = { | |
get X() { | |
return X | |
}, | |
set X(value) { | |
X.copy(value) | |
}, | |
get R() { | |
return R | |
}, | |
set R(value) { | |
R.copy(value) | |
}, | |
render | |
} | |
return Object.freeze(mesh) | |
} | |
function getWebGLContext(canvas: HTMLCanvasElement): WebGLRenderingContext { | |
if (!(canvas instanceof HTMLCanvasElement)) { | |
throw new Error("canvas must be an HTMLCanvasElement") | |
} | |
// Try to grab the standard context. If it fails, fallback to experimental. | |
const context = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') | |
if (context) { | |
return context | |
} | |
else { | |
throw new Error("Unable to get WebGL context. Your browser may not support it.") | |
} | |
} | |
export function engine(canvasId: string): Engine { | |
const canvas = <HTMLCanvasElement> document.getElementById(canvasId) | |
if (!canvas) { | |
throw new Error(`Unable to get element with id '${canvasId}'`) | |
} | |
const gl = getWebGLContext(canvas) | |
const that: Engine = { | |
get gl() { | |
return gl | |
} | |
} | |
return that | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<!-- STYLES-MARKER --> | |
<style> | |
/* STYLE-MARKER */ | |
</style> | |
<!-- SYSTEM-SHIM-MARKER --> | |
<!-- SHADERS-MARKER --> | |
<!-- SCRIPTS-MARKER --> | |
</head> | |
<body> | |
<canvas id='my-canvas' width='500' height='500'> | |
Your browser does not support the HTML5 canvas element. | |
</canvas> | |
<pre id='my-output'></pre> | |
<script> | |
// CODE-MARKER | |
</script> | |
<script> | |
System.defaultJSExtensions = true | |
System.import('./script') | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Geometric2 } from 'davinci-eight' | |
export const zero = Geometric2.zero() | |
export const e1 = Geometric2.e1() | |
export const e2 = Geometric2.e2() | |
/** | |
* PI is the area of the unit disk. | |
*/ | |
export const PI = Math.PI | |
/** | |
* TAU is a complete turn. | |
*/ | |
export const TAU = 2 * Math.PI | |
export function exp(x: Geometric2): Geometric2 { | |
if (x) { | |
return x.clone().exp() | |
} | |
else { | |
throw new Error("x must be a Geometric2") | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "geometry-2d-webgl", | |
"version": "1.0.0", | |
"description": "2D Geometry WebGL Scaffold", | |
"dependencies": { | |
"stats.js": "0.16.0", | |
"davinci-eight": "7.4.4" | |
}, | |
"keywords": [ | |
"WebGL", | |
"shaders", | |
"low level", | |
"GLSL", | |
"Graphics", | |
"functions", | |
"2D", | |
"Introduction", | |
"Geometric", | |
"Algebra", | |
"Geometry", | |
"Plane" | |
], | |
"author": "David Geo Holmes", | |
"operatorOverloading": true, | |
"hideConfigFiles": true, | |
"linting": true | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { displayError } from './displayError' | |
/** | |
* Catches exceptions thrown in the animation callback and displays them. | |
*/ | |
export function requestFrame(callback: FrameRequestCallback): number { | |
const wrapper: FrameRequestCallback = function(time: number) { | |
try { | |
callback(time) | |
} | |
catch (e) { | |
displayError(e) | |
} | |
} | |
return window.requestAnimationFrame(wrapper) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { engine, material, mesh } from './graphics' | |
import { requestFrame } from './requestFrame' | |
import { triangle } from './triangle' | |
import { zero, e1, e2, TAU, exp } from './math' | |
const eng = engine('my-canvas') | |
const gl = eng.gl | |
gl.clearColor(0.0, 0.0, 0.0, 1.0) | |
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) | |
gl.lineWidth(2) | |
const vs = (<HTMLElement> document.getElementById('shader-vs')).innerText | |
const fs = (<HTMLElement> document.getElementById('shader-fs')).innerText | |
const t1 = triangle(gl) | |
t1.a = zero | |
t1.b = e1 | |
t1.c = e2 | |
const m1 = mesh(t1, material(gl, vs, fs)) | |
const t2 = triangle(gl) | |
t2.a = zero | |
t2.b = e1 | |
t2.c = e2 | |
const m2 = mesh(t2, material(gl, vs, fs)) | |
/** | |
* Amount to increase θ per animation frame. | |
*/ | |
const Δθ = TAU / 360 | |
/** | |
* The initial rotation angle (attitude) of the second triangle. | |
*/ | |
let θ = TAU / 4 | |
const animate = function() { | |
gl.clear(gl.COLOR_BUFFER_BIT) | |
m1.render() | |
m2.X = 0.0 * e1 + 0.0 * e2 | |
const B = e1 ^ e2 | |
θ = θ + Δθ | |
m2.R = exp(-B * θ / 2) | |
m2.render() | |
requestFrame(animate) | |
} | |
requestFrame(animate) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Fragment Shader. | |
* | |
* The primary purpose of this GLSL function is to determine the color and alpha value | |
* of a pixel. This is done by assigning a vec4, interpreted as [red, green, blue, alpha], | |
* to the reserved gl_FragColor variable. | |
*/ | |
/** | |
* The interpolated vertex color. | |
*/ | |
varying mediump vec3 vColor; | |
void main(void) { | |
gl_FragColor = vec4(vColor.r, vColor.g, vColor.b, 1.0); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Vertex Shader | |
* | |
* The primary purpose of this GLSL function is to determine a vertex position | |
* in space from the specifies attribute and uniform parameters. This is done by assigning | |
* a vec4, interpreted as [x, y, z, w], to the reserved gl_Position variable. | |
*/ | |
/** | |
* The position of the vertex relative to uPosition and before applying uAttitude. | |
*/ | |
attribute vec2 aPosition; | |
/** | |
* The color of the vertex. | |
*/ | |
attribute vec3 aColor; | |
/** | |
* The translation (vector) applied to all vertices. | |
*/ | |
uniform vec2 uPosition; | |
/** | |
* The rotation (rotor) applied to all vertices. | |
*/ | |
uniform vec2 uAttitude; | |
/** | |
* The color that will be passed to the fragment shader. | |
*/ | |
varying mediump vec3 vColor; | |
/** | |
* Returns R * v * ~R. | |
* A counter-clockwise rotation through θ radians is R = exp(-B * θ / 2), | |
* where B = e1 ^ e2. | |
*/ | |
highp vec2 rotate(in vec2 v, in vec2 R) { | |
/** | |
* a = +cos(θ/2) | |
*/ | |
float a = R.x; | |
/** | |
* b = -sin(θ/2) | |
*/ | |
float b = R.y; | |
/** | |
* c = cos(θ) | |
*/ | |
float c = a * a - b * b; | |
/** | |
* s = -sin(θ) | |
*/ | |
float s = 2.0 * a * b; | |
/** | |
* The simplification of the rotation equation has led to the familiar | |
* Linear Algebra rotation matrix, | |
* Q = cos(θ) -sin(θ) | |
* sin(θ) cos(θ) for a counter-clockwise θ rotation in the plane. | |
* Q operates on the column vector [x, y] (transposed) from the left. | |
*/ | |
return vec2(c * v.x + s * v.y, c * v.y - s * v.x); | |
} | |
/** | |
* Returns v + T | |
*/ | |
highp vec2 translate(in vec2 v, in vec2 T) { | |
// We compute the translation by components to be explicit. | |
// Note that WebGL permits the following form which is probably optimized too. | |
// return v + T; | |
return vec2(v.x + T.x, v.y + T.y); | |
} | |
void main(void) { | |
// Calculations are performed using Geometric Algebra. | |
// Note: It is usual in WebGL to use projective coordinates in order to unify | |
// translations and rotations into matrix multiplication | |
vec2 v = translate(rotate(aPosition, uAttitude), uPosition); | |
gl_Position = vec4(v.x, v.y, 0.0, 1.0); | |
vColor = aColor; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background-color: #242424; | |
overflow: hidden; | |
} | |
canvas { | |
background-color: white; | |
position: relative; | |
left: 10px; | |
top: 10px; | |
z-index: 2; | |
} | |
pre { | |
position: relative; | |
color: rgb(255, 255, 255); | |
left: 10px; | |
z-index: 1; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Geometric2, Color } from 'davinci-eight' | |
import { BeginMode, Geometry, Material, Target, Usage } from './graphics' | |
const BYTES_PER_FLOAT = 4 | |
export interface Triangle extends Geometry { | |
a: Geometric2 | |
b: Geometric2 | |
c: Geometric2 | |
} | |
export function triangle(gl: WebGLRenderingContext): Triangle { | |
const a = Geometric2.zero() | |
const b = Geometric2.vector(1, 0) | |
const c = Geometric2.vector(0, 1) | |
const colorA = Color.red | |
const colorB = Color.green | |
const colorC = Color.blue | |
const data = new Float32Array([ | |
a.x, a.y, colorA.r, colorA.g, colorA.b, | |
b.x, b.y, colorB.r, colorB.g, colorB.b, | |
c.x, c.y, colorC.r, colorC.g, colorC.b | |
]) | |
const vbo = gl.createBuffer() | |
const bind = function() { | |
gl.bindBuffer(Target.ARRAY_BUFFER, vbo) | |
} | |
const draw = function(material: Material) { | |
if (typeof material !== 'object') { | |
throw new Error("material must be an object.") | |
} | |
// This could should be optimized. | |
upload() | |
const aPosition = gl.getAttribLocation(material.program, 'aPosition') | |
const aColor = gl.getAttribLocation(material.program, 'aColor') | |
const stride = 5 * BYTES_PER_FLOAT | |
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, stride, 0) | |
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, stride, 2 * BYTES_PER_FLOAT) | |
enableAttribs(material) | |
gl.drawArrays(BeginMode.LINE_LOOP, 0, 3) | |
disableAttribs(material) | |
} | |
const unbind = function() { | |
gl.bindBuffer(Target.ARRAY_BUFFER, null) | |
} | |
const enableAttribs = function(material: Material) { | |
const aPosition = gl.getAttribLocation(material.program, 'aPosition') | |
gl.enableVertexAttribArray(aPosition) | |
const aColor = gl.getAttribLocation(material.program, 'aColor') | |
gl.enableVertexAttribArray(aColor) | |
} | |
const disableAttribs = function(material: Material) { | |
const aPosition = gl.getAttribLocation(material.program, 'aPosition') | |
gl.disableVertexAttribArray(aPosition) | |
const aColor = gl.getAttribLocation(material.program, 'aColor') | |
gl.disableVertexAttribArray(aColor) | |
} | |
const upload = function() { | |
data[0] = a.x | |
data[1] = a.y | |
data[2] = colorA.r | |
data[3] = colorA.g | |
data[4] = colorA.b | |
data[5] = b.x | |
data[6] = b.y | |
data[7] = colorB.r | |
data[8] = colorB.g | |
data[9] = colorB.b | |
data[10] = c.x | |
data[11] = c.y | |
data[12] = colorC.r | |
data[13] = colorC.g | |
data[14] = colorC.b | |
gl.bufferData(Target.ARRAY_BUFFER, data, Usage.DYNAMIC_DRAW) | |
} | |
const geo: Triangle = { | |
get a() { | |
return a | |
}, | |
set a(value) { | |
a.copyVector(value) | |
}, | |
get b() { | |
return b | |
}, | |
set b(value) { | |
b.copyVector(value) | |
}, | |
get c() { | |
return c | |
}, | |
set c(value) { | |
c.copyVector(value) | |
}, | |
bind, | |
draw, | |
unbind | |
} | |
return Object.freeze(geo) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Show hidden characters
{ | |
"allowJs": true, | |
"checkJs": true, | |
"declaration": true, | |
"emitDecoratorMetadata": true, | |
"experimentalDecorators": true, | |
"jsx": "react", | |
"module": "system", | |
"noImplicitAny": true, | |
"noImplicitReturns": true, | |
"noImplicitThis": true, | |
"noUnusedLocals": true, | |
"noUnusedParameters": true, | |
"preserveConstEnums": true, | |
"removeComments": false, | |
"skipLibCheck": true, | |
"sourceMap": true, | |
"strictNullChecks": true, | |
"suppressImplicitAnyIndexErrors": true, | |
"target": "es5", | |
"traceResolution": true | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment