Created
February 24, 2023 15:09
-
-
Save MrHus/6d17394691d8e0b1ce546cf43d8de10e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useRef } from 'react'; | |
// CONFIG | |
const WIDTH = 660; | |
const HEIGHT = 440; | |
const COLUMN_WIDTH = 20; | |
const COLUMNS = WIDTH / COLUMN_WIDTH; | |
const ROW_HEIGHT = 26; | |
const ROWS = HEIGHT / ROW_HEIGHT; | |
const RAINDROP_SPAWN_RATE = 0.8; | |
const MATRIX_CHARACTERS = [ | |
'ハ', | |
'ミ', | |
'ヒ', | |
'ー', | |
'ウ', | |
'シ', | |
'ナ', | |
'モ', | |
'ニ', | |
'サ', | |
'ワ', | |
'ツ', | |
'オ', | |
'リ', | |
'ア', | |
'ホ', | |
'テ', | |
'マ', | |
'ケ', | |
'メ', | |
'エ', | |
'カ', | |
'キ', | |
'ム', | |
'ユ', | |
'ラ', | |
'セ', | |
'ネ', | |
'ス', | |
'タ', | |
'ヌ', | |
'ヘ', | |
'ヲ', | |
'イ', | |
'ク', | |
'コ', | |
'ソ', | |
'チ', | |
'ト', | |
'ノ', | |
'フ', | |
'ヤ', | |
'ヨ', | |
'ル', | |
'レ', | |
'ロ', | |
'ン', | |
'0', | |
'1', | |
'2', | |
'3', | |
'4', | |
'5', | |
'7', | |
'8', | |
'9', | |
'Z', | |
'*', | |
'+', | |
'-', | |
'<', | |
'>', | |
'+', | |
'-', | |
'<', | |
'>', | |
'¦', | |
'|', | |
'ç', | |
'リ', | |
'ク', | |
] as const; | |
const GREENS = ['#15803d', '#16a34a', '#22c55e', '#4ade80'] as const; | |
const WHITE = '#f0fdf4'; | |
const FRAME_RATE = 1000 / 20; | |
// TYPES | |
type Greens = typeof GREENS[number]; | |
type Color = typeof WHITE | Greens; | |
type Cell = { | |
/** | |
* The position / index of the cell within the column. Used | |
* to determine what the next cell in the column is. | |
*/ | |
position: number; | |
/** | |
* The amount of ticks the cell will be active / part of a raindrop. | |
* | |
* If the `activeFor` is 5 this means that for five ticks the | |
* cell will be shown on the matrix. It will also get a new color | |
* and char when `activeFor` is greater than zero. | |
* | |
* Each tick decreases `activeFor` by one. | |
*/ | |
activeFor: number; | |
/** | |
* The character / symbol of the cell. | |
* | |
* The `char` will change when `retainChar` is `0`. | |
*/ | |
char: string; | |
/** | |
* The number of ticks to retain the 'char' for. | |
*/ | |
retainChar: number; | |
/** | |
* The color the cell has, will be WHITE when head, and a | |
* GREENS when in the trail. | |
* | |
* The `color` will change when `retainChar` is `0`. | |
*/ | |
color: Color; | |
/** | |
* The number of ticks to retain the 'color' for. | |
*/ | |
retainColor: number; | |
}; | |
type Column = { | |
/** | |
* The cells that make up this column. | |
*/ | |
cells: Cell[]; | |
/** | |
* The cell which is currently the head of the raindrop. | |
* When it is `undefined` it means that the head of the raindrop | |
* is not on the screen, but it could still have a trail. | |
*/ | |
head?: Cell; | |
/** | |
* The length of the current raindrop's trail. | |
* | |
* Each raindrop is assigned a new random trail. | |
*/ | |
trail: number; | |
/** | |
* The number of ticks left in the current raindrops animation. | |
*/ | |
ticksLeft: number; | |
/** | |
* The speed factor of the raindrop. The lower the number the higher | |
* the speed. | |
* | |
* Each raindrop is assigned a new random speed. | |
*/ | |
speed: number; | |
}; | |
type Matrix = Column[]; | |
// The implementation | |
export function MatrixRainV12() { | |
const canvasRef = useRef<HTMLCanvasElement>(null); | |
useEffect(() => { | |
if (canvasRef.current) { | |
const canvas = canvasRef.current; | |
const ctx = canvas.getContext('2d'); | |
ctx.font = '32px mono'; | |
const matrix: Matrix = createMatrix(); | |
const intervalId = window.setInterval(() => { | |
tick(matrix); | |
render(matrix, ctx); | |
}, FRAME_RATE); | |
return () => { | |
window.clearInterval(intervalId); | |
}; | |
} | |
}, []); | |
useEffect(() => { | |
function resizeCanvas() { | |
if (canvasRef.current) { | |
const width = Math.min(WIDTH, document.body.clientWidth - 16); | |
canvasRef.current.style.width = `${width}px`; | |
} | |
} | |
window.addEventListener('resize', resizeCanvas); | |
resizeCanvas(); | |
return () => { | |
window.removeEventListener('resize', resizeCanvas); | |
}; | |
}, []); | |
return ( | |
<div className="flex justify-center"> | |
<canvas ref={canvasRef} width={WIDTH} height={HEIGHT} className="mb-4"> | |
The rain effect of the "Matrix" film | |
</canvas> | |
</div> | |
); | |
} | |
function render(matrix: Matrix, ctx: CanvasRenderingContext2D): void { | |
ctx.fillStyle = 'rgb(0,16,0)'; | |
ctx.fillRect(0, 0, WIDTH, HEIGHT); | |
let x = 0; | |
for (const column of matrix) { | |
let y = ROW_HEIGHT; | |
for (const cell of column.cells) { | |
ctx.fillStyle = cell.color; | |
ctx.fillText(cell.char, x, y); | |
y += ROW_HEIGHT; | |
} | |
x += COLUMN_WIDTH; | |
} | |
} | |
// Keep track of the number of ticks made, this is used to determine | |
// the speed of a column. | |
let tickNo = 0; | |
function tick(matrix: Matrix): void { | |
for (const column of matrix) { | |
// Move to the next column if the current column should not tick. | |
// This will give raindrops different speeds. | |
if (tickNo % column.speed !== 0) { | |
continue; | |
} | |
// Spawn a raindrop every once in a while, when there is no | |
// trail. As the animation should only repeat runs after the | |
// complete raindrop is no longer on screen. | |
const animationComplete = column.ticksLeft <= 0; | |
if (animationComplete && Math.random() > RAINDROP_SPAWN_RATE) { | |
// Some drops are really quite long! | |
column.trail = randomNumberBetween(3, ROWS * 2); | |
// The animation is done once the HEAD has moved through all | |
// ROWS, and when the trail has moved past all ROWS. | |
column.ticksLeft = ROWS + column.trail; | |
// Manually vetted these speeds, 1 feels nice and fast, | |
// and 6 can just barely be followed along. | |
column.speed = randomNumberBetween(1, 6); | |
column.head = column.cells[0]; | |
column.head.char = randomChar(); | |
// By setting `activeFor` to the column.trail, the first cell | |
// will be visible for that many ticks. | |
column.head.activeFor = column.trail; | |
} else { | |
if (column.head) { | |
const nextCell = column.cells[column.head.position + 1]; | |
// If there is a next cell we are not at the end of the screen. | |
if (nextCell) { | |
column.head = nextCell; | |
nextCell.activeFor = column.trail; | |
} else { | |
column.head.char = ''; | |
column.head = undefined; | |
} | |
} | |
column.ticksLeft -= 1; | |
} | |
for (const cell of column.cells) { | |
if (cell.activeFor > 0) { | |
if (column.head === cell) { | |
// Make head white and update it the next tick | |
cell.color = WHITE; | |
cell.retainColor = 0; | |
// Always give the head a random char | |
cell.char = randomChar(); | |
cell.retainChar = randomNumberBetween(1, 10); | |
} else { | |
if (cell.retainColor <= 0) { | |
cell.color = randomGreen(); | |
cell.retainColor = randomNumberBetween(1, 10); | |
} else { | |
cell.retainColor -= 1; | |
} | |
if (cell.retainChar <= 0) { | |
cell.char = randomChar(); | |
cell.retainChar = randomNumberBetween(1, 10); | |
} else { | |
cell.retainChar -= 1; | |
} | |
} | |
cell.activeFor -= 1; | |
} else { | |
cell.char = ''; | |
} | |
} | |
} | |
} | |
function createMatrix(): Matrix { | |
const columns: Column[] = []; | |
for (let i = 0; i < COLUMNS; i++) { | |
const cells: Cell[] = []; | |
for (let j = 0; j < ROWS; j++) { | |
const cell: Cell = { | |
position: j, | |
activeFor: 0, | |
char: '', | |
retainChar: 0, | |
color: WHITE, | |
retainColor: 0, | |
}; | |
cells.push(cell); | |
} | |
columns.push({ | |
cells, | |
head: undefined, | |
trail: 0, | |
ticksLeft: 0, | |
speed: 2, | |
}); | |
} | |
return columns; | |
} | |
// Utils | |
function randomChar(): string { | |
return randomFromArray(MATRIX_CHARACTERS); | |
} | |
function randomGreen(): Greens { | |
return randomFromArray(GREENS); | |
} | |
function randomFromArray<T>(array: readonly T[]): T { | |
const random = Math.floor(Math.random() * array.length); | |
return array[random]; | |
} | |
function randomNumberBetween(min: number, max: number): number { | |
return Math.ceil(Math.random() * (max - min) + min); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment