UNPKG

kampos

Version:

Tiny and fast effects compositor on WebGL

1,627 lines (1,554 loc) 144 kB
'use strict'; /** * Exposes the `u_resolution` uniform for use inside fragment shaders. * * @function resolution * @param {Object} [params] * @param {number} [params.width] initial canvas width. Defaults to `window.innerWidth`. * @param {number} [params.height] initial canvas height. Defaults to `window.innerHeight`. * @returns {resolutionUtility} * * @example * resolution({width: 1600, height: 900}) */ function resolution({ width = window.innerWidth, height = window.innerHeight, } = {}) { /** * @typedef {Object} resolutionUtility * @property {{width: number?, height: number?}} resolution * * @example * mouse.resolution = {width: 854, height: 480}; */ return { fragment: { uniform: { u_resolution: 'vec2', }, }, get resolution() { const [x, y] = this.uniforms[0].data; return { x, y }; }, set resolution({ width: x, height: y }) { if (typeof x !== 'undefined') this.uniforms[0].data[0] = x; if (typeof y !== 'undefined') this.uniforms[0].data[1] = y; }, uniforms: [ { name: 'u_resolution', type: 'f', data: [width || window.innerWidth, height || window.innerHeight], }, ], }; } /** * Exposes the `u_mouse` uniform for use inside fragment shaders. * Note that internally the `y` coordinate is inverted to match the WebGL coordinate system. * * @function mouse * @param {Object} [params] * @param {{x: number?, y: number?}} [params.initial] initial mouse position. Defaults to `{x: 0, y: 0}`. * @returns {mouseUtility} * * @example mouse({initial: {x: 0.5, y: 0.5}}) */ function mouse({ initial = { x: 0, y: 0 }, } = {}) { /** * @typedef {Object} mouseUtility * @property {{x: number?, y: number?}} position * * @example * mouse.position = {x: 0.4, y: 0.2}; */ return { fragment: { uniform: { u_mouse: 'vec2', }, }, get position() { const [x, y] = this.uniforms[0].data; return { x, y: 1 - y }; }, set position({ x, y }) { if (typeof x !== 'undefined') this.uniforms[0].data[0] = x; if (typeof y !== 'undefined') this.uniforms[0].data[1] = 1 - y; }, uniforms: [ { name: 'u_mouse', type: 'f', data: [initial.x || 0, 1 - initial.y || 0], }, ], }; } /** * Exposes the `circle` function to be used by effects. * This function takes a point, radius, and spread, and returns a value between 0 and 1. * * @function circle * @returns {circleUtility} * * @example * circle() */ function circle() { /** * @typedef {Object} circleUtility * * @example * float aspectRatio = u_resolution.x / u_resolution.y; * vec2 st_ = gl_FragCoord.xy / u_resolution; * float circle_ = circle( * vec2(st_.x * aspectRatio, st_.y), * vec2(u_mouse.x * aspectRatio, u_mouse.y), * 0.35, * 0.1 * ); */ return { fragment: { constant: ` float circle(vec2 _point1, vec2 _point2, float _radius, float _spread){ vec2 dist = _point1 - _point2; return 1.0 - smoothstep(_radius - _spread, _radius + _spread, sqrt(dot(dist, dist)) / _radius); }` }, }; } /** * @function alphaMask * @param {Object} [params] * @param {boolean} [params.isLuminance=false] whether to use luminance when reading mask values * @returns {alphaMaskEffect} * * @example alphaMask() */ function alphaMask ({ isLuminance = false } = {}) { /** * @typedef {Object} alphaMaskEffect * @property {ArrayBufferView|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap} mask * @property {boolean} disabled * @property {boolean} isLuminance * * @description Multiplies `alpha` value with values read from `mask` media source. * * @example * const img = new Image(); * img.src = 'picture.png'; * effect.mask = img; * effect.disabled = true; */ return { vertex: { attribute: { a_alphaMaskTexCoord: 'vec2', }, main: ` v_alphaMaskTexCoord = a_alphaMaskTexCoord;`, }, fragment: { uniform: { u_alphaMaskEnabled: 'bool', u_alphaMaskIsLuminance: 'bool', u_mask: 'sampler2D', }, main: ` if (u_alphaMaskEnabled) { vec4 alphaMaskPixel = texture2D(u_mask, v_alphaMaskTexCoord); if (u_alphaMaskIsLuminance) { alpha *= dot(lumcoeff, alphaMaskPixel.rgb) * alphaMaskPixel.a; } else { alpha *= alphaMaskPixel.a; } }`, }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, get mask() { return this.textures[0].data; }, set mask(img) { this.textures[0].data = img; }, get isLuminance() { return !!this.uniforms[2].data[0]; }, set isLuminance(toggle) { this.uniforms[2].data[0] = +toggle; this.textures[0].format = toggle ? 'RGBA' : 'ALPHA'; }, varying: { v_alphaMaskTexCoord: 'vec2', }, uniforms: [ { name: 'u_alphaMaskEnabled', type: 'i', data: [1], }, { name: 'u_mask', type: 'i', data: [1], }, { name: 'u_alphaMaskIsLuminance', type: 'i', data: [+!!isLuminance], }, ], attributes: [ { name: 'a_alphaMaskTexCoord', extends: 'a_texCoord', }, ], textures: [ { format: isLuminance ? 'RGBA' : 'ALPHA', }, ], }; } /** * Depends on the `resolution` utility. * Depends on the `mouse` utility. * * @function deformation * @param {Object} [params] * @param {number} [params.radius] initial radius to use for circle of effect boundaries. Defaults to 0 which means no effect. * @param {string} [params.wrap] wrapping method to use. Defaults to `deformation.CLAMP`. * @param {string} [params.deformation] deformation method to use within the radius. Defaults to `deformation.NONE`. * @returns {deformationEffect} * * @example deformation({radius: 0.1, wrap: deformation.CLAMP, deformation: deformation.TUNNEL}) */ function deformation({ radius, wrap = WRAP_METHODS$1.WRAP, deformation = DEFORMATION_METHODS.NONE, } = {}) { const dataRadius = radius || 0; /** * @typedef {Object} deformationEffect * @property {boolean} disabled * @property {number} radius * * @example * effect.disabled = true; * effect.radius = 0.253; */ return { fragment: { uniform: { u_deformationEnabled: 'bool', u_radius: 'float', }, source: ` float _aspectRatio = u_resolution.x / u_resolution.y; vec2 _position = u_mouse; vec2 diff = sourceCoord - _position; float dist = diff.x * diff.x * _aspectRatio * _aspectRatio + diff.y * diff.y; float r = sqrt(dist); bool isInsideDeformation = dist < u_radius * u_radius; if (u_deformationEnabled) { if (isInsideDeformation) { vec2 dispVec = diff; float a = atan(diff.y, diff.x); ${deformation} dispVec = dispVec + _position; ${wrap} sourceCoord = dispVec; } }`, // main: ` // if (isInsideDeformation) { // color = mix(color, texture2D(u_source, v_texCoord).rgb, vec3(pow(r / u_radius, 4.0))); // }`, }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, get radius() { return this.uniforms[1].data[0]; }, set radius(r) { if (typeof r !== 'undefined') this.uniforms[1].data[0] = r; }, uniforms: [ { name: 'u_deformationEnabled', type: 'i', data: [1], }, { name: 'u_radius', type: 'f', data: [dataRadius], }, ], }; } const WRAP_METHODS$1 = { CLAMP: `dispVec = clamp(dispVec, 0.0, 1.0);`, DISCARD: `if (dispVec.x < 0.0 || dispVec.x > 1.0 || dispVec.y > 1.0 || dispVec.y < 0.0) { discard; }`, WRAP: `dispVec = mod(dispVec, 1.0);`, }; deformation.CLAMP = WRAP_METHODS$1.CLAMP; deformation.DISCARD = WRAP_METHODS$1.DISCARD; deformation.WRAP = WRAP_METHODS$1.WRAP; const DEFORMATION_METHODS = { NONE: ``, TUNNEL: `dispVec = vec2(dispVec.x * cos(r + r) - dispVec.y * sin(r + r), dispVec.y * cos(r + r) + dispVec.x * sin(r + r));`, SOMETHING: `dispVec = vec2(0.3 / (10.0 * r + dispVec.x), 0.5 * a / PI);`, SOMETHING2: `dispVec = vec2(0.02 * dispVec.y + 0.03 * cos(a) / r, 0.02 * dispVec.x + 0.03 * sin(a) / r);`, INVERT: `dispVec = dispVec * -1.0;`, SCALE: `dispVec = dispVec * 0.75;`, MAGNIFY: `dispVec = dispVec * (pow(2.0, r / u_radius) - 1.0);`, UNMAGNIFY: `dispVec = dispVec * (pow(2.0, min(u_radius / r, 4.0)));`, }; deformation.NONE = DEFORMATION_METHODS.NONE; deformation.TUNNEL = DEFORMATION_METHODS.TUNNEL; deformation.SOMETHING = DEFORMATION_METHODS.SOMETHING; deformation.SOMETHING2 = DEFORMATION_METHODS.SOMETHING2; deformation.INVERT = DEFORMATION_METHODS.INVERT; deformation.SCALE = DEFORMATION_METHODS.SCALE; deformation.MAGNIFY = DEFORMATION_METHODS.MAGNIFY; deformation.UNMAGNIFY = DEFORMATION_METHODS.UNMAGNIFY; const MODES_AUX = { blend_luminosity: `float blend_luminosity (vec3 c) { return dot(c, blendLum); }`, blend_saturation: `float blend_saturation (vec3 c) { return max(max(c.r, c.g), c.b) - min(min(c.r, c.g), c.b); }`, blend_set_luminosity: `vec3 blend_clip_color (vec3 c) { float l = blend_luminosity(c); float cMin = min(min(c.r, c.g), c.b); float cMax = max(max(c.r, c.g), c.b); if (cMin < 0.0) return l + (((c - l) * l) / (l - cMin)); if (cMax > 1.0) return l + (((c - l) * (1.0 - l)) / (cMax - l)); return c; } vec3 blend_set_luminosity (vec3 c, float l) { vec3 delta = vec3(l - blend_luminosity(c)); return blend_clip_color(vec3(c.rgb + delta.rgb)); }`, blend_set_saturation: ` float getBlendMid (vec3 c) { float bigger = max(c.r, c.g); if (bigger < c.b) { return bigger; } float smaller = min(c.r, c.g); if (c.b < smaller) { return smaller; } return c.b; } vec3 blend_set_saturation (vec3 c, float s) { if (s == 0.0) return vec3(0.0); float cMax = max(max(c.r, c.g), c.b); float cMid = getBlendMid(c); float cMin = min(min(c.r, c.g), c.b); float r, g, b; cMid = (((cMid - cMin) * s) / (cMax - cMin)); cMax = s; cMin = 0.0; if (c.r > c.g) { // r > g if (c.b > c.r) { // g < r < b g = cMin; r = cMid; b = cMax; } else if (c.g > c.b) { // b < g < r b = cMin; g = cMid; r = cMax; } else { // g < b < r g = cMin; b = cMid; r = cMax; } } // g > r else if (c.g > c.b) { // g > b if (c.b > c.r) { // r < b < g r = cMin; b = cMid; g = cMax; } else { // b < r < g b = cMin; r = cMid; g = cMax; } } else { // r < g < b r = cMin; g = cMid; b = cMax; } return vec3(r, g, b); }`, }; const MODES_CONSTANT = { normal: '', multiply: '', screen: '', overlay: `float blend_overlay (float b, float c) { if (b <= 0.5) return 2.0 * b * c; else return 1.0 - 2.0 * ((1.0 - b) * (1.0 - c)); }`, darken: '', lighten: '', colorDodge: `float blend_colorDodge (float b, float c) { if (b == 0.0) return 0.0; else if (c == 1.0) return 1.0; else return min(1.0, b / (1.0 - c)); }`, colorBurn: `float blend_colorBurn (float b, float c) { if (b == 1.0) { return 1.0; } else if (c == 0.0) { return 0.0; } else { return 1.0 - min(1.0, (1.0 - b) / c); } }`, hardLight: `float blend_hardLight (float b, float c) { if (c <= 0.5) { return 2.0 * b * c; } else { return 1.0 - 2.0 * ((1.0 - b) * (1.0 - c)); } }`, softLight: `float blend_softLight (float b, float c) { if (c <= 0.5) { return b - (1.0 - 2.0 * c) * b * (1.0 - b); } else { float d; if (b <= 0.25) { d = ((16.0 * b - 12.0) * b + 4.0) * b; } else { d = sqrt(b); } return b + (2.0 * c - 1.0) * (d - b); } }`, difference: `float blend_difference (float b, float c) { return abs(b - c); }`, exclusion: `float blend_exclusion (float b, float c) { return b + c - 2.0 * b * c; }`, hue: `${MODES_AUX.blend_luminosity} ${MODES_AUX.blend_saturation} ${MODES_AUX.blend_set_saturation} ${MODES_AUX.blend_set_luminosity}`, saturation: `${MODES_AUX.blend_luminosity} ${MODES_AUX.blend_saturation} ${MODES_AUX.blend_set_saturation} ${MODES_AUX.blend_set_luminosity}`, color: `${MODES_AUX.blend_luminosity} ${MODES_AUX.blend_set_luminosity}`, luminosity: `${MODES_AUX.blend_luminosity} ${MODES_AUX.blend_set_luminosity}`, }; function generateBlendVector(name) { return `vec3(${name}(backdrop.r, source.r), ${name}(backdrop.g, source.g), ${name}(backdrop.b, source.b))`; } const MODES_MAIN = { normal: 'source', multiply: 'source * backdrop', screen: 'backdrop + source - backdrop * source', overlay: generateBlendVector('blend_overlay'), darken: generateBlendVector('min'), lighten: generateBlendVector('max'), colorDodge: generateBlendVector('blend_colorDodge'), colorBurn: generateBlendVector('blend_colorBurn'), hardLight: generateBlendVector('blend_hardLight'), softLight: generateBlendVector('blend_softLight'), difference: generateBlendVector('blend_difference'), exclusion: generateBlendVector('blend_exclusion'), hue: 'blend_set_luminosity(blend_set_saturation(source, blend_saturation(backdrop)), blend_luminosity(backdrop))', saturation: 'blend_set_luminosity(blend_set_saturation(backdrop, blend_saturation(source)), blend_luminosity(backdrop))', color: 'blend_set_luminosity(source, blend_luminosity(backdrop))', luminosity: 'blend_set_luminosity(backdrop, blend_luminosity(source))', }; /** * @function blend * @param {Object} [params] * @param {'normal'|'multiply'|'screen'|'overlay'|'darken'|'lighten'|'color-dodge'|'color-burn'|'hard-light'|'soft-light'|'difference'|'exclusion'|'hue'|'saturation'|'color'|'luminosity'} [params.mode='normal'] blend mode to use * @param {number[]} [params.color=[0, 0, 0, 1]] Initial color to use when blending to a solid color * @returns {blendEffect} * @example blend('colorBurn') */ function blend ({ mode = 'normal', color = [0.0, 0.0, 0.0, 1.0], } = {}) { /** * @typedef {Object} blendEffect * @property {number[]} color backdrop solid color as Array of 4 numbers, normalized (0.0 - 1.0) * @property {ArrayBufferView|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap} image to use as backdrop * @property {boolean} disabled * * @example * const img = new Image(); * img.src = 'picture.png'; * effect.color = [0.3, 0.55, 0.8, 1.0]; * effect.image = img; */ return { vertex: { attribute: { a_blendImageTexCoord: 'vec2', }, main: ` v_blendImageTexCoord = a_blendImageTexCoord;`, }, fragment: { uniform: { u_blendEnabled: 'bool', u_blendColorEnabled: 'bool', u_blendImageEnabled: 'bool', u_blendColor: 'vec4', u_blendImage: 'sampler2D', }, constant: `const vec3 blendLum = vec3(0.3, 0.59, 0.11); ${MODES_CONSTANT[mode]}`, main: ` if (u_blendEnabled) { vec3 backdrop = vec3(0.0); float backdropAlpha = 1.0; if (u_blendColorEnabled) { backdrop = u_blendColor.rgb; backdropAlpha = u_blendColor.a; } if (u_blendImageEnabled) { vec4 blendBackdropPixel = texture2D(u_blendImage, v_blendImageTexCoord); if (u_blendColorEnabled) { vec3 source = blendBackdropPixel.rgb; float sourceAlpha = blendBackdropPixel.a; backdrop = (1.0 - backdropAlpha) * source + backdropAlpha * clamp(${MODES_MAIN[mode]}, 0.0, 1.0); backdropAlpha = sourceAlpha + backdropAlpha * (1.0 - sourceAlpha); } else { backdrop = blendBackdropPixel.rgb; backdropAlpha = blendBackdropPixel.a; } } vec3 source = vec3(color.rgb); color = (1.0 - backdropAlpha) * source + backdropAlpha * clamp(${MODES_MAIN[mode]}, 0.0, 1.0); alpha = alpha + backdropAlpha * (1.0 - alpha); }`, }, get color() { return this.uniforms[1].data.slice(0); }, set color(l) { if (!l || !l.length) { this.uniforms[2].data[0] = 0; } else { this.uniforms[2].data[0] = 1; l.forEach((c, i) => { if (!Number.isNaN(c)) { this.uniforms[1].data[i] = c; } }); } }, get image() { return this.textures[0].data; }, set image(img) { if (img) { this.uniforms[4].data[0] = 1; this.textures[0].data = img; } else { this.uniforms[4].data[0] = 0; } }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, varying: { v_blendImageTexCoord: 'vec2', }, uniforms: [ { name: 'u_blendEnabled', type: 'i', data: [1], }, { name: 'u_blendColor', type: 'f', data: color, }, { name: 'u_blendColorEnabled', type: 'i', data: [1], }, { name: 'u_blendImage', type: 'i', data: [1], }, { name: 'u_blendImageEnabled', type: 'i', data: [0], }, ], attributes: [ { name: 'a_blendImageTexCoord', extends: 'a_texCoord', }, ], textures: [ { format: 'RGBA', }, ], }; } /** * @function brightnessContrast * @property {number} brightness * @property {number} contrast * @param {Object} [params] * @param {number} [params.brightness=1.0] initial brightness to use. * @param {number} [params.contrast=1.0] initial contrast to use. * @returns {brightnessContrastEffect} * * @example brightnessContrast({brightness: 1.5, contrast: 0.8}) */ function brightnessContrast ({ brightness = 1.0, contrast = 1.0 } = {}) { /** * @typedef {Object} brightnessContrastEffect * @property {number} brightness * @property {number} contrast * @property {boolean} brightnessDisabled * @property {boolean} contrastDisabled * * @example * effect.brightness = 1.5; * effect.contrast = 0.9; * effect.contrastDisabled = true; */ return { fragment: { uniform: { u_brEnabled: 'bool', u_ctEnabled: 'bool', u_contrast: 'float', u_brightness: 'float', }, constant: 'const vec3 half3 = vec3(0.5);', main: ` if (u_brEnabled) { color *= u_brightness; } if (u_ctEnabled) { color = (color - half3) * u_contrast + half3; } color = clamp(color, 0.0, 1.0);`, }, get brightness() { return this.uniforms[2].data[0]; }, set brightness(value) { this.uniforms[2].data[0] = parseFloat(Math.max(0, value)); }, get contrast() { return this.uniforms[3].data[0]; }, set contrast(value) { this.uniforms[3].data[0] = parseFloat(Math.max(0, value)); }, get brightnessDisabled() { return !this.uniforms[0].data[0]; }, set brightnessDisabled(toggle) { this.uniforms[0].data[0] = +!toggle; }, get contrastDisabled() { return !this.uniforms[1].data[0]; }, set contrastDisabled(toggle) { this.uniforms[1].data[0] = +!toggle; }, uniforms: [ { name: 'u_brEnabled', type: 'i', data: [1], }, { name: 'u_ctEnabled', type: 'i', data: [1], }, { name: 'u_brightness', type: 'f', data: [brightness], }, { name: 'u_contrast', type: 'f', data: [contrast], }, ], }; } /** * @function hueSaturation * @property {number} hue rotation in degrees * @property {number} saturation * @param {Object} [params] * @param {number} [params.hue=0.0] initial hue value * @param {number} [params.saturation=1.0] initial saturation value * @returns {hueSaturationEffect} * @example hueSaturation({hue: 45, saturation: 1.3}) */ function hueSaturation ({ hue = 0.0, saturation = 1.0 } = {}) { /** * @typedef {Object} hueSaturationEffect * @property {number} hue * @property {number} saturation * @property {boolean} hueDisabled * @property {boolean} saturationDisabled * * @example * effect.hue = 45; * effect.saturation = 0.8; */ return { vertex: { uniform: { u_hue: 'float', u_saturation: 'float', }, // for implementation see: https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement constant: ` const mat3 lummat = mat3( lumcoeff, lumcoeff, lumcoeff ); const mat3 cosmat = mat3( vec3(0.787, -0.715, -0.072), vec3(-0.213, 0.285, -0.072), vec3(-0.213, -0.715, 0.928) ); const mat3 sinmat = mat3( vec3(-0.213, -0.715, 0.928), vec3(0.143, 0.140, -0.283), vec3(-0.787, 0.715, 0.072) ); const mat3 satmat = mat3( vec3(0.787, -0.715, -0.072), vec3(-0.213, 0.285, -0.072), vec3(-0.213, -0.715, 0.928) );`, main: ` float angle = (u_hue / 180.0) * 3.14159265358979323846264; v_hueRotation = lummat + cos(angle) * cosmat + sin(angle) * sinmat; v_saturation = lummat + satmat * u_saturation;`, }, fragment: { uniform: { u_hueEnabled: 'bool', u_satEnabled: 'bool', u_hue: 'float', u_saturation: 'float', }, main: ` if (u_hueEnabled) { color = vec3( dot(color, v_hueRotation[0]), dot(color, v_hueRotation[1]), dot(color, v_hueRotation[2]) ); } if (u_satEnabled) { color = vec3( dot(color, v_saturation[0]), dot(color, v_saturation[1]), dot(color, v_saturation[2]) ); } color = clamp(color, 0.0, 1.0);`, }, varying: { v_hueRotation: 'mat3', v_saturation: 'mat3', }, get hue() { return this.uniforms[2].data[0]; }, set hue(h) { this.uniforms[2].data[0] = parseFloat(h); }, get saturation() { return this.uniforms[3].data[0]; }, set saturation(s) { this.uniforms[3].data[0] = parseFloat(Math.max(0, s)); }, get hueDisabled() { return !this.uniforms[0].data[0]; }, set hueDisabled(b) { this.uniforms[0].data[0] = +!b; }, get saturationDisabled() { return !this.uniforms[1].data[0]; }, set saturationDisabled(b) { this.uniforms[1].data[0] = +!b; }, uniforms: [ { name: 'u_hueEnabled', type: 'i', data: [1], }, { name: 'u_satEnabled', type: 'i', data: [1], }, { name: 'u_hue', type: 'f', data: [hue], }, { name: 'u_saturation', type: 'f', data: [saturation], }, ], }; } /** * @function duotone * @param {Object} [params] * @param {number[]} [params.dark=[0.741, 0.0431, 0.568, 1]] initial dark color to use. * @param {number[]} [params.light=[0.988, 0.733, 0.051, 1]] initial light color to use. * @returns {duotoneEffect} * * @example duotone({dark: [0.2, 0.11, 0.33, 1], light: [0.88, 0.78, 0.43, 1]}) */ function duotone ({ dark = [0.7411764706, 0.0431372549, 0.568627451, 1], light = [0.9882352941, 0.7333333333, 0.05098039216, 1], } = {}) { /** * @typedef {Object} duotoneEffect * @property {number[]} light Array of 4 numbers, normalized (0.0 - 1.0) * @property {number[]} dark Array of 4 numbers, normalized (0.0 - 1.0) * @property {boolean} disabled * * @example * effect.light = [1.0, 1.0, 0.8]; * effect.dark = [0.2, 0.6, 0.33]; */ return { fragment: { uniform: { u_duotoneEnabled: 'bool', u_light: 'vec4', u_dark: 'vec4', }, main: ` if (u_duotoneEnabled) { vec3 gray = vec3(dot(lumcoeff, color)); color = mix(u_dark.rgb, u_light.rgb, gray); }`, }, get light() { return this.uniforms[1].data.slice(0); }, set light(l) { l.forEach((c, i) => { if (!Number.isNaN(c)) { this.uniforms[1].data[i] = c; } }); }, get dark() { return this.uniforms[2].data.slice(0); }, set dark(d) { d.forEach((c, i) => { if (!Number.isNaN(c)) { this.uniforms[2].data[i] = c; } }); }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, uniforms: [ { name: 'u_duotoneEnabled', type: 'i', data: [1], }, { name: 'u_light', type: 'f', data: light, }, { name: 'u_dark', type: 'f', data: dark, }, ], }; } /** * @function displacement * @property {string} TEXTURE use texture sampling as input method. * @property {string} CLAMP stretch the last value to the edge. This is the default behavior. * @property {string} DISCARD discard values beyond the edge of the media - leaving a transparent pixel. * @property {string} WRAP continue rendering values from opposite direction when reaching the edge. * @param {Object} [params] * @param {string} [params.wrap] wrapping method to use. Defaults to `displacement.CLAMP`. * @param {string} [params.input] input method to use. Defaults to `displacement.TEXTURE`. * @param {{x: number, y: number}} [params.scale] initial scale to use for x and y displacement. Defaults to `{x: 0.0, y: 0.0}` which means no displacement. * @param {boolean} [params.enableBlueChannel] enable blue channel for displacement intensity. Defaults to `false`. * @returns {displacementEffect} * * @example displacement({wrap: displacement.DISCARD, scale: {x: 0.5, y: -0.5}}) */ function displacement({ wrap = WRAP_METHODS.CLAMP, input = INPUT_METHODS.TEXTURE, scale, enableBlueChannel } = {}) { const { x: sx, y: sy } = scale || { x: 0.0, y: 0.0 }; /** * @typedef {Object} displacementEffect * @property {ArrayBufferView|ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|ImageBitmap} map * @property {{x: number?, y: number?}} scale * @property {boolean} disabled * @property {boolean} enableBlueChannel * * @example * const img = new Image(); * img.src = 'disp.jpg'; * effect.map = img; * effect.scale = {x: 0.4}; */ return { vertex: { attribute: { a_displacementMapTexCoord: 'vec2', }, main: ` v_displacementMapTexCoord = a_displacementMapTexCoord;`, }, fragment: { uniform: { u_displacementEnabled: 'bool', u_enableBlueChannel: 'bool', u_dispMap: 'sampler2D', u_dispScale: 'vec2', }, source: ` if (u_displacementEnabled) { ${input} vec2 dispVec = vec2(sourceCoord.x + (u_dispScale.x + dispIntensity) * dispPosition.r, sourceCoord.y + (u_dispScale.y + dispIntensity) * dispPosition.g); ${wrap} sourceCoord = dispVec; }`, }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, get scale() { const [x, y] = this.uniforms[2].data; return { x, y }; }, set scale({ x, y }) { if (typeof x !== 'undefined') this.uniforms[2].data[0] = x; if (typeof y !== 'undefined') this.uniforms[2].data[1] = y; }, get map() { return this.textures[0].data; }, set map(img) { this.textures[0].data = img; }, get enableBlueChannel() { return this.uniforms[3].data[0]; }, set enableBlueChannel(b) { this.uniforms[3].data[0] = +b; }, varying: { v_displacementMapTexCoord: 'vec2', }, uniforms: [ { name: 'u_displacementEnabled', type: 'i', data: [1], }, { name: 'u_dispMap', type: 'i', data: [1], }, { name: 'u_dispScale', type: 'f', data: [sx, sy], }, { name: 'u_enableBlueChannel', type: 'i', data: [+enableBlueChannel], }, ], attributes: [ { name: 'a_displacementMapTexCoord', extends: 'a_texCoord', }, ], textures: [ { format: 'RGB', }, ], }; } const INPUT_METHODS = { TEXTURE: `vec3 dispMap = texture2D(u_dispMap, v_displacementMapTexCoord).rgb; vec2 dispPosition = dispMap.rg - 0.5; float dispIntensity = u_enableBlueChannel ? dispMap.b : 0.0;`, TURBULENCE: `vec3 dispMap = vec3(turbulenceValue); vec2 dispPosition = dispMap.rg - 0.5; float dispIntensity = u_enableBlueChannel ? dispMap.b : 0.0;`, }; const WRAP_METHODS = { CLAMP: `dispVec = clamp(dispVec, 0.0, 1.0);`, DISCARD: `if (dispVec.x < 0.0 || dispVec.x > 1.0 || dispVec.y > 1.0 || dispVec.y < 0.0) { discard; }`, WRAP: `dispVec = mod(dispVec, 1.0);`, }; displacement.TEXTURE = INPUT_METHODS.TEXTURE; displacement.TURBULENCE = INPUT_METHODS.TURBULENCE; displacement.CLAMP = WRAP_METHODS.CLAMP; displacement.DISCARD = WRAP_METHODS.DISCARD; displacement.WRAP = WRAP_METHODS.WRAP; /** * @function channelSplit * @param {Object} [params] * @param {{x: number?, y: number?}} [params.offsetRed] initial offset to use for red channel offset. Defaults to `{x: 0.0, y: 0.0}` which means no offset. * @param {{x: number?, y: number?}} [params.offsetGreen] initial offset to use for green channel offset. Defaults to `{x: 0.0, y: 0.0}` which means no offset. * @param {{x: number?, y: number?}} [params.offsetBlue] initial offset to use for blue channel offset. Defaults to `{x: 0.0, y: 0.0}` which means no offset. * @param {string} [params.offsetInputR] code to use as input for the red offset. Defaults to `u_channelOffsetR`. * @param {string} [params.offsetInputG] code to use as input for the green offset. Defaults to `u_channelOffsetG`. * @param {string} [params.offsetInputB] code to use as input for the blue offset. Defaults to `u_channelOffsetB`. * @param {function} [params.boundsOffsetFactor] function that takes name of variable for channel offset and returns a float value as string. Defaults to returning `'1.0`'. * @returns {channelSplitEffect} * * @example channelSplit({offsetRed: {x: 0.02, y: 0.0}}) */ function channelSplit({ offsetRed = { x: 0.01, y: 0.01 }, offsetGreen = { x: -0.01, y: -0.01 }, offsetBlue = { x: -0.01, y: -0.01 }, offsetInputR = 'u_channelOffsetR', offsetInputG = 'u_channelOffsetG', offsetInputB = 'u_channelOffsetB', boundsOffsetFactor = (boundsOffset) => '1.0', } = {}) { /** * @typedef {Object} channelSplitEffect * @property {boolean} disabled * @property {{x: number?, y: number?}} offsetRed * @property {{x: number?, y: number?}} offsetGreen * @property {{x: number?, y: number?}} offsetBlue * * @example * effect.offsetRed = { x: 0.1, y: 0.0 }; */ return { fragment: { uniform: { u_channelSplitEnabled: 'bool', u_channelOffsetR: 'vec2', u_channelOffsetG: 'vec2', u_channelOffsetB: 'vec2', }, main: ` if (u_channelSplitEnabled) { vec2 _splitOffsetR = ${offsetInputR}; vec2 _splitOffsetG = ${offsetInputG}; vec2 _splitOffsetB = ${offsetInputB}; vec2 redSample = sourceCoord + _splitOffsetR; vec2 greenSample = sourceCoord + _splitOffsetG; vec2 blueSample = sourceCoord + _splitOffsetB; float redBoundsOffset = min(0.0, min(min(redSample.x, redSample.y), min(1.0 - redSample.x, 1.0 - redSample.y))); float greenBoundsOffset = min(0.0, min(min(greenSample.x, greenSample.y), min(1.0 - greenSample.x, 1.0 - greenSample.y))); float blueBoundsOffset = min(0.0, min(min(blueSample.x, blueSample.y), min(1.0 - blueSample.x, 1.0 - blueSample.y))); float redSplit = texture2D(u_source, sourceCoord + _splitOffsetR).r * ${boundsOffsetFactor( 'redBoundsOffset' )}; float greenSplit = texture2D(u_source, sourceCoord + _splitOffsetG).g * ${boundsOffsetFactor( 'greenBoundsOffset' )}; float blueSplit = texture2D(u_source, sourceCoord + _splitOffsetB).b * ${boundsOffsetFactor( 'blueBoundsOffset' )}; color = vec3(redSplit, greenSplit, blueSplit); }`, }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, get red() { const [x, y] = this.uniforms[1].data; return { x, y }; }, set red({ x, y }) { if (typeof x !== 'undefined') this.uniforms[1].data[0] = x; if (typeof y !== 'undefined') this.uniforms[1].data[1] = y; }, get green() { const [x, y] = this.uniforms[2].data; return { x, y }; }, set green({ x, y }) { if (typeof x !== 'undefined') this.uniforms[2].data[0] = x; if (typeof y !== 'undefined') this.uniforms[2].data[1] = y; }, get blue() { const [x, y] = this.uniforms[3].data; return { x, y }; }, set blue({ x, y }) { if (typeof x !== 'undefined') this.uniforms[3].data[0] = x; if (typeof y !== 'undefined') this.uniforms[3].data[1] = y; }, uniforms: [ { name: 'u_channelSplitEnabled', type: 'i', data: [1], }, { name: 'u_channelOffsetR', type: 'f', data: [offsetRed.x, offsetRed.y], }, { name: 'u_channelOffsetG', type: 'f', data: [offsetGreen.x, offsetGreen.y], }, { name: 'u_channelOffsetB', type: 'f', data: [offsetBlue.x, offsetBlue.y], }, ], }; } /** * @function kaleidoscope * @param {Object} [params] * @param {number} [params.segments=6] number of times the view is divided. * @param {number} [params.offset={x: 0.0, y: 0.0}] offset to move the source media from the center. * @param {number} [params.rotation=0] extra angle to rotate the view. * @returns {kaleidoscopeEffect} * * @example kaleidoscope({segments: 12}) */ function kaleidoscope ({ segments = 6, offset, rotation = 0 } = {}) { const { x: offsetX, y: offsetY } = offset || { x: 0.0, y: 0.0 }; /** * @typedef {Object} kaleidoscopeEffect * @property {number} segments * @property {{x: number?, y: number?}} offset * @property {number} rotation * @property {boolean} disabled * * @example * effect.segments = 8; * effect.offset = { x: 0.5, y: 0.5 }; * effect.rotation = 45; */ return { fragment: { uniform: { u_kaleidoscopeEnabled: 'bool', u_segments: 'float', u_offset: 'vec2', u_rotation: 'float', }, source: ` if (u_kaleidoscopeEnabled && u_segments > 0.0) { vec2 centered = v_texCoord - 0.5; float r = length(centered); float theta = atan(centered.y, centered.x); theta = mod(theta, 2.0 * PI / u_segments) + radians(u_rotation); theta = abs(theta - PI / u_segments) - PI / u_segments; vec2 newCoords = r * vec2(cos(theta), sin(theta)) + 0.5; sourceCoord = newCoords - u_offset; // mirrored repeat sourceCoord = mod(sourceCoord, 1.0) * (mod(sourceCoord - 1.0, 2.0) - mod(sourceCoord, 1.0)) + mod(-sourceCoord, 1.0) * (mod(sourceCoord, 2.0) - mod(sourceCoord, 1.0)); }`, }, get segments() { return this.uniforms[1].data[0]; }, set segments(n) { this.uniforms[1].data[0] = +n; }, get offset() { const [x, y] = this.uniforms[2].data; return { x, y }; }, set offset({ x, y }) { if (typeof x !== 'undefined') this.uniforms[2].data[0] = x; if (typeof y !== 'undefined') this.uniforms[2].data[1] = y; }, get rotation() { return this.uniforms[3].data[0]; }, set rotation(r) { this.uniforms[3].data[0] = r; }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, uniforms: [ { name: 'u_kaleidoscopeEnabled', type: 'i', data: [1], }, { name: 'u_segments', type: 'f', data: [segments], }, { name: 'u_offset', type: 'f', data: [offsetX, offsetY], }, { name: 'u_rotation', type: 'f', data: [rotation], }, ], }; } /** * @function slitScan * @requires resolution * @param {Object} params * @param {noise} params.noise 2D noise implementation to use. * @param {number} [params.time=0.0] initial time for controlling initial noise value. * @param {number} [params.intensity=0.1] initial intensity to use. * @param {number} [params.frequency] initial frequency to use . * @param {string} [params.direction='x'] direction to apply the slit scan effect. * @param {string} [params.offsetInput] code to use as input for adding offset. Defaults to empty. * @returns {slitScanEffect} * * @example slitScan({intensity: 0.5, frequency: 3.0}) */ function slitScan ({ noise, time = 0.0, intensity = 0.1, frequency = 2.0, direction = 'x', offsetInput = '', }) { /** * @typedef {Object} slitScanEffect * @property {boolean} disabled * @property {number} intensity * @property {number} frequency * @property {number} time * * @example * effect.intensity = 0.5; * effect.frequency = 3.5; */ const isHorizontal = direction === 'x'; const noiseFragPart = `(gl_FragCoord.${direction} / u_resolution.${direction}${offsetInput ? `+ ${offsetInput}` : ''}) * u_frequency`; const noiseTimePart = 'u_time * 0.0001'; return { fragment: { uniform: { u_slitScanEnabled: 'bool', u_intensity: 'float', u_frequency: 'float', u_time: 'float', u_horizontal: 'bool' }, constant: noise, source: ` if (u_slitScanEnabled) { float noiseValue = noise(vec2(${isHorizontal ? noiseFragPart : noiseTimePart}, ${isHorizontal ? noiseTimePart : noiseFragPart})); float source_ = sourceCoord.${direction} + noiseValue * u_intensity; float mirrored_ = mod(source_, 1.0) * (mod(source_ - 1.0, 2.0) - mod(source_, 1.0)) + mod(-source_, 1.0) * (mod(source_, 2.0) - mod(source_, 1.0)); sourceCoord = ${isHorizontal ? 'vec2(mirrored_, sourceCoord.y)' : 'vec2(sourceCoord.x, mirrored_)'}; }`, }, get disabled() { return !this.uniforms[0].data[0]; }, set disabled(b) { this.uniforms[0].data[0] = +!b; }, get intensity() { return this.uniforms[1].data[0]; }, set intensity(i) { this.uniforms[1].data[0] = i; }, get frequency() { return this.uniforms[2].data[0]; }, set frequency(f) { this.uniforms[2].data[0] = f; }, get time() { return this.uniforms[3].data[0]; }, set time(t) { this.uniforms[3].data[0] = t; }, uniforms: [ { name: 'u_slitScanEnabled', type: 'i', data: [1], }, { name: 'u_intensity', type: 'f', data: [intensity], }, { name: 'u_frequency', type: 'f', data: [frequency], }, { name: 'u_time', type: 'f', data: [time], }, ], }; } /*! * GLSL textureless classic 3D noise "cnoise", * with an RSL-style periodic variant "pnoise". * Author: Stefan Gustavson (stefan.gustavson@liu.se) * Version: 2011-10-11 * * Many thanks to Ian McEwan of Ashima Arts for the * ideas for permutation and gradient selection. * * Copyright (c) 2011 Stefan Gustavson. All rights reserved. * Distributed under the MIT license. See LICENSE file. * https://github.com/ashima/webgl-noise */ /** * Implementation of a 3D classic Perlin noise. Exposes a `noise(vec3 P)` function for use inside fragment shaders. */ var perlinNoise = ` vec3 mod289 (vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 mod289 (vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 permute (vec4 x) { return mod289(((x*34.0)+1.0)*x); } vec4 taylorInvSqrt (vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } vec3 fade (vec3 t) { return t*t*t*(t*(t*6.0-15.0)+10.0); } // Classic Perlin noise float noise (vec3 P) { vec3 Pi0 = floor(P); // Integer part for indexing vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 Pi0 = mod289(Pi0); Pi1 = mod289(Pi1); vec3 Pf0 = fract(P); // Fractional part for interpolation vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); vec4 iy = vec4(Pi0.yy, Pi1.yy); vec4 iz0 = Pi0.zzzz; vec4 iz1 = Pi1.zzzz; vec4 ixy = permute(permute(ix) + iy); vec4 ixy0 = permute(ixy + iz0); vec4 ixy1 = permute(ixy + iz1); vec4 gx0 = ixy0 * (1.0 / 7.0); vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; gx0 = fract(gx0); vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); vec4 sz0 = step(gz0, vec4(0.0)); gx0 -= sz0 * (step(0.0, gx0) - 0.5); gy0 -= sz0 * (step(0.0, gy0) - 0.5); vec4 gx1 = ixy1 * (1.0 / 7.0); vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; gx1 = fract(gx1); vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); vec4 sz1 = step(gz1, vec4(0.0)); gx1 -= sz1 * (step(0.0, gx1) - 0.5); gy1 -= sz1 * (step(0.0, gy1) - 0.5); vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); g000 *= norm0.x; g010 *= norm0.y; g100 *= norm0.z; g110 *= norm0.w; vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); g001 *= norm1.x; g011 *= norm1.y; g101 *= norm1.z; g111 *= norm1.w; float n000 = dot(g000, Pf0); float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); float n111 = dot(g111, Pf1); vec3 fade_xyz = fade(Pf0); vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); return 2.2 * n_xyz; }`; /*! * Cellular noise ("Worley noise") in 3D in GLSL. * Author: Stefan Gustavson (stefan.gustavson@liu.se) * Version: Stefan Gustavson 2011-04-19 * * Many thanks to Ian McEwan of Ashima Arts for the * ideas for permutation and gradient selection. * * Copyright (c) 2011 Stefan Gustavson. All rights reserved. * Distributed under the MIT license. See LICENSE file. * https://github.com/ashima/webgl-noise */ /** * Cellular noise ("Worley noise") in 3D in GLSL. Exposes a `noise(vec3 P)` function for use inside fragment shaders. */ var cellular = ` vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } // Modulo 7 without a division vec3 mod7(vec3 x) { return x - floor(x * (1.0 / 7.0)) * 7.0; } // Permutation polynomial: (34x^2 + x) mod 289 vec3 permute(vec3 x) { return mod289((34.0 * x + 1.0) * x); } float noise(vec3 P) { #define K 0.142857142857 // 1/7 #define Ko 0.428571428571 // 1/2-K/2 #define K2 0.020408163265306 // 1/(7*7) #define Kz 0.166666666667 // 1/6 #define Kzo 0.416666666667 // 1/2-1/6*2 #define jitter 1.0 // smaller jitter gives more regular pattern vec3 Pi = mod289(floor(P)); vec3 Pf = fract(P) - 0.5; vec3 Pfx = Pf.x + vec3(1.0, 0.0, -1.0); vec3 Pfy = Pf.y + vec3(1.0, 0.0, -1.0); vec3 Pfz = Pf.z + vec3(1.0, 0.0, -1.0); vec3 p = permute(Pi.x + vec3(-1.0, 0.0, 1.0)); vec3 p1 = permute(p + Pi.y - 1.0); vec3 p2 = permute(p + Pi.y); vec3 p3 = permute(p + Pi.y + 1.0); vec3 p11 = permute(p1 + Pi.z - 1.0); vec3 p12 = permute(p1 + Pi.z); vec3 p13 = permute(p1 + Pi.z + 1.0); vec3 p21 = permute(p2 + Pi.z - 1.0); vec3 p22 = permute(p2 + Pi.z); vec3 p23 = permute(p2 + Pi.z + 1.0); vec3 p31 = permute(p3 + Pi.z -