Skip to content

Instantly share code, notes, and snippets.

@fre-sch
Last active August 6, 2024 18:17
Show Gist options
  • Save fre-sch/7223701 to your computer and use it in GitHub Desktop.
Save fre-sch/7223701 to your computer and use it in GitHub Desktop.
Matrix rain effect
<!DOCTYPE html>
<html>
<style>
html, body {
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
background: black;
}
body {
display: flex;
justify-content: center;
}
</style>
<body>
<canvas id="display"></canvas>
<script>
function measure_text(context, font, text) {
context.font = font
let is_mono = font.indexOf("monospace") >= 0
let hpad = is_mono ? 1.1 : 0.85
let wpad = is_mono ? 1.18 : 0.6
let tm = context.measureText(text)
let h = Math.ceil(tm.fontBoundingBoxAscent * hpad)
let w = Math.ceil((tm.actualBoundingBoxRight + tm.actualBoundingBoxLeft) * wpad)
return {w, h}
}
function new_chars_canvas(font, tm, chars, scale) {
let tw = Math.ceil(tm.w * scale)
let th = Math.ceil(tm.h * scale)
let canvas = new OffscreenCanvas(tw * chars.length, th)
let context = canvas.getContext("2d")
context.globalAlpha = 0.0
context.fillStyle = "#000"
context.fillRect(0, 0, canvas.width, canvas.height)
context.globalAlpha = 1.0
context.scale(-scale, scale)
context.font = font
context.fillStyle = "#FFF"
for (let i = 0; i < chars.length; i++) {
context.fillText(chars[i], tm.w * -i, tm.h)
}
// scanlines
// for (let y = 0; y < canvas.height; y += 2) {
// context.fillStyle = "rgba(0,0,0,0.5)"
// context.fillRect(0, y, -canvas.width, 1)
// }
return {canvas, context}
}
var
font = "16px serif"
,char_scale = 1.5
,prev_glyph_style = {operation: "multiply", color: "hsl(150, 75%, 45%)"}
,new_glyph_style = {operation: "soft-light", color: "hsl(160, 100%, 65%)"}
,glow_enabled = true
,rf = Math.random
,ri = function (min, max) {return Math.floor(rf() * (max - min + 1) + min)}
,display_ctx = display.getContext('2d')
,width = display.width = Math.ceil(window.innerWidth / 2)
,height = display.height = Math.ceil(window.innerHeight / 2)
,buf = new OffscreenCanvas(width, height)
,ctx = buf.getContext("2d")
,tm = measure_text(ctx, font, "M")
,num_rows = Math.ceil(height / tm.h)
,num_columns = Math.ceil(width / tm.w)
,row_mod = (i) => (i % num_rows)
,chars = Array.from(' 日ハヒシツウーナミモニサワオリホマエキムテケメカユラセネスタヌ0123456789Z*+:=.<>"|¦_')
// ,chars = Array.from(' abcdefABCDEF0123456789\\;:[]_="()-`\'{}')
,chars_offscreen = new_chars_canvas(font, tm, chars, char_scale)
,pushers = ri(num_columns/4, num_columns/2)
;
ctx.fillStyle='#000'
ctx.fillRect(0,0,width,height)
// ctx.drawImage(charsOffscreen.canvas, 0, 0)
function draw_glyph(glyph_id, fill_style, dx, dy) {
ctx.save()
ctx.globalCompositeOperation = "source-over"
let sw = tm.w * char_scale
let sh = tm.h * char_scale
ctx.fillStyle = "#000"
ctx.fillRect(dx, dy, tm.w, tm.h)
// ctx.save()
// ctx.filter = "blur(1px)"
// ctx.drawImage(
// chars_offscreen.canvas,
// glyph_id * sw, 0, sw, sh,
// dx, dy, tm.w, tm.h)
// ctx.restore()
ctx.drawImage(
chars_offscreen.canvas,
glyph_id * sw, 0, sw, sh,
dx, dy, tm.w, tm.h)
if (fill_style !== null && fill_style !== undefined) {
ctx.globalCompositeOperation = fill_style.operation
ctx.fillStyle = fill_style.color
ctx.fillRect(dx, dy, tm.w, tm.h)
}
ctx.restore()
}
function *writer(column, start_row, start_length, darken_style, fill_style) {
let row = start_row
let length = start_length
let is_idler = rf() < 0.05
let x = column * tm.w
while (length > 0) {
let y = (row % num_rows) * tm.h
draw_glyph(ri(0, chars.length-1), fill_style, x, y)
if (is_idler && (rf() < 0.75)) {
// skip
}
else {
// darken previous glyphs
if (darken_style !== null && darken_style !== undefined) {
ctx.save()
ctx.globalCompositeOperation = darken_style.operation
ctx.fillStyle = darken_style.color
ctx.fillRect(x, y - tm.h, tm.w, tm.h)
ctx.restore()
}
++row
--length
}
yield
}
row = start_row
length = start_length
while (length > 0) {
let y = (row % num_rows) * tm.h
let action = rf()
// mostly clear trail, but sometimes spin the glyph
if (action < 0.85) {
ctx.fillStyle = "#000"
ctx.fillRect(x, y, tm.w, tm.h)
++row
--length
}
else if (action > 0.7 && action < 0.85) {
draw_glyph(ri(0, chars.length-1), fill_style, x, row * tm.h)
}
yield
}
return
}
let hlwriter = (column) => writer(
column, ri(0, num_rows), ri(num_rows*0.7, num_rows),
prev_glyph_style, new_glyph_style
)
hl_writers = new Array(num_columns).fill(0).map((_, i) => hlwriter(i))
function fullscreen_fade() {
ctx.save()
ctx.globalCompositeOperation = "multiply"
ctx.fillStyle = `hsl(0, 0%, 95%)`
ctx.fillRect(0,0,width,height)
ctx.restore()
}
let blur_canvas = new OffscreenCanvas(parseInt(display.width/2), parseInt(display.height/2))
let blur_ctx = blur_canvas.getContext("2d")
function swap_canvas() {
if (glow_enabled) {
blur_ctx.fillStyle = "#000"
blur_ctx.fillRect(0, 0, display.width, display.height)
blur_ctx.save()
blur_ctx.filter = "blur(2px) brightness(150%)"
blur_ctx.drawImage(buf, 0, 0, blur_canvas.width, blur_canvas.height)
blur_ctx.restore()
display_ctx.fillStyle = "#000"
display_ctx.fillRect(0, 0, display.width, display.height)
display_ctx.drawImage(blur_canvas, 0, 0, display.width, display.height)
display_ctx.save()
display_ctx.globalCompositeOperation = "screen"
display_ctx.drawImage(buf, 0, 0)
display_ctx.restore()
}
else {
display_ctx.drawImage(buf, 0, 0)
}
}
function draw() {
// Matrix 1 had no fadeout of glyphs
// fullscreen_fade()
for (let i=0; i < hl_writers.length; i++) {
let {done} = hl_writers[i].next()
if (done) hl_writers[i] = hlwriter(i)
}
swap_canvas()
// push columns down randomly
// for (var i=0; i<pushers; i++) {
// let xpos = ri(0, num_columns) * tm.w
// ,ypos = ri(0, num_rows * 0.9) * tm.h
// ,len=ri(3, num_rows * 0.4) * tm.h
// ctx.drawImage(q,
// xpos, ypos, tm.w, len,
// xpos, ypos + tm.h, tm.w, len
// )
// ctx.fillStyle="rgba(0, 0, 0, 0.5)"
// ctx.fillRect(xpos, ypos, tm.w, tm.h)
// }
}
if (true) {
setInterval(draw, 1000/24)
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<style>
html, body {
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
background: black;
}
body {
display: flex;
}
</style>
<body>
<canvas id="q"></canvas>
<script>
function measureText(ctx, font, text) {
ctx.font = font
let tm = ctx.measureText(text)
let h = Math.ceil(tm.fontBoundingBoxAscent)
let w = Math.ceil(tm.actualBoundingBoxRight + tm.actualBoundingBoxLeft)
return {w, h}
}
function newCharsCanvas(font, tm, chars, scale) {
let tw = Math.ceil(tm.w * scale)
let th = Math.ceil(tm.h * scale)
let canvas = new OffscreenCanvas(tw * chars.length, th)
let context = canvas.getContext("2d")
context.fillStyle = "#000"
context.fillRect(0, 0, canvas.width, canvas.height)
context.font = font
context.fillStyle = "#FFF"
context.scale(-scale, scale)
for (let i = 0; i < chars.length; i++) {
context.fillText(chars[i], tm.w * -i, tm.h)
}
return {canvas, context}
}
var s = window.screen
,font = "14px serif"
,ctx = q.getContext('2d')
,width = q.width = 800
,height = q.height = 450
,tm = measureText(ctx, font, "M")
,column_len = Math.ceil(height / tm.h)
,num_columns = Math.ceil(width / tm.w)
,rf = Math.random
,charScale = 0.9
,chars = Array.from('日ハヒシツウーナミモニサワオリホマエキムテケメカユラセネスタヌ0123456789Z*+:=.<>"|¦_ ')
,charsOffscreen = newCharsCanvas(font, tm, chars, charScale)
,ri = function (min, max) {return Math.floor(rf() * (max - min + 1) + min)}
,primaryColor = 180
,t = Date.now()
,dt = 0
,matrix = new Uint32Array(num_columns * column_len)
,GLYPH_PROP = [0x00_00_00_FF, 0]
,AGE_PROP = [0x00_00_FF_00, 8]
,TRAILLEN_PROP = [0x00_FF_00_00, 16]
,DELAY_PROP = [0xFF_00_00_00, 24]
;
function decay (matrix, i, prop) {
const [mask, shift] = prop
let value = (matrix[i] & mask) >> shift
value = Math.max(0, value - 1)
setv(matrix, i, value, prop)
return value
}
function getv(cell, prop) {
const [mask, shift] = prop
return (cell & mask) >> shift
}
function setv(matrix, i, value, prop) {
const [mask, shift] = prop
matrix[i] = (value << shift) | (matrix[i] & ~mask)
}
function spawn (matrix, column_len, column_offset, i) {
let i_ = column_offset + ((i - column_offset) % column_len)
let glyph = ri(0, 53)
let traillen = ri(parseInt(column_len / 4), parseInt(column_len / 2))
let delay = ri(0, 0x80)
setv(matrix, i_, glyph, GLYPH_PROP)
setv(matrix, i_, traillen + 1, AGE_PROP)
setv(matrix, i_, traillen, TRAILLEN_PROP)
setv(matrix, i_, delay, DELAY_PROP)
}
function cell_props(cell) {
return {
value: cell.toString(16),
glyph: getv(cell, GLYPH_PROP),
age: getv(cell, AGE_PROP),
traillen: getv(cell, TRAILLEN_PROP),
delay: getv(cell, DELAY_PROP),
}
}
function update_cell (matrix, column_len, column_offset, i) {
let age = decay(matrix, i, AGE_PROP)
let traillen = getv(matrix[i], TRAILLEN_PROP)
if (age == (traillen - 1)) {
spawn(matrix, column_len, column_offset, i + 1)
}
else if (age == 0) {
if (decay(matrix, i, DELAY_PROP) <= 0) {
spawn(matrix, column_len, column_offset, ri(0, column_len - 1))
}
}
}
function update_matrix(matrix, column_len) {
for (let i = 0; i < matrix.length; ++i) {
let column_offset = parseInt(i / column_len) * column_len
update_cell(matrix, column_len, column_offset, i)
}
}
function draw_matrix(matrix, column_len) {
let x = 0
let y = 0
let fade = 0
for (let i = 0; i < matrix.length; i++) {
let column_offset = parseInt(i / column_len) * column_len
x = parseInt(i / column_len) * tm.w
y = (i - column_offset) * tm.h
fade = (getv(matrix[i], AGE_PROP) / getv(matrix[i], TRAILLEN_PROP))
fade = Math.pow(fade, 4) * 50
drawGlyph(getv(matrix[i], GLYPH_PROP), x, y, null, `hsl(10, 100%, ${fade+25}%)`)
}
}
function drawGlyph(glyph, dx, dy, darkenStyle, fillStyle) {
ctx.save()
// darken previous glyphs
if (darkenStyle !== null && darkenStyle !== undefined) {
ctx.globalCompositeOperation = "multiply"
ctx.fillStyle = darkenStyle
ctx.fillRect(dx, dy - tm.h, tm.w, tm.h)
}
ctx.globalCompositeOperation = "source-over"
let sw = tm.w * charScale
let sh = tm.h * charScale
ctx.drawImage(
charsOffscreen.canvas,
glyph * sw, 0, sw, sh,
dx, dy, tm.w, tm.h)
if (fillStyle !== null && fillStyle !== undefined) {
ctx.globalCompositeOperation = "multiply"
ctx.fillStyle = fillStyle
ctx.fillRect(dx, dy, tm.w, tm.h)
}
ctx.restore()
}
ctx.fillStyle='#000'
ctx.fillRect(0,0,width,height)
// ctx.drawImage(charsOffscreen.canvas, 0, 0)
function draw_frame() {
update_matrix(matrix, column_len)
draw_matrix(matrix, column_len)
// colorize
ctx.globalCompositeOperation = "multiply"
ctx.fillStyle='hsl(179, 100%, 50%)'
ctx.fillRect(0,0,width,height)
}
if (true) {
setInterval(draw_frame, 1000/10)
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment