<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="z-style.css">
<script src="init.js"></script>
<script src="draw.js"></script>
<script src="y-main.js"></script>
<script src="z-halp.js"></script>
<script src="z-webgl.js"></script>
<script src="z-video.js"></script>
<script src="z-CanvasAnimationBoilerPlate.js"></script>
</head>
<body>
<error></error>
<info>loading...</info>
<!-- canvas for webgl shenanigans -->
<canvas width="512px" height="512px"></canvas>
<!-- use the webcam input -->
<video width="512px" height="512px" autoplay class="hidden"></video>
<!-- used to get the pixel data from the video -->
<canvas width="512px" height="512px" class="hidden"></canvas>
<div><label>displacement:</label><input type="range" id="displacement"/></div>
<div><label>bumpiness:</label><input type="range" id="bumpiness"/></div>
</body>
</html>
const ready = function(fragment, vertex, model, video) {
var initCallbackWrapper = function(self) {
self.fragment = fragment;
self.vertex = vertex;
self.model = JSON.parse(model);
self.video = video;
initCallback(self);
info('');
};
// FIXME: why is this hack needed?
byTag('canvas')[0].getContext('webgl');
new CanvasAnimationBoilerPlate(drawCallback, initCallbackWrapper);
};
const ajaxed = function(requests) {
var video = byTag('video')[0];
var videoCallback = function(v) {
ready(requests.fragment, requests.vertex, requests.model, v);
}
setupVideoPlayer(video, videoCallback, error);
};
const errored = function(url, ouch) {
error(url + '\n' + JSON.stringify(ouch, false, '\t'));
};
const main = function() {
var requests = {
'fragment': 'x-fragment.glsl',
'vertex': 'x-vertex.glsl',
'model': 'x-model.json'
};
ajaxEm(requests, ajaxed, errored);
};
window.onload = main;
body {
background:#123;
color:#EDC;
}
error {
color: red;
white-space: pre-wrap;
}
info {
color: #DDFF;
white-space: pre-wrap;
}
.hidden {
display:none;
}
/* styling this is too much work! :-P */
input[type=range] {
width:233px;
color:red;
background:#EDC;
}
label {
width:7em;
display:inline-block;
font-weight:bold;
}
# what is what?
made the x-fragment.glsl a little fancier to do a bump map type effect using the cursor location over the canvas as the light source and webcam input as the texture.
:-D
# stuff I used
1. https://webglfundamentals.org/webgl/lessons/webgl-3d-textures.html
2. https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL
3. http://learningwebgl.com/blog/?p=507
# files
## innarestin' files
1. x-model.json: simple model definition
2. x-vertex.glsl: code for the vertex shader
3. x-fragment.glsl: code for the fragment shader
4. init.js: code that sets up the webgl junk
5. draw.js: code that draws the webgl junk
## boring files
1. index.html: define a few html elements and pull in scripts
2. y-main.js: ajax in the innarestin' files and register init and draw callbacks
## lame files
1. z-webgl.js: code to bump around with webgl nastiness, kinda innarestin' cuz webgl is so awful
2. z-halp.js: code to make dom stuff simpler and provide weak ajax support
3. z-style.css: weak af
4. z-CanvasAnimationBoilerPlate.js: some canvas animation junk I use a lot
5. README.md: this lame document
attribute vec3 theVertex;
attribute vec2 theTexture;
attribute float bumpiness;
attribute float displacement;
attribute vec2 light;
varying vec4 copyTheVertex;
varying vec2 copytheTexture;
varying float copyTheBumpiness;
varying float copyTheDisplacement;
varying vec2 copyTheLight;
uniform mat4 rotationMatrix;
void
main() {
copyTheVertex = vec4( theVertex, 1 );
gl_Position = copyTheVertex * rotationMatrix;
copytheTexture = theTexture;
copyTheBumpiness = bumpiness;
copyTheDisplacement = displacement;
copyTheLight = light;
}
{
"vertices": [
-1, -1, 1
, 1, -1, 1
, 1, 1, -1
, -1, 1, -1
]
, "faces": [
0, 1, 3
, 3, 1, 2
]
, "texture": [
0.0, 0.0
, 1.0, 0.0
, 1.0, 1.0
, 0.0, 1.0
]
}
precision mediump float;
varying vec4 copyTheVertex;
varying vec2 copytheTexture;
varying float copyTheBumpiness;
varying float copyTheDisplacement;
varying vec2 copyTheLight;
uniform sampler2D sampler;
float
gray( vec4 color ) {
return ( color.x + color.y + color.z ) / 3.0;
}
// quick bit of bumpmapping-ish fun
void
bump( float displacement, float bumpiness ) {
// look at the pixel above and to the right of current
vec2 up = vec2( copytheTexture.x, copytheTexture.y * displacement );
vec2 left = vec2( copytheTexture.x * displacement, copytheTexture.y );
// get the texture values
vec4 up_text = texture2D( sampler, up );
vec4 left_text = texture2D( sampler, left );
vec4 here_text = texture2D( sampler, copytheTexture );
// convert to gray scale
float up_gray = gray( up_text );
float left_gray = gray( left_text );
float here_gray = gray( here_text );
// compare to current
float xdiff = here_gray - left_gray;
float ydiff = here_gray - up_gray;
// how far is the position versus the light
float x = copyTheVertex.x - copyTheLight.x;
float y = copyTheVertex.y - copyTheLight.y;
// use the texture difference info to compute the bump
float xvalue = x + xdiff * -bumpiness;
float yvalue = y + ydiff * -bumpiness;
// keep the bump in some nice bounds
float min = 0.1;
float max = 1.1;
float leBump = clamp(
max - ( xvalue * xvalue + yvalue * yvalue ) + min
, min
, max
);
// bump 'em if ya got 'em!
vec4 color = leBump * texture2D( sampler, copytheTexture );
// leave the alpha value alone!
color.w = 1.0;
// set the color
gl_FragColor = color;
}
void
main() {
if ( copyTheBumpiness < 0.33 ) {
gl_FragColor = texture2D( sampler, copytheTexture );
} else {
bump(copyTheDisplacement,copyTheBumpiness);
}
}
const webglContext = function(canvas) {
var gl = false;
var names = 'webgl experimental-webgl'.split(' ');
for (var i = 0; i < names.length && !gl; i++) {
var name = names[i];
gl = canvas.getContext(name);
info('context:' + name + ' -> ' + gl);
}
return gl;
};
const createProgram = function(gl, vertexSrc, fragmentSrc) {
var program = gl.createProgram();
compileVertexShader(gl, program, vertexSrc);
compileFragmentShader(gl, program, fragmentSrc);
gl.linkProgram(program);
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
info('shader program is ready to rox u!');
} else {
throw 'shader program failed to link!';
}
gl.useProgram(program);
return program;
};
const compileVertexShader = function(gl, program, src) {
compileShader(gl, program, src, gl.VERTEX_SHADER);
};
const compileFragmentShader = function(gl, program, src) {
compileShader(gl, program, src, gl.FRAGMENT_SHADER);
};
const compileShader = function(gl, program, src, type) {
var shader = gl.createShader(type);
gl.shaderSource(shader, src);
gl.compileShader(shader);
var typeName = (gl.VERTEX_SHADER == type) ? 'vertex' : 'fragment';
// check the status just in case
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
gl.attachShader(program, shader);
info(typeName + ' shader compiled like a true hero of the people');
} else {
throw (typeName + ' shader failed to compile:' + gl.getShaderInfoLog(shader));
}
};
const bindVertices = function(gl, program, name, vertices) {
var vertex_buffer = gl.createBuffer();
vertex_buffer.raw = new Float32Array(vertices);
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertex_buffer.raw, gl.STATIC_DRAW);
var attribute = gl.getAttribLocation(program, name);
if (-1 == attribute) {
throw 'could not getAttribLocation for "' + name + '"';
}
info('bind vertices: "' + name + '" -> #' + attribute);
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 3, gl.FLOAT, false, 0, 0);
// this is hacky
vertex_buffer.name = name;
vertex_buffer.attribute = attribute;
return vertex_buffer;
};
const bindFaces = function(gl, program, faces) {
var faces_buffer = gl.createBuffer();
faces_buffer.raw = new Uint16Array(faces);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faces_buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, faces_buffer.raw, gl.STATIC_DRAW);
info('bind faces');
return faces_buffer;
};
const bindTextureCoordinates = function(gl, program, name, textureCoordinates, samplerName ) {
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
var textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
var attribute = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(attribute);
gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 0, 0);
info('bind texture: ' + name + ' -> #' + attribute + ' using ' + textureCoordinates);
// this is hacky
textureBuffer.name = name;
textureBuffer.attribute = attribute;
textureBuffer.texture = texture;
textureBuffer.sampler = gl.getUniformLocation(program, samplerName || 'sampler'); // lame!
return textureBuffer;
};
const textureData = function(gl, texture, width, height, pixels) {
// TODO: as parameters?
const level = 0;
const internalFormat = gl.RGBA;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, new Uint8Array(pixels));
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // TODO: as parameter?
gl.generateMipmap(gl.TEXTURE_2D); // TODO: non-mipmap version
};
const textureImage = function(gl, texture, image) {
// TODO: as parameters?
const level = 0;
const internalFormat = gl.RGBA;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, srcFormat, srcType, image );
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // TODO: as parameter?
gl.generateMipmap(gl.TEXTURE_2D); // TODO: non-mipmap version
};
// from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL
const isPowerOf2 = function(value) {
return (value & (value - 1)) === 0;
};
const glRanger = function(gl, program, id, settings) {
var range = byId(id);
range.setAttribute('step', (settings.max - settings.min) / 512);
for (var key in settings) {
range.setAttribute(key, settings[key]);
}
// simple toggle plz
var attribute = gl.getAttribLocation(program, id);
gl.vertexAttrib1f(attribute, 1.0 * settings.value);
return range.onchange = range.onblur = function() {
gl.vertexAttrib1f(attribute, 1.0 * range.value);
console.log(id + ' -> ' + range.value);
};
};
const byTag = function(tag) {
return document.getElementsByTagName(tag);
};
const byId = function(id) {
return document.getElementById(id);
};
const txt = function(id) {
return byId(id).innerHTML;
};
var ten = function(v) {
return v < 10 ? '0' + v : v
};
var timeString = function(when) {
if (!when) when = new Date();
var x = when;
// so gross...
return (
[x.getFullYear(), ten(x.getMonth()), ten(x.getDate())].join('-') + ' ' + [ten(x.getHours()), ten(x.getMinutes()), ten(x.getSeconds())].join(':') + '.' + x.getMilliseconds()
);
};
const log = function(level, content) {
if ('' === content) {
return '';
}
var message = '|' + timeString() + ' | ' + level + ' | ' + content;
console.log(message);
return message;
};
const info = function(content) {
try {
byTag('info')[0].innerHTML = log('INFO', content);
} catch (e) {
console.log('info.error:' + e + ' for ' + content);
}
};
const warn = function(content) {
try {
byTag('info')[0].innerHTML = log('WARN', content);
} catch (e) {
console.log('warn.error:' + e + ' for ' + content);
}
};
const error = function(content) {
try {
byTag('error')[0].innerHTML = log('ERROR', content);
} catch (e) {
console.log('error.error:' + e + ' for ' + content);
}
};
const ajax = function(url, callback, errorCallback) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if (200 == this.status) {
callback(url, this.responseText);
} else {
if (errorCallback) {
errorCallback(url, this);
}
}
}
};
xhttp.open("GET", url, true);
xhttp.send();
};
const ajaxEm = function(requests, callback, errorCallback, results) {
if (!results) results = {}
var keys = Object.keys(requests);
if (0 === keys.length) {
info('ajaxEm: done: ' + Object.keys(results));
callback(results);
}
var key = keys[0];
if (!key) return; // FIXME: how does control get here?
var url = requests[key];
delete requests[key];
var nextCallback = function(url, contents) {
info('ajaxEm: got ' + key + '=' + url);
results[key] = contents;
ajaxEm(requests, callback, errorCallback, results);
}
info('ajaxEm: get ' + key + '=' + url);
ajax(url, nextCallback, errorCallback);
};
// https://stackoverflow.com/questions/1480133/how-can-i-get-an-objects-absolute-position-on-the-page-in-javascript
const cumulativeOffset = function(element) {
var top = 0, left = 0;
do {
top += element.offsetTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while(element);
return {
top: top,
left: left
};
};
const initCallback = function(self) {
try {
var gl = self.gl = webglContext(self.canvas);
if (!self.gl) {
throw 'could not get webgl context for ' + canvas;
}
var program = self.program = createProgram(gl, self.vertex, self.fragment);
self.vertices = bindVertices(gl, program, 'theVertex', self.model.vertices);
self.faces = bindFaces(gl, program, self.model.faces);
self.textureBuffer = bindTextureCoordinates(gl, program, 'theTexture', self.model.texture);
// for getting the pixels from the webcam
self.imageCanvas = byTag('canvas')[1];
self.imageContext = self.imageCanvas.getContext('2d');
// let mouse position over canvas be the light location
lightItUp(self, gl, program);
// hack in a quick rotation matrix
rotational(self, gl, program);
// some range controls for stuff
rangeControls(gl, program);
info('initCallback finished');
} catch (e) {
error(e.toString());
throw e;
}
};
const lightItUp = function(self, gl, program) {
var light = gl.getAttribLocation(program, 'light');
gl.vertexAttrib2f(light, 0, 0);
var off = cumulativeOffset(self.canvas);
self.canvas.onmousemove = function(e) {
x = (e.clientX - off.left) / self.canvas.width;
y = (e.clientY - off.top) / self.canvas.height;
var x2 = -1 + 2 * x;
var y2 = 1 - 2 * y;
gl.vertexAttrib2f(light, x2, y2);
}
};
const rotational = function(self, gl, program) {
self.rotationMatrix = gl.getUniformLocation(program, 'rotationMatrix');
info('rotationMatrix -> #' + self.rotationMatrix);
var v = self.rotationMatrix.values = new Float32Array(16);
v[0] = v[5] = v[10] = v[15] = 1; // identity matrix
self.angle = 0;
info('rotation:' + self.rotationMatrix.values);
};
const rangeControls = function(gl, program) {
var controlled = {
bumpiness: {
min: 0,
max: 44,
value: 11
},
displacement: {
min: 0.99,
max: 1,
value: 1.0 - (1.0 / 512.0)
}
}
for (var id in controlled) {
glRanger(gl, program, id, controlled[id])();
}
};
var WW = 0;
const drawCallback = function(self) {
var gl = self.gl;
const width = 512;
const height = 512;
self.imageContext.drawImage(self.video, 0, 0, width, height);
textureImage(gl, self.textureBuffer.texture, self.imageCanvas);
var s = Math.sin(self.angle);
var c = Math.cos(self.angle);
//self.angle += 0.1;
var r = self.rotationMatrix.values;
// https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations
switch (2) {
case 0:
r[5] = c;
r[6] = -s;
r[9] = s;
r[10] = c;
break; // x
case 1:
r[0] = c;
r[2] = s;
r[8] = -s;
r[10] = c;
break; // y
case 2:
r[0] = c;
r[1] = -s;
r[4] = s;
r[5] = c; // z
}
gl.uniformMatrix4fv(self.rotationMatrix, false, self.rotationMatrix.values);
gl.activeTexture(gl.TEXTURE0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, self.model.faces.length, gl.UNSIGNED_SHORT, 0);
};
const CanvasAnimationBoilerPlate = function(drawCallback, initCallback, fps, canvas) {
var self = this;
self.init = function(drawCallback, initCallback, fps, canvas) {
self.initCallback = initCallback || function(c) {};
self.drawCallback = drawCallback || function(c) {
self.demo(c)
};
self.fps = fps || 24;
self.canvas = canvas || document.getElementsByTagName('canvas')[0];
self.w = self.canvas.width;
self.h = self.canvas.height;
self.context = self.canvas.getContext('2d');
self.initCallback(self);
self.drawWrapper = function() {
self.drawCallback(self);
setTimeout(
function() {
requestAnimationFrame(self.drawWrapper);
}, 1000 / self.fps
);
}
self.drawWrapper();
};
self.random = function(n) {
return Math.random() * n;
};
self.demo = function(c) {
var max = parseInt('FFFFFF', 16);
var color = Math.floor(c.random(max));
c.context.fillStyle = '#' + color.toString(16);
c.context.fillRect(c.random(c.w), c.random(c.h), 33, 44);
};
self.init(drawCallback, initCallback, fps, canvas);
};
const setupVideoPlayer = function(video, callback, errorCallback) {
/// gross: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
navigator.mediaDevices.getUserMedia({"video": true })
.then(
function(stream) {
video.srcObject = stream;
video.play();
info('got the camera' );
callback(video);
}
)
.catch(
function(err) {
errorCallback(err);
}
);
};