mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-14 12:29:30 -07:00
Matrix React component 1.0.0
This commit is contained in:
6
.babelrc
Normal file
6
.babelrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
]
|
||||||
|
}
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal 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
|
||||||
16
README.md
16
README.md
@@ -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
140
js/Matrix.js
Normal 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>;
|
||||||
|
});
|
||||||
581
js/config.js
581
js/config.js
@@ -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
48
js/index.js
Normal 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 />);
|
||||||
@@ -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.
|
||||||
@@ -8,98 +13,136 @@ const pyramidHeight = 5;
|
|||||||
// A pyramid is just an array of FBOs, where each FBO is half the width
|
// A pyramid is just an array of FBOs, where each FBO is half the width
|
||||||
// and half the height of the FBO below it.
|
// and half the height of the FBO below it.
|
||||||
const makePyramid = (regl, height, halfFloat) =>
|
const makePyramid = (regl, height, halfFloat) =>
|
||||||
Array(height)
|
Array(height)
|
||||||
.fill()
|
.fill()
|
||||||
.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;
|
||||||
const enabled = bloomSize > 0 && bloomStrength > 0;
|
const enabled = bloomSize > 0 && bloomStrength > 0;
|
||||||
|
|
||||||
// If there's no bloom to apply, return a no-op pass with an empty bloom texture
|
// If there's no bloom to apply, return a no-op pass with an empty bloom texture
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return makePass({
|
return makePass({
|
||||||
primary: inputs.primary,
|
primary: inputs.primary,
|
||||||
bloom: makePassFBO(regl),
|
bloom: makePassFBO(regl),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build three pyramids of FBOs, one for each step in the process
|
// Build three pyramids of FBOs, one for each step in the process
|
||||||
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
const highPassPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||||
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
const hBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat);
|
||||||
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);
|
||||||
|
|
||||||
// The high pass restricts the blur to bright things in our input texture.
|
// one big triangle that covers the whole screen
|
||||||
const highPassFrag = loadText("shaders/glsl/bloomPass.highPass.frag.glsl");
|
const fullScreenTriangle = regl.buffer([
|
||||||
const highPass = regl({
|
-1, -1,
|
||||||
frag: regl.prop("frag"),
|
3, -1,
|
||||||
uniforms: {
|
-1, 3,
|
||||||
highPassThreshold,
|
]);
|
||||||
tex: regl.prop("tex"),
|
|
||||||
},
|
const commonDrawProps = {
|
||||||
framebuffer: regl.prop("fbo"),
|
attributes: { aPosition: fullScreenTriangle },
|
||||||
});
|
count: 3,
|
||||||
|
};
|
||||||
|
// The high pass restricts the blur to bright things in our input texture.
|
||||||
|
const highPass = regl({
|
||||||
|
...commonDrawProps,
|
||||||
|
vert: fsVert,
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
uniforms: {
|
||||||
|
highPassThreshold,
|
||||||
|
tex: regl.prop("tex"),
|
||||||
|
},
|
||||||
|
framebuffer: regl.prop("fbo"),
|
||||||
|
});
|
||||||
|
|
||||||
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
|
|
||||||
// The FBO pyramid's levels represent separate levels of detail;
|
|
||||||
// by blurring them all, this 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
|
|
||||||
|
|
||||||
const blurFrag = loadText("shaders/glsl/bloomPass.blur.frag.glsl");
|
|
||||||
const blur = regl({
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
uniforms: {
|
|
||||||
tex: regl.prop("tex"),
|
|
||||||
direction: regl.prop("direction"),
|
|
||||||
height: regl.context("viewportWidth"),
|
|
||||||
width: regl.context("viewportHeight"),
|
|
||||||
},
|
|
||||||
framebuffer: regl.prop("fbo"),
|
|
||||||
});
|
|
||||||
|
|
||||||
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
|
|
||||||
const combineFrag = loadText("shaders/glsl/bloomPass.combine.frag.glsl");
|
|
||||||
const combine = regl({
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
uniforms: {
|
|
||||||
bloomStrength,
|
|
||||||
...Object.fromEntries(vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])),
|
|
||||||
},
|
|
||||||
framebuffer: output,
|
|
||||||
});
|
|
||||||
|
|
||||||
return makePass(
|
// A 2D gaussian blur is just a 1D blur done horizontally, then done vertically.
|
||||||
{
|
// The FBO pyramid's levels represent separate levels of detail;
|
||||||
primary: inputs.primary,
|
// by blurring them all, this basic blur approximates a more complex gaussian:
|
||||||
bloom: output,
|
// https://web.archive.org/web/20191124072602/https://software.intel.com/en-us/articles/compute-shader-hdr-and-bloom
|
||||||
},
|
|
||||||
Promise.all([highPassFrag.loaded, blurFrag.loaded]),
|
|
||||||
(w, h) => {
|
|
||||||
// The blur pyramids can be lower resolution than the screen.
|
|
||||||
resizePyramid(highPassPyramid, w, h, bloomSize);
|
|
||||||
resizePyramid(hBlurPyramid, w, h, bloomSize);
|
|
||||||
resizePyramid(vBlurPyramid, w, h, bloomSize);
|
|
||||||
output.resize(w, h);
|
|
||||||
},
|
|
||||||
(shouldRender) => {
|
|
||||||
if (!shouldRender) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < pyramidHeight; i++) {
|
const blur = regl({
|
||||||
const highPassFBO = highPassPyramid[i];
|
...commonDrawProps,
|
||||||
const hBlurFBO = hBlurPyramid[i];
|
vert: fsVert,
|
||||||
const vBlurFBO = vBlurPyramid[i];
|
frag: regl.prop("frag"),
|
||||||
highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: i === 0 ? inputs.primary : highPassPyramid[i - 1] });
|
uniforms: {
|
||||||
blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] });
|
tex: regl.prop("tex"),
|
||||||
blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] });
|
direction: regl.prop("direction"),
|
||||||
}
|
height: regl.context("viewportWidth"),
|
||||||
|
width: regl.context("viewportHeight"),
|
||||||
|
},
|
||||||
|
framebuffer: regl.prop("fbo"),
|
||||||
|
});
|
||||||
|
|
||||||
combine({ frag: combineFrag.text() });
|
// The pyramid of textures gets flattened (summed) into a final blurry "bloom" texture
|
||||||
}
|
const combine = regl({
|
||||||
);
|
...commonDrawProps,
|
||||||
|
vert: fsVert,
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
uniforms: {
|
||||||
|
bloomStrength,
|
||||||
|
...Object.fromEntries(
|
||||||
|
vBlurPyramid.map((fbo, index) => [`pyr_${index}`, fbo])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
framebuffer: output,
|
||||||
|
});
|
||||||
|
|
||||||
|
return makePass(
|
||||||
|
{
|
||||||
|
primary: inputs.primary,
|
||||||
|
bloom: output,
|
||||||
|
},
|
||||||
|
// Promise.all([highPassFrag.loaded, blurFrag.loaded]),
|
||||||
|
(w, h) => {
|
||||||
|
// The blur pyramids can be lower resolution than the screen.
|
||||||
|
resizePyramid(highPassPyramid, w, h, bloomSize);
|
||||||
|
resizePyramid(hBlurPyramid, w, h, bloomSize);
|
||||||
|
resizePyramid(vBlurPyramid, w, h, bloomSize);
|
||||||
|
output.resize(w, h);
|
||||||
|
},
|
||||||
|
(shouldRender) => {
|
||||||
|
if (!shouldRender) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < pyramidHeight; i++) {
|
||||||
|
const highPassFBO = highPassPyramid[i];
|
||||||
|
const hBlurFBO = hBlurPyramid[i];
|
||||||
|
const vBlurFBO = vBlurPyramid[i];
|
||||||
|
highPass({
|
||||||
|
fbo: highPassFBO,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,89 +1,101 @@
|
|||||||
|
// 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: {
|
||||||
DPI: { value: 324 },
|
DPI: { value: 324 },
|
||||||
center: { value: 0.15018756687641144 },
|
center: { value: 0.15018756687641144 },
|
||||||
configVersion: "3.0",
|
configVersion: "3.0",
|
||||||
flipImageX: { value: 0 },
|
flipImageX: { value: 0 },
|
||||||
flipImageY: { value: 0 },
|
flipImageY: { value: 0 },
|
||||||
flipSubp: { value: 0 },
|
flipSubp: { value: 0 },
|
||||||
fringe: { value: 0 },
|
fringe: { value: 0 },
|
||||||
invView: { value: 1 },
|
invView: { value: 1 },
|
||||||
pitch: { value: 52.58013153076172 },
|
pitch: { value: 52.58013153076172 },
|
||||||
screenH: { value: 2048 },
|
screenH: { value: 2048 },
|
||||||
screenW: { value: 1536 },
|
screenW: { value: 1536 },
|
||||||
slope: { value: -7.145165920257568 },
|
slope: { value: -7.145165920257568 },
|
||||||
verticalAngle: { value: 0 },
|
verticalAngle: { value: 0 },
|
||||||
viewCone: { value: 40 },
|
viewCone: { value: 40 },
|
||||||
},
|
},
|
||||||
defaultQuilt: {
|
defaultQuilt: {
|
||||||
quiltAspect: 0.75,
|
quiltAspect: 0.75,
|
||||||
quiltX: 3840,
|
quiltX: 3840,
|
||||||
quiltY: 3840,
|
quiltY: 3840,
|
||||||
tileX: 8,
|
tileX: 8,
|
||||||
tileY: 6,
|
tileY: 6,
|
||||||
},
|
},
|
||||||
hardwareVersion: "portrait",
|
hardwareVersion: "portrait",
|
||||||
hwid: "LKG-P11063",
|
hwid: "LKG-P11063",
|
||||||
index: 0,
|
index: 0,
|
||||||
joystickIndex: -1,
|
joystickIndex: -1,
|
||||||
state: "ok",
|
state: "ok",
|
||||||
unityIndex: 1,
|
unityIndex: 1,
|
||||||
windowCoords: [1440, 900],
|
windowCoords: [1440, 900],
|
||||||
};
|
};
|
||||||
|
|
||||||
const interpretDevice = (device) => {
|
const interpretDevice = (device) => {
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
return { enabled: false, tileX: 1, tileY: 1 };
|
return { enabled: false, tileX: 1, tileY: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const fov = 15;
|
const fov = 15;
|
||||||
|
|
||||||
const calibration = Object.fromEntries(
|
const calibration = Object.fromEntries(
|
||||||
Object.entries(device.calibration)
|
Object.entries(device.calibration)
|
||||||
.map(([key, value]) => [key, value.value])
|
.map(([key, value]) => [key, value.value])
|
||||||
.filter(([key, value]) => value != null)
|
.filter(([key, value]) => value != null)
|
||||||
);
|
);
|
||||||
|
|
||||||
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 *
|
||||||
const subp = 1 / (calibration.screenW * 3);
|
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 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 {
|
||||||
...defaultQuilt,
|
...defaultQuilt,
|
||||||
...calibration,
|
...calibration,
|
||||||
pitch,
|
pitch,
|
||||||
tilt,
|
tilt,
|
||||||
subp,
|
subp,
|
||||||
|
|
||||||
quiltViewPortion,
|
quiltViewPortion,
|
||||||
fov,
|
fov,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (useHoloplay = false, useRecordedDevice = false) => {
|
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(
|
||||||
(data) => resolve(data.devices?.[0]),
|
(data) => resolve(data.devices?.[0]),
|
||||||
(error) => resolve(null)
|
(error) => resolve(null)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (device == null && useRecordedDevice) {
|
if (device == null && useRecordedDevice) {
|
||||||
return interpretDevice(recordedDevice);
|
return interpretDevice(recordedDevice);
|
||||||
}
|
}
|
||||||
return interpretDevice(device);
|
return interpretDevice(device);
|
||||||
};
|
};
|
||||||
|
|||||||
242
js/regl/main.js
242
js/regl/main.js
@@ -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,126 +7,166 @@ 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 = {
|
||||||
none: null,
|
none: null,
|
||||||
plain: makePalettePass,
|
plain: makePalettePass,
|
||||||
palette: makePalettePass,
|
palette: makePalettePass,
|
||||||
customStripes: makeStripePass,
|
customStripes: makeStripePass,
|
||||||
stripes: makeStripePass,
|
stripes: makeStripePass,
|
||||||
pride: makeStripePass,
|
pride: makeStripePass,
|
||||||
transPride: makeStripePass,
|
transPride: makeStripePass,
|
||||||
trans: makeStripePass,
|
trans: makeStripePass,
|
||||||
image: makeImagePass,
|
image: makeImagePass,
|
||||||
mirror: makeMirrorPass,
|
mirror: makeMirrorPass,
|
||||||
};
|
};
|
||||||
|
|
||||||
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")]);
|
|
||||||
|
|
||||||
const resize = () => {
|
export const createRain = async (canvas, config, gl) => {
|
||||||
const devicePixelRatio = window.devicePixelRatio ?? 1;
|
const resize = () => {
|
||||||
canvas.width = Math.ceil(canvas.clientWidth * devicePixelRatio * config.resolution);
|
const dpr = window.devicePixelRatio || 1;
|
||||||
canvas.height = Math.ceil(canvas.clientHeight * devicePixelRatio * config.resolution);
|
canvas.width = Math.ceil(window.innerWidth * dpr * config.resolution);
|
||||||
};
|
canvas.height = Math.ceil(window.innerHeight * dpr * config.resolution);
|
||||||
window.onresize = resize;
|
};
|
||||||
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
|
|
||||||
window.ondblclick = () => {
|
|
||||||
if (document.fullscreenElement == null) {
|
|
||||||
if (canvas.webkitRequestFullscreen != null) {
|
|
||||||
canvas.webkitRequestFullscreen();
|
|
||||||
} else {
|
|
||||||
canvas.requestFullscreen();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.exitFullscreen();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
resize();
|
|
||||||
|
|
||||||
if (config.useCamera) {
|
window.onresize = resize;
|
||||||
await setupCamera();
|
if (document.fullscreenEnabled || document.webkitFullscreenEnabled) {
|
||||||
}
|
window.ondblclick = () => {
|
||||||
|
if (document.fullscreenElement == null) {
|
||||||
|
if (canvas.webkitRequestFullscreen != null) {
|
||||||
|
canvas.webkitRequestFullscreen();
|
||||||
|
} else {
|
||||||
|
canvas.requestFullscreen();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
resize();
|
||||||
|
|
||||||
const extensions = ["OES_texture_half_float", "OES_texture_half_float_linear"];
|
if (config.useCamera) {
|
||||||
// These extensions are also needed, but Safari misreports that they are missing
|
await setupCamera();
|
||||||
const optionalExtensions = ["EXT_color_buffer_half_float", "WEBGL_color_buffer_float", "OES_standard_derivatives"];
|
}
|
||||||
|
|
||||||
switch (config.testFix) {
|
const extensions = [
|
||||||
case "fwidth_10_1_2022_A":
|
"OES_texture_half_float",
|
||||||
extensions.push("OES_standard_derivatives");
|
"OES_texture_half_float_linear",
|
||||||
break;
|
];
|
||||||
case "fwidth_10_1_2022_B":
|
// These extensions are also needed, but Safari misreports that they are missing
|
||||||
optionalExtensions.forEach((ext) => extensions.push(ext));
|
const optionalExtensions = [
|
||||||
extensions.length = 0;
|
"EXT_color_buffer_half_float",
|
||||||
break;
|
"WEBGL_color_buffer_float",
|
||||||
}
|
"OES_standard_derivatives",
|
||||||
|
];
|
||||||
|
|
||||||
const regl = createREGL({ canvas, pixelRatio: 1, extensions, optionalExtensions });
|
switch (config.testFix) {
|
||||||
|
case "fwidth_10_1_2022_A":
|
||||||
|
extensions.push("OES_standard_derivatives");
|
||||||
|
break;
|
||||||
|
case "fwidth_10_1_2022_B":
|
||||||
|
optionalExtensions.forEach((ext) => extensions.push(ext));
|
||||||
|
extensions.length = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const cameraTex = regl.texture(cameraCanvas);
|
const regl = createREGL({
|
||||||
const lkg = await getLKG(config.useHoloplay, true);
|
gl,
|
||||||
|
pixelRatio: 1,
|
||||||
|
extensions,
|
||||||
|
optionalExtensions,
|
||||||
|
});
|
||||||
|
|
||||||
// All this takes place in a full screen quad.
|
const cameraTex = regl.texture(cameraCanvas);
|
||||||
const fullScreenQuad = makeFullScreenQuad(regl);
|
const lkg = await getLKG(config.useHoloplay, true);
|
||||||
const effectName = config.effect in effects ? config.effect : "palette";
|
|
||||||
const context = { regl, config, lkg, cameraTex, cameraAspectRatio };
|
|
||||||
const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName], makeQuiltPass]);
|
|
||||||
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
|
|
||||||
const drawToScreen = regl({ uniforms: screenUniforms });
|
|
||||||
await Promise.all(pipeline.map((step) => step.ready));
|
|
||||||
|
|
||||||
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
// All this takes place in a full screen quad.
|
||||||
let last = NaN;
|
const fullScreenQuad = makeFullScreenQuad(regl);
|
||||||
|
const effectName = config.effect in effects ? config.effect : "palette";
|
||||||
|
const context = { regl, config, lkg, cameraTex, cameraAspectRatio };
|
||||||
|
const pipeline = makePipeline(context, [
|
||||||
|
makeRain,
|
||||||
|
makeBloomPass,
|
||||||
|
effects[effectName],
|
||||||
|
makeQuiltPass,
|
||||||
|
]);
|
||||||
|
|
||||||
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
|
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
|
||||||
if (config.once) {
|
const drawToScreen = regl({ uniforms: screenUniforms });
|
||||||
tick.cancel();
|
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 now = regl.now() * 1000;
|
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
||||||
|
let last = NaN;
|
||||||
|
|
||||||
if (isNaN(last)) {
|
const tick = regl.frame(({ viewportWidth, viewportHeight }) => {
|
||||||
last = now;
|
if (config.once) {
|
||||||
}
|
tick.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
const shouldRender = config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once == true;
|
const now = regl.now() * 1000;
|
||||||
|
|
||||||
if (shouldRender) {
|
if (isNaN(last)) {
|
||||||
while (now - targetFrameTimeMilliseconds > last) {
|
last = now;
|
||||||
last += targetFrameTimeMilliseconds;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.useCamera) {
|
const shouldRender =
|
||||||
cameraTex(cameraCanvas);
|
config.fps >= 60 ||
|
||||||
}
|
now - last >= targetFrameTimeMilliseconds ||
|
||||||
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
|
config.once == true;
|
||||||
dimensions.width = viewportWidth;
|
|
||||||
dimensions.height = viewportHeight;
|
if (shouldRender) {
|
||||||
for (const step of pipeline) {
|
while (now - targetFrameTimeMilliseconds > last) {
|
||||||
step.setSize(viewportWidth, viewportHeight);
|
last += targetFrameTimeMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fullScreenQuad(() => {
|
|
||||||
for (const step of pipeline) {
|
if (config.useCamera) {
|
||||||
step.execute(shouldRender);
|
cameraTex(cameraCanvas);
|
||||||
}
|
}
|
||||||
drawToScreen();
|
if (
|
||||||
});
|
dimensions.width !== viewportWidth ||
|
||||||
});
|
dimensions.height !== viewportHeight
|
||||||
|
) {
|
||||||
|
dimensions.width = viewportWidth;
|
||||||
|
dimensions.height = viewportHeight;
|
||||||
|
for (const step of pipeline) {
|
||||||
|
step.setSize(viewportWidth, viewportHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fullScreenQuad(() => {
|
||||||
|
for (const step of pipeline) {
|
||||||
|
step.execute(shouldRender);
|
||||||
|
}
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,45 +8,47 @@ import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
|
|||||||
// This shader introduces noise into the renders, to avoid banding
|
// This shader introduces noise into the renders, to avoid banding
|
||||||
|
|
||||||
const makePalette = (regl, entries) => {
|
const makePalette = (regl, entries) => {
|
||||||
const PALETTE_SIZE = 2048;
|
const PALETTE_SIZE = 2048;
|
||||||
const paletteColors = Array(PALETTE_SIZE);
|
const paletteColors = Array(PALETTE_SIZE);
|
||||||
|
|
||||||
// Convert HSL gradient into sorted RGB gradient, capping the ends
|
// Convert HSL gradient into sorted RGB gradient, capping the ends
|
||||||
const sortedEntries = entries
|
const sortedEntries = entries
|
||||||
.slice()
|
.slice()
|
||||||
.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.push({
|
}));
|
||||||
rgb: sortedEntries[sortedEntries.length - 1].rgb,
|
sortedEntries.unshift({ rgb: sortedEntries[0].rgb, arrayIndex: 0 });
|
||||||
arrayIndex: PALETTE_SIZE - 1,
|
sortedEntries.push({
|
||||||
});
|
rgb: sortedEntries[sortedEntries.length - 1].rgb,
|
||||||
|
arrayIndex: PALETTE_SIZE - 1,
|
||||||
|
});
|
||||||
|
|
||||||
// Interpolate between the sorted RGB entries to generate
|
// Interpolate between the sorted RGB entries to generate
|
||||||
// the palette texture data
|
// the palette texture data
|
||||||
sortedEntries.forEach((entry, index) => {
|
sortedEntries.forEach((entry, index) => {
|
||||||
paletteColors[entry.arrayIndex] = entry.rgb.slice();
|
paletteColors[entry.arrayIndex] = entry.rgb.slice();
|
||||||
if (index + 1 < sortedEntries.length) {
|
if (index + 1 < sortedEntries.length) {
|
||||||
const nextEntry = sortedEntries[index + 1];
|
const nextEntry = sortedEntries[index + 1];
|
||||||
const diff = nextEntry.arrayIndex - entry.arrayIndex;
|
const diff = nextEntry.arrayIndex - entry.arrayIndex;
|
||||||
for (let i = 0; i < diff; i++) {
|
for (let i = 0; i < diff; i++) {
|
||||||
const ratio = i / diff;
|
const ratio = i / diff;
|
||||||
paletteColors[entry.arrayIndex + i] = [
|
paletteColors[entry.arrayIndex + i] = [
|
||||||
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
|
entry.rgb[0] * (1 - ratio) + nextEntry.rgb[0] * ratio,
|
||||||
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
|
entry.rgb[1] * (1 - ratio) + nextEntry.rgb[1] * ratio,
|
||||||
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
|
entry.rgb[2] * (1 - ratio) + nextEntry.rgb[2] * ratio,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return make1DTexture(
|
return make1DTexture(
|
||||||
regl,
|
regl,
|
||||||
paletteColors.map((rgb) => [...rgb, 1])
|
paletteColors.map((rgb) => [...rgb, 1])
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// The rendered texture's values are mapped to colors in a palette texture.
|
// The rendered texture's values are mapped to colors in a palette texture.
|
||||||
@@ -55,39 +58,44 @@ const makePalette = (regl, entries) => {
|
|||||||
// in screen space.
|
// in screen space.
|
||||||
|
|
||||||
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,
|
||||||
|
cursorColor,
|
||||||
|
glintColor,
|
||||||
|
cursorIntensity,
|
||||||
|
glintIntensity,
|
||||||
|
ditherMagnitude,
|
||||||
|
} = config;
|
||||||
|
|
||||||
const palettePassFrag = loadText("shaders/glsl/palettePass.frag.glsl");
|
const render = regl({
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
|
||||||
const render = regl({
|
uniforms: {
|
||||||
frag: regl.prop("frag"),
|
backgroundColor: colorToRGB(backgroundColor),
|
||||||
|
cursorColor: colorToRGB(cursorColor),
|
||||||
|
glintColor: colorToRGB(glintColor),
|
||||||
|
cursorIntensity,
|
||||||
|
glintIntensity,
|
||||||
|
ditherMagnitude,
|
||||||
|
tex: inputs.primary,
|
||||||
|
bloomTex: inputs.bloom,
|
||||||
|
paletteTex,
|
||||||
|
},
|
||||||
|
framebuffer: output,
|
||||||
|
});
|
||||||
|
|
||||||
uniforms: {
|
return makePass(
|
||||||
backgroundColor: colorToRGB(backgroundColor),
|
{
|
||||||
cursorColor: colorToRGB(cursorColor),
|
primary: output,
|
||||||
glintColor: colorToRGB(glintColor),
|
},
|
||||||
cursorIntensity,
|
palettePassFrag.loaded,
|
||||||
glintIntensity,
|
(w, h) => output.resize(w, h),
|
||||||
ditherMagnitude,
|
(shouldRender) => {
|
||||||
tex: inputs.primary,
|
if (shouldRender) {
|
||||||
bloomTex: inputs.bloom,
|
render({ frag: palettePassFrag });
|
||||||
paletteTex,
|
}
|
||||||
},
|
}
|
||||||
framebuffer: output,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return makePass(
|
|
||||||
{
|
|
||||||
primary: output,
|
|
||||||
},
|
|
||||||
palettePassFrag.loaded,
|
|
||||||
(w, h) => output.resize(w, h),
|
|
||||||
(shouldRender) => {
|
|
||||||
if (shouldRender) {
|
|
||||||
render({ frag: palettePassFrag.text() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,25 @@
|
|||||||
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,
|
||||||
circle: 1,
|
circle: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// These compute buffers are used to compute the properties of cells in the grid.
|
// These compute buffers are used to compute the properties of cells in the grid.
|
||||||
@@ -15,13 +30,12 @@ const rippleTypes = {
|
|||||||
// These double buffers are smaller than the screen, because their pixels correspond
|
// These double buffers are smaller than the screen, because their pixels correspond
|
||||||
// with cells in the grid, and the cells' glyphs are much larger than a pixel.
|
// with cells in the grid, and the cells' glyphs are much larger than a pixel.
|
||||||
const makeComputeDoubleBuffer = (regl, height, width) =>
|
const makeComputeDoubleBuffer = (regl, height, width) =>
|
||||||
makeDoubleBuffer(regl, {
|
makeDoubleBuffer(regl, {
|
||||||
width,
|
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;
|
||||||
const tlVert = [0, 0];
|
const tlVert = [0, 0];
|
||||||
@@ -31,290 +45,352 @@ 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
|
||||||
|
// to reach the desired density, and then overlaps them
|
||||||
|
const volumetric = config.volumetric;
|
||||||
|
const density = volumetric && config.effect !== "none" ? config.density : 1;
|
||||||
|
const [numRows, numColumns] = [
|
||||||
|
config.numColumns,
|
||||||
|
Math.floor(config.numColumns * density),
|
||||||
|
];
|
||||||
|
|
||||||
// The volumetric mode multiplies the number of columns
|
// The volumetric mode requires us to create a grid of quads,
|
||||||
// to reach the desired density, and then overlaps them
|
// rather than a single quad for our geometry
|
||||||
const volumetric = config.volumetric;
|
const [numQuadRows, numQuadColumns] = volumetric
|
||||||
const density = volumetric && config.effect !== "none" ? config.density : 1;
|
? [numRows, numColumns]
|
||||||
const [numRows, numColumns] = [config.numColumns, Math.floor(config.numColumns * density)];
|
: [1, 1];
|
||||||
|
const numQuads = numQuadRows * numQuadColumns;
|
||||||
|
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
|
||||||
|
|
||||||
// The volumetric mode requires us to create a grid of quads,
|
// Various effect-related values
|
||||||
// rather than a single quad for our geometry
|
const rippleType =
|
||||||
const [numQuadRows, numQuadColumns] = volumetric ? [numRows, numColumns] : [1, 1];
|
config.rippleTypeName in rippleTypes
|
||||||
const numQuads = numQuadRows * numQuadColumns;
|
? rippleTypes[config.rippleTypeName]
|
||||||
const quadSize = [1 / numQuadColumns, 1 / numQuadRows];
|
: -1;
|
||||||
|
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 showDebugView = config.effect === "none";
|
||||||
|
|
||||||
// Various effect-related values
|
const commonUniforms = {
|
||||||
const rippleType = config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1;
|
...extractEntries(config, [
|
||||||
const slantVec = [Math.cos(config.slant), Math.sin(config.slant)];
|
"animationSpeed",
|
||||||
const slantScale = 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1);
|
"glyphHeightToWidth",
|
||||||
const showDebugView = config.effect === "none";
|
"glyphSequenceLength",
|
||||||
|
"glyphTextureGridSize",
|
||||||
|
]),
|
||||||
|
numColumns,
|
||||||
|
numRows,
|
||||||
|
showDebugView,
|
||||||
|
};
|
||||||
|
|
||||||
const glyphTransform = mat2.fromScaling(mat2.create(), vec2.fromValues(config.glyphFlip ? -1 : 1, 1));
|
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
|
||||||
mat2.rotate(glyphTransform, glyphTransform, (config.glyphRotation * Math.PI) / 180);
|
|
||||||
|
|
||||||
const commonUniforms = {
|
const introUniforms = {
|
||||||
...extractEntries(config, ["animationSpeed", "glyphHeightToWidth", "glyphSequenceLength", "glyphTextureGridSize"]),
|
...commonUniforms,
|
||||||
numColumns,
|
...extractEntries(config, ["fallSpeed", "skipIntro"]),
|
||||||
numRows,
|
};
|
||||||
showDebugView,
|
const intro = regl({
|
||||||
};
|
frag: regl.prop("frag"),
|
||||||
|
uniforms: {
|
||||||
|
...introUniforms,
|
||||||
|
previousIntroState: introDoubleBuffer.back,
|
||||||
|
},
|
||||||
|
|
||||||
const introDoubleBuffer = makeComputeDoubleBuffer(regl, 1, numColumns);
|
framebuffer: introDoubleBuffer.front,
|
||||||
const rainPassIntro = loadText("shaders/glsl/rainPass.intro.frag.glsl");
|
});
|
||||||
const introUniforms = {
|
|
||||||
...commonUniforms,
|
|
||||||
...extractEntries(config, ["fallSpeed", "skipIntro"]),
|
|
||||||
};
|
|
||||||
const intro = regl({
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
uniforms: {
|
|
||||||
...introUniforms,
|
|
||||||
previousIntroState: introDoubleBuffer.back,
|
|
||||||
},
|
|
||||||
|
|
||||||
framebuffer: introDoubleBuffer.front,
|
const raindropDoubleBuffer = makeComputeDoubleBuffer(
|
||||||
});
|
regl,
|
||||||
|
numRows,
|
||||||
|
numColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
const raindropUniforms = {
|
||||||
|
...commonUniforms,
|
||||||
|
...extractEntries(config, [
|
||||||
|
"brightnessDecay",
|
||||||
|
"fallSpeed",
|
||||||
|
"raindropLength",
|
||||||
|
"loops",
|
||||||
|
"skipIntro",
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const raindrop = regl({
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
uniforms: {
|
||||||
|
...raindropUniforms,
|
||||||
|
introState: introDoubleBuffer.front,
|
||||||
|
previousRaindropState: raindropDoubleBuffer.back,
|
||||||
|
},
|
||||||
|
|
||||||
const raindropDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
|
framebuffer: raindropDoubleBuffer.front,
|
||||||
const rainPassRaindrop = loadText("shaders/glsl/rainPass.raindrop.frag.glsl");
|
});
|
||||||
const raindropUniforms = {
|
|
||||||
...commonUniforms,
|
|
||||||
...extractEntries(config, ["brightnessDecay", "fallSpeed", "raindropLength", "loops", "skipIntro"]),
|
|
||||||
};
|
|
||||||
const raindrop = regl({
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
uniforms: {
|
|
||||||
...raindropUniforms,
|
|
||||||
introState: introDoubleBuffer.front,
|
|
||||||
previousRaindropState: raindropDoubleBuffer.back,
|
|
||||||
},
|
|
||||||
|
|
||||||
framebuffer: raindropDoubleBuffer.front,
|
const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
|
||||||
});
|
|
||||||
|
|
||||||
const symbolDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
|
const symbolUniforms = {
|
||||||
const rainPassSymbol = loadText("shaders/glsl/rainPass.symbol.frag.glsl");
|
...commonUniforms,
|
||||||
const symbolUniforms = {
|
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]),
|
||||||
...commonUniforms,
|
};
|
||||||
...extractEntries(config, ["cycleSpeed", "cycleFrameSkip", "loops"]),
|
const symbol = regl({
|
||||||
};
|
frag: regl.prop("frag"),
|
||||||
const symbol = regl({
|
uniforms: {
|
||||||
frag: regl.prop("frag"),
|
...symbolUniforms,
|
||||||
uniforms: {
|
raindropState: raindropDoubleBuffer.front,
|
||||||
...symbolUniforms,
|
previousSymbolState: symbolDoubleBuffer.back,
|
||||||
raindropState: raindropDoubleBuffer.front,
|
},
|
||||||
previousSymbolState: symbolDoubleBuffer.back,
|
|
||||||
},
|
|
||||||
|
|
||||||
framebuffer: symbolDoubleBuffer.front,
|
framebuffer: symbolDoubleBuffer.front,
|
||||||
});
|
});
|
||||||
|
|
||||||
const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
|
const effectDoubleBuffer = makeComputeDoubleBuffer(regl, numRows, numColumns);
|
||||||
const rainPassEffect = loadText("shaders/glsl/rainPass.effect.frag.glsl");
|
|
||||||
const effectUniforms = {
|
|
||||||
...commonUniforms,
|
|
||||||
...extractEntries(config, ["hasThunder", "rippleScale", "rippleSpeed", "rippleThickness", "loops"]),
|
|
||||||
rippleType,
|
|
||||||
};
|
|
||||||
const effect = regl({
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
uniforms: {
|
|
||||||
...effectUniforms,
|
|
||||||
raindropState: raindropDoubleBuffer.front,
|
|
||||||
previousEffectState: effectDoubleBuffer.back,
|
|
||||||
},
|
|
||||||
|
|
||||||
framebuffer: effectDoubleBuffer.front,
|
const effectUniforms = {
|
||||||
});
|
...commonUniforms,
|
||||||
|
...extractEntries(config, [
|
||||||
|
"hasThunder",
|
||||||
|
"rippleScale",
|
||||||
|
"rippleSpeed",
|
||||||
|
"rippleThickness",
|
||||||
|
"loops",
|
||||||
|
]),
|
||||||
|
rippleType,
|
||||||
|
};
|
||||||
|
const effect = regl({
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
uniforms: {
|
||||||
|
...effectUniforms,
|
||||||
|
raindropState: raindropDoubleBuffer.front,
|
||||||
|
previousEffectState: effectDoubleBuffer.back,
|
||||||
|
},
|
||||||
|
|
||||||
const quadPositions = Array(numQuadRows)
|
framebuffer: effectDoubleBuffer.front,
|
||||||
.fill()
|
});
|
||||||
.map((_, y) =>
|
|
||||||
Array(numQuadColumns)
|
|
||||||
.fill()
|
|
||||||
.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
|
|
||||||
);
|
|
||||||
|
|
||||||
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
|
const quadPositions = Array(numQuadRows)
|
||||||
const glyphMSDF = loadImage(regl, config.glyphMSDFURL);
|
.fill()
|
||||||
const glintMSDF = loadImage(regl, config.glintMSDFURL);
|
.map((_, y) =>
|
||||||
const baseTexture = loadImage(regl, config.baseTextureURL, true);
|
Array(numQuadColumns)
|
||||||
const glintTexture = loadImage(regl, config.glintTextureURL, true);
|
.fill()
|
||||||
const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl");
|
.map((_, x) => Array(numVerticesPerQuad).fill([x, y]))
|
||||||
const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl");
|
);
|
||||||
const output = makePassFBO(regl, config.useHalfFloat);
|
|
||||||
const renderUniforms = {
|
|
||||||
...commonUniforms,
|
|
||||||
...extractEntries(config, [
|
|
||||||
// vertex
|
|
||||||
"forwardSpeed",
|
|
||||||
"glyphVerticalSpacing",
|
|
||||||
// fragment
|
|
||||||
"baseBrightness",
|
|
||||||
"baseContrast",
|
|
||||||
"glintBrightness",
|
|
||||||
"glintContrast",
|
|
||||||
"hasBaseTexture",
|
|
||||||
"hasGlintTexture",
|
|
||||||
"brightnessThreshold",
|
|
||||||
"brightnessOverride",
|
|
||||||
"isolateCursor",
|
|
||||||
"isolateGlint",
|
|
||||||
"glyphEdgeCrop",
|
|
||||||
"isPolar",
|
|
||||||
]),
|
|
||||||
glyphTransform,
|
|
||||||
density,
|
|
||||||
numQuadColumns,
|
|
||||||
numQuadRows,
|
|
||||||
quadSize,
|
|
||||||
slantScale,
|
|
||||||
slantVec,
|
|
||||||
volumetric,
|
|
||||||
};
|
|
||||||
const render = regl({
|
|
||||||
blend: {
|
|
||||||
enable: true,
|
|
||||||
func: {
|
|
||||||
src: "one",
|
|
||||||
dst: "one",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vert: regl.prop("vert"),
|
|
||||||
frag: regl.prop("frag"),
|
|
||||||
|
|
||||||
uniforms: {
|
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
|
||||||
...renderUniforms,
|
const glyphMSDF = loadImage(regl, config.glyphMSDFURL);
|
||||||
|
const glintMSDF = loadImage(regl, config.glintMSDFURL);
|
||||||
|
const baseTexture = loadImage(regl, config.baseTextureURL, true);
|
||||||
|
const glintTexture = loadImage(regl, config.glintTextureURL, true);
|
||||||
|
const output = makePassFBO(regl, config.useHalfFloat);
|
||||||
|
const renderUniforms = {
|
||||||
|
...commonUniforms,
|
||||||
|
...extractEntries(config, [
|
||||||
|
// vertex
|
||||||
|
"forwardSpeed",
|
||||||
|
"glyphVerticalSpacing",
|
||||||
|
// fragment
|
||||||
|
"baseBrightness",
|
||||||
|
"baseContrast",
|
||||||
|
"glintBrightness",
|
||||||
|
"glintContrast",
|
||||||
|
"hasBaseTexture",
|
||||||
|
"hasGlintTexture",
|
||||||
|
"brightnessThreshold",
|
||||||
|
"brightnessOverride",
|
||||||
|
"isolateCursor",
|
||||||
|
"isolateGlint",
|
||||||
|
"glyphEdgeCrop",
|
||||||
|
"isPolar",
|
||||||
|
]),
|
||||||
|
density,
|
||||||
|
numQuadColumns,
|
||||||
|
numQuadRows,
|
||||||
|
quadSize,
|
||||||
|
slantScale,
|
||||||
|
slantVec,
|
||||||
|
volumetric,
|
||||||
|
};
|
||||||
|
const render = regl({
|
||||||
|
blend: {
|
||||||
|
enable: true,
|
||||||
|
func: {
|
||||||
|
src: "one",
|
||||||
|
dst: "one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vert: regl.prop("vert"),
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
|
||||||
raindropState: raindropDoubleBuffer.front,
|
uniforms: {
|
||||||
symbolState: symbolDoubleBuffer.front,
|
...renderUniforms,
|
||||||
effectState: effectDoubleBuffer.front,
|
|
||||||
glyphMSDF: glyphMSDF.texture,
|
|
||||||
glintMSDF: glintMSDF.texture,
|
|
||||||
baseTexture: baseTexture.texture,
|
|
||||||
glintTexture: glintTexture.texture,
|
|
||||||
|
|
||||||
msdfPxRange: 4.0,
|
raindropState: raindropDoubleBuffer.front,
|
||||||
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
|
symbolState: symbolDoubleBuffer.front,
|
||||||
glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],
|
effectState: effectDoubleBuffer.front,
|
||||||
|
glyphMSDF: glyphMSDF.texture,
|
||||||
|
glintMSDF: glintMSDF.texture,
|
||||||
|
baseTexture: baseTexture.texture,
|
||||||
|
glintTexture: glintTexture.texture,
|
||||||
|
glyphTransform: regl.prop('glyphTransform'),
|
||||||
|
|
||||||
camera: regl.prop("camera"),
|
msdfPxRange: 4.0,
|
||||||
transform: regl.prop("transform"),
|
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
|
||||||
screenSize: regl.prop("screenSize"),
|
glintMSDFSize: () => [glintMSDF.width(), glintMSDF.height()],
|
||||||
},
|
|
||||||
|
|
||||||
viewport: regl.prop("viewport"),
|
camera: regl.prop("camera"),
|
||||||
|
transform: regl.prop("transform"),
|
||||||
|
screenSize: regl.prop("screenSize"),
|
||||||
|
},
|
||||||
|
|
||||||
attributes: {
|
viewport: regl.prop("viewport"),
|
||||||
aPosition: quadPositions,
|
|
||||||
aCorner: Array(numQuads).fill(quadVertices),
|
|
||||||
},
|
|
||||||
count: numQuads * numVerticesPerQuad,
|
|
||||||
|
|
||||||
framebuffer: output,
|
attributes: {
|
||||||
});
|
aPosition: quadPositions,
|
||||||
|
aCorner: Array(numQuads).fill(quadVertices),
|
||||||
|
},
|
||||||
|
count: numQuads * numVerticesPerQuad,
|
||||||
|
|
||||||
// Camera and transform math for the volumetric mode
|
framebuffer: output,
|
||||||
const screenSize = [1, 1];
|
});
|
||||||
const transform = mat4.create();
|
|
||||||
if (volumetric && config.isometric) {
|
|
||||||
mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
|
|
||||||
mat4.rotateY(transform, transform, (Math.PI * 1) / 4);
|
|
||||||
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
|
|
||||||
mat4.scale(transform, transform, vec3.fromValues(1, 1, 2));
|
|
||||||
} else if (lkg.enabled) {
|
|
||||||
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1.1));
|
|
||||||
mat4.scale(transform, transform, vec3.fromValues(1, 1, 1));
|
|
||||||
mat4.scale(transform, transform, vec3.fromValues(0.15, 0.15, 0.15));
|
|
||||||
} else {
|
|
||||||
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
|
|
||||||
}
|
|
||||||
const camera = mat4.create();
|
|
||||||
|
|
||||||
const vantagePoints = [];
|
// Camera and transform math for the volumetric mode
|
||||||
|
const screenSize = [1, 1];
|
||||||
|
//const { mat4, vec3 } = glMatrix;
|
||||||
|
const transform = mat4.create();
|
||||||
|
if (volumetric && config.isometric) {
|
||||||
|
mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
|
||||||
|
mat4.rotateY(transform, transform, (Math.PI * 1) / 4);
|
||||||
|
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
|
||||||
|
mat4.scale(transform, transform, vec3.fromValues(1, 1, 2));
|
||||||
|
} else if (lkg.enabled) {
|
||||||
|
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1.1));
|
||||||
|
mat4.scale(transform, transform, vec3.fromValues(1, 1, 1));
|
||||||
|
mat4.scale(transform, transform, vec3.fromValues(0.15, 0.15, 0.15));
|
||||||
|
} else {
|
||||||
|
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
|
||||||
|
}
|
||||||
|
const camera = mat4.create();
|
||||||
|
|
||||||
return makePass(
|
const vantagePoints = [];
|
||||||
{
|
|
||||||
primary: output,
|
|
||||||
},
|
|
||||||
Promise.all([
|
|
||||||
glyphMSDF.loaded,
|
|
||||||
glintMSDF.loaded,
|
|
||||||
baseTexture.loaded,
|
|
||||||
glintTexture.loaded,
|
|
||||||
rainPassIntro.loaded,
|
|
||||||
rainPassRaindrop.loaded,
|
|
||||||
rainPassSymbol.loaded,
|
|
||||||
rainPassVert.loaded,
|
|
||||||
rainPassFrag.loaded,
|
|
||||||
]),
|
|
||||||
(w, h) => {
|
|
||||||
output.resize(w, h);
|
|
||||||
const aspectRatio = w / h;
|
|
||||||
|
|
||||||
const [numTileColumns, numTileRows] = [lkg.tileX, lkg.tileY];
|
return makePass(
|
||||||
const numVantagePoints = numTileRows * numTileColumns;
|
{
|
||||||
const tileWidth = Math.floor(w / numTileColumns);
|
primary: output,
|
||||||
const tileHeight = Math.floor(h / numTileRows);
|
},
|
||||||
vantagePoints.length = 0;
|
Promise.all([
|
||||||
for (let row = 0; row < numTileRows; row++) {
|
glyphMSDF.loaded,
|
||||||
for (let column = 0; column < numTileColumns; column++) {
|
glintMSDF.loaded,
|
||||||
const index = column + row * numTileColumns;
|
baseTexture.loaded,
|
||||||
const camera = mat4.create();
|
glintTexture.loaded,
|
||||||
|
]),
|
||||||
|
(w, h) => {
|
||||||
|
output.resize(w, h);
|
||||||
|
const aspectRatio = w / h;
|
||||||
|
|
||||||
if (volumetric && config.isometric) {
|
const [numTileColumns, numTileRows] = [lkg.tileX, lkg.tileY];
|
||||||
if (aspectRatio > 1) {
|
const numVantagePoints = numTileRows * numTileColumns;
|
||||||
mat4.ortho(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000);
|
const tileWidth = Math.floor(w / numTileColumns);
|
||||||
} else {
|
const tileHeight = Math.floor(h / numTileRows);
|
||||||
mat4.ortho(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000);
|
vantagePoints.length = 0;
|
||||||
}
|
for (let row = 0; row < numTileRows; row++) {
|
||||||
} else if (lkg.enabled) {
|
for (let column = 0; column < numTileColumns; column++) {
|
||||||
mat4.perspective(camera, (Math.PI / 180) * lkg.fov, lkg.quiltAspect, 0.0001, 1000);
|
const index = column + row * numTileColumns;
|
||||||
|
const camera = mat4.create();
|
||||||
|
|
||||||
const distanceToTarget = -1; // TODO: Get from somewhere else
|
if (volumetric && config.isometric) {
|
||||||
let vantagePointAngle = (Math.PI / 180) * lkg.viewCone * (index / (numVantagePoints - 1) - 0.5);
|
if (aspectRatio > 1) {
|
||||||
if (isNaN(vantagePointAngle)) {
|
mat4.ortho(
|
||||||
vantagePointAngle = 0;
|
camera,
|
||||||
}
|
-1.5 * aspectRatio,
|
||||||
const xOffset = distanceToTarget * Math.tan(vantagePointAngle);
|
1.5 * aspectRatio,
|
||||||
|
-1.5,
|
||||||
|
1.5,
|
||||||
|
-1000,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mat4.ortho(
|
||||||
|
camera,
|
||||||
|
-1.5,
|
||||||
|
1.5,
|
||||||
|
-1.5 / aspectRatio,
|
||||||
|
1.5 / aspectRatio,
|
||||||
|
-1000,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (lkg.enabled) {
|
||||||
|
mat4.perspective(
|
||||||
|
camera,
|
||||||
|
(Math.PI / 180) * lkg.fov,
|
||||||
|
lkg.quiltAspect,
|
||||||
|
0.0001,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
|
||||||
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
|
const distanceToTarget = -1; // TODO: Get from somewhere else
|
||||||
|
let vantagePointAngle =
|
||||||
|
(Math.PI / 180) *
|
||||||
|
lkg.viewCone *
|
||||||
|
(index / (numVantagePoints - 1) - 0.5);
|
||||||
|
if (isNaN(vantagePointAngle)) {
|
||||||
|
vantagePointAngle = 0;
|
||||||
|
}
|
||||||
|
const xOffset = distanceToTarget * Math.tan(vantagePointAngle);
|
||||||
|
|
||||||
camera[8] = -xOffset / (distanceToTarget * Math.tan((Math.PI / 180) * 0.5 * lkg.fov) * lkg.quiltAspect); // Is this right??
|
mat4.translate(camera, camera, vec3.fromValues(xOffset, 0, 0));
|
||||||
} else {
|
|
||||||
mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = {
|
camera[8] =
|
||||||
x: column * tileWidth,
|
-xOffset /
|
||||||
y: row * tileHeight,
|
(distanceToTarget *
|
||||||
width: tileWidth,
|
Math.tan((Math.PI / 180) * 0.5 * lkg.fov) *
|
||||||
height: tileHeight,
|
lkg.quiltAspect); // Is this right??
|
||||||
};
|
} else {
|
||||||
vantagePoints.push({ camera, viewport });
|
mat4.perspective(
|
||||||
}
|
camera,
|
||||||
}
|
(Math.PI / 180) * 90,
|
||||||
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
|
aspectRatio,
|
||||||
},
|
0.0001,
|
||||||
(shouldRender) => {
|
1000
|
||||||
intro({ frag: rainPassIntro.text() });
|
);
|
||||||
raindrop({ frag: rainPassRaindrop.text() });
|
}
|
||||||
symbol({ frag: rainPassSymbol.text() });
|
|
||||||
effect({ frag: rainPassEffect.text() });
|
|
||||||
|
|
||||||
if (shouldRender) {
|
const viewport = {
|
||||||
regl.clear({
|
x: column * tileWidth,
|
||||||
depth: 1,
|
y: row * tileHeight,
|
||||||
color: [0, 0, 0, 1],
|
width: tileWidth,
|
||||||
framebuffer: output,
|
height: tileHeight,
|
||||||
});
|
};
|
||||||
|
vantagePoints.push({ camera, viewport });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[screenSize[0], screenSize[1]] =
|
||||||
|
aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
|
||||||
|
},
|
||||||
|
(shouldRender) => {
|
||||||
|
intro({ frag: rainPassIntro });
|
||||||
|
raindrop({ frag: rainPassRaindrop });
|
||||||
|
symbol({ frag: rainPassSymbol });
|
||||||
|
effect({ frag: rainPassEffect });
|
||||||
|
|
||||||
for (const vantagePoint of vantagePoints) {
|
if (shouldRender) {
|
||||||
render({ ...vantagePoint, transform, screenSize, vert: rainPassVert.text(), frag: rainPassFrag.text() });
|
regl.clear({
|
||||||
}
|
depth: 1,
|
||||||
}
|
color: [0, 0, 0, 1],
|
||||||
}
|
framebuffer: output,
|
||||||
);
|
});
|
||||||
|
|
||||||
|
for (const vantagePoint of vantagePoints) {
|
||||||
|
render({
|
||||||
|
...vantagePoint,
|
||||||
|
transform,
|
||||||
|
screenSize,
|
||||||
|
vert: rainPassVert,
|
||||||
|
frag: rainPassFrag,
|
||||||
|
glyphTransform: [1, 0, 0, 1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,67 +8,77 @@ import { loadText, make1DTexture, makePassFBO, makePass } from "./utils.js";
|
|||||||
// This shader introduces noise into the renders, to avoid banding
|
// This shader introduces noise into the renders, to avoid banding
|
||||||
|
|
||||||
const transPrideStripeColors = [
|
const transPrideStripeColors = [
|
||||||
{ space: "rgb", values: [0.36, 0.81, 0.98] },
|
{ space: "rgb", values: [0.36, 0.81, 0.98] },
|
||||||
{ space: "rgb", values: [0.96, 0.66, 0.72] },
|
{ space: "rgb", values: [0.96, 0.66, 0.72] },
|
||||||
{ space: "rgb", values: [1.0, 1.0, 1.0] },
|
{ space: "rgb", values: [1.0, 1.0, 1.0] },
|
||||||
{ space: "rgb", values: [0.96, 0.66, 0.72] },
|
{ space: "rgb", values: [0.96, 0.66, 0.72] },
|
||||||
{ space: "rgb", values: [0.36, 0.81, 0.98] },
|
{ space: "rgb", values: [0.36, 0.81, 0.98] },
|
||||||
]
|
]
|
||||||
.map((color) => Array(3).fill(color))
|
.map((color) => Array(3).fill(color))
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
const prideStripeColors = [
|
const prideStripeColors = [
|
||||||
{ space: "rgb", values: [0.89, 0.01, 0.01] },
|
{ space: "rgb", values: [0.89, 0.01, 0.01] },
|
||||||
{ space: "rgb", values: [1.0, 0.55, 0.0] },
|
{ space: "rgb", values: [1.0, 0.55, 0.0] },
|
||||||
{ space: "rgb", values: [1.0, 0.93, 0.0] },
|
{ space: "rgb", values: [1.0, 0.93, 0.0] },
|
||||||
{ space: "rgb", values: [0.0, 0.5, 0.15] },
|
{ space: "rgb", values: [0.0, 0.5, 0.15] },
|
||||||
{ space: "rgb", values: [0.0, 0.3, 1.0] },
|
{ space: "rgb", values: [0.0, 0.3, 1.0] },
|
||||||
{ space: "rgb", values: [0.46, 0.03, 0.53] },
|
{ space: "rgb", values: [0.46, 0.03, 0.53] },
|
||||||
]
|
]
|
||||||
.map((color) => Array(2).fill(color))
|
.map((color) => Array(2).fill(color))
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
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 =
|
||||||
const stripeTex = make1DTexture(
|
"stripeColors" in config
|
||||||
regl,
|
? config.stripeColors
|
||||||
stripeColors.map((color) => [...colorToRGB(color), 1])
|
: config.effect === "pride"
|
||||||
);
|
? prideStripeColors
|
||||||
|
: transPrideStripeColors;
|
||||||
|
const stripeTex = make1DTexture(
|
||||||
|
regl,
|
||||||
|
stripeColors.map((color) => [...colorToRGB(color), 1])
|
||||||
|
);
|
||||||
|
|
||||||
const stripePassFrag = loadText("shaders/glsl/stripePass.frag.glsl");
|
const render = regl({
|
||||||
|
frag: regl.prop("frag"),
|
||||||
|
|
||||||
const render = regl({
|
uniforms: {
|
||||||
frag: regl.prop("frag"),
|
backgroundColor: colorToRGB(backgroundColor),
|
||||||
|
cursorColor: colorToRGB(cursorColor),
|
||||||
|
glintColor: colorToRGB(glintColor),
|
||||||
|
cursorIntensity,
|
||||||
|
glintIntensity,
|
||||||
|
ditherMagnitude,
|
||||||
|
tex: inputs.primary,
|
||||||
|
bloomTex: inputs.bloom,
|
||||||
|
stripeTex,
|
||||||
|
},
|
||||||
|
framebuffer: output,
|
||||||
|
});
|
||||||
|
|
||||||
uniforms: {
|
return makePass(
|
||||||
backgroundColor: colorToRGB(backgroundColor),
|
{
|
||||||
cursorColor: colorToRGB(cursorColor),
|
primary: output,
|
||||||
glintColor: colorToRGB(glintColor),
|
},
|
||||||
cursorIntensity,
|
null,
|
||||||
glintIntensity,
|
(w, h) => output.resize(w, h),
|
||||||
ditherMagnitude,
|
(shouldRender) => {
|
||||||
tex: inputs.primary,
|
if (shouldRender) {
|
||||||
bloomTex: inputs.bloom,
|
render({ frag: stripePassFrag });
|
||||||
stripeTex,
|
}
|
||||||
},
|
}
|
||||||
framebuffer: output,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return makePass(
|
|
||||||
{
|
|
||||||
primary: output,
|
|
||||||
},
|
|
||||||
stripePassFrag.loaded,
|
|
||||||
(w, h) => output.resize(w, h),
|
|
||||||
(shouldRender) => {
|
|
||||||
if (shouldRender) {
|
|
||||||
render({ frag: stripePassFrag.text() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
617
js/utils/config.js
Normal file
617
js/utils/config.js
Normal 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
7186
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal 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
11
public/index.html
Normal 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
44
rollup.config.mjs
Normal 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
|
||||||
|
],
|
||||||
|
};
|
||||||
10
shaders/glsl/bloomPass.vert.glsl
Normal file
10
shaders/glsl/bloomPass.vert.glsl
Normal 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
54
webpack.config.js
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user