import { Context } from "./Context";
import { noop } from "./helpers";
/**
* @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, shaderType, shaderSource) => {
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
*/
export const Utils = {
/**
* @property {number}
*/
THETA: Math.PI / 180,
/**
* @property {Object}
*/
// prettier-ignore
GLSL: {
VERSION: "#version 300 es\n",
DEFINE: {
RADIAN_360: "#define RADIAN_360 radians(360.)\n",
HEIGHT: "#define HEIGHT 255.\n",
ZO: "#define ZO vec2(0,1)\n",
PI: "#define PI radians(180.)\n",
},
RANDOM:
"float rand(vec2 p,float s){" +
"p=mod(p,vec2(10000));" +
"return fract(" +
"sin(" +
"dot(" +
"p," +
"vec2(" +
"sin(p.x+p.y)," +
"cos(p.y-p.x)" +
")" +
")" +
")*s" +
");" +
"}" +
"float rand(vec2 p){" +
"return rand(p,1.);" +
"}"
},
/**
* @property {Object}
*/
INFO: {
isWebGl2Supported: false,
},
/**
* Create new context config
* @param {ContextConfig} config Context config
* @returns {ContextConfig}
*/
initContextConfig: (config = {}) => ({
canvas: config.canvas || document.createElement("canvas"),
initCallback: config.initCallback,
contextAttributes: {
...{
powerPreference: "high-performance",
preserveDrawingBuffer: false,
premultipliedAlpha: false,
},
...(config.contextAttributes || {}),
},
}),
/**
* Create new renderer config
* @param {RendererConfig} config Renderer config
* @returns {RendererConfig}
*/
initRendererConfig: (config = {}) => ({
locations: config.locations || [],
context: config.context || new Context(),
}),
/**
* Call the callback function if the document.readyState interactive or complete
* @param {function} callback Callback function
*/
initApplication: (callback) => {
const isDocumentReady = () => {
if (["interactive", "complete"].indexOf(document.readyState) > -1) {
(callback ?? noop)(Utils.INFO.isWebGl2Supported);
return true;
}
};
const onReadyStateChange = () =>
isDocumentReady() &&
document.removeEventListener("readystatechange", onReadyStateChange);
if (!isDocumentReady())
document.addEventListener("readystatechange", onReadyStateChange);
},
/**
* 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,
Const.VERTEX_SHADER,
vertexShaderSource
);
const fragmentShader = _createShader(
gl,
Const.FRAGMENT_SHADER,
fragmentShaderSource
);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, Const.LINK_STATUS)) {
console.error(
"Program info:",
gl.getProgramInfoLog(program),
"\n",
"Validate status:",
gl.getProgramParameter(program, Const.VALIDATE_STATUS),
"\n",
"Vertex shader info:",
gl.getShaderInfoLog(vertexShader),
"\n",
"Fragment shader info:",
gl.getShaderInfoLog(fragmentShader)
);
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;
},
};
/**
* Contains all constant values for WebGL
* @type {Object}
*/
export const Const = {};
const _gl = document.createElement("canvas").getContext("webgl2");
if (_gl) {
for (let key in _gl) {
const value = _gl[key];
if (typeof value === "number" && key === key.toUpperCase())
Const[key] = value;
}
Utils.INFO.isWebGl2Supported = true;
Utils.INFO.maxTextureImageUnits = _gl.getParameter(
Const.MAX_TEXTURE_IMAGE_UNITS
);
}