<!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);
      }
    );
};