Extensions

Source: core/Utils.js

import { Context } from "./Context";

/**
 * @typedef {Object} WebGLContext
 */

/**
 * @typedef {Object} WebGLProgram
 */

/**
 * @typedef {Object} RendererConfig
 * @property {Array<string>} locations
 * @property {WebGLContext} context
 */

/**
 * @typedef {Object} ContextConfig
 * @property {HTMLCanvasElement} canvas
 * @property {function} initCallback
 * @property {Object} contextAttributes
 */

/**
 * Location types
 * @const {Object}
 * @property {string} a
 * @property {string} u
 * @ignore
 */
const _locationTypes = {
  a: "Attrib",
  u: "Uniform",
};

/**
 * Shade creator
 * @param {WebGLContext} gl WebGL context
 * @param {number} shaderType Shader type
 * @param {string} shaderSource Shader source
 * @returns {Object} Shader
 * @ignore
 */
const _createShader = (gl, shaderSource, shaderType) => {
  const shader = gl.createShader(shaderType);

  gl.shaderSource(shader, shaderSource);
  gl.compileShader(shader);

  return shader;
};

/**
 * Common utilities
 * @typedef {Object} Utils
 * @property {number} THETA Useful number for conversion between rad and deg
 * @property {Object} GLSL Common glsl scripts
 * @property {Object} INFO Information about WebGL
 * @property {function(ContextConfig)} initContextConfig Create new context config
 * @property {function(RendererConfig)} initRendererConfig Create new renderer config
 * @property {function(function)} initApplication Call the callback function if the document.readyState interactive or complete
 * @property {function(WebGLContext, string, string):WebGLProgram} createProgram Create a WebGL program
 * @property {function(WebGLContext, WebGLProgram, Object):Object} getLocationsFor
 */

const _THETA = Math.PI / 180;

export const Utils = {
  /**
   * @property {number}
   */
  ALPHA: 90 * _THETA,

  /**
   * @property {number}
   */
  THETA: _THETA,

  /**
   * @property {Object}
   */
  // prettier-ignore
  GLSL: {
    VERSION: "#version 300 es\n",
    DEFINE: {
      RADIANS_360: "#define RADIANS_360 radians(360.)\n",
      HEIGHT: "#define HEIGHT 255.\n",
      Z: "#define Z vec3(0,1,-1)\n", // Z swizzle patterns: Z.xx=(0,0), Z.yx=(1,0), Z.xy=(0,1), Z.yy=(1,1)
      PI: `#define PI ${Math.PI}\n`,
    },
    RANDOM: 
      "float rand(vec2 p,float s){" + 
        "p=mod(p,vec2(1e4));" +
        "return fract(sin((p.x*12.9898+p.y*78.233)*s)*43758.5453);" +
      "}" +
      "float rand(vec2 p){" + 
        "return rand(p,1.);" +
      "}",
    RANDOM2:
      "float rand2(vec2 p,float s){" +
        "p=mod(p*100.,vec2(1e4))+100.;" +
        "return fract(" +
          "dot(" +
            "p," +
            "vec2(" +
              "sin(p.x+p.y)," +
              "cos(p.y-p.x)" +
            ")*s" +
          ")" +
        ");" +
      "}" +
      "float rand2(vec2 p){" + 
        "return rand2(p,1.);" +
      "}",
  },

  /**
   * @property {Object}
   */
  INFO: {
    isWebGl2Supported: false,
  },

  /**
   * Create new context config
   * @param {ContextConfig} config Context config
   * @returns {ContextConfig}
   */
  initContextConfig: (config = {}) => ({
    canvas: document.createElement("canvas"),
    ...config,
    contextAttributes: {
      powerPreference: "high-performance",
      preserveDrawingBuffer: false,
      premultipliedAlpha: false,
      ...(config.contextAttributes || {}),
    },
  }),

  /**
   * Create new renderer config
   * @param {RendererConfig} config Renderer config
   * @returns {RendererConfig}
   */
  initRendererConfig: (config = {}) => ({
    ...config,
    locations: config.locations ?? [],
    context: (config.config && config.config.context) ?? config.context ?? new Context(),
  }),

  setLocations: (config, locations) => (config.locations = [...config.locations, ...locations]),

  /**
   * Call the callback function if the document.readyState interactive or complete
   * @param {function} callback Callback function
   */
  initApplication: (callback) => {
    const loadedCallback = () => callback(Utils.INFO.isWebGl2Supported);

    ["interactive", "complete"].includes(document.readyState)
      ? loadedCallback()
      : document.addEventListener("DOMContentLoaded", loadedCallback, {
          once: true,
        });
  },

  /**
   * Create a WebGL program
   * @param {WebGLContext} gl WebGL context
   * @param {string} vertexShaderSource Vertex shader source
   * @param {string} fragmentShaderSource Fragment shader source
   * @returns {WebGLProgram} WebGL program
   */
  createProgram: (gl, vertexShaderSource, fragmentShaderSource) => {
    const vertexShader = _createShader(gl, vertexShaderSource, WebGL2RenderingContext.VERTEX_SHADER);
    const fragmentShader = _createShader(gl, fragmentShaderSource, WebGL2RenderingContext.FRAGMENT_SHADER);
    const program = gl.createProgram();

    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, WebGL2RenderingContext.LINK_STATUS)) {
      const vertexShaderInfo = gl.getShaderInfoLog(vertexShader);
      const fragmentShaderInfo = gl.getShaderInfoLog(fragmentShader);

      console.error(
        [
          "Program info: " + gl.getProgramInfoLog(program),
          "Validate status: " + gl.getProgramParameter(program, WebGL2RenderingContext.VALIDATE_STATUS),
          ...(vertexShaderInfo
            ? ["", `Vertex shader info: ${vertexShaderInfo}`, `Vertex shader: ${vertexShaderSource}`]
            : ""),
          ...(fragmentShaderInfo
            ? ["", `Fragment shader info: ${fragmentShaderInfo}`, `Fragment shader: ${fragmentShaderSource}`]
            : ""),
        ].join("\n"),
      );

      gl.deleteShader(vertexShader);
      gl.deleteShader(fragmentShader);
      gl.deleteProgram(program);

      throw "WebGL application stoped";
    }

    return program;
  },

  /**
   * @param {WebGLContext} gl WebGL context
   * @param {WebGLProgram} program WebGL program
   * @param {Array<string>} locationsDescriptor List of attributes and uniforms locations
   * @returns {Object}
   */
  getLocationsFor: (gl, program, locationsDescriptor) => {
    const locations = {};

    locationsDescriptor.forEach(
      (name) => (locations[name] = gl[`get${_locationTypes[name[0]]}Location`](program, name)),
    );

    return locations;
  },
};

const _gl = document.createElement("canvas").getContext("webgl2");
if (_gl) {
  Utils.INFO.isWebGl2Supported = true;
  Utils.INFO.maxTextureImageUnits = _gl.getParameter(WebGL2RenderingContext.MAX_TEXTURE_IMAGE_UNITS);

  const ext = _gl.getExtension("WEBGL_lose_context");
  if (ext) {
    ext.loseContext();
  }
}