mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-14 12:29:30 -07:00
257 lines
6.9 KiB
JavaScript
257 lines
6.9 KiB
JavaScript
import { loadImage, makePassFBO, makeDoubleBuffer, makePass } from "./utils.js";
|
|
|
|
const numVerticesPerQuad = 2 * 3;
|
|
const tlVert = [0, 0];
|
|
const trVert = [0, 1];
|
|
const blVert = [1, 0];
|
|
const brVert = [1, 1];
|
|
const quadVertices = [tlVert, trVert, brVert, tlVert, brVert, blVert];
|
|
|
|
export default ({ regl }) => {
|
|
const size = 80; // The maximum dimension of the glyph grid
|
|
|
|
const commonUniforms = {
|
|
glyphSequenceLength: 57,
|
|
glyphTextureGridSize: [8, 8],
|
|
numColumns: size,
|
|
numRows: size,
|
|
};
|
|
|
|
const computeDoubleBuffer = makeDoubleBuffer(regl, {
|
|
width: size,
|
|
height: size,
|
|
wrapT: "clamp",
|
|
type: "half float",
|
|
});
|
|
|
|
const compute = regl({
|
|
frag: `
|
|
precision highp float;
|
|
|
|
#define PI 3.14159265359
|
|
#define SQRT_2 1.4142135623730951
|
|
#define SQRT_5 2.23606797749979
|
|
|
|
uniform sampler2D previousComputeState;
|
|
|
|
uniform float numColumns, numRows;
|
|
uniform float time, tick;
|
|
uniform float fallSpeed, cycleSpeed;
|
|
uniform float glyphSequenceLength;
|
|
uniform float raindropLength;
|
|
|
|
// Helper functions for generating randomness, borrowed from elsewhere
|
|
|
|
highp float randomFloat( const in vec2 uv ) {
|
|
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
|
|
highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
|
|
return fract(sin(sn) * c);
|
|
}
|
|
|
|
float wobble(float x) {
|
|
return x + 0.3 * sin(SQRT_2 * x) + 0.2 * sin(SQRT_5 * x);
|
|
}
|
|
|
|
float getRainBrightness(float simTime, vec2 glyphPos) {
|
|
float columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.)) * 1000.;
|
|
float columnSpeedOffset = randomFloat(vec2(glyphPos.x + 0.1, 0.)) * 0.5 + 0.5;
|
|
float columnTime = columnTimeOffset + simTime * fallSpeed * columnSpeedOffset;
|
|
float rainTime = (glyphPos.y * 0.01 + columnTime) / raindropLength;
|
|
rainTime = wobble(rainTime);
|
|
return 1.0 - fract(rainTime);
|
|
}
|
|
|
|
vec2 computeRaindrop(float simTime, vec2 glyphPos) {
|
|
float brightness = getRainBrightness(simTime, glyphPos);
|
|
float brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.));
|
|
bool cursor = brightness > brightnessBelow;
|
|
return vec2(brightness, cursor);
|
|
}
|
|
|
|
vec2 computeSymbol(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) {
|
|
|
|
float previousSymbol = previous.r;
|
|
float previousAge = previous.g;
|
|
bool resetGlyph = isFirstFrame;
|
|
if (resetGlyph) {
|
|
previousAge = randomFloat(screenPos + 0.5);
|
|
previousSymbol = floor(glyphSequenceLength * randomFloat(screenPos));
|
|
}
|
|
float age = previousAge;
|
|
float symbol = previousSymbol;
|
|
if (mod(tick, 1.0) == 0.) {
|
|
age += cycleSpeed;
|
|
if (age >= 1.) {
|
|
symbol = floor(glyphSequenceLength * randomFloat(screenPos + simTime));
|
|
age = fract(age);
|
|
}
|
|
}
|
|
|
|
return vec2(symbol, age);
|
|
}
|
|
|
|
void main() {
|
|
vec2 glyphPos = gl_FragCoord.xy;
|
|
vec2 screenPos = glyphPos / vec2(numColumns, numRows);
|
|
|
|
vec2 raindrop = computeRaindrop(time, glyphPos);
|
|
|
|
bool isFirstFrame = tick <= 1.;
|
|
vec4 previous = texture2D( previousComputeState, screenPos );
|
|
vec4 previousSymbol = vec4(previous.ba, 0.0, 0.0);
|
|
vec2 symbol = computeSymbol(time, isFirstFrame, glyphPos, screenPos, previousSymbol);
|
|
gl_FragColor = vec4(raindrop, symbol);
|
|
}
|
|
|
|
`,
|
|
uniforms: {
|
|
...commonUniforms,
|
|
cycleSpeed: 0.03, // The speed glyphs change
|
|
fallSpeed: 0.3, // The speed the raindrops progress downwards
|
|
raindropLength: 0.75, // Adjusts the frequency of raindrops (and their length) in a column
|
|
previousComputeState: computeDoubleBuffer.back,
|
|
},
|
|
|
|
framebuffer: computeDoubleBuffer.front,
|
|
});
|
|
|
|
// We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen
|
|
const glyphMSDF = loadImage(regl, "assets/matrixcode_msdf.png");
|
|
const output = makePassFBO(regl);
|
|
const render = regl({
|
|
blend: {
|
|
enable: true,
|
|
func: {
|
|
src: "one",
|
|
dst: "one",
|
|
},
|
|
},
|
|
vert: `
|
|
precision lowp float;
|
|
|
|
attribute vec2 aPosition;
|
|
uniform vec2 screenSize;
|
|
varying vec2 vUV;
|
|
|
|
void main() {
|
|
vUV = aPosition;
|
|
gl_Position = vec4((aPosition - 0.5) * 2.0 * screenSize, 0.0, 1.0);
|
|
}
|
|
`,
|
|
frag: `
|
|
#define PI 3.14159265359
|
|
#ifdef GL_OES_standard_derivatives
|
|
#extension GL_OES_standard_derivatives: enable
|
|
#endif
|
|
precision lowp float;
|
|
|
|
uniform sampler2D computeState;
|
|
uniform float numColumns, numRows;
|
|
uniform sampler2D glyphMSDF;
|
|
uniform float msdfPxRange;
|
|
uniform vec2 glyphMSDFSize;
|
|
uniform float glyphSequenceLength;
|
|
uniform vec2 glyphTextureGridSize;
|
|
|
|
varying vec2 vUV;
|
|
|
|
float median3(vec3 i) {
|
|
return max(min(i.r, i.g), min(max(i.r, i.g), i.b));
|
|
}
|
|
|
|
float modI(float a, float b) {
|
|
float m = a - floor((a + 0.5) / b) * b;
|
|
return floor(m + 0.5);
|
|
}
|
|
|
|
vec3 getBrightness(vec2 raindrop, vec2 uv) {
|
|
|
|
float base = raindrop.r;
|
|
bool isCursor = bool(raindrop.g);
|
|
float glint = base;
|
|
|
|
base = base * 1.1 - 0.5;
|
|
glint = glint * 2.5 - 1.5;
|
|
|
|
return vec3(
|
|
(isCursor ? vec2(0.0, 1.0) : vec2(1.0, 0.0)) * base,
|
|
glint
|
|
);
|
|
}
|
|
|
|
vec2 getSymbolUV(float index) {
|
|
float symbolX = modI(index, glyphTextureGridSize.x);
|
|
float symbolY = (index - symbolX) / glyphTextureGridSize.x;
|
|
symbolY = glyphTextureGridSize.y - symbolY - 1.;
|
|
return vec2(symbolX, symbolY);
|
|
}
|
|
|
|
vec2 getSymbol(vec2 uv, float index) {
|
|
// resolve UV to cropped position of glyph in MSDF texture
|
|
uv = fract(uv * vec2(numColumns, numRows));
|
|
uv = (uv + getSymbolUV(index)) / glyphTextureGridSize;
|
|
|
|
// MSDF: calculate brightness of fragment based on distance to shape
|
|
vec2 symbol;
|
|
{
|
|
vec2 unitRange = vec2(msdfPxRange) / glyphMSDFSize;
|
|
vec2 screenTexSize = vec2(1.0) / fwidth(uv);
|
|
float screenPxRange = max(0.5 * dot(unitRange, screenTexSize), 1.0);
|
|
|
|
float signedDistance = median3(texture2D(glyphMSDF, uv).rgb);
|
|
float screenPxDistance = screenPxRange * (signedDistance - 0.5);
|
|
symbol.r = clamp(screenPxDistance + 0.5, 0.0, 1.0);
|
|
}
|
|
|
|
return symbol;
|
|
}
|
|
|
|
void main() {
|
|
vec4 data = texture2D(computeState, vUV);
|
|
vec3 brightness = getBrightness(data.rg, vUV);
|
|
vec2 symbol = getSymbol(vUV, data.b);
|
|
gl_FragColor = vec4(brightness.rg * symbol.r, brightness.b * symbol.g, 0.);
|
|
}
|
|
`,
|
|
|
|
uniforms: {
|
|
...commonUniforms,
|
|
computeState: computeDoubleBuffer.front,
|
|
glyphMSDF: glyphMSDF.texture,
|
|
msdfPxRange: 4.0,
|
|
glyphMSDFSize: () => [glyphMSDF.width(), glyphMSDF.height()],
|
|
screenSize: regl.prop("screenSize"),
|
|
},
|
|
|
|
attributes: {
|
|
aPosition: quadVertices,
|
|
},
|
|
count: numVerticesPerQuad,
|
|
|
|
framebuffer: output,
|
|
});
|
|
|
|
const screenSize = [1, 1];
|
|
|
|
return makePass(
|
|
{
|
|
primary: output,
|
|
},
|
|
Promise.all([glyphMSDF.loaded]),
|
|
(w, h) => {
|
|
output.resize(w, h);
|
|
const aspectRatio = w / h;
|
|
[screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
|
|
},
|
|
() => {
|
|
compute();
|
|
regl.clear({
|
|
depth: 1,
|
|
color: [0, 0, 0, 1],
|
|
framebuffer: output,
|
|
});
|
|
render({ screenSize });
|
|
}
|
|
);
|
|
};
|