diff --git a/www/index.html b/www/index.html
index 7af52bb..f8308c9 100644
--- a/www/index.html
+++ b/www/index.html
@@ -9,7 +9,7 @@
-
+
diff --git a/www/js/main.js b/www/js/main.js
new file mode 100644
index 0000000..a2aa9b7
--- /dev/null
+++ b/www/js/main.js
@@ -0,0 +1,19 @@
+import { Smc } from "./smc/smc.js"
+
+main();
+
+function main() {
+ const canvas = document.querySelector("#gl-canvas");
+ canvas.setAttribute('width', window.innerWidth);
+ canvas.setAttribute('height', window.innerHeight);
+
+ new Smc(canvas)
+ .setMaxFps(30)
+ .setProgram(builder =>
+ builder
+ // .fetchVertexShader("../shaders/segfault.glsl")
+ // .fetchFragmentShader("../shaders/segfault.glsl"))
+ )
+ .run();
+}
+
diff --git a/www/js/smc/README.md b/www/js/smc/README.md
new file mode 100644
index 0000000..c013131
--- /dev/null
+++ b/www/js/smc/README.md
@@ -0,0 +1,4 @@
+# Shade My Canvas
+An easy to use and purely declarative wrapper for WebGL written in Javascript.
+The main idea is to remove all the boilerplate required to render shader
+programs, so you can focus on writing GLSL and not debugging WebGL.
diff --git a/www/js/draw-scene.js b/www/js/smc/draw-scene.js.bak
similarity index 100%
rename from www/js/draw-scene.js
rename to www/js/smc/draw-scene.js.bak
diff --git a/www/js/smc/errors.js b/www/js/smc/errors.js
new file mode 100644
index 0000000..3900885
--- /dev/null
+++ b/www/js/smc/errors.js
@@ -0,0 +1,10 @@
+export { SmcErr };
+
+const SmcErr = {
+ UNSUPPORTED: 0, // unused
+ SHADER_COMPILATION: 1,
+ PROGRAM_INIT: 2,
+ ATTRIBUTE_MISSING: 3,
+ UNIFORM_MISSING: 4,
+ FETCH_SHADER: 5,
+}
diff --git a/www/js/init-buffers.js b/www/js/smc/init-buffers.js.bak
similarity index 100%
rename from www/js/init-buffers.js
rename to www/js/smc/init-buffers.js.bak
diff --git a/www/js/smc/lib.js b/www/js/smc/lib.js
new file mode 100644
index 0000000..cbff55e
--- /dev/null
+++ b/www/js/smc/lib.js
@@ -0,0 +1,4 @@
+import { Smc } from "./smc.js";
+import { SmcErr } from "./errors.js";
+
+export { Smc, SmcErr };
diff --git a/www/js/webgl-demo.js b/www/js/smc/lib.js.bak
similarity index 83%
rename from www/js/webgl-demo.js
rename to www/js/smc/lib.js.bak
index c8c8217..4f08661 100644
--- a/www/js/webgl-demo.js
+++ b/www/js/smc/lib.js.bak
@@ -1,7 +1,7 @@
import { initBuffers } from "./init-buffers.js";
import { drawScene } from "./draw-scene.js";
-main();
+export { run };
// Initialize a shader program, so WebGL knows how to draw our data
function initShaderProgram(gl, vsSource, fsSource) {
@@ -27,25 +27,6 @@ function initShaderProgram(gl, vsSource, fsSource) {
return program;
}
-// Creates a shader of the given type, uploads the source and compiles
-function loadShader(gl, type, source) {
- const shader = gl.createShader(type);
-
- // Send the source to the shader object
- gl.shaderSource(shader, source);
-
- // Compile the shader program
- gl.compileShader(shader);
-
- // See if it compiled successfully
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
- gl.deleteShader(shader);
- throw new Error(`An error occurred compiling the shaders: ${gl.getShaderInfoLog(shader)}`);
- }
-
- return shader;
-}
-
function renderShader(gl, vsSource, fsSource) {
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
@@ -101,9 +82,7 @@ function fetchShader(name) {
}
-function main() {
- const canvas = document.querySelector("#gl-canvas");
- // Initialize the GL context
+function run(canvas) {
const gl = canvas.getContext("webgl");
// XXX: TODO: use `window.addEventListener('resize', ...);`
diff --git a/www/js/smc/progbuilder.js b/www/js/smc/progbuilder.js
new file mode 100644
index 0000000..da86049
--- /dev/null
+++ b/www/js/smc/progbuilder.js
@@ -0,0 +1,127 @@
+import { SmcErr } from './errors.js';
+
+export { SmcProgramBuilder };
+
+class SmcProgramBuilder {
+ #gl;
+ #program;
+
+ #isBuilt = false;
+ #hasVertexShader = false;
+ #hasFragmentShader = false;
+
+ #defaultVertexShader = `
+ attribute vec4 aVertex;
+
+ void main() {
+ gl_Position = aVertex;
+ }
+ `;
+
+ // TODO: reset the sample fragment shader back to the rainbow
+ #sampleFragmentShader = `
+ precision mediump float;
+
+ // uniform float uTime;
+ // uniform vec2 uResolution;
+
+ void main() {
+ // vec2 uv = gl_FragCoord.xy / uResolution;
+ // vec3 col = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0, 2, 4));
+ // gl_FragColor = vec4(col, 1.0);
+ // gl_FragColor = vec4(216., 43., 72., 255.) / 255.;
+
+
+ float maxfc = max(gl_FragCoord.x, gl_FragCoord.y);
+ gl_FragColor = vec4(gl_FragCoord.xy, maxfc, maxfc) / maxfc;
+ }
+ `;
+
+
+ constructor(gl, raiseError) {
+ this.#gl = gl;
+ this.#program = this.#gl.createProgram();
+ this.raiseError = raiseError;
+ }
+
+ addVertexShader(source) {
+ this.#gl.attachShader(
+ this.#program,
+ this.#newShader(
+ this.#gl.VERTEX_SHADER,
+ source
+ )
+ )
+ this.#hasVertexShader = true;
+ return this;
+ }
+
+ addFragmentShader(source) {
+ this.#gl.attachShader(
+ this.#program,
+ this.#newShader(
+ this.#gl.FRAGMENT_SHADER,
+ source
+ )
+ )
+ this.#hasFragmentShader = true;
+ return this;
+ }
+
+ fetchVertexShader(uri) {
+ this.#fetchShader(uri, (source) => this.addVertexShader(source));
+ return this;
+ }
+
+ fetchFragmentShader(uri) {
+ this.#fetchShader(uri, (source) => this.addFragmentShader(source));
+ return this;
+ }
+
+ build() {
+ // avoid user accidental calls to build()
+ if (!this.#isBuilt) {
+ if (!this.#hasVertexShader)
+ this.addVertexShader(this.#defaultVertexShader)
+ if (!this.#hasFragmentShader)
+ this.addFragmentShader(this.#sampleFragmentShader);
+
+ this.#gl.linkProgram(this.#program);
+ this.#gl.useProgram(this.#program);
+ }
+ return this.#program;
+ }
+
+ // Creates a shader of the given type, uploads the source and compiles
+ #newShader(type, source) {
+ const shader = this.#gl.createShader(type);
+ this.#gl.shaderSource(shader, source);
+ this.#gl.compileShader(shader);
+
+ if (!this.#gl.getShaderParameter(shader, this.#gl.COMPILE_STATUS)) {
+ this.#gl.deleteShader(shader);
+ const infoLog = this.#gl.getShaderInfoLog(shader);
+ this.raiseError(
+ SmcErr.SHADER_COMPILATION,
+ new Error(`An error occurred while compiling the shader: ${infoLog}`)
+ );
+ }
+
+ return shader;
+ }
+
+ #fetchShader(uri, delegate) {
+ return fetch(uri)
+ .then(res => {
+ if (res.ok)
+ delegate(res.text());
+ else {
+ this.raiseError(
+ SmcErr.FETCH_SHADER,
+ `Failed to load shader source ${url}: ${res.status} ${res.json()}`);
+ }
+ });
+ }
+
+}
+
diff --git a/www/js/smc/smc.js b/www/js/smc/smc.js
new file mode 100644
index 0000000..2f77955
--- /dev/null
+++ b/www/js/smc/smc.js
@@ -0,0 +1,311 @@
+import { SmcErr } from "./errors.js";
+import { SmcProgramBuilder } from "./progbuilder.js";
+import { hexToRgba } from "./util.js";
+
+export { Smc, UniformType };
+
+const UniformType = {
+ Float1: 0,
+ Float2: 1,
+ Float3: 2,
+ Float4: 3,
+ Int1: 4,
+ Int2: 5,
+ Int3: 6,
+ Int4: 7,
+};
+
+class Smc {
+ #canvas;
+ #gl;
+
+ // Position array of a "full-screen" quad (encoded as TRIANGLE_STRIP)
+ // Ref: https://en.wikipedia.org/wiki/Triangle_strip
+ // NOTE: +x,+y is top-right & -x,-y is bottom-left
+ #verticesFullscreen = [
+ -1.0, 1.0,
+ -1.0, -1.0,
+ 1.0, 1.0,
+ 1.0, -1.0,
+ ];
+ #vertices = this.#verticesFullscreen;
+ #attributes = new Map();
+ #uniforms = new Map();
+ #program = null;
+ #clearBitFlags;
+
+ #maxFps;
+ #minDeltaTimeMs; // in milliseconds
+ #prevTimeMs = 0;
+
+ #errorDelegate = (_, error) => { throw error };
+ #initDelegate = (_) => { };
+ #resizeDelegate = (_) => { };
+
+ constructor(canvas) {
+ this.raiseError = this.raiseError.bind(this);
+ this.render = this.render.bind(this);
+ this.renderLoop = this.renderLoop.bind(this);
+
+ this.#canvas = canvas;
+ this.#gl = Smc.#getWebGlContext(canvas);
+ // NOTE: smc.isWebGlSupported() should be queried prior
+ if (this.#gl == null)
+ throw new Error("Unable to initialize WebGL. Your browser or machine may not support it.");
+
+ // clear the entire depth buffer when this.#gl.clear is called
+ this.#gl.clearDepth(1.0);
+ this.#clearBitFlags = this.#gl.COLOR_BUFFER_BIT | this.#gl.DEPTH_BUFFER_BIT;
+ // set WebGL's render context (number of pixels to draw)
+ this.#gl.viewport(0, 0, this.#gl.canvas.width, this.#gl.canvas.height);
+
+ // set defaults
+ this.setMaxFps(30);
+ this.setClearColor(0., 0., 0., 255.);
+ }
+
+ static #getWebGlContext(canvas) {
+ try {
+ return canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
+ } catch {
+ return null;
+ };
+ }
+
+ static isWebGlSupported() {
+ try {
+ const canvas = document.createElement('canvas');
+ return !!window.WebGLRenderingContext && Smc.#getWebGlContext(canvas) != null;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ onError(delegate) {
+ this.#errorDelegate = delegate;
+ return this;
+ }
+
+ onInit(delegate) {
+ this.#initDelegate = delegate;
+ return this;
+ }
+
+ onResize(delegate) {
+ this.#resizeDelegate = delegate;
+ return this;
+ }
+
+ setClearColorHex(color) {
+ color = hexToRgba(color);
+ if (color == null) {
+ // this.raiseError isn't needed because this should
+ // be treated as a "compilation" error not a "runtime" error
+ throw new Error(`setClearColorHex expects an RGB/RGBA hex value, got "${color}"`);
+ }
+ return this.setClearColor(color.r, color.g, color.b, color.a);
+ }
+
+ setClearColor(r, g, b, a) {
+ this.#gl.clearColor(r / 255., g / 255., b / 255., a / 255.);
+ return this;
+ }
+
+ setVertices(positions) {
+ this.#vertices = positions;
+ return this;
+ }
+
+ setMaxFps(fps) {
+ this.#maxFps = fps;
+ this.#minDeltaTimeMs = fps ? 1000 / fps : null;
+ return this;
+ }
+
+ setProgram(delegate) {
+ const builder = new SmcProgramBuilder(this.#gl, this.raiseError);
+ delegate(builder); // i pray js passes by ref well...
+
+ this.#program = builder.build();
+ if (!this.#gl.getProgramParameter(this.#program, this.#gl.LINK_STATUS)) {
+ const infoLog = this.#gl.getProgramInfoLog(this.#program);
+ this.raiseError(
+ SmcErr.PROGRAM_INIT,
+ new Error(`Unable to initialize the shader program: ${infoLog}`)
+ )
+ }
+
+ this.#addAttribute("aVertex", this.#setVerticesAttribute.bind(this));
+ // DEBUG: uncomment afterwards
+ // this.#addUniform("uResolution", UniformType.Float2);
+ // this.#addUniform("uTime", UniformType.Float1);
+ // this.#addUniform("uDelta", UniformType.Float1);
+ return this;
+ }
+
+ run() {
+ this.#initDelegate()
+ this.setAttribute("aVertex", this.#vertices);
+ // DEBUG: uncomment afterwards
+ // this.setUniform("uResolution", new Float32Array([this.#gl.canvas.width, this.#gl.canvas.height]));
+
+ if (this.#maxFps == 0)
+ requestAnimationFrame(this.render)
+ else
+ requestAnimationFrame(this.renderLoop);
+ }
+
+ // requestAnimationFrame requests the browser to call the renderLoop
+ // callback function before the next repaint.
+ // `time` is the milliseconds elapsed since the page loaded.
+ renderLoop(time) {
+ var delta = time - this.#prevTimeMs;
+ this.render(time, delta);
+
+ setTimeout(
+ () => requestAnimationFrame(this.renderLoop),
+ Math.max(0, delta - this.#minDeltaTimeMs)
+ );
+ this.#prevTimeMs = time;
+ }
+
+ render(time, delta) {
+ // DEBUG: uncomment afterwards
+ // this.setUniform("uTime", time * 0.001);
+ // this.setUniform("uDelta", delta);
+
+ // DEBUG: START (remove if not necessary)
+ this.#gl.viewport(0, 0, this.#gl.canvas.width, this.#gl.canvas.height);
+ this.#gl.clear(this.#gl.COLOR_BUFFER_BIT | this.#gl.DEPTH_BUFFER_BIT);
+
+ this.setAttribute("aVertex", this.#vertices);
+
+ this.#gl.useProgram(this.#program);
+
+ // DEBUG: uncomment afterwards
+ // this.setUniform("uTime", time * 0.001);
+ // this.setUniform("uDelta", delta);
+ // this.setUniform("uResolution", new Float32Array([this.#gl.canvas.width, this.#gl.canvas.height]));
+
+ this.#gl.drawArrays(this.#gl.TRIANGLE_STRIP, 0, this.#vertices.length / 2);
+ // DEBUG: END (remove if not necessary)
+
+ // DEBUG: uncomment afterwards
+ // this.#gl.clear(this.#clearBitFlags);
+ // this.#gl.drawArrays(this.#gl.TRIANGLE_STRIP, 0, this.#vertices.length);
+ }
+
+ #addAttribute(name, setDelegate, required = false) {
+ var location = this.#gl.getAttribLocation(this.#program, name);
+ if (location == -1) {
+ if (required) {
+ this.raiseError(
+ SmcErr.ATTRIBUTE_MISSING,
+ `Linked program missing required attribute: "${name}"`
+ );
+ }
+ location = null;
+ }
+ this.#attributes.set(
+ name,
+ {
+ setDelegate: setDelegate,
+ location: location,
+ });
+
+ }
+
+ #addUniform(name, type, setEachFrame, setCallback, required = false) {
+ const location = this.#gl.getUniformLocation(this.#program, name);
+ if (location == -1) {
+ if (required) {
+ this.raiseError(
+ SmcErr.UNIFORM_MISSING,
+ `Linked program missing required uniform: "${name}"`
+ )
+ }
+ location = null;
+ }
+
+ if (type == UniformType.Float1)
+ var uniformfv = this.#gl.uniform1f;
+ else if (type == UniformType.Float2)
+ var uniformfv = this.#gl.uniform2fv;
+ else if (type == UniformType.Float3)
+ var uniformfv = this.#gl.uniform3fv;
+ else if (type == UniformType.Float4)
+ var uniformfv = this.#gl.uniform4fv;
+ else if (type == UniformType.Int1)
+ var uniformfv = this.#gl.uniform1i;
+ else if (type == UniformType.Int2)
+ var uniformfv = this.#gl.uniform2iv;
+ else if (type == UniformType.Int3)
+ var uniformfv = this.#gl.uniform3iv;
+ else if (type == UniformType.Int4)
+ var uniformfv = this.#gl.uniform4iv;
+ else {
+ // this.raiseError isn't needed because this should
+ // be treated as a "compilation" error not a "runtime" error
+ throw new Error(`Expected type from enum UniformType, but got "${type}"`);
+ }
+
+ const setDelegate = value => uniformfv(location, value);
+
+ // simplify function call to a single argument
+ this.#uniforms.set(
+ name,
+ {
+ setDelegate: setDelegate,
+ location: location,
+ setEachFrame: setEachFrame,
+ setCallback,
+ }
+ );
+ }
+
+ #getAttributeLocation(name) {
+ return this.#attributes.get(name).location;
+ }
+
+ #getUniformLocation(name) {
+ return this.#uniforms.get(name).location;
+ }
+
+ setAttribute(name, value) {
+ if (this.#getAttributeLocation(name) != null)
+ this.#attributes.get(name).setDelegate(value);
+ }
+
+ setUniform(name, value) {
+ if (this.#getUniformLocation(name) != null)
+ this.#uniforms.get(name).setDelegate(value);
+ }
+
+ #setVerticesAttribute(vertices) {
+ this.#vertices = vertices;
+
+ const buffer = this.#gl.createBuffer();
+ this.#gl.bindBuffer(this.#gl.ARRAY_BUFFER, buffer);
+ this.#gl.bufferData(
+ this.#gl.ARRAY_BUFFER,
+ new Float32Array(vertices),
+ this.#gl.STATIC_DRAW
+ );
+
+ this.#gl.vertexAttribPointer(
+ this.#getAttributeLocation("aVertex"),
+ 2, // (size) one vertex == 2 floats
+ this.#gl.FLOAT, // (type) vertex positions given as 32bit floats
+ false, // (normalized) don't normalize
+ 0, // (stride) buffer offset pointer BETWEEN elements (0 => packed)
+ 0, // (offset) buffer offset pointer from START to first element
+ )
+ this.#gl.enableVertexAttribArray(this.#getAttributeLocation("aVertex"));
+
+ return buffer;
+ }
+
+ raiseError(type, error) {
+ this.#errorDelegate(type, error);
+ }
+}
diff --git a/www/js/smc/smc.js.bak b/www/js/smc/smc.js.bak
new file mode 100644
index 0000000..a542864
--- /dev/null
+++ b/www/js/smc/smc.js.bak
@@ -0,0 +1,37 @@
+import { SmcErr } from "./errors.js";
+import { SmcBuilder } from "./builder.js";
+
+export { SmcErr };
+
+// XXX: TODO: merge SmcBuilder into smc
+class smc {
+ #canvas;
+ #builderDelegate = _ => { };
+
+ constructor(canvas) {
+ this.#canvas = canvas;
+ }
+
+ build(delegate) {
+ this.#builderDelegate = delegate;
+ return this;
+ }
+
+ onError(delegate) {
+ this.#errorDelegate = delegate;
+ return this;
+ }
+
+ run() {
+ this.#canvas = canvas;
+ const gl = this.#canvas.getContext("webgl");
+ if (gl == null) {
+ this.#raiseError(
+ SmcErr.UNSUPPORTED,
+ Error("Unable to initialize WebGL. Your browser or machine may not support it."),
+ );
+ }
+ const builder = this.#builderDelegate(new SmcBuilder(gl, this.#raiseError))
+ builder.render()
+ }
+}
diff --git a/www/js/smc/util.js b/www/js/smc/util.js
new file mode 100644
index 0000000..a786d02
--- /dev/null
+++ b/www/js/smc/util.js
@@ -0,0 +1,24 @@
+export { hexToRgba, hexToRgbaNormal };
+
+/* Converts a string of the form "#XXXXXX"
+ *
+*/
+function hexToRgba(hex) {
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i.exec(hex.toLowerCase());
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
+ a: result.length == 4 ? parseInt(result[4], 16) : 255.,
+ } : null;
+}
+
+function hexToRgbaNormal(hex) {
+ var result = hexToRgba(hex);
+ return result ? {
+ r: result.r / 255.,
+ g: result.g / 255.,
+ b: result.b / 255.,
+ a: result.a / 255.,
+ } : null;
+}