diff --git a/js/Matrix.js b/js/Matrix.js index 653ac90..06d8ad9 100644 --- a/js/Matrix.js +++ b/js/Matrix.js @@ -1,5 +1,5 @@ -import React, { useEffect, useRef, memo } from "react"; -import { createRain, destroyRain } from "./regl/main"; +import React, { useEffect, useState, useRef, memo } from "react"; +import { init as initRain, formulate as refreshRain, destroy as destroyRain } from "./regl/main"; import makeConfig from "./utils/config"; /** @@ -110,29 +110,33 @@ export const Matrix = memo((props) => { const { style, className, ...rest } = props; const elProps = { style, className }; const matrix = useRef(null); - const rainRef = useRef(null); - const canvasRef = useRef(null); + const [rain, setRain] = useState(null); useEffect(() => { const canvas = document.createElement("canvas"); + matrix.current.appendChild(canvas); canvas.style.width = "100%"; canvas.style.height = "100%"; - canvasRef.current = canvas; + const init = async () => { + setRain(await initRain(canvas)); + } + init(); + + return () => { + destroyRain(rain); + setRain(null); + }; }, []); useEffect(() => { - matrix.current.appendChild(canvasRef.current); - const gl = canvasRef.current.getContext("webgl"); - createRain(canvasRef.current, makeConfig({ ...rest }), gl).then((handles) => { - rainRef.current = handles; - }); - - return () => { - if (rainRef.current) { - destroyRain(rainRef.current); - } + if (rain == null) { + return; + } + const refresh = async () => { + await refreshRain(rain, makeConfig({ ...rest })); }; - }, [props]); + refresh(); + }, [props, rain]); return
; }); diff --git a/js/index.js b/js/index.js index 67e6a5a..692069a 100644 --- a/js/index.js +++ b/js/index.js @@ -23,7 +23,7 @@ const versions = [ ]; const App = () => { const [version, setVersion] = React.useState(versions[0]); - // const [number, setNumber] = React.useState(0); + const [numColumns, setNumColumns] = React.useState(10); const onButtonClick = () => { setVersion((s) => { const newVersion = versions[idx]; @@ -31,17 +31,19 @@ const App = () => { console.log(newVersion); return newVersion; }); + setNumColumns(() => { + const newColumns = 10 + Math.floor(Math.random() * 50); + console.log(newColumns); + return newColumns; + }); }; - // const newNum = () => setNumber((n) => n + 1); - console.log("version", version); - // console.log("num", number); return (

Rain

- + {/* */} - +
); }; diff --git a/js/regl/imagePass.js b/js/regl/imagePass.js index f66dcd0..0bd861b 100644 --- a/js/regl/imagePass.js +++ b/js/regl/imagePass.js @@ -6,10 +6,10 @@ import imagePassFrag from "../../shaders/glsl/imagePass.frag.glsl"; const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Flammarion_Colored.jpg/917px-Flammarion_Colored.jpg"; -export default ({ regl, config }, inputs) => { +export default ({ regl, cache, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; - const background = loadImage(regl, bgURL); + const background = loadImage(cache, regl, bgURL); const render = regl({ frag: regl.prop("frag"), uniforms: { diff --git a/js/regl/main.js b/js/regl/main.js index 3e7ef6d..ea5e328 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -23,45 +23,27 @@ const effects = { mirror: makeMirrorPass, }; -const dimensions = { width: 1, height: 1 }; - -// const loadJS = (src) => -// new Promise((resolve, reject) => { -// const tag = document.createElement("script"); -// tag.onload = resolve; -// tag.onerror = reject; -// tag.src = src; -// document.body.appendChild(tag); -// }); - -// Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]); - -export const createRain = async (canvas, config, gl) => { +export const init = async (canvas) => { const resize = () => { - const dpr = window.devicePixelRatio || 1; - canvas.width = Math.ceil(window.innerWidth * dpr * config.resolution); - canvas.height = Math.ceil(window.innerHeight * dpr * config.resolution); + const devicePixelRatio = window.devicePixelRatio ?? 1; + canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * rain.resolution); + canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * rain.resolution); }; - window.onresize = resize; - if (document.fullscreenEnabled || document.webkitFullscreenEnabled) { - window.ondblclick = () => { - if (document.fullscreenElement == null) { - if (canvas.webkitRequestFullscreen != null) { - canvas.webkitRequestFullscreen(); - } else { - canvas.requestFullscreen(); - } - } else { - document.exitFullscreen(); - } - }; - } - resize(); - - if (config.useCamera) { - await setupCamera(); - } + const doubleClick = () => { + if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) { + return; + } + if (document.fullscreenElement != null) { + document.exitFullscreen(); + return; + } + if (canvas.webkitRequestFullscreen != null) { + canvas.webkitRequestFullscreen(); + } else { + canvas.requestFullscreen(); + } + }; const extensions = ["OES_texture_half_float", "OES_texture_half_float_linear"]; // These extensions are also needed, but Safari misreports that they are missing @@ -71,22 +53,35 @@ export const createRain = async (canvas, config, gl) => { "OES_standard_derivatives", ]; - switch (config.testFix) { - case "fwidth_10_1_2022_A": - extensions.push("OES_standard_derivatives"); - break; - case "fwidth_10_1_2022_B": - optionalExtensions.forEach((ext) => extensions.push(ext)); - extensions.length = 0; - break; - } + const regl = createREGL({ canvas, pixelRatio: 1, extensions, optionalExtensions }); + const cache = new Map(); + const rain = { canvas, resize, doubleClick, cache, regl, resolution: 1 }; - const regl = createREGL({ - gl, - pixelRatio: 1, - extensions, - optionalExtensions, - }); + window.addEventListener("dblclick", doubleClick); + window.addEventListener("resize", resize); + resize(); + + return rain; +}; + +export const destroy = (rain) => { + rain.resizeObserver.disconnect(); + window.removeEventListener("resize", resize); + window.removeEventListener("dblclick", doubleClick); + cache.clear(); +}; + +export const formulate = async (rain, config) => { + + const { resize, canvas, cache, regl } = rain; + rain.resolution = config.resolution; + resize(); + + const dimensions = { width: 1, height: 1 }; + + if (config.useCamera) { + await setupCamera(); + } const cameraTex = regl.texture(cameraCanvas); const lkg = await getLKG(config.useHoloplay, true); @@ -94,7 +89,7 @@ export const createRain = async (canvas, config, gl) => { // All this takes place in a full screen quad. const fullScreenQuad = makeFullScreenQuad(regl); const effectName = config.effect in effects ? config.effect : "palette"; - const context = { regl, config, lkg, cameraTex, cameraAspectRatio }; + const context = { regl, cache, config, lkg, cameraTex, cameraAspectRatio }; const pipeline = makePipeline(context, [ makeRain, makeBloomPass, @@ -112,6 +107,14 @@ export const createRain = async (canvas, config, gl) => { const targetFrameTimeMilliseconds = 1000 / config.fps; let last = NaN; + resetREGLTime: { + const reset = regl.frame(o => { + o.time = 0; + o.tick = 0; + reset.cancel(); + }) + } + const tick = regl.frame(({ viewportWidth, viewportHeight }) => { if (config.once) { tick.cancel(); @@ -150,10 +153,15 @@ export const createRain = async (canvas, config, gl) => { }); }); - return { regl, tick, canvas }; + if (rain.tick != null) { + rain.tick.cancel(); + } + + rain.tick = tick; }; -export const destroyRain = ({ regl, tick, canvas }) => { +export const destroyRain = ({ regl, cache, tick, canvas }) => { + cache.clear(); tick.cancel(); // stop RAF regl.destroy(); // release all GPU resources & event listeners //canvas.remove(); // drop from the DOM diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index a41dc10..94b8c0d 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -37,7 +37,7 @@ const blVert = [1, 0]; const brVert = [1, 1]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; -export default ({ regl, config, lkg }) => { +export default ({ regl, cache, config, lkg }) => { // The volumetric mode multiplies the number of columns // to reach the desired density, and then overlaps them const volumetric = config.volumetric; @@ -157,10 +157,10 @@ export default ({ regl, config, lkg }) => { ); // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen - const glyphMSDF = loadImage(regl, config.glyphMSDFURL); - const glintMSDF = loadImage(regl, config.glintMSDFURL); - const baseTexture = loadImage(regl, config.baseTextureURL, true); - const glintTexture = loadImage(regl, config.glintTextureURL, true); + const glyphMSDF = loadImage(cache, regl, config.glyphMSDFURL); + const glintMSDF = loadImage(cache, regl, config.glintMSDFURL); + const baseTexture = loadImage(cache, regl, config.baseTextureURL, true); + const glintTexture = loadImage(cache, regl, config.glintTextureURL, true); const output = makePassFBO(regl, config.useHalfFloat); const renderUniforms = { ...commonUniforms, diff --git a/js/regl/utils.js b/js/regl/utils.js index 5678acf..557b5c8 100644 --- a/js/regl/utils.js +++ b/js/regl/utils.js @@ -28,10 +28,16 @@ const makeDoubleBuffer = (regl, props) => { const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0; -const loadImage = (regl, url, mipmap) => { +const loadImage = (cache, regl, url, mipmap) => { + + const key = `${url}_${mipmap}`; + if (cache.has(key)) { + return cache.get(key); + } + let texture = regl.texture([[0]]); let loaded = false; - return { + const resource = { texture: () => { if (!loaded && url != null) { console.warn(`texture still loading: ${url}`); @@ -72,12 +78,18 @@ const loadImage = (regl, url, mipmap) => { } })(), }; + cache.set(key, resource); + return resource; }; -const loadText = (url) => { +const loadText = (cache, url) => { + const key = url; + if (cache.has(key)) { + return cache.get(key); + } let text = ""; let loaded = false; - return { + const resource = { text: () => { if (!loaded) { console.warn(`text still loading: ${url}`); @@ -91,6 +103,8 @@ const loadText = (url) => { } })(), }; + cache.set(key, resource); + return resource; }; const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>