import resizeToFit from 'intrinsic-scale' import noiseUrl from "../noise-power.png" import baseVertSource from "./base.vert?raw" import baseFragSource from "./base.frag?raw" import glitchVertSource from "./glitch.vert?raw" import glitchFragSource from "./glitch.frag?raw" export class Glitcher { /** @type {HTMLCanvasElement} */ canvas glitches = [] /** * @param {HTMLCanvasElement} canvas */ constructor(canvas) { this.canvas = canvas this.videoCanvas = new OffscreenCanvas(10, 10); this.video = document.createElement("video") this.init() } init(){ this.video.muted = true this.video.loop = true this.video.autoplay = true this.video.src = "/background.webm" this.video.addEventListener("canplay", e => this.video.play()) this.ctx = this.canvas.getContext("webgl") if(!this.ctx){ throw new Error("WebGL isn't supported") } let baseProgram = createProgram(this.ctx, baseVertSource, baseFragSource); let panelPositionBuffer = this.ctx.createBuffer() this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, panelPositionBuffer) const positions = [ -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, ] this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(positions), this.ctx.STATIC_DRAW) let panelUVBuffer = this.ctx.createBuffer() this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, panelUVBuffer) const uv = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, ] this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(uv), this.ctx.STATIC_DRAW) let imageTexture = this.ctx.createTexture() this.base = { program: baseProgram, aVertexPosition: this.ctx.getAttribLocation(baseProgram, "aVertexPosition"), aTextureCoord: this.ctx.getAttribLocation(baseProgram, "aTextureCoord"), uImageSampler: this.ctx.getUniformLocation(baseProgram, "uImageSampler"), uWindowSize: this.ctx.getUniformLocation(baseProgram, "uWindowSize"), imageTexture: imageTexture, panelUVBuffer: panelUVBuffer, panelPositionsBuffer: panelPositionBuffer, } this.clearImage(); let glitchPositionBuffer = this.ctx.createBuffer() this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, glitchPositionBuffer) const glitchPosition = [ -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, ] this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(glitchPosition), this.ctx.STATIC_DRAW) let glitchProgram = createProgram(this.ctx, glitchVertSource, glitchFragSource) let videoFrameTexture = this.ctx.createTexture() let noiseTexture = this.ctx.createTexture() this.ctx.bindTexture(this.ctx.TEXTURE_2D, noiseTexture) this.ctx.texImage2D( this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, 1, 1, 0, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]) ) let noiseImage = new Image() noiseImage.addEventListener("load", () => { this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.glitch.noiseTexture) this.ctx.texImage2D( this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, noiseImage ) 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); 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); }) noiseImage.src = noiseUrl this.glitch = { program: glitchProgram, aVertexPosition: this.ctx.getAttribLocation(glitchProgram, "aVertexPosition"), uGlitchPosition: this.ctx.getUniformLocation(glitchProgram, "uGlitchPosition"), uGlitchWidth: this.ctx.getUniformLocation(glitchProgram, "uGlitchWidth"), uImageRatio: this.ctx.getUniformLocation(glitchProgram, "uImageRatio"), uWindowSize: this.ctx.getUniformLocation(glitchProgram, "uWindowSize"), uVideoSampler: this.ctx.getUniformLocation(glitchProgram, "uVideoSampler"), uNoiseSampler: this.ctx.getUniformLocation(glitchProgram, "uNoiseSampler"), uRandom: this.ctx.getUniformLocation(glitchProgram, "uRandom"), glitchPositionBuffer: glitchPositionBuffer, videoFrameTexture: videoFrameTexture, noiseTexture: noiseTexture } console.log(this.glitch) } clearImage(){ this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.base.imageTexture) this.ctx.texImage2D( this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, 1, 1, 0, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]) ) } clearGlitch(){ this.glitches = [] } addGlitch(x, y, width=1){ let computedStyle = window.getComputedStyle(this.canvas) let clientWidth = parseFloat(computedStyle.width) let clientHeight = parseFloat(computedStyle.height) this.glitches.push({ x: ((x*2)/clientWidth)-1, y: (((y*2)/clientHeight)-1)*-1, width }) } /** * @param {File} imageFile */ async setImage(imageFile){ if(imageFile == this.currentImageFile){ return } if(this.currentImage){ URL.revokeObjectURL(this.currentImage.src) } let image = new Image() const loadProm = new Promise((res, rej) => { function ok(){ image.removeEventListener("error", nok) res() } function nok(){ image.removeEventListener("load", ok) rej() } image.addEventListener("load", ok) image.addEventListener("error", nok) }) image.src = URL.createObjectURL(imageFile) try { await loadProm } catch(e) { URL.revokeObjectURL(image.src) throw e } this.resize(image.width, image.height) this.currentImageFile = imageFile this.currentImage = image this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.base.imageTexture) this.ctx.texImage2D( this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, this.currentImage ) 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.ctx.LINEAR); } async toBlob(type, quality) { this.resize(this.currentImage.width, this.currentImage.height, true) this.render() let blob = await new Promise((res, rej) => this.canvas.toBlob((imageBlob) => { if(imageBlob){ res(imageBlob) } else { rej() } }, type, quality)) this.resize(this.currentImage.width, this.currentImage.height, false) return blob } resize(width, height, force=false){ if(!force){ let computedStyle = window.getComputedStyle(this.canvas) let maxWidth = parseFloat(computedStyle["max-width"]) let maxHeight = parseFloat(computedStyle["max-height"]) if(window.devicePixelRatio){ maxWidth *= window.devicePixelRatio maxHeight *= window.devicePixelRatio } if(Number.isNaN(maxWidth)){ maxWidth = width } if(Number.isNaN(maxHeight)){ maxHeight = height } let resize = resizeToFit("contain", {width, height}, {width: maxWidth, height: maxHeight}) this.canvas.width = resize.width this.canvas.height = resize.height } else { this.canvas.width = width this.canvas.height = height } this.videoCanvas.width = this.canvas.width this.videoCanvas.height = this.canvas.height this.ctx.viewport(0, 0, this.canvas.width, this.canvas.height); } updateVideoTexture(){ let vctx = this.videoCanvas.getContext("2d") vctx.clearRect(0, 0, this.videoCanvas.width, this.videoCanvas.height) vctx.drawImage(this.video, 0, 0, this.videoCanvas.width, this.videoCanvas.height); this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.glitch.videoFrameTexture) this.ctx.texImage2D( this.ctx.TEXTURE_2D, 0, this.ctx.RGBA, this.ctx.RGBA, this.ctx.UNSIGNED_BYTE, this.videoCanvas ) 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.ctx.LINEAR); this.ctx.texParameteri(this.ctx.TEXTURE_2D, this.ctx.TEXTURE_MAG_FILTER, this.ctx.LINEAR); } render(){ if(!this.ctx){ throw new Error("Glitcher not initialized, please run init()") } this.updateVideoTexture() this.ctx.clearColor(0.0, 0.0, 0.0, 0.0) this.ctx.clearDepth(1.0) this.ctx.enable(this.ctx.DEPTH_TEST) this.ctx.enable(this.ctx.LEQUAL) this.ctx.clear(this.ctx.COLOR_BUFFER_BIT | this.ctx.DEPTH_BUFFER_BIT) { // Base rendering this.ctx.useProgram(this.base.program) this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.base.panelPositionsBuffer) this.ctx.vertexAttribPointer( this.base.aVertexPosition, 2, // N components per iteration this.ctx.FLOAT, false, //Normalize, 0, // (stride) how many bytes to get from one set of values to the next 0, // Start offset ) this.ctx.enableVertexAttribArray(this.base.aVertexPosition) this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.base.panelUVBuffer) this.ctx.vertexAttribPointer( this.base.aTextureCoord, 2, this.ctx.FLOAT, false, 0, 0, ) this.ctx.enableVertexAttribArray(this.base.aTextureCoord) this.ctx.activeTexture(this.ctx.TEXTURE0) this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.base.imageTexture) this.ctx.uniform2f(this.base.uWindowSize, this.canvas.width, this.canvas.height) this.ctx.uniform1i(this.base.uImageSampler, 0) this.ctx.drawArrays( this.ctx.TRIANGLE_STRIP, 0, // Vertex offset 4, // Total vertex ); } { // Glitch rendering this.ctx.useProgram(this.glitch.program) this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.glitch.glitchPositionBuffer) this.ctx.vertexAttribPointer( this.glitch.glitchPositionBuffer, 2, // N components per iteration this.ctx.FLOAT, false, //Normalize, 0, // (stride) how many bytes to get from one set of values to the next 0, // Start offset ) this.ctx.enableVertexAttribArray(this.glitch.aVertexPosition) this.ctx.activeTexture(this.ctx.TEXTURE0) this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.glitch.videoFrameTexture) this.ctx.uniform1i(this.glitch.uVideoSampler, 0) this.ctx.activeTexture(this.ctx.TEXTURE1) this.ctx.bindTexture(this.ctx.TEXTURE_2D, this.glitch.noiseTexture) this.ctx.uniform1i(this.glitch.uNoiseSampler, 1) this.ctx.uniform2f(this.glitch.uWindowSize, this.canvas.width, this.canvas.height) if(this.canvas.width > this.canvas.height){ this.ctx.uniform2f(this.glitch.uImageRatio, 1.0, this.canvas.width/this.canvas.height) } else { this.ctx.uniform2f(this.glitch.uImageRatio, this.canvas.height/this.canvas.width, 1.0) } for(let glitch of this.glitches) { this.ctx.uniform4f(this.glitch.uRandom, Math.random(), Math.random(), Math.random(), Math.random() ) this.ctx.uniform2f(this.glitch.uGlitchPosition, glitch.x, glitch.y) this.ctx.uniform1f(this.glitch.uGlitchWidth, glitch.width) this.ctx.drawArrays( this.ctx.TRIANGLE_STRIP, 0, // Vertex offset 4, // Total vertex ); } } } } function createProgram(gl, vertSource, fragSource){ let vertShader = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vertShader, vertSource) gl.compileShader(vertShader) if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { throw new Error(`failed to compile vertex: ${gl.getShaderInfoLog(vertShader)}`) } let fragShader = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fragShader, fragSource) gl.compileShader(fragShader) if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { throw new Error(`failed to compile fragment: ${gl.getShaderInfoLog(fragShader)}`) } let program = gl.createProgram() gl.attachShader(program, vertShader) gl.attachShader(program, fragShader) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { throw new Error(`failed to link shader: ${gl.getProgramInfoLog(program)}`) } return program }