class ShaderProgram {

    constructor( holder, options = {} ) {
  
      options = Object.assign( {
        antialias: false,
        depthTest: false,
        mousemove: false,
        autosize: true,
        side: 'front',
        vertex: `
          precision highp float;
  
          attribute vec4 a_position;
          attribute vec4 a_color;
  
          uniform float u_time;
          uniform vec2 u_resolution;
          uniform vec2 u_mousemove;
          uniform mat4 u_projection;
  
          varying vec4 v_color;
  
          void main() {
  
            gl_Position = u_projection * a_position;
            gl_PointSize = (10.0 / gl_Position.w) * 100.0;
  
            v_color = a_color;
  
          }`,
        fragment: `
          precision highp float;
  
          uniform sampler2D u_texture;
          uniform int u_hasTexture;
  
          varying vec4 v_color;
  
          void main() {
  
            if ( u_hasTexture == 1 ) {
  
              gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);
  
            } else {
  
              gl_FragColor = v_color;
  
            }
  
          }`,
        uniforms: {},
        buffers: {},
        camera: {},
        texture: null,
        onUpdate: ( () => {} ),
        onResize: ( () => {} ),
      }, options )
  
      const uniforms = Object.assign( {
        time: { type: 'float', value: 0 },
        hasTexture: { type: 'int', value: 0 },
        resolution: { type: 'vec2', value: [ 0, 0 ] },
        mousemove: { type: 'vec2', value: [ 0, 0 ] },
        projection: { type: 'mat4', value: [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ] },
      }, options.uniforms )
  
      const buffers = Object.assign( {
        position: { size: 3, data: [] },
        color: { size: 4, data: [] },
      }, options.buffers )
  
      const camera = Object.assign( {
        fov: 60,
        near: 1,
        far: 10000,
        aspect: 1,
        z: 100,
        perspective: true,
      }, options.camera )
  
      const canvas = document.createElement( 'canvas' )
      const gl = canvas.getContext( 'webgl', { antialias: options.antialias } )
  
      if ( ! gl ) return false
  
      this.count = 0
      this.gl = gl
      this.canvas = canvas
      this.camera = camera
      this.holder = holder
      this.onUpdate = options.onUpdate
      this.onResize = options.onResize
      this.data = {}
  
      holder.appendChild( canvas )
  
      this.createProgram( options.vertex, options.fragment )
  
      this.createBuffers( buffers )
      this.createUniforms( uniforms )
  
      this.updateBuffers()
      this.updateUniforms()
  
      this.createTexture( options.texture )
  
      gl.enable( gl.BLEND )
      gl.enable( gl.CULL_FACE )
      gl.blendFunc( gl.SRC_ALPHA, gl.ONE )
      gl[ options.depthTest ? 'enable' : 'disable' ]( gl.DEPTH_TEST )
  
      if ( options.autosize )
        window.addEventListener( 'resize', e => this.resize( e ), false )
      if ( options.mousemove )
        window.addEventListener( 'mousemove', e => this.mousemove( e ), false )
  
      this.resize()
  
      this.update = this.update.bind( this )
      this.time = { start: performance.now(), old: performance.now() }
      this.update()
  
    }
  
    mousemove( e ) {
  
      let x = e.pageX / this.width * 2 - 1
      let y = e.pageY / this.height * 2 - 1
  
      this.uniforms.mousemove = [ x, y ]
  
    }
  
    resize( e ) {
  
      const holder = this.holder
      const canvas = this.canvas
      const gl = this.gl
  
      const width = this.width = holder.offsetWidth
      const height = this.height = holder.offsetHeight
      const aspect = this.aspect = width / height
      const dpi = this.dpi = devicePixelRatio
  
      canvas.width = width * dpi
      canvas.height = height * dpi
      canvas.style.width = width + 'px'
      canvas.style.height = height + 'px'
  
      gl.viewport( 0, 0, width * dpi, height * dpi )
      gl.clearColor( 0, 0, 0, 0 )
  
      this.uniforms.resolution = [ width, height ]
      this.uniforms.projection = this.setProjection( aspect )
  
      this.onResize( width, height, dpi )
  
    }
  
    setProjection( aspect ) {
  
      const camera = this.camera
  
      if ( camera.perspective ) {
  
        camera.aspect = aspect
  
        const fovRad = camera.fov * ( Math.PI / 180 )
        const f = Math.tan( Math.PI * 0.5 - 0.5 * fovRad )
        const rangeInv = 1.0 / ( camera.near - camera.far )
  
        const matrix = [
          f / camera.aspect, 0, 0, 0,
          0, f, 0, 0,
          0, 0, (camera.near + camera.far) * rangeInv, -1,
          0, 0, camera.near * camera.far * rangeInv * 2, 0
        ]
  
        matrix[ 14 ] += camera.z
        matrix[ 15 ] += camera.z
  
        return matrix
  
      } else {
  
        return [
           2 / this.width, 0, 0, 0,
           0, -2 / this.height, 0, 0,
           0, 0, 1, 0,
          -1, 1, 0, 1,
        ]
  
      }
  
    }
  
    createShader( type, source ) {
  
      const gl = this.gl
      const shader = gl.createShader( type )
  
      gl.shaderSource( shader, source )
      gl.compileShader( shader )
  
      if ( gl.getShaderParameter (shader, gl.COMPILE_STATUS ) ) {
  
        return shader
  
      } else {
  
        console.log( gl.getShaderInfoLog( shader ) )
        gl.deleteShader( shader )
  
      }
  
    }
  
    createProgram( vertex, fragment ) {
  
      const gl = this.gl
  
      const vertexShader = this.createShader( gl.VERTEX_SHADER, vertex )
      const fragmentShader = this.createShader( gl.FRAGMENT_SHADER, fragment )
  
      const program = gl.createProgram()
  
      gl.attachShader( program, vertexShader )
      gl.attachShader( program, fragmentShader )
      gl.linkProgram( program )
  
      if ( gl.getProgramParameter( program, gl.LINK_STATUS ) ) {
  
        gl.useProgram( program )
        this.program = program
  
      } else {
  
        console.log( gl.getProgramInfoLog( program ) )
        gl.deleteProgram( program )
  
      }
  
    }
  
    createUniforms( data ) {
  
      const gl = this.gl
      const uniforms = this.data.uniforms = data
      const values = this.uniforms = {}
  
      Object.keys( uniforms ).forEach( name => {
  
        const uniform = uniforms[ name ]
  
        uniform.location = gl.getUniformLocation( this.program, 'u_' + name )
  
        Object.defineProperty( values, name, {
          set: value => {
  
            uniforms[ name ].value = value
            this.setUniform( name, value )
  
          },
          get: () => uniforms[ name ].value
        } )
  
      } )
  
    }
  
    setUniform( name, value ) {
  
      const gl = this.gl
      const uniform = this.data.uniforms[ name ]
  
      uniform.value = value
  
      switch ( uniform.type ) {
        case 'int': {
          gl.uniform1i( uniform.location, value )
          break
        }
        case 'float': {
          gl.uniform1f( uniform.location, value )
          break
        }
        case 'vec2': {
          gl.uniform2f( uniform.location, ...value )
          break
        }
        case 'vec3': {
          gl.uniform3f( uniform.location, ...value )
          break
        }
        case 'vec4': {
          gl.uniform4f( uniform.location, ...value )
          break
        }
        case 'mat2': {
          gl.uniformMatrix2fv( uniform.location, false, value )
          break
        }
        case 'mat3': {
          gl.uniformMatrix3fv( uniform.location, false, value )
          break
        }
        case 'mat4': {
          gl.uniformMatrix4fv( uniform.location, false, value )
          break
        }
      }
  
      // ivec2       : uniform2i,
      // ivec3       : uniform3i,
      // ivec4       : uniform4i,
      // sampler2D   : uniform1i,
      // samplerCube : uniform1i,
      // bool        : uniform1i,
      // bvec2       : uniform2i,
      // bvec3       : uniform3i,
      // bvec4       : uniform4i,
  
    }
  
    updateUniforms() {
  
      const gl = this.gl
      const uniforms = this.data.uniforms
  
      Object.keys( uniforms ).forEach( name => {
  
        const uniform = uniforms[ name ]
  
        this.uniforms[ name ] = uniform.value
  
      } )
  
    }
  
    createBuffers( data ) {
  
      const gl = this.gl
      const buffers = this.data.buffers = data
      const values = this.buffers = {}
  
      Object.keys( buffers ).forEach( name => {
  
        const buffer = buffers[ name ]
  
        buffer.buffer = this.createBuffer( 'a_' + name, buffer.size )
  
        Object.defineProperty( values, name, {
          set: data => {
  
            buffers[ name ].data = data
            this.setBuffer( name, data )
  
            if ( name == 'position' )
              this.count = buffers.position.data.length / 3
  
          },
          get: () => buffers[ name ].data
        } )
  
      } )
  
    }
  
    createBuffer( name, size ) {
  
      const gl = this.gl
      const program = this.program
  
      const index = gl.getAttribLocation( program, name )
      const buffer = gl.createBuffer()
  
      gl.bindBuffer( gl.ARRAY_BUFFER, buffer )
      gl.enableVertexAttribArray( index )
      gl.vertexAttribPointer( index, size, gl.FLOAT, false, 0, 0 )
  
      return buffer
  
    }
  
    setBuffer( name, data ) {
  
      const gl = this.gl
      const buffers = this.data.buffers
  
      if ( name == null && ! gl.bindBuffer( gl.ARRAY_BUFFER, null ) ) return
  
      gl.bindBuffer( gl.ARRAY_BUFFER, buffers[ name ].buffer )
      gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( data ), gl.STATIC_DRAW )
  
    }
  
    updateBuffers() {
  
      const gl = this.gl
      const buffers = this.buffers
  
      Object.keys( buffers ).forEach( name =>
        buffers[ name ] = buffer.data
      )
  
      this.setBuffer( null )
  
    }
  
    createTexture( src ) {
  
      const gl = this.gl
      const texture = gl.createTexture()
  
      gl.bindTexture( gl.TEXTURE_2D, texture )
      gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array( [ 0, 0, 0, 0 ] ) )
  
      this.texture = texture
  
      if ( src ) {
  
        this.uniforms.hasTexture = 1
        this.loadTexture( src )
  
      }
  
    }
  
    loadTexture( src ) {
  
      const gl = this.gl
      const texture = this.texture
  
      const textureImage = new Image()
  
      textureImage.onload = () => {
  
        gl.bindTexture( gl.TEXTURE_2D, texture )
  
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage )
  
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR )
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR )
  
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
  
        // gl.generateMipmap( gl.TEXTURE_2D )
  
      }
  
      textureImage.src = src
  
    }
  
    update() {
  
      const gl = this.gl
  
      const now = performance.now()
      const elapsed = ( now - this.time.start ) / 5000
      const delta = now - this.time.old
      this.time.old = now
  
      this.uniforms.time = elapsed
  
      if ( this.count > 0 ) {
        gl.clear( gl.COLORBUFFERBIT )
        gl.drawArrays( gl.POINTS, 0, this.count )
      }
  
      this.onUpdate( delta )
  
      requestAnimationFrame( this.update )
  
    }
  
  }
  
  const pointSize = 2.5
  
  const waves = new ShaderProgram( document.querySelector( '.waves' ), {
    texture: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAb1BMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8v0wLRAAAAJHRSTlMAC/goGvDhmwcExrVjWzrm29TRqqSKenRXVklANSIUE8mRkGpv+HOfAAABCElEQVQ4y4VT13LDMAwLrUHteO+R9f/fWMfO6dLaPeKVEECRxOULWsEGpS9nULDwia2Y+ALqUNbAWeg775zv+sA4/FFRMxt8U2FZFCVWjR/YrH4/H9sarclSKdPMWKzb8VsEeHB3m0shkhVCyNzeXeAQ9Xl4opEieX2QCGnwGbj6GMyjw9t1K0fK9YZunPXeAGsfJtYjwzxaBnozGGorYz0ypK2HzQSYx1y8DgSRo2ewOiyh2QWOEk1Y9OrQV0a8TiBM1a8eMHWYnRMy7CZ4t1CmyRkhSUvP3gRXyHOCLBxNoC3IJv//ZrJ/kxxUHPUB+6jJZZHrpg6GOjnqaOmzp4NDR48OLxn/H27SRQ08S0ZJAAAAAElFTkSuQmCC',
    uniforms: {
      size: { type: 'float', value: pointSize },
      field: { type: 'vec3', value: [ 0, 0, 0 ] },
      speed: { type: 'float', value: 5 },
    },
    vertex: `
      #define M_PI 3.1415926535897932384626433832795
  
      precision highp float;
  
      attribute vec4 a_position;
      attribute vec4 a_color;
  
      uniform float u_time;
      uniform float u_size;
      uniform float u_speed;
      uniform vec3 u_field;
      uniform mat4 u_projection;
  
      varying vec4 v_color;
  
      void main() {
  
        vec3 pos = a_position.xyz;
  
        pos.y += (
          cos(pos.x / u_field.x * M_PI * 8.0 + u_time * u_speed) +
          sin(pos.z / u_field.z * M_PI * 8.0 + u_time * u_speed)
        ) * u_field.y;
  
        gl_Position = u_projection * vec4( pos.xyz, a_position.w );
        gl_PointSize = ( u_size / gl_Position.w ) * 100.0;
  
        v_color = a_color;
  
      }`,
    fragment: `
      precision highp float;
  
      uniform sampler2D u_texture;
  
      varying vec4 v_color;
  
      void main() {
  
        gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);
  
      }`,
    onResize( w, h, dpi ) {
  
      const position = [], color = []
  
      const width = 400 * ( w / h )
      const depth = 400
      const height = 3
      const distance = 5
  
      for ( let x = 0; x < width; x += distance ) {
        for ( let z = 0; z < depth; z+= distance ) {
  
          position.push( - width / 2 + x, -30, -depth / 2 + z )
          color.push( 0, 1 - ( x / width ) * 1, 0.5 + x / width * 0.5, z / depth )
  
        }
      }
  
      this.uniforms.field = [ width, height, depth ]
  
      this.buffers.position = position
      this.buffers.color = color
  
      this.uniforms.size = ( h / 400) * pointSize * dpi
  
    },
  } )