human/src/image/imagefx.ts

396 lines
14 KiB
TypeScript
Raw Normal View History

/**
* Image Filters in WebGL algoritm implementation
* Based on: [WebGLImageFilter](https://github.com/phoboslab/WebGLImageFilter)
* This module is written in ES5 JS and does not conform to code and style standards
*/
2020-10-18 18:12:09 +02:00
2021-10-12 15:48:00 +02:00
import * as shaders from './imagefxshaders';
class GLProgram {
uniform = {};
attribute = {};
gl: WebGLRenderingContext;
id: WebGLProgram;
constructor(gl, vertexSource, fragmentSource) {
this.gl = gl;
const _vsh = this.compile(vertexSource, this.gl.VERTEX_SHADER);
const _fsh = this.compile(fragmentSource, this.gl.FRAGMENT_SHADER);
this.id = this.gl.createProgram() as WebGLProgram;
this.gl.attachShader(this.id, _vsh);
this.gl.attachShader(this.id, _fsh);
this.gl.linkProgram(this.id);
if (!this.gl.getProgramParameter(this.id, this.gl.LINK_STATUS)) throw new Error(`filter: gl link failed: ${this.gl.getProgramInfoLog(this.id)}`);
this.gl.useProgram(this.id);
this.collect(vertexSource, 'attribute', this.attribute); // Collect attributes
for (const a in this.attribute) this.attribute[a] = this.gl.getAttribLocation(this.id, a);
this.collect(vertexSource, 'uniform', this.uniform); // Collect uniforms
this.collect(fragmentSource, 'uniform', this.uniform);
for (const u in this.uniform) this.uniform[u] = this.gl.getUniformLocation(this.id, u);
}
collect = (source, prefix, collection) => {
2020-10-18 18:12:09 +02:00
const r = new RegExp('\\b' + prefix + ' \\w+ (\\w+)', 'ig');
source.replace(r, (match, name) => {
collection[name] = 0;
return match;
});
};
2021-10-12 15:48:00 +02:00
compile = (source, type): WebGLShader => {
const shader = this.gl.createShader(type) as WebGLShader;
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) throw new Error(`filter: gl compile failed: ${this.gl.getShaderInfoLog(shader)}`);
2020-10-18 18:12:09 +02:00
return shader;
};
2021-02-19 14:35:41 +01:00
}
2020-10-18 18:12:09 +02:00
2021-02-19 14:35:41 +01:00
export function GLImageFilter(params) {
2020-10-18 18:12:09 +02:00
if (!params) params = { };
let _drawCount = 0;
let _sourceTexture = null;
let _lastInChain = false;
let _currentFramebufferIndex = -1;
2021-10-12 15:48:00 +02:00
let _tempFramebuffers: [null, null] | [{ fbo: any, texture: any }] = [null, null];
let _filterChain: Record<string, unknown>[] = [];
2020-10-18 18:12:09 +02:00
let _width = -1;
let _height = -1;
let _vertexBuffer = null;
2021-10-12 15:48:00 +02:00
let _currentProgram: GLProgram | null = null;
const _canvas = params.canvas || typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(100, 100) : document.createElement('canvas');
const _shaderProgramCache = { }; // key is the shader program source, value is the compiled program
2021-02-19 14:35:41 +01:00
const DRAW = { INTERMEDIATE: 1 };
2020-11-03 15:34:36 +01:00
const gl = _canvas.getContext('webgl');
2021-10-12 15:48:00 +02:00
if (!gl) throw new Error('filter: cannot get webgl context');
2020-10-18 18:12:09 +02:00
this.addFilter = function (name) {
2020-11-02 18:21:30 +01:00
// eslint-disable-next-line prefer-rest-params
2020-10-18 18:12:09 +02:00
const args = Array.prototype.slice.call(arguments, 1);
const filter = _filter[name];
_filterChain.push({ func: filter, args });
};
this.reset = function () {
_filterChain = [];
};
const _resize = function (width, height) {
2021-10-12 15:48:00 +02:00
if (width === _width && height === _height) return; // Same width/height? Nothing to do here
2020-11-02 18:21:30 +01:00
_canvas.width = width;
_width = width;
_canvas.height = height;
_height = height;
2021-10-12 15:48:00 +02:00
if (!_vertexBuffer) { // Create the context if we don't have it yet
const vertices = new Float32Array([-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0, -1, 1, 0, 0, 1, -1, 1, 1, 1, 1, 1, 0]); // Create the vertex buffer for the two triangles [x, y, u, v] * 6
2020-11-02 18:21:30 +01:00
// eslint-disable-next-line no-unused-expressions
(_vertexBuffer = gl.createBuffer(), gl.bindBuffer(gl.ARRAY_BUFFER, _vertexBuffer));
2020-10-18 18:12:09 +02:00
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
}
gl.viewport(0, 0, _width, _height);
2021-10-12 15:48:00 +02:00
_tempFramebuffers = [null, null]; // Delete old temp framebuffers
2020-10-18 18:12:09 +02:00
};
const _createFramebufferTexture = function (width, height) {
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
const renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { fbo, texture };
};
2021-02-19 14:35:41 +01:00
const _getTempFramebuffer = function (index) {
_tempFramebuffers[index] = _tempFramebuffers[index] || _createFramebufferTexture(_width, _height);
return _tempFramebuffers[index];
};
2021-10-12 15:48:00 +02:00
const _draw = function (flags = 0) {
if (!_currentProgram) return;
2020-10-18 18:12:09 +02:00
let source = null;
let target = null;
let flipY = false;
2021-10-12 15:48:00 +02:00
if (_drawCount === 0) source = _sourceTexture; // First draw call - use the source texture
else source = _getTempFramebuffer(_currentFramebufferIndex)?.texture; // All following draw calls use the temp buffer last drawn to
2020-10-18 18:12:09 +02:00
_drawCount++;
2021-10-12 15:48:00 +02:00
if (_lastInChain && !(flags & DRAW.INTERMEDIATE)) { // Last filter in our chain - draw directly to the WebGL Canvas. We may also have to flip the image vertically now
2020-10-18 18:12:09 +02:00
target = null;
flipY = _drawCount % 2 === 0;
} else {
_currentFramebufferIndex = (_currentFramebufferIndex + 1) % 2;
2021-10-12 15:48:00 +02:00
target = _getTempFramebuffer(_currentFramebufferIndex)?.fbo; // Intermediate draw call - get a temp buffer to draw to
2020-10-18 18:12:09 +02:00
}
2021-10-12 15:48:00 +02:00
gl.bindTexture(gl.TEXTURE_2D, source); // Bind the source and target and draw the two triangles
2020-10-18 18:12:09 +02:00
gl.bindFramebuffer(gl.FRAMEBUFFER, target);
2021-10-12 15:48:00 +02:00
gl.uniform1f(_currentProgram.uniform['flipY'], (flipY ? -1 : 1));
2020-10-18 18:12:09 +02:00
gl.drawArrays(gl.TRIANGLES, 0, 6);
};
2021-02-19 14:35:41 +01:00
this.apply = function (image) {
_resize(image.width, image.height);
_drawCount = 0;
2021-10-12 15:48:00 +02:00
if (!_sourceTexture) _sourceTexture = gl.createTexture(); // Create the texture for the input image if we haven't yet
2021-02-19 14:35:41 +01:00
gl.bindTexture(gl.TEXTURE_2D, _sourceTexture);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
2021-10-12 15:48:00 +02:00
if (_filterChain.length === 0) { // draw when done with filters
2021-02-19 14:35:41 +01:00
_draw();
2021-10-12 15:48:00 +02:00
} else { // apply filters one-by-one recursively
for (let i = 0; i < _filterChain.length; i++) {
_lastInChain = (i === _filterChain.length - 1);
const f = _filterChain[i];
f.func.apply(this, f.args || []);
}
2021-02-19 14:35:41 +01:00
}
return _canvas;
};
2020-10-18 18:12:09 +02:00
const _compileShader = function (fragmentSource) {
if (_shaderProgramCache[fragmentSource]) {
_currentProgram = _shaderProgramCache[fragmentSource];
2021-10-12 15:48:00 +02:00
gl.useProgram(_currentProgram?.id);
2020-10-18 18:12:09 +02:00
return _currentProgram;
}
2021-10-12 15:48:00 +02:00
_currentProgram = new GLProgram(gl, shaders.vertexIdentity, fragmentSource);
2020-10-18 18:12:09 +02:00
const floatSize = Float32Array.BYTES_PER_ELEMENT;
const vertSize = 4 * floatSize;
2021-10-12 15:48:00 +02:00
gl.enableVertexAttribArray(_currentProgram.attribute['pos']);
gl.vertexAttribPointer(_currentProgram.attribute['pos'], 2, gl.FLOAT, false, vertSize, 0 * floatSize);
2020-10-18 18:12:09 +02:00
gl.enableVertexAttribArray(_currentProgram.attribute.uv);
2021-10-12 15:48:00 +02:00
gl.vertexAttribPointer(_currentProgram.attribute['uv'], 2, gl.FLOAT, false, vertSize, 2 * floatSize);
2020-10-18 18:12:09 +02:00
_shaderProgramCache[fragmentSource] = _currentProgram;
return _currentProgram;
};
2021-10-12 15:48:00 +02:00
// Color Matrix Filter: Used by most color filters
const _filter = {
colorMatrix: (matrix) => {
const m = new Float32Array(matrix); // Create a Float32 Array and normalize the offset component to 0-1
m[4] /= 255;
m[9] /= 255;
m[14] /= 255;
m[19] /= 255;
const shader = (m[18] === 1 && m[3] === 0 && m[8] === 0 && m[13] === 0 && m[15] === 0 && m[16] === 0 && m[17] === 0 && m[19] === 0) // Can we ignore the alpha value? Makes things a bit faster.
? shaders.colorMatrixWithoutAlpha
: shaders.colorMatrixWithAlpha;
const program = _compileShader(shader);
gl.uniform1fv(program?.uniform['m'], m);
_draw();
},
brightness: (brightness) => {
const b = (brightness || 0) + 1;
_filter.colorMatrix([
b, 0, 0, 0, 0,
0, b, 0, 0, 0,
0, 0, b, 0, 0,
0, 0, 0, 1, 0,
]);
},
saturation: (amount) => {
const x = (amount || 0) * 2 / 3 + 1;
const y = ((x - 1) * -0.5);
_filter.colorMatrix([
x, y, y, 0, 0,
y, x, y, 0, 0,
y, y, x, 0, 0,
0, 0, 0, 1, 0,
]);
},
desaturate: () => {
_filter.saturation(-1);
},
contrast: (amount) => {
const v = (amount || 0) + 1;
const o = -128 * (v - 1);
_filter.colorMatrix([
v, 0, 0, 0, o,
0, v, 0, 0, o,
0, 0, v, 0, o,
0, 0, 0, 1, 0,
]);
},
negative: () => {
_filter.contrast(-2);
},
hue: (rotation) => {
rotation = (rotation || 0) / 180 * Math.PI;
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const lumR = 0.213;
const lumG = 0.715;
const lumB = 0.072;
_filter.colorMatrix([
lumR + cos * (1 - lumR) + sin * (-lumR), lumG + cos * (-lumG) + sin * (-lumG), lumB + cos * (-lumB) + sin * (1 - lumB), 0, 0,
lumR + cos * (-lumR) + sin * (0.143), lumG + cos * (1 - lumG) + sin * (0.140), lumB + cos * (-lumB) + sin * (-0.283), 0, 0,
lumR + cos * (-lumR) + sin * (-(1 - lumR)), lumG + cos * (-lumG) + sin * (lumG), lumB + cos * (1 - lumB) + sin * (lumB), 0, 0,
0, 0, 0, 1, 0,
]);
},
desaturateLuminance: () => {
_filter.colorMatrix([
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
0, 0, 0, 1, 0,
]);
},
sepia: () => {
_filter.colorMatrix([
0.393, 0.7689999, 0.18899999, 0, 0,
0.349, 0.6859999, 0.16799999, 0, 0,
0.272, 0.5339999, 0.13099999, 0, 0,
0, 0, 0, 1, 0,
]);
},
brownie: () => {
_filter.colorMatrix([
0.5997023498159715, 0.34553243048391263, -0.2708298674538042, 0, 47.43192855600873,
-0.037703249837783157, 0.8609577587992641, 0.15059552388459913, 0, -36.96841498319127,
0.24113635128153335, -0.07441037908422492, 0.44972182064877153, 0, -7.562075277591283,
0, 0, 0, 1, 0,
]);
},
vintagePinhole: () => {
_filter.colorMatrix([
0.6279345635605994, 0.3202183420819367, -0.03965408211312453, 0, 9.651285835294123,
0.02578397704808868, 0.6441188644374771, 0.03259127616149294, 0, 7.462829176470591,
0.0466055556782719, -0.0851232987247891, 0.5241648018700465, 0, 5.159190588235296,
0, 0, 0, 1, 0,
]);
},
kodachrome: () => {
_filter.colorMatrix([
1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502,
-0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203,
-0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946,
0, 0, 0, 1, 0,
]);
},
technicolor: () => {
_filter.colorMatrix([
1.9125277891456083, -0.8545344976951645, -0.09155508482755585, 0, 11.793603434377337,
-0.3087833385928097, 1.7658908555458428, -0.10601743074722245, 0, -70.35205161461398,
-0.231103377548616, -0.7501899197440212, 1.847597816108189, 0, 30.950940869491138,
0, 0, 0, 1, 0,
]);
},
polaroid: () => {
_filter.colorMatrix([
1.438, -0.062, -0.062, 0, 0,
-0.122, 1.378, -0.122, 0, 0,
-0.016, -0.016, 1.483, 0, 0,
0, 0, 0, 1, 0,
]);
},
shiftToBGR: () => {
_filter.colorMatrix([
0, 0, 1, 0, 0,
0, 1, 0, 0, 0,
1, 0, 0, 0, 0,
0, 0, 0, 1, 0,
]);
},
// Convolution Filter
convolution: (matrix) => {
const m = new Float32Array(matrix);
const pixelSizeX = 1 / _width;
const pixelSizeY = 1 / _height;
const program = _compileShader(shaders.convolution);
gl.uniform1fv(program?.uniform['m'], m);
gl.uniform2f(program?.uniform['px'], pixelSizeX, pixelSizeY);
_draw();
},
2020-10-18 18:12:09 +02:00
2021-10-12 15:48:00 +02:00
detectEdges: () => {
_filter.convolution.call(this, [
0, 1, 0,
1, -4, 1,
0, 1, 0,
]);
},
2020-10-18 18:12:09 +02:00
2021-10-12 15:48:00 +02:00
sobelX: () => {
_filter.convolution.call(this, [
-1, 0, 1,
-2, 0, 2,
-1, 0, 1,
]);
},
2020-10-18 18:12:09 +02:00
2021-10-12 15:48:00 +02:00
sobelY: () => {
_filter.convolution.call(this, [
-1, -2, -1,
0, 0, 0,
1, 2, 1,
]);
},
sharpen: (amount) => {
const a = amount || 1;
_filter.convolution.call(this, [
0, -1 * a, 0,
-1 * a, 1 + 4 * a, -1 * a,
0, -1 * a, 0,
]);
},
emboss: (size) => {
const s = size || 1;
_filter.convolution.call(this, [
-2 * s, -1 * s, 0,
-1 * s, 1, 1 * s,
0, 1 * s, 2 * s,
]);
},
// Blur Filter
blur: (size) => {
const blurSizeX = (size / 7) / _width;
const blurSizeY = (size / 7) / _height;
const program = _compileShader(shaders.blur);
// Vertical
gl.uniform2f(program?.uniform['px'], 0, blurSizeY);
_draw(DRAW.INTERMEDIATE);
// Horizontal
gl.uniform2f(program?.uniform['px'], blurSizeX, 0);
_draw();
},
// Pixelate Filter
pixelate: (size) => {
const blurSizeX = (size) / _width;
const blurSizeY = (size) / _height;
const program = _compileShader(shaders.pixelate);
gl.uniform2f(program?.uniform['size'], blurSizeX, blurSizeY);
_draw();
},
2020-10-18 18:12:09 +02:00
};
2021-02-19 14:35:41 +01:00
}