Ran all the JS through prettier.

This commit is contained in:
Rezmason
2021-10-20 03:25:04 -07:00
parent d8a1409907
commit 91deea34d6
10 changed files with 776 additions and 864 deletions

View File

@@ -1,42 +1,39 @@
<html> <html>
<head> <head>
<title>Matrix digital rain</title> <title>Matrix digital rain</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
name="viewport" <style>
content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" body {
/> background: black;
<style> overflow: hidden;
body { margin: 0;
background: black; }
overflow: hidden;
margin: 0;
}
canvas { canvas {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
} }
</style> </style>
</head> </head>
<body> <body>
<!-- <!--
This is an implementation of the green code seen in The Matrix film and video game franchise. This is an implementation of the green code seen in The Matrix film and video game franchise.
This project demonstrates five concepts: This project demonstrates five concepts:
1. Drawing to floating point frame buffer objects, or 'FBO's, 1. Drawing to floating point frame buffer objects, or 'FBO's,
for performing computation and post-processing for performing computation and post-processing
2. GPU-side computation, with a fragment shader 2. GPU-side computation, with a fragment shader
updating two alternating FBOs updating two alternating FBOs
3. Rendering crisp "vector" graphics, with a multiple-channel 3. Rendering crisp "vector" graphics, with a multiple-channel
signed distance field (or 'MSDF') signed distance field (or 'MSDF')
4. Creating a blur/bloom effect from a texture pyramid 4. Creating a blur/bloom effect from a texture pyramid
5. Color mapping with noise, to hide banding 5. Color mapping with noise, to hide banding
For more information, please visit: https://github.com/Rezmason/matrix For more information, please visit: https://github.com/Rezmason/matrix
--> -->
<!-- <script src="lib/regl.min.js"></script> --> <!-- <script src="lib/regl.min.js"></script> -->
<script src="lib/regl.js"></script> <script src="lib/regl.js"></script>
<script src="lib/gl-matrix.js"></script> <script src="lib/gl-matrix.js"></script>
<script type="module" src="js/main.js"></script> <script type="module" src="js/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,127 +1,103 @@
import { import { loadText, extractEntries, makePassFBO, makePyramid, resizePyramid, makePass } from "./utils.js";
loadText,
extractEntries,
makePassFBO,
makePyramid,
resizePyramid,
makePass
} from "./utils.js";
// The bloom pass is basically an added high-pass blur. // The bloom pass is basically an added high-pass blur.
const pyramidHeight = 5; const pyramidHeight = 5;
const levelStrengths = Array(pyramidHeight) const levelStrengths = Array(pyramidHeight)
.fill() .fill()
.map((_, index) => .map((_, index) => Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5))
Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5) .reverse();
)
.reverse();
export default (regl, config, inputs) => { export default (regl, config, inputs) => {
const enabled = config.bloomSize > 0 && config.bloomStrength > 0;
const enabled = config.bloomSize > 0 && config.bloomStrength > 0; if (!enabled) {
return makePass({
primary: inputs.primary,
bloom: makePassFBO(regl),
});
}
if (!enabled) { const uniforms = extractEntries(config, ["bloomStrength", "highPassThreshold"]);
return makePass(
{
primary: inputs.primary,
bloom: makePassFBO(regl)
}
);
}
const uniforms = extractEntries(config, [ const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
"bloomStrength", const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
"highPassThreshold" const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
]); const output = makePassFBO(regl, config.useHalfFloat);
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); const highPassFrag = loadText("../shaders/highPass.frag");
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const output = makePassFBO(regl, config.useHalfFloat);
const highPassFrag = loadText("../shaders/highPass.frag"); // The high pass restricts the blur to bright things in our input texture.
const highPass = regl({
frag: regl.prop("frag"),
uniforms: {
...uniforms,
tex: regl.prop("tex"),
},
framebuffer: regl.prop("fbo"),
});
// The high pass restricts the blur to bright things in our input texture. // A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
const highPass = regl({ // The FBO pyramid's levels represent separate levels of detail;
frag: regl.prop("frag"), // by blurring them all, this 3x1 blur approximates a more complex gaussian.
uniforms: {
...uniforms,
tex: regl.prop("tex")
},
framebuffer: regl.prop("fbo")
});
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically. const blurFrag = loadText("../shaders/blur.frag");
// The FBO pyramid's levels represent separate levels of detail; const blur = regl({
// by blurring them all, this 3x1 blur approximates a more complex gaussian. frag: regl.prop("frag"),
uniforms: {
...uniforms,
tex: regl.prop("tex"),
direction: regl.prop("direction"),
height: regl.context("viewportWidth"),
width: regl.context("viewportHeight"),
},
framebuffer: regl.prop("fbo"),
});
const blurFrag = loadText("../shaders/blur.frag"); // The pyramid of textures gets flattened onto the source texture.
const blur = regl({ const flattenPyramid = regl({
frag: regl.prop("frag"), frag: `
uniforms: { precision mediump float;
...uniforms, varying vec2 vUV;
tex: regl.prop("tex"), ${vBlurPyramid.map((_, index) => `uniform sampler2D pyr_${index};`).join("\n")}
direction: regl.prop("direction"), uniform float bloomStrength;
height: regl.context("viewportWidth"), void main() {
width: regl.context("viewportHeight") vec4 total = vec4(0.);
}, ${vBlurPyramid.map((_, index) => `total += texture2D(pyr_${index}, vUV) * ${levelStrengths[index]};`).join("\n")}
framebuffer: regl.prop("fbo") gl_FragColor = total * bloomStrength;
}); }
`,
uniforms: {
...uniforms,
...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])),
},
framebuffer: output,
});
// The pyramid of textures gets flattened onto the source texture. return makePass(
const flattenPyramid = regl({ {
frag: ` primary: inputs.primary,
precision mediump float; bloom: output,
varying vec2 vUV; },
${vBlurPyramid () => {
.map((_, index) => `uniform sampler2D pyr_${index};`) for (let i = 0; i < pyramidHeight; i++) {
.join("\n")} const highPassFBO = highPassPyramid[i];
uniform float bloomStrength; const hBlurFBO = hBlurPyramid[i];
void main() { const vBlurFBO = vBlurPyramid[i];
vec4 total = vec4(0.); highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: inputs.primary });
${vBlurPyramid blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] });
.map( blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] });
(_, index) => }
`total += texture2D(pyr_${index}, vUV) * ${levelStrengths[index]};`
)
.join("\n")}
gl_FragColor = total * bloomStrength;
}
`,
uniforms: {
...uniforms,
...Object.fromEntries(
vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])
)
},
framebuffer: output
});
return makePass( flattenPyramid();
{ },
primary: inputs.primary, (w, h) => {
bloom: output // The blur pyramids can be lower resolution than the screen.
}, resizePyramid(highPassPyramid, w, h, config.bloomSize);
() => { resizePyramid(hBlurPyramid, w, h, config.bloomSize);
for (let i = 0; i < pyramidHeight; i++) { resizePyramid(vBlurPyramid, w, h, config.bloomSize);
const highPassFBO = highPassPyramid[i]; output.resize(w, h);
const hBlurFBO = hBlurPyramid[i]; },
const vBlurFBO = vBlurPyramid[i]; [highPassFrag.laoded, blurFrag.loaded]
highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: inputs.primary }); );
blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] });
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] });
}
flattenPyramid();
},
(w, h) => {
// The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, config.bloomSize);
resizePyramid(hBlurPyramid, w, h, config.bloomSize);
resizePyramid(vBlurPyramid, w, h, config.bloomSize);
output.resize(w, h);
},
[highPassFrag.laoded, blurFrag.loaded]
);
}; };

View File

@@ -1,204 +1,197 @@
const fonts = { const fonts = {
coptic: { coptic: {
glyphTexURL: "coptic_msdf.png", glyphTexURL: "coptic_msdf.png",
glyphSequenceLength: 32, glyphSequenceLength: 32,
glyphTextureColumns: 8 glyphTextureColumns: 8,
}, },
gothic: { gothic: {
glyphTexURL: "gothic_msdf.png", glyphTexURL: "gothic_msdf.png",
glyphSequenceLength: 27, glyphSequenceLength: 27,
glyphTextureColumns: 8 glyphTextureColumns: 8,
}, },
matrixcode: { matrixcode: {
glyphTexURL: "matrixcode_msdf.png", glyphTexURL: "matrixcode_msdf.png",
glyphSequenceLength: 57, glyphSequenceLength: 57,
glyphTextureColumns: 8 glyphTextureColumns: 8,
} },
}; };
const defaults = { const defaults = {
backgroundColor: [0, 0, 0], backgroundColor: [0, 0, 0],
volumetric: false, volumetric: false,
resurrectingCodeRatio: 0, resurrectingCodeRatio: 0,
animationSpeed: 1, animationSpeed: 1,
forwardSpeed: 0.25, forwardSpeed: 0.25,
bloomStrength: 1, bloomStrength: 1,
bloomSize: 0.6, bloomSize: 0.6,
highPassThreshold: 0.1, highPassThreshold: 0.1,
cycleSpeed: 1, cycleSpeed: 1,
cycleStyleName: "cycleFasterWhenDimmed", cycleStyleName: "cycleFasterWhenDimmed",
cursorEffectThreshold: 1, cursorEffectThreshold: 1,
brightnessOffset: 0.0, brightnessOffset: 0.0,
brightnessMultiplier: 1.0, brightnessMultiplier: 1.0,
brightnessMix: 1.0, brightnessMix: 1.0,
brightnessMinimum: 0, brightnessMinimum: 0,
fallSpeed: 1, fallSpeed: 1,
glyphEdgeCrop: 0.0, glyphEdgeCrop: 0.0,
glyphHeightToWidth: 1, glyphHeightToWidth: 1,
hasSun: false, hasSun: false,
hasThunder: false, hasThunder: false,
isPolar: false, isPolar: false,
rippleTypeName: null, rippleTypeName: null,
rippleThickness: 0.2, rippleThickness: 0.2,
rippleScale: 30, rippleScale: 30,
rippleSpeed: 0.2, rippleSpeed: 0.2,
numColumns: 80, numColumns: 80,
density: 1, density: 1,
paletteEntries: [ paletteEntries: [
{ hsl: [0.3, 0.9, 0.0], at: 0.0 }, { hsl: [0.3, 0.9, 0.0], at: 0.0 },
{ hsl: [0.3, 0.9, 0.2], at: 0.2 }, { hsl: [0.3, 0.9, 0.2], at: 0.2 },
{ hsl: [0.3, 0.9, 0.7], at: 0.7 }, { hsl: [0.3, 0.9, 0.7], at: 0.7 },
{ hsl: [0.3, 0.9, 0.8], at: 0.8 } { hsl: [0.3, 0.9, 0.8], at: 0.8 },
], ],
raindropLength: 1, raindropLength: 1,
slant: 0, slant: 0,
resolution: 1, resolution: 1,
useHalfFloat: false, useHalfFloat: false,
}; };
const versions = { const versions = {
classic: { classic: {
...defaults, ...defaults,
...fonts.matrixcode ...fonts.matrixcode,
}, },
operator: { operator: {
...defaults, ...defaults,
...fonts.matrixcode, ...fonts.matrixcode,
bloomStrength: 0.75, bloomStrength: 0.75,
highPassThreshold: 0.0, highPassThreshold: 0.0,
cycleSpeed: 0.05, cycleSpeed: 0.05,
cycleStyleName: "cycleRandomly", cycleStyleName: "cycleRandomly",
cursorEffectThreshold: 0.64, cursorEffectThreshold: 0.64,
brightnessOffset: 0.25, brightnessOffset: 0.25,
brightnessMultiplier: 0.0, brightnessMultiplier: 0.0,
brightnessMinimum: -1.0, brightnessMinimum: -1.0,
fallSpeed: 0.65, fallSpeed: 0.65,
glyphEdgeCrop: 0.15, glyphEdgeCrop: 0.15,
glyphHeightToWidth: 1.35, glyphHeightToWidth: 1.35,
rippleTypeName: "box", rippleTypeName: "box",
numColumns: 108, numColumns: 108,
paletteEntries: [ paletteEntries: [
{ hsl: [0.4, 0.8, 0.0], at: 0.0 }, { hsl: [0.4, 0.8, 0.0], at: 0.0 },
{ hsl: [0.4, 0.8, 0.5], at: 0.5 }, { hsl: [0.4, 0.8, 0.5], at: 0.5 },
{ hsl: [0.4, 0.8, 1.0], at: 1.0 } { hsl: [0.4, 0.8, 1.0], at: 1.0 },
], ],
raindropLength: 1.5 raindropLength: 1.5,
}, },
nightmare: { nightmare: {
...defaults, ...defaults,
...fonts.gothic, ...fonts.gothic,
highPassThreshold: 0.7, highPassThreshold: 0.7,
brightnessMix: 0.75, brightnessMix: 0.75,
fallSpeed: 2.0, fallSpeed: 2.0,
hasThunder: true, hasThunder: true,
numColumns: 60, numColumns: 60,
paletteEntries: [ paletteEntries: [
{ hsl: [0.0, 1.0, 0.0], at: 0.0 }, { hsl: [0.0, 1.0, 0.0], at: 0.0 },
{ hsl: [0.0, 1.0, 0.2], at: 0.2 }, { hsl: [0.0, 1.0, 0.2], at: 0.2 },
{ hsl: [0.0, 1.0, 0.4], at: 0.4 }, { hsl: [0.0, 1.0, 0.4], at: 0.4 },
{ hsl: [0.1, 1.0, 0.7], at: 0.7 }, { hsl: [0.1, 1.0, 0.7], at: 0.7 },
{ hsl: [0.2, 1.0, 1.0], at: 1.0 } { hsl: [0.2, 1.0, 1.0], at: 1.0 },
], ],
raindropLength: 0.6, raindropLength: 0.6,
slant: (22.5 * Math.PI) / 180 slant: (22.5 * Math.PI) / 180,
}, },
paradise: { paradise: {
...defaults, ...defaults,
...fonts.coptic, ...fonts.coptic,
bloomStrength: 1.75, bloomStrength: 1.75,
highPassThreshold: 0, highPassThreshold: 0,
cycleSpeed: 0.1, cycleSpeed: 0.1,
brightnessMix: 0.05, brightnessMix: 0.05,
fallSpeed: 0.08, fallSpeed: 0.08,
hasSun: true, hasSun: true,
isPolar: true, isPolar: true,
rippleTypeName: "circle", rippleTypeName: "circle",
rippleSpeed: 0.1, rippleSpeed: 0.1,
numColumns: 30, numColumns: 30,
paletteEntries: [ paletteEntries: [
{ hsl: [0.0, 0.0, 0.0], at: 0.0 }, { hsl: [0.0, 0.0, 0.0], at: 0.0 },
{ hsl: [0.0, 0.8, 0.3], at: 0.3 }, { hsl: [0.0, 0.8, 0.3], at: 0.3 },
{ hsl: [0.1, 0.8, 0.5], at: 0.5 }, { hsl: [0.1, 0.8, 0.5], at: 0.5 },
{ hsl: [0.1, 1.0, 0.6], at: 0.6 }, { hsl: [0.1, 1.0, 0.6], at: 0.6 },
{ hsl: [0.1, 1.0, 0.9], at: 0.9 } { hsl: [0.1, 1.0, 0.9], at: 0.9 },
], ],
raindropLength: 0.4 raindropLength: 0.4,
}, },
resurrections: { resurrections: {
...defaults, ...defaults,
...fonts.matrixcode, ...fonts.matrixcode,
resurrectingCodeRatio: 0.25, resurrectingCodeRatio: 0.25,
effect:"resurrections", effect: "resurrections",
width:100, width: 100,
volumetric:true, volumetric: true,
density:1.5, density: 1.5,
fallSpeed:1.2, fallSpeed: 1.2,
raindropLength:1.25 raindropLength: 1.25,
} },
}; };
versions.throwback = versions.operator; versions.throwback = versions.operator;
versions["1999"] = versions.classic; versions["1999"] = versions.classic;
const range = (f, min = -Infinity, max = Infinity) => const range = (f, min = -Infinity, max = Infinity) => Math.max(min, Math.min(max, f));
Math.max(min, Math.min(max, f)); const nullNaN = (f) => (isNaN(f) ? null : f);
const nullNaN = f => (isNaN(f) ? null : f);
const paramMapping = { const paramMapping = {
version: { key: "version", parser: s => s }, version: { key: "version", parser: (s) => s },
effect: { key: "effect", parser: s => s }, effect: { key: "effect", parser: (s) => s },
width: { key: "numColumns", parser: s => nullNaN(parseInt(s)) }, width: { key: "numColumns", parser: (s) => nullNaN(parseInt(s)) },
numColumns: { key: "numColumns", parser: s => nullNaN(parseInt(s)) }, numColumns: { key: "numColumns", parser: (s) => nullNaN(parseInt(s)) },
density: { key: "density", parser: s => nullNaN(range(parseFloat(s), 0)) }, density: { key: "density", parser: (s) => nullNaN(range(parseFloat(s), 0)) },
resolution: { key: "resolution", parser: s => nullNaN(parseFloat(s)) }, resolution: { key: "resolution", parser: (s) => nullNaN(parseFloat(s)) },
animationSpeed: { animationSpeed: {
key: "animationSpeed", key: "animationSpeed",
parser: s => nullNaN(parseFloat(s)) parser: (s) => nullNaN(parseFloat(s)),
}, },
forwardSpeed: { forwardSpeed: {
key: "forwardSpeed", key: "forwardSpeed",
parser: s => nullNaN(parseFloat(s)) parser: (s) => nullNaN(parseFloat(s)),
}, },
cycleSpeed: { key: "cycleSpeed", parser: s => nullNaN(parseFloat(s)) }, cycleSpeed: { key: "cycleSpeed", parser: (s) => nullNaN(parseFloat(s)) },
fallSpeed: { key: "fallSpeed", parser: s => nullNaN(parseFloat(s)) }, fallSpeed: { key: "fallSpeed", parser: (s) => nullNaN(parseFloat(s)) },
raindropLength: { raindropLength: {
key: "raindropLength", key: "raindropLength",
parser: s => nullNaN(parseFloat(s)) parser: (s) => nullNaN(parseFloat(s)),
}, },
slant: { slant: {
key: "slant", key: "slant",
parser: s => nullNaN((parseFloat(s) * Math.PI) / 180) parser: (s) => nullNaN((parseFloat(s) * Math.PI) / 180),
}, },
bloomSize: { bloomSize: {
key: "bloomSize", key: "bloomSize",
parser: s => nullNaN(range(parseFloat(s), 0, 1)) parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
}, },
url: { key: "bgURL", parser: s => s }, url: { key: "bgURL", parser: (s) => s },
stripeColors: { key: "stripeColors", parser: s => s }, stripeColors: { key: "stripeColors", parser: (s) => s },
backgroundColor: { key: "backgroundColor", parser: s => s.split(",").map(parseFloat) }, backgroundColor: { key: "backgroundColor", parser: (s) => s.split(",").map(parseFloat) },
volumetric: { key: "volumetric", parser: s => s.toLowerCase().includes("true") } volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") },
}; };
paramMapping.dropLength = paramMapping.raindropLength; paramMapping.dropLength = paramMapping.raindropLength;
paramMapping.angle = paramMapping.slant; paramMapping.angle = paramMapping.slant;
paramMapping.colors = paramMapping.stripeColors; paramMapping.colors = paramMapping.stripeColors;
export default (searchString, make1DTexture) => { export default (searchString, make1DTexture) => {
const urlParams = Object.fromEntries( const urlParams = Object.fromEntries(
Array.from(new URLSearchParams(searchString).entries()) Array.from(new URLSearchParams(searchString).entries())
.filter(([key]) => key in paramMapping) .filter(([key]) => key in paramMapping)
.map(([key, value]) => [ .map(([key, value]) => [paramMapping[key].key, paramMapping[key].parser(value)])
paramMapping[key].key, .filter(([_, value]) => value != null)
paramMapping[key].parser(value) );
])
.filter(([_, value]) => value != null)
);
const version = const version = urlParams.version in versions ? versions[urlParams.version] : versions.classic;
urlParams.version in versions
? versions[urlParams.version]
: versions.classic;
return { return {
...version, ...version,
...urlParams ...urlParams,
}; };
}; };

View File

@@ -1,28 +1,27 @@
import { loadImage, loadText, makePassFBO, makePass } from "./utils.js"; import { loadImage, loadText, makePassFBO, makePass } from "./utils.js";
const defaultBGURL = const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg";
"https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg";
export default (regl, config, inputs) => { export default (regl, config, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
const background = loadImage(regl, bgURL); const background = loadImage(regl, bgURL);
const imagePassFrag = loadText("../shaders/imagePass.frag"); const imagePassFrag = loadText("../shaders/imagePass.frag");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
backgroundTex: background.texture, backgroundTex: background.texture,
tex: inputs.primary, tex: inputs.primary,
bloomTex: inputs.bloom bloomTex: inputs.bloom,
}, },
framebuffer: output framebuffer: output,
}); });
return makePass( return makePass(
{ {
primary: output primary: output,
}, },
() => render({frag: imagePassFrag.text()}), () => render({ frag: imagePassFrag.text() }),
null, null,
[background.loaded, imagePassFrag.loaded] [background.loaded, imagePassFrag.loaded]
); );
}; };

View File

@@ -9,30 +9,26 @@ import makeResurrectionPass from "./resurrectionPass.js";
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
document.body.appendChild(canvas); document.body.appendChild(canvas);
document.addEventListener("touchmove", e => e.preventDefault(), { document.addEventListener("touchmove", (e) => e.preventDefault(), {
passive: false passive: false,
}); });
const regl = createREGL({ const regl = createREGL({
canvas, canvas,
extensions: ["OES_texture_half_float", "OES_texture_half_float_linear"], extensions: ["OES_texture_half_float", "OES_texture_half_float_linear"],
// These extensions are also needed, but Safari misreports that they are missing // These extensions are also needed, but Safari misreports that they are missing
optionalExtensions: [ optionalExtensions: ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"],
"EXT_color_buffer_half_float",
"WEBGL_color_buffer_float",
"OES_standard_derivatives"
]
}); });
const effects = { const effects = {
none: null, none: null,
plain: makePalettePass, plain: makePalettePass,
customStripes: makeStripePass, customStripes: makeStripePass,
stripes: makeStripePass, stripes: makeStripePass,
pride: makeStripePass, pride: makeStripePass,
image: makeImagePass, image: makeImagePass,
resurrection: makeResurrectionPass, resurrection: makeResurrectionPass,
resurrections: makeResurrectionPass resurrections: makeResurrectionPass,
}; };
const config = makeConfig(window.location.search); const config = makeConfig(window.location.search);
@@ -40,8 +36,8 @@ const resolution = config.resolution;
const effect = config.effect in effects ? config.effect : "plain"; const effect = config.effect in effects ? config.effect : "plain";
const resize = () => { const resize = () => {
canvas.width = Math.ceil(canvas.clientWidth * resolution); canvas.width = Math.ceil(canvas.clientWidth * resolution);
canvas.height = Math.ceil(canvas.clientHeight * resolution); canvas.height = Math.ceil(canvas.clientHeight * resolution);
}; };
window.onresize = resize; window.onresize = resize;
resize(); resize();
@@ -49,41 +45,29 @@ resize();
const dimensions = { width: 1, height: 1 }; const dimensions = { width: 1, height: 1 };
document.body.onload = async () => { document.body.onload = async () => {
// All this takes place in a full screen quad. // All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl); const fullScreenQuad = makeFullScreenQuad(regl);
const pipeline = makePipeline( const pipeline = makePipeline([makeMatrixRenderer, effect === "none" ? null : makeBloomPass, effects[effect]], (p) => p.outputs, regl, config);
[ const drawToScreen = regl({
makeMatrixRenderer, uniforms: {
effect === "none" ? null : makeBloomPass, tex: pipeline[pipeline.length - 1].outputs.primary,
effects[effect] },
], });
p => p.outputs, await Promise.all(pipeline.map(({ ready }) => ready));
regl, const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
config // tick.cancel();
); if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
const drawToScreen = regl({ dimensions.width = viewportWidth;
uniforms: { dimensions.height = viewportHeight;
tex: pipeline[pipeline.length - 1].outputs.primary for (const step of pipeline) {
} step.resize(viewportWidth, viewportHeight);
}); }
await Promise.all(pipeline.map(({ ready }) => ready)); }
const tick = regl.frame(({ viewportWidth, viewportHeight }) => { fullScreenQuad(() => {
// tick.cancel(); for (const step of pipeline) {
if ( step.render();
dimensions.width !== viewportWidth || }
dimensions.height !== viewportHeight drawToScreen();
) { });
dimensions.width = viewportWidth; });
dimensions.height = viewportHeight;
for (const step of pipeline) {
step.resize(viewportWidth, viewportHeight);
}
}
fullScreenQuad(() => {
for (const step of pipeline) {
step.render();
}
drawToScreen();
});
});
}; };

View File

@@ -1,51 +1,49 @@
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
const colorToRGB = ([hue, saturation, lightness]) => { const colorToRGB = ([hue, saturation, lightness]) => {
const a = saturation * Math.min(lightness, 1 - lightness); const a = saturation * Math.min(lightness, 1 - lightness);
const f = (n) => { const f = (n) => {
const k = (n + hue * 12) % 12; const k = (n + hue * 12) % 12;
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}; };
return [f(0), f(8), f(4)]; return [f(0), f(8), f(4)];
}; };
const makePalette = (regl, entries) => { const makePalette = (regl, entries) => {
const PALETTE_SIZE = 2048; const PALETTE_SIZE = 2048;
const paletteColors = Array(PALETTE_SIZE); const paletteColors = Array(PALETTE_SIZE);
const sortedEntries = entries const sortedEntries = entries
.slice() .slice()
.sort((e1, e2) => e1.at - e2.at) .sort((e1, e2) => e1.at - e2.at)
.map(entry => ({ .map((entry) => ({
rgb: colorToRGB(entry.hsl), rgb: colorToRGB(entry.hsl),
arrayIndex: Math.floor( arrayIndex: Math.floor(Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)),
Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1) }));
) sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
})); sortedEntries.push({
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 }); rgb: sortedEntries[sortedEntries.length - 1].rgb,
sortedEntries.push({ arrayIndex: PALETTE_SIZE - 1,
rgb: sortedEntries[sortedEntries.length - 1].rgb, });
arrayIndex: PALETTE_SIZE - 1 sortedEntries.forEach((entry, index) => {
}); paletteColors[entry.arrayIndex] = entry.rgb.slice();
sortedEntries.forEach((entry, index) => { if (index + 1 < sortedEntries.length) {
paletteColors[entry.arrayIndex] = entry.rgb.slice(); const nextEntry = sortedEntries[index + 1];
if (index + 1 < sortedEntries.length) { const diff = nextEntry.arrayIndex - entry.arrayIndex;
const nextEntry = sortedEntries[index + 1]; for (let i = 0; i < diff; i++) {
const diff = nextEntry.arrayIndex - entry.arrayIndex; const ratio = i / diff;
for (let i = 0; i < diff; i++) { paletteColors[entry.arrayIndex + i] = [
const ratio = i / diff; entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
paletteColors[entry.arrayIndex + i] = [ entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio, entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio, ];
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio }
]; }
} });
}
});
return make1DTexture( return make1DTexture(
regl, regl,
paletteColors.flat().map(i => i * 0xff) paletteColors.flat().map((i) => i * 0xff)
); );
}; };
// The rendered texture's values are mapped to colors in a palette texture. // The rendered texture's values are mapped to colors in a palette texture.
@@ -55,32 +53,30 @@ const makePalette = (regl, entries) => {
// in screen space. // in screen space.
export default (regl, config, inputs) => { export default (regl, config, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const palette = makePalette(regl, config.paletteEntries); const palette = makePalette(regl, config.paletteEntries);
const palettePassFrag = loadText("../shaders/palettePass.frag"); const palettePassFrag = loadText("../shaders/palettePass.frag");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
...extractEntries(config, [ ...extractEntries(config, ["backgroundColor"]),
"backgroundColor", tex: inputs.primary,
]), bloomTex: inputs.bloom,
tex: inputs.primary, palette,
bloomTex: inputs.bloom, ditherMagnitude: 0.05,
palette, },
ditherMagnitude: 0.05 framebuffer: output,
}, });
framebuffer: output
});
return makePass( return makePass(
{ {
primary: output primary: output,
}, },
() => render({ frag: palettePassFrag.text() }), () => render({ frag: palettePassFrag.text() }),
null, null,
palettePassFrag.loaded palettePassFrag.loaded
); );
}; };

View File

@@ -1,187 +1,180 @@
import { import { extractEntries, loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";
extractEntries,
loadImage,
loadText,
makePassFBO,
makeDoubleBuffer,
makePass
} from "./utils.js";
const rippleTypes = { const rippleTypes = {
box: 0, box: 0,
circle: 1 circle: 1,
}; };
const cycleStyles = { const cycleStyles = {
cycleFasterWhenDimmed: 0, cycleFasterWhenDimmed: 0,
cycleRandomly: 1 cycleRandomly: 1,
}; };
const numVerticesPerQuad = 2 * 3; const numVerticesPerQuad = 2 * 3;
export default (regl, config) => { export default (regl, config) => {
const volumetric = config.volumetric;
const density = volumetric && config.effect !== "none" ? config.density : 1;
const [numRows, numColumns] = [config.numColumns, config.numColumns * density];
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1];
const numQuads = numQuadRows * numQuadColumns;
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
const volumetric = config.volumetric; // These two framebuffers are used to compute the raining code.
const density = volumetric && config.effect !== "none" ? config.density : 1; // they take turns being the source and destination of the "compute" shader.
const [numRows, numColumns] = [config.numColumns, config.numColumns * density]; // The half float data type is crucial! It lets us store almost any real number,
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1]; // whereas the default type limits us to integers between 0 and 255.
const numQuads = numQuadRows * numQuadColumns;
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
// These two framebuffers are used to compute the raining code. // This double buffer is smaller than the screen, because its pixels correspond
// they take turns being the source and destination of the "compute" shader. // with glyphs in the final image, and the glyphs are much larger than a pixel.
// The half float data type is crucial! It lets us store almost any real number, const doubleBuffer = makeDoubleBuffer(regl, {
// whereas the default type limits us to integers between 0 and 255. width: numColumns,
height: numRows,
wrapT: "clamp",
type: "half float",
});
// This double buffer is smaller than the screen, because its pixels correspond const output = makePassFBO(regl, config.useHalfFloat);
// with glyphs in the final image, and the glyphs are much larger than a pixel.
const doubleBuffer = makeDoubleBuffer(regl, {
width: numColumns,
height: numRows,
wrapT: "clamp",
type: "half float"
});
const uniforms = {
...extractEntries(config, [
// rain general
"glyphHeightToWidth",
"glyphTextureColumns",
// rain update
"animationSpeed",
"brightnessMinimum",
"brightnessMix",
"brightnessMultiplier",
"brightnessOffset",
"cursorEffectThreshold",
"cycleSpeed",
"fallSpeed",
"glyphSequenceLength",
"hasSun",
"hasThunder",
"raindropLength",
"rippleScale",
"rippleSpeed",
"rippleThickness",
"resurrectingCodeRatio",
// rain vertex
"forwardSpeed",
// rain render
"glyphEdgeCrop",
"isPolar",
]),
density,
numRows,
numColumns,
numQuadRows,
numQuadColumns,
quadSize,
volumetric,
};
const output = makePassFBO(regl, config.useHalfFloat); uniforms.rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1;
uniforms.cycleStyle = config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0;
uniforms.slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
uniforms.slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
uniforms.showComputationTexture = config.effect === "none";
const uniforms = { const msdf = loadImage(regl, config.glyphTexURL);
...extractEntries(config, [
// rain general
"glyphHeightToWidth",
"glyphTextureColumns",
// rain update
"animationSpeed",
"brightnessMinimum",
"brightnessMix",
"brightnessMultiplier",
"brightnessOffset",
"cursorEffectThreshold",
"cycleSpeed",
"fallSpeed",
"glyphSequenceLength",
"hasSun",
"hasThunder",
"raindropLength",
"rippleScale",
"rippleSpeed",
"rippleThickness",
"resurrectingCodeRatio",
// rain vertex
"forwardSpeed",
// rain render
"glyphEdgeCrop",
"isPolar",
]),
density,
numRows,
numColumns,
numQuadRows,
numQuadColumns,
quadSize,
volumetric
};
uniforms.rippleType = const updateFrag = loadText("../shaders/update.frag");
config.rippleTypeName in rippleTypes const update = regl({
? rippleTypes[config.rippleTypeName] frag: regl.prop("frag"),
: -1; uniforms: {
uniforms.cycleStyle = ...uniforms,
config.cycleStyleName in cycleStyles lastState: doubleBuffer.back,
? cycleStyles[config.cycleStyleName] },
: 0;
uniforms.slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
uniforms.slantScale =
1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
uniforms.showComputationTexture = config.effect === "none";
const msdf = loadImage(regl, config.glyphTexURL); framebuffer: doubleBuffer.front,
});
const updateFrag = loadText("../shaders/update.frag"); const quadPositions = Array(numQuadRows)
const update = regl({ .fill()
frag: regl.prop("frag"), .map((_, y) =>
uniforms: { Array(numQuadColumns)
...uniforms, .fill()
lastState: doubleBuffer.back .map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
}, );
framebuffer: doubleBuffer.front const quadCorners = Array(numQuads).fill([
}); [0, 0],
[0, 1],
[1, 1],
[0, 0],
[1, 1],
[1, 0],
]);
const quadPositions = Array(numQuadRows).fill().map((_, y) => // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
Array(numQuadColumns).fill().map((_, x) => const renderVert = loadText("../shaders/render.vert");
Array(numVerticesPerQuad).fill([x, y]) const renderFrag = loadText("../shaders/render.frag");
) const render = regl({
); blend: {
enable: true,
func: {
srcRGB: "src alpha",
srcAlpha: 1,
dstRGB: "dst alpha",
dstAlpha: 1,
},
},
vert: regl.prop("vert"),
frag: regl.prop("frag"),
const quadCorners = Array(numQuads).fill([[0, 0], [0, 1], [1, 1], [0, 0], [1, 1], [1, 0]]); uniforms: {
...uniforms,
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen lastState: doubleBuffer.front,
const renderVert = loadText("../shaders/render.vert"); glyphTex: msdf.texture,
const renderFrag = loadText("../shaders/render.frag");
const render = regl({
blend: {
enable: true,
func: {
srcRGB: "src alpha",
srcAlpha: 1,
dstRGB: "dst alpha",
dstAlpha: 1
}
},
vert: regl.prop("vert"),
frag: regl.prop("frag"),
uniforms: { camera: regl.prop("camera"),
...uniforms, transform: regl.prop("transform"),
screenSize: regl.prop("screenSize"),
},
lastState: doubleBuffer.front, attributes: {
glyphTex: msdf.texture, aPosition: quadPositions,
aCorner: quadCorners,
},
count: numQuads * numVerticesPerQuad,
camera: regl.prop("camera"), framebuffer: output,
transform: regl.prop("transform"), });
screenSize: regl.prop("screenSize")
},
attributes: { const screenSize = [1, 1];
aPosition: quadPositions, const { mat4, vec3 } = glMatrix;
aCorner: quadCorners const camera = mat4.create();
}, const translation = vec3.set(vec3.create(), 0, 0.5 / numRows, -1);
count: numQuads * numVerticesPerQuad, const scale = vec3.set(vec3.create(), 1, 1, 1);
const transform = mat4.create();
mat4.translate(transform, transform, translation);
mat4.scale(transform, transform, scale);
framebuffer: output return makePass(
}); {
primary: output,
},
() => {
const time = Date.now();
const screenSize = [1, 1]; update({ frag: updateFrag.text() });
const {mat4, vec3} = glMatrix; regl.clear({
const camera = mat4.create(); depth: 1,
const translation = vec3.set(vec3.create(), 0, 0.5 / numRows, -1); color: [0, 0, 0, 1],
const scale = vec3.set(vec3.create(), 1, 1, 1); framebuffer: output,
const transform = mat4.create(); });
mat4.translate(transform, transform, translation); render({ camera, transform, screenSize, vert: renderVert.text(), frag: renderFrag.text() });
mat4.scale(transform, transform, scale); },
(w, h) => {
return makePass( output.resize(w, h);
{ const aspectRatio = w / h;
primary: output glMatrix.mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
}, [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
() => { },
const time = Date.now(); [msdf.loaded, updateFrag.loaded, renderVert.loaded, renderFrag.loaded]
);
update({frag: updateFrag.text()});
regl.clear({
depth: 1,
color: [0, 0, 0, 1],
framebuffer: output
});
render({camera, transform, screenSize, vert: renderVert.text(), frag: renderFrag.text()});
},
(w, h) => {
output.resize(w, h);
const aspectRatio = w / h;
glMatrix.mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
},
[msdf.loaded, updateFrag.loaded, renderVert.loaded, renderFrag.loaded]
);
}; };

View File

@@ -1,37 +1,35 @@
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
const colorToRGB = ([hue, saturation, lightness]) => { const colorToRGB = ([hue, saturation, lightness]) => {
const a = saturation * Math.min(lightness, 1 - lightness); const a = saturation * Math.min(lightness, 1 - lightness);
const f = (n) => { const f = (n) => {
const k = (n + hue * 12) % 12; const k = (n + hue * 12) % 12;
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
}; };
return [f(0), f(8), f(4)]; return [f(0), f(8), f(4)];
}; };
export default (regl, config, inputs) => { export default (regl, config, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const resurrectionPassFrag = loadText("../shaders/resurrectionPass.frag"); const resurrectionPassFrag = loadText("../shaders/resurrectionPass.frag");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
...extractEntries(config, [ ...extractEntries(config, ["backgroundColor"]),
"backgroundColor", tex: inputs.primary,
]), bloomTex: inputs.bloom,
tex: inputs.primary, ditherMagnitude: 0.05,
bloomTex: inputs.bloom, },
ditherMagnitude: 0.05 framebuffer: output,
}, });
framebuffer: output
});
return makePass( return makePass(
{ {
primary: output primary: output,
}, },
() => render({frag: resurrectionPassFrag.text() }) () => render({ frag: resurrectionPassFrag.text() })
); );
}; };

View File

@@ -1,61 +1,55 @@
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
const neapolitanStripeColors = [ const neapolitanStripeColors = [
[0.4, 0.15, 0.1], [0.4, 0.15, 0.1],
[0.4, 0.15, 0.1], [0.4, 0.15, 0.1],
[0.8, 0.8, 0.6], [0.8, 0.8, 0.6],
[0.8, 0.8, 0.6], [0.8, 0.8, 0.6],
[1.0, 0.7, 0.8], [1.0, 0.7, 0.8],
[1.0, 0.7, 0.8] [1.0, 0.7, 0.8],
].flat(); ].flat();
const prideStripeColors = [ const prideStripeColors = [
[1, 0, 0], [1, 0, 0],
[1, 0.5, 0], [1, 0.5, 0],
[1, 1, 0], [1, 1, 0],
[0, 1, 0], [0, 1, 0],
[0, 0, 1], [0, 0, 1],
[0.8, 0, 1] [0.8, 0, 1],
].flat(); ].flat();
export default (regl, config, inputs) => { export default (regl, config, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const stripeColors = const stripeColors =
"stripeColors" in config "stripeColors" in config ? config.stripeColors.split(",").map(parseFloat) : config.effect === "pride" ? prideStripeColors : neapolitanStripeColors;
? config.stripeColors.split(",").map(parseFloat) const numStripeColors = Math.floor(stripeColors.length / 3);
: config.effect === "pride" const stripes = make1DTexture(
? prideStripeColors regl,
: neapolitanStripeColors; stripeColors.slice(0, numStripeColors * 3).map((f) => Math.floor(f * 0xff))
const numStripeColors = Math.floor(stripeColors.length / 3); );
const stripes = make1DTexture(
regl,
stripeColors.slice(0, numStripeColors * 3).map(f => Math.floor(f * 0xff))
);
const stripePassFrag = loadText("../shaders/stripePass.frag"); const stripePassFrag = loadText("../shaders/stripePass.frag");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
...extractEntries(config, [ ...extractEntries(config, ["backgroundColor"]),
"backgroundColor", tex: inputs.primary,
]), bloomTex: inputs.bloom,
tex: inputs.primary, stripes,
bloomTex: inputs.bloom, ditherMagnitude: 0.05,
stripes, },
ditherMagnitude: 0.05 framebuffer: output,
}, });
framebuffer: output
});
return makePass( return makePass(
{ {
primary: output primary: output,
}, },
() => render({frag: stripePassFrag.text()}), () => render({ frag: stripePassFrag.text() }),
null, null,
stripePassFrag.loaded stripePassFrag.loaded
); );
}; };

View File

@@ -1,128 +1,120 @@
const extractEntries = (src, keys) => const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)));
Object.fromEntries(
Array.from(Object.entries(src)).filter(([key]) => keys.includes(key))
);
const makePassTexture = (regl, halfFloat) => const makePassTexture = (regl, halfFloat) =>
regl.texture({ regl.texture({
width: 1, width: 1,
height: 1, height: 1,
type: halfFloat ? "half float" : "uint8", type: halfFloat ? "half float" : "uint8",
wrap: "clamp", wrap: "clamp",
min: "linear", min: "linear",
mag: "linear" mag: "linear",
}); });
const makePassFBO = (regl, halfFloat) => regl.framebuffer({ color: makePassTexture(regl, halfFloat) }); const makePassFBO = (regl, halfFloat) => regl.framebuffer({ color: makePassTexture(regl, halfFloat) });
// A pyramid is just an array of FBOs, where each FBO is half the width // A pyramid is just an array of FBOs, where each FBO is half the width
// and half the height of the FBO below it. // and half the height of the FBO below it.
const makePyramid = (regl, height, halfFloat) => const makePyramid = (regl, height, halfFloat) =>
Array(height) Array(height)
.fill() .fill()
.map(_ => makePassFBO(regl, halfFloat)); .map((_) => makePassFBO(regl, halfFloat));
const makeDoubleBuffer = (regl, props) => { const makeDoubleBuffer = (regl, props) => {
const state = Array(2) const state = Array(2)
.fill() .fill()
.map(() => .map(() =>
regl.framebuffer({ regl.framebuffer({
color: regl.texture(props), color: regl.texture(props),
depthStencil: false depthStencil: false,
}) })
); );
return { return {
front: ({ tick }) => state[tick % 2], front: ({ tick }) => state[tick % 2],
back: ({ tick }) => state[(tick + 1) % 2] back: ({ tick }) => state[(tick + 1) % 2],
}; };
}; };
const resizePyramid = (pyramid, vw, vh, scale) => const resizePyramid = (pyramid, vw, vh, scale) =>
pyramid.forEach((fbo, index) => pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index)));
fbo.resize(
Math.floor((vw * scale) / 2 ** index),
Math.floor((vh * scale) / 2 ** index)
)
);
const loadImage = (regl, url) => { const loadImage = (regl, url) => {
let texture = regl.texture([[0]]); let texture = regl.texture([[0]]);
let loaded = false; let loaded = false;
return { return {
texture: () => { texture: () => {
if (!loaded) { if (!loaded) {
console.warn(`texture still loading: ${url}`); console.warn(`texture still loading: ${url}`);
} }
return texture; return texture;
}, },
loaded: (async () => { loaded: (async () => {
if (url != null) { if (url != null) {
const data = new Image(); const data = new Image();
data.crossOrigin = "anonymous"; data.crossOrigin = "anonymous";
data.src = url; data.src = url;
await data.decode(); await data.decode();
loaded = true; loaded = true;
texture = regl.texture({ texture = regl.texture({
data, data,
mag: "linear", mag: "linear",
min: "linear", min: "linear",
flipY: true flipY: true,
}); });
} }
})() })(),
}; };
}; };
const loadShader = (regl, url) => { const loadShader = (regl, url) => {
let texture = regl.texture([[0]]); let texture = regl.texture([[0]]);
let loaded = false; let loaded = false;
return { return {
texture: () => { texture: () => {
if (!loaded) { if (!loaded) {
console.warn(`texture still loading: ${url}`); console.warn(`texture still loading: ${url}`);
} }
return texture; return texture;
}, },
loaded: (async () => { loaded: (async () => {
if (url != null) { if (url != null) {
const data = new Image(); const data = new Image();
data.crossOrigin = "anonymous"; data.crossOrigin = "anonymous";
data.src = url; data.src = url;
await data.decode(); await data.decode();
loaded = true; loaded = true;
texture = regl.texture({ texture = regl.texture({
data, data,
mag: "linear", mag: "linear",
min: "linear", min: "linear",
flipY: true flipY: true,
}); });
} }
})() })(),
}; };
}; };
const loadText = (url) => { const loadText = (url) => {
let text = ""; let text = "";
let loaded = false; let loaded = false;
return { return {
text: () => { text: () => {
if (!loaded) { if (!loaded) {
console.warn(`text still loading: ${url}`); console.warn(`text still loading: ${url}`);
} }
return text; return text;
}, },
loaded: (async () => { loaded: (async () => {
if (url != null) { if (url != null) {
text = await (await fetch(url)).text(); text = await (await fetch(url)).text();
loaded = true; loaded = true;
} }
})() })(),
}; };
}; };
const makeFullScreenQuad = (regl, uniforms = {}, context = {}) => const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
regl({ regl({
vert: ` vert: `
precision mediump float; precision mediump float;
attribute vec2 aPosition; attribute vec2 aPosition;
varying vec2 vUV; varying vec2 vUV;
@@ -132,7 +124,7 @@ const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
} }
`, `,
frag: ` frag: `
precision mediump float; precision mediump float;
varying vec2 vUV; varying vec2 vUV;
uniform sampler2D tex; uniform sampler2D tex;
@@ -141,75 +133,65 @@ const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
} }
`, `,
attributes: { attributes: {
aPosition: [-4, -4, 4, -4, 0, 4] aPosition: [-4, -4, 4, -4, 0, 4],
}, },
count: 3, count: 3,
uniforms: { uniforms: {
...uniforms, ...uniforms,
time: regl.context("time") time: regl.context("time"),
}, },
context, context,
depth: { enable: false }, depth: { enable: false },
});
});
const make1DTexture = (regl, data) => const make1DTexture = (regl, data) =>
regl.texture({ regl.texture({
data, data,
width: data.length / 3, width: data.length / 3,
height: 1, height: 1,
format: "rgb", format: "rgb",
mag: "linear", mag: "linear",
min: "linear" min: "linear",
}); });
const makePass = (outputs, render, resize, ready) => { const makePass = (outputs, render, resize, ready) => {
if (render == null) { if (render == null) {
render = () => {}; render = () => {};
} }
if (resize == null) { if (resize == null) {
resize = (w, h) => resize = (w, h) => Object.values(outputs).forEach((output) => output.resize(w, h));
Object.values(outputs).forEach(output => output.resize(w, h)); }
} if (ready == null) {
if (ready == null) { ready = Promise.resolve();
ready = Promise.resolve(); } else if (ready instanceof Array) {
} else if (ready instanceof Array) { ready = Promise.all(ready);
ready = Promise.all(ready); }
} return {
return { outputs,
outputs, render,
render, resize,
resize, ready,
ready };
};
}; };
const makePipeline = (steps, getInputs, ...params) => const makePipeline = (steps, getInputs, ...params) =>
steps steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(...params, i == 0 ? null : getInputs(pipeline[i - 1]))], []);
.filter(f => f != null)
.reduce(
(pipeline, f, i) => [
...pipeline,
f(...params, i == 0 ? null : getInputs(pipeline[i - 1]))
],
[]
);
export { export {
extractEntries, extractEntries,
makePassTexture, makePassTexture,
makePassFBO, makePassFBO,
makeDoubleBuffer, makeDoubleBuffer,
makePyramid, makePyramid,
resizePyramid, resizePyramid,
loadImage, loadImage,
loadText, loadText,
makeFullScreenQuad, makeFullScreenQuad,
make1DTexture, make1DTexture,
makePass, makePass,
makePipeline makePipeline,
}; };