diff --git a/TODO.txt b/TODO.txt index b184889..22ecf3f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,6 +1,8 @@ TODO: Clean up config.js + Too many responsibilities + Pass-specific properties should be made into uniforms in the passes Reach out to someone about producing sounds Raindrop sound diff --git a/index.html b/index.html index c549116..3064545 100644 --- a/index.html +++ b/index.html @@ -20,90 +20,22 @@ - - + For more information, please visit: https://github.com/Rezmason/matrix + --> + + + diff --git a/js/bloomPass.js b/js/bloomPass.js index 0913a63..9de7cc3 100644 --- a/js/bloomPass.js +++ b/js/bloomPass.js @@ -10,11 +10,7 @@ const levelStrengths = Array(pyramidHeight) ) .reverse(); -export default (regl, config, input) => { - if (!config.performBloom) { - return makePass(input, null, null); - } - +export default (regl, { bloomSize }, input) => { const highPassPyramid = makePyramid(regl, pyramidHeight); const hBlurPyramid = makePyramid(regl, pyramidHeight); const vBlurPyramid = makePyramid(regl, pyramidHeight); @@ -102,7 +98,7 @@ export default (regl, config, input) => { framebuffer: output }); - const scale = config.bloomSize; + const scale = bloomSize; return makePass( output, diff --git a/js/colorPass.js b/js/colorPass.js deleted file mode 100644 index e121371..0000000 --- a/js/colorPass.js +++ /dev/null @@ -1,112 +0,0 @@ -import { makePassFBO, makePass } from "./utils.js"; - -const colorizeByPalette = (regl, uniforms, framebuffer) => - // The rendered texture's values are mapped to colors in a palette texture. - // A little noise is introduced, to hide the banding that appears - // in subtle gradients. The noise is also time-driven, so its grain - // won't persist across subsequent frames. This is a safe trick - // in screen space. - regl({ - frag: ` - precision mediump float; - #define PI 3.14159265359 - - uniform sampler2D tex; - uniform sampler2D palette; - uniform float ditherMagnitude; - uniform float time; - varying vec2 vUV; - - highp float rand( const in vec2 uv, const in float t ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c + t); - } - - void main() { - float at = texture2D( tex, vUV ).r - rand( gl_FragCoord.xy, time ) * ditherMagnitude; - gl_FragColor = texture2D( palette, vec2(at, 0.0)); - } - `, - - uniforms: { - ...uniforms, - ditherMagnitude: 0.05 - }, - framebuffer - }); - -const colorizeByStripes = (regl, uniforms, framebuffer) => - regl({ - frag: ` - precision mediump float; - #define PI 3.14159265359 - - uniform sampler2D tex; - uniform sampler2D stripes; - uniform float ditherMagnitude; - varying vec2 vUV; - - highp float rand( const in vec2 uv ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c); - } - - void main() { - vec3 color = texture2D(stripes, vUV).rgb - rand( gl_FragCoord.xy ) * ditherMagnitude; - float brightness = texture2D(tex, vUV).r; - gl_FragColor = vec4(color * brightness, 1.0); - } - `, - - uniforms: { - ...uniforms, - ditherMagnitude: 0.1 - }, - framebuffer - }); - -const colorizeByImage = (regl, uniforms, framebuffer) => - regl({ - frag: ` - precision mediump float; - uniform sampler2D tex; - uniform sampler2D bgTex; - varying vec2 vUV; - - void main() { - vec3 bgColor = texture2D(bgTex, vUV).rgb; - float brightness = pow(texture2D(tex, vUV).r, 1.5); - gl_FragColor = vec4(bgColor * brightness, 1.0); - } - `, - uniforms, - framebuffer - }); - -const colorizersByEffect = { - plain: colorizeByPalette, - customStripes: colorizeByStripes, - stripes: colorizeByStripes, - image: colorizeByImage -}; - -export default (regl, config, { bgTex }, input) => { - if (config.effect === "none") { - return makePass(input, null, null); - } - - if (bgTex == null) { - bgTex = 0; - } - - const output = makePassFBO(regl); - - return makePass( - output, - (config.effect in colorizersByEffect - ? colorizersByEffect[config.effect] - : colorizeByPalette)(regl, { bgTex, tex: input }, output) - ); -}; diff --git a/js/config.js b/js/config.js index 8285f33..7bb1894 100644 --- a/js/config.js +++ b/js/config.js @@ -171,6 +171,8 @@ const versions = { versions.throwback = versions.operator; versions["1999"] = versions.classic; +// Start here + export default (searchString, make1DTexture) => { const urlParams = new URLSearchParams(searchString); const getParam = (keyOrKeys, defaultValue) => { @@ -185,8 +187,7 @@ export default (searchString, make1DTexture) => { }; const versionName = getParam("version", "classic"); - const version = - versions[versionName] == null ? versions.classic : versions[versionName]; + const version = versions[versionName] == null ? versions.classic : versions[versionName]; const config = { ...version }; @@ -194,43 +195,19 @@ export default (searchString, make1DTexture) => { config.fallSpeed *= parseFloat(getParam("fallSpeed", 1)); config.cycleSpeed *= parseFloat(getParam("cycleSpeed", 1)); config.numColumns = parseInt(getParam("width", config.numColumns)); - config.raindropLength = parseFloat( - getParam(["raindropLength", "dropLength"], config.raindropLength) - ); + config.raindropLength = parseFloat(getParam(["raindropLength", "dropLength"], config.raindropLength)); config.glyphSequenceLength = config.glyphSequenceLength; - config.slant = - (parseFloat(getParam(["slant", "angle"], config.slant)) * Math.PI) / 180; + config.slant = (parseFloat(getParam(["slant", "angle"], config.slant)) * Math.PI) / 180; config.slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; - config.slantScale = - 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); + config.slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); config.glyphEdgeCrop = parseFloat(getParam("encroach", config.glyphEdgeCrop)); - config.glyphHeightToWidth = parseFloat( - getParam("stretch", config.glyphHeightToWidth) - ); - config.cursorEffectThreshold = getParam( - "cursorEffectThreshold", - config.cursorEffectThreshold - ); - config.bloomSize = Math.max( - 0.01, - Math.min(1, parseFloat(getParam("bloomSize", 0.5))) - ); + config.glyphHeightToWidth = parseFloat(getParam("stretch", config.glyphHeightToWidth)); + config.cursorEffectThreshold = getParam("cursorEffectThreshold", config.cursorEffectThreshold); + config.bloomSize = Math.max(0.01, Math.min(1, parseFloat(getParam("bloomSize", 0.5)))); config.effect = getParam("effect", "plain"); - config.bgURL = getParam( - "url", - "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg" - ); - config.customStripes = getParam( - "colors", - "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," - ) - .split(",") - .map(parseFloat); + config.bgURL = getParam("url", "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg"); + config.customStripes = getParam("colors", "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,").split(",").map(parseFloat); config.showComputationTexture = config.effect === "none"; - config.performBloom = - config.effect !== "none" && - config.bloomSize > 0 && - config.bloomStrength > 0; switch (config.cycleStyleName) { case "cycleFasterWhenDimmed": @@ -291,7 +268,7 @@ export default (searchString, make1DTexture) => { if (config.effect === "pride") { config.effect = "stripes"; - config.customStripes = [ + config.stripeColors = [ [1, 0, 0], [1, 0.5, 0], [1, 1, 0], @@ -302,8 +279,9 @@ export default (searchString, make1DTexture) => { } if (config.effect === "customStripes" || config.effect === "stripes") { - const numFlagColors = Math.floor(config.customStripes.length / 3); - stripeColors = config.customStripes.slice(0, numFlagColors * 3); + config.effect = "stripes"; + const numStripeColors = Math.floor(config.stripeColors.length / 3); + stripeColors = config.stripeColors.slice(0, numStripeColors * 3); } config.stripes = make1DTexture(stripeColors.map(f => Math.floor(f * 0xff))); diff --git a/js/imagePass.js b/js/imagePass.js new file mode 100644 index 0000000..f0b43b8 --- /dev/null +++ b/js/imagePass.js @@ -0,0 +1,27 @@ +import { loadImage, makePassFBO, makePass } from "./utils.js"; + +export default (regl, { bgURL }, input) => { + const output = makePassFBO(regl); + const bgLoader = loadImage(regl, bgURL); + return makePass( + output, + regl({ + frag: ` + precision mediump float; + uniform sampler2D tex; + uniform sampler2D bgTex; + varying vec2 vUV; + + void main() { + vec3 bgColor = texture2D(bgTex, vUV).rgb; + float brightness = pow(texture2D(tex, vUV).r, 1.5); + gl_FragColor = vec4(bgColor * brightness, 1.0); + } + `, + uniforms: { bgTex: bgLoader.texture, tex: input }, + framebuffer: output + }), + null, + bgLoader.ready + ); +}; diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..4d52f15 --- /dev/null +++ b/js/main.js @@ -0,0 +1,71 @@ +import { makeFullScreenQuad, make1DTexture, makePipeline } from "./utils.js"; +import makeConfig from "./config.js"; +import makeMatrixRenderer from "./renderer.js"; +import makeBloomPass from "./bloomPass.js"; +import makePalettePass from "./palettePass.js"; +import makeStripePass from "./stripePass.js"; +import makeImagePass from "./imagePass.js"; + +const canvas = document.createElement("canvas"); +document.body.appendChild(canvas); +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" + ] +}); + +const effects = { + none: null, + plain: makePalettePass, + stripes: makeStripePass, + image: makeImagePass +}; + +const [config, uniforms] = makeConfig(window.location.search, data => + make1DTexture(regl, data) +); +const effect = config.effect in effects ? config.effect : "plain"; + +const resize = () => { + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; +}; +window.onresize = resize; +resize(); + +document.body.onload = async () => { + // All this takes place in a full screen quad. + const fullScreenQuad = makeFullScreenQuad(regl, uniforms); + const pipeline = makePipeline( + [ + makeMatrixRenderer, + effect === "none" ? null : makeBloomPass, + effects[effect] + ], + p => p.output, + regl, + config + ); + const drawToScreen = regl({ + uniforms: { + tex: pipeline[pipeline.length - 1].output + } + }); + await Promise.all(pipeline.map(({ ready }) => ready)); + regl.frame(({ viewportWidth, viewportHeight }) => { + pipeline.forEach(({ resize }) => resize(viewportWidth, viewportHeight)); + fullScreenQuad(() => { + pipeline.forEach(({ render }) => render()); + drawToScreen(); + }); + }); +}; diff --git a/js/palettePass.js b/js/palettePass.js new file mode 100644 index 0000000..2ad6d41 --- /dev/null +++ b/js/palettePass.js @@ -0,0 +1,43 @@ +import { makePassFBO, makePass } from "./utils.js"; + +// The rendered texture's values are mapped to colors in a palette texture. +// A little noise is introduced, to hide the banding that appears +// in subtle gradients. The noise is also time-driven, so its grain +// won't persist across subsequent frames. This is a safe trick +// in screen space. + +export default (regl, {}, input) => { + const output = makePassFBO(regl); + return makePass( + output, + regl({ + frag: ` + precision mediump float; + #define PI 3.14159265359 + + uniform sampler2D tex; + uniform sampler2D palette; + uniform float ditherMagnitude; + uniform float time; + varying vec2 vUV; + + highp float rand( const in vec2 uv, const in float t ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c + t); + } + + void main() { + float at = texture2D( tex, vUV ).r - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + gl_FragColor = texture2D( palette, vec2(at, 0.0)); + } + `, + + uniforms: { + tex: input, + ditherMagnitude: 0.05 + }, + framebuffer: output + }) + ); +}; diff --git a/js/renderer.js b/js/renderer.js index 5325a9f..453a779 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -1,6 +1,6 @@ -import { makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; +import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; -export default (regl, config, { msdfTex }) => { +export default (regl, config) => { // 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, @@ -213,6 +213,8 @@ export default (regl, config, { msdfTex }) => { framebuffer: doubleBuffer.front }); + const msdfLoader = loadImage(regl, config.glyphTexURL); + // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen const render = regl({ vert: ` @@ -235,7 +237,7 @@ export default (regl, config, { msdfTex }) => { #endif precision lowp float; - uniform sampler2D msdfTex; + uniform sampler2D glyphTex; uniform sampler2D lastState; uniform float numColumns; uniform float glyphTextureColumns; @@ -298,7 +300,7 @@ export default (regl, config, { msdfTex }) => { vec2 msdfUV = (glyphUV + symbolUV) / glyphTextureColumns; // MSDF - vec3 dist = texture2D(msdfTex, msdfUV).rgb; + vec3 dist = texture2D(glyphTex, msdfUV).rgb; float sigDist = median3(dist) - 0.5; float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0); @@ -307,7 +309,7 @@ export default (regl, config, { msdfTex }) => { `, uniforms: { - msdfTex, + glyphTex: msdfLoader.texture, height: regl.context("viewportWidth"), width: regl.context("viewportHeight"), lastState: doubleBuffer.front @@ -316,8 +318,13 @@ export default (regl, config, { msdfTex }) => { framebuffer: output }); - return makePass(output, resources => { - update(); - render(resources); - }); + return makePass( + output, + resources => { + update(); + render(resources); + }, + null, + msdfLoader.ready + ); }; diff --git a/js/stripePass.js b/js/stripePass.js new file mode 100644 index 0000000..32ee70b --- /dev/null +++ b/js/stripePass.js @@ -0,0 +1,37 @@ +import { makePassFBO, makePass } from "./utils.js"; + +export default (regl, {}, input) => { + const output = makePassFBO(regl); + return makePass( + output, + regl({ + frag: ` + precision mediump float; + #define PI 3.14159265359 + + uniform sampler2D tex; + uniform sampler2D stripes; + uniform float ditherMagnitude; + varying vec2 vUV; + + highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c); + } + + void main() { + vec3 color = texture2D(stripes, vUV).rgb - rand( gl_FragCoord.xy ) * ditherMagnitude; + float brightness = texture2D(tex, vUV).r; + gl_FragColor = vec4(color * brightness, 1.0); + } + `, + + uniforms: { + tex: input, + ditherMagnitude: 0.1 + }, + framebuffer: output + }) + ); +}; diff --git a/js/utils.js b/js/utils.js index 5380184..8084bd1 100644 --- a/js/utils.js +++ b/js/utils.js @@ -43,11 +43,11 @@ const resizePyramid = (pyramid, vw, vh, scale) => const loadImages = async (regl, manifest) => { const keys = Object.keys(manifest); const urls = Object.values(manifest); - const images = await Promise.all(urls.map(url => loadImage(regl, url))); + const images = await Promise.all(urls.map(url => loadImageOld(regl, url))); return Object.fromEntries(images.map((image, index) => [keys[index], image])); }; -const loadImage = async (regl, url) => { +const loadImageOld = async (regl, url) => { if (url == null) { return null; } @@ -63,6 +63,34 @@ const loadImage = async (regl, url) => { }); }; +const loadImage = (regl, url) => { + let texture = regl.texture([[0]]); + let loaded = false; + return { + texture: () => { + if (!loaded) { + console.warn(`texture still loading: ${url}`); + } + return texture; + }, + ready: (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 makeFullScreenQuad = (regl, uniforms = {}, context = {}) => regl({ vert: ` @@ -109,24 +137,35 @@ const make1DTexture = (regl, data) => min: "linear" }); -const makePass = (output, render, resize) => { +const makePass = (output, render, resize, ready) => { if (render == null) { render = () => {}; } - if (resize === undefined) { - // "default" resize function is on the FBO + if (resize == null) { resize = (w, h) => output.resize(w, h); } - if (resize == null) { - resize = () => {}; + if (ready == null) { + ready = Promise.resolve(); } return { output, render, - resize + resize, + ready }; }; +const makePipeline = (steps, getInput, ...params) => + steps + .filter(f => f != null) + .reduce( + (pipeline, f, i) => [ + ...pipeline, + f(...params, i == 0 ? null : getInput(pipeline[i - 1])) + ], + [] + ); + export { makePassTexture, makePassFBO, @@ -137,5 +176,6 @@ export { loadImages, makeFullScreenQuad, make1DTexture, - makePass + makePass, + makePipeline };