Last active
August 6, 2024 18:17
-
-
Save fre-sch/7223701 to your computer and use it in GitHub Desktop.
Matrix rain effect
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
<!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> |
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
<!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