diff --git a/packages/webamp-modern-2/src/skin/GammaGroup.ts b/packages/webamp-modern-2/src/skin/GammaGroup.ts index dcae6956..5ec051a4 100644 --- a/packages/webamp-modern-2/src/skin/GammaGroup.ts +++ b/packages/webamp-modern-2/src/skin/GammaGroup.ts @@ -1,4 +1,5 @@ -import { clamp, normalizeDomId, num, toBool } from "../utils"; +import { clamp, normalizeDomId, num } from "../utils"; +import { glTransformImage } from "./GammaWebGL"; // https://www.pawelporwisz.pl/winamp/wct_en.php export default class GammaGroup { @@ -47,6 +48,10 @@ export default class GammaGroup { // TODO: Figure out how to actually implement this. transformImage(img: HTMLImageElement): string { + // Toggle this to play with gl transforming + if (false) { + return glTransformImage(img); + } const [r, g, b] = this._value.split(",").map((v) => { return (Number(v) / 4096) * 255; }); diff --git a/packages/webamp-modern-2/src/skin/GammaWebGL.ts b/packages/webamp-modern-2/src/skin/GammaWebGL.ts new file mode 100644 index 00000000..3eedebc4 --- /dev/null +++ b/packages/webamp-modern-2/src/skin/GammaWebGL.ts @@ -0,0 +1,239 @@ +const VERTEXT_SHADER = ` +attribute vec2 a_position; +attribute vec2 a_texCoord; + +uniform vec2 u_resolution; + +varying vec2 v_texCoord; + +void main() { + // convert the rectangle from pixels to 0.0 to 1.0 + vec2 zeroToOne = a_position / u_resolution; + + // convert from 0->1 to 0->2 + vec2 zeroToTwo = zeroToOne * 2.0; + + // convert from 0->2 to -1->+1 (clipspace) + vec2 clipSpace = zeroToTwo - 1.0; + + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); + + // pass the texCoord to the fragment shader + // The GPU will interpolate this value between points. + v_texCoord = a_texCoord; +}`; + +const FRAGMENT_SHADER = ` +precision mediump float; + +// our texture +uniform sampler2D u_image; + +// the texCoords passed in from the vertex shader. +varying vec2 v_texCoord; + +void main() { + gl_FragColor = texture2D(u_image, v_texCoord).rrba; +}`; + +const canvas: HTMLCanvasElement = document.createElement("canvas"); +if (canvas == null) { + throw new Error("Missing Canvas"); +} +const gl = canvas.getContext("webgl2"); +if (!gl) { + throw new Error("Missing WebGL2 context"); +} + +// An attempt at doing some kind of image gamma filtering in WebGL. +// A real solution will need: +// * Reuse gl context between images? +// * Handle multiple gammagroups with the same compiled shader code? +// +// Mostly stolen from https://webglfundamentals.org/webgl/lessons/webgl-image-processing.html +export function glTransformImage(image: HTMLImageElement): string { + canvas.width = image.width; + canvas.height = image.height; + // setup GLSL program + const shaders = [ + loadShader(gl, VERTEXT_SHADER, gl.VERTEX_SHADER), + loadShader(gl, FRAGMENT_SHADER, gl.FRAGMENT_SHADER), + ]; + const program = createProgram(gl, shaders); + + // look up where the vertex data needs to go. + var positionLocation = gl.getAttribLocation(program, "a_position"); + var texcoordLocation = gl.getAttribLocation(program, "a_texCoord"); + + // Create a buffer to put three 2d clip space points in + var positionBuffer = gl.createBuffer(); + + // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer) + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + // Set a rectangle the same size as the image. + setRectangle(gl, 0, 0, image.width, image.height); + + // provide texture coordinates for the rectangle. + var texcoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + ]), + gl.STATIC_DRAW + ); + + // Create a texture. + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Set the parameters so we can render any size image. + 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + // Upload the image into the texture. + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); + + // lookup uniforms + var resolutionLocation = gl.getUniformLocation(program, "u_resolution"); + + // Tell WebGL how to convert from clip space to pixels + gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + + // Clear the canvas + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Tell it to use our program (pair of shaders) + gl.useProgram(program); + + // Turn on the position attribute + gl.enableVertexAttribArray(positionLocation); + + // Bind the position buffer. + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + + // Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER) + var size = 2; // 2 components per iteration + var type = gl.FLOAT; // the data is 32bit floats + var normalize = false; // don't normalize the data + var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position + var offset = 0; // start at the beginning of the buffer + gl.vertexAttribPointer( + positionLocation, + size, + type, + normalize, + stride, + offset + ); + + // Turn on the texcoord attribute + gl.enableVertexAttribArray(texcoordLocation); + + // bind the texcoord buffer. + gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer); + + // Tell the texcoord attribute how to get data out of texcoordBuffer (ARRAY_BUFFER) + var size = 2; // 2 components per iteration + var type = gl.FLOAT; // the data is 32bit floats + var normalize = false; // don't normalize the data + var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position + var offset = 0; // start at the beginning of the buffer + gl.vertexAttribPointer( + texcoordLocation, + size, + type, + normalize, + stride, + offset + ); + + // set the resolution + gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height); + + // Draw the rectangle. + var primitiveType = gl.TRIANGLES; + var offset = 0; + var count = 6; + gl.drawArrays(primitiveType, offset, count); + + return (gl.canvas as HTMLCanvasElement).toDataURL(); +} + +function setRectangle( + gl: WebGL2RenderingContext, + x: number, + y: number, + width: number, + height: number +) { + var x1 = x; + var x2 = x + width; + var y1 = y; + var y2 = y + height; + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), + gl.STATIC_DRAW + ); +} + +function loadShader( + gl: WebGL2RenderingContext, + shaderSource: string, + shaderType: number +): WebGLShader { + // Create the shader object + const shader = gl.createShader(shaderType); + + // Load the shader source + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check the compile status + const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + // Something went wrong during compilation; get the error + console.error(gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + + return shader; +} + +function createProgram(gl: WebGL2RenderingContext, shaders: WebGLShader[]) { + const program = gl.createProgram(); + shaders.forEach(function (shader) { + gl.attachShader(program, shader); + }); + gl.linkProgram(program); + + // Check the link status + const linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + // something went wrong with the link + console.error(gl.getProgramInfoLog(program)); + + gl.deleteProgram(program); + return null; + } + return program; +}