Gallery
Neon Lime Rising Bubbles
Canvas 2DLight
busy rising bubbles floating upward with a rim highlight. Canvas 2D.
#bubbles#canvas#rise#neon-lime
Live preview
1 file
// @ts-nocheck
import { useEffect, useRef } from "react";
// Render <Bubbles /> inside a container with position: relative.
const opts = {"variant":"rise","bg":"#060a04","colors":["#a3e635","#4ade80","#bef264"],"light":{"bg":"#f6faee","colors":["#4d7c0f","#15803d","#65a30d"]},"speed":1.35,"density":1.4};
export function Bubbles() {
const ref = useRef(null);
useEffect(() => {
const canvas = ref.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
/* 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, V = opts.variant;
let w = 0, h = 0, dpr = 1, raf = 0, t = 0;
const mouse = { x: -9999, y: -9999, on: false };
const rnd = (a, b) => a + Math.random() * (b - a);
const pick = (a) => a[(Math.random() * a.length) | 0];
const TAU = Math.PI * 2;
const TUNE = {
rise: { size: [6, 26], up: 0.55, wob: 0.7, fill: 0.16 },
foam: { size: [3, 12], up: 0.35, wob: 0.4, fill: 0.30 },
jelly: { size: [14, 46], up: 0.28, wob: 1.2, fill: 0.12 },
soda: { size: [2, 7], up: 1.5, wob: 0.5, fill: 0.42 },
};
const K = TUNE[V] || TUNE.rise;
let ps = [];
function make(anywhere) {
return {
x: rnd(0, w),
y: anywhere ? rnd(0, h) : h + rnd(10, 80),
r: rnd(K.size[0], K.size[1]),
ph: rnd(0, TAU),
vy: rnd(0.4, 1.1) * K.up,
c: pick(COLORS),
a: rnd(0.35, 0.9),
};
}
function seed() {
const n = Math.max(18, Math.min(150, Math.floor((w * h) / 14000 * DENSITY)));
ps = Array.from({ length: n }, () => make(true));
}
function draw() {
ctx.fillStyle = BG;
ctx.fillRect(0, 0, w, h);
for (const p of ps) {
p.ph += 0.02 * SPEED;
p.y -= p.vy * SPEED;
p.x += Math.sin(p.ph) * K.wob * SPEED;
if (mouse.on) {
const dx = p.x - mouse.x, dy = p.y - mouse.y, d = Math.hypot(dx, dy) + 1;
if (d < 140) { p.x += (dx / d) * 1.4; p.y += (dy / d) * 1.4; }
}
if (p.y < -p.r - 20) Object.assign(p, make(false));
const squash = V === "jelly" ? 1 + Math.sin(p.ph * 2) * 0.14 : 1;
ctx.save();
ctx.translate(p.x, p.y);
ctx.scale(squash, 1 / squash);
ctx.globalAlpha = p.a * K.fill;
ctx.fillStyle = p.c;
ctx.beginPath(); ctx.arc(0, 0, p.r, 0, TAU); ctx.fill();
ctx.globalAlpha = p.a * 0.75;
ctx.strokeStyle = p.c;
ctx.lineWidth = 1.1;
ctx.beginPath(); ctx.arc(0, 0, p.r, 0, TAU); ctx.stroke();
ctx.globalAlpha = p.a;
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.arc(0, 0, p.r * 0.72, Math.PI * 1.05, Math.PI * 1.5);
ctx.stroke();
ctx.restore();
}
ctx.globalAlpha = 1;
}
function 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);
seed();
}
const onMove = (e) => { const r = canvas.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; mouse.on = true; };
const onLeave = () => { mouse.on = false; };
const loop = () => { t += 0.016 * SPEED; draw(); raf = requestAnimationFrame(loop); };
const ro = new ResizeObserver(resize); ro.observe(canvas);
resize(); loop();
canvas.addEventListener("pointermove", onMove); canvas.addEventListener("pointerleave", onLeave);
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener("pointermove", onMove); canvas.removeEventListener("pointerleave", onLeave); 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