Matrix React component 1.0.0

This commit is contained in:
nohren
2023-08-07 02:03:46 -07:00
parent 5ba9049045
commit 3bc0d5d346
24 changed files with 9039 additions and 1236 deletions

6
.babelrc Normal file
View File

@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

15
.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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 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. 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

140
js/Matrix.js Normal file
View File

@@ -0,0 +1,140 @@
import React, { useEffect, useRef, memo } from "react";
import { createRain, destroyRain } from "./regl/main";
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" |
* "holoplay" | "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,
* useHoloplay?: 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 rainRef = useRef(null);
const canvasRef = useRef(null);
useEffect(() => {
const canvas = document.createElement("canvas");
canvas.style.width = "100%";
canvas.style.height = "100%";
canvasRef.current = canvas;
}, []);
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);
}
};
}, [props]);
return <div ref={matrix} {...elProps}></div>;
});

View File

@@ -1,581 +0,0 @@
const fonts = {
coptic: {
// The script the Gnostic codices were written in
glyphMSDFURL: "assets/coptic_msdf.png",
glyphSequenceLength: 32,
glyphTextureGridSize: [8, 8],
},
gothic: {
// The script the Codex Argenteus was written in
glyphMSDFURL: "assets/gothic_msdf.png",
glyphSequenceLength: 27,
glyphTextureGridSize: [8, 8],
},
matrixcode: {
// The glyphs seen in the film trilogy
glyphMSDFURL: "assets/matrixcode_msdf.png",
glyphSequenceLength: 57,
glyphTextureGridSize: [8, 8],
},
megacity: {
// The glyphs seen in the film trilogy
glyphMSDFURL: "assets/megacity_msdf.png",
glyphSequenceLength: 64,
glyphTextureGridSize: [8, 8],
},
resurrections: {
// The glyphs seen in the film trilogy
glyphMSDFURL: "assets/resurrections_msdf.png",
glintMSDFURL: "assets/resurrections_glint_msdf.png",
glyphSequenceLength: 135,
glyphTextureGridSize: [13, 12],
},
huberfishA: {
glyphMSDFURL: "assets/huberfish_a_msdf.png",
glyphSequenceLength: 34,
glyphTextureGridSize: [6, 6],
},
huberfishD: {
glyphMSDFURL: "assets/huberfish_d_msdf.png",
glyphSequenceLength: 34,
glyphTextureGridSize: [6, 6],
},
gtarg_tenretniolleh: {
glyphMSDFURL: "assets/gtarg_tenretniolleh_msdf.png",
glyphSequenceLength: 36,
glyphTextureGridSize: [6, 6],
},
gtarg_alientext: {
glyphMSDFURL: "assets/gtarg_alientext_msdf.png",
glyphSequenceLength: 38,
glyphTextureGridSize: [8, 5],
},
neomatrixology: {
glyphMSDFURL: "assets/neomatrixology_msdf.png",
glyphSequenceLength: 12,
glyphTextureGridSize: [4, 4],
},
};
const textureURLs = {
sand: "assets/sand.png",
pixels: "assets/pixel_grid.png",
mesh: "assets/mesh.png",
metal: "assets/metal.png",
};
const hsl = (...values) => ({ space: "hsl", values });
const rgb = (...values) => ({ space: "rgb", values });
const defaults = {
font: "matrixcode",
effect: "palette", // The name of the effect to apply at the end of the process— mainly handles coloration
baseTexture: null, // The name of the texture to apply to the base layer of the glyphs
glintTexture: null, // The name of the texture to apply to the glint layer of the glyphs
useCamera: false,
backgroundColor: hsl(0, 0, 0), // The color "behind" the glyphs
isolateCursor: true, // Whether the "cursor"— the brightest glyph at the bottom of a raindrop— has its own color
cursorColor: hsl(0.242, 1, 0.73), // The color of the cursor
cursorIntensity: 2, // The intensity of the cursor
isolateGlint: false, // Whether the "glint"— highlights on certain symbols in the font— should appear
glintColor: hsl(0, 0, 1), // The color of the glint
glintIntensity: 1, // The intensity of the glint
volumetric: false, // A mode where the raindrops appear in perspective
animationSpeed: 1, // The global rate that all animations progress
fps: 60, // The target frame rate (frames per second) of the effect
forwardSpeed: 0.25, // The speed volumetric rain approaches the eye
bloomStrength: 0.7, // The intensity of the bloom
bloomSize: 0.4, // The amount the bloom calculation is scaled
highPassThreshold: 0.1, // The minimum brightness that is still blurred
cycleSpeed: 0.03, // The speed glyphs change
cycleFrameSkip: 1, // The global minimum number of frames between glyphs cycling
baseBrightness: -0.5, // The brightness of the glyphs, before any effects are applied
baseContrast: 1.1, // The contrast of the glyphs, before any effects are applied
glintBrightness: -1.5, // The brightness of the glints, before any effects are applied
glintContrast: 2.5, // The contrast of the glints, before any effects are applied
brightnessOverride: 0.0, // A global override to the brightness of displayed glyphs. Only used if it is > 0.
brightnessThreshold: 0, // The minimum brightness for a glyph to still be considered visible
brightnessDecay: 1.0, // The rate at which glyphs light up and dim
ditherMagnitude: 0.05, // The magnitude of the random per-pixel dimming
fallSpeed: 0.3, // The speed the raindrops progress downwards
glyphEdgeCrop: 0.0, // The border around a glyph in a font texture that should be cropped out
glyphHeightToWidth: 1, // The aspect ratio of glyphs
glyphVerticalSpacing: 1, // The ratio of the vertical distance between glyphs to their height
glyphFlip: false, // Whether to horizontally reflect the glyphs
glyphRotation: 0, // An angle to rotate the glyphs. Currently limited to 90° increments
hasThunder: false, // An effect that adds dramatic lightning flashes
isPolar: false, // Whether the glyphs arc across the screen or sit in a standard grid
rippleTypeName: null, // The variety of the ripple effect
rippleThickness: 0.2, // The thickness of the ripple effect
rippleScale: 30, // The size of the ripple effect
rippleSpeed: 0.2, // The rate at which the ripple effect progresses
numColumns: 80, // The maximum dimension of the glyph grid
density: 1, // In volumetric mode, the number of actual columns compared to the grid
palette: [
// The color palette that glyph brightness is color mapped to
{ color: hsl(0.3, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.3, 0.9, 0.2), at: 0.2 },
{ color: hsl(0.3, 0.9, 0.7), at: 0.7 },
{ color: hsl(0.3, 0.9, 0.8), at: 0.8 },
],
raindropLength: 0.75, // Adjusts the frequency of raindrops (and their length) in a column
slant: 0, // The angle at which rain falls; the orientation of the glyph grid
resolution: 0.75, // An overall scale multiplier
useHalfFloat: false,
renderer: "regl", // The preferred web graphics API
suppressWarnings: false, // Whether to show warnings to visitors on load
isometric: false,
useHoloplay: false,
loops: false,
skipIntro: true,
testFix: null,
};
const versions = {
classic: {},
megacity: {
font: "megacity",
animationSpeed: 0.5,
numColumns: 40,
},
neomatrixology: {
font: "neomatrixology",
animationSpeed: 0.8,
numColumns: 40,
palette: [
{ color: hsl(0.15, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.15, 0.9, 0.2), at: 0.2 },
{ color: hsl(0.15, 0.9, 0.7), at: 0.7 },
{ color: hsl(0.15, 0.9, 0.8), at: 0.8 },
],
cursorColor: hsl(0.167, 1, 0.75),
cursorIntensity: 2,
},
operator: {
cursorColor: hsl(0.375, 1, 0.66),
cursorIntensity: 3,
bloomSize: 0.6,
bloomStrength: 0.75,
highPassThreshold: 0.0,
cycleSpeed: 0.01,
cycleFrameSkip: 8,
brightnessOverride: 0.22,
brightnessThreshold: 0,
fallSpeed: 0.6,
glyphEdgeCrop: 0.15,
glyphHeightToWidth: 1.35,
rippleTypeName: "box",
numColumns: 108,
palette: [
{ color: hsl(0.4, 0.8, 0.0), at: 0.0 },
{ color: hsl(0.4, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.4, 0.8, 1.0), at: 1.0 },
],
raindropLength: 1.5,
},
nightmare: {
font: "gothic",
isolateCursor: false,
highPassThreshold: 0.7,
baseBrightness: -0.8,
brightnessDecay: 0.75,
fallSpeed: 1.2,
hasThunder: true,
numColumns: 60,
cycleSpeed: 0.35,
palette: [
{ color: hsl(0.0, 1.0, 0.0), at: 0.0 },
{ color: hsl(0.0, 1.0, 0.2), at: 0.2 },
{ color: hsl(0.0, 1.0, 0.4), at: 0.4 },
{ color: hsl(0.1, 1.0, 0.7), at: 0.7 },
{ color: hsl(0.2, 1.0, 1.0), at: 1.0 },
],
raindropLength: 0.5,
slant: (22.5 * Math.PI) / 180,
},
paradise: {
font: "coptic",
isolateCursor: false,
bloomStrength: 1,
highPassThreshold: 0,
cycleSpeed: 0.005,
baseBrightness: -1.3,
baseContrast: 2,
brightnessDecay: 0.05,
fallSpeed: 0.02,
isPolar: true,
rippleTypeName: "circle",
rippleSpeed: 0.1,
numColumns: 40,
palette: [
{ color: hsl(0.0, 0.0, 0.0), at: 0.0 },
{ color: hsl(0.0, 0.8, 0.3), at: 0.3 },
{ color: hsl(0.1, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.1, 1.0, 0.6), at: 0.6 },
{ color: hsl(0.1, 1.0, 0.9), at: 0.9 },
],
raindropLength: 0.4,
},
resurrections: {
font: "resurrections",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
baseBrightness: -0.7,
baseContrast: 1.17,
highPassThreshold: 0,
numColumns: 70,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.375, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.375, 1.0, 0.6), at: 0.92 },
{ color: hsl(0.375, 1.0, 1.0), at: 1.0 },
],
},
trinity: {
font: "resurrections",
glintTexture: "metal",
baseTexture: "pixels",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.131, 1, 0.6),
glintIntensity: 3,
glintBrightness: -0.5,
glintContrast: 1.5,
baseBrightness: -0.4,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.37, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.37, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
volumetric: true,
forwardSpeed: 0.2,
raindropLength: 0.3,
density: 0.75,
},
morpheus: {
font: "resurrections",
glintTexture: "mesh",
baseTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.333, 1, 0.85),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.4, 1, 0.5),
glintIntensity: 2,
glintBrightness: -1.5,
glintContrast: 3,
baseBrightness: -0.3,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.97, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.97, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.015,
volumetric: true,
forwardSpeed: 0.1,
raindropLength: 0.4,
density: 0.75,
},
bugs: {
font: "resurrections",
glintTexture: "sand",
baseTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.619, 1, 0.65),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.625, 1, 0.6),
glintIntensity: 3,
glintBrightness: -1,
glintContrast: 3,
baseBrightness: -0.3,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.12, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.14, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
volumetric: true,
forwardSpeed: 0.4,
raindropLength: 0.3,
density: 0.75,
},
palimpsest: {
font: "huberfishA",
isolateCursor: false,
bloomStrength: 0.2,
numColumns: 40,
raindropLength: 1.2,
cycleFrameSkip: 3,
fallSpeed: 0.5,
slant: Math.PI * -0.0625,
palette: [
{ color: hsl(0.15, 0.25, 0.9), at: 0.0 },
{ color: hsl(0.6, 0.8, 0.1), at: 0.4 },
],
},
twilight: {
font: "huberfishD",
cursorColor: hsl(0.167, 1, 0.8),
cursorIntensity: 1.5,
bloomStrength: 0.1,
numColumns: 50,
raindropLength: 0.9,
fallSpeed: 0.1,
highPassThreshold: 0.0,
palette: [
{ color: hsl(0.6, 1.0, 0.05), at: 0.0 },
{ color: hsl(0.6, 0.8, 0.1), at: 0.1 },
{ color: hsl(0.88, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.15, 1.0, 0.6), at: 0.8 },
// { color: hsl(0.1, 1.0, 0.9), at: 1.0 },
],
},
holoplay: {
font: "resurrections",
glintTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.131, 1, 0.6),
glintIntensity: 3,
glintBrightness: -0.5,
glintContrast: 1.5,
baseBrightness: -0.4,
baseContrast: 1.5,
highPassThreshold: 0,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.37, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.37, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
raindropLength: 0.3,
renderer: "regl",
numColumns: 20,
ditherMagnitude: 0,
bloomStrength: 0,
volumetric: true,
forwardSpeed: 0,
density: 3,
useHoloplay: true,
},
["3d"]: {
volumetric: true,
fallSpeed: 0.5,
cycleSpeed: 0.03,
baseBrightness: -0.9,
baseContrast: 1.5,
raindropLength: 0.3,
},
};
versions.throwback = versions.operator;
versions.updated = versions.resurrections;
versions["1999"] = versions.operator;
versions["2003"] = versions.classic;
versions["2021"] = versions.resurrections;
const range = (f, min = -Infinity, max = Infinity) => Math.max(min, Math.min(max, f));
const nullNaN = (f) => (isNaN(f) ? null : f);
const isTrue = (s) => s.toLowerCase().includes("true");
const parseColor = (isHSL) => (s) => ({
space: isHSL ? "hsl" : "rgb",
values: s.split(",").map(parseFloat),
});
const parseColors = (isHSL) => (s) => {
const values = s.split(",").map(parseFloat);
const space = isHSL ? "hsl" : "rgb";
return Array(Math.floor(values.length / 3))
.fill()
.map((_, index) => ({
space,
values: values.slice(index * 3, (index + 1) * 3),
}));
};
const parsePalette = (isHSL) => (s) => {
const values = s.split(",").map(parseFloat);
const space = isHSL ? "hsl" : "rgb";
return Array(Math.floor(values.length / 4))
.fill()
.map((_, index) => {
const colorValues = values.slice(index * 4, (index + 1) * 4);
return {
color: {
space,
values: colorValues.slice(0, 3),
},
at: colorValues[3],
};
});
};
const paramMapping = {
testFix: { key: "testFix", parser: (s) => s },
version: { key: "version", parser: (s) => s },
font: { key: "font", parser: (s) => s },
effect: { key: "effect", parser: (s) => s },
camera: { key: "useCamera", parser: isTrue },
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)),
},
bloomStrength: {
key: "bloomStrength",
parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
},
ditherMagnitude: {
key: "ditherMagnitude",
parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
},
url: { key: "bgURL", parser: (s) => s },
palette: { key: "palette", parser: parsePalette(false) },
stripeColors: { key: "stripeColors", parser: parseColors(false) },
backgroundColor: { key: "backgroundColor", parser: parseColor(false) },
cursorColor: { key: "cursorColor", parser: parseColor(false) },
glintColor: { key: "glintColor", parser: parseColor(false) },
paletteHSL: { key: "palette", parser: parsePalette(true) },
stripeHSL: { key: "stripeColors", parser: parseColors(true) },
backgroundHSL: { key: "backgroundColor", parser: parseColor(true) },
cursorHSL: { key: "cursorColor", parser: parseColor(true) },
glintHSL: { key: "glintColor", parser: parseColor(true) },
cursorIntensity: {
key: "cursorIntensity",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
glyphIntensity: {
key: "glyphIntensity",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
volumetric: { key: "volumetric", parser: isTrue },
glyphFlip: { key: "glyphFlip", parser: isTrue },
glyphRotation: {
key: "glyphRotation",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
loops: { key: "loops", parser: isTrue },
fps: { key: "fps", parser: (s) => nullNaN(range(parseFloat(s), 0, 60)) },
skipIntro: { key: "skipIntro", parser: isTrue },
renderer: { key: "renderer", parser: (s) => s },
suppressWarnings: { key: "suppressWarnings", parser: isTrue },
once: { key: "once", parser: isTrue },
isometric: { key: "isometric", parser: isTrue },
};
paramMapping.paletteRGB = paramMapping.palette;
paramMapping.stripeRGB = paramMapping.stripeColors;
paramMapping.backgroundRGB = paramMapping.backgroundColor;
paramMapping.cursorRGB = paramMapping.cursorColor;
paramMapping.glintRGB = paramMapping.glintColor;
paramMapping.width = paramMapping.numColumns;
paramMapping.dropLength = paramMapping.raindropLength;
paramMapping.angle = paramMapping.slant;
paramMapping.colors = paramMapping.stripeColors;
export default (urlParams) => {
const validParams = Object.fromEntries(
Object.entries(urlParams)
.filter(([key]) => key in paramMapping)
.map(([key, value]) => [paramMapping[key].key, paramMapping[key].parser(value)])
.filter(([_, value]) => value != null),
);
if (validParams.effect != null) {
if (validParams.cursorColor == null) {
validParams.cursorColor = hsl(0, 0, 1);
}
if (validParams.cursorIntensity == null) {
validParams.cursorIntensity = 2;
}
if (validParams.glintColor == null) {
validParams.glintColor = hsl(0, 0, 1);
}
if (validParams.glyphIntensity == null) {
validParams.glyphIntensity = 1;
}
}
const version = validParams.version in versions ? versions[validParams.version] : versions.classic;
const fontName = [validParams.font, version.font, defaults.font].find((name) => name in fonts);
const font = fonts[fontName];
const baseTextureURL = textureURLs[[version.baseTexture, defaults.baseTexture].find((name) => name in textureURLs)];
const hasBaseTexture = baseTextureURL != null;
const glintTextureURL = textureURLs[[version.glintTexture, defaults.glintTexture].find((name) => name in textureURLs)];
const hasGlintTexture = glintTextureURL != null;
const config = {
...defaults,
...version,
...font,
...validParams,
baseTextureURL,
glintTextureURL,
hasBaseTexture,
hasGlintTexture,
};
if (config.bloomSize <= 0) {
config.bloomStrength = 0;
}
return config;
};

48
js/index.js Normal file
View File

@@ -0,0 +1,48 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { Matrix } from "./Matrix";
//import { Matrix } from "react-matrix-rain";
const root = createRoot(document.getElementById("root"));
let idx = 1;
const versions = [
"3d",
"trinity",
"bugs",
"megacity",
"nightmare",
"paradise",
"resurrections",
"operator",
"holoplay",
"throwback",
"updated",
"1999",
"2003",
"2021",
];
const App = () => {
const [version, setVersion] = React.useState(versions[0]);
// const [number, setNumber] = React.useState(0);
const onButtonClick = () => {
setVersion((s) => {
const newVersion = versions[idx];
idx = (idx + 1) % versions.length;
console.log(newVersion);
return newVersion;
});
};
// const newNum = () => setNumber((n) => n + 1);
console.log("version", version);
// console.log("num", number);
return (
<div>
<h1>Rain</h1>
<button onClick={onButtonClick}>change version</button>
{/* <button onClick={newNum}>change number</button> */}
<Matrix version={version} density={7.0} />
</div>
);
};
root.render(<App />);

View File

@@ -1,4 +1,9 @@
import { loadText, makePassFBO, makePass } from "./utils.js"; import { makePassFBO, makePass } from "./utils";
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 fsVert from '../../shaders/glsl/bloomPass.vert.glsl';
// The bloom pass is basically an added high-pass blur. // The bloom pass is basically an added high-pass blur.
// The blur approximation is the sum of a pyramid of downscaled, blurred textures. // The blur approximation is the sum of a pyramid of downscaled, blurred textures.
@@ -13,7 +18,12 @@ const makePyramid = (regl, height, halfFloat) =>
.map((_) => makePassFBO(regl, halfFloat)); .map((_) => makePassFBO(regl, halfFloat));
const resizePyramid = (pyramid, vw, vh, scale) => 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)
)
);
export default ({ regl, config }, inputs) => { export default ({ regl, config }, inputs) => {
const { bloomStrength, bloomSize, highPassThreshold } = config; const { bloomStrength, bloomSize, highPassThreshold } = config;
@@ -33,9 +43,21 @@ export default ({ regl, config }, inputs) => {
const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
// one big triangle that covers the whole screen
const fullScreenTriangle = regl.buffer([
-1, -1,
3, -1,
-1, 3,
]);
const commonDrawProps = {
attributes: { aPosition: fullScreenTriangle },
count: 3,
};
// The high pass restricts the blur to bright things in our input texture. // The high pass restricts the blur to bright things in our input texture.
const highPassFrag = loadText("shaders/glsl/bloomPass.highPass.frag.glsl");
const highPass = regl({ const highPass = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
highPassThreshold, highPassThreshold,
@@ -44,13 +66,17 @@ export default ({ regl, config }, inputs) => {
framebuffer: regl.prop("fbo"), framebuffer: regl.prop("fbo"),
}); });
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically. // A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
// The FBO pyramid's levels represent separate levels of detail; // The FBO pyramid's levels represent separate levels of detail;
// by blurring them all, this basic blur approximates a more complex gaussian: // by blurring them all, this basic blur approximates a more complex gaussian:
// https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom // https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom
const blurFrag = loadText("shaders/glsl/bloomPass.blur.frag.glsl");
const blur = regl({ const blur = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
tex: regl.prop("tex"), tex: regl.prop("tex"),
@@ -62,12 +88,15 @@ export default ({ regl, config }, inputs) => {
}); });
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture // The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
const combineFrag = loadText("shaders/glsl/bloomPass.combine.frag.glsl");
const combine = regl({ const combine = regl({
...commonDrawProps,
vert: fsVert,
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
bloomStrength, bloomStrength,
...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])), ...Object.fromEntries(
vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])
),
}, },
framebuffer: output, framebuffer: output,
}); });
@@ -77,7 +106,7 @@ export default ({ regl, config }, inputs) => {
primary: inputs.primary, primary: inputs.primary,
bloom: output, bloom: output,
}, },
Promise.all([highPassFrag.loaded, blurFrag.loaded]), // Promise.all([highPassFrag.loaded, blurFrag.loaded]),
(w, h) => { (w, h) => {
// The blur pyramids can be lower resolution than the screen. // The blur pyramids can be lower resolution than the screen.
resizePyramid(highPassPyramid, w, h, bloomSize); resizePyramid(highPassPyramid, w, h, bloomSize);
@@ -94,12 +123,26 @@ export default ({ regl, config }, inputs) => {
const highPassFBO = highPassPyramid[i]; const highPassFBO = highPassPyramid[i];
const hBlurFBO = hBlurPyramid[i]; const hBlurFBO = hBlurPyramid[i];
const vBlurFBO = vBlurPyramid[i]; const vBlurFBO = vBlurPyramid[i];
highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] }); highPass({
blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); fbo: highPassFBO,
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); frag: highPassFrag,
tex: i === 0 ? inputs.primary : highPassPyramid[i - 1],
});
blur({
fbo: hBlurFBO,
frag: blurFrag,
tex: highPassFBO,
direction: [1, 0],
});
blur({
fbo: vBlurFBO,
frag: blurFrag,
tex: hBlurFBO,
direction: [0, 1],
});
} }
combine({ frag: combineFrag.text() }); combine({ frag: combineFrag });
} }
); );
}; };

View File

@@ -1,4 +1,5 @@
import { loadImage, loadText, makePassFBO, makePass } from "./utils.js"; import { loadImage, loadText, makePassFBO, makePass } from "./utils.js";
import imagePassFrag from "../../shaders/glsl/imagePass.frag.glsl";
// Multiplies the rendered rain and bloom by a loaded in image // Multiplies the rendered rain and bloom by a loaded in image
@@ -8,7 +9,6 @@ export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL;
const background = loadImage(regl, bgURL); const background = loadImage(regl, bgURL);
const imagePassFrag = loadText("shaders/glsl/imagePass.frag.glsl");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
@@ -22,11 +22,11 @@ export default ({ regl, config }, inputs) => {
{ {
primary: output, primary: output,
}, },
Promise.all([background.loaded, imagePassFrag.loaded]), Promise.all([background.loaded]),
(w, h) => output.resize(w, h), (w, h) => output.resize(w, h),
(shouldRender) => { (shouldRender) => {
if (shouldRender) { if (shouldRender) {
render({ frag: imagePassFrag.text() }); render({ frag: imagePassFrag });
} }
} }
); );

View File

@@ -1,3 +1,6 @@
// import HoloPlayCore from "holoplay-core";
const HoloPlayCore = require("holoplay-core");
const recordedDevice = { const recordedDevice = {
buttons: [0, 0, 0, 0], buttons: [0, 0, 0, 0],
calibration: { calibration: {
@@ -46,15 +49,24 @@ const interpretDevice = (device) => {
); );
const screenInches = calibration.screenW / calibration.DPI; const screenInches = calibration.screenW / calibration.DPI;
const pitch = calibration.pitch * screenInches * Math.cos(Math.atan(1.0 / calibration.slope)); const pitch =
const tilt = (calibration.screenH / (calibration.screenW * calibration.slope)) * -(calibration.flipImageX * 2 - 1); calibration.pitch *
screenInches *
Math.cos(Math.atan(1.0 / calibration.slope));
const tilt =
(calibration.screenH / (calibration.screenW * calibration.slope)) *
-(calibration.flipImageX * 2 - 1);
const subp = 1 / (calibration.screenW * 3); const subp = 1 / (calibration.screenW * 3);
const defaultQuilt = device.defaultQuilt; const defaultQuilt = device.defaultQuilt;
const quiltViewPortion = [ const quiltViewPortion = [
(Math.floor(defaultQuilt.quiltX / defaultQuilt.tileX) * defaultQuilt.tileX) / defaultQuilt.quiltX, (Math.floor(defaultQuilt.quiltX / defaultQuilt.tileX) *
(Math.floor(defaultQuilt.quiltY / defaultQuilt.tileY) * defaultQuilt.tileY) / defaultQuilt.quiltY, defaultQuilt.tileX) /
defaultQuilt.quiltX,
(Math.floor(defaultQuilt.quiltY / defaultQuilt.tileY) *
defaultQuilt.tileY) /
defaultQuilt.quiltY,
]; ];
return { return {
@@ -74,7 +86,7 @@ export default async (useHoloplay = false, useRecordedDevice = false) => {
if (!useHoloplay) { if (!useHoloplay) {
return interpretDevice(null); return interpretDevice(null);
} }
const HoloPlayCore = await import("../../lib/holoplaycore.module.js"); // const HoloPlayCore = await import("../../lib/holoplaycore.module.js");
const device = await new Promise( const device = await new Promise(
(resolve, reject) => (resolve, reject) =>
new HoloPlayCore.Client( new HoloPlayCore.Client(

View File

@@ -1,5 +1,5 @@
import { makeFullScreenQuad, makePipeline } from "./utils.js"; import { makeFullScreenQuad, makePipeline } from "./utils.js";
import createREGL from "regl";
import makeRain from "./rainPass.js"; import makeRain from "./rainPass.js";
import makeBloomPass from "./bloomPass.js"; import makeBloomPass from "./bloomPass.js";
import makePalettePass from "./palettePass.js"; import makePalettePass from "./palettePass.js";
@@ -7,7 +7,11 @@ import makeStripePass from "./stripePass.js";
import makeImagePass from "./imagePass.js"; import makeImagePass from "./imagePass.js";
import makeQuiltPass from "./quiltPass.js"; import makeQuiltPass from "./quiltPass.js";
import makeMirrorPass from "./mirrorPass.js"; import makeMirrorPass from "./mirrorPass.js";
import { setupCamera, cameraCanvas, cameraAspectRatio } from "../camera.js"; import {
setupCamera,
cameraCanvas,
cameraAspectRatio,
} from "../utils/camera.js";
import getLKG from "./lkgHelper.js"; import getLKG from "./lkgHelper.js";
const effects = { const effects = {
@@ -25,23 +29,24 @@ const effects = {
const dimensions = { width: 1, height: 1 }; const dimensions = { width: 1, height: 1 };
const loadJS = (src) => // const loadJS = (src) =>
new Promise((resolve, reject) => { // new Promise((resolve, reject) => {
const tag = document.createElement("script"); // const tag = document.createElement("script");
tag.onload = resolve; // tag.onload = resolve;
tag.onerror = reject; // tag.onerror = reject;
tag.src = src; // tag.src = src;
document.body.appendChild(tag); // document.body.appendChild(tag);
}); // });
export default async (canvas, config) => { // Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
await Promise.all([loadJS("lib/regl.min.js"), loadJS("lib/gl-matrix.js")]);
export const createRain = async (canvas, config, gl) => {
const resize = () => { const resize = () => {
const devicePixelRatio = window.devicePixelRatio ?? 1; const dpr = window.devicePixelRatio || 1;
canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution); canvas.width = Math.ceil(window.innerWidth * dpr * config.resolution);
canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution); canvas.height = Math.ceil(window.innerHeight * dpr * config.resolution);
}; };
window.onresize = resize; window.onresize = resize;
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) { if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
window.ondblclick = () => { window.ondblclick = () => {
@@ -62,9 +67,16 @@ export default async (canvas, config) => {
await setupCamera(); await setupCamera();
} }
const extensions = ["OES_texture_half_float", "OES_texture_half_float_linear"]; const extensions = [
"OES_texture_half_float",
"OES_texture_half_float_linear",
];
// These extensions are also needed, but Safari misreports that they are missing // These extensions are also needed, but Safari misreports that they are missing
const optionalExtensions = ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"]; const optionalExtensions = [
"EXT_color_buffer_half_float",
"WEBGL_color_buffer_float",
"OES_standard_derivatives",
];
switch (config.testFix) { switch (config.testFix) {
case "fwidth_10_1_2022_A": case "fwidth_10_1_2022_A":
@@ -76,7 +88,12 @@ export default async (canvas, config) => {
break; break;
} }
const regl = createREGL({ canvas, pixelRatio: 1, extensions, optionalExtensions }); const regl = createREGL({
gl,
pixelRatio: 1,
extensions,
optionalExtensions,
});
const cameraTex = regl.texture(cameraCanvas); const cameraTex = regl.texture(cameraCanvas);
const lkg = await getLKG(config.useHoloplay, true); const lkg = await getLKG(config.useHoloplay, true);
@@ -85,10 +102,19 @@ export default async (canvas, config) => {
const fullScreenQuad = makeFullScreenQuad(regl); const fullScreenQuad = makeFullScreenQuad(regl);
const effectName = config.effect in effects ? config.effect : "palette"; const effectName = config.effect in effects ? config.effect : "palette";
const context = { regl, config, lkg, cameraTex, cameraAspectRatio }; const context = { regl, config, lkg, cameraTex, cameraAspectRatio };
const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName], makeQuiltPass]); const pipeline = makePipeline(context, [
makeRain,
makeBloomPass,
effects[effectName],
makeQuiltPass,
]);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
const drawToScreen = regl({ uniforms: screenUniforms }); const drawToScreen = regl({ uniforms: screenUniforms });
await Promise.all(pipeline.map((step) => step.ready)); await Promise.all(pipeline.map((step) => step.ready));
pipeline.forEach((step) => step.setSize(canvas.width, canvas.height));
dimensions.width = canvas.width;
dimensions.height = canvas.height;
const targetFrameTimeMilliseconds = 1000 / config.fps; const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN; let last = NaN;
@@ -104,7 +130,10 @@ export default async (canvas, config) => {
last = now; last = now;
} }
const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once == true; const shouldRender =
config.fps >= 60 ||
now - last >= targetFrameTimeMilliseconds ||
config.once == true;
if (shouldRender) { if (shouldRender) {
while (now - targetFrameTimeMilliseconds > last) { while (now - targetFrameTimeMilliseconds > last) {
@@ -115,7 +144,10 @@ export default async (canvas, config) => {
if (config.useCamera) { if (config.useCamera) {
cameraTex(cameraCanvas); cameraTex(cameraCanvas);
} }
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) { if (
dimensions.width !== viewportWidth ||
dimensions.height !== viewportHeight
) {
dimensions.width = viewportWidth; dimensions.width = viewportWidth;
dimensions.height = viewportHeight; dimensions.height = viewportHeight;
for (const step of pipeline) { for (const step of pipeline) {
@@ -129,4 +161,12 @@ export default async (canvas, config) => {
drawToScreen(); drawToScreen();
}); });
}); });
return { regl, tick, canvas };
};
export const destroyRain = ({ regl, tick, canvas }) => {
tick.cancel(); // stop RAF
regl.destroy(); // release all GPU resources & event listeners
//canvas.remove(); // drop from the DOM
}; };

View File

@@ -1,4 +1,5 @@
import { loadText, makePassFBO, makePass } from "./utils.js"; import { loadText, makePassFBO, makePass } from "./utils.js";
import mirrorPassFrag from "../../shaders/glsl/mirrorPass.frag.glsl";
let start; let start;
const numClicks = 5; const numClicks = 5;
@@ -15,7 +16,6 @@ window.onclick = (e) => {
export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => { export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const mirrorPassFrag = loadText("shaders/glsl/mirrorPass.frag.glsl");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
@@ -36,14 +36,14 @@ export default ({ regl, config, cameraTex, cameraAspectRatio }, inputs) => {
{ {
primary: output, primary: output,
}, },
Promise.all([mirrorPassFrag.loaded]), null, // No async loading, glsl bundled and loaded into memory at document load
(w, h) => { (w, h) => {
output.resize(w, h); output.resize(w, h);
aspectRatio = w / h; aspectRatio = w / h;
}, },
(shouldRender) => { (shouldRender) => {
if (shouldRender) { if (shouldRender) {
render({ frag: mirrorPassFrag.text() }); render({ frag: mirrorPassFrag });
} }
} }
); );

View File

@@ -1,5 +1,6 @@
import colorToRGB from "../colorToRGB.js"; import colorToRGB from "../utils/colorToRGB";
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js"; import { make1DTexture, makePassFBO, makePass } from "./utils.js";
import palettePassFrag from "../../shaders/glsl/palettePass.frag.glsl";
// Maps the brightness of the rendered rain and bloom to colors // Maps the brightness of the rendered rain and bloom to colors
// in a 1D gradient palette texture generated from the passed-in color sequence // in a 1D gradient palette texture generated from the passed-in color sequence
@@ -16,7 +17,9 @@ const makePalette = (regl, entries) => {
.sort((e1, e2) => e1.at - e2.at) .sort((e1, e2) => e1.at - e2.at)
.map((entry) => ({ .map((entry) => ({
rgb: colorToRGB(entry.color), rgb: colorToRGB(entry.color),
arrayIndex: Math.floor(Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)), arrayIndex: Math.floor(
Math.max(Math.min(1, entry.at), 0) * (PALETTE_SIZE - 1)
),
})); }));
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 }); sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
sortedEntries.push({ sortedEntries.push({
@@ -57,9 +60,14 @@ const makePalette = (regl, entries) => {
export default ({ regl, config }, inputs) => { export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const paletteTex = makePalette(regl, config.palette); const paletteTex = makePalette(regl, config.palette);
const { backgroundColor, cursorColor, glintColor, cursorIntensity, glintIntensity, ditherMagnitude } = config; const {
backgroundColor,
const palettePassFrag = loadText("shaders/glsl/palettePass.frag.glsl"); cursorColor,
glintColor,
cursorIntensity,
glintIntensity,
ditherMagnitude,
} = config;
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
@@ -86,7 +94,7 @@ export default ({ regl, config }, inputs) => {
(w, h) => output.resize(w, h), (w, h) => output.resize(w, h),
(shouldRender) => { (shouldRender) => {
if (shouldRender) { if (shouldRender) {
render({ frag: palettePassFrag.text() }); render({ frag: palettePassFrag });
} }
} }
); );

View File

@@ -1,4 +1,5 @@
import { loadText, makePassFBO, makePass } from "./utils.js"; import { makePassFBO, makePass } from "./utils.js";
import quiltPassFrag from "../../shaders/glsl/quiltPass.frag.glsl";
// Multiplies the rendered rain and bloom by a loaded in image // Multiplies the rendered rain and bloom by a loaded in image
@@ -10,7 +11,7 @@ export default ({ regl, config, lkg }, inputs) => {
} }
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const quiltPassFrag = loadText("shaders/glsl/quiltPass.frag.glsl");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
uniforms: { uniforms: {
@@ -23,11 +24,11 @@ export default ({ regl, config, lkg }, inputs) => {
{ {
primary: output, primary: output,
}, },
Promise.all([quiltPassFrag.loaded]), null,
(w, h) => output.resize(w, h), (w, h) => output.resize(w, h),
(shouldRender) => { (shouldRender) => {
if (shouldRender) { if (shouldRender) {
render({ frag: quiltPassFrag.text() }); render({ frag: quiltPassFrag });
} }
} }
); );

View File

@@ -1,6 +1,21 @@
import { loadImage, loadText, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js"; import {
loadImage,
makePassFBO,
makeDoubleBuffer,
makePass,
} from "./utils.js";
import { mat4, vec3 } from "gl-matrix";
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";
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 rippleTypes = { const rippleTypes = {
box: 0, box: 0,
@@ -20,7 +35,6 @@ const makeComputeDoubleBuffer = (regl, height, width) =>
height, height,
wrapT: "clamp", wrapT: "clamp",
type: "half float", type: "half float",
data: Array(width * height * 4).fill(0)
}); });
const numVerticesPerQuad = 2 * 3; const numVerticesPerQuad = 2 * 3;
@@ -31,38 +45,47 @@ const brVert = [1, 1];
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert]; const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
export default ({ regl, config, lkg }) => { export default ({ regl, config, lkg }) => {
const { mat2, mat4, vec2, vec3 } = glMatrix;
// The volumetric mode multiplies the number of columns // The volumetric mode multiplies the number of columns
// to reach the desired density, and then overlaps them // to reach the desired density, and then overlaps them
const volumetric = config.volumetric; const volumetric = config.volumetric;
const density = volumetric && config.effect !== "none" ? config.density : 1; const density = volumetric && config.effect !== "none" ? config.density : 1;
const [numRows, numColumns] = [config.numColumns, Math.floor(config.numColumns * density)]; const [numRows, numColumns] = [
config.numColumns,
Math.floor(config.numColumns * density),
];
// The volumetric mode requires us to create a grid of quads, // The volumetric mode requires us to create a grid of quads,
// rather than a single quad for our geometry // rather than a single quad for our geometry
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1]; const [numQuadRows, numQuadColumns] = volumetric
? [numRows, numColumns]
: [1, 1];
const numQuads = numQuadRows * numQuadColumns; const numQuads = numQuadRows * numQuadColumns;
const quadSize = [1 / numQuadColumns, 1 / numQuadRows]; const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
// Various effect-related values // Various effect-related values
const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1; const rippleType =
config.rippleTypeName in rippleTypes
? rippleTypes[config.rippleTypeName]
: -1;
const slantVec = [Math.cos(config.slant), Math.sin(config.slant)]; const slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1); const slantScale =
1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
const showDebugView = config.effect === "none"; const showDebugView = config.effect === "none";
const glyphTransform = mat2.fromScaling(mat2.create(), vec2.fromValues(config.glyphFlip ? -1 : 1, 1));
mat2.rotate(glyphTransform, glyphTransform, (config.glyphRotation * Math.PI) / 180);
const commonUniforms = { const commonUniforms = {
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]), ...extractEntries(config, [
"animationSpeed",
"glyphHeightToWidth",
"glyphSequenceLength",
"glyphTextureGridSize",
]),
numColumns, numColumns,
numRows, numRows,
showDebugView, showDebugView,
}; };
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns); const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
const rainPassIntro = loadText("shaders/glsl/rainPass.intro.frag.glsl");
const introUniforms = { const introUniforms = {
...commonUniforms, ...commonUniforms,
...extractEntries(config, ["fallSpeed", "skipIntro"]), ...extractEntries(config, ["fallSpeed", "skipIntro"]),
@@ -77,11 +100,21 @@ export default ({ regl, config, lkg }) => {
framebuffer: introDoubleBuffer.front, framebuffer: introDoubleBuffer.front,
}); });
const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const raindropDoubleBuffer = makeComputeDoubleBuffer(
const rainPassRaindrop = loadText("shaders/glsl/rainPass.raindrop.frag.glsl"); regl,
numRows,
numColumns
);
const raindropUniforms = { const raindropUniforms = {
...commonUniforms, ...commonUniforms,
...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops", "skipIntro"]), ...extractEntries(config, [
"brightnessDecay",
"fallSpeed",
"raindropLength",
"loops",
"skipIntro",
]),
}; };
const raindrop = regl({ const raindrop = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
@@ -95,7 +128,7 @@ export default ({ regl, config, lkg }) => {
}); });
const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
const rainPassSymbol = loadText("shaders/glsl/rainPass.symbol.frag.glsl");
const symbolUniforms = { const symbolUniforms = {
...commonUniforms, ...commonUniforms,
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]), ...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]),
@@ -112,10 +145,16 @@ export default ({ regl, config, lkg }) => {
}); });
const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns); const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
const rainPassEffect = loadText("shaders/glsl/rainPass.effect.frag.glsl");
const effectUniforms = { const effectUniforms = {
...commonUniforms, ...commonUniforms,
...extractEntries(config, ["hasThunder", "rippleScale", "rippleSpeed", "rippleThickness", "loops"]), ...extractEntries(config, [
"hasThunder",
"rippleScale",
"rippleSpeed",
"rippleThickness",
"loops",
]),
rippleType, rippleType,
}; };
const effect = regl({ const effect = regl({
@@ -142,8 +181,6 @@ export default ({ regl, config, lkg }) => {
const glintMSDF = loadImage(regl, config.glintMSDFURL); const glintMSDF = loadImage(regl, config.glintMSDFURL);
const baseTexture = loadImage(regl, config.baseTextureURL, true); const baseTexture = loadImage(regl, config.baseTextureURL, true);
const glintTexture = loadImage(regl, config.glintTextureURL, true); const glintTexture = loadImage(regl, config.glintTextureURL, true);
const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl");
const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl");
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const renderUniforms = { const renderUniforms = {
...commonUniforms, ...commonUniforms,
@@ -165,7 +202,6 @@ export default ({ regl, config, lkg }) => {
"glyphEdgeCrop", "glyphEdgeCrop",
"isPolar", "isPolar",
]), ]),
glyphTransform,
density, density,
numQuadColumns, numQuadColumns,
numQuadRows, numQuadRows,
@@ -195,6 +231,7 @@ export default ({ regl, config, lkg }) => {
glintMSDF: glintMSDF.texture, glintMSDF: glintMSDF.texture,
baseTexture: baseTexture.texture, baseTexture: baseTexture.texture,
glintTexture: glintTexture.texture, glintTexture: glintTexture.texture,
glyphTransform: regl.prop('glyphTransform'),
msdfPxRange: 4.0, msdfPxRange: 4.0,
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()], glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
@@ -218,6 +255,7 @@ export default ({ regl, config, lkg }) => {
// Camera and transform math for the volumetric mode // Camera and transform math for the volumetric mode
const screenSize = [1, 1]; const screenSize = [1, 1];
//const { mat4, vec3 } = glMatrix;
const transform = mat4.create(); const transform = mat4.create();
if (volumetric && config.isometric) { if (volumetric && config.isometric) {
mat4.rotateX(transform, transform, (Math.PI * 1) / 8); mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
@@ -244,11 +282,6 @@ export default ({ regl, config, lkg }) => {
glintMSDF.loaded, glintMSDF.loaded,
baseTexture.loaded, baseTexture.loaded,
glintTexture.loaded, glintTexture.loaded,
rainPassIntro.loaded,
rainPassRaindrop.loaded,
rainPassSymbol.loaded,
rainPassVert.loaded,
rainPassFrag.loaded,
]), ]),
(w, h) => { (w, h) => {
output.resize(w, h); output.resize(w, h);
@@ -266,15 +299,40 @@ export default ({ regl, config, lkg }) => {
if (volumetric && config.isometric) { if (volumetric && config.isometric) {
if (aspectRatio > 1) { if (aspectRatio > 1) {
mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000); mat4.ortho(
camera,
-1.5 * aspectRatio,
1.5 * aspectRatio,
-1.5,
1.5,
-1000,
1000
);
} else { } else {
mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000); mat4.ortho(
camera,
-1.5,
1.5,
-1.5 / aspectRatio,
1.5 / aspectRatio,
-1000,
1000
);
} }
} else if (lkg.enabled) { } else if (lkg.enabled) {
mat4.perspective(camera, (Math.PI / 180) * lkg.fov, lkg.quiltAspect, 0.0001, 1000); mat4.perspective(
camera,
(Math.PI / 180) * lkg.fov,
lkg.quiltAspect,
0.0001,
1000
);
const distanceToTarget = -1; // TODO: Get from somewhere else const distanceToTarget = -1; // TODO: Get from somewhere else
let vantagePointAngle = (Math.PI / 180) * lkg.viewCone * (index / (numVantagePoints - 1) - 0.5); let vantagePointAngle =
(Math.PI / 180) *
lkg.viewCone *
(index / (numVantagePoints - 1) - 0.5);
if (isNaN(vantagePointAngle)) { if (isNaN(vantagePointAngle)) {
vantagePointAngle = 0; vantagePointAngle = 0;
} }
@@ -282,9 +340,19 @@ export default ({ regl, config, lkg }) => {
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0)); mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
camera[8] = -xOffset / (distanceToTarget * Math.tan((Math.PI / 180) * 0.5 * lkg.fov) * lkg.quiltAspect); // Is this right?? camera[8] =
-xOffset /
(distanceToTarget *
Math.tan((Math.PI / 180) * 0.5 * lkg.fov) *
lkg.quiltAspect); // Is this right??
} else { } else {
mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); mat4.perspective(
camera,
(Math.PI / 180) * 90,
aspectRatio,
0.0001,
1000
);
} }
const viewport = { const viewport = {
@@ -296,13 +364,14 @@ export default ({ regl, config, lkg }) => {
vantagePoints.push({ camera, viewport }); vantagePoints.push({ camera, viewport });
} }
} }
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; [screenSize[0], screenSize[1]] =
aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
}, },
(shouldRender) => { (shouldRender) => {
intro({ frag: rainPassIntro.text() }); intro({ frag: rainPassIntro });
raindrop({ frag: rainPassRaindrop.text() }); raindrop({ frag: rainPassRaindrop });
symbol({ frag: rainPassSymbol.text() }); symbol({ frag: rainPassSymbol });
effect({ frag: rainPassEffect.text() }); effect({ frag: rainPassEffect });
if (shouldRender) { if (shouldRender) {
regl.clear({ regl.clear({
@@ -312,7 +381,14 @@ export default ({ regl, config, lkg }) => {
}); });
for (const vantagePoint of vantagePoints) { for (const vantagePoint of vantagePoints) {
render({ ...vantagePoint, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() }); render({
...vantagePoint,
transform,
screenSize,
vert: rainPassVert,
frag: rainPassFrag,
glyphTransform: [1, 0, 0, 1]
});
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import colorToRGB from "../colorToRGB.js"; import colorToRGB from "../utils/colorToRGB";
import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js"; import { make1DTexture, makePassFBO, makePass } from "./utils";
import stripePassFrag from "../../shaders/glsl/stripePass.frag.glsl";
// Multiplies the rendered rain and bloom by a 1D gradient texture // Multiplies the rendered rain and bloom by a 1D gradient texture
// generated from the passed-in color sequence // generated from the passed-in color sequence
@@ -30,17 +31,27 @@ const prideStripeColors = [
export default ({ regl, config }, inputs) => { export default ({ regl, config }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const { backgroundColor, cursorColor, glintColor, cursorIntensity, glintIntensity, ditherMagnitude } = config; const {
backgroundColor,
cursorColor,
glintColor,
cursorIntensity,
glintIntensity,
ditherMagnitude,
} = config;
// Expand and convert stripe colors into 1D texture data // Expand and convert stripe colors into 1D texture data
const stripeColors = "stripeColors" in config ? config.stripeColors : config.effect === "pride" ? prideStripeColors : transPrideStripeColors; const stripeColors =
"stripeColors" in config
? config.stripeColors
: config.effect === "pride"
? prideStripeColors
: transPrideStripeColors;
const stripeTex = make1DTexture( const stripeTex = make1DTexture(
regl, regl,
stripeColors.map((color) => [...colorToRGB(color), 1]) stripeColors.map((color) => [...colorToRGB(color), 1])
); );
const stripePassFrag = loadText("shaders/glsl/stripePass.frag.glsl");
const render = regl({ const render = regl({
frag: regl.prop("frag"), frag: regl.prop("frag"),
@@ -62,11 +73,11 @@ export default ({ regl, config }, inputs) => {
{ {
primary: output, primary: output,
}, },
stripePassFrag.loaded, null,
(w, h) => output.resize(w, h), (w, h) => output.resize(w, h),
(shouldRender) => { (shouldRender) => {
if (shouldRender) { if (shouldRender) {
render({ frag: stripePassFrag.text() }); render({ frag: stripePassFrag });
} }
} }
); );

617
js/utils/config.js Normal file
View File

@@ -0,0 +1,617 @@
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";
const fonts = {
coptic: {
// The script the Gnostic codices were written in
glyphMSDFURL: msdfCoptic,
glyphSequenceLength: 32,
glyphTextureGridSize: [8, 8],
},
gothic: {
// The script the Codex Argenteus was written in
glyphMSDFURL: msdfGothic,
glyphSequenceLength: 27,
glyphTextureGridSize: [8, 8],
},
matrixcode: {
// The glyphs seen in the film trilogy
glyphMSDFURL: msdfMatrixCode,
glyphSequenceLength: 57,
glyphTextureGridSize: [8, 8],
},
megacity: {
// The glyphs seen in the film trilogy
glyphMSDFURL: megacity,
glyphSequenceLength: 64,
glyphTextureGridSize: [8, 8],
},
resurrections: {
// The glyphs seen in the film trilogy
glyphMSDFURL: msdfRes,
glintMSDFURL: msdfResGlint,
glyphSequenceLength: 135,
glyphTextureGridSize: [13, 12],
},
huberfishA: {
glyphMSDFURL: msdfHuberfishA,
glyphSequenceLength: 34,
glyphTextureGridSize: [6, 6],
},
huberfishD: {
glyphMSDFURL: msdfHuberfishD,
glyphSequenceLength: 34,
glyphTextureGridSize: [6, 6],
},
gtarg_tenretniolleh: {
glyphMSDFURL: msdfGtargTenretni,
glyphSequenceLength: 36,
glyphTextureGridSize: [6, 6],
},
gtarg_alientext: {
glyphMSDFURL: msdfGtargAlienText,
glyphSequenceLength: 38,
glyphTextureGridSize: [8, 5],
},
neomatrixology: {
glyphMSDFURL: msdfNeoMatrixology,
glyphSequenceLength: 12,
glyphTextureGridSize: [4, 4],
},
};
const textureURLs = {
sand: texSand,
pixels: texPixels,
mesh: texMesh,
metal: texMetal,
};
const hsl = (...values) => ({ space: "hsl", values });
const rgb = (...values) => ({ space: "rgb", values });
const defaults = {
font: "matrixcode",
effect: "palette", // The name of the effect to apply at the end of the process— mainly handles coloration
baseTexture: null, // The name of the texture to apply to the base layer of the glyphs
glintTexture: null, // The name of the texture to apply to the glint layer of the glyphs
useCamera: false,
backgroundColor: hsl(0, 0, 0), // The color "behind" the glyphs
isolateCursor: true, // Whether the "cursor"— the brightest glyph at the bottom of a raindrop— has its own color
cursorColor: hsl(0.242, 1, 0.73), // The color of the cursor
cursorIntensity: 2, // The intensity of the cursor
isolateGlint: false, // Whether the "glint"— highlights on certain symbols in the font— should appear
glintColor: hsl(0, 0, 1), // The color of the glint
glintIntensity: 1, // The intensity of the glint
volumetric: false, // A mode where the raindrops appear in perspective
animationSpeed: 1, // The global rate that all animations progress
fps: 60, // The target frame rate (frames per second) of the effect
forwardSpeed: 0.25, // The speed volumetric rain approaches the eye
bloomStrength: 0.7, // The intensity of the bloom
bloomSize: 0.4, // The amount the bloom calculation is scaled
highPassThreshold: 0.1, // The minimum brightness that is still blurred
cycleSpeed: 0.03, // The speed glyphs change
cycleFrameSkip: 1, // The global minimum number of frames between glyphs cycling
baseBrightness: -0.5, // The brightness of the glyphs, before any effects are applied
baseContrast: 1.1, // The contrast of the glyphs, before any effects are applied
glintBrightness: -1.5, // The brightness of the glints, before any effects are applied
glintContrast: 2.5, // The contrast of the glints, before any effects are applied
brightnessOverride: 0.0, // A global override to the brightness of displayed glyphs. Only used if it is > 0.
brightnessThreshold: 0, // The minimum brightness for a glyph to still be considered visible
brightnessDecay: 1.0, // The rate at which glyphs light up and dim
ditherMagnitude: 0.05, // The magnitude of the random per-pixel dimming
fallSpeed: 0.3, // The speed the raindrops progress downwards
glyphEdgeCrop: 0.0, // The border around a glyph in a font texture that should be cropped out
glyphHeightToWidth: 1, // The aspect ratio of glyphs
glyphVerticalSpacing: 1, // The ratio of the vertical distance between glyphs to their height
glyphFlip: false, // Whether to horizontally reflect the glyphs
glyphRotation: 0, // An angle to rotate the glyphs. Currently limited to 90° increments
hasThunder: false, // An effect that adds dramatic lightning flashes
isPolar: false, // Whether the glyphs arc across the screen or sit in a standard grid
rippleTypeName: null, // The variety of the ripple effect
rippleThickness: 0.2, // The thickness of the ripple effect
rippleScale: 30, // The size of the ripple effect
rippleSpeed: 0.2, // The rate at which the ripple effect progresses
numColumns: 80, // The maximum dimension of the glyph grid
density: 1, // In volumetric mode, the number of actual columns compared to the grid
palette: [
// The color palette that glyph brightness is color mapped to
{ color: hsl(0.3, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.3, 0.9, 0.2), at: 0.2 },
{ color: hsl(0.3, 0.9, 0.7), at: 0.7 },
{ color: hsl(0.3, 0.9, 0.8), at: 0.8 },
],
raindropLength: 0.75, // Adjusts the frequency of raindrops (and their length) in a column
slant: 0, // The angle at which rain falls; the orientation of the glyph grid
resolution: 0.75, // An overall scale multiplier
useHalfFloat: false,
renderer: "regl", // The preferred web graphics API
suppressWarnings: false, // Whether to show warnings to visitors on load
isometric: false,
useHoloplay: false,
loops: false,
skipIntro: true,
testFix: null,
};
const versions = {
classic: {},
megacity: {
font: "megacity",
animationSpeed: 0.5,
numColumns: 40,
},
neomatrixology: {
font: "neomatrixology",
animationSpeed: 0.8,
numColumns: 40,
palette: [
{ color: hsl(0.15, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.15, 0.9, 0.2), at: 0.2 },
{ color: hsl(0.15, 0.9, 0.7), at: 0.7 },
{ color: hsl(0.15, 0.9, 0.8), at: 0.8 },
],
cursorColor: hsl(0.167, 1, 0.75),
cursorIntensity: 2,
},
operator: {
cursorColor: hsl(0.375, 1, 0.66),
cursorIntensity: 3,
bloomSize: 0.6,
bloomStrength: 0.75,
highPassThreshold: 0.0,
cycleSpeed: 0.01,
cycleFrameSkip: 8,
brightnessOverride: 0.22,
brightnessThreshold: 0,
fallSpeed: 0.6,
glyphEdgeCrop: 0.15,
glyphHeightToWidth: 1.35,
rippleTypeName: "box",
numColumns: 108,
palette: [
{ color: hsl(0.4, 0.8, 0.0), at: 0.0 },
{ color: hsl(0.4, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.4, 0.8, 1.0), at: 1.0 },
],
raindropLength: 1.5,
},
nightmare: {
font: "gothic",
isolateCursor: false,
highPassThreshold: 0.7,
baseBrightness: -0.8,
brightnessDecay: 0.75,
fallSpeed: 1.2,
hasThunder: true,
numColumns: 60,
cycleSpeed: 0.35,
palette: [
{ color: hsl(0.0, 1.0, 0.0), at: 0.0 },
{ color: hsl(0.0, 1.0, 0.2), at: 0.2 },
{ color: hsl(0.0, 1.0, 0.4), at: 0.4 },
{ color: hsl(0.1, 1.0, 0.7), at: 0.7 },
{ color: hsl(0.2, 1.0, 1.0), at: 1.0 },
],
raindropLength: 0.5,
slant: (22.5 * Math.PI) / 180,
},
paradise: {
font: "coptic",
isolateCursor: false,
bloomStrength: 1,
highPassThreshold: 0,
cycleSpeed: 0.005,
baseBrightness: -1.3,
baseContrast: 2,
brightnessDecay: 0.05,
fallSpeed: 0.02,
isPolar: true,
rippleTypeName: "circle",
rippleSpeed: 0.1,
numColumns: 40,
palette: [
{ color: hsl(0.0, 0.0, 0.0), at: 0.0 },
{ color: hsl(0.0, 0.8, 0.3), at: 0.3 },
{ color: hsl(0.1, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.1, 1.0, 0.6), at: 0.6 },
{ color: hsl(0.1, 1.0, 0.9), at: 0.9 },
],
raindropLength: 0.4,
},
resurrections: {
font: "resurrections",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
baseBrightness: -0.7,
baseContrast: 1.17,
highPassThreshold: 0,
numColumns: 70,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.375, 0.9, 0.0), at: 0.0 },
{ color: hsl(0.375, 1.0, 0.6), at: 0.92 },
{ color: hsl(0.375, 1.0, 1.0), at: 1.0 },
],
},
trinity: {
font: "resurrections",
glintTexture: "metal",
baseTexture: "pixels",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.131, 1, 0.6),
glintIntensity: 3,
glintBrightness: -0.5,
glintContrast: 1.5,
baseBrightness: -0.4,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.37, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.37, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
volumetric: true,
forwardSpeed: 0.2,
raindropLength: 0.3,
density: 0.75,
},
morpheus: {
font: "resurrections",
glintTexture: "mesh",
baseTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.333, 1, 0.85),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.4, 1, 0.5),
glintIntensity: 2,
glintBrightness: -1.5,
glintContrast: 3,
baseBrightness: -0.3,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.97, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.97, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.015,
volumetric: true,
forwardSpeed: 0.1,
raindropLength: 0.4,
density: 0.75,
},
bugs: {
font: "resurrections",
glintTexture: "sand",
baseTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.619, 1, 0.65),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.625, 1, 0.6),
glintIntensity: 3,
glintBrightness: -1,
glintContrast: 3,
baseBrightness: -0.3,
baseContrast: 1.5,
highPassThreshold: 0,
numColumns: 60,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.12, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.14, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
volumetric: true,
forwardSpeed: 0.4,
raindropLength: 0.3,
density: 0.75,
},
palimpsest: {
font: "huberfishA",
isolateCursor: false,
bloomStrength: 0.2,
numColumns: 40,
raindropLength: 1.2,
cycleFrameSkip: 3,
fallSpeed: 0.5,
slant: Math.PI * -0.0625,
palette: [
{ color: hsl(0.15, 0.25, 0.9), at: 0.0 },
{ color: hsl(0.6, 0.8, 0.1), at: 0.4 },
],
},
twilight: {
font: "huberfishD",
cursorColor: hsl(0.167, 1, 0.8),
cursorIntensity: 1.5,
bloomStrength: 0.1,
numColumns: 50,
raindropLength: 0.9,
fallSpeed: 0.1,
highPassThreshold: 0.0,
palette: [
{ color: hsl(0.6, 1.0, 0.05), at: 0.0 },
{ color: hsl(0.6, 0.8, 0.1), at: 0.1 },
{ color: hsl(0.88, 0.8, 0.5), at: 0.5 },
{ color: hsl(0.15, 1.0, 0.6), at: 0.8 },
// { color: hsl(0.1, 1.0, 0.9), at: 1.0 },
],
},
holoplay: {
font: "resurrections",
glintTexture: "metal",
glyphEdgeCrop: 0.1,
cursorColor: hsl(0.292, 1, 0.8),
cursorIntensity: 2,
isolateGlint: true,
glintColor: hsl(0.131, 1, 0.6),
glintIntensity: 3,
glintBrightness: -0.5,
glintContrast: 1.5,
baseBrightness: -0.4,
baseContrast: 1.5,
highPassThreshold: 0,
cycleSpeed: 0.03,
bloomStrength: 0.7,
fallSpeed: 0.3,
palette: [
{ color: hsl(0.37, 0.6, 0.0), at: 0.0 },
{ color: hsl(0.37, 0.6, 0.5), at: 1.0 },
],
cycleSpeed: 0.01,
raindropLength: 0.3,
renderer: "regl",
numColumns: 20,
ditherMagnitude: 0,
bloomStrength: 0,
volumetric: true,
forwardSpeed: 0,
density: 3,
useHoloplay: true,
},
["3d"]: {
volumetric: true,
fallSpeed: 0.5,
cycleSpeed: 0.03,
baseBrightness: -0.9,
baseContrast: 1.5,
raindropLength: 0.3,
},
};
versions.throwback = versions.operator;
versions.updated = versions.resurrections;
versions["1999"] = versions.operator;
versions["2003"] = versions.classic;
versions["2021"] = versions.resurrections;
const range = (f, min = -Infinity, max = Infinity) =>
Math.max(min, Math.min(max, f));
const nullNaN = (f) => (isNaN(f) ? null : f);
const isTrue = (s) => s.toLowerCase().includes("true");
const parseColor = (isHSL) => (s) => ({
space: isHSL ? "hsl" : "rgb",
values: s.split(",").map(parseFloat),
});
const parseColors = (isHSL) => (s) => {
const values = s.split(",").map(parseFloat);
const space = isHSL ? "hsl" : "rgb";
return Array(Math.floor(values.length / 3))
.fill()
.map((_, index) => ({
space,
values: values.slice(index * 3, (index + 1) * 3),
}));
};
const parsePalette = (isHSL) => (s) => {
const values = s.split(",").map(parseFloat);
const space = isHSL ? "hsl" : "rgb";
return Array(Math.floor(values.length / 4))
.fill()
.map((_, index) => {
const colorValues = values.slice(index * 4, (index + 1) * 4);
return {
color: {
space,
values: colorValues.slice(0, 3),
},
at: colorValues[3],
};
});
};
const paramMapping = {
testFix: { key: "testFix", parser: (s) => s },
version: { key: "version", parser: (s) => s },
font: { key: "font", parser: (s) => s },
effect: { key: "effect", parser: (s) => s },
camera: { key: "useCamera", parser: isTrue },
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)),
},
bloomStrength: {
key: "bloomStrength",
parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
},
ditherMagnitude: {
key: "ditherMagnitude",
parser: (s) => nullNaN(range(parseFloat(s), 0, 1)),
},
url: { key: "bgURL", parser: (s) => s },
palette: { key: "palette", parser: parsePalette(false) },
stripeColors: { key: "stripeColors", parser: parseColors(false) },
backgroundColor: { key: "backgroundColor", parser: parseColor(false) },
cursorColor: { key: "cursorColor", parser: parseColor(false) },
glintColor: { key: "glintColor", parser: parseColor(false) },
paletteHSL: { key: "palette", parser: parsePalette(true) },
stripeHSL: { key: "stripeColors", parser: parseColors(true) },
backgroundHSL: { key: "backgroundColor", parser: parseColor(true) },
cursorHSL: { key: "cursorColor", parser: parseColor(true) },
glintHSL: { key: "glintColor", parser: parseColor(true) },
cursorIntensity: {
key: "cursorIntensity",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
glyphIntensity: {
key: "glyphIntensity",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
volumetric: { key: "volumetric", parser: isTrue },
glyphFlip: { key: "glyphFlip", parser: isTrue },
glyphRotation: {
key: "glyphRotation",
parser: (s) => nullNaN(range(parseFloat(s), 0, Infinity)),
},
loops: { key: "loops", parser: isTrue },
fps: { key: "fps", parser: (s) => nullNaN(range(parseFloat(s), 0, 60)) },
skipIntro: { key: "skipIntro", parser: isTrue },
renderer: { key: "renderer", parser: (s) => s },
suppressWarnings: { key: "suppressWarnings", parser: isTrue },
once: { key: "once", parser: isTrue },
isometric: { key: "isometric", parser: isTrue },
};
paramMapping.paletteRGB = paramMapping.palette;
paramMapping.stripeRGB = paramMapping.stripeColors;
paramMapping.backgroundRGB = paramMapping.backgroundColor;
paramMapping.cursorRGB = paramMapping.cursorColor;
paramMapping.glintRGB = paramMapping.glintColor;
paramMapping.width = paramMapping.numColumns;
paramMapping.dropLength = paramMapping.raindropLength;
paramMapping.angle = paramMapping.slant;
paramMapping.colors = paramMapping.stripeColors;
export default (urlParams) => {
const validParams = Object.fromEntries(
Object.entries(urlParams)
.filter(([key]) => key in paramMapping)
.map(([key, value]) => [
paramMapping[key].key,
paramMapping[key].parser(value),
])
.filter(([_, value]) => value != null)
);
if (validParams.effect != null) {
if (validParams.cursorColor == null) {
validParams.cursorColor = hsl(0, 0, 1);
}
if (validParams.cursorIntensity == null) {
validParams.cursorIntensity = 2;
}
if (validParams.glintColor == null) {
validParams.glintColor = hsl(0, 0, 1);
}
if (validParams.glyphIntensity == null) {
validParams.glyphIntensity = 1;
}
}
const version =
validParams.version in versions
? versions[validParams.version]
: versions.classic;
const fontName = [validParams.font, version.font, defaults.font].find(
(name) => name in fonts
);
const font = fonts[fontName];
const baseTextureURL =
textureURLs[
[version.baseTexture, defaults.baseTexture].find(
(name) => name in textureURLs
)
];
const hasBaseTexture = baseTextureURL != null;
const glintTextureURL =
textureURLs[
[version.glintTexture, defaults.glintTexture].find(
(name) => name in textureURLs
)
];
const hasGlintTexture = glintTextureURL != null;
const config = {
...defaults,
...version,
...font,
...validParams,
baseTextureURL,
glintTextureURL,
hasBaseTexture,
hasGlintTexture,
};
if (config.bloomSize <= 0) {
config.bloomStrength = 0;
}
return config;
};

7186
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "react-matrix-rain",
"version": "1.0.0",
"description": "web-based green code rain, made with love, for react",
"main": "dist/index.cjs.js",
"files": [
"/dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack serve --config ./webpack.config.js",
"build": "rollup -c"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"gl-matrix": "^3.4.3",
"holoplay-core": "^0.0.9",
"regl": "^2.1.0"
},
"devDependencies": {
"@babel/core": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-url": "^8.0.2",
"babel-loader": "^9.1.3",
"html-webpack-plugin": "^5.5.3",
"raw-loader": "^4.0.2",
"rollup": "^4.40.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-string": "^3.0.0",
"rollup-plugin-visualizer": "^5.14.0",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

11
public/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

44
rollup.config.mjs Normal file
View File

@@ -0,0 +1,44 @@
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import nodeResolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import babel from "@rollup/plugin-babel";
import url from "@rollup/plugin-url";
import { visualizer } from "rollup-plugin-visualizer"; // <- size report
import terser from "@rollup/plugin-terser";
import { string } from "rollup-plugin-string";
export default {
input: "js/Matrix.js",
external: ["react", "react-dom"], // keep them out of your bundle
plugins: [
peerDepsExternal(), // auto-exclude peerDeps
nodeResolve(), // so Rollup can find deps in node_modules
string({ include: ["**/*.glsl"] }),
url({ include: ["**/*.png"], limit: 0 }),
babel({
exclude: "node_modules/**", // transpile JSX
babelHelpers: "bundled",
presets: ["@babel/preset-react", "@babel/preset-env"],
}),
commonjs(), // turn CJS deps into ES
terser({
sourceMap: false, // <- suppress .map generation
format: { comments: false },
}),
visualizer({
filename: "dist/stats.html",
gzipSize: true,
brotliSize: true,
includeAssets: true,
}), // bundle-size treemap
],
output: [
{
file: "dist/index.cjs.js",
format: "cjs",
exports: "named",
sourcemap: false,
},
// { file: 'dist/index.esm.js', format: 'es' } // optional ESM build
],
};

View File

@@ -0,0 +1,10 @@
/* shaders/glsl/fullscreen.vert.glsl */
precision mediump float;
attribute vec2 aPosition; // will come from JS buffer
varying vec2 vUV;
void main () {
vUV = aPosition * 0.5 + 0.5; // (1…1) → (0…1)
gl_Position = vec4(aPosition, 0.0, 1.0);
}

54
webpack.config.js Normal file
View File

@@ -0,0 +1,54 @@
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: path.resolve(__dirname, "./js/index.js"),
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ["babel-loader"],
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|j?g|svg|gif)?$/,
type: "asset/resource",
},
{
test: /\.(glsl|frag|vert)$/i,
exclude: /node_modules/,
use: ["raw-loader"],
},
],
},
resolve: {
extensions: ["*", ".js", ".jsx"],
},
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[name].bundle.js",
clean: true,
},
devtool: "inline-source-map",
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "public/index.html"),
filename: "index.html",
}),
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
historyApiFallback: true,
static: path.resolve(__dirname, "./dist"),
compress: true,
hot: true,
open: true,
port: 3000,
},
};