├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── filters │ ├── cubicspline.js │ ├── filterBlend.js │ ├── filterBlurBokeh.js │ ├── filterBlurGaussian.js │ ├── filterCurves.js │ ├── filterInsta.js │ ├── filterMatrix.js │ └── filterPerspective.js ├── minigl.js └── minigl_filters.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.lock 3 | node_modules 4 | dist 5 | dist-ssr 6 | *.local 7 | .env* 8 | package-lock.json 9 | TODO.txt 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | src/ 3 | node_modules/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 xdadda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-gl 2 | A small webgl2 library to edit images and apply filters. 3 | 4 | Inspired and partially based on [glfx.js](https://github.com/evanw/glfx.js) by Evan Wallace 5 | 6 | Note: the library adopts a sRGB correct workflow. Keep in mind if adding new shaders/ filters. 7 | 8 | ### Current "filters" 9 | * lights: brightness, exposure, gamma, contrast, shadows, highlights, bloom 10 | * colors: temperature, tint, vibrance, saturation, sepia 11 | * effects: clarity/ sharpness, noise reduction, vignette 12 | * color curves 13 | * insta filters 14 | * image blender 15 | * bokeh/lens and gaussian blur 16 | * perspective correction 17 | * translate-rotate-scale matrix 18 | 19 | 20 | Demo https://mini2-photo-editor.netlify.app 21 | (src https://github.com/xdadda/mini-photo-editor) 22 | 23 | 24 | ## Setup 25 | 26 | Install: 27 | `npm i @xdadda/mini-gl` 28 | 29 | 30 | Import in js: 31 | ```js 32 | import { minigl} from '@xdadda/mini-gl' 33 | ``` 34 | 35 | 36 | ## Constructor 37 | 38 | ```js 39 | const _wgl = minigl(canvas,image,colorspace) 40 | ``` 41 | * `canvas`: is the destination [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) on which minigl will render the image 42 | * `image`: is the source [HTMLImageElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElemen) with the original image 43 | * `colorspace`: specifies the color space of the rendering context ('srg'|'display-p3'); the image's colorspace can be extracted from the file's ICC profile metadata ([@xdadda/mini-exif](https://github.com/xdadda/mini-exif)) 44 | 45 | 46 | ## Render chain 47 | 48 | 1. Load original image texture in memory 49 | ```js 50 | _wgl.loadimage() 51 | ``` 52 | 53 | 2. Apply filters (one or more as required) 54 | ```js 55 | // TRANSLATE/ROTATE/SCALE filter 56 | // input: {translateX:0,translateY:0,angle:0,scale:0,flipv:0,fliph:0} 57 | // where scale:0 is 1:1 scale 58 | _wgl.filterMatrix({translateX:0,translateY:0,angle:0,scale:0,flipv:0,fliph:0}) 59 | 60 | // BASIC ADJUSTMENTS filter 61 | // input: {brightness:0, clarity:0, contrast:0, exposure:0, gamma:0, gray:0, 62 | // saturation:0, sepia:0, temperature:0, tint:0, vibrance:0, vignette:0} 63 | _wgl.filterAdjustments({...}) 64 | 65 | // BLOOM filter 66 | // input: strength 67 | _wgl.filterBloom(0.5) 68 | 69 | // NOISE filter 70 | // input: strength 71 | _wgl.filterNoise(0.5) 72 | 73 | // HIGHLIGHTS & SHADOWS filter 74 | // input: highlights_strength, shadows_strength 75 | _wgl.filterHighlightsShadows(0.2,0.3) 76 | 77 | // CURVES filter 78 | // input: Array of 'curves' for RGB/Luminance, RED, GREEN, BLUE 79 | // where a 'curve' is an array of points (x,y) across which a spline is interpolated 80 | // (x,y) represent the value mapping, from x to y 81 | // a 'curve' can be null to signify a linear interpolation 82 | // linear input example: [ [[0,0],[0.25,0.25],[0.75,0.75],[1,1]], [...], null, null ] 83 | _wgl.filterCurves([ [...], [...], [...], [...] ]) 84 | ``` 85 | 86 | 3. Draw to canvas 87 | ```js 88 | _wgl.paintCanvas() 89 | ``` 90 | 91 | ## Other functions 92 | 93 | Destroy textures and clear memory: 94 | ```js 95 | _wgl.destroy() 96 | ``` 97 | 98 | Generate an Image element from the current render: 99 | ```js 100 | _wgl.captureImage() 101 | ``` 102 | 103 | Crop image: 104 | ```js 105 | _wgl.crop({left, top, width, height}) 106 | ``` 107 | 108 | Clear crop and restore original image: 109 | ```js 110 | _wgl.resetCrop() 111 | ``` 112 | 113 | Resize image: 114 | ```js 115 | _wgl.resize({left, top, width, height}) 116 | ``` 117 | 118 | Clear resize and restore original image: 119 | ```js 120 | _wgl.resetResize() 121 | ``` 122 | 123 | 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xdadda/mini-gl", 3 | "version": "0.1.14", 4 | "description": "webgl image editing library with filters and effects", 5 | "main": "minigl.js", 6 | "type": "module", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "vite build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/xdadda/mini-gl.git" 14 | }, 15 | "keywords": [ 16 | "WebGL", 17 | "filters", 18 | "photo", 19 | "image", 20 | "editor" 21 | ], 22 | "author": "xdadda", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/xdadda/mini-gl/issues" 26 | }, 27 | "devDependencies": { 28 | "vite": "^6.2.2" 29 | }, 30 | "homepage": "https://github.com/xdadda/mini-gl#readme", 31 | "exports": { 32 | ".": { 33 | "import": "./dist/mini-gl.js", 34 | "require": "./dist/mini-gl.umd.cjs" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/filters/cubicspline.js: -------------------------------------------------------------------------------- 1 | export function Spline(points) { 2 | var n = points.length; 3 | this.xa = []; 4 | this.ya = []; 5 | this.u = []; 6 | this.y2 = []; 7 | 8 | points.sort(function(a, b) { 9 | return a[0] - b[0]; 10 | }); 11 | for (var i = 0; i < n; i++) { 12 | this.xa.push(points[i][0]); 13 | this.ya.push(points[i][1]); 14 | } 15 | 16 | this.u[0] = 0; 17 | this.y2[0] = 0; 18 | 19 | for (var i = 1; i < n - 1; ++i) { 20 | // This is the decomposition loop of the tridiagonal algorithm. 21 | // y2 and u are used for temporary storage of the decomposed factors. 22 | var wx = this.xa[i + 1] - this.xa[i - 1]; 23 | var sig = (this.xa[i] - this.xa[i - 1]) / wx; 24 | var p = sig * this.y2[i - 1] + 2.0; 25 | 26 | this.y2[i] = (sig - 1.0) / p; 27 | 28 | var ddydx = 29 | (this.ya[i + 1] - this.ya[i]) / (this.xa[i + 1] - this.xa[i]) - 30 | (this.ya[i] - this.ya[i - 1]) / (this.xa[i] - this.xa[i - 1]); 31 | 32 | this.u[i] = (6.0 * ddydx / wx - sig * this.u[i - 1]) / p; 33 | } 34 | 35 | this.y2[n - 1] = 0; 36 | 37 | // This is the backsubstitution loop of the tridiagonal algorithm 38 | for (var i = n - 2; i >= 0; --i) { 39 | this.y2[i] = this.y2[i] * this.y2[i + 1] + this.u[i]; 40 | } 41 | } 42 | 43 | Spline.prototype.at = function(x) { 44 | var n = this.ya.length; 45 | var klo = 0; 46 | var khi = n - 1; 47 | 48 | // We will find the right place in the table by means of 49 | // bisection. This is optimal if sequential calls to this 50 | // routine are at random values of x. If sequential calls 51 | // are in order, and closely spaced, one would do better 52 | // to store previous values of klo and khi. 53 | while (khi - klo > 1) { 54 | var k = (khi + klo) >> 1; 55 | 56 | if (this.xa[k] > x) { 57 | khi = k; 58 | } else { 59 | klo = k; 60 | } 61 | } 62 | 63 | var h = this.xa[khi] - this.xa[klo]; 64 | var a = (this.xa[khi] - x) / h; 65 | var b = (x - this.xa[klo]) / h; 66 | 67 | // Cubic spline polynomial is now evaluated. 68 | return a * this.ya[klo] + b * this.ya[khi] + 69 | ((a * a * a - a) * this.y2[klo] + (b * b * b - b) * this.y2[khi]) * (h * h) / 6.0; 70 | }; -------------------------------------------------------------------------------- /src/filters/filterBlend.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | export function filterBlend(mini, blendmap, blendmix){ 4 | const {gl} = mini 5 | 6 | const _fragment = `#version 300 es 7 | precision highp float; 8 | 9 | in vec2 texCoord; 10 | uniform sampler2D _texture; 11 | out vec4 outColor; 12 | 13 | uniform sampler2D map; 14 | uniform float filterStrength; 15 | 16 | vec4 fromLinear(vec4 linearRGB) { 17 | bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); 18 | vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); 19 | vec3 lower = linearRGB.rgb * vec3(12.92); 20 | return vec4(mix(higher, lower, cutoff), linearRGB.a); 21 | } 22 | vec4 toLinear(vec4 sRGB) { 23 | bvec3 cutoff = lessThan(sRGB.rgb, vec3(0.04045)); 24 | vec3 higher = pow((sRGB.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)); 25 | vec3 lower = sRGB.rgb/vec3(12.92); 26 | return vec4(mix(higher, lower, cutoff), sRGB.a); 27 | } 28 | 29 | void main(){ 30 | vec4 color = texture(_texture, texCoord); 31 | vec4 texc = texture(map, texCoord); 32 | color = toLinear(color); 33 | texc = toLinear(texc); 34 | color = mix(color, texc, filterStrength); 35 | color = fromLinear(color); 36 | outColor = color; 37 | }` 38 | 39 | mini._.$blend = mini._.$blend || new Shader(gl, null, _fragment); 40 | mini._.$blendtxt = mini._.$blendtxt || new Texture(gl); 41 | mini._.$blendtxt.loadImage(blendmap) 42 | mini._.$blendtxt.use(1); 43 | mini.runFilter(mini._.$blend, { filterStrength: blendmix??1, map:{unit:1} }); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/filters/filterBlurBokeh.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | export function filterBlurBokeh(mini, params) { 4 | const _fragment = `#version 300 es 5 | //Bokeh disc. by David Hoskins. 6 | //https://www.shadertoy.com/view/4d2Xzw 7 | precision highp float; 8 | 9 | in vec2 texCoord; 10 | uniform sampler2D _texture; 11 | out vec4 outColor; 12 | 13 | uniform float bokehstrength; 14 | uniform float bokehlensin; 15 | uniform float bokehlensout; 16 | uniform float centerX; 17 | uniform float centerY; 18 | 19 | #define GOLDEN_ANGLE 2.39996323 20 | #define ITERATIONS 512 21 | const mat2 rot = mat2(cos(GOLDEN_ANGLE), sin(GOLDEN_ANGLE), -sin(GOLDEN_ANGLE), cos(GOLDEN_ANGLE)); 22 | vec3 Bokeh(sampler2D tex, vec2 uv, float radius) 23 | { 24 | vec3 acc = vec3(0), div = acc; 25 | float r = 1.; 26 | vec2 vangle = vec2(0.0,radius*.01 / sqrt(float(ITERATIONS))); 27 | 28 | for (int j = 0; j < ITERATIONS; j++) 29 | { 30 | // the approx increase in the scale of sqrt(0, 1, 2, 3...) 31 | r += 1. / r; 32 | vangle = rot * vangle; 33 | vec3 col = texture(tex, uv + (r-1.) * vangle).xyz; /// ... Sample the image 34 | //col = col * col *1.8; // ... Contrast it for better highlights - leave this out elsewhere. 35 | vec3 bokeh = pow(col, vec3(4)); 36 | acc += col * bokeh; 37 | div += bokeh; 38 | } 39 | return acc / div; 40 | } 41 | 42 | 43 | void main() { 44 | vec4 color = texture(_texture, texCoord); 45 | vec4 bcolor = vec4(Bokeh(_texture, texCoord, bokehstrength), 1.); 46 | 47 | //vignette used to control alpha 48 | //to blur inside circle smoothstep(lensin, lensout, dist) 49 | //to blur outside circle smoothstep(lensout, lensin, dist) 50 | float dist = distance(texCoord.xy, vec2(centerX,centerY)); 51 | float vigfin = pow(1.-smoothstep(max(0.001,bokehlensout), bokehlensin, dist),2.); 52 | 53 | outColor = mix( color, bcolor, vigfin); 54 | } 55 | ` 56 | 57 | const {gl}=mini 58 | let { bokehstrength=0.5, bokehlensin=0, bokehlensout=0.5, centerX=0, centerY=0} = params || {} 59 | //setup and run effect 60 | mini._.$lensblur = mini._.$lensblur || new Shader(gl, null, _fragment); 61 | mini.runFilter(mini._.$lensblur, {bokehstrength,bokehlensin,bokehlensout,centerX,centerY} ) 62 | } -------------------------------------------------------------------------------- /src/filters/filterBlurGaussian.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | export function filterBlurGaussian(mini, params) { 4 | const _fragment = `#version 300 es 5 | //https://www.shadertoy.com/view/XdfGDH 6 | precision highp float; 7 | 8 | in vec2 texCoord; 9 | uniform sampler2D _texture; 10 | out vec4 outColor; 11 | 12 | uniform vec2 uResolution; 13 | uniform float gaussianstrength; 14 | uniform float gaussianlensin; 15 | uniform float gaussianlensout; 16 | uniform float centerX; 17 | uniform float centerY; 18 | 19 | float normpdf(in float x, in float sigma) 20 | { 21 | return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma; 22 | } 23 | 24 | void main() { 25 | vec4 color = texture(_texture, texCoord); 26 | 27 | //declare stuff 28 | const int mSize = 11; 29 | const int kSize = (mSize-1)/2; 30 | float kernel[mSize]; 31 | vec3 final_colour = vec3(0.0); 32 | 33 | //create the 1-D kernel 34 | float sigma = 7.0*gaussianstrength; 35 | float Z = 0.0; 36 | for (int j = 0; j <= kSize; ++j) 37 | { 38 | kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma); 39 | } 40 | 41 | //get the normalization factor (as the gaussian has been clamped) 42 | for (int j = 0; j < mSize; ++j) 43 | { 44 | Z += kernel[j]; 45 | } 46 | 47 | //read out the texels 48 | for (int i=-kSize; i <= kSize; ++i) 49 | { 50 | for (int j=-kSize; j <= kSize; ++j) 51 | { 52 | final_colour += kernel[kSize+j]*kernel[kSize+i]*texture(_texture, (texCoord.xy+vec2(float(i),float(j))/uResolution)).rgb; 53 | } 54 | } 55 | 56 | //vignette used to control alpha 57 | //to blur inside circle smoothstep(lensin, lensout, dist) 58 | //to blur outside circle smoothstep(lensout, lensin, dist) 59 | float dist = distance(texCoord.xy, vec2(centerX,centerY)); 60 | float vigfin = pow(1.-smoothstep(max(0.001,gaussianlensout), gaussianlensin, dist),2.); 61 | 62 | outColor = mix( color, vec4(final_colour/(Z*Z), 1.0), vigfin); 63 | } 64 | ` 65 | 66 | const {gl}=mini 67 | let { gaussianstrength=0.5, gaussianlensin=0, gaussianlensout=0.5, centerX=0, centerY=0} = params || {} 68 | const uResolution = [gl.canvas.width,gl.canvas.height]; 69 | //setup and run effect 70 | 71 | mini._.$gaussianblur = mini._.$gaussianblur || new Shader(gl, null, _fragment); 72 | mini.runFilter(mini._.$gaussianblur, {gaussianstrength,gaussianlensin,gaussianlensout,centerX,centerY,uResolution} ) 73 | } -------------------------------------------------------------------------------- /src/filters/filterCurves.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | import { Spline } from './cubicspline.js' 3 | 4 | function splineInterpolate(points) { 5 | var spline = new Spline(points); 6 | var curve = []; 7 | for (var i = 0; i < 256; i++) { 8 | curve.push(clamp(0, Math.floor(spline.at(i / 255) * 256), 255)); 9 | } 10 | return curve; 11 | } 12 | 13 | function clamp(lo, value, hi) { 14 | return Math.max(lo, Math.min(value, hi)); 15 | } 16 | 17 | //red,green,blue arrays [[0,0],...,[1,1]] describing channel curve 18 | export function filterCurves(mini, array) { 19 | //console.log('filterCurves') 20 | if(array.every(e=>e===null)) return //console.error('curves: need at least one array') 21 | if(!array[0]) array[0]=[[0,0],[1,1]] //linear identity curve 22 | let red=array[1]||array[0]; 23 | let green=array[2]||array[0]; 24 | let blue=array[3]||array[0]; 25 | red = splineInterpolate(red); 26 | green = splineInterpolate(green); 27 | blue = splineInterpolate(blue); 28 | if(red.length!==256 || green.length!==256 || blue.length!==256) return console.error('curves: input unknown') 29 | 30 | var array = []; 31 | for (var i = 0; i < 256; i++) { 32 | array.splice(array.length, 0, red[i], green[i], blue[i], 255); 33 | } 34 | 35 | const _fragment = `#version 300 es 36 | precision highp float; 37 | 38 | in vec2 texCoord; 39 | uniform sampler2D _texture; 40 | out vec4 outColor; 41 | 42 | uniform sampler2D curvemap; 43 | 44 | void main() { 45 | vec4 color = texture(_texture, texCoord); 46 | color.r = texture(curvemap, vec2(color.r)).r; 47 | color.g = texture(curvemap, vec2(color.g)).g; 48 | color.b = texture(curvemap, vec2(color.b)).b; 49 | outColor = color; 50 | } 51 | ` 52 | 53 | const {gl}=mini 54 | //setup and run effect 55 | mini._.$curvestexture = mini._.$curvestexture || new Texture(gl); 56 | mini._.$curvestexture.initFromBytes(256, 1, array, gl.RGBA); //otherwise artifacts will be introduced 57 | mini._.$curvestexture.use(2); 58 | mini._.$curves = mini._.$curves || new Shader(gl, null, _fragment); 59 | mini.runFilter(mini._.$curves, {curvemap:{unit:2}} ) 60 | } -------------------------------------------------------------------------------- /src/filters/filterInsta.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | export function filterInsta(mini, opt, mix){ 4 | //console.log('filterInsta',opt,mix) 5 | const {gl} = mini 6 | mix+=1 7 | 8 | const srgb_linear_fn = ` 9 | vec3 fromLinear(vec3 linearRGB) { 10 | bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); 11 | vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); 12 | vec3 lower = linearRGB.rgb * vec3(12.92); 13 | return vec3(mix(higher, lower, cutoff)); 14 | } 15 | vec3 toLinear(vec3 sRGB) { 16 | bvec3 cutoff = lessThan(sRGB.rgb, vec3(0.04045)); 17 | vec3 higher = pow((sRGB.rgb + vec3(0.055))/vec3(1.055), vec3(2.4)); 18 | vec3 lower = sRGB.rgb/vec3(12.92); 19 | return vec3(mix(higher, lower, cutoff)); 20 | }` 21 | 22 | if(opt.type==='1'){ 23 | /// SHADER 1: vertical LUT 33x10889px 24 | //ADEN, CREMA, JUNO, LARK, LUDWIG, REYES 25 | const _fragment = `#version 300 es 26 | precision highp float; 27 | 28 | in vec2 texCoord; 29 | uniform sampler2D _texture; 30 | out vec4 outColor; 31 | 32 | uniform sampler2D map; 33 | uniform float filterStrength; 34 | 35 | ${srgb_linear_fn} 36 | 37 | vec4 lut(vec4 color) { 38 | vec3 texel = color.rgb; 39 | texel = fromLinear(texel); 40 | float size = 33.0; 41 | float sliceSize = 1.0 / size; 42 | float slicePixelSize = sliceSize / size; 43 | float sliceInnerSize = slicePixelSize * (size - 1.0); 44 | float xOffset = 0.5 * sliceSize + texel.x * (1.0 - sliceSize); 45 | float yOffset = 0.5 * slicePixelSize + texel.y * sliceInnerSize; 46 | float zOffset = texel.z * (size - 1.0); 47 | float zSlice0 = floor(zOffset); 48 | float zSlice1 = zSlice0 + 1.0; 49 | float s0 = yOffset + (zSlice0 * sliceSize); 50 | float s1 = yOffset + (zSlice1 * sliceSize); 51 | vec4 slice0Color = texture(map, vec2(xOffset, s0)); 52 | vec4 slice1Color = texture(map, vec2(xOffset, s1)); 53 | texel = mix(slice0Color, slice1Color, zOffset - zSlice0).rgb; 54 | texel = toLinear(texel); 55 | return vec4(texel, color.a); 56 | } 57 | 58 | void main() { 59 | vec4 color = texture(_texture, texCoord); 60 | outColor = color * (1.0 - filterStrength) + lut(color) * filterStrength; 61 | } 62 | ` 63 | mini._.$insta1 = mini._.$insta1 || new Shader(gl, null, _fragment); 64 | mini._.$instatxt1 = mini._.$instatxt1 || new Texture(gl); 65 | mini._.$instatxt1.loadImage(opt.map1, gl.RGBA) 66 | mini._.$instatxt1.use(1); 67 | mini.runFilter(mini._.$insta1, { filterStrength: mix??1, map:{unit:1} }); 68 | } 69 | else if(opt.type==='2'){ 70 | //SHADER 2: 2x horizontal curve 256x1 (map1=luma, map2=rgb) 71 | //CLARENDON 72 | //NOTE: color is linear -> load LUTs as linear -> map color-to-srgb vs LUT -> return linear 73 | const _fragment = `#version 300 es 74 | precision highp float; 75 | precision highp int; 76 | 77 | in vec2 texCoord; 78 | uniform sampler2D _texture; 79 | out vec4 outColor; 80 | 81 | uniform sampler2D map; 82 | uniform sampler2D map2; 83 | uniform float filterStrength; 84 | 85 | ${srgb_linear_fn} 86 | 87 | vec4 lut(vec4 color) { 88 | vec3 texel = color.rgb; 89 | texel = fromLinear(texel); 90 | texel.r = texture(map, vec2(texel.r, 0.5)).r; 91 | texel.g = texture(map, vec2(texel.g, 0.5)).g; 92 | texel.b = texture(map, vec2(texel.b, 0.5)).b; 93 | float luma = dot(vec3(0.2126, 0.7152, 0.0722), texel); 94 | float shadowCoeff = 0.35 * max(0.0, 1.0 - luma); 95 | texel = mix(texel, max(vec3(0.0), 2.0 * texel - 1.0), shadowCoeff); 96 | texel = mix(texel, vec3(luma), -0.3); 97 | texel.r = texture(map2, vec2(texel.r, 0.5)).r; 98 | texel.g = texture(map2, vec2(texel.g, 0.5)).g; 99 | texel.b = texture(map2, vec2(texel.b, 0.5)).b; 100 | texel = toLinear(texel); 101 | return vec4(texel, color.a); 102 | } 103 | 104 | void main() { 105 | vec4 color = texture(_texture, texCoord); 106 | color = color * (1.0 - filterStrength) + lut(color) * filterStrength; 107 | outColor = color; 108 | } 109 | ` 110 | mini._.$insta2 = mini._.$insta2 || new Shader(gl, null, _fragment); 111 | mini._.$instatxt1 = mini._.$instatxt1 || new Texture(gl); 112 | mini._.$instatxt2 = mini._.$instatxt2 || new Texture(gl); 113 | mini._.$instatxt1.loadImage(opt.map1, gl.RGBA) 114 | mini._.$instatxt2.loadImage(opt.map2, gl.RGBA) 115 | 116 | mini._.$instatxt1.use(1); 117 | mini._.$instatxt2.use(2); 118 | mini.runFilter(mini._.$insta2, { filterStrength: mix??1, map:{unit:1}, map2:{unit:2} }); 119 | } 120 | else if(opt.type==='3'){ 121 | //SHADER 3: 2x horizontal curve 256x1 (map e mapLgg) 122 | //GINGHAM 123 | const _fragment = `#version 300 es 124 | precision highp float; 125 | precision highp int; 126 | 127 | in vec2 texCoord; 128 | uniform sampler2D _texture; 129 | out vec4 outColor; 130 | 131 | uniform sampler2D map; 132 | uniform sampler2D mapLgg; 133 | uniform float filterStrength; 134 | 135 | ${srgb_linear_fn} 136 | 137 | vec4 lut(vec4 color) { 138 | vec3 texel = color.rgb; 139 | texel = fromLinear(texel); 140 | texel = min(texel * 1.1343, vec3(1.0)); 141 | texel.r = texture(map, vec2(texel.r, 0.5)).r; 142 | texel.g = texture(map, vec2(texel.g, 0.5)).g; 143 | texel.b = texture(map, vec2(texel.b, 0.5)).b; 144 | vec3 shadowColor = vec3(0.956862, 0.0, 0.83529); 145 | float luma = dot(vec3(0.309, 0.609, 0.082), texel); 146 | vec3 shadowBlend = 2.0 * shadowColor * texel; 147 | float shadowAmount = 0.6 * max(0.0, (1.0 - 4.0 * luma)); 148 | texel = mix(texel, shadowBlend, shadowAmount); 149 | vec3 lgg; 150 | lgg.r = texture(mapLgg, vec2(texel.r, 0.5)).r; 151 | lgg.g = texture(mapLgg, vec2(texel.g, 0.5)).g; 152 | lgg.b = texture(mapLgg, vec2(texel.b, 0.5)).b; 153 | texel = mix(texel, lgg, min(1.0, 0.8 + luma)); 154 | texel = toLinear(texel); 155 | return vec4(texel, color.a); 156 | } 157 | 158 | void main() { 159 | vec4 color = texture(_texture, texCoord); 160 | outColor = color * (1.0 - filterStrength) + lut(color) * filterStrength; 161 | } 162 | ` 163 | mini._.$insta3 = mini._.$insta3 || new Shader(gl, null, _fragment); 164 | mini._.$instatxt1 = mini._.$instatxt1 || new Texture(gl, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE); 165 | mini._.$instatxt1.loadImage(opt.map1, gl.RGBA) 166 | mini._.$instatxt2 = mini._.$instatxt2 || new Texture(gl, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE); 167 | mini._.$instatxt2.loadImage(opt.map2, gl.RGBA) 168 | 169 | mini._.$instatxt1.use(1); 170 | mini._.$instatxt2.use(2); 171 | mini.runFilter(mini._.$insta3, { filterStrength: mix??1, map:{unit:1}, mapLgg:{unit:2} }); 172 | } 173 | else if(opt.type==='4'){ 174 | //SHADER 4: 2x horizontal curve 256x1 (map1=desat e map2=rgb) 175 | //MOON 176 | const _fragment = `#version 300 es 177 | precision highp float; 178 | precision highp int; 179 | 180 | in vec2 texCoord; 181 | uniform sampler2D _texture; 182 | out vec4 outColor; 183 | 184 | uniform sampler2D map; 185 | uniform sampler2D map2; 186 | uniform float filterStrength; 187 | 188 | ${srgb_linear_fn} 189 | 190 | vec4 lut(vec4 color) { 191 | vec3 texel = color.rgb; 192 | texel = fromLinear(texel); 193 | texel.r = texture(map, vec2(texel.r, 0.5)).r; 194 | texel.g = texture(map, vec2(texel.g, 0.5)).g; 195 | texel.b = texture(map, vec2(texel.b, 0.5)).b; 196 | vec3 desat = vec3(dot(vec3(0.7, 0.2, 0.1), texel)); 197 | texel = mix(texel, desat, 0.79); 198 | texel = vec3(min(1.0, 1.2 * dot(vec3(0.2, 0.7, 0.1), texel))); 199 | texel.r = texture(map2, vec2(texel.r, 0.5)).r; 200 | texel.g = texture(map2, vec2(texel.g, 0.5)).g; 201 | texel.b = texture(map2, vec2(texel.b, 0.5)).b; 202 | texel = toLinear(texel); 203 | return vec4(texel, color.a); 204 | } 205 | 206 | void main() { 207 | vec4 color = texture(_texture, texCoord); 208 | outColor = color * (1.0 - filterStrength) + lut(color) * filterStrength; 209 | } 210 | ` 211 | mini._.$insta4 = mini._.$insta4 || new Shader(gl, null, _fragment); 212 | mini._.$instatxt1 = mini._.$instatxt1 || new Texture(gl, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE); 213 | mini._.$instatxt1.loadImage(opt.map1, gl.RGBA) 214 | mini._.$instatxt2 = mini._.$instatxt2 || new Texture(gl, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE); 215 | mini._.$instatxt2.loadImage(opt.map2, gl.RGBA) 216 | 217 | mini._.$instatxt1.use(1); 218 | mini._.$instatxt2.use(2); 219 | mini.runFilter(mini._.$insta4, { filterStrength: mix??1, map:{unit:1}, map2:{unit:2} }); 220 | } 221 | else if(opt.type==='MTX'){ 222 | 223 | //COLORMATRIX (eg from pixie.js) 224 | 225 | const _fragment = `#version 300 es 226 | precision highp float; 227 | precision highp int; 228 | 229 | in vec2 texCoord; 230 | uniform sampler2D _texture; 231 | out vec4 outColor; 232 | 233 | uniform float filterStrength; 234 | uniform mat4 uColorMatrix; 235 | uniform vec4 uColorOffset; 236 | 237 | vec4 applyColorMatrix(vec4 c, mat4 m, vec4 o) { 238 | vec4 res = (c * m) + (o * c.a); 239 | res = clamp(res, 0.0, 1.0); 240 | return res; 241 | } 242 | 243 | void main() { 244 | vec4 color = texture(_texture, texCoord); 245 | color = applyColorMatrix(color, uColorMatrix, uColorOffset); 246 | outColor = color; 247 | } 248 | ` 249 | 250 | 251 | let colormatrix={ //[r,g,b,a,w] 252 | identity: [ 253 | [1,0,0,0,0], 254 | [0,1,0,0,0], 255 | [0,0,1,0,0], 256 | [0,0,0,1,0], 257 | ], 258 | polaroid: [ 259 | [1+.438*mix, -0.062*mix, -0.062*mix, 0, 0,], 260 | [-0.122*mix, 1+.378*mix, -0.122*mix, 0, 0,], 261 | [-0.016*mix, -0.016*mix, 1+.483*mix, 0, 0,], 262 | [0, 0, 0, 1, 0,] 263 | ], 264 | kodachrome:[ 265 | [(1+.1285582396593525*mix)*((mix/2+1)/2+0.5), -0.3967382283601348*mix, -0.03992559172921793*mix, 0, 0.06372958762196502*mix,], 266 | [-0.16404339962244616*mix, (1+.0835251566291304*mix)*((mix/2+1)/2+0.5), -0.05498805115633132*mix, 0, 0.024732407896706203*mix,], 267 | [-0.16786010706155763*mix, -0.5603416277695248*mix, (1+.6014850761964943*mix)*((mix/2+1)/2+0.5), 0, 0.03562982807460946*mix,], 268 | [0, 0, 0, 1, 0,] 269 | ], 270 | browni: [ 271 | [(1-0.4002976502*mix)*((mix/1.5+1)/2+0.5), 0.34553243048391263*mix, -0.2708298674538042*mix, 0, 47.43192855600873/500*mix,], 272 | [-0.037703249837783157*mix, (1-0.1390422412*mix)*((mix/1.5+1)/2+0.5), 0.15059552388459913*mix, 0, -36.96841498319127/500*mix,], 273 | [0.24113635128153335*mix, -0.07441037908422492*mix, (1-0.5502781794*mix)*((mix/1.5+1)/2+0.5), 0, -7.562075277591283/500*mix,], 274 | [0, 0, 0, 1, 0,] 275 | ], 276 | vintage: [ 277 | [(1-0.3720654364*mix)*((mix/1.5+1)/2+0.5), 0.3202183420819367*mix, -0.03965408211312453*mix, 0, 9.651285835294123/1000*mix,], 278 | [0.02578397704808868*mix, (1-0.3558811356*mix)*((mix/1.5+1)/2+0.5), 0.03259127616149294*mix, 0, 7.462829176470591/1000*mix,], 279 | [0.0466055556782719*mix, -0.0851232987247891*mix, (1-0.4758351981*mix)*((mix/1.5+1)/2+0.5), 0, 5.159190588235296/1000*mix,], 280 | [0, 0, 0, 1, 0,] 281 | ], 282 | } 283 | 284 | let cMatrix=colormatrix.identity 285 | let cOffset = [0,0,0,0] 286 | 287 | if(mix) cMatrix = multiplyM(cMatrix,colormatrix[opt.mtx], 4) 288 | if(mix) cOffset = [0,1,2,3].map(i=> cOffset[i]+colormatrix[opt.mtx][i][4]) 289 | 290 | mini._.$insta5 = mini._.$insta5 || new Shader(gl, null, _fragment); 291 | const uColorMatrix = cMatrix.flat(); 292 | const uColorOffset = cOffset; 293 | mini.runFilter(mini._.$insta5, { uColorMatrix, uColorOffset }); 294 | } 295 | } 296 | 297 | function multiplyM(A, B, N=3) { 298 | let C=[]; 299 | for (var i = 0; i < N; i++) 300 | { 301 | C.push([]) 302 | for (var j = 0; j < N; j++) 303 | { 304 | C[i].push(0) 305 | for (var k = 0; k < N; k++) 306 | { 307 | if(A[i]&&B[k]) C[i][j] += A[i][k]*B[k][j]; 308 | } 309 | } 310 | } 311 | return C 312 | } 313 | -------------------------------------------------------------------------------- /src/filters/filterMatrix.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | export function filterMatrix(mini,params){ 4 | const {gl,img}=mini 5 | //console.log('filterMatrix') 6 | params=params||{translateX:0,translateY:0,angle:0,scale:0,flipv:0,fliph:0}; 7 | let {translateX,translateY,angle,scale:_scale,flipv,fliph} = params 8 | _scale+=1 //in order to use -1/+1 range as scale input, so that 0 = 1:1 scale 9 | let scale=[_scale,_scale] 10 | //convert translateX/Y from clip space(0-1) to pixel space 11 | const translation = [ 12 | Math.round(gl.canvas.width*translateX*100)/100, 13 | Math.round(gl.canvas.height*translateY*100)/100, 14 | ] 15 | 16 | const _vertex = `#version 300 es 17 | in vec2 vertex; 18 | uniform mat3 matrix; 19 | out vec2 texCoord; 20 | void main() { 21 | texCoord = vertex; 22 | gl_Position = vec4((matrix * vec3(vertex, 1)).xy, 0, 1); 23 | } 24 | ` 25 | 26 | const _fragment = `#version 300 es 27 | precision highp float; 28 | in vec2 texCoord; 29 | uniform sampler2D _texture; 30 | out vec4 outColor; 31 | void main() { 32 | outColor = texture(_texture, vec2(texCoord.x, texCoord.y)); 33 | } 34 | ` 35 | 36 | //check if canvas is rotated using mini.height, as it's updated in case of crop or resize 37 | if(gl.canvas.width===mini.height){ 38 | const aspectratio = img.width/img.height 39 | scale[0]*=aspectratio 40 | scale[1]/=aspectratio 41 | } 42 | // Compute the matrices 43 | const projectionMatrix = m3.projection(gl.canvas.width, gl.canvas.height); 44 | const translationMatrix = m3.translation(translation[0], translation[1]); 45 | const rotationMatrix = m3.rotation(-angle * Math.PI / 180); 46 | const scaleMatrix = m3.scaling(scale[0]*(-fliph||1), scale[1]*(-flipv||1)); 47 | 48 | //center crop selection 49 | let crop_translationMatrix 50 | 51 | let matrix = [1,0,0,0,1,0,0,0,1] //identity matrix 52 | matrix = m3.multiply(matrix, projectionMatrix); 53 | //matrix = m3.multiply(matrix, _scaleMatrix); 54 | matrix = m3.multiply(matrix, translationMatrix); 55 | //set pivot at the center of the image 56 | matrix = m3.multiply(matrix, m3.translation(gl.canvas.width/2, gl.canvas.height/2)) 57 | matrix = m3.multiply(matrix, rotationMatrix); 58 | matrix = m3.multiply(matrix, scaleMatrix); 59 | //reset pivot 60 | matrix = m3.multiply(matrix, m3.translation(-gl.canvas.width/2, -gl.canvas.height/2)) 61 | // scale our 1 unit quad from 1 unit to img width & height 62 | matrix = m3.multiply(matrix, m3.scaling(gl.canvas.width, gl.canvas.height)) 63 | //setup and run effect 64 | mini._.$matrix = mini._.$matrix || new Shader(gl, _vertex, _fragment) 65 | mini.runFilter(mini._.$matrix, {matrix}) 66 | } 67 | 68 | var m3 = { 69 | projection: function projection(width, height) { 70 | // Note: This matrix flips the Y axis so that 0 is at the top. 71 | return [ 72 | 2 / width, 0, 0, 73 | 0, 2 / height, 0, 74 | -1, -1, 1, 75 | ]; 76 | }, 77 | 78 | translation: function translation(tx, ty) { 79 | return [ 80 | 1, 0, 0, 81 | 0, 1, 0, 82 | tx, ty, 1, 83 | ]; 84 | }, 85 | 86 | rotation: function rotation(angleInRadians) { 87 | var c = Math.cos(angleInRadians); 88 | var s = Math.sin(angleInRadians); 89 | return [ 90 | c, -s, 0, 91 | s, c, 0, 92 | 0, 0, 1, 93 | ]; 94 | }, 95 | 96 | scaling: function scaling(sx, sy) { 97 | return [ 98 | sx, 0, 0, 99 | 0, sy, 0, 100 | 0, 0, 1, 101 | ]; 102 | }, 103 | 104 | multiply: function multiply(a, b) { 105 | var a00 = a[0 * 3 + 0]; 106 | var a01 = a[0 * 3 + 1]; 107 | var a02 = a[0 * 3 + 2]; 108 | var a10 = a[1 * 3 + 0]; 109 | var a11 = a[1 * 3 + 1]; 110 | var a12 = a[1 * 3 + 2]; 111 | var a20 = a[2 * 3 + 0]; 112 | var a21 = a[2 * 3 + 1]; 113 | var a22 = a[2 * 3 + 2]; 114 | var b00 = b[0 * 3 + 0]; 115 | var b01 = b[0 * 3 + 1]; 116 | var b02 = b[0 * 3 + 2]; 117 | var b10 = b[1 * 3 + 0]; 118 | var b11 = b[1 * 3 + 1]; 119 | var b12 = b[1 * 3 + 2]; 120 | var b20 = b[2 * 3 + 0]; 121 | var b21 = b[2 * 3 + 1]; 122 | var b22 = b[2 * 3 + 2]; 123 | return [ 124 | b00 * a00 + b01 * a10 + b02 * a20, 125 | b00 * a01 + b01 * a11 + b02 * a21, 126 | b00 * a02 + b01 * a12 + b02 * a22, 127 | b10 * a00 + b11 * a10 + b12 * a20, 128 | b10 * a01 + b11 * a11 + b12 * a21, 129 | b10 * a02 + b11 * a12 + b12 * a22, 130 | b20 * a00 + b21 * a10 + b22 * a20, 131 | b20 * a01 + b21 * a11 + b22 * a21, 132 | b20 * a02 + b21 * a12 + b22 * a22, 133 | ]; 134 | }, 135 | }; 136 | -------------------------------------------------------------------------------- /src/filters/filterPerspective.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from '../minigl.js' 2 | 3 | /** 4 | * @filter Matrix Warp 5 | * @description Transforms an image by a 2x2 or 3x3 matrix. The coordinates used in 6 | * the transformation are (x, y) for a 2x2 matrix or (x, y, 1) for a 7 | * 3x3 matrix, where x and y are in units of pixels. 8 | * @param matrix A 2x2 or 3x3 matrix represented as either a list or a list of lists. 9 | * For example, the 3x3 matrix [[2,0,0],[0,3,0],[0,0,1]] can also be 10 | * represented as [2,0,0,0,3,0,0,0,1] or just [2,0,0,3]. 11 | * @param inverse A boolean value that, when true, applies the inverse transformation 12 | * instead. (optional, defaults to false) 13 | * @param useTextureSpace A boolean value that, when true, uses texture-space coordinates 14 | * instead of screen-space coordinates. Texture-space coordinates range 15 | * from -1 to 1 instead of 0 to width - 1 or height - 1, and are easier 16 | * to use for simple operations like flipping and rotating. 17 | */ 18 | function matrixWarp(mini, matrix, inverse, useTextureSpace) { 19 | 20 | const _fragment = `#version 300 es 21 | precision highp float; 22 | 23 | in vec2 texCoord; 24 | uniform sampler2D _texture; 25 | uniform vec2 uResolution; 26 | uniform mat3 matrix; 27 | uniform bool useTextureSpace; 28 | out vec4 outColor; 29 | 30 | void main() { 31 | vec2 coord = texCoord * uResolution; 32 | if (useTextureSpace) coord = coord / uResolution * 2.0 - 1.0; 33 | vec3 warp = matrix * vec3(coord, 1.0); 34 | coord = warp.xy / warp.z; 35 | if (useTextureSpace) coord = (coord * 0.5 + 0.5) * uResolution; 36 | vec4 color = texture(_texture, coord / uResolution); 37 | vec2 clampedCoord = clamp(coord, vec2(0.0), uResolution); 38 | if (coord != clampedCoord) { 39 | //color.a *= max(0.0, 1.0 - length(coord - clampedCoord)); 40 | color.a = 0.; 41 | } 42 | outColor = color; 43 | } 44 | ` 45 | 46 | 47 | const {gl, img}=mini 48 | mini._.$warp = mini._.$warp || new Shader(gl, null, _fragment); 49 | 50 | // Flatten all members of matrix into one big list 51 | matrix = Array.prototype.concat.apply([], matrix); 52 | // Extract a 3x3 matrix out of the arguments 53 | if (matrix.length == 4) { 54 | matrix = [ 55 | matrix[0], matrix[1], 0, 56 | matrix[2], matrix[3], 0, 57 | 0, 0, 1 58 | ]; 59 | } else if (matrix.length != 9) { 60 | throw 'can only warp with 2x2 or 3x3 matrix'; 61 | } 62 | 63 | const uResolution = [gl.canvas.width,gl.canvas.height]; 64 | mini.runFilter(mini._.$warp, { 65 | matrix: inverse ? getInverse(matrix) : matrix, 66 | uResolution, 67 | useTextureSpace: useTextureSpace | 0 68 | }); 69 | } 70 | 71 | 72 | export function filterPerspective(mini, before, after, inverse, useTextureSpace ) { 73 | 74 | before=before.flat() 75 | after=after.flat() 76 | var a = getSquareToQuad.apply(null, after); 77 | var b = getSquareToQuad.apply(null, before); 78 | var c = multiply(getInverse(a), b); 79 | return matrixWarp(mini, c, inverse, useTextureSpace); 80 | } 81 | 82 | 83 | 84 | // from javax.media.jai.PerspectiveTransform 85 | function getSquareToQuad(x0, y0, x1, y1, x2, y2, x3, y3) { 86 | var dx1 = x1 - x2; 87 | var dy1 = y1 - y2; 88 | var dx2 = x3 - x2; 89 | var dy2 = y3 - y2; 90 | var dx3 = x0 - x1 + x2 - x3; 91 | var dy3 = y0 - y1 + y2 - y3; 92 | var det = dx1*dy2 - dx2*dy1; 93 | var a = (dx3*dy2 - dx2*dy3) / det; 94 | var b = (dx1*dy3 - dx3*dy1) / det; 95 | return [ 96 | x1 - x0 + a*x1, y1 - y0 + a*y1, a, 97 | x3 - x0 + b*x3, y3 - y0 + b*y3, b, 98 | x0, y0, 1 99 | ]; 100 | } 101 | 102 | function getInverse(m) { 103 | var a = m[0], b = m[1], c = m[2]; 104 | var d = m[3], e = m[4], f = m[5]; 105 | var g = m[6], h = m[7], i = m[8]; 106 | var det = a*e*i - a*f*h - b*d*i + b*f*g + c*d*h - c*e*g; 107 | return [ 108 | (e*i - f*h) / det, (c*h - b*i) / det, (b*f - c*e) / det, 109 | (f*g - d*i) / det, (a*i - c*g) / det, (c*d - a*f) / det, 110 | (d*h - e*g) / det, (b*g - a*h) / det, (a*e - b*d) / det 111 | ]; 112 | } 113 | 114 | function multiply(a, b) { 115 | return [ 116 | a[0]*b[0] + a[1]*b[3] + a[2]*b[6], 117 | a[0]*b[1] + a[1]*b[4] + a[2]*b[7], 118 | a[0]*b[2] + a[1]*b[5] + a[2]*b[8], 119 | a[3]*b[0] + a[4]*b[3] + a[5]*b[6], 120 | a[3]*b[1] + a[4]*b[4] + a[5]*b[7], 121 | a[3]*b[2] + a[4]*b[5] + a[5]*b[8], 122 | a[6]*b[0] + a[7]*b[3] + a[8]*b[6], 123 | a[6]*b[1] + a[7]*b[4] + a[8]*b[7], 124 | a[6]*b[2] + a[7]*b[5] + a[8]*b[8] 125 | ]; 126 | } -------------------------------------------------------------------------------- /src/minigl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HEAVILY MODIFIED VERSION of glfx.js by Evan Wallace 3 | * https://evanw.github.io/glfx.js/ 4 | */ 5 | 6 | import * as Filters from './minigl_filters.js' 7 | export {Spline} from './filters/cubicspline.js' 8 | 9 | const usesrgb=true //to guarantee gamma correct workflow, SRGB in input - processing is linear - SRGB in output 10 | 11 | export function minigl(canvas,img,colorspace) { 12 | let gl = canvas.getContext("webgl2",{ antialias:false, premultipliedAlpha: true, }) 13 | if (!gl) return console.error("webgl2 not supported!") 14 | if(colorspace==="display-p3") { 15 | gl.drawingBufferColorSpace = "display-p3"; 16 | gl.unpackColorSpace = "display-p3"; 17 | } else { 18 | gl.drawingBufferColorSpace = "srgb"; 19 | gl.unpackColorSpace = "srgb"; 20 | } 21 | 22 | const _minigl= { 23 | width:0, 24 | height:0, 25 | gl, 26 | img, 27 | destroy, 28 | loadImage, 29 | paintCanvas, 30 | crop, 31 | resetCrop, 32 | resize, 33 | resetResize, 34 | captureImage, 35 | readPixels, 36 | runFilter, 37 | setupFiltersTextures, 38 | _:{} //for filters' storage 39 | } 40 | 41 | //update canvas size to image for full resolution. Use style to change visible sizes 42 | gl.canvas.width=_minigl.width=img.width 43 | gl.canvas.height=_minigl.height=img.height 44 | 45 | //create IMAGE TEXTURE && load image 46 | const imageTexture = new Texture(gl) 47 | imageTexture.loadImage(img) 48 | //imageTexture.label='imageTXT' 49 | //create default SHADER 50 | const defaultShader = new Shader(gl) 51 | const flippedShader = new Shader(gl,null,flippedFragmentSource) 52 | 53 | //create two effects' blank textures to handle [image-->shaderA-->txt1-->shaderB-->txt2-->canvas] 54 | //note: setupFiltersTextures needs to be re-run if canvas width/height change (eg when changing aspect ratio) 55 | let textures, count=0; 56 | function setupFiltersTextures(){ 57 | if(textures?.length) textures.forEach(e=>e.destroy()) 58 | textures=[] 59 | for (var ii = 0; ii < 2; ++ii) { 60 | // make the blank texture the same size as the canvas 61 | const texture = new Texture(gl,gl.canvas.width, gl.canvas.height); 62 | textures.push(texture); 63 | } 64 | } 65 | setupFiltersTextures() 66 | 67 | function destroy(){ 68 | if(textures?.length) textures.forEach(e=>e.destroy()) 69 | if(croppedTexture) croppedTexture.destroy() 70 | imageTexture.destroy() 71 | delete _minigl.img_cropped 72 | } 73 | 74 | let current_texture 75 | function runFilter(shader, uniforms){ 76 | if(uniforms) shader.uniforms(uniforms) 77 | //console.log('runFilter',current_texture.label) 78 | if(current_texture) current_texture.use() 79 | textures[count%2].drawTo() 80 | shader.drawRect() 81 | current_texture=textures[count%2] 82 | count++ 83 | } 84 | 85 | function loadImage(){ 86 | if(croppedTexture) current_texture= croppedTexture 87 | else current_texture=imageTexture 88 | runFilter(defaultShader,null) 89 | } 90 | 91 | function paintCanvas(){ 92 | if(current_texture) current_texture.use() 93 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) //draw to canvas 94 | flippedShader.drawRect() 95 | } 96 | 97 | 98 | let resized = {width:0,height:0} 99 | function resize(width,height){ 100 | gl.canvas.width=_minigl.width=resized.width = width 101 | gl.canvas.height=_minigl.height=resized.height = height 102 | setupFiltersTextures() 103 | } 104 | function resetResize(){ 105 | if(!resized.width) return 106 | resized.width=resized.height=0 107 | gl.canvas.width=_minigl.width= cropsize.width || img.width 108 | gl.canvas.height=_minigl.height= cropsize.height || img.height 109 | setupFiltersTextures() 110 | } 111 | 112 | 113 | let croppedTexture 114 | let cropsize = {width:0,height:0} 115 | function crop({left, top, width, height}){ 116 | const length = width * height * 4; 117 | const data = new Uint8Array(length); 118 | 119 | //FIX FOR SAFARI & display-p3 bug (a direct GL->2D drawImage loses colorspace ... this is a workaround) 120 | runFilter(defaultShader,{}) 121 | gl.readPixels(left,top,width,height,gl.RGBA,gl.UNSIGNED_BYTE,data); 122 | const colorspace=gl.unpackColorSpace 123 | const imgdata_cropped = new ImageData(new Uint8ClampedArray(data.buffer), width, height, { colorSpace: colorspace}) 124 | 125 | croppedTexture = new Texture(gl) 126 | croppedTexture.loadImage(imgdata_cropped) 127 | gl.canvas.width=_minigl.width=cropsize.width = width 128 | gl.canvas.height=_minigl.height=cropsize.height = height 129 | setupFiltersTextures() 130 | _minigl.img_cropped = imagedata_to_image(imgdata_cropped,colorspace) 131 | } 132 | 133 | function resetCrop(){ 134 | if(!croppedTexture) return 135 | croppedTexture.destroy() 136 | croppedTexture=null 137 | cropsize.width=cropsize.height=0 138 | gl.canvas.width=_minigl.width= resized.width || img.width 139 | gl.canvas.height=_minigl.height= resized.height || img.height 140 | delete _minigl.img_cropped 141 | setupFiltersTextures() 142 | } 143 | 144 | //type: String - indicating the image format. The default type is image/png 145 | //quality: Number - between 0 and 1 indicating the image quality to be used with lossy compression 146 | //returns Image 147 | function captureImage(type, quality){ 148 | runFilter(defaultShader,{}) 149 | const {width,height}=gl.canvas 150 | const length = width * height * 4; 151 | const data = new Uint8Array(length); 152 | gl.readPixels(0,0,width,height,gl.RGBA,gl.UNSIGNED_BYTE,data); 153 | //note: data.buffer contains raw pixel ArrayBuffer (for future reference to feed an image compressor) 154 | const colorspace=gl.unpackColorSpace 155 | const imgdata = new ImageData(new Uint8ClampedArray(data.buffer), width, height, { colorSpace: colorspace}) 156 | return imagedata_to_image(imgdata, colorspace, type,quality) 157 | } 158 | 159 | function readPixels(){ 160 | runFilter(defaultShader,{}) 161 | const {width,height}=gl.canvas 162 | const length = width * height * 4; 163 | const data = new Uint8Array(length); 164 | gl.readPixels(0,0,width,height,gl.RGBA,gl.UNSIGNED_BYTE,data); 165 | return data 166 | } 167 | 168 | 169 | 170 | //load all filters 171 | function wrap(fn){ 172 | return function(...args){fn(_minigl,...args)} 173 | } 174 | Object.keys(Filters).forEach(f=>_minigl[f]=wrap(Filters[f])) 175 | 176 | return _minigl 177 | } 178 | 179 | const flippedFragmentSource = usesrgb 180 | ?`#version 300 es 181 | precision highp float; 182 | in vec2 texCoord; 183 | uniform sampler2D _texture; 184 | out vec4 outColor; 185 | 186 | vec4 fromLinear(vec4 linearRGB) { 187 | bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308)); 188 | vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055); 189 | vec3 lower = linearRGB.rgb * vec3(12.92); 190 | return vec4(mix(higher, lower, cutoff), linearRGB.a); 191 | } 192 | 193 | void main() { 194 | vec4 color = texture(_texture, vec2(texCoord.x, 1.0 - texCoord.y)); 195 | //outColor = color; 196 | outColor = fromLinear(color); 197 | }` 198 | :`#version 300 es 199 | precision highp float; 200 | in vec2 texCoord; 201 | uniform sampler2D _texture; 202 | out vec4 outColor; 203 | 204 | void main() { 205 | outColor = texture(_texture, vec2(texCoord.x, 1.0 - texCoord.y)); 206 | }` 207 | 208 | 209 | export function Shader(gl,vertexSrc,fragmentSrc) { 210 | 211 | const defaultVertexSource = `#version 300 es 212 | in vec2 vertex; 213 | out vec2 texCoord; 214 | 215 | void main() { 216 | texCoord = vertex; 217 | gl_Position = vec4(vertex * 2.0 - 1.0, 0.0, 1.0); 218 | } 219 | `; 220 | 221 | const defaultFragmentSource = `#version 300 es 222 | precision highp float; 223 | 224 | in vec2 texCoord; 225 | uniform sampler2D _texture; 226 | out vec4 outColor; 227 | 228 | void main() { 229 | outColor = texture(_texture, texCoord); 230 | } 231 | `; 232 | 233 | 234 | const program = gl.createProgram() 235 | let vertex 236 | gl.attachShader(program,compileSource(gl, gl.VERTEX_SHADER, vertexSrc||defaultVertexSource)) 237 | gl.attachShader(program,compileSource(gl, gl.FRAGMENT_SHADER,fragmentSrc|| defaultFragmentSource)) 238 | gl.linkProgram(program) 239 | 240 | 241 | function drawRect(refresh=true, left, top, right, bottom){ 242 | //get the current viewport 243 | const viewport = gl.getParameter(gl.VIEWPORT); 244 | left = left !== undefined ? (left - viewport[0]) / viewport[2] : 0; 245 | top = top !== undefined ? (top - viewport[1]) / viewport[3] : 0; 246 | right = right !== undefined ? (right - viewport[0]) / viewport[2] : 1; 247 | bottom = bottom !== undefined ? (bottom - viewport[1]) / viewport[3] : 1; 248 | 249 | //prepare vertex 250 | gl.useProgram(program) 251 | gl.vertexBuffer = gl.vertexBuffer || gl.createBuffer() 252 | gl.bindBuffer(gl.ARRAY_BUFFER, gl.vertexBuffer) 253 | //1 unit wad 254 | gl.bufferData( 255 | gl.ARRAY_BUFFER, 256 | new Float32Array([ left, top, left, bottom, right, top, right, bottom ]), 257 | gl.STATIC_DRAW 258 | ) 259 | if(!vertex) { 260 | vertex = gl.getAttribLocation(program, "vertex") 261 | gl.enableVertexAttribArray(vertex) 262 | } 263 | gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0) 264 | 265 | //convert from clip space to pixel space 266 | //gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) 267 | //clear canvas 268 | if(refresh){ 269 | gl.clearColor(0, 0, 0, 0); 270 | gl.clear(gl.COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT); 271 | } 272 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) 273 | } 274 | 275 | function uniforms(uni={}){ 276 | gl.useProgram(program); 277 | for (let name in uni) { 278 | const location = gl.getUniformLocation(program, name); 279 | if (location === null) continue; // will be null if the uniform isn't used in the shader 280 | let value = uni[name]; 281 | if (Array.isArray(value)) { 282 | switch (value.length) { 283 | case 1: { 284 | if(Array.isArray(value[0])) value=value[0] //to load uniform float array[9] 285 | gl.uniform1fv(location, new Float32Array(value)); break; 286 | } 287 | case 2: gl.uniform2fv(location, new Float32Array(value)); break; 288 | case 3: gl.uniform3fv(location, new Float32Array(value)); break; 289 | case 4: gl.uniform4fv(location, new Float32Array(value)); break; 290 | case 9: gl.uniformMatrix3fv(location, false, new Float32Array(value)); break; 291 | case 16: gl.uniformMatrix4fv(location, false, new Float32Array(value)); break; 292 | default: throw 'dont\'t know how to load uniform "' + name + '" of length ' + value.length; 293 | } 294 | } 295 | else if (value?.unit) { // {unit:1} ... it's a texture loaded in slot unit 296 | gl.uniform1i(location, value.unit); 297 | } 298 | else if (typeof value === 'number') { 299 | gl.uniform1f(location, value); 300 | } 301 | else { 302 | throw 'attempted to set uniform "' + name + '" to invalid value ' + (value || 'undefined').toString(); 303 | } 304 | } 305 | } 306 | 307 | function compileSource(gl, type, source) { 308 | var shader = gl.createShader(type) 309 | gl.shaderSource(shader, source) 310 | gl.compileShader(shader) 311 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 312 | throw "compile error: " + gl.getShaderInfoLog(shader) 313 | } 314 | return shader 315 | } 316 | 317 | 318 | return {drawRect, uniforms} 319 | } 320 | 321 | export function Texture(gl, width, height) { 322 | let _width=width, _height=height 323 | let txt = gl.createTexture() 324 | gl.bindTexture(gl.TEXTURE_2D, txt); 325 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 326 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 327 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 328 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 329 | //if size provided, create blank texture 330 | 331 | const internalFormat = usesrgb ? gl.SRGB8_ALPHA8 : gl.RGBA 332 | if (width && height) gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 333 | //if (width && height) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 334 | 335 | function use(unit=0){ 336 | if(!txt) return console.error('texture has been destroyed') 337 | gl.activeTexture(gl.TEXTURE0 + unit); 338 | gl.bindTexture(gl.TEXTURE_2D, txt) 339 | } 340 | function destroy(){ 341 | gl.deleteTexture(txt); 342 | txt=null 343 | } 344 | function drawTo(){ 345 | if(!txt) return console.error('texture has been destroyed') 346 | // create/ reuse a framebuffer 347 | gl.framebuffer = gl.framebuffer || gl.createFramebuffer(); 348 | gl.bindFramebuffer(gl.FRAMEBUFFER, gl.framebuffer); 349 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, txt, 0); 350 | if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) { 351 | throw new Error('incomplete framebuffer'); 352 | } 353 | //sets the conversion from normalized device coordinates/ clip space to pixel space 354 | gl.viewport(0,0,_width,_height) 355 | } 356 | function loadImage(img, format){ 357 | if(!txt) return console.error('texture has been destroyed') 358 | _width=img.width 359 | _height=img.height 360 | gl.bindTexture(gl.TEXTURE_2D, txt) 361 | 362 | let internalFormat = format || (usesrgb ? gl.SRGB8_ALPHA8 : gl.RGBA) 363 | gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, gl.RGBA, gl.UNSIGNED_BYTE, img) 364 | //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img) 365 | //gl.texImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, gl.RGBA, gl.UNSIGNED_BYTE, img) 366 | } 367 | function initFromBytes(width, height, data, format) { 368 | _width=width 369 | _height=height 370 | gl.bindTexture(gl.TEXTURE_2D, txt); 371 | let internalFormat = format || (usesrgb ? gl.SRGB8_ALPHA8 : gl.RGBA) 372 | //console.log('initFromBytes',internalFormat) 373 | gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(data)); 374 | //gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(data)); 375 | }; 376 | 377 | return {use, destroy, drawTo, loadImage, initFromBytes} 378 | } 379 | 380 | function imagedata_to_image(imagedata, colorspace, type, quality) { 381 | const canvas = document.createElement('canvas'); 382 | var ctx = canvas.getContext('2d',{ colorSpace: colorspace }); 383 | canvas.width = imagedata.width; 384 | canvas.height = imagedata.height; 385 | ctx.putImageData(imagedata, 0, 0); 386 | 387 | var image = new Image(); 388 | image.src = canvas.toDataURL(type, quality); 389 | return image; 390 | } 391 | 392 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /src/minigl_filters.js: -------------------------------------------------------------------------------- 1 | import { Shader, Texture } from './minigl.js' 2 | 3 | export { filterMatrix } from './filters/filterMatrix.js' 4 | export { filterInsta } from './filters/filterInsta.js' 5 | export { filterCurves } from './filters/filterCurves.js' 6 | export { filterPerspective } from './filters/filterPerspective.js' 7 | export { filterBlend } from './filters/filterBlend.js' 8 | export { filterBlurBokeh } from './filters/filterBlurBokeh.js' 9 | export { filterBlurGaussian } from './filters/filterBlurGaussian.js' 10 | 11 | 12 | export function filterAdjustments(mini, effects) { 13 | //from https://pqina.nl/pintura/ 14 | //from https://tsev.dev/posts/2020-06-19-colour-correction-with-webgl/ 15 | //from https://api.pixijs.io/@pixi/filter-color-matrix/src/ColorMatrixFilter.ts.html 16 | 17 | const _fragment = `#version 300 es 18 | precision highp float; 19 | 20 | in vec2 texCoord; 21 | uniform sampler2D _texture; 22 | out vec4 outColor; 23 | 24 | uniform vec2 uTextureSize; 25 | uniform mat4 uColorMatrix; 26 | uniform vec4 uColorOffset; 27 | uniform float uClarityKernel[9]; 28 | uniform float uClarityKernelWeight; 29 | uniform float uColorGamma; 30 | uniform float uVibrance; 31 | uniform float uColorVignette; 32 | uniform vec2 uVignettePos; 33 | uniform float vibrance; 34 | 35 | vec4 applyGamma(vec4 c, float g) { 36 | c.r = pow(c.r, g); 37 | c.g = pow(c.g, g); 38 | c.b = pow(c.b, g); 39 | return c; 40 | } 41 | vec4 applyVibrance(vec4 c, float v){ 42 | float max = max(c.r, max(c.g, c.b)); 43 | float avg = (c.r + c.g + c.b) / 3.0; 44 | float amt = (abs(max - avg) * 2.0) * -v; 45 | c.r += max != c.r ? (max - c.r) * amt : 0.00; 46 | c.g += max != c.g ? (max - c.g) * amt : 0.00; 47 | c.b += max != c.b ? (max - c.b) * amt : 0.00; 48 | return c; 49 | } 50 | vec4 applyColorMatrix(vec4 c, mat4 m, vec4 o) { 51 | vec4 res = (c * m) + (o * c.a); 52 | res = clamp(res, 0.0, 1.0); 53 | return res; 54 | } 55 | vec4 applyConvolutionMatrix(vec4 c, float k0, float k1, float k2, float k3, float k4, float k5, float k6, float k7, float k8, float w) { 56 | vec2 pixel = vec2(1) / uTextureSize; 57 | vec4 colorSum = texture(_texture, texCoord - pixel) * k0 + texture(_texture, texCoord + pixel * vec2(0.0, -1.0)) * k1 + texture(_texture, texCoord + pixel * vec2(1.0, -1.0)) * k2 + texture(_texture, texCoord + pixel * vec2(-1.0, 0.0)) * k3 + texture(_texture, texCoord) * k4 + texture(_texture, texCoord + pixel * vec2(1.0, 0.0)) * k5 + texture(_texture, texCoord + pixel * vec2(-1.0, 1.0)) * k6 + texture(_texture, texCoord + pixel * vec2(0.0, 1.0)) * k7 + texture(_texture, texCoord + pixel) * k8; 58 | vec4 color = vec4(clamp((colorSum / w), 0.0, 1.0).rgb, c.a); 59 | return color; 60 | } 61 | 62 | vec4 applyVignette2(vec4 c, vec2 pos, float v, vec2 upos){ 63 | #define inner .20 64 | #define outer 1.1 65 | #define curvature .65 66 | vec2 curve = pow(abs(pos),vec2(1./curvature)); 67 | float edge = pow(length(curve),curvature); 68 | float scale = 1.-abs(upos.x); 69 | float vignette = 1.-v*smoothstep(inner*scale,outer*scale,edge); 70 | vec4 color = vec4(c.rgb *= vignette , c.a); 71 | return color; 72 | } 73 | 74 | vec4 vignette3(vec4 c, vec2 pos, float radius) 75 | { 76 | float ambientlight = 0.14; 77 | float circle = length(pos) - radius; 78 | float v = 1.0 - smoothstep(0.0, 0.4f, circle) + ambientlight; 79 | return vec4(c.rgb*v,c.a); 80 | } 81 | 82 | void main() { 83 | vec4 color = texture(_texture, texCoord); 84 | if (uClarityKernelWeight != -1.0) { 85 | color = applyConvolutionMatrix(color, uClarityKernel[0], uClarityKernel[1], uClarityKernel[2], uClarityKernel[3], uClarityKernel[4], uClarityKernel[5], uClarityKernel[6], uClarityKernel[7], uClarityKernel[8], uClarityKernelWeight); 86 | } 87 | color = applyGamma(color, uColorGamma); 88 | color = applyVibrance(color, uVibrance); 89 | color = applyColorMatrix(color, uColorMatrix, uColorOffset); 90 | if (uColorVignette != 0.0) { 91 | vec2 pos = texCoord.xy*2.-1. - uVignettePos; 92 | //color = vignette3(color, pos, uColorVignette); 93 | color = applyVignette2(color, pos, uColorVignette, uVignettePos); 94 | } 95 | outColor = color; 96 | } 97 | ` 98 | 99 | const {gl} = mini 100 | 101 | let vignpos = [0,0] 102 | let {brightness: b=0, contrast: c=0, saturation: s=0, exposure: e=0, temperature: t=0, gamma=0, clarity: l=0, vibrance=0, vignette=0, tint:tt=0, sepia:sp=0} = effects 103 | //some params adjustments to fit shader and user experience 104 | b=b/4;c=(c+1)/2+0.5;s=s+1;e=((e>0?e*3:e*1.5)+1)/2+0.5;gamma+=1;t*=2,tt*=2; 105 | 106 | let colormatrix={ //[r,g,b,a,w] 107 | //brightness t (0-2) 108 | brightness: [ 109 | [1, 0, 0, 0, b, ], 110 | [0, 1, 0, 0, b, ], 111 | [0, 0, 1, 0, b, ], 112 | [0, 0, 0, 1, 0] 113 | ], 114 | //constrast t (0-2) 115 | contrast: [ 116 | [c, 0, 0, 0, 0.5 * (1 - c), ], 117 | [0, c, 0, 0, 0.5 * (1 - c), ], 118 | [0, 0, c, 0, 0.5 * (1 - c), ], 119 | [0, 0, 0, 1, 0] 120 | ], 121 | //saturation (0-2) 122 | saturation: [ 123 | [0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, ], 124 | [0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, ], 125 | [0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, ], 126 | [0, 0, 0, 1, 0] 127 | ], 128 | //exposure (0-2) 129 | exposure: [ 130 | [e, 0, 0, 0, 0, ], 131 | [0, e, 0, 0, 0, ], 132 | [0, 0, e, 0, 0, ], 133 | [0, 0, 0, 1, 0] 134 | ], 135 | //temperature (-1 +1) 136 | temperature: t > 0 ? [ 137 | [1 + .1 * t, 0, 0, 0, 0, ], 138 | [0, 1, 0, 0, 0, ], 139 | [0, 0, 1 + .1 * -t, 0, 0, ], 140 | [0, 0, 0, 1, 0] 141 | ] : [ 142 | [1 + .15 * t, 0, 0, 0, 0, ], 143 | [0, 1 + .05 * t, 0, 0, 0, ], 144 | [0, 0, 1 + .15 * -t, 0, 0, ], 145 | [0, 0, 0, 1, 0] 146 | ], 147 | //tint 148 | tint: [ 149 | [1,0,0,0,0], 150 | [0,1+0.1*tt,0,0,0], 151 | [0,0,1,0,0], 152 | [0,0,0,1,0], 153 | ], 154 | //sepia 155 | sepia: [ 156 | [1-.607*sp,.769*sp,.189*sp,0,0], 157 | [.349*sp,1-.314*sp,.168*sp,0,0], 158 | [.272*sp,.534*sp,1-.869*sp,0,0], 159 | [0,0,0,1,0], 160 | ], 161 | /* 162 | //same as saturation! 163 | gray: [ 164 | [1-.7874*g,.7152*g,.0722*g,0,0], 165 | [.2126*g,1-.2848*g,.0722*g,0,0], 166 | [.2126*g,.7152*g,1-.9278*g,0,0], 167 | [0,0,0,1,0], 168 | ], 169 | */ 170 | identity: [ 171 | [1,0,0,0,0], 172 | [0,1,0,0,0], 173 | [0,0,1,0,0], 174 | [0,0,0,1,0], 175 | ], 176 | } 177 | 178 | let cMatrix=colormatrix.identity 179 | let cOffset = [0,0,0,0] 180 | cMatrix = multiplyM(cMatrix,colormatrix.brightness, 4) 181 | cOffset= [0,1,2,3].map(i=> cOffset[i]+colormatrix.brightness[i][4]) 182 | cMatrix = multiplyM(cMatrix,colormatrix.contrast, 4) 183 | cOffset =[0,1,2,3].map(i=> cOffset[i]+colormatrix.contrast[i][4]) 184 | cMatrix = multiplyM(cMatrix,colormatrix.saturation, 4) 185 | cMatrix = multiplyM(cMatrix,colormatrix.exposure, 4) 186 | cMatrix = multiplyM(cMatrix,colormatrix.temperature, 4) 187 | cMatrix = multiplyM(cMatrix,colormatrix.tint, 4) 188 | cMatrix = multiplyM(cMatrix,colormatrix.sepia, 4) 189 | 190 | 191 | let claritykernel = l >= 0 ? [ 192 | 0, -1 * l, 0, 193 | -1 * l, 1 + 4 * l, -1 * l, 194 | 0, -1 * l, 0 195 | ] : [ 196 | -1 * l, -2 * l, -1 * l, 197 | -2 * l, 1 + -3 * l, -2 * l, 198 | -1 * l, -2 * l, -1 * l 199 | ] 200 | let clarityweight = claritykernel.reduce(((e, t) => e + t), 0) 201 | clarityweight = clarityweight <= 0 ? 1 : clarityweight 202 | //clarity kernel has lenght=9, as a 3x3 matrix .. envelop in an array for Shader.uniforms to recognise it as a float array[] 203 | claritykernel = [claritykernel] 204 | 205 | //const {temperature,tint} = effects 206 | const uColorMatrix = cMatrix.flat(); 207 | const uColorOffset = cOffset; 208 | const uTextureSize = [gl.canvas.width,gl.canvas.height]; //[width,height]; 209 | const uVibrance=vibrance; 210 | const uColorVignette=vignette; 211 | const uClarityKernel=claritykernel; 212 | const uClarityKernelWeight=clarityweight; 213 | const uVignettePos=vignpos; 214 | 215 | 216 | //setup and run effect 217 | mini._.$adj = mini._.$adj || new Shader(gl, null, _fragment) 218 | mini.runFilter(mini._.$adj, {uColorMatrix, uColorOffset, uColorGamma:1/gamma, uClarityKernel, uClarityKernelWeight, uTextureSize, uVibrance, uColorVignette, uVignettePos}) 219 | } 220 | 221 | export function filterHighlightsShadows(mini,val1,val2){ 222 | //SHADOWS-HIGHLIGHTS - https://stackoverflow.com/questions/26511037/how-can-i-modify-this-webgl-fragment-shader-to-increase-brightness-of-highlights 223 | const _fragment = `#version 300 es 224 | precision highp float; 225 | 226 | in vec2 texCoord; 227 | uniform sampler2D _texture; 228 | out vec4 outColor; 229 | 230 | uniform float shadows; 231 | uniform float highlights; 232 | 233 | const mediump vec3 luminanceWeighting = vec3(0.2125, 0.7154, 0.0721); 234 | 235 | void main() { 236 | vec4 color = texture(_texture, texCoord); 237 | 238 | float luminance = dot(color.rgb, luminanceWeighting); 239 | float shadow = clamp((pow(luminance, 1.0/shadows) + (-0.76)*pow(luminance, 2.0/shadows)) - luminance, 0.0, 1.0); 240 | float highlight = clamp((1.0 - (pow(1.0-luminance, 1.0/(2.0-highlights)) + (-0.8)*pow(1.0-luminance, 2.0/(2.0-highlights)))) - luminance, -1.0, 0.0); 241 | vec3 result = vec3(0.0, 0.0, 0.0) + (luminance + shadow + highlight) * ((color.rgb - vec3(0.0, 0.0, 0.0))/luminance ); 242 | 243 | // blend toward white if highlights is more than 1 244 | float contrastedLuminance = ((luminance - 0.5) * 1.5) + 0.5; 245 | float whiteInterp = contrastedLuminance*contrastedLuminance*contrastedLuminance; 246 | float whiteTarget = clamp(highlights, 0.0, 2.0) - 1.0; 247 | result = mix(result, vec3(1.0), whiteInterp*whiteTarget); 248 | 249 | // blend toward black if shadows is less than 1 250 | float invContrastedLuminance = 1.0 - contrastedLuminance; 251 | float blackInterp = invContrastedLuminance*invContrastedLuminance*invContrastedLuminance; 252 | float blackTarget = 1.0 - clamp(shadows, 0.0, 1.0); 253 | result = mix(result, vec3(0.0), blackInterp*blackTarget); 254 | 255 | outColor = vec4(result, color.a); 256 | } 257 | ` 258 | const {gl}=mini 259 | //setup and run effect 260 | mini._.$sg = mini._.$sg || new Shader(gl, null, _fragment); 261 | mini.runFilter(mini._.$sg, { highlights:val1+1, shadows: val2/2+1 } ) 262 | } 263 | 264 | export function filterBloom(mini,val){ 265 | //BLOOM - https://www.shadertoy.com/view/Ms2Xz3 266 | const _fragment = `#version 300 es 267 | precision highp float; 268 | 269 | in vec2 texCoord; 270 | uniform sampler2D _texture; 271 | out vec4 outColor; 272 | 273 | uniform vec2 uResolution; 274 | uniform float filterStrength; 275 | 276 | 277 | vec4 BlurColor (in vec2 Coord, in sampler2D Tex, in float MipBias) 278 | { 279 | vec2 TexelSize = MipBias/uResolution.xy; 280 | vec4 Color = texture(Tex, Coord, MipBias); 281 | Color += texture(Tex, Coord + vec2(TexelSize.x,0.0), MipBias); 282 | Color += texture(Tex, Coord + vec2(-TexelSize.x,0.0), MipBias); 283 | Color += texture(Tex, Coord + vec2(0.0,TexelSize.y), MipBias); 284 | Color += texture(Tex, Coord + vec2(0.0,-TexelSize.y), MipBias); 285 | Color += texture(Tex, Coord + vec2(TexelSize.x,TexelSize.y), MipBias); 286 | Color += texture(Tex, Coord + vec2(-TexelSize.x,TexelSize.y), MipBias); 287 | Color += texture(Tex, Coord + vec2(TexelSize.x,-TexelSize.y), MipBias); 288 | Color += texture(Tex, Coord + vec2(-TexelSize.x,-TexelSize.y), MipBias); 289 | return Color/9.0; 290 | } 291 | 292 | void main() { 293 | float Threshold = 0.4; 294 | float Intensity = filterStrength*1.0; 295 | float BlurSize = 3.0 * Intensity; 296 | 297 | vec4 color = texture(_texture, texCoord); 298 | vec4 Highlight = clamp(BlurColor(texCoord.xy, _texture, BlurSize)-Threshold,0.0,1.0)*1.0/(1.0-Threshold); 299 | outColor = 1.0-(1.0-color)*(1.0-Highlight*Intensity); //Screen Blend Mode 300 | } 301 | ` 302 | const {gl}=mini 303 | const uResolution = [gl.canvas.width,gl.canvas.height]; //[width,height]; 304 | mini._.$bloom = mini._.$bloom || new Shader(gl, null, _fragment); 305 | mini.runFilter(mini._.$bloom, { filterStrength: val, uResolution }); 306 | } 307 | 308 | export function filterNoise(mini,val){ 309 | //BILATER FILTER https://www.shadertoy.com/view/4dfGDH 310 | const _fragment = `#version 300 es 311 | precision highp float; 312 | 313 | in vec2 texCoord; 314 | uniform sampler2D _texture; 315 | out vec4 outColor; 316 | 317 | uniform vec2 uResolution; 318 | uniform float filterStrength; 319 | 320 | #define SIGMA 10.0 321 | #define BSIGMA 0.1 322 | #define MSIZE 15 323 | 324 | float normpdf(in float x, in float sigma) 325 | { 326 | return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma; 327 | } 328 | 329 | float normpdf3(in vec3 v, in float sigma) 330 | { 331 | return 0.39894*exp(-0.5*dot(v,v)/(sigma*sigma))/sigma; 332 | } 333 | 334 | vec4 applyFilter(vec4 c, sampler2D _texture, vec2 texCoord) { 335 | 336 | const int kSize = (MSIZE-1)/2; 337 | float kernel[MSIZE] = float[MSIZE](0.031225216, 0.033322271, 0.035206333, 0.036826804, 0.038138565, 0.039104044, 0.039695028, 0.039894000, 0.039695028, 0.039104044, 0.038138565, 0.036826804, 0.035206333, 0.033322271, 0.031225216); 338 | vec3 final_colour = vec3(0.0); 339 | 340 | vec3 cc; 341 | float factor; 342 | float Z = 0.0; 343 | float bZ = 1.0/normpdf(0.0, BSIGMA); 344 | for (int i=-kSize; i <= kSize; ++i) 345 | { 346 | for (int j=-kSize; j <= kSize; ++j) 347 | { 348 | cc = texture(_texture, (texCoord.xy+vec2(float(i),float(j))/uResolution)).rgb; 349 | factor = normpdf3(cc-c.rgb, BSIGMA)*bZ*kernel[kSize+j]*kernel[kSize+i]; 350 | Z += factor; 351 | final_colour += factor*cc; 352 | } 353 | } 354 | 355 | return vec4(final_colour/Z, 1.0); 356 | } 357 | 358 | void main() { 359 | vec4 color = texture(_texture, texCoord); 360 | color = color * (1.0 - filterStrength) + applyFilter(color, _texture, texCoord) * filterStrength; 361 | outColor = color; 362 | } 363 | ` 364 | const {gl}=mini 365 | const uResolution = [gl.canvas.width,gl.canvas.height];// [width,height]; 366 | mini._.$noise = mini._.$noise || new Shader(gl, null, _fragment); 367 | mini.runFilter(mini._.$noise, { filterStrength: val, uResolution }); 368 | } 369 | 370 | 371 | 372 | ///////// UTILITY FUNCTIONS //////////////////// 373 | 374 | function multiplyM(A, B, N=3) { 375 | let C=[]; 376 | for (var i = 0; i < N; i++) 377 | { 378 | C.push([]) 379 | for (var j = 0; j < N; j++) 380 | { 381 | C[i].push(0) 382 | for (var k = 0; k < N; k++) 383 | { 384 | if(A[i]&&B[k]) C[i][j] += A[i][k]*B[k][j]; 385 | } 386 | } 387 | } 388 | return C 389 | } 390 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | import { dirname, resolve } from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)) 7 | 8 | export default defineConfig(({isSsrBuild, mode})=>{ 9 | 10 | return { 11 | build: { 12 | target: 'esnext', 13 | minify: true, //in production to reduce size 14 | sourcemap: false, //unless required during development to debug production code artifacts 15 | modulePreload: { polyfill: false }, //not needed for modern browsers 16 | cssCodeSplit:false, //if small enough it's better to have it in one file to avoid flickering during suspend 17 | copyPublicDir: isSsrBuild?false:true, 18 | lib: { 19 | entry: { 20 | 'mini-gl':resolve(__dirname, 'src/minigl.js'), 21 | }, 22 | name: 'mini-gl', 23 | } 24 | } 25 | } 26 | }) 27 | --------------------------------------------------------------------------------