const DEFAULT_VERTEX_SHADER = ` attribute float aVertexIndex; varying highp vec2 vTextureCoord; // This vertex shader produces the following, when drawn using indices 0..3: // // 1 | 0-----x.....2 // 0 | | s | . ´ // -1 | x_____x´ // -2 | : .´ // -3 | 1´ // +--------------- // -1 0 1 2 3 // // The axes are clip-space x and y. The region marked s is the visible region. // The digits in the corners of the right-angled triangle are the vertex // indices. // // The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0. // This means that the UV gets interpolated to 1,1 at the bottom-right corner // of the clip-space rectangle that is at 1,-1 in clip space. void main() { vec2 uv = vec2(floor(aVertexIndex / 2.0), floor(mod(aVertexIndex, 2.0))) * 2.0; gl_Position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); vTextureCoord = uv; } ` const FRAGMENT_SHADER_UTILITIES = ` highp float noise1(highp vec2 co){ return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453); } // Color spaces from https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl const highp float HCV_EPSILON = 1e-10; const highp float HSL_EPSILON = 1e-10; highp vec3 hue_to_rgb(highp float hue) { highp float R = abs(hue * 6.0 - 3.0) - 1.0; highp float G = 2.0 - abs(hue * 6.0 - 2.0); highp float B = 2.0 - abs(hue * 6.0 - 4.0); return clamp(vec3(R,G,B), vec3(0), vec3(1)); } // Converts from HSL to linear RGB highp vec3 hsl_to_rgb(highp vec3 hsl) { highp vec3 rgb = hue_to_rgb(hsl.x); highp float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y; return (rgb - 0.5) * C + hsl.z; } // Converts a value from linear RGB to HCV (Hue, Chroma, Value) highp vec3 rgb_to_hcv(highp vec3 rgb) { // Based on work by Sam Hocevar and Emil Persson highp vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0); highp vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx); highp float C = Q.x - min(Q.w, Q.y); highp float H = abs((Q.w - Q.y) / (6.0 * C + HCV_EPSILON) + Q.z); return vec3(H, C, Q.x); } // Converts from linear rgb to HSL highp vec3 rgb_to_hsl(highp vec3 rgb) { highp vec3 HCV = rgb_to_hcv(rgb); highp float L = HCV.z - HCV.y * 0.5; highp float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + HSL_EPSILON); return vec3(HCV.x, S, L); } highp vec3 hue_rotate(highp vec3 rgb_input, highp float amount) { highp vec3 hsl = rgb_to_hsl(rgb_input); hsl.x = mod(hsl.x+amount,1.0); return hsl_to_rgb(hsl); } ` const FRAGMENT_SHADER_UTILITIES_N_LINES = FRAGMENT_SHADER_UTILITIES.split(/\n/g).length const DEFAULT_FRAGMENT_SHADER = ` // Some random values computed for each frame uniform highp vec4 uRandom; // Size (in logical pixel) of current CuteTexture canvas uniform lowp vec2 uCanvasSize; // Number of seconds since canvas was initialized uniform lowp float uTime; // Coordinates of current pixel being computed from [0,0] (top left) to [1,1] (bottom right) varying highp vec2 vTextureCoord; void main() { gl_FragColor = vec4(vTextureCoord.x, vTextureCoord.y, 0.0, 1.0); } ` /** * Initialise un canvas d'arrière plan "CuteTexture" avec les options données */ export function initCuteTexture(){ /** @type {HTMLCanvasElement} */ const targetCanvas = document.querySelector("canvas[data-cute-texture]") || document.querySelector("canvas.cute-texture") || document.querySelector("canvas"); if(!targetCanvas) throw new Error(`Aucun élément trouvé sur la page`) { let computedStyle = getComputedStyle(targetCanvas) let elementWidth = parseFloat(computedStyle.width) let elementHeight = parseFloat(computedStyle.height) targetCanvas.width = elementWidth targetCanvas.height = elementHeight } const ctx = targetCanvas.getContext("webgl"); if(ctx == null) throw new Error("WebGL n'ext pas supporté sur votre machine") let mainShader = initMainShader(ctx) console.log("Shader CuteTexture initialisé", mainShader) const initTime = Date.now() function startRender(){ renderCuteTexture(targetCanvas, mainShader, initTime) requestAnimationFrame(startRender) } startRender() return { target: targetCanvas, initTime, resize: function(width, height) { targetCanvas.width = width targetCanvas.height = height } } } /** * @param {WebGLRenderingContext} ctx */ function initMainShader(ctx, oldShader=null){ const vertexShader = ctx.createShader(ctx.VERTEX_SHADER) ctx.shaderSource(vertexShader, DEFAULT_VERTEX_SHADER) ctx.compileShader(vertexShader) if (!ctx.getShaderParameter(vertexShader, ctx.COMPILE_STATUS)) { throw new Error(`Le vertex shader n'a pas pu être compilé: ${ctx.getShaderInfoLog(vertexShader)}`) ctx.deleteShader(vertexShader) return null } let fragmentShaderCode; let fragmentShaderCodeElement = document.querySelector(`script[type="x-shader/glsl+fragment"]`); if(fragmentShaderCodeElement){ fragmentShaderCode = fragmentShaderCodeElement.innerHTML } else { fragmentShaderCode = DEFAULT_FRAGMENT_SHADER } const fragmentShader = ctx.createShader(ctx.FRAGMENT_SHADER) ctx.shaderSource(fragmentShader, FRAGMENT_SHADER_UTILITIES+fragmentShaderCode) ctx.compileShader(fragmentShader) if (!ctx.getShaderParameter(fragmentShader, ctx.COMPILE_STATUS)) { let error = ctx.getShaderInfoLog(fragmentShader) let errorLine = error.match(/\d+:(\d+):/) if(errorLine) { let lineNum = parseInt(errorLine[1]) let lineNumeWithoutUtils = lineNum-FRAGMENT_SHADER_UTILITIES_N_LINES; if(lineNumeWithoutUtils >= 0){ error = `Ligne ${lineNumeWithoutUtils}: ${error}` } } ctx.deleteShader(fragmentShader) throw new Error(`Le fragment shader n'a pas pu être compilé: ${error}`) } const shaderProgram = ctx.createProgram() ctx.attachShader(shaderProgram, vertexShader) ctx.attachShader(shaderProgram, fragmentShader) ctx.linkProgram(shaderProgram) if (!ctx.getProgramParameter(shaderProgram, ctx.LINK_STATUS)) { throw new Error(`Le shader n'a pas pu être linké: ${ctx.getProgramInfoLog(shaderProgram)}`) ctx.deleteShader(vertexShader) ctx.deleteShader(fragmentShader) ctx.deleteProgram(shaderProgram) return null } if(oldShader){ ctx.deleteProgram(oldShader.program) ctx.deleteShader(oldShader.vertexShader) ctx.deleteShader(oldShader.fragmentShader) ctx.deleteBuffer(oldShader.vertexIndexBuffer) } const vertexIndexBuffer = ctx.createBuffer() ctx.bindBuffer(ctx.ARRAY_BUFFER, vertexIndexBuffer) ctx.bufferData(ctx.ARRAY_BUFFER, new Float32Array([0, 1, 2]), ctx.STATIC_DRAW) return { program: shaderProgram, vertexShader, fragmentShader, fragmentShaderElement: fragmentShaderCodeElement, aVertexIndex: ctx.getAttribLocation(shaderProgram, "aVertexIndex"), uRandom: ctx.getUniformLocation(shaderProgram, "uRandom"), uCanvasSize: ctx.getUniformLocation(shaderProgram, "uCanvasSize"), uTime: ctx.getUniformLocation(shaderProgram, "uTime"), vertexIndexBuffer, } } /** * @param {HTMLCanvasElement} target * @param {*} mainShader */ function renderCuteTexture(target, mainShader, initTime){ let ctx = target.getContext("webgl") ctx.clearColor(0, 0, 0, 0) ctx.clearDepth(1.0) ctx.enable(ctx.DEPTH_TEST) ctx.depthFunc(ctx.LEQUAL) ctx.clear(ctx.COLOR_BUFFER_BIT) ctx.bindBuffer(ctx.ARRAY_BUFFER, mainShader.vertexIndexBuffer) ctx.vertexAttribPointer( mainShader.aVertexIndex, // location 1, // pull out 1 value per iteration ctx.FLOAT, // the data in the buffer is 32bit floats false, // don't normalize 0, // how many bytes to get from one set of values to the next 0 // how many bytes inside the buffer to start from ) ctx.enableVertexAttribArray(mainShader.aVertexIndex) ctx.useProgram(mainShader.program) if(mainShader.uRandom){ ctx.uniform4f( mainShader.uRandom, Math.random(), Math.random(), Math.random(), Math.random() ) } if(mainShader.uCanvasSize){ ctx.uniform2f( mainShader.uCanvasSize, target.width, target.height ) } if(mainShader.uTime){ ctx.uniform1f( mainShader.uTime, (Date.now() - initTime)/1000 ) } ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, 3); }