kampos
Version:
Tiny and fast effects compositor on WebGL
1,627 lines (1,554 loc) • 144 kB
JavaScript
'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 -