Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3bc0d5d
Matrix React component 1.0.0
nohren Aug 7, 2023
42011c3
Running npm audit fix
Rezmason Apr 24, 2025
6183acc
Repairing the bloom
Rezmason May 5, 2025
7a10893
Installed prettier and added the format script to the project
Rezmason May 5, 2025
237990b
Ran the format script
Rezmason May 5, 2025
664f484
Component now reuses its canvas. regl implementation reuses its regl …
Rezmason May 6, 2025
f3cd449
Added WebGPU support and added caching to the WebGPU version.
Rezmason May 6, 2025
6663c92
Removing holoplay for now.
Rezmason May 6, 2025
eea341f
Exploring ways to preserve the vanilla JS browser demo without compro…
Rezmason May 6, 2025
a1332d8
Separating the resource imports from the Matrix.js module
Rezmason May 8, 2025
319b539
Testing hot-swapping renderers, which requires destroying and rebuild…
Rezmason May 8, 2025
2224192
regl's destroy function destroys all resources created with it; so do…
Rezmason May 9, 2025
b7085c5
Implementing and verifying the integrity of the cleanup function on t…
Rezmason May 15, 2025
45059c4
Removing unnecessary trick in inclusions.js that was meant to prevent…
Rezmason May 15, 2025
24e9390
Added some other basic inputs to see how continuous changes of variou…
Rezmason May 15, 2025
f61a4e2
Added cache check to WebGPU renderer's loadShader method. Un-commente…
Rezmason May 20, 2025
658f07c
Code intended to leverage imports in the load functions.
Rezmason May 20, 2025
3b837c6
Massive overhaul: the renderers are now classes that implement Render…
Rezmason May 23, 2025
1da1feb
Converting tabs to spaces in TODO.txt and the quilt pass fragment shader
Rezmason May 23, 2025
b6570de
Fixed some major bugs: the WebGPU cache should store loaded images an…
Rezmason May 25, 2025
83f1eb0
Update renderer.js
Rezmason May 25, 2025
0884c6a
WebGPU Renderer now awaits the submitted work in the old device's que…
Rezmason May 26, 2025
0ff8e99
Added some files that were git-ignored because of their directory nam…
Rezmason May 27, 2025
41f2f83
Fixed a typo in the format script
Rezmason May 30, 2025
00805c8
Ran formatter.
Rezmason May 30, 2025
068e366
Added a non-vite test, to be sure the rolled up libraries can run out…
Rezmason May 30, 2025
53e7e42
Removing extra formatting step from test-bundles
Rezmason Jun 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.idea/
.vscode/
node_modules/
build/
.DS_Store
*.tgz
my-app*
template/src/__tests__/__snapshots__/
lerna-debug.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.changelog
.npm/
/dist
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,19 @@ The glyphs used in the "Palimpsest" and "Twilight" versions are derived from [Te
The glyphs are formatted as a multi-channel distance field (or MSDF) via Victor Chlumsky's [msdfgen](https://github.com/Chlumsky/msdfgen). This format preserves the crisp edges and corners of vector graphics when rendered as textures. Chlumsky's thesis paper, which is in English and is also easy to read, is [available to download here](https://dspace.cvut.cz/handle/10467/62770).

The raindrops themselves are particles [computed on the GPU and stored in textures](https://threejs.org/examples/webgl_gpgpu_water.html), much smaller than the final render. The data sent from the CPU to the GPU every frame is negligible.


## react-matrix-rain
This is an effort to produce an npm package that bundles this repo's effects as a single react component for use in SPA applications. Work on the legacy code will update the component. Steps to build the component are as follows.

### Offline testing
```
npm pack --dry-run # assess that the package is viable.

npm pack # creates the tarball

npm install react-matrix-rain-<version>.tgz
```

### Publishing
... TBD
33 changes: 21 additions & 12 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
TODO:
Create multiple distributions
core
One embedded MSDF, combined from the two main glyph sets and their configs
fun
Other MSDFs and configs
and then one with built-in MSDF generation
(TTF + glyphString) --> MSDF
Is MSDF strictly necessary?
Move off of regl
Expanded configurability
Modify regl pass
async build(config, inputs)
loads all required stuff
async resize - adjusts the sizes
run — performs the GPU operation
Rewrite main code to accept changes to config

Live config update roadmap
Modify regl pass
async build(config, inputs)
loads all required stuff
async resize - adjusts the sizes
run — performs the GPU operation
Rewrite main code to accept changes to config
Build a Tweakpane for config
https://cocopon.github.io/tweakpane
Show a gear emoji in the top right on mouse move
Hide after three seconds
If tapped, show the tweakpane
Build a Tweakpane for config
https://cocopon.github.io/tweakpane
Show a gear emoji in the top right on mouse move
Hide after three seconds
If tapped, show the tweakpane

Seems like bloom size and resolution impact the REGL and WebGPU bloom implementations differently
Move high pass into WebGPU bloom
Expand Down
171 changes: 171 additions & 0 deletions js/Matrix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useEffect, useState, useRef, memo } from "react";
import makeConfig from "./utils/config";

/**
* @typedef {object} Colour
* @property {"hsl"|"rgb"} space
* @property {number[]} values // 3-tuple [0-1] or [0-360,0-1,0-1]
*/

/**
* Complete runtime configuration for the Matrix / Digital-Rain component.
*
* @typedef {{
* /* ------------- core identity ------------- * /
* version?: (
* "classic" | "megacity" | "neomatrixology" | "operator" |
* "nightmare" | "paradise" | "resurrections" | "trinity" |
* "morpheus" | "bugs" | "palimpsest" | "twilight" |
* "3d" | "throwback" | "updated" |
* "1999" | "2003" | "2021" | string /* custom * /
* ),
* font?: keyof typeof fonts, // "matrixcode", …
* effect?: "palette" | "stripe" | string,
*
* /* ------------- texture assets ------------- * /
* baseTexture?: keyof typeof textureURLs | null,
* glintTexture?: keyof typeof textureURLs | null,
*
* /* ------------- global toggles ------------- * /
* useCamera?: boolean,
* volumetric?: boolean,
* loops?: boolean,
* skipIntro?: boolean,
* renderer?: "regl" | "three" | string,
* suppressWarnings?: boolean,
* useHalfFloat?: boolean,
* isometric?: boolean,
*
* /* ------------- glyph appearance ------------- * /
* glyphEdgeCrop?: number,
* glyphHeightToWidth?: number,
* glyphVerticalSpacing?: number,
* glyphFlip?: boolean,
* glyphRotation?: number, // radians (multiples of π/2 supported)
*
* /* ------------- cursor & glint ------------- * /
* isolateCursor?: boolean,
* cursorColor?: Colour,
* cursorIntensity?: number,
* isolateGlint?: boolean,
* glintColor?: Colour,
* glintIntensity?: number,
*
* /* ------------- animation & timing ------------- * /
* animationSpeed?: number,
* fps?: number,
* cycleSpeed?: number,
* cycleFrameSkip?: number,
* fallSpeed?: number,
* forwardSpeed?: number,
* raindropLength?: number,
* slant?: number, // radians
*
* /* ------------- optical effects ------------- * /
* bloomStrength?: number,
* bloomSize?: number,
* highPassThreshold?: number,
* baseBrightness?: number,
* baseContrast?: number,
* glintBrightness?: number,
* glintContrast?: number,
* brightnessOverride?: number,
* brightnessThreshold?: number,
* brightnessDecay?: number,
* ditherMagnitude?: number,
* hasThunder?: boolean,
*
* /* ------------- geometry ------------- * /
* numColumns?: number,
* density?: number,
* isPolar?: boolean,
* rippleTypeName?: ("circle"|"box"|string|null),
* rippleThickness?: number,
* rippleScale?: number,
* rippleSpeed?: number,
*
* /* ------------- colour mapping ------------- * /
* palette?: {color: Colour, at: number}[],
* stripeColors?: Colour[],
* backgroundColor?: Colour,
* glyphIntensity?: number,
*
* /* ------------- misc / experimental ------------- * /
* resolution?: number,
* testFix?: string|null,
*
* /* ------------- React pass-through ------------- * /
* style?: React.CSSProperties,
* className?: string,
*
* /* ------------- catch-all ------------- * /
* [key: string]: unknown
* }} MatrixProps
*/

/** @param {MatrixProps} props */
export const Matrix = memo((props) => {
const { style, className, ...rest } = props;
const elProps = { style, className };
const matrix = useRef(null);
const [rCanvas, setCanvas] = useState(null);
const [rRenderer, setRenderer] = useState(null);
const [rRain, setRain] = useState(null);

const supportsWebGPU = () => {
return (
window.GPUQueue != null &&
navigator.gpu != null &&
navigator.gpu.getPreferredCanvasFormat != null
);
};

useEffect(() => {
const useWebGPU = supportsWebGPU() && ["webgpu"].includes(rest.renderer?.toLowerCase());
const isWebGPU = rRenderer?.type === "webgpu";

if (rRenderer != null && useWebGPU === isWebGPU) {
return;
}

if (rCanvas != null) {
matrix.current.removeChild(rCanvas);
setCanvas(null);
}

if (rRain != null) {
rRenderer?.destroy(rRain);
setRain(null);
}

if (rRenderer != null) {
setRenderer(null);
}

const canvas = document.createElement("canvas");
canvas.style.width = "100%";
canvas.style.height = "100%";
matrix.current.appendChild(canvas);
setCanvas(canvas);

const loadRain = async () => {
const renderer = await import(`./${useWebGPU ? "webgpu" : "regl"}/main.js`);
setRenderer(renderer);
const rain = await renderer.init(canvas);
setRain(rain);
};
loadRain();
}, [props.renderer]);

useEffect(() => {
if (rRain == null || rRain.destroyed) {
return;
}
const refresh = async () => {
await rRenderer.formulate(rRain, makeConfig({ ...rest }));
};
refresh();
}, [props, rRain]);

return <div ref={matrix} {...elProps}></div>;
});
8 changes: 8 additions & 0 deletions js/bundle-contents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Matrix } from "./Matrix";
import inclusions from "./inclusions";
import * as reglRenderer from "./regl/main";
import * as webgpuRenderer from "./webgpu/main";
globalThis.inclusions = inclusions;
globalThis.reglRenderer = reglRenderer;
globalThis.webgpuRenderer = webgpuRenderer;
globalThis.Matrix = Matrix;
20 changes: 20 additions & 0 deletions js/fetchLibraries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default async () => {
let glMatrix, createREGL;

try {
glMatrix = await import("gl-matrix");
createREGL = (await import("regl")).default;
} catch {
const loadJS = (src) =>
new Promise((resolve, reject) => {
const tag = document.createElement("script");
[tag.onload, tag.onerror, tag.src] = [resolve, reject, src];
document.body.appendChild(tag);
});
await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
glMatrix = globalThis.glMatrix;
createREGL = globalThis.createREGL;
}

return { glMatrix, createREGL };
};
77 changes: 77 additions & 0 deletions js/inclusions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import highPassFrag from "../shaders/glsl/bloomPass.highPass.frag.glsl";
import blurFrag from "../shaders/glsl/bloomPass.blur.frag.glsl";
import combineFrag from "../shaders/glsl/bloomPass.combine.frag.glsl";
import imagePassFrag from "../shaders/glsl/imagePass.frag.glsl";
import mirrorPassFrag from "../shaders/glsl/mirrorPass.frag.glsl";
import palettePassFrag from "../shaders/glsl/palettePass.frag.glsl";
import rainPassIntro from "../shaders/glsl/rainPass.intro.frag.glsl";
import rainPassRaindrop from "../shaders/glsl/rainPass.raindrop.frag.glsl";
import rainPassSymbol from "../shaders/glsl/rainPass.symbol.frag.glsl";
import rainPassEffect from "../shaders/glsl/rainPass.effect.frag.glsl";
import rainPassVert from "../shaders/glsl/rainPass.vert.glsl";
import rainPassFrag from "../shaders/glsl/rainPass.frag.glsl";
import stripePassFrag from "../shaders/glsl/stripePass.frag.glsl";
import msdfCoptic from "../assets/coptic_msdf.png";
import msdfGothic from "../assets/gothic_msdf.png";
import msdfMatrixCode from "../assets/matrixcode_msdf.png";
import msdfRes from "../assets/resurrections_msdf.png";
// import megacity from "../assets/megacity_msdf.png";
import msdfResGlint from "../assets/resurrections_glint_msdf.png";
// import msdfHuberfishA from "../assets/huberfish_a_msdf.png";
// import msdfHuberfishD from "../assets/huberfish_d_msdf.png";
// import msdfGtargTenretni from "../assets/gtarg_tenretniolleh_msdf.png";
// import msdfGtargAlienText from "../assets/gtarg_alientext_msdf.png";
// import msdfNeoMatrixology from "../assets/neomatrixology_msdf.png";
// import texSand from "../assets/sand.png";
// import texPixels from "../assets/pixel_grid.png";
import texMesh from "../assets/mesh.png";
import texMetal from "../assets/metal.png";
import bloomBlurShader from "../shaders/wgsl/bloomBlur.wgsl";
import bloomCombineShader from "../shaders/wgsl/bloomCombine.wgsl";
import endPassShader from "../shaders/wgsl/endPass.wgsl";
import imagePassShader from "../shaders/wgsl/imagePass.wgsl";
import mirrorPassShader from "../shaders/wgsl/mirrorPass.wgsl";
import palettePassShader from "../shaders/wgsl/palettePass.wgsl";
import rainPassShader from "../shaders/wgsl/rainPass.wgsl";
import stripePassShader from "../shaders/wgsl/stripePass.wgsl";

const inclusions = [
highPassFrag,
blurFrag,
combineFrag,
imagePassFrag,
mirrorPassFrag,
palettePassFrag,
rainPassIntro,
rainPassRaindrop,
rainPassSymbol,
rainPassEffect,
rainPassVert,
rainPassFrag,
stripePassFrag,
msdfCoptic,
msdfGothic,
msdfMatrixCode,
msdfRes,
// megacity,
msdfResGlint,
// msdfHuberfishA,
// msdfHuberfishD,
// msdfGtargTenretni,
// msdfGtargAlienText,
// msdfNeoMatrixology,
// texSand,
// texPixels,
texMesh,
texMetal,
bloomBlurShader,
bloomCombineShader,
endPassShader,
imagePassShader,
mirrorPassShader,
palettePassShader,
rainPassShader,
stripePassShader,
].reduce((i, s) => s.length + i, 0);

export default inclusions;
Loading