Skip to content

Commit 9ce76f1

Browse files
authored
Merge pull request #1 from vrrdnt/claude/revert-background-style-cYsNt
2 parents 77dbad3 + 52d2c88 commit 9ce76f1

1 file changed

Lines changed: 115 additions & 106 deletions

File tree

ascii-bg.js

Lines changed: 115 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -15,132 +15,142 @@
1515
const FPS = 60;
1616
const FRAME_MS = 1000 / FPS;
1717

18-
// Matrix rain tunables
19-
const STREAM_SPEED_MIN = 0.25; // rows per frame
20-
const STREAM_SPEED_MAX = 0.65;
21-
const STREAM_LEN_MIN = 10; // trail length in cells
22-
const STREAM_LEN_MAX = 24;
23-
const COL_SPAWN_MIN = 20; // frames between spawns per column
24-
const COL_SPAWN_MAX = 120;
25-
const HEAD_ALPHA = 0.92; // alpha of the leading head cell
18+
// Radiation tunables
19+
const BASE_A = 0.04; // base alpha of idle characters
20+
21+
// Cloud-chamber streak tunables
22+
const STREAK_SPEED_MIN = 2.0; // cells per frame
23+
const STREAK_SPEED_MAX = 5.5;
24+
const STREAK_HEAD_A = 0.80; // alpha stamped at the head cell each frame
25+
const STREAK_DECAY = 0.055; // alpha lost per frame (trail ~15 frames)
26+
const STREAK_SPAWN_PROB = 0.022; // chance per frame to spawn a new streak
27+
const STREAK_MAX = 7; // max simultaneous streaks
2628

2729
// GoL tunables
28-
const GOL_ALIVE_A = 0.32; // base alpha for alive cells
29-
const BASE_A = 0.04; // base alpha for dead/idle cells
30-
const GOL_SEED = 0.30; // initial alive probability
31-
const GOL_RESEED = 0.04; // reseed threshold (fraction of cells alive)
32-
const GOL_TICK = 4; // advance GoL state every N rendered frames
30+
const GOL_ALIVE_A = 0.32; // base alpha for alive cells
31+
const GOL_SEED = 0.30; // initial alive probability
32+
const GOL_RESEED = 0.04; // reseed threshold (fraction of cells alive)
33+
const GOL_TICK = 4; // advance GoL state every N rendered frames
3334

3435
let canvas, ctx;
3536
let cols, rows, cells;
3637
let mouseX = -9999, mouseY = -9999;
3738
let lastTs = 0;
38-
let mode; // 'ascii' | 'gol'
39-
40-
// Matrix rain state
41-
let streams = [];
42-
let colCooldown = [];
39+
let mode; // 'radiation' | 'gol'
4340

4441
// GoL double-buffer (Uint8Array for speed)
4542
let golCur, golNxt;
4643
let golFrame = 0;
4744

45+
// Cloud-chamber streaks
46+
let streakA; // Float32Array — per-cell streak alpha contribution
47+
let streaks = [];
48+
4849
// ── Helpers ────────────────────────────────────────────────────────────────
4950

5051
function rchar() { return CHARS[Math.random() * CHARS.length | 0]; }
5152

52-
// ── Matrix rain ─────────────────────────────────────────────────────────────
53+
// ── Cloud-chamber streaks ──────────────────────────────────────────────────
5354

54-
function initStreams() {
55-
streams = [];
56-
colCooldown = Array.from({ length: cols }, () => (Math.random() * COL_SPAWN_MAX) | 0);
55+
function initStreakBuffer() {
56+
streakA = new Float32Array(cols * rows);
57+
streaks = [];
5758
}
5859

59-
function updateStreams() {
60-
// Spawn new streams
61-
for (let col = 0; col < cols; col++) {
62-
if (colCooldown[col] > 0) { colCooldown[col]--; continue; }
63-
// Don't spawn a new stream while one is still near the top of this column
64-
const tooClose = streams.some(s => s.col === col && s.headY < STREAM_LEN_MAX + 2);
65-
if (!tooClose) {
66-
streams.push({
67-
col,
68-
headY: 0,
69-
speed: STREAM_SPEED_MIN + Math.random() * (STREAM_SPEED_MAX - STREAM_SPEED_MIN),
70-
len: (STREAM_LEN_MIN + Math.random() * (STREAM_LEN_MAX - STREAM_LEN_MIN)) | 0,
71-
rc: {}, // row -> char
72-
});
73-
colCooldown[col] = (COL_SPAWN_MIN + Math.random() * (COL_SPAWN_MAX - COL_SPAWN_MIN)) | 0;
60+
function updateStreaks() {
61+
// Fade all trail cells
62+
for (let i = 0; i < streakA.length; i++) {
63+
if (streakA[i] > 0) {
64+
streakA[i] -= STREAK_DECAY;
65+
if (streakA[i] < 0) streakA[i] = 0;
7466
}
7567
}
7668

77-
// Advance all streams
78-
for (let i = streams.length - 1; i >= 0; i--) {
79-
const s = streams[i];
80-
s.headY += s.speed;
81-
const hr = s.headY | 0;
82-
83-
// Head char changes rapidly
84-
s.rc[hr] = rchar();
85-
86-
// Assign and occasionally mutate trail chars
87-
for (let d = 1; d < s.len; d++) {
88-
const r = hr - d;
89-
if (r < 0) continue;
90-
if (!s.rc[r]) s.rc[r] = rchar();
91-
else if (Math.random() < 0.04) s.rc[r] = rchar();
69+
// Spawn a new streak
70+
if (streaks.length < STREAK_MAX && Math.random() < STREAK_SPAWN_PROB) {
71+
// Start on a random edge
72+
const side = Math.random() * 4 | 0;
73+
let x, y;
74+
if (side === 0) { x = Math.random() * cols; y = 0; }
75+
else if (side === 1) { x = cols; y = Math.random() * rows; }
76+
else if (side === 2) { x = Math.random() * cols; y = rows; }
77+
else { x = 0; y = Math.random() * rows; }
78+
79+
// Aim roughly toward a random interior point
80+
const tx = cols * (0.25 + Math.random() * 0.5);
81+
const ty = rows * (0.25 + Math.random() * 0.5);
82+
const dist = Math.hypot(tx - x, ty - y) || 1;
83+
const speed = STREAK_SPEED_MIN + Math.random() * (STREAK_SPEED_MAX - STREAK_SPEED_MIN);
84+
streaks.push({ x, y, dx: (tx - x) / dist, dy: (ty - y) / dist, speed });
85+
}
86+
87+
// Advance each streak
88+
for (let i = streaks.length - 1; i >= 0; i--) {
89+
const s = streaks[i];
90+
s.x += s.dx * s.speed;
91+
s.y += s.dy * s.speed;
92+
const col = s.x | 0;
93+
const row = s.y | 0;
94+
if (col >= 0 && col < cols && row >= 0 && row < rows) {
95+
streakA[row * cols + col] = STREAK_HEAD_A;
9296
}
97+
if (s.x < -1 || s.x > cols + 1 || s.y < -1 || s.y > rows + 1) {
98+
streaks.splice(i, 1);
99+
}
100+
}
101+
}
102+
103+
// ── Radiation ───────────────────────────────────────────────────────────────
93104

94-
// Prune chars that have scrolled above the tail
95-
for (const r in s.rc) {
96-
if (+r < hr - s.len - 1) delete s.rc[r];
105+
function updateRadiation() {
106+
updateStreaks();
107+
108+
for (let i = 0; i < cells.length; i++) {
109+
const c = cells[i];
110+
111+
// Character cycling
112+
if (--c.t <= 0) {
113+
c.ch = rchar();
114+
c.t = (8 + Math.random() * 70) | 0;
97115
}
98116

99-
// Remove streams whose tail is fully off the bottom
100-
if (s.headY - s.len > rows + 1) streams.splice(i, 1);
117+
// Random flash trigger
118+
if (c.flash > 0) {
119+
c.flash -= FLASH_DECAY;
120+
if (c.flash < 0) c.flash = 0;
121+
} else if (Math.random() < FLASH_CHANCE) {
122+
c.flash = FLASH_PEAK;
123+
}
101124
}
102125
}
103126

104-
function drawMatrix() {
127+
function drawRadiation() {
105128
ctx.clearRect(0, 0, canvas.width, canvas.height);
106129
ctx.font = `${FONT_SZ}px "JetBrains Mono", monospace`;
107130
ctx.textAlign = 'center';
108131
ctx.textBaseline = 'middle';
109132

110-
for (const s of streams) {
111-
const hr = s.headY | 0;
112-
const cx = s.col * CELL + CELL * 0.5;
113-
114-
for (let d = 0; d <= s.len; d++) {
115-
const row = hr - d;
116-
if (row < 0 || row >= rows) continue;
117-
118-
const cy = row * CELL + CELL * 0.5;
119-
const ch = s.rc[row] || ' ';
120-
const idx = row * cols + s.col;
121-
122-
if (d === 0) {
123-
// Head: near-white, bright
124-
ctx.fillStyle = `rgba(200,255,210,${HEAD_ALPHA})`;
125-
ctx.fillText(ch, cx, cy);
126-
continue;
127-
}
128-
129-
// Trail: fade with distance from head
130-
let a = 0.7 * Math.pow(1 - d / s.len, 1.8);
133+
for (let row = 0; row < rows; row++) {
134+
const cy = row * CELL + CELL * 0.5;
135+
for (let col = 0; col < cols; col++) {
136+
const cx = col * CELL + CELL * 0.5;
137+
const c = cells[row * cols + col];
131138

132-
// Mouse glow
139+
// Mouse proximity glow
133140
const md = Math.hypot(cx - mouseX, cy - mouseY);
134-
a += md < MOUSE_R ? MOUSE_PEAK * Math.pow(1 - md / MOUSE_R, 1.8) : 0;
141+
let a = BASE_A + (md < MOUSE_R
142+
? MOUSE_PEAK * Math.pow(1 - md / MOUSE_R, 1.8)
143+
: 0);
135144

136-
// Flash
137-
if (cells[idx]) a += cells[idx].flash;
145+
// Random flash + streak trail
146+
a += c.flash;
147+
a += streakA[row * cols + col];
138148

139149
if (a < 0.008) continue;
140150
if (a > 0.88) a = 0.88;
141151

142152
ctx.fillStyle = `rgba(0,200,71,${a.toFixed(3)})`;
143-
ctx.fillText(ch, cx, cy);
153+
ctx.fillText(c.ch, cx, cy);
144154
}
145155
}
146156
}
@@ -220,32 +230,31 @@
220230
for (let i = 0; i < n; i++) {
221231
cells[i] = { ch: rchar(), t: (Math.random() * 70) | 0, flash: 0 };
222232
}
223-
if (mode === 'ascii') initStreams();
224-
if (mode === 'gol') initGol();
233+
if (mode === 'radiation') initStreakBuffer();
234+
if (mode === 'gol') initGol();
225235
}
226236

227237
function update() {
228-
if (mode === 'ascii') {
229-
updateStreams();
238+
if (mode === 'radiation') {
239+
updateRadiation();
230240
} else {
231-
if (++golFrame >= GOL_TICK) { golFrame = 0; stepGol(); }
232-
}
233-
234-
// Random flashes — GoL only flashes alive cells; matrix flashes are visible only on trail cells
235-
for (let i = 0; i < cells.length; i++) {
236-
const c = cells[i];
237-
if (c.flash > 0) {
238-
c.flash -= FLASH_DECAY;
239-
if (c.flash < 0) c.flash = 0;
240-
} else if (mode === 'ascii' || golCur[i]) {
241-
if (Math.random() < FLASH_CHANCE) c.flash = FLASH_PEAK;
241+
// Flash on alive GoL cells
242+
for (let i = 0; i < cells.length; i++) {
243+
const c = cells[i];
244+
if (c.flash > 0) {
245+
c.flash -= FLASH_DECAY;
246+
if (c.flash < 0) c.flash = 0;
247+
} else if (golCur[i] && Math.random() < FLASH_CHANCE) {
248+
c.flash = FLASH_PEAK;
249+
}
242250
}
251+
if (++golFrame >= GOL_TICK) { golFrame = 0; stepGol(); }
243252
}
244253
}
245254

246255
function draw() {
247-
if (mode === 'ascii') drawMatrix();
248-
else drawGol();
256+
if (mode === 'radiation') drawRadiation();
257+
else drawGol();
249258
}
250259

251260
function loop(ts) {
@@ -258,7 +267,7 @@
258267

259268
// ── Toggle button ────────────────────────────────────────────────────────────
260269

261-
const TOGGLE_LABELS = { ascii: '[ life ]', gol: '[ matrix ]' };
270+
const TOGGLE_LABELS = { radiation: '[ life ]', gol: '[ death ]' };
262271
// ASCII-only pool for button glitch — avoids double-width CJK glyphs shifting layout
263272
const BTN_CHARS = '0123456789ABCDEFabcdef+x=[]{}|/<>?!#$%';
264273
function rbchar() { return BTN_CHARS[Math.random() * BTN_CHARS.length | 0]; }
@@ -289,9 +298,9 @@
289298
}, 80);
290299

291300
btn.addEventListener('click', () => {
292-
mode = mode === 'ascii' ? 'gol' : 'ascii';
293-
if (mode === 'gol') { initGol(); golFrame = 0; }
294-
if (mode === 'ascii') { initStreams(); }
301+
mode = mode === 'radiation' ? 'gol' : 'radiation';
302+
if (mode === 'gol') { initGol(); golFrame = 0; }
303+
if (mode === 'radiation') { initStreakBuffer(); }
295304
buildLabel(btn, TOGGLE_LABELS[mode]);
296305
});
297306
}
@@ -303,7 +312,7 @@
303312
if (!canvas) return;
304313
ctx = canvas.getContext('2d');
305314

306-
mode = Math.random() < 0.5 ? 'ascii' : 'gol';
315+
mode = Math.random() < 0.5 ? 'radiation' : 'gol';
307316

308317
resize();
309318
window.addEventListener('resize', resize);

0 commit comments

Comments
 (0)