Matrix digital rain · solar-pro4
Create the Matrix digital rain effect on a full-screen black canvas: columns of falling green katakana glyphs with a trailing fade and a brighter leading character.
transcript · 4 messages
Create the Matrix digital rain effect on a full-screen black canvas: columns of falling green katakana glyphs with a trailing fade and a brighter leading character.
thinking
The user wants me to create a Matrix digital rain effect - the classic falling green katakana characters on a black screen. Let me build this as a single HTML file with a canvas-based animation.
Key features:
- Full-screen black canvas
- Columns of falling green katakana glyphs
- Trailing fade effect (fading out characters behind the leading one)
- Brighter leading character
- Green color scheme (classic Matrix green)
Let me think about the implementation:
1. Use a canvas element that fills the viewport
2. Create columns that descend at varying speeds
3. Each column has a "head" position that moves down, and trailing characters that fade out
4. Use katakana characters from Unicode
5. The leading character should be brighter (more opaque / brighter green)
6. Trailing characters should fade with decreasing opacity
Let me design this carefully:
- Canvas fills the screen
- Each column has a random speed
- Each column has a head y-position that decreases (falls down)
- When the head goes off the bottom, it resets to the top
- The head position is where the brightest character is drawn
- Behind the head, draw faded characters to create the trail
- The trail length can vary
Actually, a common and elegant approach:
- Each column has a `y` position (the lead character)
- Each column has a `speed`
- Each column has a `trail` array or we just draw a fade gradient behind the lead character
- Use a gradient of opacity from the lead character backward
Let me use a trail array approach:
- Each column stores its recent y-positions or just draws a fading trail
- Actually simpler: each column has a `y` (head position) and we draw the trail by going backward from `y` with decreasing opacity
Wait, the trail needs to show the actual characters that were drawn. A common approach is:
- Each column tracks its positions in a history buffer
- Or: each column has a "drop" that consists of the lead character and a fading trail
Let me use the approach where each column has:
- `x` position
- `y` position (head, starts at random position, moves down, resets when off-screen)
- `speed` (random)
- `length` (trail length, random)
And we draw from the head position upward, with characters at decreasing opacity.
Actually, the classic Matrix rain has each drop as a continuous stream. Let me implement it as:
For each column:
- `x`: fixed x position
- `y`: current head y position (decreases as it falls... actually increases as it falls down the screen, so y increases)
- `speed`: how fast it falls
- `trail`: the length of the trail
When drawing:
- Start from y and go upward (decreasing y) for `trail` steps
- Each step draws the character with decreasing opacity
- The lead character at y has full brightness
When y goes below the canvas height, reset y to a random position above the canvas (or negative), and reset speed.
Let me also make the head character slightly wider or brighter.
For the katakana characters, I'll use a set of common Katakana characters from Unicode (U+30A0 to U+30FF range).
Let me also add a slight variation in color - some drops could be slightly different shades of green.
Here's my plan:
```javascript
const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリウィゥクスツヌフムユュルグゼドボヂヅガヰグズデゼドバッパヵヶ'; // truncated
```
Actually, let me use a more complete set:
The katakana range in Unicode is U+30A0 to U+30FF. Let me generate characters from this range.
Let me write the full HTML now.
For the rendering approach:
- Use `globalAlpha` or rgba with varying alpha
- Draw each character with `fillText`
- The lead character uses full opacity and brighter color (maybe slightly cyan-green or pure bright green)
- The trail fades from bright to dark green/black
For performance, I'll limit the number of columns and use efficient rendering.
Let me also consider: the classic effect has the columns appearing and disappearing. I'll have each column's `y` start above the visible canvas (negative) so it emerges from the top.
Actually, a more realistic approach:
- Each column has `y` starting at a random position above the canvas
- When `y` exceeds canvas height, reset to a random negative value
- This creates the continuous falling effect
For the trail, I'll draw characters at `y - i * charHeight` for `i` from 0 to trailLength, with decreasing opacity.
Let me code this up:
```javascript
const CHAR_HEIGHT = 16;
const CHAR_WIDTH = 12;
// Each drop
const drops = [];
for (let i = 0; i < numColumns; i++) {
drops.push({
x: i * spacing,
y: Math.random() * -canvas.height, // start above
speed: 1 + Math.random() * 3,
length: 10 + Math.random() * 20,
});
}
```
For drawing each frame:
1. Fill canvas with very dark green/black (not pure black, to let the trail show slightly) - actually, for the fade effect, we need to draw a semi-transparent black rectangle over the canvas each frame, then draw the new characters on top. This creates the motion blur / fade effect.
Wait, that's a clever technique! Instead of drawing each character with decreasing opacity, we can:
1. Each frame, draw a semi-transparent black rectangle over the entire canvas (this fades the previous frame's characters)
2. Then draw the new lead characters at full brightness
This creates a natural fade trail effect! The characters from previous frames slowly fade out as the semi-transparent black is drawn over them repeatedly.
This is actually how the original Matrix screensaver worked (or at least a common implementation). Let me use this approach.
The opacity of the fade rectangle determines the trail length. Lower opacity = longer trail.
Let me use this approach:
- `fadeAlpha`: how much to fade each frame (e.g., 0.05-0.1)
- Each frame: `ctx.fillStyle = `rgba(0, 0, 0, ${fadeAlpha})`; ctx.fillRect(0, 0, w, h);`
- Then draw all current lead characters at full brightness
This is much more performant and creates a beautiful trail effect naturally.
Let me write the complete implementation:
For the katakana characters, I'll generate them from the Unicode range and also include some Latin characters occasionally for variety.
Actually, the user specifically asked for katakana glyphs, so I'll stick with katakana.
Let me create a good set of katakana characters. I'll use the standard katakana Unicode block.
Here's my final design:
1. Full-screen canvas
2. Semi-transparent fade each frame creates the trail effect
3. Columns of katakana characters falling at varying speeds
4. Lead character drawn with bright green, full opacity, slightly larger
5. Fade alpha controls trail length
6. Random reset positions when drops go off screen
7. Dark background (not pure black, maybe very dark green for atmosphere)
Actually, for the fade technique, the background should be black, and the fade rectangle should be semi-transparent black. Each frame, the previous frame's characters get slightly dimmer. The lead characters drawn each frame are bright. This creates the effect of bright leading characters with fading trails behind them.
Let me finalize and write the code.
One more detail: I'll make the lead character slightly brighter - maybe use a color like `rgba(0, 255, 70, 1)` for the lead and varying for the trail (but the trail is handled by the fade technique, so the lead is always full bright).
Actually, with the fade technique, the lead characters are drawn each frame at full brightness. The "trail" is just the previous positions of the lead, which naturally fade due to the semi-transparent black overlay.
But wait - with this technique, the "trail" characters are the same character repeated at the same x position. That's exactly what we want! Each frame, we draw the new character at the new y position, and the old ones fade.
But there's a subtlety: we need to draw the character at the current y position each frame. The previous frames' characters at previous y positions will persist and fade. This creates the trail.
So the algorithm is:
1. For each drop, update y += speed
2. Draw semi-transparent black over canvas (fade)
3. For each drop, draw the character at (x, y) with full brightness
The trail length is controlled by the fade alpha. Lower alpha = longer trail.
This is elegant and efficient!
Let me also add some visual polish:
- The lead character could be slightly brighter or a different shade
- Maybe some drops are brighter than others
- Add a subtle glow effect on the lead character (shadow blur)
Let me write the code now. I'll also add a subtle scanline effect or vignette for atmosphere, but keep it subtle.
Actually, let me keep it clean and focused on the rain effect. No need for extra effects unless they enhance the Matrix feel.
Let me also consider: should the characters be drawn with a slight offset or jitter? In the original Matrix, each character is drawn precisely. Let me keep it precise.
One more thing: the classic Matrix rain sometimes has the drops start at random positions and the "heads" are the brightest. With the fade technique, every character drawn is bright, and the trail is formed by the fading of previous characters. The "head" is simply the most recently drawn character, which is the brightest because it hasn't been faded yet.
Wait, actually with the fade technique, the head is the same brightness as the first character of the trail (since both are drawn fresh each frame). The difference is that the head is at the newest position, and the trail characters are at older positions that have been faded more.
Hmm, but the user asked for a "brighter leading character". With the simple fade technique, the leading character isn't brighter than the start of the trail - they're drawn at the same opacity.
To make the leading character brighter, I can:
1. Draw the leading character at full opacity with a brighter color
2. Then draw the fade rectangle
3. Then... no, that won't work because the fade would affect the leading character too.
Alternative approach:
1. Draw the fade rectangle (semi-transparent black)
2. Draw all characters at their current positions, but make the leading character brighter
Actually, the standard approach is:
1. Draw fade rectangle
2. For each drop, draw the character at (x, y) with full brightness
The leading character IS the brightest because it's drawn last (on top of the fade). The trail characters from previous frames have been faded by multiple overlay passes.
So the leading character naturally appears brighter because it's drawn after the fade each frame. The trail characters have been subjected to the fade overlay multiple times.
This works! The leading character is effectively the brightest.
But to make it even more pronounced, I can draw the leading character with a slightly brighter color or with a glow effect.
Let me implement this:
- Draw fade overlay
- Draw each character at its current position with bright green
- Optionally draw an extra brighter version of the leading character with a glow
Actually, I think the natural fade technique already makes the leading character the brightest. Let me just use that and add a subtle glow on the lead character for extra effect.
Let me write the code:
```html
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; overflow: hidden; background: black; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// ... implementation
</script>
</body>
</html>
```
Let me generate the katakana string. I'll use the Unicode range U+30A0 to U+30FF, which covers katakana. I'll also add some extended katakana.
Actually, let me just hardcode a good set of katakana characters:
```javascript
const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリウィゥクスツヌフムユュルグゼゾドヂヅデゼドバッパヵヶ';
```
Hmm, let me generate from Unicode instead:
```javascript
const katakana = [];
for (let i = 0x30A0; i <= 0x30FF; i++) {
katakana.push(String.fromCharCode(i));
}
// Also add some from U+31F0 to U+31FF (Katakana Phonetic Extensions)
for (let i = 0x31F0; i <= 0x31FF; i++) {
katakana.push(String.fromCharCode(i));
}
```
This gives a good variety. Let me use this approach.
Now let me write the full code:
For the canvas sizing, I'll make it fill the window and handle resize.
For the drop spacing, I'll use a random spacing or fixed spacing. Fixed spacing with slight random offset looks good. Let me use a grid with some randomness.
Actually, for a more organic look, I'll use random x positions with some minimum spacing.
Let me use a column count based on canvas width / character width.
Here's my final implementation plan:
```
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let width, height;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
}
const katakana = generateKatakana();
const drops = [];
const NUM_DROPS = Math.floor(width / 16); // roughly one per 16 pixels
const CHAR_WIDTH = 14;
const CHAR_HEIGHT = 18;
const FONT_SIZE = 16;
function initDrops() {
drops.length = 0;
for (let i = 0; i < NUM_DROPS; i++) {
drops.push({
x: Math.random() * width,
y: Math.random() * -height,
speed: 1 + Math.random() * 4,
katakana: katakana[Math.floor(Math.random() * katakana.length)],
brightness: 0.8 + Math.random() * 0.2, // slight variation
});
}
}
function draw() {
// Fade overlay
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
ctx.fillRect(0, 0, width, height);
// Draw characters
ctx.font = `${FONT_SIZE}px monospace`;
ctx.textAlign = 'center';
for (const drop of drops) {
// Update position
drop.y += drop.speed;
// Reset if off screen
if (drop.y > height) {
drop.y = Math.random() * -20;
drop.speed = 1 + Math.random() * 4;
drop.katakana = katakana[Math.floor(Math.random() * katakana.length)];
}
// Draw character
ctx.fillStyle = `rgba(0, 255, 70, ${drop.brightness})`;
ctx.fillText(drop.katakana, drop.x, drop.y);
}
requestAnimationFrame(draw);
}
```
Wait, I need to think about the fade alpha. If I use 0.05, the trail will be about 20 frames long (since 0.95^20 ≈ 0.36, still visible; 0.95^60 ≈ 0.047, nearly gone). At 60fps, that's about 1 second of trail, which seems good.
Actually, let me use a slightly lower alpha for a longer trail: 0.03-0.04.
Let me also add a subtle glow effect on the leading characters using shadowBlur.
Now, one important thing: the `fillText` y-coordinate is the baseline of the text. So the character is drawn above the y coordinate. I need to account for this. Actually, for the rain effect, it doesn't matter much since we're just drawing characters at positions. The y coordinate is where the baseline is, and the character appears above it.
Let me also consider: should I use `textBaseline = 'top'` so that y is the top of the character? This makes positioning more intuitive. Let me use that.
OK let me also think about the visual quality. The monospace font might not render katakana well. Let me use a Japanese-capable font. Since we can't load external fonts, I'll use the system font. In most systems, a sans-serif or serif font will render katakana. Let me use `'monospace'` or a generic font.
Actually, for the Matrix effect, a monospace font is ideal because it keeps the columns aligned. Let me use `'monospace'` or `'Courier New'`.
Hmm, but monospace fonts might not have good katakana glyphs. Let me test with what's available. Since I can't test, I'll use a generic approach and hope the system has a font that renders katakana.
Let me use: `ctx.font = '16px "Monaco", "Courier New", monospace';`
Or I could just use a sans-serif font. Let me use `'16px sans-serif'` and hope for the best. Actually, for the Matrix effect, people often use a pixel-style or monospace font. Let me use monospace.
OK, let me also add some refinements:
1. The leading character could be drawn slightly larger or with a glow
2. Different drops could have slightly different green shades (some more yellow-green, some more blue-green)
3. A subtle vignette effect
For the glow, I can use `ctx.shadowBlur = 10; ctx.shadowColor = 'rgba(0, 255, 70, 0.5)';` before drawing the leading character, then reset it after.
But shadowBlur is expensive. Let me use it selectively or skip it for performance.
Actually, for a full-screen effect with potentially hundreds of characters, shadowBlur might be too slow. Let me skip it and rely on the brightness difference.
Let me also think about the color. The classic Matrix green is approximately RGB(0, 255, 70) or similar. Let me use `#0f0` (pure green) or `#00ff40` for a slightly more yellow-green.
For variation, some characters could be slightly dimmer or a different shade.
OK, let me finalize the code. I'll make it polished and well-structured.
One more thought: the fade technique means I should draw the fade FIRST, then the characters. This way, the characters drawn this frame are bright, and older characters fade.
Wait, actually the order matters:
1. Draw fade (semi-transparent black) - this darkens everything that was drawn previously
2. Draw new characters at full brightness
This is correct. The new characters are drawn on top of the faded background, so they appear bright. The old characters (from previous frames) have been faded by the overlay.
But there's a subtlety: if I draw the fade first and then the characters, the characters are drawn at full opacity on top of the semi-transparent black. This means the characters are bright, and the background is dark. The trail is formed by the characters from previous frames that have been faded by multiple overlay passes.
This works perfectly.
Let me also handle the case where the canvas size changes (window resize). I'll reinitialize drops on resize.
Alright, let me write the final code:
Actually, let me reconsider the approach for making the leading character brighter. With the fade technique, the "leading character" is just the most recently drawn one. It's naturally the brightest because it hasn't been faded yet.
But to make it even more pronounced, I could:
1. Draw the fade overlay
2. Draw all characters at their positions (these are the "trail" characters from this frame's perspective - but actually, since we're drawing them fresh, they're all bright)
Hmm wait, I'm confusing myself. Let me think again.
With the fade technique:
- Frame N: draw fade, draw chars at positions P_N
- Frame N+1: draw fade (this fades everything from frame N), draw chars at positions P_{N+1}
So at frame N+1:
- Characters at P_{N+1} are bright (just drawn)
- Characters at P_N are faded (one overlay pass)
- Characters at P_{N-1} are more faded (two overlay passes)
- etc.
So the "leading character" at P_{N+1} is the brightest. The trail is the fading characters behind it.
This is exactly what we want! The leading character is naturally brighter because it's the most recently drawn.
To make the leading character even more prominent, I can draw it with a slightly different color or add a glow. But the natural fade already creates the effect.
Let me add a subtle enhancement: draw the leading character with a slightly brighter color or with a "glow" by drawing it twice - once with a larger, more transparent version.
Actually, let me keep it simple and just use the fade technique. It already produces the desired effect.
Let me write the final code now:
```javascript
// Generate katakana characters
const katakanaChars = [];
// Standard katakana: U+30A0 to U+30FF
for (let cp = 0x30A0; cp <= 0x30FF; cp++) {
katakanaChars.push(String.fromCodePoint(cp));
}
// Katakana phonetic extensions: U+31F0 to U+31FF
for (let cp = 0x31F0; cp <= 0x31FF; cp++) {
katakanaChars.push(String.fromCodePoint(cp));
}
// Half-width katakana: U+FF65 to U+FF9F
for (let cp = 0xFF65; cp <= 0xFF9F; cp++) {
katakanaChars.push(String.fromCodePoint(cp));
}
```
This gives a large set of katakana characters. Let me use this.
Now, for the drop initialization, I want the drops to be spread across the width with some randomness. Let me use:
```javascript
const SPACING = 16; // pixels between columns
const drops = [];
for (let x = SPACING/2; x < width; x += SPACING) {
drops.push({
x: x + (Math.random() - 0.5) * 4, // slight jitter
y: Math.random() * -height * 2, // start well above
speed: 0.5 + Math.random() * 2.5,
char: katakanaChars[Math.floor(Math.random() * katakanaChars.length)],
brightness: 0.85 + Math.random() * 0.15,
});
}
```
For the drawing:
```javascript
function draw() {
// Fade
ctx.fillStyle = 'rgba(0, 0, 0, 0.045)';
ctx.fillRect(0, 0, width, height);
// Draw drops
ctx.font = `${fontSize}px monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (const drop of drops) {
drop.y += drop.speed;
if (drop.y > height + fontSize) {
drop.y = -fontSize - Math.random() * 100;
drop.speed = 0.5 + Math.random() * 2.5;
drop.char = katakanaChars[Math.floor(Math.random() * katakanaChars.length)];
}
// Draw with glow for leading character
ctx.fillStyle = `rgba(0, 255, 65, ${drop.brightness})`;
ctx.fillText(drop.char, drop.x, drop.y);
}
requestAnimationFrame(draw);
}
```
Hmm, I want the leading character to be brighter. Let me draw it separately with a brighter color:
Actually, with the fade technique, the leading character IS the brightest. But I can enhance it by:
1. Drawing the character twice - once with a larger, semi-transparent version (glow), then the normal version
2. Using a brighter color for the character
Let me add a glow effect using shadowBlur, but only if performance allows. Actually, shadowBlur is expensive. Let me instead draw a larger, semi-transparent version of the character behind the main character:
```javascript
// Glow
ctx.fillStyle = `rgba(0, 255, 65, ${drop.brightness * 0.3})`;
ctx.font = `${fontSize * 1.5}px monospace`;
ctx.fillText(drop.char, drop.x, drop.y);
// Main character
ctx.fillStyle = `rgba(0, 255, 65, ${drop.brightness})`;
ctx.font = `${fontSize}px monospace`;
ctx.fillText(drop.char, drop.x, drop.y);
```
This creates a simple glow effect without shadowBlur. But it doubles the draw calls. For performance, let me only do this for a subset of drops, or skip it entirely.
Actually, let me just use the simple approach without glow, and rely on the fade technique for the trail effect. The leading character will naturally be the brightest.
Let me also consider: should I use a slightly different green for different drops? This adds visual interest. Some drops could be more yellow-green, some more blue-green.
```javascript
// Color variation
const greenVariations = [
[0, 255, 65], // bright green
[0, 240, 80], // slightly different
[10, 255, 50], // slightly warm
[0, 255, 100], // slightly cool
];
```
Each drop gets a random color from this set, and the brightness varies.
OK, I think I have a good plan. Let me write the complete code.
Actually, one more thing: I should handle the case where the window is resized. On resize, I should reinitialize the drops to match the new dimensions.
Let me also make sure the canvas is truly full-screen with no scrollbars.
Here's my final code structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Matrix Digital Rain</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; position: fixed; top: 0; left: 0; }
</style>
</head>
<body>
<canvas id="rain"></canvas>
<script>
// Full implementation
</script>
</body>
</html>
```
Let me write the complete JavaScript now.
For the katakana generation, I'll use codePoint-based generation. Let me also include some Latin characters occasionally for variety (like in the original Matrix).
Actually, the user asked specifically for katakana glyphs, so I'll stick with katakana. But I could add a small percentage of Latin/numbers for authenticity.
Let me include about 10% Latin characters and numbers, and 90% katakana.
OK, let me write the final implementation:
Actually, you know what, let me also think about performance. With a full-screen canvas and potentially 100+ drops, each frame we're:
1. Drawing a semi-transparent rectangle (cheap)
2. Drawing 100+ characters with fillText (moderately expensive)
At 60fps, this should be fine on modern hardware. fillText is not super fast, but 100 characters per frame should be OK.
Let me also consider using a more efficient approach: instead of drawing each character individually, I could use a pre-rendered texture. But that's overkill for this effect.
Alright, let me write the code.
One more refinement: I want the drops to have varying lengths. In the original Matrix, some columns are longer than others. With the fade technique, the "length" is determined by the fade alpha and the speed of the drop. Faster drops have shorter trails (because they move further between frames, so the trail is more spread out). Slower drops have longer, denser trails.
Actually, the trail length is determined by how long it takes for a character to fade to invisibility. With a fixed fade alpha, all drops have the same trail length in terms of time (frames). But in terms of distance, faster drops have longer trails (because they cover more distance in the same number of frames).
So the visual trail length varies naturally with speed. Faster drops = longer, more spread out trails. Slower drops = shorter, denser trails.
This is actually a nice effect! Let me keep it.
Let me also add some drops that start from the bottom and move upward? No, that's not the Matrix effect. The Matrix rain only falls downward.
OK, let me finalize and write the code.
Let me also think about the font size. For a full-screen effect, the font size should be large enough to be readable but small enough to fit many columns. A font size of 14-18px is good. Let me use 16px.
Actually, let me make the font size responsive to the screen size. On larger screens, use a larger font. But this adds complexity. Let me just use a fixed size that works well on most screens.
Font size: 16px, character dimensions: roughly 12px wide, 16px tall (for monospace).
Column spacing: 16px (so characters don't overlap horizontally).
Number of columns: width / 16.
On a 1920px wide screen: 120 columns. That's a lot. Each frame, we draw 120 characters. At 60fps, that's 7200 characters per second. Should be fine.
On a 1366px wide screen: ~85 columns. Fine.
OK, let me write the code now. I'll be thorough and make it polished.
Let me think about one more thing: the initial state. When the page loads, the rain should already be in progress (not starting from empty). I'll initialize the drops with random y positions spread across the screen, so the rain is immediately visible.
Actually, with the fade technique, if I initialize all drops at random y positions, the screen will initially be filled with characters at various positions, and then they'll all start falling. The fade will create trails immediately.
Alternatively, I can initialize drops at random y positions above the screen, so they emerge from the top. This creates a more natural "starting" effect.
Let me initialize drops at random y positions (some above, some below) so the screen is immediately filled with rain.
OK, here's my final approach for initialization:
- y positions: random from -height to height (so some are above, some below, some in the middle)
- This fills the screen immediately with rain at various stages of falling
Let me write the complete code now.
Actually, I realize I should also consider: the fade alpha value. Let me use 0.04. This means each frame, the previous content is multiplied by 0.96. After 25 frames (about 0.4 seconds at 60fps), the content is at 0.96^25 ≈ 0.36 of its original brightness. After 60 frames (1 second), it's at 0.96^60 ≈ 0.047. So the trail is about 1 second long, which looks good.
Let me use 0.035 for a slightly longer trail.
Alright, writing the code now:
Let me also add a very subtle background effect - maybe a slight gradient or vignette. Actually, let me keep it pure black for authenticity.
Let me also consider adding a scanline effect - horizontal lines that slightly dim the screen. This is a classic Matrix aesthetic. I can do this with a CSS overlay or by drawing lines on the canvas.
A CSS overlay with repeating linear gradient would be efficient:
```css
canvas::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15) 0px,
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 3px
);
pointer-events: none;
}
```
Wait, canvas doesn't support ::after. I'd need a separate div overlay. Let me add a div overlay for scanlines.
Actually, let me keep it simple and skip scanlines. The rain effect itself is the main attraction.
But a subtle scanline effect really adds to the Matrix feel. Let me add it as a separate div overlay.
OK, let me finalize the design and write the code. Here's my final plan:
1. Full-screen canvas for the rain
2. Semi-transparent black overlay div for scanlines (subtle)
3. JavaScript for the rain effect using the fade technique
4. Katakana characters with occasional Latin characters
5. Leading character naturally brightest due to fade technique
6. Slight color variation among drops
7. Responsive to window resize
Let me write it all now.
Hmm, actually for the scanlines, let me use a CSS-only approach with a pseudo-element on the body or a separate div. Let me use a separate div.
Wait, I want to keep the HTML minimal. Let me use a body::after pseudo-element for scanlines.
```css
body::after {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.12) 0px,
rgba(0, 0, 0, 0.12) 1px,
transparent 1px,
transparent 3px
);
pointer-events: none;
z-index: 10;
}
```
This adds subtle scanlines without affecting the canvas rendering.
OK, let me write the complete code now. I'll be thorough.
Let me also add a very subtle green glow to the background. When the rain is heavy, the screen should have a slight green tint. I can achieve this with a CSS background on the body or a separate overlay.
Actually, the fade technique with semi-transparent black means the background is always black. The characters are green. There's no green glow on the background.
To add a green glow, I could:
1. Use a very dark green background instead of black: `rgb(0, 10, 0)` or similar
2. Add a green-tinted overlay
Let me use a very dark green background: `rgb(0, 5, 0)`. This gives a subtle green tint to the black areas.
OK, final code:
Actually wait, let me reconsider. With the fade technique:
- Background: black (or very dark green)
- Fade overlay: semi-transparent black (or very dark green)
- Characters: bright green
If the background is very dark green and the fade is also dark green, the overall effect is a dark green tint with bright green characters. This looks more like the Matrix.
Let me use:
- Background: `rgb(0, 0, 0)` (pure black)
- Fade overlay: `rgba(0, 0, 0, 0.04)` (semi-transparent black)
- Characters: `rgba(0, 255, 65, 1)` (bright green)
This is the classic look. Let me go with this.
Actually, I just realized: if the background is black and the fade is black, then the areas between characters are pure black. This is the classic Matrix look. Good.
Let me write the code now. For real this time.
Let me also think about whether to add any user interaction. The user didn't ask for any, so I'll keep it as a pure visual effect with no interaction.
Here's my final code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Matrix Digital Rain</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
canvas {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
body::after {
content: '';
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.10) 0px,
rgba(0, 0, 0, 0.10) 1px,
transparent 1px,
transparent 3px
);
pointer-events: none;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="matrix"></canvas>
<script>
const canvas = document.getElementById('matrix');
const ctx = canvas.getContext('2d');
let W, H;
const FONT_SIZE = 16;
const FADE_ALPHA = 0.038;
// Generate katakana character set
const katakana = [];
// Standard katakana block (U+30A0–U+30FF)
for (let cp = 0x30A0; cp <= 0x30FF; cp++) {
katakana.push(String.fromCodePoint(cp));
}
// Katakana phonetic extensions (U+31F0–U+31FF)
for (let cp = 0x31F0; cp <= 0x31FF; cp++) {
katakana.push(String.fromCodePoint(cp));
}
// Half-width katakana (U+FF65–U+FF9F)
for (let cp = 0xFF65; cp <= 0xFF9F; cp++) {
katakana.push(String.fromCodePoint(cp));
}
// Latin chars for occasional variety (10%)
const latin = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const drops = [];
const SPACING = 14;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
initDrops();
}
function initDrops() {
drops.length = 0;
for (let x = SPACING / 2; x < W; x += SPACING) {
drops.push({
x: x + (Math.random() - 0.5) * 3,
y: Math.random() * -H,
speed: 0.8 + Math.random() * 2.2,
char: katakana[Math.floor(Math.random() * katakana.length)],
brightness: 0.85 + Math.random() * 0.15,
hueShift: Math.random() > 0.9 ? latin : katakana,
});
}
}
function draw() {
// Fade previous frame
ctx.fillStyle = `rgba(0, 0, 0, ${FADE_ALPHA})`;
ctx.fillRect(0, 0, W, H);
ctx.font = `${FONT_SIZE}px "Courier New", monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (const d of drops) {
d.y += d.speed;
if (d.y > H + FONT_SIZE) {
d.y = -FONT_SIZE - Math.random() * 80;
d.speed = 0.8 + Math.random() * 2.2;
d.char = d.hueShift[Math.floor(Math.random() * d.hueShift.length)];
}
ctx.fillStyle = `rgba(0, 255, 65, ${d.brightness})`;
ctx.fillText(d.char, d.x, d.y);
}
requestAnimationFrame(draw);
}
window.addEventListener('resize', resize);
resize();
draw();
</script>
</body>
</html>
```
Wait, there's an issue with the `hueShift` logic. I set `hueShift` to either `latin` or `katakana`, and then use it to pick a character. But the `char` field is initially set from `katakana`. Let me fix this:
```javascript
function initDrops() {
drops.length = 0;
for (let x = SPACING / 2; x < W; x += SPACING) {
const useLatin = Math.random() < 0.1; // 10% Latin
const charSet = useLatin ? latin : katakana;
drops.push({
x: x + (Math.random() - 0.5) * 3,
y: Math.random() * -H,
speed: 0.8 + Math.random() * 2.2,
char: charSet[Math.floor(Math.random() * charSet.length)],
brightness: 0.85 + Math.random() * 0.15,
charSet: charSet,
});
}
}
```
And in the draw loop:
```javascript
if (d.y > H + FONT_SIZE) {
d.y = -FONT_SIZE - Math.random() * 80;
d.speed = 0.8 + Math.random() * 2.2;
d.char = d.charSet[Math.floor(Math.random() * d.charSet.length)];
}
```
This is cleaner.
Now, let me also think about the brightness of the leading character. The user specifically asked for a "brighter leading character". With the fade technique, the leading character is naturally the brightest. But I could make it even brighter by drawing it with a higher opacity or a different color.
One approach: draw the leading character with a slight glow. I can do this by drawing a larger, semi-transparent version of the character behind it:
```javascript
// Glow (larger, semi-transparent)
ctx.fillStyle = `rgba(0, 255, 65, ${d.brightness * 0.25})`;
ctx.font = `${FONT_SIZE * 1.6}px "Courier New", monospace`;
ctx.fillText(d.char, d.x, d.y);
// Main character
ctx.fillStyle = `rgba(0, 255, 65, ${d.brightness})`;
ctx.font = `${FONT_SIZE}px "Courier New", monospace`;
ctx.fillText(d.char, d.x, d.y);
```
But this doubles the draw calls and might impact performance. Let me only do this for a subset of drops, or skip it.
Actually, let me try a different approach: use shadowBlur for the leading character. But shadowBlur is expensive per draw call.
Let me skip the glow and rely on the natural brightness from the fade technique. The leading character IS the brightest.
But wait, the user said "brighter leading character". Let me interpret this as: the leading character should be brighter than the trail. With the fade technique, this is already the case. The leading character is drawn fresh each frame (full brightness), while the trail characters have been faded by the overlay.
To make this even more pronounced, I could:
1. Use a slightly higher brightness for the character (which I already do with `d.brightness`)
2. Draw the leading character with a pure green color (no alpha) while the trail characters from previous frames have been faded
Actually, the way I have it, every character is drawn with `rgba(0, 255, 65, d.brightness)`. The `d.brightness` is a fixed value per drop. The trail effect comes from the fade overlay, not from varying alpha per character.
So the leading character and the trail characters (from the same drop) are drawn with the same brightness. The difference is that the trail characters have been faded by the overlay in subsequent frames.
This means the leading character IS brighter than the trail, but the difference is subtle. To make it more pronounced, I could:
1. Draw the leading character with full opacity (alpha = 1) and the trail with lower opacity
2. But with the fade technique, we don't draw the trail explicitly; the trail is the faded remnants of previous frames
OK, I think the natural fade technique is sufficient. The leading character is the most recently drawn, so it's the brightest. Let me go with this.
But to add an extra touch, let me make the leading character use a pure green color (no alpha) while the drops have a slightly lower brightness. This way, the leading character is always bright green, and the trail fades from bright green to black.
Actually, I can achieve this by drawing the character at full opacity and letting the fade handle the trail:
```javascript
ctx.fillStyle = `rgba(0, 255, 65, ${d.brightness})`;
ctx.fillText(d.char, d.x, d.y);
```
With `d.brightness` around 0.9-1.0, the character is nearly full bright green. The fade overlay then creates the trail.
OK, I think this is good enough. Let me finalize the code.
One more thing: I want to make sure the characters are rendered correctly. Some systems might not have good katakana support in monospace fonts. Let me use a more compatible font stack.
For katakana rendering, the font needs to support Japanese characters. Most modern systems have this. Let me use:
```javascript
ctx.font = `${FONT_SIZE}px "Hiragino Kaku Gothic ProN", "Yu Gothic", "Meiryo", "MS Gothic", "Courier New", monospace`;
```
This prioritizes Japanese fonts, then falls back to monospace.
Actually, for the Matrix effect, a monospace font is preferred for the aligned columns. But Japanese monospace fonts (like MS Gothic) also support katakana. Let me use a font stack that prioritizes Japanese-capable monospace fonts.
Hmm, this is getting complex. Let me just use a simple font stack and hope for the best:
```javascript
ctx.font = `${FONT_SIZE}px monospace`;
```
Most systems will render katakana in monospace just fine.
OK, let me write the final code. I'll make it clean and well-organized.
Let me also add a nice touch: the speed of each drop varies, and the brightness varies. This creates a more organic, less uniform look.
Final code coming up. Let me also make the fade alpha slightly dynamic - maybe vary it slightly per frame for a more organic feel. No, let me keep it simple and consistent.
Here's my final implementation. Let me write it out completely.
Actually, I want to reconsider the trail effect one more time. The fade technique creates a trail where characters fade out over time. But the trail is uniform in the sense that all characters from all drops fade at the same rate.
In the original Matrix, the trails look more like continuous streams of characters. With the fade technique, the trail is a series of characters at the same x position, at different y positions, with decreasing brightness.
This looks correct. Each drop creates a vertical trail of characters that fades out as it goes up (toward older positions).
OK, I'm confident this will look good. Let me write the final code.
Let me also add a small optimization: instead of using `fillRect` for the fade, I could use `fillStyle` with `fillRect`. That's what I'm already doing. Good.
One more optimization: I could pre-compute the character set and avoid regenerating it on each frame. I'm already doing this. Good.
Let me write the code now. For real.
Let me also think about the initial visual. When the page loads, the canvas is black. Then the rain starts. With the fade technique, the first frame draws characters on a black background. The second frame fades the first frame's characters and draws new ones. After a few frames, the trails become visible.
To make the initial state look good, I'll initialize drops with random y positions spread across the screen (not just above). This way, the screen is immediately filled with rain at various stages.
```javascript
y: Math.random() * H * 1.5 - H * 0.5, // spread from -0.5H to H
```
This puts drops at all heights initially, so the screen looks full right away.
OK, writing the code now.
Wait, one more thing: the user said "trailing fade". This is exactly what the fade technique does. The characters trail behind the leading character and fade out. Good.
And "brighter leading character" - the leading character is the most recently drawn, so it's the brightest. Good.
Let me write the complete code.
I also want to add a very subtle "glow" to the leading characters. Let me try using a two-pass approach:
1. Draw the character with a larger font and low opacity (glow)
2. Draw the character with normal font and full opacity
But I'll only do this for every other drop to keep performance reasonable. Or I'll skip it entirely.
Actually, let me try a different approach for the glow: use `ctx.shadowBlur` and `ctx.shadowColor`. This is a native canvas feature that adds a glow around drawn shapes.
```javascript
ctx.shadowBlur = 8;
ctx.shadowColor = 'rgba(0, 255, 65, 0.6)';
ctx.fillStyle = `rgba(0, 255, 65, ${d.brightness})`;
ctx.fillText(d.char, d.x, d.y);
ctx.shadowBlur = 0; // reset
```
But shadowBlur is expensive, especially with many characters. Let me test it mentally: with 100 drops, each frame we'd do 100 shadowBlur operations. This might be slow.
Let me skip shadowBlur and use the simpler approach. The natural brightness from the fade technique is sufficient.
OK, FINAL CODE. Writing now.
Let me also add a nice touch: occasionally, a drop could be brighter than others (like a "highlight"). I can achieve this by giving some drops a higher brightness value.
I already have `d.brightness` varying from 0.85 to 1.0. Let me expand the range to 0.7 to 1.0 for more variation.
Actually, let me keep the brightness range narrow (0.85-1.0) so all characters are clearly visible. The trail effect comes from the fade, not from varying brightness.
OK, here's my truly final code. Let me write it out.
Hmm, one more consideration. The `fillText` y-coordinate with `textBaseline = 'top'` means y is the top of the text. As the drop falls, y increases, and the text moves down. When y > H, the text is below the visible area. I reset y to a negative value (above the screen) when this happens.
But with `textBaseline = 'top'`, the text at y = 0 is at the very top of the canvas. The text at y = -FONT_SIZE is above the canvas (not visible). When I reset y to `-FONT_SIZE - Math.random() * 80`, the text starts above the canvas and falls into view.
This is correct.
Let me also make sure the text doesn't get cut off at the bottom. When y > H, the text is below the canvas. I should reset before the text is completely off-screen. Let me use `y > H + FONT_SIZE` as the reset condition, which gives a little buffer.
OK, writing the code now. This is the final version.
Let me also consider: should I use `devicePixelRatio` for sharp rendering on high-DPI displays? This would make the text sharper but might impact performance.
For simplicity, let me skip DPR scaling. The text will be slightly blurry on high-DPI displays, but the effect will still look good.
Actually, let me add DPR scaling for better quality:
```javascript
function resize() {
const dpr = window.devicePixelRatio || 1;
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.scale(dpr, dpr);
initDrops();
}
```
Then all drawing coordinates are in CSS pixels, and the canvas is scaled by DPR for sharp rendering.
This is a good improvement. Let me include it.
But I need to be careful: after `ctx.scale(dpr, dpr)`, all subsequent drawing operations are in CSS pixels. The `fillRect` for the fade should also be in CSS pixels.
OK, let me include DPR scaling.
Actually, for performance, let me skip DPR scaling. The Matrix rain effect is meant to look slightly pixelated/glitchy, so sharp text isn't critical. Let me keep it simple.
No wait, sharp text looks better. Let me include DPR scaling. It's a small addition that improves quality.
OK, let me write the final code with DPR scaling.
Hmm, actually, with DPR scaling, the canvas buffer is larger (e.g., 2x for Retina), which means more pixels to fill with the fade rectangle. This impacts performance. For a full-screen effect, this could be significant.
Let me skip DPR scaling for performance. The visual quality is fine without it.
OK, FINAL DECISION: no DPR scaling. Keep it simple and performant.
Writing the code now. This is it.
Let me also think about the font size one more time. 16px is good for most screens. On very large screens (4K monitors), 16px might look small. On small screens (phones), 16px might look large.
For responsiveness, I could adjust the font size based on the screen size. But this adds complexity. Let me just use 16px and accept that it might not be perfect on all screens.
Actually, let me make the font size slightly responsive:
```javascript
const FONT_SIZE = Math.min(18, Math.max(12, Math.floor(W / 100)));
```
This gives a font size between 12 and 18, based on screen width. On a 1920px screen, it's 18px. On a 1366px screen, it's 13px. On a 800px screen, it's 12px.
Hmm, this might look inconsistent. Let me just use a fixed 16px.
OK, 16px it is. Writing the code now.
Let me also decide on the column spacing. With 16px font size, the characters are roughly 10-12px wide (for monospace, it's typically the same as the font size or slightly less). Let me use a spacing of 14px to allow some breathing room between columns.
With 14px spacing on a 1920px screen: ~137 columns. That's a lot. Each frame, we draw 137 characters. At 60fps, that's 8220 characters per second. Should be fine.
On a 1366px screen: ~97 columns. Fine.
On a 800px screen: ~57 columns. Fine.
OK, 14px spacing. Writing the code.
Let me also add a slight horizontal jitter to each column's x position, so they're not perfectly aligned. This creates a more organic look.
```javascript
x: x + (Math.random() - 0.5) * 4, // ±2px jitter
```
This is subtle but adds visual interest.
OK, writing the final code now. No more deliberation.
Let me also make the fade alpha slightly dynamic. Instead of a fixed 0.038, let me vary it slightly per frame:
```javascript
const fadeAlpha = 0.035 + Math.random() * 0.008;
```
This creates a subtle variation in trail length, making the effect feel more organic.
Actually, this might cause visible flickering in the trail length. Let me keep it fixed.
OK, fixed fade alpha of 0.038. Writing the code.
One more thing: I want to make sure the katakana characters are diverse enough. Let me check how many unique characters I'm generating.
Standard katakana (U+30A0 to U+30FF): 96 code points, but not all are valid katakana. The valid ones are roughly 100+ characters including variants.
Katakana phonetic extensions (U+31F0 to U+31FF): 16 characters.
Half-width katakana (U+FF65 to U+FF9F): 59 characters.
Total: ~175 characters. That's a good variety.
But some of these code points might not render as visible characters (they could be combining marks, diacritics, etc.). Let me filter out any that don't render properly.
Actually, I can't easily filter them without rendering. Let me just use a curated list of known katakana characters instead of generating from Unicode ranges.
Here's a curated list of katakana:
```javascript
const katakana = 'アァカサタナハマヤャラワガザダバパイィキシチニヒミリィゥヴェヲンッーガキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
```
Wait, this is too limited. Let me use a more comprehensive list.
Actually, let me generate from Unicode but filter out characters that are likely to be problematic (like combining marks).
The katakana block (U+30A0 to U+30FF) includes:
- U+30A0: ・ (katakana middle dot)
- U+30A1-U+30F6: standard katakana
- U+30F7-U+30FA: extended katakana (ヷ, ヸ, ヹ, ヺ)
- U+30FB: ・ (katakana middle dot, again?)
- U+30FC: ー (katakana long vowel mark)
- U+30FD-U+30FF: small katakana variants
Let me just generate all code points and let the browser handle it. If a character doesn't render, it'll just be blank or a box, which is fine.
Actually, let me be more careful. Let me generate from the ranges but skip code points that are likely to be non-printing characters.
You know what, let me just use a hardcoded string of katakana characters. This is the safest approach:
```javascript
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダデドバビブベボパピプペポッャュョー';
```
This covers all standard katakana. Let me also add some extended ones:
```javascript
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダデドバビブベボパピプペポッャュョーヴァヴシェジェチェヂヅヵヶ';
```
This is comprehensive. Let me use this.
Actually, I realize I'm overthinking this. Let me just generate from Unicode and use whatever renders. The browser will handle it.
OK, let me use the Unicode generation approach. If some characters don't render, they'll just be invisible, which is fine.
Let me write the final code now. I'll use Unicode generation for katakana.
Here's the code:
Actually, let me use a hybrid approach: generate from Unicode, but also include a hardcoded fallback string of common katakana. This ensures we always have characters to draw.
No, that's overcomplicating it. Let me just use Unicode generation.
FINAL CODE. Writing now. No more changes.
Let me also add a subtle feature: the drops could occasionally "flicker" (briefly become brighter). This adds visual interest. I can implement this by randomly increasing a drop's brightness for one frame.
No, let me keep it simple. The basic effect is already good.
OK, here's my final code. I'm writing it now and not changing anything else.
Wait, one more thing: I should make sure the canvas handles device orientation changes (for mobile). The resize handler already covers this.
Also, I should prevent the default touch behavior on mobile (scrolling, zooming). I can add:
```javascript
canvas.addEventListener('touchstart', e => e.preventDefault());
```
But the user didn't mention mobile, so let me skip this.
OK, writing the code. Final version.
Actually, let me reconsider the approach one more time. The user said "columns of falling green katakana glyphs with a trailing fade and a brighter leading character."
This clearly describes the Matrix digital rain. The key elements are:
1. Columns of katakana characters
2. Falling (downward motion)
3. Trailing fade (characters behind the lead fade out)
4. Brighter leading character
My implementation with the fade technique addresses all of these. The leading character is the most recently drawn, so it's the brightest. The trail is created by the fade overlay.
I'm confident this will look great. Let me write the code.
One final thought: should I add a slight "flicker" or "glitch" effect? Like occasional bright flashes or character scrambling? This would add to the Matrix aesthetic.
Let me add a subtle glitch effect: occasionally, a drop's character changes rapidly for a few frames, creating a scrambling effect. This is a classic Matrix visual.
Implementation: for each drop, there's a small chance (e.g., 0.1% per frame) that the character starts "glitching" - rapidly changing to random characters for 3-5 frames, then settling on a new character.
This adds visual interest without being distracting.
Let me implement this:
```javascript
// In the drop object:
glitchTimer: 0, // frames remaining in glitch
glitchChar: '', // character to display during glitch
// In the draw loop:
if (d.glitchTimer > 0) {
d.glitchTimer--;
d.char = d.glitchChar;
if (d.glitchTimer === 0) {
d.char = d.charSet[Math.floor(Math.random() * d.charSet.length)];
}
} else if (Math.random() < 0.002) {
// Start glitching
d.glitchTimer = 3 + Math.floor(Math.random() * 4);
d.glitchChar = d.charSet[Math.floor(Math.random() * d.charSet.length)];
}
```
This creates a subtle glitch effect where characters occasionally scramble before settling on a new one.
Actually, this might be distracting. Let me make it very subtle - low probability and short duration.
Let me include it but with low probability.
OK, let me write the final code with the glitch effect included.
Actually, you know what, let me keep it simple and skip the glitch effect. The basic rain effect is already what the user asked for. Let me not over-engineer it.
FINAL CODE. Writing now. For real. No more changes.
Let me also use `requestAnimationFrame` for smooth animation. I'm already doing this.
OK here we go:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Matrix Digital Rain</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%; overflow: hidden;
background: #000;
}
canvas {
display: block;
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
}
/* Subtle scanlines */
body::after {
content: '';
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,0.10) 0px,
rgba(0,0,0,0.10) 1px,
transparent 1px,
transparent 3px
);
pointer-events: none; z-index: 10;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
const FS = 16; // font size
const FADE = 0.038; // per-frame fade opacity
const SPACING = 14; // horizontal spacing between columns
/* Katakana character pool */
const KANA = [];
for (let cp = 0x30A1; cp <= 0x30F6; cp++) KANA.push(String.fromCodePoint(cp));
for (let cp = 0x31F0; cp <= 0x31FF; cp++) KANA.push(String.fromCodePoint(cp));
for (let cp = 0xFF65; cp <= 0xFF9F; cp++) KANA.push(String.fromCodePoint(cp));
// Add long-vowel mark and a few extras
KANA.push('ー', 'ヵ', 'ヶ');
const LATIN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const drops = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
drops.length = 0;
for (let x = SPACING/2; x < W; x += SPACING) {
const useLatin = Math.random() < 0.08;
const pool = useLatin ? LATIN : KANA;
drops.push({
x: x + (Math.random() - 0.5) * 3,
y: Math.random() * (H + FS * 4) - FS * 2,
speed: 0.7 + Math.random() * 2.3,
pool: pool,
char: pool[Math.floor(Math.random() * pool.length)],
brightness: 0.82 + Math.random() * 0.18,
});
}
}
function frame() {
// Fade previous content
ctx.fillStyle = `rgba(0,0,0,${FADE})`;
ctx.fillRect(0, 0, W, H);
ctx.font = `${FS}px "Courier New", "MS Gothic", monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (const d of drops) {
d.y += d.speed;
if (d.y > H + FS) {
d.y = -FS - Math.random() * 60;
d.speed = 0.7 + Math.random() * 2.3;
d.char = d.pool[Math.floor(Math.random() * d.pool.length)];
}
ctx.fillStyle = `rgba(0,255,65,${d.brightness})`;
ctx.fillText(d.char, d.x, d.y);
}
requestAnimationFrame(frame);
}
window.addEventListener('resize', resize);
resize();
requestAnimationFrame(frame);
</script>
</body>
</html>
```
Wait, I just realized there's a potential issue with the `ctx.scale` if I were using DPR. Since I'm not using DPR, I don't need to worry about it.
Let me review the code:
1. Canvas fills the window ✓
2. Fade overlay creates trail effect ✓
3. Katakana characters falling ✓
4. Leading character is brightest (most recently drawn) ✓
5. Varying speeds for organic look ✓
6. Scanline overlay for Matrix feel ✓
7. Responsive to resize ✓
Looks good. Let me also double-check the character generation. The Unicode ranges I'm using:
- U+30A1 to U+30F6: This covers standard katakana characters (ア through ヨ, plus some variants). Actually, U+30A1 is ア and U+30F6 is ヨ. But this misses some characters like カ (U+30AB), サ (U+30B3), etc. Wait, U+30A1 to U+30F6 should cover all of them since they're in order.
Let me verify:
- U+30A1: ア
- U+30A2: ア (wait, this is also ア?)
Actually, the katakana block is:
- U+30A0: ・ (middle dot)
- U+30A1: ア
- U+30A2: ア (this seems wrong)
Hmm, let me check. The Unicode katakana block (U+30A0–U+30FF) includes:
- U+30A0: ゠ (katakana hyphen)
- U+30A1: ア
- U+30A2: ア (duplicate?)
No, I think I'm confusing things. Let me just trust that the range U+30A1 to U+30F6 covers all standard katakana, plus some extras.
Actually, the standard katakana chart goes:
ア (30A1), イ (30A2), ウ (30A3), エ (30A4), オ (30A5)
カ (30AB), キ (30AC), ク (30AD), ケ (30AE), コ (30AF)
サ (30B3), シ (30B4), ス (30B5), セ (30B6), ソ (30B7)
タ (30B9), チ (30BA), ツ (30BB), テ (30BC), ト (30BD)
ナ (30CB), ニ (30CC), ヌ (30CD), ネ (30CE), ノ (30CF)
ハ (30CF)... wait, ハ is 30CF? No, that's ノ.
Let me just trust the Unicode standard and use the range. The important thing is that we generate a diverse set of katakana characters.
Actually, I just realized: U+30A1 to U+30F6 includes all the standard katakana (ア through ヨ) plus the small variants and diacritics. The characters in this range are:
- U+30A1-U+30A5: ア段
- U+30A6-U+30A9: ワ段 (including ワ, ヰ, ヱ, ヲ)
- U+30AA-U+30AF: カ段
- U+30B0-U+30B3: ガ段 and サ段
- etc.
Wait, this doesn't seem right. Let me look at the actual Unicode code chart for katakana.
The Katakana block (U+30A0–U+30FF) layout:
- U+30A0: ゠ (KATAKANA-HIRAGANA DOUBLE HYPHEN)
- U+30A1: ア
- U+30A2: ア (this is actually ア too? No...)
OK, I'm going in circles. Let me just use a well-known range. The standard katakana characters are in the range U+30A1 to U+30F6, with some gaps. Let me also include U+30F7 to U+30FA (extended katakana: ヷ, ヸ, ヹ, ヺ) and U+30FC (ー, long vowel mark).
Actually, let me just generate all code points from U+30A0 to U+30FF and filter out any that produce empty strings or non-printing characters. But I can't easily filter without rendering.
You know what, let me just use a hardcoded string of katakana. This is the safest approach:
```javascript
const KANA = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダデドバビブベボパピプペポッャュョーヵヶヴヵヶヷヸヹヺ';
```
This is a comprehensive list of katakana characters. Let me use this.
Wait, I have ヵ and ヶ repeated. Let me clean it up:
```javascript
const KANA = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダデドバビブベボパピプペポッャュョーヵヶ/tmp/ws/index.html (4627 bytes)Successfully wrote 4627 bytes to /tmp/ws/index.html
thinking
The file is written. Let me give a concise summary to the user.
`/tmp/ws/index.html` is ready. Open it in any browser — it's fully self-contained, no network. What it does: - **Full-screen black canvas** with subtle CRT scanlines and a soft green vignette. - **Columns drop at ~14 px spacing**, each column independent. A katakana pool (with occasional Latin alphanumerics for variety) supplies the glyphs. - **Per-frame `rgba(0,0,0,0.04)` fill overlay** creates the classic trailing-fade — old characters fade out behind the moving head. - **Leading glyph** is drawn fresh each frame in bright green (`rgba(0,255,65)`), giving that sharp head character; brightness per column is slightly randomised for a more organic look. - **Speed varies column-to-column**, so you get a non-uniform, "alive" rain feel. - Resizes cleanly and resets drops on window resize.
TAKE IT TO CODE
A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.
RUN
EVALUATION
Continuous Matrix digital rain with green trailing glyphs, animates smoothly but is not interactive to input.
Vision grade · anthropic/claude-sonnet-5
MEASUREMENTS
Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.