流动粒子(flow01001)

流动粒子(flow01001)

更多有趣示例 尽在知屋安砖社区

示例

在这里插入图片描述

HTML

<script id="vertexShader_particle" type="x-shader/x-vertex">
  attribute vec3 a_position;
  attribute vec3 a_particle;
  attribute vec2 a_reference;
  
  uniform float u_time;
  uniform mat4 u_m_model;
  uniform mat4 u_m_view;
  uniform mat4 u_m_MVP;
  uniform mat4 u_m_proj;
  
  uniform sampler2D b_position;
  uniform sampler2D b_velocity;
  
  varying vec3 v_colour;
  varying float v_fogDepth;
  varying float v_opacity;
  
  float random(vec2 st) {
    return fract(sin(dot(st,
                         vec2(12.9898,78.233)))*
        43758.5453123);
  }
  
  vec3 hsv2rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
  }
  float hash21(vec2 p) {
    p = fract(p * vec2(233.34, 851.74));
    p += dot(p, p + 23.45);
    return fract(p.x * p.y);
  }
  
  mat3 fromQuat(vec4 q) {
    float x = q.x;
    float y = q.y;
    float z = q.z;
    float w = q.w;
    float x2 = q.x*2.;
    float y2 = q.y*2.;
    float z2 = q.z*2.;
    
    float xx = x * x2;
    float yx = y * x2;
    float yy = y * y2;
    float zx = z * x2;
    float zy = z * y2;
    float zz = z * z2;
    float wx = w * x2;
    float wy = w * y2;
    float wz = w * z2;
      
    return mat3(
      1. - yy -zz, yx -wz, zx + wy,
      yx + wz, 1. - xx - zz, zy - wx,
      zx - wy, zy + wx, 1. - xx - yy
    );
  }
  
  /**
   * Generates a look-at matrix with the given eye position, focal point, and up axis.
   * If you want a matrix that actually makes an object look at another object, you should use targetTo instead.
   *
   * @param {mat4} out mat4 frustum matrix will be written into
   * @param {vec3} eye Position of the viewer
   * @param {vec3} center Point the viewer is looking at
   * @param {vec3} up vec3 pointing up
   * @returns {mat4} out
   */
  mat4 lookAt(vec3 e, vec3 c, vec3 u) {
    
      // if (Math.abs(e.x - c.x) < EPSILON &&
      //     Math.abs(e.y - c.y) < EPSILON &&
      //     Math.abs(e.z - c.z) < EPSILON) {
      //   return new Mat4();
      // }
      
      vec3 off = normalize(e - c);
      
      vec3 or = vec3(
        u.y * off.z - u.z * off.y,
        u.z * off.x - u.x * off.z,
        u.x * off.y - u.y * off.x
      );
      or = normalize(or);
      
      vec3 tn = vec3(
        off.y * or.z - off.z * or.y,
        off.z * or.x - off.x * or.z,
        off.x * or.y - off.y * or.x
      );
      tn = normalize(tn);
      
      return mat4(
        or.x,
        tn.x,
        off.x,
        0,
        
        or.y,
        tn.y,
        off.y,
        0,
        
        or.z,
        tn.z,
        off.z,
        0,
        
        -(or.x * e.x + or.y * e.y + or.z * e.z),
        -(tn.x * e.x + tn.y * e.y + tn.z * e.z),
        -(off.x * e.x + off.y * e.y + off.z * e.z),
        1
      );
  }
  vec3 palette( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d ) {
    return a + b*cos( 6.28318*(c*t+d) );
  }
  
  void main() {
    // vec4 pos = vec4(a_particle, 1.);
    // vec4 mvPos = u_m_view * u_m_model * pos;
    // gl_Position = u_m_proj * mvPos;
    // float isq = ( 1. / -mvPos.z );
    // gl_PointSize = (100.) * isq;
    
    vec3 position = texture2D(b_position, a_reference).xyz;
    vec3 velocity = texture2D(b_velocity, a_reference).xyz;
    
    // vec4 quat = vec4(normalize(velocity), 0.);
    // mat3 mat = fromQuat(quat);
    // vec3 particle = a_particle * vec3(1, 1.+length(velocity*velocity*.05), 1) * .1 * mat;
    
    float vl = min(length(velocity*velocity)*.01, 5.);
    vec3 p = a_particle * vec3(1.-vl*.2,1,1.+vl);
    float vl1 = smoothstep(0., 20., length(velocity));
    p *= (.3 + vl1);
    
    mat4 look = lookAt(
      (vec3(0,0,0)), 
      normalize(velocity), 
      (vec3(0,1,0))
    );
    vec3 particle = (vec4(p  * .2, 1) * look).xyz;
    // vec3 particle = (vec4(a_particle.yzx*vec3(1,1,1.+length(velocity*velocity)*.01), 1) * .2 * look).xyz;
    
    position += particle;
    
    vec4 pos = vec4(position, 1.);
    float l = length(pos);
    
    vec4 mvPos = u_m_view * u_m_model * pos;
    
    v_fogDepth = mvPos.z;
    
    float isq = ( 1. / -mvPos.z );
    
    float b = smoothstep(0., 80., l);
    float s = clamp(b*6., 0.1, 1.);
    v_opacity = b;
    
    gl_Position = u_m_proj * mvPos;
    float hash = hash21(a_reference);
    v_colour = palette(
      hash*.5+.3, 
      vec3(0.5,0.5,0.5),
      vec3(0.5,0.5,0.5),
      vec3(1.0,1.0,1.0),
      vec3(0.5,0.3,0.2)
    );
    // v_colour = hsv2rgb(vec3(.6 + hash * .3 + vl1 * .1, 1., hash * .5 + .3));
    // if(length(a_reference) == 0.) v_colour = vec3(1,0,0);
  }
</script>
<script id="vertexShader_buffer" type="x-shader/x-vertex">attribute vec4 a_position;
  
  uniform mat4 u_modelViewMatrix;
  uniform mat4 u_projectionMatrix;
  
  void main() {
    gl_Position = a_position;
  }
</script>
<script id="fragmentShader_velocity" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform sampler2D s_noise;
  uniform int u_frame;
  uniform float u_nsize;
  uniform float u_seed;
  
  uniform sampler2D b_velocity;
  uniform sampler2D b_position;
  
  #define PI 3.141592653589793
  #define HPI 1.5707963267948966
  #define TAU 6.283185307179586
  #define G 0.67408
  mat4 rotationMatrix(vec3 axis, float angle)
  {
      axis = normalize(axis);
      float s = sin(angle);
      float c = cos(angle);
      float oc = 1.0 - c;

      return mat4(oc * axis.x * axis.x + c,           oc * axis.x * axis.y - axis.z * s,  oc * axis.z * axis.x + axis.y * s,  0.0,
                  oc * axis.x * axis.y + axis.z * s,  oc * axis.y * axis.y + c,           oc * axis.y * axis.z - axis.x * s,  0.0,
                  oc * axis.z * axis.x - axis.y * s,  oc * axis.y * axis.z + axis.x * s,  oc * axis.z * axis.z + c,           0.0,
                  0.0,                                0.0,                                0.0,                                1.0);
  }
  
  vec3 hash33(vec3 p) {
    return fract(vec3(
      sin(p.x) * 43543.454354,
      sin(p.y) * 7531.154354,
      sin(p.z) * 10053.75315
    ));
  }

  void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution.xy;
    vec3 position = texture2D(b_position, uv).xyz;
    vec3 velocity = texture2D(b_velocity, uv).xyz;
    vec3 acceleration = vec3(position);
    vec3 f = position;
    
    float tm = u_time;
    
    float l = length(position);
    position = ( vec4(position, 1.) * rotationMatrix(vec3(sin(tm * 25.), cos(tm * 10.), sin(tm) * cos(tm * 5.)), .5 + 10. / l) ).xyz;
    
    vec3 spherical = vec3(1./max(l, .1), atan(position.y, position.x), acos(position.z / l));
    
    float a = sin(length(spherical.yz) * 5. + tm) * 5.;
    
    acceleration.x = spherical.x * sin(spherical.z) * cos(spherical.y) * a;
    acceleration.y = spherical.x * sin(spherical.z) * sin(spherical.y) * a;
    acceleration.z = spherical.x * cos(spherical.z) * a;
    
    f = normalize(f - acceleration) * -1.;
    f *= smoothstep(10., 40., l) * 2.;
    
    vec3 vel = velocity * .99 + (acceleration + f) * .5;
    
    gl_FragColor = vec4(vel, 1.0);
  }
</script>
<script id="fragmentShader_position" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform float u_delta;
  uniform sampler2D s_noise;
  uniform bool u_nreset;
  
  uniform sampler2D b_prime;
  uniform sampler2D b_velocity;
  uniform sampler2D b_position;
  uniform sampler2D b_origin;
  
  vec2 getScreenSpace() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
    
    return uv;
  }
  
  vec3 hash33(vec3 p) {
    return fract(vec3(
      sin(p.x) * 43543.454354,
      sin(p.y) * 7531.154354,
      sin(p.z) * 10053.75315
    ));
  }

  void main() {
    vec2 uv = getScreenSpace();
    vec2 s = gl_FragCoord.xy / u_resolution.xy;
    
    vec3 position = texture2D(b_position, s).xyz;
    vec3 velocity = texture2D(b_velocity, s).xyz;
    
    vec3 pos = position + velocity * u_delta * .5;
    
    if(length(pos) > 100.) {
      pos = pos / length(pos) * 2.;
    }

    gl_FragColor = vec4(pos, 1.0);
  }
</script>
<script id="fragmentShader_particle" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform sampler2D s_noise;
  uniform bool u_transition;
  uniform float u_transition_val;
  
  uniform sampler2D b_prime;
  uniform sampler2D b_position;
  
  varying vec3 v_colour;
  varying float v_fogDepth;
  varying float v_opacity;
  
  vec2 getScreenSpace() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
    
    return uv;
  }

  void main() {
    float fade = smoothstep(1., 0., u_transition_val);
    gl_FragColor = vec4(mix(vec3(1), v_colour, fade), 1.);
//     vec2 uv = gl_PointCoord.xy - .5;
//     vec2 s = gl_FragCoord.xy / u_resolution.xy;
    
//     float l = length(uv);
//     float c = smoothstep(.5, 0., l);
//     float fog = smoothstep(-200., -1., v_fogDepth);
//     float opacity = c*fog;
//     if(c < .1) discard;
    
//     float fade = smoothstep(1., 0., u_transition_val);
    
//     gl_FragColor = vec4(
//       mix(
//         vec4(1.),
//         mix(
//           vec4(1), 
//           vec4(v_colour, opacity), 
//           c),
//         fade
//       )
//     );
  }
  
</script>
<script id="fragmentShader_blur" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform sampler2D s_noise;
  
  uniform sampler2D b_prime;
  uniform sampler2D b_blur;
  
  varying vec3 v_colour;
  
  vec2 getScreenSpace() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
    
    return uv;
  }

  void main() {
    vec2 uv = getScreenSpace();
    
    vec2 s = gl_FragCoord.xy / u_resolution.xy;
    
    vec4 n1 = texture2D(b_blur, s);
    vec4 n = clamp(texture2D(b_prime, s), 0., 1.);
    
    vec4 c = n1*.5 + n*.5;

    gl_FragColor = clamp(c, 0., 1.);
  }
  
</script>
<script id="fragmentShader_bloom" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform sampler2D s_noise;
  uniform int u_bloomstep;
  
  uniform sampler2D b_prime;
  uniform sampler2D b_blur;
  uniform sampler2D b_bloom;
  
  varying vec3 v_colour;
  
  vec2 getScreenSpace() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
    
    return uv;
  }
  
  vec4 tex(sampler2D tex, vec2 co) {
    return clamp(texture2D(tex, co), 0., 1.);
  }

  void main() {
    vec2 uv = getScreenSpace();
    
    vec2 s = gl_FragCoord.xy / u_resolution.xy;
    vec2 p = 1./u_resolution.xy;
    
    
    // vec4 n1 = texture2D(b_blur, s);
    // vec4 n = texture2D(b_prime, s);
    
    vec4 n1;
    vec4 c;
    vec4 n = texture2D(b_prime, s);
    vec4 n2 = n;
    if(u_bloomstep == 0) {
      n1 = tex(b_blur, s);
      n1 += tex(b_blur, s + vec2(0, p.y));
      n1 += tex(b_blur, s + vec2(0, p.y*2.));
      n1 += tex(b_blur, s + vec2(0, p.y*3.));
      n1 += tex(b_blur, s + vec2(0, p.y*-1.));
      n1 += tex(b_blur, s + vec2(0, p.y*-2.));
      n1 += tex(b_blur, s + vec2(0, p.y*-3.));
      n1/=7.;
      c = n1;
    } else if(u_bloomstep == 1) {
      n1 = tex(b_bloom, s);
      n1 += tex(b_bloom, s + vec2(p.x, 0.));
      n1 += tex(b_bloom, s + vec2(p.x*2., 0));
      n1 += tex(b_bloom, s + vec2(p.x*3., 0));
      n1 += tex(b_bloom, s + vec2(p.x*-1., 0));
      n1 += tex(b_bloom, s + vec2(p.x*-2., 0));
      n1 += tex(b_bloom, s + vec2(p.x*-3., 0));
      n1/=7.;
      c = pow(n1, vec4(2.));
    }
    
    gl_FragColor = clamp(n1, 0., 1.);
  }
  
</script>
<script id="fragmentShader_buffer" type="x-shader/x-fragment">
  #extension GL_OES_standard_derivatives : enable
  precision highp float;
  
  uniform vec2 u_resolution;
  uniform vec2 u_mouse;
  uniform float u_time;
  uniform sampler2D s_noise;
  uniform bool u_transition;
  uniform float u_transition_val;
  
  uniform sampler2D b_prime;
  uniform sampler2D b_blur;
  uniform sampler2D b_bloom;
  
  varying vec3 v_colour;
  
  vec2 getScreenSpace() {
    vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.y, u_resolution.x);
    
    return uv;
  }

  void main() {
    vec2 uv = getScreenSpace();
    
    vec2 s = gl_FragCoord.xy / u_resolution.xy;
    
    vec2 p = .5/u_resolution.xy;
    
    // vec4 bloom = texture2D(b_bloom, s);
    vec4 n = texture2D(b_blur, s);
    
    float fade = smoothstep(1., 0., u_transition_val);

    gl_FragColor = vec4(
      mix(
        vec3(1), 
        n.rgb, fade) * (1. + (1.-fade)), 1.);
  }
  
</script>
<div class="controls">
  <div class="playpause checked">
    <label>
      <input checked="checked" type="checkbox" value="None" id="playpause" name="check" />
    </label>
  </div>
</div>

CSS

body {
  background: #66f;
  margin: 0;
  overflow: hidden;
}
canvas {
  height: 100vh;
  width: 100vw;
  touch-action: none;
}
.osc {
  left: 0px;
  position: fixed;
  top: 0px;
}

.button {
  position: fixed;
  z-index: 10;
  right: 0;
  bottom: 0;
}
.controls {
  position: fixed;
  z-index: 10;
  left: 0;
  bottom: 0;
}
.playpause {
  background: #AAB;
  padding: 10px;
}
.playpause label {
  display: block;
  box-sizing: border-box;

  width: 0;
  height: 20px;

  cursor: pointer;

  border-color: transparent transparent transparent #202020;
  transition: 100ms all ease;
  will-change: border-width;

  border-style: double;
  border-width: 0px 0 0px 20px;
}
.playpause input[type='checkbox'] {
  visibility: hidden;
}
.playpause.checked label {
  border-style: double;
  border-width: 0px 0 0px 20px;
}
.playpause label {
  border-style: solid;
  border-width: 10px 0 10px 20px;
}
/* } */

JS

import { Vec2, Vec3, Mat2, Mat3, Mat4, Quat } from 'https://cdn.skypack.dev/wtc-math';

import gifJs from 'https://cdn.skypack.dev/gif.js';

console.clear();

// Determine whether a number is a power of 2
function powerOf2(v) {
  return v && !(v & (v - 1));
}
// Return the next greatest power of 2
function nextPow2( v ) {
  v--;
  v |= v >> 1;
  v |= v >> 2;
  v |= v >> 4;
  v |= v >> 8;
  v |= v >> 16;
  v++;
  return v;
}
// Update a provided image to the nearest power of 2 in size.
const pow2Image = (c) => {
  const newWidth = powerOf2(c.width) ? c.width : nextPow2(c.width);
  const newHeight = powerOf2(c.height) ? c.height : nextPow2(c.height);
  const _c = document.createElement('canvas');
  const ctx = _c.getContext('2d');
  _c.width = newWidth;
  _c.height = newHeight;
  ctx.drawImage(c, 0, 0, newWidth, newHeight);
  return _c;
}
const asyncImageLoad = function(img, src) {
  return new Promise((resolve, reject) => {
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  })
}
const glEnumToString = (function() {
  const haveEnumsForType = {};
  const enums = {};

  function addEnums(gl) {
    const type = gl.constructor.name;
    if (!haveEnumsForType[type]) {
      for (const key in gl) {
        if (typeof gl[key] === 'number') {
          const existing = enums[gl[key]];
          enums[gl[key]] = existing ? `${existing} | ${key}` : key;
        }
      }
      haveEnumsForType[type] = true;
    }
  }

  return function glEnumToString(gl, value) {
    addEnums(gl);
    return enums[value] || (typeof value === 'number' ? `0x${value.toString(16)}` : value);
  };
}());
const addExtensions = (ctx) => {
  // Set up the extensions
  ctx.getExtension('OES_standard_derivatives');
  ctx.getExtension('EXT_shader_texture_lod');
  ctx.getExtension('OES_texture_float');
  ctx.getExtension('WEBGL_color_buffer_float');
  ctx.getExtension('OES_texture_float_linear');
  ctx.getExtension('EXT_color_buffer_float');
}
function createContext(c, opt_attribs, params) {
  const ctx = c.getContext("webgl", params) || this._el.getContext("experimental-webgl", params);
  
  addExtensions(ctx);
  
  return ctx;
}

const quatToMat4 = (q) => {
    if(q.array) q = q.array; // This just transforms a provided vector into to an array.
    
    if(q instanceof Array && q.length >= 4) {
      const [x, y, z, w] = q;
      const [x2, y2, z2] = q.map(x => x * 2.);
      
      const xx = x * x2,
            yx = y * x2,
            yy = y * y2,
            zx = z * x2,
            zy = z * y2,
            zz = z * z2,
            wx = w * x2,
            wy = w * y2,
            wz = w * z2;
      
      return new Mat4(
        1 - yy -zz, yx -wz, zx + wy, 0, 
        yx + wz, 1 - xx - zz, zy - wx, 0, 
        zx - wy, zy + wx, 1 - xx - yy, 0,
        0, 0, 0, 1
      );
    }
  }

class Renderer {
  static #defaultOptions = {
    width: 512,
    height: 512,
    pxRatio: Math.min(window.devicePixelRatio, 2),
    clearing: true,
    depthTesting: true,
    premultipliedAlpha: true
  }
    
  static BLENDING_DEBUG      = -1;
  static BLENDING_NORMAL      = 1;
  static BLENDING_ADDITIVE    = 2;
  static BLENDING_SUBTRACTIVE = 4;
  static BLENDING_MULTIPLY    = 8;
  static BLENDING_OFF         = 16;

  isWebgl2 = false;

  #blending;
  #blendingEnabled = false;
  #buffers = [];

  constructor(canvas, options) {
    options = Object.assign({}, Renderer.#defaultOptions, options);
    this.width = options.width;
    this.height = options.height;
    this.pxRatio = options.pxRatio;
    this.clearing = options.clearing;
    this.depthTesting = options.depthTesting;
    this.canvas = canvas || document.createElement('canvas');
    this.canvas.width = this.width * this.pxRatio;
    this.canvas.height = this.height * this.pxRatio;
    this.premultipliedAlpha = options.premultipliedAlpha;
    
    this.ctx = this.canvas.getContext("webgl", options) || this.canvas.getContext("experimental-webgl", options);
    
    this.ctx.viewportWidth = this.canvas.width;
    this.ctx.viewportHeight = this.canvas.height;
    
    this.uniformResolution = new Uniform(this.ctx, 'resolution', Uniform.TYPE_V2, [this.canvas.width, this.canvas.height]);
    
    this.addExtensions();
  }
  resize(w, h, ratio) {
    this.width = w;
    this.height = h;
    this.pxRatio = ratio || this.pxRatio;
    this.canvas.width = this.width * this.pxRatio;
    this.canvas.height = this.height * this.pxRatio;
    
    this.ctx.viewportWidth = this.canvas.width;
    this.ctx.viewportHeight = this.canvas.height;
    
    this.uniformResolution = new Uniform(this.ctx, 'resolution', Uniform.TYPE_V2, [this.canvas.width, this.canvas.height]);
  }
  setViewport(dimensions) {
    let w = this.width*this.pxRatio;
    let h = this.height*this.pxRatio;
    if(dimensions) {
      w = dimensions[0];
      h = dimensions[1];
    }
    this.ctx.viewport(0, 0, w, h);
    this.uniformResolution = new Uniform(this.ctx, 'resolution', Uniform.TYPE_V2, [w, h]);
  }
  addExtensions() {
    this.ctx.getExtension('OES_standard_derivatives');
    this.ctx.getExtension('EXT_shader_texture_lod');
    this.ctx.getExtension('OES_texture_float');
    this.ctx.getExtension('WEBGL_color_buffer_float');
    this.ctx.getExtension('OES_texture_float_linear');
    this.ctx.getExtension('EXT_color_buffer_float');
  }
  linkBuffer(buffer) {
    let hasBuffer = false;
    this.#buffers.forEach((b) => {
      if(buffer === b) hasBuffer = true;
    });
    if(!hasBuffer) {
      this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, buffer.buffer);
      this.ctx.bufferData(
        this.ctx.ARRAY_BUFFER,
        buffer.data,
        buffer.drawType);
    }
    buffer.link(this.currentProgram.program);
  }
  setupProgram(program, buffers, attributes, uniforms) {
    this.currentProgram = program;
    this.ctx.useProgram(program.program);
    
    this.premultiplied = program.premultiplied;
    
    this.depthTesting = program.depthTesting;
    
    
    if(program.blending === Program.BLENDING_NORMAL && program.transparent === false ) {
      this.blending = Program.BLENDING_OFF;
    } else {
      this.blending = program.blending;
    }
    
    this.clearColour = program.clearColour;
    const a = this.clearColour[3];
    // console.log('prem', this.premultipliedAlpha)
    if(this.premultipliedAlpha) this.clearColour = this.clearColour.map((c, i) => c * a );
    
    this.ctx.clearColor(...this.clearColour);
    
    // TODO: Unlink unused buffers during this setup phase as well.
    buffers.forEach(buffer => {
      this.linkBuffer(buffer);
    });
      
    // this.ctx.enable(ctx.DEPTH_TEST);
    if(this.depthTesting) this.ctx.enable(ctx.DEPTH_TEST);
    else this.ctx.disable(ctx.DEPTH_TEST);
    
    uniforms.forEach(uniform => {
      uniform.bind(program.program);
    });
    this.uniformResolution.bind(program.program);
  }
  render(points, buffer) {
    this.ctx.bindFramebuffer(this.ctx.FRAMEBUFFER, buffer?.fb || null);
    if(this.clearing) {
      this.ctx.clear( this.ctx.COLOR_BUFFER_BIT );
      
      if(this.depthTesting) this.ctx.clear( this.ctx.DEPTH_BUFFER_BIT );
    }
    switch(this.currentProgram.renderType) {
      case Program.RENDER_TRIANGLES: 
        this.ctx.drawArrays(this.ctx.TRIANGLES, 0, points);
        break;
      case Program.RENDER_STRIP: 
        this.ctx.drawArrays(this.ctx.TRIANGLE_STRIP, 0, points);
        break;
      case Program.RENDER_LINES: 
        this.ctx.drawArrays(this.ctx.LINE_STRIP, 0, points);
        break;
      case Program.RENDER_LINELOOP: 
        this.ctx.drawArrays(this.ctx.LINE_LOOP, 0, points);
        break;
      case Program.RENDER_POINTS: 
        this.ctx.drawArrays(this.ctx.POINTS, 0, points);
        break;
    }
    
  }

  /* SETTERS AND GETTERS */
  get blending() {
    return this.#blending || Program.BLENDING_NORMAL;
  }
  set blending(blending) {
    
    if(blending === Renderer.BLENDING_DEBUG) {
      
      if(!this.breakLog) {
        console.log(blending, Renderer.BLENDING_OFF, this.premultiplied)
        this.breakLog = true;
      }
      this.#blending = blending;
      this.ctx.enable(this.ctx.BLEND);
        this.ctx.blendFuncSeparate( this.ctx.ONE, this.ctx.ONE_MINUS_SRC_ALPHA, this.ctx.ONE, this.ctx.ONE_MINUS_SRC_ALPHA );
      return;
    }
    
    this.#blending = blending;
    if(blending === Renderer.BLENDING_OFF) {
      this.ctx.disable(this.ctx.BLEND);
      this.#blendingEnabled = false;
      return;
    }
		if ( this.#blendingEnabled === false ) {
      this.ctx.enable(this.ctx.BLEND);
      // this.ctx.alphaFunc(this.ctx.GL_GREATER, 0.5);
      // this.ctx.enable(this.ctx.GL_ALPHA_TEST);
			this.#blendingEnabled = true;
		}
    
		if( this.premultiplied ) {
      switch (this.blending) {
        case Renderer.BLENDING_NORMAL:
          this.ctx.blendFuncSeparate( this.ctx.ONE, this.ctx.ONE_MINUS_SRC_ALPHA, this.ctx.ONE, this.ctx.ONE_MINUS_SRC_ALPHA );
          break;
        case Renderer.BLENDING_ADDITIVE: 
          this.ctx.blendFunc( this.ctx.ONE, this.ctx.ONE );
          break;
        case Renderer.BLENDING_SUBTRACTIVE: 
          this.ctx.blendFuncSeparate( this.ctx.ZERO, this.ctx.ZERO, this.ctx.ONE_MINUS_SRC_COLOR, this.ctx.ONE_MINUS_SRC_ALPHA );
          break;
        case Renderer.BLENDING_MULTIPLY:
          this.ctx.blendFuncSeparate( this.ctx.ZERO, this.ctx.SRC_COLOR, this.ctx.ZERO, this.ctx.SRC_ALPHA );
          break;
      }
    } else {
      switch (this.blending) {
        case Renderer.BLENDING_NORMAL: 
          this.ctx.blendFuncSeparate( this.ctx.SRC_ALPHA, this.ctx.ONE_MINUS_SRC_ALPHA, this.ctx.ONE, this.ctx.ONE_MINUS_SRC_ALPHA );
          break;
        case Renderer.BLENDING_ADDITIVE: 
          this.ctx.blendFunc( this.ctx.SRC_ALPHA, this.ctx.ONE );
          break;
        case Renderer.BLENDING_SUBTRACTIVE: 
          this.ctx.blendFunc( this.ctx.ZERO, this.ctx.ONE_MINUS_SRC_COLOR );
          break;
        case Renderer.BLENDING_MULTIPLY:
          this.ctx.blendFunc( this.ctx.ZERO, this.ctx.SRC_COLOR );
          break;
      }
    }
  }
}
class Buffer {
  static #defaultAttribute = {
        numComponents: 2,
        offset: 0,
        stride: 0
      };
  static #defaults = {
    attributes: [{
        name: 'position'
      }
    ],
    normalized: false,
    drawType: window.WebGLRenderingContext.STATIC_DRAW,
    type: window.WebGLRenderingContext.FLOAT
  }
  constructor(ctx, data, options) {
    this.ctx = ctx;
    this.name = name;
    options = Object.assign({}, Buffer.#defaults, options);
    this.attributes = options.attributes.map(a => Object.assign({}, Buffer.#defaultAttribute, a));
    
    this.normalized = options.normalized;
    this.drawType = options.drawType;
    this.type = options.type;
    if(data instanceof Array) data = new Float32Array(data);
    this.data = data;
    this.buffer = ctx.createBuffer();
  }

  link(program, hasBuffer = false) {
    let location = this.ctx.getAttribLocation(program, `a_${this.name}`);
    
    this.attributes.forEach(attribute => {
      const location = this.ctx.getAttribLocation(program, `a_${attribute.name}`);
      this.ctx.vertexAttribPointer(location, attribute.numComponents, this.type, this.normalized, attribute.stride, attribute.offset);
      this.ctx.enableVertexAttribArray(location);
    });
  }

  get length() {
    return this.data.length;
  }
}
class Program {
  
  static RENDER_TRIANGLES     = 0;
  static RENDER_STRIP         = 1;
  static RENDER_LINES         = 2;
  static RENDER_LINELOOP      = 4;
  static RENDER_POINTS        = 8;
  
  static #defaultOptions = {
    renderType: Program.RENDER_TRIANGLES,
    clearColour: [1.0, 1.0, 1.0, 1.0],
    blending: Renderer.BLENDING_OFF,
    premultiplied: true,
    transparent: false,
    depthTesting: true
  }
  
  #vShader
  #fShader
  #p
  #renderType
  
  constructor(ctx, vertexShaderSource, fragmentShaderSource, options = {}) {
    options = Object.assign({}, Program.#defaultOptions, options);
    
    this.ctx = ctx;
    
    this.renderType = options.renderType;
    
    this.clearColour = options.clearColour;
    this.blending = options.blending;
    this.premultiplied = options.premultiplied;
    this.transparent = options.transparent;
    this.depthTesting = options.depthTesting;
    
    // Create the shaders
    this.vShader = Program.createShaderOfType(this.ctx, this.ctx.VERTEX_SHADER, vertexShaderSource);
    this.fShader = Program.createShaderOfType(this.ctx, this.ctx.FRAGMENT_SHADER, fragmentShaderSource);
    
    // Create the program and link the shaders
    this.#p = this.ctx.createProgram();
    this.ctx.attachShader(this.#p, this.vShader);
    this.ctx.attachShader(this.#p, this.fShader);
    
    this.ctx.linkProgram(this.#p);
    
    // Check the result of linking
    var linked = this.ctx.getProgramParameter(this.#p, this.ctx.LINK_STATUS);
    if (!linked) {
      var error = this.ctx.getProgramInfoLog(this.#p);
      console.log('Failed to link program: ' + error);
      this.ctx.deleteProgram(this.#p);
      this.ctx.deleteShader(this.fShader);
      this.ctx.deleteShader(this.vShader);
    }
  }
  
  get program() {
    return this.#p;
  }

  /* SETTERS AND GETTERS */

  set renderType(value) {
    if([
      Program.RENDER_TRIANGLES,
      Program.RENDER_STRIP,
      Program.RENDER_LINES,
      Program.RENDER_LINELOOP,
      Program.RENDER_POINTS
    ].indexOf(value) > -1) this.#renderType = value;
  }
  get renderType() {
    return this.#renderType;
  }
  
  /**
   * Static Methods
   */

	/**
	 * Create a shader of a given type given a context, type and source.
	 *
   * @static
	 * @param  {WebGLContext} ctx The context under which to create the shader
	 * @param  {WebGLShaderType} type The shader type, vertex or fragment
	 * @param  {string} source The shader source.
	 * @return {WebGLShader} The created shader
	 */
  static createShaderOfType(ctx, type, source) {
    const shader = ctx.createShader(type);
    ctx.shaderSource(shader, source);
    ctx.compileShader(shader);
    
    // Check the compile status
    const compiled = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
    if (!compiled) {
      // Something went wrong during compilation; get the error
      const lastError = ctx.getShaderInfoLog(shader);
      console.error(`${Program.addLineNumbersWithError(source, lastError)}\nError compiling ${glEnumToString(ctx, type)}: ${lastError}`);
      ctx.deleteShader(shader);
      return null;
    }
    
    return shader;
  }
  static addLineNumbersWithError(src, log = '') {
    console.log(src)
    const errorRE = /ERROR:\s*\d+:(\d+)/gi;
    // Note: Error message formats are not defined by any spec so this may or may not work.
    const matches = [...log.matchAll(errorRE)];
    const lineNoToErrorMap = new Map(matches.map((m, ndx) => {
      const lineNo = parseInt(m[1]);
      const next = matches[ndx + 1];
      const end = next ? next.index : log.length;
      const msg = log.substring(m.index, end);
      return [lineNo - 1, msg];
    }));
    return src.split('\n').map((line, lineNo) => {
      const err = lineNoToErrorMap.get(lineNo);
      return `${lineNo + 1}: ${line}${err ? `\n\n^^^ ${err}` : ''}`;
    }).join('\n');
  }
}
class Uniform {
  static TYPE_INT = 0
  static TYPE_FLOAT = 1
  static TYPE_V2 = 2
  static TYPE_V3 = 3
  static TYPE_V4 = 4
  static TYPE_BOOL = 5
  static TYPE_M2 = 6
  static TYPE_M3 = 7
  static TYPE_M4 = 8
  
  #prefix = 'u'
  
  constructor(ctx, name, type, value) {
    this.ctx = ctx;
    this.name = name;
    this.type = type;
    this.value = value;
  }
  
  prebind() {
    
  }
  
  bind(program) {
    this.prebind(program);
    const location = this.ctx.getUniformLocation(program, `${this.#prefix}_${this.name}`);
    switch(this.type) {
      case Uniform.TYPE_INT : 
        if(!isNaN(this.value)) this.ctx.uniform1i( location, this.value );
        break;
      case Uniform.TYPE_FLOAT : 
        if(!isNaN(this.value)) this.ctx.uniform1f( location, this.value);
        break;
      case Uniform.TYPE_V2 : 
        if(this.value instanceof Array && this.value.length === 2.) this.ctx.uniform2fv( location, this.value);
        break;
      case Uniform.TYPE_V3 : 
        if(this.value instanceof Array && this.value.length === 3.) this.ctx.uniform3fv( location, this.value);
        break;
      case Uniform.TYPE_V4 : 
        if(this.value instanceof Array && this.value.length === 4.) this.ctx.uniform4fv( location, this.value);
        break;
      case Uniform.TYPE_BOOL : 
        if(!isNaN(this.value)) this.ctx.uniform1i( location, this.value);
        break;
      case Uniform.TYPE_M2 : 
        if(this.value instanceof Array && this.value.length === 4.) this.ctx.uniformMatrix2fv( location, false, this.value);
      case Uniform.TYPE_M3 : 
        if(this.value instanceof Array && this.value.length === 9.) this.ctx.uniformMatrix3fv( location, false, this.value);
      case Uniform.TYPE_M4 : 
        if(this.value instanceof Array && this.value.length === 16.) this.ctx.uniformMatrix4fv( location, false, this.value);
        break;
    }
  }
}
class Texture extends Uniform {
  #prefix = 's'
  static #defaultOptions = {
    textureType: 0,
    minFilter: window.WebGLRenderingContext.LINEAR,
    magFilter: window.WebGLRenderingContext.LINEAR,
    makePowerOf2: false,
    generateMipMap: false
  }
  static masteri = 0
  
  static IMAGETYPE_REGULAR = 0
  static IMAGETYPE_TILE = 1
  static IMAGETYPE_MIRROR = 2
  
  constructor(ctx, name, options) {
    super(ctx, name, 0, null);
    options = Object.assign({}, Texture.#defaultOptions, options);
    this.textureType = options.textureType;
    this.minFilter = options.minFilter;
    this.magFilter = options.magFilter;
    this.makePowerOf2 = options.makePowerOf2;
    this.generateMipMap = options.generateMipMap;
    this.url = options.url;
    this.data = options.data;
    this.value = Texture.masteri++;
  }
  async preload() {
    const store = {};

    const img = new Image();
    img.crossOrigin = "anonymous";

    await asyncImageLoad(img, this.url);
    
    if(this.makePowerOf2) this.image = pow2Image(img);
    else this.image = img;

    // this.loadTexture(gl, n, store);
    return this;
  }
  
  prebind(program) {
    if(!this.image) return;
    
    const texture = this.ctx.createTexture(); // Create the texture object
    
    this.ctx.pixelStorei(this.ctx.UNPACK_FLIP_Y_WEBGL, true);
    this.ctx.bindTexture(this.ctx.TEXTURE_2D, texture);
    
    this.ctx.activeTexture(this.ctx.TEXTURE0 + this.value);
    
    // Set the parameters based on the passed type
    // In WebGL images are wrapped by default, so we don't need to check for that
    if(this.textureType === Texture.IMAGETYPE_MIRROR) {
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.MIRRORED_REPEAT);
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.MIRRORED_REPEAT);
    } else if(this.textureType === Texture.IMAGETYPE_REGULAR) {
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.CLAMP_TO_EDGE);
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.CLAMP_TO_EDGE);
    }
    
    this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MIN_FILTER, this.minFilter);
    this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MAG_FILTER, this.magFilter);
    
    // Upload the image into the texture.
    if(this.data) {
      this.ctx.texImage2D(this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, this.data);
    } else {
      this.ctx.texImage2D(this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, this.image);
    }
    
    if(this.generateMipMap) this.ctx.generateMipmap(this.ctx.TEXTURE_2D);
  }
}
class FrameBuffer {
  static #defaultOptions = {
    width: 512,
    height: 512,
    pxRatio: Math.min(window.devicePixelRatio, 2),
    tiling: Texture.IMAGETYPE_REGULAR,
    texdepth: FrameBuffer.TEXTYPE_HALF_FLOAT_OES,
    data: null,
    depthTesting: false
  }
  static TEXTYPE_FLOAT = 0
  static TEXTYPE_UNSIGNED_BYTE = 1
  static TEXTYPE_HALF_FLOAT_OES = 2

  #fb1
  #fb2
  #activeFB
  #name
  #width
  #height
  #pxRatio
  #tiling = Texture.IMAGETYPE_REGULAR
  #texdepth = FrameBuffer.TEXTYPE_HALF_FLOAT_OES
  #data;

  constructor(renderer, name, options) {
    options = Object.assign({}, FrameBuffer.#defaultOptions, options);
    
    this.width = options.width;
    this.height = options.height;
    this.pxRatio = options.pxRatio;
    this.tiling = options.tiling;
    this.texdepth = options.texdepth;
    this.depthTesting = options.depthTesting;
    
    this.#name = name;
    this.value = Texture.masteri++;
    
    this.ctx = renderer.ctx;
    this.renderer = renderer;
    
    this.data = options.data;
    
    this.#fb1 = this.createFrameBuffer();
    this.#fb2 = this.createFrameBuffer();
    this.#activeFB = this.#fb1;
  }
  resize(width, height) {
    this.width = width;
    this.height = height;
    this.#fb1 = this.createFrameBuffer();
    this.#fb2 = this.createFrameBuffer();
    this.#activeFB = this.#fb1;
  }
  createFrameBuffer() {
    const targetTexture = this.ctx.createTexture();
    this.ctx.bindTexture(this.ctx.TEXTURE_2D, targetTexture);
    {
      // define size and format of level 0
      const level = 0;
      let internalFormat = this.ctx.RGBA;
      const border = 0;
      let format = this.ctx.RGBA;
      let t;
      if(this.texdepth === FrameBuffer.TEXTYPE_FLOAT) {
        const e = this.ctx.getExtension('OES_texture_float');
        t = this.ctx.FLOAT;
        // internalFormat = this.ctx.FLOAT;
        // format = this.ctx.FLOAT;
      } else if(this.texdepth & FrameBuffer.TEXTYPE_HALF_FLOAT_OES) {
        // t = gl.renderer.isWebgl2 ? e.HALF_FLOAT : e.HALF_FLOAT_OES;
        //     gl.renderer.extensions['OES_texture_half_float'] ? gl.renderer.extensions['OES_texture_half_float'].HALF_FLOAT_OES : 
        //     gl.UNSIGNED_BYTE;
        const e = this.ctx.getExtension('OES_texture_half_float');
        t = this.renderer.isWebgl2 ? this.ctx.HALF_FLOAT : e.HALF_FLOAT_OES;
        // format = gl.RGBA;
        if(this.renderer.isWebgl2) {
          internalFormat = this.ctx.RGBA16F;
        }
        // internalFormat = gl.RGB32F;
        // format = gl.RGB32F;
        // window.gl = gl
        // t = e.HALF_FLOAT_OES;
      } else {
        t = this.ctx.UNSIGNED_BYTE;
      }
      const type = t;
      const data = this.data;
      this.ctx.texImage2D(this.ctx.TEXTURE_2D, level, internalFormat,
                    this.width*this.pxRatio, this.height*this.pxRatio, border,
                    format, type, data);
      // gl.generateMipmap(gl.TEXTURE_2D);

      // set the filtering so we don't need mips
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MIN_FILTER, this.ctx.NEAREST);
      this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MAG_FILTER, this.ctx.NEAREST);
      
      // Set the parameters based on the passed type
      if(this.tiling === Texture.IMAGETYPE_TILE) {
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.REPEAT);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.REPEAT);
      } else if(this.tiling === Texture.IMAGETYPE_MIRROR) {
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.MIRRORED_REPEAT);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.MIRRORED_REPEAT);
      } else if(this.tiling === Texture.IMAGETYPE_REGULAR) {
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_S, this.ctx.CLAMP_TO_EDGE);
        this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_WRAP_T, this.ctx.CLAMP_TO_EDGE);
      }
    }
    
    // Create and bind the framebuffer
    const fb = this.ctx.createFramebuffer();
    this.ctx.bindFramebuffer(this.ctx.FRAMEBUFFER, fb);
    
    if(this.depthTesting) {
      var ext = this.ctx.getExtension('WEBGL_depth_texture');
      let depth = this.ctx.createTexture();
      this.ctx.bindTexture(this.ctx.TEXTURE_2D, depth);
      this.ctx.texImage2D(
        this.ctx.TEXTURE_2D, 0, this.ctx.DEPTH_COMPONENT, this.width*this.pxRatio, this.height*this.pxRatio, 0, this.ctx.DEPTH_COMPONENT, this.ctx.UNSIGNED_SHORT, null);
      this.ctx.framebufferTexture2D(this.ctx.FRAMEBUFFER, this.ctx.DEPTH_ATTACHMENT, this.ctx.TEXTURE_2D, depth, 0);
    }

    // attach the texture as the first color attachment
    const attachmentPoint = this.ctx.COLOR_ATTACHMENT0;
    const level = 0;
    this.ctx.framebufferTexture2D(
      this.ctx.FRAMEBUFFER, 
      attachmentPoint, 
      this.ctx.TEXTURE_2D, 
      targetTexture, 
      level);

    return {
      fb: fb,
      frameTexture: targetTexture
    };
  }
  bind() {
    // find the active texture based on the index
    let uniform = this.ctx.getUniformLocation(this.renderer.currentProgram.program, `b_${this.#name}`);
    
    // Set the texture unit to the uniform
    this.ctx.uniform1i(uniform, this.value);
    this.ctx.activeTexture(this.ctx.TEXTURE0 + this.value);
    // Bind the texture
    this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.#activeFB.frameTexture);
  }
  render(n) {
    this.bind();
    
    // Finally, ping-pong the texture
    this.#activeFB = this.#activeFB === this.#fb1 ? this.#fb2 : this.#fb1;
    
    // this.renderer.render(n, this.#activeFB);
    this.renderer.render(n, this.#activeFB, [this.width, this.height]);
  }

  set data(value) {
    if(value instanceof Float32Array) this.#data = value;
  }
  get data() {
    return this.#data || null;
  }
  set width(value) {
    if(value > 0) this.#width = value;
  }
  get width() {
    return this.#width || 1;
  }
  set height(value) {
    if(value > 0) this.#height = value;
  }
  get height() {
    return this.#height || 1;
  }
  set pxRatio(value) {
    if(value > 0) this.#pxRatio = value;
  }
  get pxRatio() {
    return this.#pxRatio || 1;
  }
  set tiling(value) {
    if([Texture.IMAGETYPE_REGULAR, Texture.IMAGETYPE_TILE, Texture.IMAGETYPE_MIRROR].indexOf(value) > -1) this.#tiling = value;
  }
  get tiling() {
    return this.#tiling;
  }
  set texdepth(value) {
    if([FrameBuffer.TEXTYPE_FLOAT, FrameBuffer.TEXTYPE_UNSIGNED_BYTE, FrameBuffer.TEXTYPE_HALF_FLOAT_OES].indexOf(value) > -1) this.#texdepth = value;
  }
  get texdepth() {
    return this.#texdepth;
  }
}
class Camera {
  static #defaultOptions = {
    fov: 30 * Math.PI / 180,
    aspect: window.innerWidth / window.innerHeight,
    near: .5,
    far: 100,
    pos: new Vec3(3, 1, -5),
    target: new Vec3(0, 0, 0),
    up: new Vec3(0, 1, 0)
  }

  #fov
  #aspect
  #near
  #far
  #pos
  #target
  #up
  #updateDebounce
  
  #model
  #view
  #proj
  #MVP
  
  #u_model
  #u_view
  #u_proj
  #u_MVP
  
  #q
  
  #name
  
  constructor(renderer, name, options) {
    options = Object.assign({}, Camera.#defaultOptions, options);
    
    this.renderer = renderer; 
    this.ctx = renderer.ctx;
    
    this.fov = options.fov;
    this.aspect = options.aspect;
    this.near = options.near;
    this.far = options.far;
    this.pos = options.pos;
    this.target = options.target;
    this.up = options.up;
    
    this.q = new Quat();
    
    this.name = name;
    
    this.update(true);
  }
  set q(value) {
    if(value instanceof Quat) {
      this.#q = value;
      this.#model = quatToMat4(this.#q);
      this.#u_model = new Uniform(this.ctx, 'm_model', Uniform.TYPE_M4, this.#model.array);
    }
  }
  get q() {
    return this.#q || new Quat();
  }
  update(nt = false) {
    clearTimeout(this.#updateDebounce);
    // this.#updateDebounce = setTimeout(() => {
      this.#model = new Mat4();
      this.#view = Mat4.lookAt(this.pos, this.target, this.up);
      this.#proj = Mat4.perspective(this.fov, this.aspect, this.near, this.far);
      this.#MVP = this.#proj.multiplyNew(this.#view).multiply(this.#model);
      
      this.#u_view = new Uniform(this.ctx, 'm_view', Uniform.TYPE_M4, this.#view.array);
      this.#u_proj = new Uniform(this.ctx, 'm_proj', Uniform.TYPE_M4, this.#proj.array);
      this.#u_MVP = new Uniform(this.ctx, 'm_MVP', Uniform.TYPE_M4, this.#MVP.array);
      
      this.setup = true;
    // }, nt ? 0 : 50);
  }

  set name(value) {
    if(typeof value === 'string') this.#name = value;
  }
  get name() {
    return this.#name || 'camera';
  }
  set fov(value) {
    if(!isNaN(value)) this.#fov = value;
  }
  get fov() {
    return this.#fov;
  }
  set aspect(value) {
    if(!isNaN(value)) this.#aspect = value;
  }
  get aspect() {
    return this.#aspect;
  }
  set near(value) {
    if(!isNaN(value)) this.#near = value;
  }
  get near() {
    return this.#near;
  }
  set far(value) {
    if(!isNaN(value)) this.#far = value;
  }
  get far() {
    return this.#far;
  }
  set pos(value) {
    if(value instanceof Vec3) this.#pos = value;
  }
  get pos() {
    return this.#pos;
  }
  set target(value) {
    if(value instanceof Vec3) this.#target = value;
  }
  get target() {
    return this.#target;
  }
  set up(value) {
    if(value instanceof Vec3) this.#up = value;
  }
  get up() {
    return this.#up;
  }
  get u_model() {
    return this.#u_model;
  }
  get u_view() {
    return this.#u_view;
  }
  get u_proj() {
    return this.#u_proj;
  }
  get u_MVP() {
    return this.#u_MVP;
  }
  get uniforms() {
    return [this.u_model, this.u_view, this.u_proj, this.u_MVP];
  }
}

const dimensions = [window.innerWidth, window.innerHeight];
const texturesize = 128;
const particles = texturesize * texturesize;
const textureArraySize = texturesize*texturesize*4.;

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);

const renderer = new Renderer(canvas, { width: dimensions[0], height: dimensions[1], alpha: false, premultipliedAlpha: true, preserveDrawingBuffer: true });
const ctx = renderer.ctx;

let drawing = new Float32Array([ -1.0,  1.0,   1.0,  1.0,   -1.0, -1.0,   1.0, -1.0]);
let vertices = new Float32Array(particles * 6 * 3).fill(0);
for (let i = 0; i < vertices.length; i += 6 * 3) {
  vertices[i] = -1;
  vertices[i+2] = 1;
  vertices[i+1] = 0;
  
  vertices[i+3] = -1;
  vertices[i+5] = -1;
  vertices[i+4] = 0;
  
  vertices[i+6] = 1;
  vertices[i+8] = -1;
  vertices[i+7] = 0;
  
  
  vertices[i+9] = 1;
  vertices[i+11] = -1;
  vertices[i+10] = 0;
  
  vertices[i+12] = 1;
  vertices[i+14] = 1;
  vertices[i+13] = 0;
  
  vertices[i+15] = -1;
  vertices[i+17] = 1;
  vertices[i+16] = 0;
}
window.vertices = vertices;
let references = new Float32Array(particles * 6 * 2).fill(0);
let referenceIndex = 0;
for (let i = 0; i < references.length; i += 6 * 2) {
  let index = referenceIndex / 2;
  referenceIndex += 2;
  
  const x = (index % texturesize) / texturesize;
  const y = Math.floor(index / texturesize) / texturesize;
  
    // console.log(i, '----');
  for(let j = 0; j < 6 * 2; j += 2) {
  
    // console.log(i, i + j, i + j + 1);
    // console.log(x, y);
    // console.log('  ');
    references[i + j] = x;
    references[i + j + 1] = y;
  }
}

const drawBuffer = new Buffer(ctx, drawing);
const particleBuffer = new Buffer(ctx, vertices, { 
  attributes: [{
        name: 'particle',
        numComponents: 3
      }]
});
const referenceBuffer = new Buffer(ctx, references, { 
  attributes: [{
        name: 'reference',
        numComponents: 2
      }]
});

const vertexShader_particle = document.getElementById('vertexShader_particle').innerText;
const vertexShader_buffer = document.getElementById('vertexShader_buffer').innerText;

const programPosition = new Program(ctx, vertexShader_buffer, document.getElementById('fragmentShader_position').innerText, {  
  renderType: Program.RENDER_STRIP });
const programVelocity = new Program(ctx, vertexShader_buffer, document.getElementById('fragmentShader_velocity').innerText, {  
  renderType: Program.RENDER_STRIP });
const programBuffer = new Program(ctx, vertexShader_buffer, document.getElementById('fragmentShader_buffer').innerText, { 
  clearColour: [.1,.1,.15, 1], 
  // blending: Renderer.BLENDING_ADDITIVE, 
  depthTesting: true, 
  transparent: false,  
  renderType: Program.RENDER_STRIP });
const programBlur = new Program(ctx, vertexShader_buffer, document.getElementById('fragmentShader_blur').innerText, {  
  clearColour: [.1,.1,.15, 0], 
  renderType: Program.RENDER_STRIP });
const programBloom = new Program(ctx, vertexShader_buffer, document.getElementById('fragmentShader_bloom').innerText, {  
  clearColour: [.1,.1,.15, 0], 
  renderType: Program.RENDER_STRIP });
const programMain = new Program(ctx, vertexShader_particle, document.getElementById('fragmentShader_particle').innerText, { 
  clearColour: [.05, .15, .35, 1.], 
  // renderType: Program.RENDER_POINTS, 
  renderType: Program.RENDER_TRIANGLES, 
  // blending: Renderer.BLENDING_MULTIPLY, 
  depthTesting: true, 
  transparent: true
});

const time = new Uniform(ctx, 'time', Uniform.TYPE_FLOAT, 100);
const uDelta = new Uniform(ctx, 'delta', Uniform.TYPE_FLOAT, 100);
const mouse = new Uniform(ctx, 'mouse', Uniform.TYPE_V2, [0.,0.]);
const frame = new Uniform(ctx, 'frame', Uniform.TYPE_INT, 0);
const nsize = new Uniform(ctx, 'nsize', Uniform.TYPE_FLOAT, .1);
const nreset = new Uniform(ctx, 'nreset', Uniform.TYPE_BOOL, false);
const transition = new Uniform(ctx, 'transition', Uniform.TYPE_BOOL, false);
const transition_val = new Uniform(ctx, 'transition_val', Uniform.TYPE_FLOAT, 0);
const bloomstep = new Uniform(ctx, 'bloomstep', Uniform.TYPE_INT, 0);
const seed = new Uniform(ctx, 'seed', Uniform.TYPE_FLOAT, 0);

const originData = new Float32Array(particles * 4).fill(0);
const positionData = new Float32Array(particles * 4).fill(0);
const velocityData = new Float32Array(particles * 4).fill(0);
let bailout = 0.;
const create = (i) => {
  let radius = 5.;
  let minRadius = 1.;
  const rn = Math.random();
  let phi = Math.random() * Math.PI * 2.;
  let costheta = Math.random() * 2. - 1.;
  let u = Math.random();

  let theta = Math.acos( costheta );
  // let r = radius * Math.max(.8, Math.min(Math.cbrt( u ), .85));
  let r = radius * Math.cbrt( u );

  let x = r * Math.sin( theta ) * Math.cos( phi );
  let y = r * Math.sin( theta ) * Math.sin( phi );
  let z = r * Math.cos( theta );
  /*
  const posit = new Vec2(z, x);
  if(Math.abs(y) > .5) {
    bailout++;
    if(bailout >= 100.) return;
    else {
      create(i);
      return;
    }
  } else {
    bailout = 0.;
  }*/

  positionData[i] = x;
  positionData[i + 1] = y;
  positionData[i + 2] = z;
  positionData[i + 3] = 1;
  
  originData[i] = x;
  originData[i + 1] = y;
  originData[i + 2] = z;
  originData[i + 3] = 1;
  
  velocityData[i] = x*2;
  velocityData[i + 1] = y*2;
  velocityData[i + 2] = z*2;
  velocityData[i + 3] = 1;
}
for (let i = 0; i < textureArraySize; i += 4) {
  create(i);
}

setTimeout(() => {
  requestAnimationFrame(run);
}, 10);

const primarybuffer = new FrameBuffer(renderer, 'prime', {
  width: dimensions[0],
  height: dimensions[1],
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_REGULAR,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: Math.min(window.devicePixelRatio, 2),
  depthTesting: true
});
const blurBuffer = new FrameBuffer(renderer, 'blur', {
  width: dimensions[0],
  height: dimensions[1],
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_REGULAR,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: Math.min(window.devicePixelRatio, 2),
  depthTesting: true
});
const bloomBuffer = new FrameBuffer(renderer, 'bloom', {
  width: dimensions[0],
  height: dimensions[1],
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_REGULAR,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: Math.min(window.devicePixelRatio, 2)
});
const positionBuffer = new FrameBuffer(renderer, 'position', {
  width: texturesize,
  height: texturesize,
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_TILE,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: 1,
  data: positionData
});
const velocityBuffer = new FrameBuffer(renderer, 'velocity', {
  width: texturesize,
  height: texturesize,
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_TILE,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: 1,
  data: velocityData
});
const originBuffer = new FrameBuffer(renderer, 'origin', {
  width: texturesize,
  height: texturesize,
  // tiling: Texture.IMAGETYPE_TILE,
  tiling: Texture.IMAGETYPE_TILE,
  texdepth: FrameBuffer.TEXTYPE_FLOAT,
  pxRatio: 1,
  data: originData
});

const cp = new Vec3(-25, 75, 100);
cp.length = 100;

const compositions = [
  {
    pos: new Vec3(0, 0, 40),
    target: new Vec3(0,0,0),
    endPos: new Vec3(0, 3, 30),
    endTarget: new Vec3(0,0,0),
    time: 50
  },
  {
    pos: new Vec3(0, -30, 30),
    target: new Vec3(0, 30, 0),
    endPos: new Vec3(0, -30, 0),
    endTarget: new Vec3(0, 30, 0),
    time: 20
  },
  {
    pos: new Vec3(0, 100, 0),
    target: new Vec3(0, 0, 0),
    endPos: new Vec3(0, 20, 40),
    endTarget: new Vec3(0, 20, -20),
    time: 40
  },
  {
    pos: new Vec3(0, 20, 40),
    target: new Vec3(15, 0, -20),
    endPos: new Vec3(0, 20, 40),
    endTarget: new Vec3(-15, 0, -20),
    time: 20
  },
  {
    pos: new Vec3(0, 20, -60),
    target: new Vec3(5, 0, 0),
    endPos: new Vec3(0, 30, 50),
    endTarget: new Vec3(5, 0, 0),
    time: 50
  },
  {
    pos: new Vec3(0, -20, 0),
    target: new Vec3(0, 0, 0),
    endPos: new Vec3(10, 80, 10),
    endTarget: new Vec3(0, 0, 0),
    time: 40
  },
  {
    pos: new Vec3(30, 0, 0),
    target: new Vec3(20, 0, 0),
    endPos: new Vec3(30, 0, 30),
    endTarget: new Vec3(0, 0, 0),
    time: 15
  },
  {
    pos: new Vec3(80, 0, 0),
    target: new Vec3(0, 0, 0),
    endPos: new Vec3(0, 0, 80),
    endTarget: new Vec3(0, 0, 0),
    time: 40
  },
  {
    pos: new Vec3(0, 10, 0),
    target: new Vec3(0, 0, 0),
    endPos: new Vec3(10, 10, 60),
    endTarget: new Vec3(0, 0, 0),
    time: 40
  },
  {
    pos: cp.clone().scaleNew(.2),
    target: new Vec3(0, 0, 0),
    endPos: cp.clone(),
    endTarget: new Vec3(0, 0, 0),
    time: 30
  },
  {
    pos: new Vec3(0, 2, 30),
    target: new Vec3(-5, 0, 0),
    endPos: new Vec3(0, 5, -20),
    endTarget: new Vec3(0, 0, 0),
    time: 30
  },
  {
    pos: new Vec3(0, 5, 0),
    target: new Vec3(0,0,0),
    endPos: new Vec3(0, 40, 5),
    endTarget: new Vec3(0,0,0),
    time: 30
  }
];

const cam = new Camera(renderer, 'main', { fov: 1.8, far: 500, aspect: dimensions[0]/dimensions[1] });
let camop = compositions[0];
let camtime = 0;
let pointerdown = false;
let pointermoving = false;
let pointerTimeout;
let lastPos = new Vec2();
window.addEventListener('pointerdown', (e) => {
  pointerdown = true;
  pointerTimeout = setTimeout(() => {
    pointermoving = true;
  }, 200)
  lastPos = new Vec2(e.x, e.y);
});
window.addEventListener('pointerup', (e) => {
  pointerdown = false;
  pointerTimeout = setTimeout(() => {
    pointermoving = false;
  }, 0)
  clearTimeout(pointerTimeout);
});
window.addEventListener('pointermove', (e) => {
  if(pointerdown) {
    let newPos = new Vec2(e.x, e.y);
    let diff = newPos.clone().subtract(lastPos);
    lastPos = newPos.clone();
    let q = cam.q;
    q.rotateY(diff.x * -.01);
    q.rotateX(diff.y * -.01);
    cam.q = q;
  }
//   let q = cam.q;
  
//       this.#u_model = new Uniform(this.ctx, 'm_model', Uniform.TYPE_M4, this.#model.array);
});

let playing = true;
const setPlaying = (value) => {
  playing = value;
}

const playpause = document.querySelector('.playpause');
const playpauseCheck = document.querySelector('.playpause input');
playpauseCheck.addEventListener('change', (e)=> {
  playpause.classList.toggle('checked', e.target.checked);
  setPlaying(e.target.checked);
});

let autoTransitionTimer = 0;
let timeToTransition = 0;
const setupValues = (i) => {
  transition.value = false;
  nsize.value = Math.random() * .5 + .1;
  nreset.value = true;
  dimensions[0] = window.innerWidth;
  dimensions[1] = window.innerHeight;
  cam.aspect = dimensions[0]/dimensions[1];
  console.log(i);
  // camop = compositions[i];,
  camop = {
    pos: compositions[i].pos.scaleNew(.5),
    target: compositions[i].target.scaleNew(.5),
    endPos: compositions[i].endPos.scaleNew(.5),
    endTarget: compositions[i].endTarget.scaleNew(.5),
    time: compositions[i].time
  }
  cam.pos = camop.pos;
  cam.target = camop.target;
  cam.update(true);
  cam.q = new Quat();
  camtime = 0;
  renderer.resize(dimensions[0], dimensions[1]);
  primarybuffer.resize(dimensions[0], dimensions[1]);
  blurBuffer.resize(dimensions[0], dimensions[1]);
  
  time.value = -10000;
  seed.value = Math.random() * -1000.;
  setTimeout(() => {
    seed.value += Math.random() * .5;
  }, 5000);

  timeToTransition = camop.time*1000;
  autoTransitionTimer = 0;
  // autoTransitionTimer = setTimeout(setupTransition, );
}

const setupTransition = () => {
  if(transition.value === true) return;
  clearTimeout(autoTransitionTimer);
  transition.value = true;
  transition_val.value = 0;
  setTimeout(() => {
    setupValues(Math.floor(Math.random()*compositions.length));
  }, 2000);
}

setupValues(0);

let oldpos = 0;
canvas.addEventListener('click', (e) => {
  if(playing !== true) return;
  if(pointermoving === true) return;
  setupTransition();
});

let timeout;
window.addEventListener('resize', () => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    dimensions[0] = window.innerWidth;
    dimensions[1] = window.innerHeight;
    cam.aspect = dimensions[0]/dimensions[1];
    cam.update(true);
    renderer.resize(dimensions[0], dimensions[1]);
    primarybuffer.resize(dimensions[0], dimensions[1]);
    blurBuffer.resize(dimensions[0], dimensions[1]);
    bloomBuffer.resize(dimensions[0], dimensions[1]);
  }, 100);
});


const cubeInOut = d => {
  if (d < 0.5) return 4 * d**3;
  return 1 - (-2 * d + 2)**3 * 0.5;
}

const opx = renderer.pxRatio;
let then = 0;
// let framenum = 0;
// let framesPerFrame = 10;
// let gif = new gifJs({
//   workers: 2,
//   quality: 10
// });
// gif.on('finished', function(blob) {
//   console.log('ss')
//   window.open(URL.createObjectURL(blob));
// });

// const offscreenCanvas = document.createElement("canvas");
// offscreenCanvas.className = 'osc';
// offscreenCanvas.width = canvas.width;
// offscreenCanvas.height = canvas.height;
// const osctx = offscreenCanvas.getContext("2d");
// document.body.appendChild(offscreenCanvas);

let gifDone = false;
const run = (delta) => {
  
//   if(framenum < 10 * framesPerFrame) {
//     if(framenum % framesPerFrame == 0) {
//       // gif.addFrame(canvas, {delay: 100});

//       osctx.drawImage(canvas,0,0);

//       gif.addFrame(offscreenCanvas, {copy: true, delay: 100});
//       // gif.addFrame(ctx, {copy: true});
//     }
//     framenum++;
//   } else if(gifDone === false) {
//     console.log(framenum)

//     gif.render();
    
//     window.gif = gif;
    
//     gifDone = true;
//   }
  
	let now = Date.now() / 1000;
	let _delta = now - then;
	then = now;
  
  if(_delta > 1000) {
    requestAnimationFrame(run);
    return;
  }
  
  
  if(playing) {
    
    // timeToTransition = camop.time*1000;
    autoTransitionTimer += _delta*1000;
    if(autoTransitionTimer > timeToTransition) {
      timeToTransition = Infinity;
      setupTransition();
    }

    bloomstep.value = 0;

    uDelta.value = Math.min(_delta, 0.5);
    time.value += _delta * .05;

    frame.value += 1;

    if(transition.value === true) {
      transition_val.value += _delta * .5;
    } else if(transition_val.value >= 0.) {
      transition_val.value -= _delta * .5;
    }
    
    camtime += _delta;
    if(camtime <= camop.time && camop.endPos) {
      let ntime = Math.min(camtime/camop.time, 1.);
      let tween = cubeInOut(ntime);
      cam.pos = Vec3.lerp(camop.pos, camop.endPos, tween);
      cam.target = Vec3.lerp(camop.target, camop.endTarget, tween);
      cam.update(true);
    }

    renderer.setViewport([velocityBuffer.width, velocityBuffer.height]);
    renderer.setupProgram(programVelocity, [drawBuffer], [], [time, frame, mouse, velocityBuffer, positionBuffer, nsize, seed]);
    velocityBuffer.render(4);

    renderer.setupProgram(programPosition, [drawBuffer], [], [time, frame, mouse, velocityBuffer, positionBuffer, uDelta, originBuffer, nreset]);
    positionBuffer.render(4);
  }
  
  renderer.setViewport();
  renderer.setupProgram(programMain, [particleBuffer, referenceBuffer], [], [time, frame, mouse, velocityBuffer, primarybuffer, positionBuffer, ...cam.uniforms, transition, transition_val]);
  primarybuffer.render(particles * 6);
   // renderer.render(particles * 6);
  
  renderer.setupProgram(programBlur, [drawBuffer], [], [time, mouse, primarybuffer, positionBuffer, blurBuffer]);
  blurBuffer.render(4);
  
  // bloomstep.value = 0;
  // renderer.setupProgram(programBloom, [drawBuffer], [], [time, mouse, primarybuffer, positionBuffer, blurBuffer, bloomstep, bloomBuffer]);
  // bloomBuffer.render(4);
  // bloomstep.value = 1;
  // renderer.setupProgram(programBloom, [drawBuffer], [], [time, mouse, primarybuffer, positionBuffer, blurBuffer, bloomstep, bloomBuffer]);
  // bloomBuffer.render(4);
  
  renderer.setupProgram(programBuffer, [drawBuffer], [], [time, mouse, primarybuffer, positionBuffer, bloomBuffer, blurBuffer, transition, transition_val]);
  renderer.render(4);
  
  nreset.value = false;
  
  requestAnimationFrame(run);
}