Skip to content
bgHub
Gallery

Royal Violet Slanted Rain

Canvas 2DMedium

heavy slanted rain streaking down a dark canvas. Canvas 2D.

#rain#lines#canvas#slant#royal-violet
Live preview
1 file
// @ts-nocheck
import { useEffect, useRef } from "react";

// Render <Rain /> inside a container with position: relative.
const opts = {"variant":"slant","bg":"#07050d","colors":["#7c6cf6","#a78bfa","#c4b5fd"],"light":{"bg":"#f6f4fd","colors":["#5b21b6","#6d28d9","#8b5cf6"]},"speed":1.4,"density":1.5};

export function Rain() {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;

const ctx = canvas.getContext("2d");
const V = opts.variant;
/* Both schemes ship in opts; `light` is absent in the live preview, which
   resolves the scheme itself, so nothing is observed there. */
const schemeMq =
  opts.light && typeof matchMedia === "function"
    ? matchMedia("(prefers-color-scheme: light)")
    : null;
const schemeNow = () => (schemeMq && schemeMq.matches ? opts.light : opts);
/** The scheme the scene or particle set was first built from. */
const SCHEME_BUILT = schemeNow();
let COLORS = SCHEME_BUILT.colors, BG = SCHEME_BUILT.bg;
const onScheme = () => {
  const s = schemeNow();
  COLORS = s.colors; BG = s.bg;
  if (typeof seed === "function") seed();
  if (typeof applyScheme === "function") applyScheme();
};
if (schemeMq) schemeMq.addEventListener("change", onScheme);
const SPEED = opts.speed || 1, DENSITY = opts.density || 1;
const slant = V === "slant" ? 0.4 : 0;
const rnd = (a, b) => a + Math.random() * (b - a);
const pick = (a) => a[(Math.random() * a.length) | 0];
let w = 0, h = 0, dpr = 1, drops = [], raf = 0;
const count = () => Math.max(30, Math.min(500, Math.floor((w / 6) * DENSITY)));
const mk = () => ({ x: rnd(0, w), y: rnd(-h, 0), len: V === "drops" ? rnd(6, 16) : rnd(12, 28), sp: rnd(4, 9) * SPEED, c: pick(COLORS), a: rnd(0.2, 0.7) });
const resize = () => {
  w = canvas.clientWidth; h = canvas.clientHeight;
  dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = w * dpr; canvas.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  drops = Array.from({ length: count() }, mk);
};
/* Each drop copies a colour at spawn, and the reseed lives in resize here rather
   than in a seed() the prelude would find on its own. */
function applyScheme() { resize(); }
const step = () => {
  ctx.fillStyle = BG; ctx.fillRect(0, 0, w, h);
  ctx.lineWidth = V === "drops" ? 2 : 1;
  for (const d of drops) {
    ctx.globalAlpha = d.a; ctx.strokeStyle = d.c;
    if (V === "drops") { ctx.shadowBlur = 8; ctx.shadowColor = d.c; }
    ctx.beginPath(); ctx.moveTo(d.x, d.y); ctx.lineTo(d.x + d.len * slant, d.y + d.len); ctx.stroke();
    ctx.shadowBlur = 0;
    d.y += d.sp; d.x += d.sp * slant;
    if (d.y > h) { d.y = rnd(-40, 0); d.x = rnd(0, w); }
  }
  ctx.globalAlpha = 1;
};
const ro = new ResizeObserver(resize); ro.observe(canvas); resize();
const loop = () => { step(); raf = requestAnimationFrame(loop); }; loop();
return () => { cancelAnimationFrame(raf); ro.disconnect(); if (schemeMq) schemeMq.removeEventListener("change", onScheme); };

  }, []);
  return (
    <canvas ref={ref} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }} />
  );
}

Copy or download any file. The HTML build loads Three.js from a CDN via an import map, so it runs by just opening index.html. The React build is react-three-fiber; setup notes live at the top of each file.

Variants

40 of this pattern · page 1 of 4