Custom Code Experiment •
Shark Loader
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Glitch great white loader</title>
<style>
:root {
box-sizing: border-box;
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
--page: #EDEDED;
--ring-track: rgba(0, 0, 0, 0.12);
--accent: #FFC21A;
--void: #0A0A0A;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
}
:root[data-theme="dark"] {
--page: #141414;
--ring-track: rgba(255, 255, 255, 0.12);
}
*, *::before, *::after { box-sizing: inherit; }
html { height: 100%; scroll-padding-top: env(safe-area-inset-top, 0px); }
body {
margin: 0;
height: 100%;
background: var(--page);
display: grid;
place-items: center;
}
.orb {
position: relative;
width: min(260px, 70vw);
aspect-ratio: 1;
}
/* Animated yellow ring */
.ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.ring circle { fill: none; stroke-width: 3; }
.ring .track { stroke: var(--ring-track); }
.ring .arc {
stroke: var(--accent);
stroke-linecap: round;
stroke-dasharray: 90 400;
animation: sweep 1.6s cubic-bezier(.6,.1,.3,.9) infinite;
transform-origin: 50% 50%;
}
@keyframes sweep {
0% { stroke-dasharray: 20 400; transform: rotate(0deg); }
50% { stroke-dasharray: 160 400; }
100% { stroke-dasharray: 20 400; transform: rotate(360deg); }
}
/* Circular viewport */
.window {
position: absolute;
inset: 12px;
border-radius: 50%;
overflow: hidden;
background: radial-gradient(circle at 50% 45%, #262626 0%, var(--void) 70%);
box-shadow: inset 0 0 40px rgba(0,0,0,.8), inset 0 0 0 1px rgba(255,255,255,.2);
}
.window canvas { display: block; width: 100%; height: 100%; }
/* faint shafts of light from the surface */
.window::before {
content: "";
position: absolute;
inset: 0;
opacity: .05;
background:
repeating-conic-gradient(from 150deg at 46% -45%, transparent 0deg, #fff 1deg, transparent 2deg 9.5deg),
repeating-conic-gradient(from 157deg at 60% -60%, transparent 0deg, #fff 0.6deg, transparent 1.2deg 13.7deg);
-webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 80%);
mask-image: linear-gradient(to bottom, #000 0%, transparent 80%);
pointer-events: none;
}
/* scanlines */
.window::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,.28) 2px 3px);
pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
.ring .arc { animation-duration: 4s; }
}
</style>
</head>
<body>
<div class="orb" role="status" aria-label="Loading">
<svg class="ring" viewBox="0 0 100 100" aria-hidden="true">
<circle class="track" cx="50" cy="50" r="48"/>
<circle class="arc" cx="50" cy="50" r="48"/>
</svg>
<div class="window">
<canvas id="shark" aria-hidden="true"></canvas>
</div>
</div>
<script>
(() => {
const canvas = document.getElementById('shark');
const ctx = canvas.getContext('2d');
const buf = document.createElement('canvas');
const bctx = buf.getContext('2d');
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const WHITE = '#FFFFFF';
const SNOW = '#9A9A9A';
const GHOST_A = '#FF2E88', GHOST_B = '#2EE6FF'; // glitch ghosts split into pink and cyan
const EYE = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#FFC21A';
let W = 0, H = 0, dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
const r = canvas.getBoundingClientRect();
W = Math.round(r.width * dpr); H = Math.round(r.height * dpr);
canvas.width = buf.width = W;
canvas.height = buf.height = H;
}
new ResizeObserver(resize).observe(canvas);
resize();
const lerp = (a, b, t) => a + (b - a) * t;
const clamp01 = (v) => Math.min(1, Math.max(0, v));
const smooth = (t) => t * t * (3 - 2 * t);
const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const mul = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
const norm = (a) => { const l = Math.hypot(a[0], a[1], a[2]) || 1; return [a[0] / l, a[1] / l, a[2] / l]; };
// ---------- Rainbow ----------
// Lines ask for 'rainbow' (most of the shark) or 'rainbowHi' (outline, fins, gills, jaws).
// Both are conic gradients centred on the shark that turn slowly, so the colours sweep round it.
const HUES = [0, 50, 100, 160, 210, 260, 310, 360];
const paint = {};
function rainbow(cx, cy, angle, light) {
const g = bctx.createConicGradient
? bctx.createConicGradient(angle, cx, cy)
: bctx.createLinearGradient(cx - W * 0.3, cy, cx + W * 0.3, cy);
HUES.forEach((h, i) => g.addColorStop(i / (HUES.length - 1), `hsl(${h % 360}, 100%, ${light}%)`));
return g;
}
// ---------- Body ----------
// Model space: x points to the nose, y is up, z is the shark's right side.
// The body is a tube swept along a spine. u runs 0 (snout) .. 1 (base of the tail).
// The spine curves gently, as if the shark is always turning, and a swimming wave
// runs down it that grows toward the tail.
const LEN = 1.7, XN = 0.95, BEND = 0.42;
const girth = (u) => (u < 0.33
? Math.pow(Math.max(0, u) / 0.33, 0.62) // conical snout
: 1 - 0.9 * smooth(Math.min(1, (u - 0.33) / 0.6))); // long taper to a slim tail stock
function spine(u, s) {
const x = XN - u * LEN + s.surge, d = XN - u * LEN - (XN - 0.45 * LEN);
const z = -BEND * d * d / 2
+ (0.012 + 0.2 * Math.max(0, u - 0.25) ** 2) * Math.sin(s.phase - u * 4.2)
+ s.shake * (1 - u) ** 3;
const y = s.gape * 0.05 * Math.max(0, 1 - u / 0.35) ** 2; // snout lifts as the jaws open
return [x, y, z];
}
// Cross-section frame at u: centre, forward T, right N, up U, plus the section's radii
function station(u, s) {
const e = 0.004;
const C = spine(u, s), T = norm(sub(spine(u - e, s), spine(u + e, s)));
const U = norm([-T[0] * T[1], 1 - T[1] * T[1], -T[2] * T[1]]);
const g = girth(u), dg = (girth(u + e) - girth(u - e)) / (2 * e);
const under = 0.8 + 0.2 * Math.min(1, u / 0.3); // flatter under the snout
return { u, C, T, U, N: cross(T, U),
a: 0.165 * g, bT: 0.2 * g, bB: 0.17 * g * under, // half width, height above, depth below
da: 0.165 * dg, dbT: 0.2 * dg, dbB: 0.17 * dg * under };
}
// Point on the skin; phi = 0 is the right flank, PI/2 the back
function at(st, phi, lift = 0) {
const c = Math.cos(phi), s = Math.sin(phi), b = s >= 0 ? st.bT : st.bB;
return add(st.C, add(mul(st.N, (st.a + lift) * c), mul(st.U, (b + lift) * s)));
}
function addMesh(out, table) {
for (const j of [6, 12, 19, 26, 33, 40, 47, 54]) {
out.push({ pts: Array.from({ length: 25 }, (_, i) => at(table[j], 2 * Math.PI * i / 24)), color: 'rainbow', w: 0.8, dim: true });
}
for (let i = 0; i < 8; i++) {
out.push({ pts: table.map((st) => at(st, i * Math.PI / 4)), color: 'rainbow', w: 0.8, dim: true });
}
// The ragged line where the grey back meets the white belly
const shade = (u) => -0.12 + 0.1 * Math.sin(u * 31) + 0.05 * Math.sin(u * 73);
for (const side of [1, -1]) {
out.push({ pts: table.slice(5, 57).map((st) => at(st, side > 0 ? shade(st.u) : Math.PI - shade(st.u), 0.002)),
color: 'rainbow', w: 1.0 });
}
}
// Outline as seen from the camera: on each cross-section, the two places where the
// skin turns side-on to the view. cam is the direction toward the camera in model space.
function addOutline(out, table, cam) {
const M = 36;
let A = [], B = [], fromNose = false;
const flush = (closed) => {
if (A.length > 1) {
const pts = A.slice().reverse();
if (fromNose) pts.push(table[0].C);
pts.push(...B);
if (closed) pts.push(A[A.length - 1]);
out.push({ pts, color: 'rainbowHi', w: 1.5 });
}
A = []; B = [];
};
for (let j = 1; j < table.length; j++) {
const st = table[j], g = [];
for (let i = 0; i < M; i++) {
const phi = 2 * Math.PI * i / M, c = Math.cos(phi), s = Math.sin(phi);
const b = s >= 0 ? st.bT : st.bB, db = s >= 0 ? st.dbT : st.dbB;
const dPhi = add(mul(st.N, -st.a * s), mul(st.U, b * c));
const dU = add(mul(st.T, -LEN), add(mul(st.N, st.da * c), mul(st.U, db * s)));
g.push(dot(cross(dPhi, dU), cam));
}
let a = null, b = null;
for (let i = 0; i < M; i++) {
const g0 = g[i], g1 = g[(i + 1) % M];
if ((g0 > 0) !== (g1 > 0)) {
const p = at(st, 2 * Math.PI * (i + g0 / (g0 - g1)) / M);
if (g0 > 0) a = p; else b = p;
}
}
if (a && b) { if (!A.length) fromNose = j === 1; A.push(a); B.push(b); }
else flush(true);
}
flush(false);
}
// ---------- Head ----------
const disc = (c, u, v, r, n = 14) =>
Array.from({ length: n + 1 }, (_, j) => { const a = 2 * Math.PI * j / n; return add(c, add(mul(u, Math.cos(a) * r), mul(v, Math.sin(a) * r))); });
function addHead(out, s) {
// Five gill slits on each side
for (const side of [1, -1]) {
for (let i = 0; i < 5; i++) {
const pts = [];
for (let k = 0; k <= 6; k++) {
const v = -1 + 2 * k / 6, phi = lerp(-0.75, 0.5, (v + 1) / 2) * (1 - i * 0.04);
const st = station(0.235 + i * 0.024 + 0.014 * v * v - 0.006 * v, s);
pts.push(at(st, side > 0 ? phi : Math.PI - phi, 0.003));
}
out.push({ pts, color: 'rainbowHi', w: 0.9 });
}
}
// Eyes
const st = station(0.11, s);
for (const phi of [0.3, Math.PI - 0.3]) {
const n = norm(add(mul(st.N, Math.cos(phi)), mul(st.U, Math.sin(phi))));
const c = at(st, phi, 0.004), v = norm(cross(n, st.T));
out.push({ pts: disc(c, st.T, v, 0.027), color: EYE, w: 1.0, fill: 0.25, glow: 0.2 });
out.push({ pts: disc(add(c, mul(n, 0.004)), st.T, v, 0.011, 8), color: WHITE, w: 0.8, fill: 0.9 });
}
// Jaws: a crescent under the snout. The lower jaw swings down from the corners,
// the upper jaw pushes forward, and both rows of teeth come out as it opens.
const n = 16, upper = [], lower = [], width = [];
for (let i = 0; i <= n; i++) {
const w = -1 + 2 * i / n, open = 1 - w * w;
const js = station(0.105 + 0.085 * w * w, s);
const base = at(js, -Math.PI / 2 + w * 1.15, -0.004);
upper.push(add(base, add(mul(js.U, -0.03 * s.gape * open), mul(js.T, 0.03 * s.gape * open))));
lower.push(add(base, add(mul(js.U, -(0.012 + 0.25 * s.gape * Math.pow(open, 0.8))), mul(js.T, -0.05 * s.gape * open))));
width.push(1 - 0.4 * w * w);
}
out.push({ pts: upper, color: 'rainbowHi', w: 1.3 });
out.push({ pts: lower, color: 'rainbowHi', w: 1.3 });
const show = clamp01((s.gape - 0.08) / 0.3);
if (show > 0) {
const teeth = (jaw, dir) => {
const pts = [];
for (let i = 1; i < n; i++) {
pts.push(mul(add(jaw[i - 1], jaw[i]), 0.5), add(jaw[i], mul(dir, 0.048 * show * width[i])));
}
pts.push(mul(add(jaw[n - 1], jaw[n]), 0.5));
out.push({ pts, color: WHITE, w: 1.1, glow: 0.2 * show });
};
teeth(upper, [0, -1, 0]);
teeth(lower, [0, 1, 0]);
// Throat lines give the open mouth some depth
const throat = station(0.27, s);
for (const w of [-0.6, 0, 0.6]) {
const i = Math.round((w + 1) / 2 * n);
out.push({ pts: [lower[i], at(throat, -Math.PI / 2 + w * 0.8)], color: 'rainbow', w: 0.8, dim: true, alpha: show });
}
}
}
// ---------- Fins ----------
// Outlines in 2D: [distance back from the fin's leading root, distance out from the body]
const bez = (p0, p1, p2, n) => Array.from({ length: n + 1 }, (_, i) => {
const t = i / n, m = 1 - t;
return [m * m * p0[0] + 2 * m * t * p1[0] + t * t * p2[0], m * m * p0[1] + 2 * m * t * p1[1] + t * t * p2[1]];
});
const join = (...parts) => parts.reduce((all, p, i) => all.concat(i ? p.slice(1) : p), []);
const DORSAL = join(bez([0, 0], [0.06, 0.2], [0.24, 0.31], 8), bez([0.24, 0.31], [0.17, 0.15], [0.3, 0], 7));
const PECTORAL = join(bez([0, 0], [0.05, 0.2], [0.22, 0.38], 8), bez([0.22, 0.38], [0.15, 0.16], [0.16, 0], 7));
const PELVIC = join(bez([0, 0], [0.03, 0.07], [0.11, 0.1], 5), bez([0.11, 0.1], [0.08, 0.04], [0.08, 0], 4));
const SMALL = [[0, 0], [0.03, 0.05], [0.055, 0.06], [0.05, 0.03], [0.07, 0]];
const CAUDAL = join(bez([0, 0.03], [0.08, 0.2], [0.26, 0.42], 8), bez([0.26, 0.42], [0.13, 0.2], [0.12, 0], 6),
bez([0.12, 0], [0.12, -0.16], [0.22, -0.33], 6), bez([0.22, -0.33], [0.07, -0.15], [0, -0.03], 7));
// A fin riding on the body: each outline point sits at its own station, pushed out along outDir
function bodyFin(out, shape, u0, phi, outDir, s) {
const pts = shape.map(([d, h]) => { const st = station(Math.min(1, u0 + d / LEN), s); return add(at(st, phi), mul(outDir(st), h)); });
out.push({ pts, color: 'rainbowHi', w: 1.2, fill: 0.08 });
}
function addFins(out, s) {
const up = (st) => st.U, down = (st) => mul(st.U, -1);
bodyFin(out, DORSAL, 0.36, Math.PI / 2, up, s);
bodyFin(out, SMALL, 0.8, Math.PI / 2, up, s);
bodyFin(out, SMALL, 0.82, -Math.PI / 2, down, s);
const droop = 0.4 + 0.05 * Math.sin(s.phase * 0.5);
for (const side of [1, -1]) {
bodyFin(out, PECTORAL, 0.27, side > 0 ? -0.55 : Math.PI + 0.55,
(st) => norm(add(mul(st.N, side * Math.cos(droop)), mul(st.U, -Math.sin(droop)))), s);
bodyFin(out, PELVIC, 0.66, -Math.PI / 2 + side * 0.5,
(st) => norm(add(mul(st.N, side * 0.6), mul(st.U, -0.8))), s);
}
// Crescent tail: follows the end of the spine and flexes a little behind it
const st = station(1, s), flex = -0.35 * Math.cos(s.phase - 4.2);
const back = mul(st.T, -1);
const dir = add(mul(back, Math.cos(flex)), mul(cross(st.U, back), Math.sin(flex)));
out.push({ pts: CAUDAL.map(([d, h]) => add(st.C, add(mul(dir, d), mul(st.U, h)))), color: 'rainbowHi', w: 1.3, fill: 0.08 });
}
// ---------- Hunt cycle ----------
// Cruise, open up while surging forward, hold, then snap shut and shake the head.
const PERIOD = reduced ? 7 : 4.2;
const SNAP = 0.78;
function bite(q) { // -> [jaw gape, lunge]
if (q < 0.5) return [0.04, 0];
if (q < 0.68) { const u = (q - 0.5) / 0.18; return [lerp(0.04, 1, 1 - (1 - u) ** 3), smooth(u)]; }
if (q < 0.76) return [1, 1];
if (q < SNAP) { const u = (q - 0.76) / (SNAP - 0.76); return [1 - u * u, 1]; }
const u = smooth((q - SNAP) / (1 - SNAP));
return [0.04 * u, 1 - u];
}
// ---------- Marine snow ----------
const SNOWFLAKES = Array.from({ length: 28 }, (_, i) => {
const h = (n) => { const v = Math.sin(i * 91.7 + n * 37.3) * 43758.5453; return v - Math.floor(v); };
return { p: [h(1) * 3 - 1.5, h(2) * 2.6 - 1.3, h(3) * 2.4 - 1.2], r: 0.004 + h(4) * 0.006, fall: 0.02 + h(5) * 0.03 };
});
// ---------- Projection ----------
const CENTER_Y = 0.05, SCALE = 0.36, PERSP = 4.2;
const view = (rotY, tilt, bob) =>
({ cr: Math.cos(rotY), sr: Math.sin(rotY), ct: Math.cos(tilt), st: Math.sin(tilt), bob });
function project(p, v, jitter) {
let x = p[0], y = p[1] - CENTER_Y + v.bob, z = p[2];
if (jitter) { x += (Math.random() - .5) * jitter; y += (Math.random() - .5) * jitter; }
const x1 = x * v.cr + z * v.sr, z1 = -x * v.sr + z * v.cr;
const y1 = y * v.ct - z1 * v.st, z2 = y * v.st + z1 * v.ct;
const s = W * SCALE * PERSP / (PERSP - z2);
return [W / 2 + x1 * s, H / 2 - y1 * s, z2, s];
}
function strokeAll(polys, v, color, alpha, ox, oy, jitter) {
for (const poly of polys) {
bctx.beginPath();
let depth = 0;
if (poly.r) { // a dot that always faces the camera
const [sx, sy, sz, s] = project(poly.c, v, jitter);
bctx.arc(sx + ox, sy + oy, poly.r * s, 0, 2 * Math.PI);
depth = sz;
} else {
for (let i = 0; i < poly.pts.length; i++) {
const [sx, sy, sz] = project(poly.pts[i], v, jitter);
depth += sz;
if (i === 0) bctx.moveTo(sx + ox, sy + oy); else bctx.lineTo(sx + ox, sy + oy);
}
depth /= poly.pts.length;
}
const fade = 0.5 + 0.5 * Math.min(1, Math.max(0, (depth + 1.2) / 2.4));
const a = alpha * fade * (poly.dim ? 0.55 : 1) * (poly.alpha ?? 1);
bctx.strokeStyle = bctx.fillStyle = color || paint[poly.color] || poly.color;
if (poly.fill) { bctx.globalAlpha = a * poly.fill; bctx.fill(); }
if (poly.glow) { bctx.globalAlpha = a * poly.glow; bctx.lineWidth = poly.w * 4 * dpr; bctx.stroke(); }
bctx.globalAlpha = a;
bctx.lineWidth = poly.w * dpr;
bctx.stroke();
}
bctx.globalAlpha = 1;
}
// ---------- Loop ----------
let glitch = 0, glitchStrength = 0;
let rot = 0, last = performance.now();
let frozenRot = null;
let phase = 0, lastQ = 0, snapAt = -10;
function frame(now) {
if (W === 0 || H === 0) {
resize();
if (W === 0 || H === 0) { last = now; requestAnimationFrame(frame); return; }
}
const dt = Math.min(50, now - last) / 1000; last = now;
const t = now / 1000;
rot += dt * (reduced ? 0.25 : 0.6);
// The jaws snapping shut kicks off a glitch of their own
const q = (t / PERIOD) % 1;
if (lastQ < SNAP && q >= SNAP) {
snapAt = t;
if (!reduced) { glitch = 7; glitchStrength = 0.9; frozenRot = null; }
}
lastQ = q;
if (!reduced && glitch <= 0 && Math.random() < 0.025) {
glitch = 4 + Math.floor(Math.random() * 10);
glitchStrength = 0.4 + Math.random() * 0.6;
if (Math.random() < 0.4) frozenRot = rot + (Math.random() - .5) * 1.2;
}
const g = glitch > 0;
if (g) glitch--; else frozenRot = null;
const bob = Math.sin(t * 2.2) * 0.04;
const tilt = 0.3 + Math.sin(t * 0.9) * 0.06;
const sharkView = view(frozenRot ?? rot, tilt, bob);
const waterView = view(0, tilt, 0);
const [gape, lunge] = bite(q);
const since = t - snapAt;
const shake = reduced || since > 1 ? 0 : 0.07 * Math.exp(-since * 5) * Math.sin(since * 34);
phase += dt * 2 * Math.PI * lerp(1.5, 3, lunge) * (reduced ? 0.5 : 1); // tail beats harder in the lunge
const s = { phase, gape, shake, surge: 0.08 * lunge };
const table = Array.from({ length: 61 }, (_, j) => station(j / 60, s));
const [cx, cy] = project([0, 0, 0], sharkView, 0);
paint.rainbow = rainbow(cx, cy, t * 0.9, 62);
paint.rainbowHi = rainbow(cx, cy, t * 0.9, 74);
const shark = [];
addMesh(shark, table);
addOutline(shark, table, [-sharkView.ct * sharkView.sr, sharkView.st, sharkView.ct * sharkView.cr]);
addFins(shark, s);
addHead(shark, s);
const water = SNOWFLAKES.map((f) => {
f.p[1] -= f.fall * dt;
f.p[0] += 0.015 * dt;
if (f.p[1] < -1.3) f.p[1] += 2.6;
if (f.p[0] > 1.5) f.p[0] -= 3;
return { c: f.p, r: f.r, color: SNOW, w: 0.5, fill: 0.8, dim: true };
});
bctx.clearRect(0, 0, W, H);
bctx.lineJoin = 'round'; bctx.lineCap = 'round';
bctx.globalCompositeOperation = 'lighter';
const drawScene = (color, alpha, ox, oy, jitter) => {
strokeAll(water, waterView, color, alpha, ox, oy, jitter);
strokeAll(shark, sharkView, color, alpha, ox, oy, jitter);
};
const baseJit = reduced ? 0 : 0.005;
if (g) {
const off = (4 + Math.random() * 8) * dpr * glitchStrength;
drawScene(GHOST_A, 0.7, -off, 0, 0.03 * glitchStrength);
drawScene(GHOST_B, 0.6, off, (Math.random() - .5) * 2 * dpr, 0.03 * glitchStrength);
}
drawScene(null, g ? 0.85 : 1, 0, 0, g ? 0.02 : baseJit);
bctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, W, H);
if (g) {
let y = 0;
while (y < H) {
const h = Math.min(H - y, Math.max(2, Math.floor((4 + Math.random() * 30) * dpr)));
const shift = Math.random() < 0.35 ? (Math.random() - .5) * 40 * dpr * glitchStrength : 0;
ctx.drawImage(buf, 0, y, W, h, shift, y, W, h);
y += h;
}
const blocks = Math.floor(Math.random() * 4);
for (let i = 0; i < blocks; i++) {
ctx.fillStyle = [WHITE, GHOST_A, GHOST_B][i % 3];
ctx.globalAlpha = 0.18 + Math.random() * 0.3;
ctx.fillRect(Math.random() * W, Math.random() * H, (10 + Math.random() * 60) * dpr, (1 + Math.random() * 3) * dpr);
}
ctx.globalAlpha = 1;
} else {
ctx.drawImage(buf, 0, 0);
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>