mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-14 12:29:30 -07:00
Ran all the JS through prettier.
This commit is contained in:
194
js/bloomPass.js
194
js/bloomPass.js
@@ -1,127 +1,103 @@
|
||||
import {
|
||||
loadText,
|
||||
extractEntries,
|
||||
makePassFBO,
|
||||
makePyramid,
|
||||
resizePyramid,
|
||||
makePass
|
||||
} from "./utils.js";
|
||||
import { loadText, extractEntries, makePassFBO, makePyramid, resizePyramid, makePass } from "./utils.js";
|
||||
|
||||
// The bloom pass is basically an added high-pass blur.
|
||||
|
||||
const pyramidHeight = 5;
|
||||
const levelStrengths = Array(pyramidHeight)
|
||||
.fill()
|
||||
.map((_, index) =>
|
||||
Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5)
|
||||
)
|
||||
.reverse();
|
||||
.fill()
|
||||
.map((_, index) => Math.pow(index / (pyramidHeight * 2) + 0.5, 1 / 3).toPrecision(5))
|
||||
.reverse();
|
||||
|
||||
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) {
|
||||
return makePass(
|
||||
{
|
||||
primary: inputs.primary,
|
||||
bloom: makePassFBO(regl)
|
||||
}
|
||||
);
|
||||
}
|
||||
const uniforms = extractEntries(config, ["bloomStrength", "highPassThreshold"]);
|
||||
|
||||
const uniforms = extractEntries(config, [
|
||||
"bloomStrength",
|
||||
"highPassThreshold"
|
||||
]);
|
||||
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||
const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
|
||||
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||
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");
|
||||
|
||||
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.
|
||||
const highPass = regl({
|
||||
frag: regl.prop("frag"),
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
tex: regl.prop("tex")
|
||||
},
|
||||
framebuffer: regl.prop("fbo")
|
||||
});
|
||||
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
|
||||
// The FBO pyramid's levels represent separate levels of detail;
|
||||
// by blurring them all, this 3x1 blur approximates a more complex gaussian.
|
||||
|
||||
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
|
||||
// The FBO pyramid's levels represent separate levels of detail;
|
||||
// by blurring them all, this 3x1 blur approximates a more complex gaussian.
|
||||
const blurFrag = loadText("../shaders/blur.frag");
|
||||
const blur = regl({
|
||||
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");
|
||||
const blur = regl({
|
||||
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")
|
||||
});
|
||||
// The pyramid of textures gets flattened onto the source texture.
|
||||
const flattenPyramid = regl({
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec2 vUV;
|
||||
${vBlurPyramid.map((_, index) => `uniform sampler2D pyr_${index};`).join("\n")}
|
||||
uniform float bloomStrength;
|
||||
void main() {
|
||||
vec4 total = vec4(0.);
|
||||
${vBlurPyramid.map((_, 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,
|
||||
});
|
||||
|
||||
// The pyramid of textures gets flattened onto the source texture.
|
||||
const flattenPyramid = regl({
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec2 vUV;
|
||||
${vBlurPyramid
|
||||
.map((_, index) => `uniform sampler2D pyr_${index};`)
|
||||
.join("\n")}
|
||||
uniform float bloomStrength;
|
||||
void main() {
|
||||
vec4 total = vec4(0.);
|
||||
${vBlurPyramid
|
||||
.map(
|
||||
(_, 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(
|
||||
{
|
||||
primary: inputs.primary,
|
||||
bloom: output,
|
||||
},
|
||||
() => {
|
||||
for (let i = 0; i < pyramidHeight; i++) {
|
||||
const highPassFBO = highPassPyramid[i];
|
||||
const hBlurFBO = hBlurPyramid[i];
|
||||
const vBlurFBO = vBlurPyramid[i];
|
||||
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] });
|
||||
}
|
||||
|
||||
return makePass(
|
||||
{
|
||||
primary: inputs.primary,
|
||||
bloom: output
|
||||
},
|
||||
() => {
|
||||
for (let i = 0; i < pyramidHeight; i++) {
|
||||
const highPassFBO = highPassPyramid[i];
|
||||
const hBlurFBO = hBlurPyramid[i];
|
||||
const vBlurFBO = vBlurPyramid[i];
|
||||
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]
|
||||
);
|
||||
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]
|
||||
);
|
||||
};
|
||||
|
||||
357
js/config.js
357
js/config.js
@@ -1,204 +1,197 @@
|
||||
const fonts = {
|
||||
coptic: {
|
||||
glyphTexURL: "coptic_msdf.png",
|
||||
glyphSequenceLength: 32,
|
||||
glyphTextureColumns: 8
|
||||
},
|
||||
gothic: {
|
||||
glyphTexURL: "gothic_msdf.png",
|
||||
glyphSequenceLength: 27,
|
||||
glyphTextureColumns: 8
|
||||
},
|
||||
matrixcode: {
|
||||
glyphTexURL: "matrixcode_msdf.png",
|
||||
glyphSequenceLength: 57,
|
||||
glyphTextureColumns: 8
|
||||
}
|
||||
coptic: {
|
||||
glyphTexURL: "coptic_msdf.png",
|
||||
glyphSequenceLength: 32,
|
||||
glyphTextureColumns: 8,
|
||||
},
|
||||
gothic: {
|
||||
glyphTexURL: "gothic_msdf.png",
|
||||
glyphSequenceLength: 27,
|
||||
glyphTextureColumns: 8,
|
||||
},
|
||||
matrixcode: {
|
||||
glyphTexURL: "matrixcode_msdf.png",
|
||||
glyphSequenceLength: 57,
|
||||
glyphTextureColumns: 8,
|
||||
},
|
||||
};
|
||||
|
||||
const defaults = {
|
||||
backgroundColor: [0, 0, 0],
|
||||
volumetric: false,
|
||||
resurrectingCodeRatio: 0,
|
||||
animationSpeed: 1,
|
||||
forwardSpeed: 0.25,
|
||||
bloomStrength: 1,
|
||||
bloomSize: 0.6,
|
||||
highPassThreshold: 0.1,
|
||||
cycleSpeed: 1,
|
||||
cycleStyleName: "cycleFasterWhenDimmed",
|
||||
cursorEffectThreshold: 1,
|
||||
brightnessOffset: 0.0,
|
||||
brightnessMultiplier: 1.0,
|
||||
brightnessMix: 1.0,
|
||||
brightnessMinimum: 0,
|
||||
fallSpeed: 1,
|
||||
glyphEdgeCrop: 0.0,
|
||||
glyphHeightToWidth: 1,
|
||||
hasSun: false,
|
||||
hasThunder: false,
|
||||
isPolar: false,
|
||||
rippleTypeName: null,
|
||||
rippleThickness: 0.2,
|
||||
rippleScale: 30,
|
||||
rippleSpeed: 0.2,
|
||||
numColumns: 80,
|
||||
density: 1,
|
||||
paletteEntries: [
|
||||
{ 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.7], at: 0.7 },
|
||||
{ hsl: [0.3, 0.9, 0.8], at: 0.8 }
|
||||
],
|
||||
raindropLength: 1,
|
||||
slant: 0,
|
||||
resolution: 1,
|
||||
useHalfFloat: false,
|
||||
backgroundColor: [0, 0, 0],
|
||||
volumetric: false,
|
||||
resurrectingCodeRatio: 0,
|
||||
animationSpeed: 1,
|
||||
forwardSpeed: 0.25,
|
||||
bloomStrength: 1,
|
||||
bloomSize: 0.6,
|
||||
highPassThreshold: 0.1,
|
||||
cycleSpeed: 1,
|
||||
cycleStyleName: "cycleFasterWhenDimmed",
|
||||
cursorEffectThreshold: 1,
|
||||
brightnessOffset: 0.0,
|
||||
brightnessMultiplier: 1.0,
|
||||
brightnessMix: 1.0,
|
||||
brightnessMinimum: 0,
|
||||
fallSpeed: 1,
|
||||
glyphEdgeCrop: 0.0,
|
||||
glyphHeightToWidth: 1,
|
||||
hasSun: false,
|
||||
hasThunder: false,
|
||||
isPolar: false,
|
||||
rippleTypeName: null,
|
||||
rippleThickness: 0.2,
|
||||
rippleScale: 30,
|
||||
rippleSpeed: 0.2,
|
||||
numColumns: 80,
|
||||
density: 1,
|
||||
paletteEntries: [
|
||||
{ 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.7], at: 0.7 },
|
||||
{ hsl: [0.3, 0.9, 0.8], at: 0.8 },
|
||||
],
|
||||
raindropLength: 1,
|
||||
slant: 0,
|
||||
resolution: 1,
|
||||
useHalfFloat: false,
|
||||
};
|
||||
|
||||
const versions = {
|
||||
classic: {
|
||||
...defaults,
|
||||
...fonts.matrixcode
|
||||
},
|
||||
operator: {
|
||||
...defaults,
|
||||
...fonts.matrixcode,
|
||||
bloomStrength: 0.75,
|
||||
highPassThreshold: 0.0,
|
||||
cycleSpeed: 0.05,
|
||||
cycleStyleName: "cycleRandomly",
|
||||
cursorEffectThreshold: 0.64,
|
||||
brightnessOffset: 0.25,
|
||||
brightnessMultiplier: 0.0,
|
||||
brightnessMinimum: -1.0,
|
||||
fallSpeed: 0.65,
|
||||
glyphEdgeCrop: 0.15,
|
||||
glyphHeightToWidth: 1.35,
|
||||
rippleTypeName: "box",
|
||||
numColumns: 108,
|
||||
paletteEntries: [
|
||||
{ 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, 1.0], at: 1.0 }
|
||||
],
|
||||
raindropLength: 1.5
|
||||
},
|
||||
nightmare: {
|
||||
...defaults,
|
||||
...fonts.gothic,
|
||||
highPassThreshold: 0.7,
|
||||
brightnessMix: 0.75,
|
||||
fallSpeed: 2.0,
|
||||
hasThunder: true,
|
||||
numColumns: 60,
|
||||
paletteEntries: [
|
||||
{ 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.4], at: 0.4 },
|
||||
{ hsl: [0.1, 1.0, 0.7], at: 0.7 },
|
||||
{ hsl: [0.2, 1.0, 1.0], at: 1.0 }
|
||||
],
|
||||
raindropLength: 0.6,
|
||||
slant: (22.5 * Math.PI) / 180
|
||||
},
|
||||
paradise: {
|
||||
...defaults,
|
||||
...fonts.coptic,
|
||||
bloomStrength: 1.75,
|
||||
highPassThreshold: 0,
|
||||
cycleSpeed: 0.1,
|
||||
brightnessMix: 0.05,
|
||||
fallSpeed: 0.08,
|
||||
hasSun: true,
|
||||
isPolar: true,
|
||||
rippleTypeName: "circle",
|
||||
rippleSpeed: 0.1,
|
||||
numColumns: 30,
|
||||
paletteEntries: [
|
||||
{ hsl: [0.0, 0.0, 0.0], at: 0.0 },
|
||||
{ hsl: [0.0, 0.8, 0.3], at: 0.3 },
|
||||
{ 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.9], at: 0.9 }
|
||||
],
|
||||
raindropLength: 0.4
|
||||
},
|
||||
resurrections: {
|
||||
...defaults,
|
||||
...fonts.matrixcode,
|
||||
resurrectingCodeRatio: 0.25,
|
||||
effect:"resurrections",
|
||||
width:100,
|
||||
volumetric:true,
|
||||
density:1.5,
|
||||
fallSpeed:1.2,
|
||||
raindropLength:1.25
|
||||
}
|
||||
classic: {
|
||||
...defaults,
|
||||
...fonts.matrixcode,
|
||||
},
|
||||
operator: {
|
||||
...defaults,
|
||||
...fonts.matrixcode,
|
||||
bloomStrength: 0.75,
|
||||
highPassThreshold: 0.0,
|
||||
cycleSpeed: 0.05,
|
||||
cycleStyleName: "cycleRandomly",
|
||||
cursorEffectThreshold: 0.64,
|
||||
brightnessOffset: 0.25,
|
||||
brightnessMultiplier: 0.0,
|
||||
brightnessMinimum: -1.0,
|
||||
fallSpeed: 0.65,
|
||||
glyphEdgeCrop: 0.15,
|
||||
glyphHeightToWidth: 1.35,
|
||||
rippleTypeName: "box",
|
||||
numColumns: 108,
|
||||
paletteEntries: [
|
||||
{ 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, 1.0], at: 1.0 },
|
||||
],
|
||||
raindropLength: 1.5,
|
||||
},
|
||||
nightmare: {
|
||||
...defaults,
|
||||
...fonts.gothic,
|
||||
highPassThreshold: 0.7,
|
||||
brightnessMix: 0.75,
|
||||
fallSpeed: 2.0,
|
||||
hasThunder: true,
|
||||
numColumns: 60,
|
||||
paletteEntries: [
|
||||
{ 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.4], at: 0.4 },
|
||||
{ hsl: [0.1, 1.0, 0.7], at: 0.7 },
|
||||
{ hsl: [0.2, 1.0, 1.0], at: 1.0 },
|
||||
],
|
||||
raindropLength: 0.6,
|
||||
slant: (22.5 * Math.PI) / 180,
|
||||
},
|
||||
paradise: {
|
||||
...defaults,
|
||||
...fonts.coptic,
|
||||
bloomStrength: 1.75,
|
||||
highPassThreshold: 0,
|
||||
cycleSpeed: 0.1,
|
||||
brightnessMix: 0.05,
|
||||
fallSpeed: 0.08,
|
||||
hasSun: true,
|
||||
isPolar: true,
|
||||
rippleTypeName: "circle",
|
||||
rippleSpeed: 0.1,
|
||||
numColumns: 30,
|
||||
paletteEntries: [
|
||||
{ hsl: [0.0, 0.0, 0.0], at: 0.0 },
|
||||
{ hsl: [0.0, 0.8, 0.3], at: 0.3 },
|
||||
{ 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.9], at: 0.9 },
|
||||
],
|
||||
raindropLength: 0.4,
|
||||
},
|
||||
resurrections: {
|
||||
...defaults,
|
||||
...fonts.matrixcode,
|
||||
resurrectingCodeRatio: 0.25,
|
||||
effect: "resurrections",
|
||||
width: 100,
|
||||
volumetric: true,
|
||||
density: 1.5,
|
||||
fallSpeed: 1.2,
|
||||
raindropLength: 1.25,
|
||||
},
|
||||
};
|
||||
versions.throwback = versions.operator;
|
||||
versions["1999"] = versions.classic;
|
||||
|
||||
const range = (f, min = -Infinity, max = Infinity) =>
|
||||
Math.max(min, Math.min(max, f));
|
||||
const nullNaN = f => (isNaN(f) ? null : f);
|
||||
const range = (f, min = -Infinity, max = Infinity) => Math.max(min, Math.min(max, f));
|
||||
const nullNaN = (f) => (isNaN(f) ? null : f);
|
||||
|
||||
const paramMapping = {
|
||||
version: { key: "version", parser: s => s },
|
||||
effect: { key: "effect", parser: s => s },
|
||||
width: { 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)) },
|
||||
resolution: { key: "resolution", parser: s => nullNaN(parseFloat(s)) },
|
||||
animationSpeed: {
|
||||
key: "animationSpeed",
|
||||
parser: s => nullNaN(parseFloat(s))
|
||||
},
|
||||
forwardSpeed: {
|
||||
key: "forwardSpeed",
|
||||
parser: s => nullNaN(parseFloat(s))
|
||||
},
|
||||
cycleSpeed: { key: "cycleSpeed", parser: s => nullNaN(parseFloat(s)) },
|
||||
fallSpeed: { key: "fallSpeed", parser: s => nullNaN(parseFloat(s)) },
|
||||
raindropLength: {
|
||||
key: "raindropLength",
|
||||
parser: s => nullNaN(parseFloat(s))
|
||||
},
|
||||
slant: {
|
||||
key: "slant",
|
||||
parser: s => nullNaN((parseFloat(s) * Math.PI) / 180)
|
||||
},
|
||||
bloomSize: {
|
||||
key: "bloomSize",
|
||||
parser: s => nullNaN(range(parseFloat(s), 0, 1))
|
||||
},
|
||||
url: { key: "bgURL", parser: s => s },
|
||||
stripeColors: { key: "stripeColors", parser: s => s },
|
||||
backgroundColor: { key: "backgroundColor", parser: s => s.split(",").map(parseFloat) },
|
||||
volumetric: { key: "volumetric", parser: s => s.toLowerCase().includes("true") }
|
||||
version: { key: "version", parser: (s) => s },
|
||||
effect: { key: "effect", parser: (s) => s },
|
||||
width: { 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)) },
|
||||
resolution: { key: "resolution", parser: (s) => nullNaN(parseFloat(s)) },
|
||||
animationSpeed: {
|
||||
key: "animationSpeed",
|
||||
parser: (s) => nullNaN(parseFloat(s)),
|
||||
},
|
||||
forwardSpeed: {
|
||||
key: "forwardSpeed",
|
||||
parser: (s) => nullNaN(parseFloat(s)),
|
||||
},
|
||||
cycleSpeed: { key: "cycleSpeed", parser: (s) => nullNaN(parseFloat(s)) },
|
||||
fallSpeed: { key: "fallSpeed", parser: (s) => nullNaN(parseFloat(s)) },
|
||||
raindropLength: {
|
||||
key: "raindropLength",
|
||||
parser: (s) => nullNaN(parseFloat(s)),
|
||||
},
|
||||
slant: {
|
||||
key: "slant",
|
||||
parser: (s) => nullNaN((parseFloat(s) * Math.PI) / 180),
|
||||
},
|
||||
bloomSize: {
|
||||
key: "bloomSize",
|
||||
parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
|
||||
},
|
||||
url: { key: "bgURL", parser: (s) => s },
|
||||
stripeColors: { key: "stripeColors", parser: (s) => s },
|
||||
backgroundColor: { key: "backgroundColor", parser: (s) => s.split(",").map(parseFloat) },
|
||||
volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") },
|
||||
};
|
||||
paramMapping.dropLength = paramMapping.raindropLength;
|
||||
paramMapping.angle = paramMapping.slant;
|
||||
paramMapping.colors = paramMapping.stripeColors;
|
||||
|
||||
export default (searchString, make1DTexture) => {
|
||||
const urlParams = Object.fromEntries(
|
||||
Array.from(new URLSearchParams(searchString).entries())
|
||||
.filter(([key]) => key in paramMapping)
|
||||
.map(([key, value]) => [
|
||||
paramMapping[key].key,
|
||||
paramMapping[key].parser(value)
|
||||
])
|
||||
.filter(([_, value]) => value != null)
|
||||
);
|
||||
const urlParams = Object.fromEntries(
|
||||
Array.from(new URLSearchParams(searchString).entries())
|
||||
.filter(([key]) => key in paramMapping)
|
||||
.map(([key, value]) => [paramMapping[key].key, paramMapping[key].parser(value)])
|
||||
.filter(([_, value]) => value != null)
|
||||
);
|
||||
|
||||
const version =
|
||||
urlParams.version in versions
|
||||
? versions[urlParams.version]
|
||||
: versions.classic;
|
||||
const version = urlParams.version in versions ? versions[urlParams.version] : versions.classic;
|
||||
|
||||
return {
|
||||
...version,
|
||||
...urlParams
|
||||
};
|
||||
return {
|
||||
...version,
|
||||
...urlParams,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { loadImage, loadText, makePassFBO, makePass } from "./utils.js";
|
||||
|
||||
const defaultBGURL =
|
||||
"https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg";
|
||||
const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg";
|
||||
|
||||
export default (regl, config, inputs) => {
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
|
||||
const background = loadImage(regl, bgURL);
|
||||
const imagePassFrag = loadText("../shaders/imagePass.frag");
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
uniforms: {
|
||||
backgroundTex: background.texture,
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom
|
||||
},
|
||||
framebuffer: output
|
||||
});
|
||||
return makePass(
|
||||
{
|
||||
primary: output
|
||||
},
|
||||
() => render({frag: imagePassFrag.text()}),
|
||||
null,
|
||||
[background.loaded, imagePassFrag.loaded]
|
||||
);
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
|
||||
const background = loadImage(regl, bgURL);
|
||||
const imagePassFrag = loadText("../shaders/imagePass.frag");
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
uniforms: {
|
||||
backgroundTex: background.texture,
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
},
|
||||
framebuffer: output,
|
||||
});
|
||||
return makePass(
|
||||
{
|
||||
primary: output,
|
||||
},
|
||||
() => render({ frag: imagePassFrag.text() }),
|
||||
null,
|
||||
[background.loaded, imagePassFrag.loaded]
|
||||
);
|
||||
};
|
||||
|
||||
98
js/main.js
98
js/main.js
@@ -9,30 +9,26 @@ import makeResurrectionPass from "./resurrectionPass.js";
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
document.body.appendChild(canvas);
|
||||
document.addEventListener("touchmove", e => e.preventDefault(), {
|
||||
passive: false
|
||||
document.addEventListener("touchmove", (e) => e.preventDefault(), {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
const regl = createREGL({
|
||||
canvas,
|
||||
extensions: ["OES_texture_half_float", "OES_texture_half_float_linear"],
|
||||
// These extensions are also needed, but Safari misreports that they are missing
|
||||
optionalExtensions: [
|
||||
"EXT_color_buffer_half_float",
|
||||
"WEBGL_color_buffer_float",
|
||||
"OES_standard_derivatives"
|
||||
]
|
||||
canvas,
|
||||
extensions: ["OES_texture_half_float", "OES_texture_half_float_linear"],
|
||||
// These extensions are also needed, but Safari misreports that they are missing
|
||||
optionalExtensions: ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"],
|
||||
});
|
||||
|
||||
const effects = {
|
||||
none: null,
|
||||
plain: makePalettePass,
|
||||
customStripes: makeStripePass,
|
||||
stripes: makeStripePass,
|
||||
pride: makeStripePass,
|
||||
image: makeImagePass,
|
||||
resurrection: makeResurrectionPass,
|
||||
resurrections: makeResurrectionPass
|
||||
none: null,
|
||||
plain: makePalettePass,
|
||||
customStripes: makeStripePass,
|
||||
stripes: makeStripePass,
|
||||
pride: makeStripePass,
|
||||
image: makeImagePass,
|
||||
resurrection: makeResurrectionPass,
|
||||
resurrections: makeResurrectionPass,
|
||||
};
|
||||
|
||||
const config = makeConfig(window.location.search);
|
||||
@@ -40,8 +36,8 @@ const resolution = config.resolution;
|
||||
const effect = config.effect in effects ? config.effect : "plain";
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = Math.ceil(canvas.clientWidth * resolution);
|
||||
canvas.height = Math.ceil(canvas.clientHeight * resolution);
|
||||
canvas.width = Math.ceil(canvas.clientWidth * resolution);
|
||||
canvas.height = Math.ceil(canvas.clientHeight * resolution);
|
||||
};
|
||||
window.onresize = resize;
|
||||
resize();
|
||||
@@ -49,41 +45,29 @@ resize();
|
||||
const dimensions = { width: 1, height: 1 };
|
||||
|
||||
document.body.onload = async () => {
|
||||
// All this takes place in a full screen quad.
|
||||
const fullScreenQuad = makeFullScreenQuad(regl);
|
||||
const pipeline = makePipeline(
|
||||
[
|
||||
makeMatrixRenderer,
|
||||
effect === "none" ? null : makeBloomPass,
|
||||
effects[effect]
|
||||
],
|
||||
p => p.outputs,
|
||||
regl,
|
||||
config
|
||||
);
|
||||
const drawToScreen = regl({
|
||||
uniforms: {
|
||||
tex: pipeline[pipeline.length - 1].outputs.primary
|
||||
}
|
||||
});
|
||||
await Promise.all(pipeline.map(({ ready }) => ready));
|
||||
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
|
||||
// tick.cancel();
|
||||
if (
|
||||
dimensions.width !== viewportWidth ||
|
||||
dimensions.height !== viewportHeight
|
||||
) {
|
||||
dimensions.width = viewportWidth;
|
||||
dimensions.height = viewportHeight;
|
||||
for (const step of pipeline) {
|
||||
step.resize(viewportWidth, viewportHeight);
|
||||
}
|
||||
}
|
||||
fullScreenQuad(() => {
|
||||
for (const step of pipeline) {
|
||||
step.render();
|
||||
}
|
||||
drawToScreen();
|
||||
});
|
||||
});
|
||||
// All this takes place in a full screen quad.
|
||||
const fullScreenQuad = makeFullScreenQuad(regl);
|
||||
const pipeline = makePipeline([makeMatrixRenderer, effect === "none" ? null : makeBloomPass, effects[effect]], (p) => p.outputs, regl, config);
|
||||
const drawToScreen = regl({
|
||||
uniforms: {
|
||||
tex: pipeline[pipeline.length - 1].outputs.primary,
|
||||
},
|
||||
});
|
||||
await Promise.all(pipeline.map(({ ready }) => ready));
|
||||
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
|
||||
// tick.cancel();
|
||||
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
|
||||
dimensions.width = viewportWidth;
|
||||
dimensions.height = viewportHeight;
|
||||
for (const step of pipeline) {
|
||||
step.resize(viewportWidth, viewportHeight);
|
||||
}
|
||||
}
|
||||
fullScreenQuad(() => {
|
||||
for (const step of pipeline) {
|
||||
step.render();
|
||||
}
|
||||
drawToScreen();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,51 +1,49 @@
|
||||
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
|
||||
|
||||
const colorToRGB = ([hue, saturation, lightness]) => {
|
||||
const a = saturation * Math.min(lightness, 1 - lightness);
|
||||
const f = (n) => {
|
||||
const k = (n + hue * 12) % 12;
|
||||
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
};
|
||||
return [f(0), f(8), f(4)];
|
||||
const a = saturation * Math.min(lightness, 1 - lightness);
|
||||
const f = (n) => {
|
||||
const k = (n + hue * 12) % 12;
|
||||
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
};
|
||||
return [f(0), f(8), f(4)];
|
||||
};
|
||||
|
||||
const makePalette = (regl, entries) => {
|
||||
const PALETTE_SIZE = 2048;
|
||||
const paletteColors = Array(PALETTE_SIZE);
|
||||
const sortedEntries = entries
|
||||
.slice()
|
||||
.sort((e1, e2) => e1.at - e2.at)
|
||||
.map(entry => ({
|
||||
rgb: colorToRGB(entry.hsl),
|
||||
arrayIndex: Math.floor(
|
||||
Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)
|
||||
)
|
||||
}));
|
||||
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
|
||||
sortedEntries.push({
|
||||
rgb: sortedEntries[sortedEntries.length - 1].rgb,
|
||||
arrayIndex: PALETTE_SIZE - 1
|
||||
});
|
||||
sortedEntries.forEach((entry, index) => {
|
||||
paletteColors[entry.arrayIndex] = entry.rgb.slice();
|
||||
if (index + 1 < sortedEntries.length) {
|
||||
const nextEntry = sortedEntries[index + 1];
|
||||
const diff = nextEntry.arrayIndex - entry.arrayIndex;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
const ratio = i / diff;
|
||||
paletteColors[entry.arrayIndex + i] = [
|
||||
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
|
||||
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
|
||||
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
const PALETTE_SIZE = 2048;
|
||||
const paletteColors = Array(PALETTE_SIZE);
|
||||
const sortedEntries = entries
|
||||
.slice()
|
||||
.sort((e1, e2) => e1.at - e2.at)
|
||||
.map((entry) => ({
|
||||
rgb: colorToRGB(entry.hsl),
|
||||
arrayIndex: Math.floor(Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)),
|
||||
}));
|
||||
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
|
||||
sortedEntries.push({
|
||||
rgb: sortedEntries[sortedEntries.length - 1].rgb,
|
||||
arrayIndex: PALETTE_SIZE - 1,
|
||||
});
|
||||
sortedEntries.forEach((entry, index) => {
|
||||
paletteColors[entry.arrayIndex] = entry.rgb.slice();
|
||||
if (index + 1 < sortedEntries.length) {
|
||||
const nextEntry = sortedEntries[index + 1];
|
||||
const diff = nextEntry.arrayIndex - entry.arrayIndex;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
const ratio = i / diff;
|
||||
paletteColors[entry.arrayIndex + i] = [
|
||||
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
|
||||
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
|
||||
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return make1DTexture(
|
||||
regl,
|
||||
paletteColors.flat().map(i => i * 0xff)
|
||||
);
|
||||
return make1DTexture(
|
||||
regl,
|
||||
paletteColors.flat().map((i) => i * 0xff)
|
||||
);
|
||||
};
|
||||
|
||||
// The rendered texture's values are mapped to colors in a palette texture.
|
||||
@@ -55,32 +53,30 @@ const makePalette = (regl, entries) => {
|
||||
// in screen space.
|
||||
|
||||
export default (regl, config, inputs) => {
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
const palette = makePalette(regl, config.paletteEntries);
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
const palette = makePalette(regl, config.paletteEntries);
|
||||
|
||||
const palettePassFrag = loadText("../shaders/palettePass.frag");
|
||||
const palettePassFrag = loadText("../shaders/palettePass.frag");
|
||||
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
|
||||
uniforms: {
|
||||
...extractEntries(config, [
|
||||
"backgroundColor",
|
||||
]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
palette,
|
||||
ditherMagnitude: 0.05
|
||||
},
|
||||
framebuffer: output
|
||||
});
|
||||
uniforms: {
|
||||
...extractEntries(config, ["backgroundColor"]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
palette,
|
||||
ditherMagnitude: 0.05,
|
||||
},
|
||||
framebuffer: output,
|
||||
});
|
||||
|
||||
return makePass(
|
||||
{
|
||||
primary: output
|
||||
},
|
||||
() => render({ frag: palettePassFrag.text() }),
|
||||
null,
|
||||
palettePassFrag.loaded
|
||||
);
|
||||
return makePass(
|
||||
{
|
||||
primary: output,
|
||||
},
|
||||
() => render({ frag: palettePassFrag.text() }),
|
||||
null,
|
||||
palettePassFrag.loaded
|
||||
);
|
||||
};
|
||||
|
||||
307
js/renderer.js
307
js/renderer.js
@@ -1,187 +1,180 @@
|
||||
import {
|
||||
extractEntries,
|
||||
loadImage,
|
||||
loadText,
|
||||
makePassFBO,
|
||||
makeDoubleBuffer,
|
||||
makePass
|
||||
} from "./utils.js";
|
||||
import { extractEntries, loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";
|
||||
|
||||
const rippleTypes = {
|
||||
box: 0,
|
||||
circle: 1
|
||||
box: 0,
|
||||
circle: 1,
|
||||
};
|
||||
|
||||
const cycleStyles = {
|
||||
cycleFasterWhenDimmed: 0,
|
||||
cycleRandomly: 1
|
||||
cycleFasterWhenDimmed: 0,
|
||||
cycleRandomly: 1,
|
||||
};
|
||||
|
||||
const numVerticesPerQuad = 2 * 3;
|
||||
|
||||
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;
|
||||
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];
|
||||
// These two framebuffers are used to compute the raining code.
|
||||
// they take turns being the source and destination of the "compute" shader.
|
||||
// The half float data type is crucial! It lets us store almost any real number,
|
||||
// whereas the default type limits us to integers between 0 and 255.
|
||||
|
||||
// These two framebuffers are used to compute the raining code.
|
||||
// they take turns being the source and destination of the "compute" shader.
|
||||
// The half float data type is crucial! It lets us store almost any real number,
|
||||
// whereas the default type limits us to integers between 0 and 255.
|
||||
// This double buffer is smaller than the screen, because its pixels correspond
|
||||
// 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",
|
||||
});
|
||||
|
||||
// This double buffer is smaller than the screen, because its pixels correspond
|
||||
// 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 output = makePassFBO(regl, config.useHalfFloat);
|
||||
|
||||
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 = {
|
||||
...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 msdf = loadImage(regl, config.glyphTexURL);
|
||||
|
||||
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 updateFrag = loadText("../shaders/update.frag");
|
||||
const update = regl({
|
||||
frag: regl.prop("frag"),
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
lastState: doubleBuffer.back,
|
||||
},
|
||||
|
||||
const msdf = loadImage(regl, config.glyphTexURL);
|
||||
framebuffer: doubleBuffer.front,
|
||||
});
|
||||
|
||||
const updateFrag = loadText("../shaders/update.frag");
|
||||
const update = regl({
|
||||
frag: regl.prop("frag"),
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
lastState: doubleBuffer.back
|
||||
},
|
||||
const quadPositions = Array(numQuadRows)
|
||||
.fill()
|
||||
.map((_, y) =>
|
||||
Array(numQuadColumns)
|
||||
.fill()
|
||||
.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) =>
|
||||
Array(numQuadColumns).fill().map((_, x) =>
|
||||
Array(numVerticesPerQuad).fill([x, y])
|
||||
)
|
||||
);
|
||||
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
|
||||
const renderVert = loadText("../shaders/render.vert");
|
||||
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
|
||||
const renderVert = loadText("../shaders/render.vert");
|
||||
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"),
|
||||
lastState: doubleBuffer.front,
|
||||
glyphTex: msdf.texture,
|
||||
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
camera: regl.prop("camera"),
|
||||
transform: regl.prop("transform"),
|
||||
screenSize: regl.prop("screenSize"),
|
||||
},
|
||||
|
||||
lastState: doubleBuffer.front,
|
||||
glyphTex: msdf.texture,
|
||||
attributes: {
|
||||
aPosition: quadPositions,
|
||||
aCorner: quadCorners,
|
||||
},
|
||||
count: numQuads * numVerticesPerQuad,
|
||||
|
||||
camera: regl.prop("camera"),
|
||||
transform: regl.prop("transform"),
|
||||
screenSize: regl.prop("screenSize")
|
||||
},
|
||||
framebuffer: output,
|
||||
});
|
||||
|
||||
attributes: {
|
||||
aPosition: quadPositions,
|
||||
aCorner: quadCorners
|
||||
},
|
||||
count: numQuads * numVerticesPerQuad,
|
||||
const screenSize = [1, 1];
|
||||
const { mat4, vec3 } = glMatrix;
|
||||
const camera = mat4.create();
|
||||
const translation = vec3.set(vec3.create(), 0, 0.5 / numRows, -1);
|
||||
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];
|
||||
const {mat4, vec3} = glMatrix;
|
||||
const camera = mat4.create();
|
||||
const translation = vec3.set(vec3.create(), 0, 0.5 / numRows, -1);
|
||||
const scale = vec3.set(vec3.create(), 1, 1, 1);
|
||||
const transform = mat4.create();
|
||||
mat4.translate(transform, transform, translation);
|
||||
mat4.scale(transform, transform, scale);
|
||||
|
||||
return makePass(
|
||||
{
|
||||
primary: output
|
||||
},
|
||||
() => {
|
||||
const time = Date.now();
|
||||
|
||||
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]
|
||||
);
|
||||
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]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
|
||||
|
||||
const colorToRGB = ([hue, saturation, lightness]) => {
|
||||
const a = saturation * Math.min(lightness, 1 - lightness);
|
||||
const f = (n) => {
|
||||
const k = (n + hue * 12) % 12;
|
||||
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
};
|
||||
return [f(0), f(8), f(4)];
|
||||
const a = saturation * Math.min(lightness, 1 - lightness);
|
||||
const f = (n) => {
|
||||
const k = (n + hue * 12) % 12;
|
||||
return lightness - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
||||
};
|
||||
return [f(0), f(8), f(4)];
|
||||
};
|
||||
|
||||
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({
|
||||
frag: regl.prop("frag"),
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
|
||||
uniforms: {
|
||||
...extractEntries(config, [
|
||||
"backgroundColor",
|
||||
]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
ditherMagnitude: 0.05
|
||||
},
|
||||
framebuffer: output
|
||||
});
|
||||
uniforms: {
|
||||
...extractEntries(config, ["backgroundColor"]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
ditherMagnitude: 0.05,
|
||||
},
|
||||
framebuffer: output,
|
||||
});
|
||||
|
||||
return makePass(
|
||||
{
|
||||
primary: output
|
||||
},
|
||||
() => render({frag: resurrectionPassFrag.text() })
|
||||
);
|
||||
return makePass(
|
||||
{
|
||||
primary: output,
|
||||
},
|
||||
() => render({ frag: resurrectionPassFrag.text() })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,61 +1,55 @@
|
||||
import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js";
|
||||
|
||||
const neapolitanStripeColors = [
|
||||
[0.4, 0.15, 0.1],
|
||||
[0.4, 0.15, 0.1],
|
||||
[0.8, 0.8, 0.6],
|
||||
[0.8, 0.8, 0.6],
|
||||
[1.0, 0.7, 0.8],
|
||||
[1.0, 0.7, 0.8]
|
||||
[0.4, 0.15, 0.1],
|
||||
[0.4, 0.15, 0.1],
|
||||
[0.8, 0.8, 0.6],
|
||||
[0.8, 0.8, 0.6],
|
||||
[1.0, 0.7, 0.8],
|
||||
[1.0, 0.7, 0.8],
|
||||
].flat();
|
||||
|
||||
const prideStripeColors = [
|
||||
[1, 0, 0],
|
||||
[1, 0.5, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
[0.8, 0, 1]
|
||||
[1, 0, 0],
|
||||
[1, 0.5, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1],
|
||||
[0.8, 0, 1],
|
||||
].flat();
|
||||
|
||||
export default (regl, config, inputs) => {
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
const output = makePassFBO(regl, config.useHalfFloat);
|
||||
|
||||
const stripeColors =
|
||||
"stripeColors" in config
|
||||
? config.stripeColors.split(",").map(parseFloat)
|
||||
: config.effect === "pride"
|
||||
? prideStripeColors
|
||||
: neapolitanStripeColors;
|
||||
const numStripeColors = Math.floor(stripeColors.length / 3);
|
||||
const stripes = make1DTexture(
|
||||
regl,
|
||||
stripeColors.slice(0, numStripeColors * 3).map(f => Math.floor(f * 0xff))
|
||||
);
|
||||
const stripeColors =
|
||||
"stripeColors" in config ? config.stripeColors.split(",").map(parseFloat) : config.effect === "pride" ? prideStripeColors : neapolitanStripeColors;
|
||||
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({
|
||||
frag: regl.prop("frag"),
|
||||
const render = regl({
|
||||
frag: regl.prop("frag"),
|
||||
|
||||
uniforms: {
|
||||
...extractEntries(config, [
|
||||
"backgroundColor",
|
||||
]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
stripes,
|
||||
ditherMagnitude: 0.05
|
||||
},
|
||||
framebuffer: output
|
||||
});
|
||||
uniforms: {
|
||||
...extractEntries(config, ["backgroundColor"]),
|
||||
tex: inputs.primary,
|
||||
bloomTex: inputs.bloom,
|
||||
stripes,
|
||||
ditherMagnitude: 0.05,
|
||||
},
|
||||
framebuffer: output,
|
||||
});
|
||||
|
||||
return makePass(
|
||||
{
|
||||
primary: output
|
||||
},
|
||||
() => render({frag: stripePassFrag.text()}),
|
||||
null,
|
||||
stripePassFrag.loaded
|
||||
);
|
||||
return makePass(
|
||||
{
|
||||
primary: output,
|
||||
},
|
||||
() => render({ frag: stripePassFrag.text() }),
|
||||
null,
|
||||
stripePassFrag.loaded
|
||||
);
|
||||
};
|
||||
|
||||
304
js/utils.js
304
js/utils.js
@@ -1,128 +1,120 @@
|
||||
const extractEntries = (src, keys) =>
|
||||
Object.fromEntries(
|
||||
Array.from(Object.entries(src)).filter(([key]) => keys.includes(key))
|
||||
);
|
||||
const extractEntries = (src, keys) => Object.fromEntries(Array.from(Object.entries(src)).filter(([key]) => keys.includes(key)));
|
||||
|
||||
const makePassTexture = (regl, halfFloat) =>
|
||||
regl.texture({
|
||||
width: 1,
|
||||
height: 1,
|
||||
type: halfFloat ? "half float" : "uint8",
|
||||
wrap: "clamp",
|
||||
min: "linear",
|
||||
mag: "linear"
|
||||
});
|
||||
regl.texture({
|
||||
width: 1,
|
||||
height: 1,
|
||||
type: halfFloat ? "half float" : "uint8",
|
||||
wrap: "clamp",
|
||||
min: "linear",
|
||||
mag: "linear",
|
||||
});
|
||||
|
||||
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
|
||||
// and half the height of the FBO below it.
|
||||
const makePyramid = (regl, height, halfFloat) =>
|
||||
Array(height)
|
||||
.fill()
|
||||
.map(_ => makePassFBO(regl, halfFloat));
|
||||
Array(height)
|
||||
.fill()
|
||||
.map((_) => makePassFBO(regl, halfFloat));
|
||||
|
||||
const makeDoubleBuffer = (regl, props) => {
|
||||
const state = Array(2)
|
||||
.fill()
|
||||
.map(() =>
|
||||
regl.framebuffer({
|
||||
color: regl.texture(props),
|
||||
depthStencil: false
|
||||
})
|
||||
);
|
||||
return {
|
||||
front: ({ tick }) => state[tick % 2],
|
||||
back: ({ tick }) => state[(tick + 1) % 2]
|
||||
};
|
||||
const state = Array(2)
|
||||
.fill()
|
||||
.map(() =>
|
||||
regl.framebuffer({
|
||||
color: regl.texture(props),
|
||||
depthStencil: false,
|
||||
})
|
||||
);
|
||||
return {
|
||||
front: ({ tick }) => state[tick % 2],
|
||||
back: ({ tick }) => state[(tick + 1) % 2],
|
||||
};
|
||||
};
|
||||
|
||||
const resizePyramid = (pyramid, vw, vh, scale) =>
|
||||
pyramid.forEach((fbo, index) =>
|
||||
fbo.resize(
|
||||
Math.floor((vw * scale) / 2 ** index),
|
||||
Math.floor((vh * scale) / 2 ** index)
|
||||
)
|
||||
);
|
||||
pyramid.forEach((fbo, index) => fbo.resize(Math.floor((vw * scale) / 2 ** index), Math.floor((vh * scale) / 2 ** index)));
|
||||
|
||||
const loadImage = (regl, url) => {
|
||||
let texture = regl.texture([[0]]);
|
||||
let loaded = false;
|
||||
return {
|
||||
texture: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`texture still loading: ${url}`);
|
||||
}
|
||||
return texture;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
const data = new Image();
|
||||
data.crossOrigin = "anonymous";
|
||||
data.src = url;
|
||||
await data.decode();
|
||||
loaded = true;
|
||||
texture = regl.texture({
|
||||
data,
|
||||
mag: "linear",
|
||||
min: "linear",
|
||||
flipY: true
|
||||
});
|
||||
}
|
||||
})()
|
||||
};
|
||||
let texture = regl.texture([[0]]);
|
||||
let loaded = false;
|
||||
return {
|
||||
texture: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`texture still loading: ${url}`);
|
||||
}
|
||||
return texture;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
const data = new Image();
|
||||
data.crossOrigin = "anonymous";
|
||||
data.src = url;
|
||||
await data.decode();
|
||||
loaded = true;
|
||||
texture = regl.texture({
|
||||
data,
|
||||
mag: "linear",
|
||||
min: "linear",
|
||||
flipY: true,
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
};
|
||||
|
||||
const loadShader = (regl, url) => {
|
||||
let texture = regl.texture([[0]]);
|
||||
let loaded = false;
|
||||
return {
|
||||
texture: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`texture still loading: ${url}`);
|
||||
}
|
||||
return texture;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
const data = new Image();
|
||||
data.crossOrigin = "anonymous";
|
||||
data.src = url;
|
||||
await data.decode();
|
||||
loaded = true;
|
||||
texture = regl.texture({
|
||||
data,
|
||||
mag: "linear",
|
||||
min: "linear",
|
||||
flipY: true
|
||||
});
|
||||
}
|
||||
})()
|
||||
};
|
||||
let texture = regl.texture([[0]]);
|
||||
let loaded = false;
|
||||
return {
|
||||
texture: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`texture still loading: ${url}`);
|
||||
}
|
||||
return texture;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
const data = new Image();
|
||||
data.crossOrigin = "anonymous";
|
||||
data.src = url;
|
||||
await data.decode();
|
||||
loaded = true;
|
||||
texture = regl.texture({
|
||||
data,
|
||||
mag: "linear",
|
||||
min: "linear",
|
||||
flipY: true,
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
};
|
||||
|
||||
const loadText = (url) => {
|
||||
let text = "";
|
||||
let loaded = false;
|
||||
return {
|
||||
text: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`text still loading: ${url}`);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
text = await (await fetch(url)).text();
|
||||
loaded = true;
|
||||
}
|
||||
})()
|
||||
};
|
||||
let text = "";
|
||||
let loaded = false;
|
||||
return {
|
||||
text: () => {
|
||||
if (!loaded) {
|
||||
console.warn(`text still loading: ${url}`);
|
||||
}
|
||||
return text;
|
||||
},
|
||||
loaded: (async () => {
|
||||
if (url != null) {
|
||||
text = await (await fetch(url)).text();
|
||||
loaded = true;
|
||||
}
|
||||
})(),
|
||||
};
|
||||
};
|
||||
|
||||
const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
|
||||
regl({
|
||||
vert: `
|
||||
regl({
|
||||
vert: `
|
||||
precision mediump float;
|
||||
attribute vec2 aPosition;
|
||||
varying vec2 vUV;
|
||||
@@ -132,7 +124,7 @@ const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
|
||||
}
|
||||
`,
|
||||
|
||||
frag: `
|
||||
frag: `
|
||||
precision mediump float;
|
||||
varying vec2 vUV;
|
||||
uniform sampler2D tex;
|
||||
@@ -141,75 +133,65 @@ const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
|
||||
}
|
||||
`,
|
||||
|
||||
attributes: {
|
||||
aPosition: [-4, -4, 4, -4, 0, 4]
|
||||
},
|
||||
count: 3,
|
||||
attributes: {
|
||||
aPosition: [-4, -4, 4, -4, 0, 4],
|
||||
},
|
||||
count: 3,
|
||||
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
time: regl.context("time")
|
||||
},
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
time: regl.context("time"),
|
||||
},
|
||||
|
||||
context,
|
||||
context,
|
||||
|
||||
depth: { enable: false },
|
||||
|
||||
});
|
||||
depth: { enable: false },
|
||||
});
|
||||
|
||||
const make1DTexture = (regl, data) =>
|
||||
regl.texture({
|
||||
data,
|
||||
width: data.length / 3,
|
||||
height: 1,
|
||||
format: "rgb",
|
||||
mag: "linear",
|
||||
min: "linear"
|
||||
});
|
||||
regl.texture({
|
||||
data,
|
||||
width: data.length / 3,
|
||||
height: 1,
|
||||
format: "rgb",
|
||||
mag: "linear",
|
||||
min: "linear",
|
||||
});
|
||||
|
||||
const makePass = (outputs, render, resize, ready) => {
|
||||
if (render == null) {
|
||||
render = () => {};
|
||||
}
|
||||
if (resize == null) {
|
||||
resize = (w, h) =>
|
||||
Object.values(outputs).forEach(output => output.resize(w, h));
|
||||
}
|
||||
if (ready == null) {
|
||||
ready = Promise.resolve();
|
||||
} else if (ready instanceof Array) {
|
||||
ready = Promise.all(ready);
|
||||
}
|
||||
return {
|
||||
outputs,
|
||||
render,
|
||||
resize,
|
||||
ready
|
||||
};
|
||||
if (render == null) {
|
||||
render = () => {};
|
||||
}
|
||||
if (resize == null) {
|
||||
resize = (w, h) => Object.values(outputs).forEach((output) => output.resize(w, h));
|
||||
}
|
||||
if (ready == null) {
|
||||
ready = Promise.resolve();
|
||||
} else if (ready instanceof Array) {
|
||||
ready = Promise.all(ready);
|
||||
}
|
||||
return {
|
||||
outputs,
|
||||
render,
|
||||
resize,
|
||||
ready,
|
||||
};
|
||||
};
|
||||
|
||||
const makePipeline = (steps, getInputs, ...params) =>
|
||||
steps
|
||||
.filter(f => f != null)
|
||||
.reduce(
|
||||
(pipeline, f, i) => [
|
||||
...pipeline,
|
||||
f(...params, i == 0 ? null : getInputs(pipeline[i - 1]))
|
||||
],
|
||||
[]
|
||||
);
|
||||
steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(...params, i == 0 ? null : getInputs(pipeline[i - 1]))], []);
|
||||
|
||||
export {
|
||||
extractEntries,
|
||||
makePassTexture,
|
||||
makePassFBO,
|
||||
makeDoubleBuffer,
|
||||
makePyramid,
|
||||
resizePyramid,
|
||||
loadImage,
|
||||
loadText,
|
||||
makeFullScreenQuad,
|
||||
make1DTexture,
|
||||
makePass,
|
||||
makePipeline
|
||||
extractEntries,
|
||||
makePassTexture,
|
||||
makePassFBO,
|
||||
makeDoubleBuffer,
|
||||
makePyramid,
|
||||
resizePyramid,
|
||||
loadImage,
|
||||
loadText,
|
||||
makeFullScreenQuad,
|
||||
make1DTexture,
|
||||
makePass,
|
||||
makePipeline,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user