PageSourceSearch

https://lucasteske.dev/assets/matrix-rain.js

js lucasteske.dev collected 2026-10-02 06:13:59 UTC 6,770 bytes, 209 lines download raw bytes

1(() => {
2  "use strict";
3
4  const canvas = document.querySelector("canvas.matrix-bg");
5  if (!(canvas instanceof HTMLCanvasElement)) {
6    return;
7  }
8
9  const context = canvas.getContext("2d");
10  if (!context) {
11    return;
12  }
13
14  const MAX_PIXEL_RATIO = 2;
15  const FONT_FAMILY = '"Courier New", Courier, monospace, "Microsoft YaHei", "PingFang SC", sans-serif';
16  const HEX_GLYPHS = "0123456789ABCDEF";
17  const HAN_GLYPHS = String.fromCodePoint(
18    0x7535, 0x8111, 0x7f51, 0x7edc, 0x6570, 0x636e, 0x7cfb, 0x7edf,
19    0x7801, 0x673a, 0x82af, 0x7247, 0x7b97, 0x6cd5, 0x865a, 0x62df,
20    0x4e91, 0x667a, 0x80fd, 0x6e90,
21  );
22  const COLORS = ["#00ff41", "#bd00ff", "#00f0ff", "#ff38b6"];
23  const STREAM_GROUPS = [
24    { count: 34, glyphCount: 52, duration: [64, 90], blur: [3, 5], opacity: [0.2, 0.35], size: [0.6, 0.85] },
25    { count: 37, glyphCount: 52, duration: [128, 172], blur: [3, 5], opacity: [0.2, 0.35], size: [0.6, 0.85] },
26    { count: 18, glyphCount: 26, duration: [42, 58], blur: [2, 3], opacity: [0.4, 0.5], size: [0.9, 1] },
27    { count: 17, glyphCount: 26, duration: [84, 116], blur: [2, 3], opacity: [0.4, 0.5], size: [0.9, 1] },
28    { count: 10, glyphCount: 16, duration: [28, 38], blur: [6, 8], opacity: [0.2, 0.3], size: [1.1, 1.3] },
29    { count: 10, glyphCount: 16, duration: [56, 72], blur: [6, 8], opacity: [0.2, 0.3], size: [1.1, 1.3] },
30  ];
31
32  let randomState = 0x1b873593;
33  let viewportWidth = 0;
34  let viewportHeight = 0;
35  let pixelRatio = 1;
36  let rootFontSize = 16;
37  let animationFrame = 0;
38
39  function random() {
40    randomState += 0x6d2b79f5;
41    let value = randomState;
42    value = Math.imul(value ^ (value >>> 15), value | 1);
43    value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
44    return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
45  }
46
47  function randomBetween([minimum, maximum]) {
48    return minimum + (maximum - minimum) * random();
49  }
50
51  function randomInteger([minimum, maximum]) {
52    return Math.floor(randomBetween([minimum, maximum + 1]));
53  }
54
55  function createGlyphs(length) {
56    let glyphs = "";
57
58    for (let index = 0; index < length; index += 1) {
59      const alphabet = index % 4 === 3 ? HAN_GLYPHS : HEX_GLYPHS;
60      glyphs += alphabet[Math.floor(random() * alphabet.length)];
61    }
62
63    return glyphs;
64  }
65
66  function createStreams() {
67    const streams = [];
68
69    for (const group of STREAM_GROUPS) {
70      for (let index = 0; index < group.count; index += 1) {
71        streams.push({
72          blur: randomInteger(group.blur),
73          color: COLORS[Math.floor(random() * COLORS.length)],
74          duration: randomBetween(group.duration) * 1000,
75          glyphs: createGlyphs(group.glyphCount),
76          opacity: randomBetween(group.opacity),
77          phase: random(),
78          size: randomBetween(group.size),
79          x: (index + random()) / group.count,
80          texture: null,
81          textureHeight: 0,
82          textureTop: 0,
83          textureWidth: 0,
84        });
85      }
86    }
87
88    return streams;
89  }
90
91  const streams = createStreams();
92  const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
93
94  function createTexture(stream) {
95    const fontSize = Math.max(stream.size * rootFontSize, viewportWidth / 100);
96    const lineHeight = fontSize * 1.18;
97    const padding = Math.ceil(stream.blur * 3 + 2);
98    const textureWidth = Math.ceil(fontSize * 1.25 + padding * 2);
99    const textureHeight = Math.ceil(lineHeight * stream.glyphs.length + padding * 2);
100    const texture = document.createElement("canvas");
101
102    texture.width = Math.ceil(textureWidth * pixelRatio);
103    texture.height = Math.ceil(textureHeight * pixelRatio);
104
105    const textureContext = texture.getContext("2d");
106    if (!textureContext) {
107      return null;
108    }
109
110    textureContext.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
111    textureContext.filter = `blur(${stream.blur}px)`;
112    textureContext.fillStyle = stream.color;
113    textureContext.font = `700 ${fontSize}px ${FONT_FAMILY}`;
114    textureContext.globalAlpha = stream.opacity;
115    textureContext.textAlign = "center";
116    textureContext.textBaseline = "top";
117
118    for (let index = 0; index < stream.glyphs.length; index += 1) {
119      textureContext.fillText(stream.glyphs[index], textureWidth / 2, padding + index * lineHeight);
120    }
121
122    stream.textureHeight = textureHeight;
123    stream.textureTop = padding;
124    stream.textureWidth = textureWidth;
125    return texture;
126  }
127
128  function render(timestamp) {
129    context.clearRect(0, 0, viewportWidth, viewportHeight);
130
131    const travelDistance = viewportHeight * 3;
132    const staticTop = viewportHeight * 0.2;
133
134    for (const stream of streams) {
135      if (!stream.texture) {
136        continue;
137      }
138
139      const top = motionQuery.matches
140        ? staticTop
141        : -viewportHeight * 1.5 + ((timestamp / stream.duration + stream.phase) % 1) * travelDistance;
142
143      context.drawImage(
144        stream.texture,
145        stream.x * viewportWidth - stream.textureWidth / 2,
146        top - stream.textureTop,
147        stream.textureWidth,
148        stream.textureHeight,
149      );
150    }
151  }
152
153  function resize() {
154    const bounds = canvas.getBoundingClientRect();
155    const nextWidth = Math.max(1, Math.round(bounds.width));
156    const nextHeight = Math.max(1, Math.round(bounds.height));
157    const nextPixelRatio = Math.min(window.devicePixelRatio || 1, MAX_PIXEL_RATIO);
158
159    if (
160      nextWidth === viewportWidth
161      && nextHeight === viewportHeight
162      && nextPixelRatio === pixelRatio
163    ) {
164      return;
165    }
166
167    viewportWidth = nextWidth;
168    viewportHeight = nextHeight;
169    pixelRatio = nextPixelRatio;
170    rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
171    canvas.width = Math.ceil(viewportWidth * pixelRatio);
172    canvas.height = Math.ceil(viewportHeight * pixelRatio);
173    context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
174
175    for (const stream of streams) {
176      stream.texture = createTexture(stream);
177    }
178
179    render(performance.now());
180  }
181
182  function scheduleFrame() {
183    if (animationFrame || document.hidden || motionQuery.matches) {
184      return;
185    }
186
187    animationFrame = window.requestAnimationFrame((timestamp) => {
188      animationFrame = 0;
189      render(timestamp);
190      scheduleFrame();
191    });
192  }
193
194  function refreshAnimation() {
195    if (animationFrame) {
196      window.cancelAnimationFrame(animationFrame);
197      animationFrame = 0;
198    }
199
200    resize();
201    render(performance.now());
202    scheduleFrame();
203  }
204
205  window.addEventListener("resize", refreshAnimation, { passive: true });
206  document.addEventListener("visibilitychange", refreshAnimation);
207  motionQuery.addEventListener("change", refreshAnimation);
208  window.requestAnimationFrame(refreshAnimation);
209})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.