Jan 2023
Matrix
Here I explore creating a performant falling green code animation from the Matrix using canvas.
<html>
<head>
<meta charset="utf-8">
<link rel="preload" href="matrix.ttf" as="font" type="font/ttf">
<style>
@font-face { font-family: matrix; src: url(matrix.ttf); }
* { box-sizing: border-box; }
html,body { margin: 0; background: #000; overflow: hidden; height: 100%; }
#root { width: 100%; height: 100%; }
canvas { width: 100%; height: 100%; object-fit: cover;}
.fps { position: absolute; left: 30px; bottom: 30px; font-size: 14px; font-family: monospace; color: #ffffd1; z-index: 2; }
</style>
<script>
const createNode = (options) => {
const node = document.createElement(options.tag);
if(options.className) node.setAttribute('class',options.className);
if(options.innerHTML) node.innerHTML = options.innerHTML;
if(options.attributes) Object.keys(options.attributes).forEach(key => node.setAttribute(key,options.attributes[key]) );
if(options.style) Object.keys(options.style).forEach(key => node.style[key] = options.style[key]);
if(options.root) options.root.appendChild(node);
return node;
}
class Matrix {
constructor(){
window.addEventListener('resize',this.init.bind(this));
document.addEventListener('DOMContentLoaded',this.init.bind(this));
this.glyphs = ['モ','エ','ヤ','キ','オ','カ','7','ケ','サ','ス','z','1','5','2','ヨ','タ','ワ','4','ネ','ヌ','ナ','9','8','ヒ','0','ホ','ア','3','ウ',' ','セ','¦',':','"','꞊','ミ','ラ','リ','╌','ツ','テ','ニ','ハ','ソ','▪','—','<','>','0','|','+','*','コ','シ','マ','ム','メ'];
this.message = 'GITHUB.COM/IAMJOHNMILLS';
this.glyph_size = 12;
this.trail_length = 25;
this.alpha_step = (1 / this.trail_length) * 100;
this.els = [];
}
async init(){
if(this.interval) cancelAnimationFrame(this.interval);
this.width = window.innerWidth;
this.height = window.innerHeight;
this.root = document.getElementById('root');
this.root.innerHTML = ``;
this.max_columns = Math.ceil(this.width / this.glyph_size);
this.max_rows = Math.ceil(this.height / this.glyph_size);
this.rows_center = Math.floor(this.max_rows / 2);
this.columns_center = Math.floor(this.max_columns / 2);
this.overflow = this.max_rows + 40;
this.els.fps = createNode({ root: this.root, tag: 'div', className: `fps`, style: { display: `none` } });
this.els.canvas = createNode({ root: this.root, tag: 'canvas', attributes: { width: this.width, height: this.height } });
this.context = this.els.canvas.getContext('2d');
this.matrix = this.generate();
this.interval = window.requestAnimationFrame(this.render.bind(this));
}
async render(){
this.context.clearRect(0, 0, this.width, this.height);
for(let x = 0; x < this.matrix.length; x++){
const column = this.matrix[x];
let position = column.drop_position + (1 * column.drop_speed);
column.drop_position = position > this.overflow ? 0 : position;
let position_y = Math.floor(column.drop_position);
for(let y = 0; y < column.glyphs.length; y++){
const row = column.glyphs[y];
let render_x = x * this.glyph_size;
let render_y = y * this.glyph_size;
if(row.active){
row.active = !!row.letter;
this.context.font = 'bold 11px sans-serif';
this.context.fillStyle = '#ffffd1';
const letter_width = this.context.measureText(row.letter.letter).width;
render_x = ((this.glyph_size - letter_width) / 2) + render_x;
this.context.fillText(row.letter.letter, render_x, render_y);
} else if(y < position_y){
const position_spread = position_y - y;
if(position_spread <= this.trail_length && position_spread > 0){
let alpha = (100 - position_spread * this.alpha_step) / 100;
this.context.font = '11px matrix';
this.context.fillStyle = `rgba(180,255,160,${alpha})`;
this.context.fillText(row.glyph, render_x, render_y);
}
} else if(y === position_y){
row.active = !!row.letter;
this.context.font = '11px matrix';
this.context.fillStyle = '#ffffd1';
this.context.fillText(row.glyph, render_x, render_y);
}
if(x === this.matrix.length - 1 && y === column.glyphs.length - 1) {
this.interval = window.requestAnimationFrame(this.render.bind(this));
}
}
}
}
generate(){
const message_parts = this.message.split('');
const message_start = this.columns_center - (Math.floor(message_parts.length / 2));
const message_letters = message_parts.map( (message_part,i) => ({ position: message_start + i, letter: message_part }))
return Array.from(Array(this.max_columns)).map((column,x) => {
return {
drop_position: Math.floor(Math.random()*this.max_rows),
drop_speed: Math.random() * (0.5 - 0.1) + 0.1,
glyphs: Array.from(Array(this.max_rows)).map((row,y) => ({
active: false,
glyph: this.glyphs[Math.floor(Math.random()*this.glyphs.length)],
letter: message_letters.find(message_letter => message_letter.position === x && this.rows_center === y),
}) )
}
});
}
}
const matrix = new Matrix();
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
The first step is to find the font used, because without it, it doesn't look quite right. Each glyph is then defined in an array and a random one is chosen to be placed at a coordinate derived from the canvas width and height and glyph size.
Once the matrix is generated, the requestAnimationFrame is triggered and on each iteration, each column has a current position value that is updated. By applying an RGB color based on column drop speed and current position values, a simulated movement effect is created.
Project Assets
Asset | File | Description |
---|---|---|
matrix.ttf |
Download | Font used to make the glyphs look nice. |