oneshotlm
Compare all models on this prompt →

Matrix digital rain · seed-2.0-mini

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.

⛨ sandboxed Open in new tab ↗
transcript · 4 messages
user

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.

assistant
thinking
Got it, let's tackle this. First, I need to make a full-screen Matrix digital rain effect in a single index.html file, no external resources. Let's start by outlining the structure:

First, the HTML will have a canvas element that's full screen, with a style that makes the body have no margins, overflow hidden, background black, since the canvas will cover it. Wait, actually, the canvas should be positioned fixed or absolute to cover the whole viewport.

Then the JS part: need to set up the canvas, get the 2D context. Then create columns of katakana characters. Let's remember what katakana we can use—common ones are アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン, plus maybe some others.

First, let's list the katakana characters array. Then, each column has a position (x coordinate based on font size), a speed, a current position (y), a bunch of characters in the trail, and the head of the column (the bright one).

Wait, the effect: each column falls down, the leading character is brighter green, the trailing ones fade out (lower opacity) as they go behind. The background is black, so we don't need to clear the whole screen each frame—instead, draw a semi-transparent black rectangle over the canvas each frame to create the fade trail effect. Oh right, that's a common trick for the matrix rain, instead of redrawing every character every time with fading, just overlay a dark rect with low alpha each frame so older characters get darker over time.

Wait, let's break down the steps:

1. HTML structure: <!DOCTYPE html>, <html>, <body> with a <canvas id="matrixCanvas">, then a <style> tag that sets body to margin 0, padding 0, overflow hidden, canvas to position fixed, top 0, left 0, width 100vw, height 100vh, background black? Wait no, actually the canvas's background is black, but the semi-transparent overlay will handle the trails. Wait, no, first fill the canvas once with black, then each frame draw the semi-transparent rect to fade the previous frames.

Wait, let's set the canvas width and height to the window's innerWidth and innerHeight, so it's full screen. Also, handle window resizing, so we update the canvas size when the window changes.

2. Font settings: Use a monospace font, like 'Courier New', or 'MS Gothic' which is good for katakana, since it's a Japanese font. The font size should be, say, 16px? Wait, columns should be spaced by the font width, so each column's x is i * fontSize, where i is the column index. So first calculate how many columns there are: Math.floor(canvasWidth / fontSize).

3. Katakana characters array: Let's make a string of common katakana, then split into an array. Like const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン'; then split into [...katakana].

4. Columns array: Each column can be an object with x position, speed, y position (starts above the canvas so they fall in), and an array of characters for the trail, or just track the current characters. Wait, actually, for each column, we can have:
- x: the horizontal position (index * fontSize)
- y: the current vertical position of the leading character
- speed: random speed between, say, 5 and 15 pixels per frame?
- chars: an array of characters that are currently in the column (the trail)
Wait no, maybe each column has a length (how many characters are in the trail), and each frame we generate a new character at the top, shift the trail, and move down. Or alternatively, each column has a list of recent characters, with their opacity.

Wait another approach: For each frame:
- Draw a rectangle with fillStyle = 'rgba(0, 0, 0, 0.05)' (or 0.1) over the entire canvas. This will make the older pixels fade out over time.
- Then, for each column:
  - Pick a random katakana character for the current head.
  - Draw the head character in bright green (like #00ff00 or #0f0) at (x, y)
  - Then draw the trailing characters below it, each with lower opacity: the first trail is 0.8, then 0.6, etc., down to almost 0, spaced by fontSize (since each character is monospace, so vertical spacing is same as font size)
  - Then increment y by the column's speed. When y exceeds canvasHeight + some offset, reset y to -fontSize so it starts again above the top.

Wait also, some columns can be shorter or longer? Or all same length? Maybe randomize the trail length per column, so some columns are longer trails than others.

Wait also, the leading character should be the brightest, then each subsequent one down the trail is dimmer. So for the trail, each character's opacity decreases as it gets further from the head.

Let's also set the color: the bright green is #00ff00, the dimmer ones are varying shades of green, like #00aa00, #007700, etc., but using opacity is easier with the fill style.

Wait let's outline the code step by step.

First, the 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>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background: #000;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="matrixCanvas"></canvas>
    <script>
        // JS code here
    </script>
</body>
</html>

Now inside the script tag:

First, get the canvas and context:

const canvas = document.getElementById('matrixCanvas');
const ctx = canvas.getContext('2d');

Then set the canvas size to window size, and handle resize:

function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();

Then define the font size, katakana chars:

const fontSize = 16;
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
const chars = katakana.split('');
const columns = Math.floor(canvas.width / fontSize);

Then create an array to track each column's current y position, speed, and maybe last character time? Wait no, each column should have:

Wait, let's create an array of drops, where each drop is:
{
    x: index * fontSize, // horizontal position
    y: Math.random() * -canvas.height, // start above the top of the screen
    speed: Math.random() * 10 + 5, // random speed between 5 and 15
    trailLength: Math.floor(Math.random() * 10) + 5, // trail length between 5 and 15
    lastCharTime: 0 // when we last updated the character, to make it randomize occasionally?
}

Wait, yes, because if we just have each column drop a new character every frame, all the columns would have the same character at the same time, which looks weird. So instead, each column has its own interval for changing the character, or we pick a new random character for the head each frame, but wait no—each frame, the head is a new random character? Wait no, the head should change as it falls? Or no, the head is a single character that moves down, and the trail is the previous characters. Wait, actually, the classic matrix effect is that each column has a stream of characters, where the leading one is bright, and the ones behind are fading, and each position in the stream gets a new random character as it passes by.

Wait another way: For each column, we can have an array of characters that are currently active, with their y positions. But maybe simpler: each frame, for each column, we draw a series of characters starting at y, going down, each spaced by fontSize, with each character's opacity decreasing as we go down. The head is the first one (brightest), then each next one is dimmer. Then we increment y by speed, and when y > canvas.height + fontSize * trailLength, reset y to -fontSize * trailLength so the whole column comes back from the top.

Wait also, the font: we need to set the font correctly, so ctx.font = `${fontSize}px 'MS Gothic', 'Courier New', monospace;` because MS Gothic supports katakana well.

Then the fill style for the background fade: each frame, we draw a semi-transparent black rectangle over the canvas, so the older characters fade out. So ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; ctx.fillRect(0, 0, canvas.width, canvas.height);

Then, for each column i:

First, get the x position: x = i * fontSize. Then, the current y position of the column's head. Wait, let's have the drops array as an array of y positions for each column, initialized to random values above the canvas. Oh right, that's even simpler! Let's do that:

// Initialize drops: each drop is the current y position of the column's head
let drops = [];
for (let i = 0; i < columns; i++) {
    drops[i] = Math.random() * -canvas.height;
}

Then, in the animation loop:

function drawMatrix() {
    // Fade effect
    ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Set font and text align
    ctx.font = `${fontSize}px 'MS Gothic', 'Courier New', monospace`;
    ctx.textAlign = 'center';

    // Loop through each column
    for (let i = 0; i < columns; i++) {
        // Pick a random katakana character
        const char = chars[Math.floor(Math.random() * chars.length)];
        // X position is center of the column
        const x = i * fontSize + fontSize / 2;
        // Y position is drops[i]
        const y = drops[i];

        // Draw the leading character (bright green)
        ctx.fillStyle = '#00ff00';
        ctx.fillText(char, x, y);

        // Draw the trailing characters, dimmer, going down
        for (let j = 1; j < 15; j++) {
            // Lower opacity as j increases
            ctx.fillStyle = `rgba(0, 255, 0, ${1 / (j * 0.7)})`;
            // Y position is drops[i] + j * fontSize
            const trailY = y + j * fontSize;
            // Only draw if trailY is within the canvas
            if (trailY > 0 && trailY < canvas.height) {
                const trailChar = chars[Math.floor(Math.random() * chars.length)];
                ctx.fillText(trailChar, x, trailY);
            }
        }

        // Move the drop down
        drops[i] += fontSize * (Math.random() * 0.5 + 0.5); // random speed between 0.5x and 1x fontSize per frame? Wait, or use a fixed speed per column, but randomizing the speed each frame adds variation.

        // Reset the drop when it goes below the canvas
        if (drops[i] > canvas.height + fontSize * 15) {
            drops[i] = -fontSize * 15;
        }
    }
}

Then, start the animation loop with requestAnimationFrame:

function animate() {
    drawMatrix();
    requestAnimationFrame(animate);
}
animate();

Wait, but wait: in the original code, the trailing characters are different each frame? That's okay, that's how the classic effect works—each position in the trail has a random character. Also, the leading character changes every frame, which is fine.

Wait but let's test: when the drop moves down, the leading character is at drops[i], then the trail is below it. Each frame, we redraw all the characters, with the fade rect in between, so the older characters get dimmer each frame. That should work.

Wait but let's adjust the speed: if fontSize is 16, then each frame the drop moves by, say, 2 to 4 pixels? Wait no, if we do drops[i] += fontSize * 0.5, that's 8 pixels per frame, which is a good speed. Wait in the code above, I had drops[i] += fontSize * (Math.random() * 0.5 +0.5), so between 8 and 16 pixels per frame, that's okay.

Also, the trail length: in the loop j from 1 to 15, that's 15 trailing characters, which is a good length. The opacity for j=1 is 1/(1*0.7) ~ 1.4, wait no, wait 1/(j*0.7) for j=1 is ~1.4, which is more than 1, so that's full opacity. For j=2, it's ~0.71, j=3 ~0.47, j=15 ~0.09, which is perfect, that fades out nicely.

Wait but let's fix the fillStyle for the trail: maybe use rgba(0, 200, 0, ...) instead of full 255, but no, the opacity will handle the dimming. Wait actually, the leading character is #00ff00 (full opacity), then the trail is same color but lower opacity, which is correct.

Also, the text align: center, so the x position is i*fontSize + fontSize/2, which centers the character in each column, that's good.

Wait what about the font? If MS Gothic isn't available, Courier New might not have katakana, but most modern systems have some Japanese font. Alternatively, we can use a generic monospace font, but katakana might not render correctly. Wait but the user asked for katakana glyphs, so we need a font that supports them. Maybe add 'Meiryo' as another option, or just use 'monospace' but that's hit or miss. But for the sake of the code, let's include MS Gothic, Courier New, monospace.

Also, let's add a check for window resize, so when the window resizes, we update the columns array and the drops array. Wait right now, the resize function just sets canvas.width and height, but the drops array and columns variable aren't updated. Oh right, that's a problem. So when the window resizes, we need to recalculate the columns, and reset the drops array.

So let's modify the resize event handler:

window.addEventListener('resize', () => {
    resizeCanvas();
    // Recalculate columns and reset drops
    columns = Math.floor(canvas.width / fontSize);
    drops = [];
    for (let i = 0; i < columns; i++) {
        drops[i] = Math.random() * -canvas.height;
    }
});

Wait but we need to declare columns with let instead of const, right? Because we're reassigning it on resize. Oh right! So change const columns to let columns = Math.floor(canvas.width / fontSize); initially.

Also, the initial drops array: let's declare let drops = []; then initialize it after resizeCanvas is called the first time? Wait no, in the code above, we call resizeCanvas() before initializing drops, so let's see:

Wait the order:

First, resizeCanvas() sets canvas.width and height to window.innerWidth/innerHeight. Then columns = Math.floor(canvas.width / fontSize); then drops is initialized as for loop over columns. That's correct.

Wait let's adjust the code order:

Inside the script:

const canvas = document.getElementById('matrixCanvas');
const ctx = canvas.getContext('2d');
const fontSize = 16;
const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
const chars = katakana.split('');
let columns;
let drops = [];

function resizeCanvas() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}

window.addEventListener('resize', () => {
    resizeCanvas();
    columns = Math.floor(canvas.width / fontSize);
    drops = [];
    for (let i = 0; i < columns; i++) {
        drops[i] = Math.random() * -canvas.height;
    }
});

// Initial setup
resizeCanvas();
columns = Math.floor(canvas.width / fontSize);
for (let i = 0; i < columns; i++) {
    drops[i] = Math.random() * -canvas.height;
}

Then the drawMatrix and animate functions as before.

Wait another thing: some columns might be faster than others, which adds variation, that's good. Also, the reset condition: when drops[i] > canvas.height + fontSize *15, reset to -fontSize*15, so the entire trail of 15 characters comes back into view from the top. That's correct.

Wait let's test the code: when you run this, the canvas is full screen, black background, green katakana falling down, with trailing fade. The leading character is bright green, the trails get dimmer as they go down. That's exactly what the user asked for.

Wait also, let's remove the text align? No, ctx.textAlign = 'center' is correct because we're using x = i*fontSize + fontSize/2, so the text is centered at that x position. If we don't set text align, it's left by default, so the character would be drawn starting at x, which would make it not centered. So yes, text align center is correct.

Wait another adjustment: the trail's y position is y + j*fontSize, so for j=1, it's right below the leading character, j=2 is below that, etc. That's correct.

Also, in the trail loop, we check if trailY is between 0 and canvas.height, so we don't draw characters that are off-screen, which saves some performance. Good.

Wait what about the initial position of the drops: drops[i] = Math.random() * -canvas.height, so some columns start at different positions above the canvas, which makes the rain start at different times, that's nice.

Let's also add a note that this is self-contained, no external resources, all in index.html.

Wait let's put it all together now, making sure all the code is correct, no missing parts. Let's check for any errors:

Wait in the drawMatrix function, when we loop j from 1 to 15, that's 15 trailing characters, which is correct. The opacity for j is 1/(j*0.7), which for j=1 is ~1.4 (so full opacity, same as leading), j=2 ~0.7, j=3 ~0.47, j=10 ~0.14, j=15 ~0.09, which fades out nicely.

Wait but the leading character is drawn with #00ff00, full opacity, then the first trail character (j=1) is also rgba(0,255,0, 1/0.7) which is ~1.4, which is same as full opacity, so that's a bit bright, but maybe we can adjust the opacity formula. Let's change it to ctx.fillStyle = `rgba(0, 255, 0, ${0.8 / (j * 0.5)})`; no, wait better: let's make the leading character opacity 1, then each trail is 1/(j * 1.5), so for j=1, 0.66, j=2 0.33, j=3 0.22, etc. Wait no, the classic effect is that the leading character is the brightest, then each subsequent one is dimmer, so the first trail right after the head is almost as bright, then fades. Wait actually, let's use a linear fade: for the nth character behind the head, opacity is (maxOpacity - (n / trailLength)) * baseOpacity. Wait but maybe simpler: use 1 / (j * 0.8) but cap it at 1. So for j=1, 1/0.8=1.25, so we set it to 1, then j=2 is 0.625, j=3 0.416, etc. Oh right, because if we use 1/(j*0.8) for j=1, it's more than 1, which is same as 1, so let's adjust that:

ctx.fillStyle = `rgba(0, 255, 0, Math.min(1, 1 / (j * 0.8)))`;

That way, the first trail character is full opacity, same as the leading? No, wait the leading is already full, so the first trail is just below, same brightness, then each next one fades. That's better, because the trail starts at full brightness and then fades, which looks more natural.

Wait let's modify that line in the trail loop:

ctx.fillStyle = `rgba(0, 255, 0, Math.min(1, 1 / (j * 0.8)))`;

Yes, that's better.

Also, let's change the speed: instead of drops[i] += fontSize * (Math.random() * 0.5 + 0.5), let's use a fixed speed per column, but randomize the speed when initializing. Wait, let's add a speed array, so each column has its own speed. Oh right, that's better, so each column falls at a constant random speed, instead of varying speed each frame. Let's adjust that:

First, let's have a speeds array:

let speeds = [];

Then, during initialization:

for (let i = 0; i < columns; i++) {
    drops[i] = Math.random() * -canvas.height;
    speeds[i] = Math.random() * 8 + 3; // between 3 and 11 pixels per frame
}

Then, in the draw loop:

drops[i] += speeds[i];

That way, each column has its own constant speed, which looks more natural.

Ah, that's a good improvement. Because if the speed changes each frame, the columns can speed up and slow down, which is okay, but constant per column is more classic.

So let's update the code:

In the initial setup:

window.addEventListener('resize', () => {
    resizeCanvas();
    columns = Math.floor(canvas.width / fontSize);
    drops = [];
    speeds = [];
    for (let i = 0; i < columns; i++) {
        drops[i] = Math.random() * -canvas.height;
        speeds[i] = Math.random() * 8 + 3;
    }
});

// Initial setup
resizeCanvas();
columns = Math.floor(canvas.width / fontSize);
speeds = [];
for (let i = 0; i < columns; i++) {
    drops[i] = Math.random() * -canvas.height;
    speeds[i] = Math.random() * 8 + 3;
}

Then in drawMatrix:

// Move the drop down
drops[i] += speeds[i];

That's better.

Also, let's adjust the reset condition: when drops[i] > canvas.height + fontSize * 15, reset to -fontSize * 15 - Math.random() * fontSize *15, so that some columns reset at different positions, adding variation. Wait no, the current reset is:

if (drops[i] > canvas.height + fontSize * 15) {
    drops[i] = -fontSize * 15;
}

Which resets all columns to the same position, but if we add a random offset, like:

drops[i] = -fontSize * 15 - Math.random() * canvas.height * 0.5;

That way, some columns reset further up than others, which is nicer.

Yes, let's change that:

if (drops[i] > canvas.height + fontSize * 15) {
    drops[i] = -fontSize * 15 - Math.random() * canvas.height * 0.5;
}

That way, when a column goes off the bottom, it resets to a random position above the top, so the rain doesn't all sync up.

Another thing: the katakana string—maybe add some more characters, like some punctuation or other Japanese characters? But the ones we have are enough, but let's add a few more, like ガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポ, which are the voiced and half-voiced katakana. That will make the characters more varied. So expand the katakana string:

const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポ';

That's more characters, so more variety.

Okay, putting all this together, the final index.html file should look like this:

Wait let's write the full code:

First, the entire HTML file:

<!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>
        body {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background: #000000;
        }
        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <canvas id="matrixCanvas"></canvas>
    <script>
        const canvas = document.getElementById('matrixCanvas');
        const ctx = canvas.getContext('2d');
        const fontSize = 16;
        // Extended katakana characters including voiced/half-voiced
        const katakana = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンガギグゲゴザジズゼゾダヂヅデドバビブベボパピプペポ';
        const chars = katakana.split('');
        let columns;
        let drops = [];
        let speeds = [];

        function resizeCanvas() {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
        }

        // Handle window resize
        window.addEventListener('resize', () => {
            resizeCanvas();
            columns = Math.floor(canvas.width / fontSize);
            drops = [];
            speeds = [];
            for (let i = 0; i < columns; i++) {
                drops[i] = Math.random() * -canvas.height;
                speeds[i] = Math.random() * 8 + 3;
            }
        });

        // Initial canvas setup
        resizeCanvas();
        columns = Math.floor(canvas.width / fontSize);
        for (let i = 0; i < columns; i++) {
            drops[i] = Math.random() * -canvas.height;
            speeds[i] = Math.random() * 8 + 3;
        }

        function drawMatrix() {
            // Create fade trail effect with semi-transparent black
            ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Set font and text alignment
            ctx.font = `${fontSize}px 'MS Gothic', 'Courier New', monospace`;
            ctx.textAlign = 'center';

            // Draw each column
            for (let i = 0; i < columns; i++) {
                const x = i * fontSize + fontSize / 2;
                const y = drops[i];

                // Draw leading bright green character
                ctx.fillStyle = '#00ff00';
                ctx.fillText(chars[Math.floor(Math.random() * chars.length)], x, y);

                // Draw trailing faded characters
                for (let j = 1; j < 15; j++) {
                    // Calculate opacity with fade out over trail length
                    const opacity = Math.min(1, 1 / (j * 0.8));
                    ctx.fillStyle = `rgba(0, 255, 0, ${opacity})`;
                    const trailY = y + j * fontSize;
                    
                    // Only draw if within canvas bounds
                    if (trailY > 0 && trailY < canvas.height) {
                        ctx.fillText(chars[Math.floor(Math.random() * chars.length)], x, trailY);
                    }
                }

                // Move column down
                drops[i] += speeds[i];

                // Reset column when it goes off screen
                if (drops[i] > canvas.height + fontSize * 15) {
                    drops[i] = -fontSize * 15 - Math.random() * canvas.height * 0.5;
                }
            }
        }

        // Start animation loop
        function animate() {
            drawMatrix();
            requestAnimationFrame(animate);
        }
        animate();
    </script>
</body>
</html>

Wait that's the full code. Let's check: all CSS is in a style tag, JS is in a script tag, no external resources, full screen, black background, green katakana, trailing fade, bright leading character. Perfect.

Now, I need to write this file to /tmp/ws/index.html. So use the write function with path "/tmp/ws/index.html" and content as the above code.
write /tmp/ws/index.html (3571 bytes)
toolResult

Successfully wrote 3571 bytes to /tmp/ws/index.html

assistant
thinking
Great, the file was written successfully. Now I should confirm that the file is present and maybe check its contents? Wait no, the user asked to create the single-page app, and we wrote it to index.html in the current directory. Let's just let the user know that the file is ready, and that they can open it in a browser to see the Matrix digital rain effect.

Created a self-contained Matrix digital rain effect in `/tmp/ws/index.html`: - Full-screen black canvas with falling green katakana glyphs - Bright leading character per column with a smoothly fading trailing trail - Responsive to window resizing - No external resources or network requests required Open the file in any modern web browser to view the effect.

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

modelseed-2.0-mini
vendorByteDance · Seed
harnesspi-agent
generated2026-08-01

EVALUATION

4/5 working

Classic Matrix digital rain with green glyph columns fading trails and bright leading characters, animating continuously though not interactive.

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.

idle motion39.4%
on click46.4%
on drag49.7%
on wheel50.9%
on enter + space49.3%
on arrow keys51.7%
on w/a/s/d52.0%
frame spread24 / 255
console errors0
js errors none

METRICS

runtime56.4s
tokens in12k
tokens out8.5k
cached0
cost / run$0.0046