How would you describe an animated CSS hero section for a craft beer website where bubbles rise from the bottom, vary in size and opacity to mimic real foam, slow down and pop near the top, and include interactive controls for foam density and colour tint
📦 Prompts
✨ The Prompt Phrase
Create a CSS foam effect for the hero section of a craft beer website. Generate animated foam bubbles that rise from the bottom of the header area, vary in size and opacity like real beer foam, and gradually slow and pop as they reach the top. Include a foam density control and a foam colour tint selector matching different beer styles from pale ale to stout.
💻 Code Preview
📦 All-in-One Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>HopCraft Brewery</title>
<style>
/* ═══════════════════════════════════════════════════════════
TOKENS
═══════════════════════════════════════════════════════════ */
:root {
--beer-bg: #c8860a;
--beer-mid: #a06808;
--beer-dark: #7a4e06;
--foam-tint: rgba(255,248,220,0.82);
--foam-edge: rgba(255,255,255,0.95);
--foam-shadow: rgba(180,130,40,0.25);
--text-light: #fff8e7;
--text-muted: rgba(255,248,220,0.65);
--accent: #f5c842;
--ui-bg: rgba(0,0,0,0.35);
--ui-border: rgba(255,255,255,0.15);
--r: 12px;
}
/* ═══════════════════════════════════════════════════════════
RESET
═══════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #1a0f00;
color: var(--text-light);
overflow-x: hidden;
}
/* ═══════════════════════════════════════════════════════════
HERO SECTION
═══════════════════════════════════════════════════════════ */
.hero {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
background: var(--beer-bg);
transition: background 1.2s ease;
}
/* ── Layered beer gradient background ── */
.hero-bg {
position: absolute; inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% 110%,
rgba(0,0,0,0.4) 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 30% 20%,
rgba(255,220,100,0.15) 0%, transparent 60%),
linear-gradient(180deg,
var(--beer-dark) 0%,
var(--beer-bg) 40%,
var(--beer-mid) 100%);
transition: background 1.2s ease;
z-index: 0;
}
/* ── Carbonation stream lines ── */
.streams {
position: absolute; inset: 0;
z-index: 1; pointer-events: none;
overflow: hidden;
}
.stream {
position: absolute;
bottom: 0;
width: 1px;
background: linear-gradient(180deg,
transparent 0%,
rgba(255,255,255,0.12) 40%,
rgba(255,255,255,0.06) 100%);
animation: streamRise linear infinite;
transform-origin: bottom center;
}
@keyframes streamRise {
0% { height: 0; opacity: 0; }
10% { opacity: 1; }
90% { opacity: .4; }
100% { height: 100vh; opacity: 0; }
}
/* ── Foam layer at top ── */
.foam-layer {
position: absolute;
top: 0; left: -5%; right: -5%;
height: 90px;
z-index: 10;
pointer-events: none;
overflow: hidden;
}
.foam-layer svg {
width: 110%; height: 100%;
animation: foamSway 6s ease-in-out infinite alternate;
}
@keyframes foamSway {
0% { transform: translateX(0); }
100% { transform: translateX(-4%); }
}
/* ── Bubble canvas ── */
#bubble-canvas {
position: absolute; inset: 0;
z-index: 2; pointer-events: none;
}
/* ═══════════════════════════════════════════════════════════
HERO CONTENT
═══════════════════════════════════════════════════════════ */
.hero-content {
position: relative; z-index: 20;
text-align: center;
padding: 2rem 1.5rem;
max-width: 760px;
}
.hero-eyebrow {
display: inline-flex; align-items: center; gap: .5rem;
font-size: .72rem; font-weight: 800;
letter-spacing: .18em; text-transform: uppercase;
color: var(--accent);
background: rgba(0,0,0,.25);
border: 1px solid rgba(245,200,66,.25);
border-radius: 99px;
padding: .35rem 1rem;
margin-bottom: 1.5rem;
}
.eyebrow-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: .5; transform: scale(.7); }
}
.hero-title {
font-size: clamp(2.8rem, 8vw, 5.5rem);
font-weight: 900;
letter-spacing: -.04em;
line-height: 1.0;
color: #fff;
text-shadow:
0 2px 0 rgba(0,0,0,.3),
0 8px 32px rgba(0,0,0,.4);
margin-bottom: 1.25rem;
}
.hero-title em {
font-style: normal;
color: var(--accent);
text-shadow:
0 0 40px rgba(245,200,66,.5),
0 2px 0 rgba(0,0,0,.3);
}
.hero-sub {
font-size: clamp(.95rem, 2vw, 1.2rem);
line-height: 1.75;
color: var(--text-muted);
max-width: 52ch;
margin: 0 auto 2.5rem;
}
.hero-btns {
display: flex; gap: .75rem;
justify-content: center; flex-wrap: wrap;
}
.btn {
padding: .8rem 2rem;
border-radius: 99px;
font-size: .9rem; font-weight: 800;
letter-spacing: .03em;
cursor: pointer; border: none;
font-family: inherit;
transition: transform .15s, box-shadow .15s, opacity .15s;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: scale(.97); }
.btn-primary {
background: linear-gradient(135deg, var(--accent), #e8a820);
color: #1a0f00;
box-shadow: 0 6px 24px rgba(245,200,66,.4);
}
.btn-primary:hover { box-shadow: 0 10px 32px rgba(245,200,66,.55); }
.btn-ghost {
background: rgba(255,255,255,.1);
color: #fff;
border: 1.5px solid rgba(255,255,255,.25);
backdrop-filter: blur(6px);
}
.btn-ghost:hover { background: rgba(255,255,255,.18); }
/* ═══════════════════════════════════════════════════════════
CONTROLS PANEL
═══════════════════════════════════════════════════════════ */
.controls {
position: fixed;
bottom: 1.5rem; left: 50%;
transform: translateX(-50%);
z-index: 100;
background: rgba(10,5,0,.75);
backdrop-filter: blur(16px);
border: 1px solid var(--ui-border);
border-radius: 20px;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
justify-content: center;
box-shadow: 0 16px 48px rgba(0,0,0,.5);
max-width: 95vw;
}
.ctrl-group {
display: flex; flex-direction: column; gap: .35rem;
}
.ctrl-label {
font-size: .6rem; font-weight: 800;
letter-spacing: .12em; text-transform: uppercase;
color: rgba(255,255,255,.45);
}
/* Density slider */
.density-wrap {
display: flex; align-items: center; gap: .6rem;
}
.density-slider {
-webkit-appearance: none;
width: 110px; height: 4px;
background: rgba(255,255,255,.15);
border-radius: 99px; outline: none; cursor: pointer;
}
.density-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px; height: 16px; border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px rgba(245,200,66,.5);
cursor: pointer;
transition: transform .15s;
}
.density-slider::-webkit-slider-thumb:hover { transform: scale(1.25); }
.density-val {
font-size: .75rem; font-weight: 800;
color: var(--accent); min-width: 28px;
font-variant-numeric: tabular-nums;
}
/* Beer style swatches */
.swatches {
display: flex; gap: .45rem; align-items: center;
}
.swatch {
width: 28px; height: 28px; border-radius: 50%;
cursor: pointer; border: 2px solid transparent;
transition: transform .15s, border-color .15s, box-shadow .15s;
position: relative;
flex-shrink: 0;
}
.swatch:hover { transform: scale(1.15); }
.swatch.active {
border-color: #fff;
box-shadow: 0 0 12px rgba(255,255,255,.4);
transform: scale(1.2);
}
.swatch[title]::after {
content: attr(title);
position: absolute;
bottom: calc(100% + 6px);
left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,.85);
color: #fff; font-size: .6rem; font-weight: 700;
padding: .2em .5em; border-radius: 5px;
white-space: nowrap; pointer-events: none;
opacity: 0; transition: opacity .15s;
}
.swatch:hover::after { opacity: 1; }
/* Speed toggle */
.speed-btns { display: flex; gap: .3rem; }
.speed-btn {
padding: .3rem .65rem;
border-radius: 8px;
font-size: .7rem; font-weight: 700;
background: rgba(255,255,255,.08);
border: 1px solid rgba(255,255,255,.12);
color: rgba(255,255,255,.55);
cursor: pointer; font-family: inherit;
transition: all .15s;
}
.speed-btn:hover { background: rgba(255,255,255,.14); color: #fff; }
.speed-btn.active {
background: rgba(245,200,66,.2);
border-color: rgba(245,200,66,.4);
color: var(--accent);
}
.ctrl-sep {
width: 1px; height: 36px;
background: rgba(255,255,255,.1);
flex-shrink: 0;
}
/* ═══════════════════════════════════════════════════════════
SCROLL INDICATOR
═══════════════════════════════════════════════════════════ */
.scroll-hint {
position: absolute;
bottom: 7rem; left: 50%;
transform: translateX(-50%);
z-index: 20;
display: flex; flex-direction: column;
align-items: center; gap: .4rem;
opacity: .5;
animation: scrollBob 2s ease-in-out infinite;
}
@keyframes scrollBob {
0%,100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(6px); }
}
.scroll-hint span {
font-size: .62rem; font-weight: 700;
letter-spacing: .12em; text-transform: uppercase;
color: var(--text-muted);
}
.scroll-arrow {
width: 20px; height: 20px;
border-right: 2px solid var(--text-muted);
border-bottom: 2px solid var(--text-muted);
transform: rotate(45deg);
}
/* ═══════════════════════════════════════════════════════════
BELOW-FOLD SECTION (demo)
═══════════════════════════════════════════════════════════ */
.below {
background: #120900;
padding: 5rem 2rem;
text-align: center;
}
.below h2 {
font-size: 2rem; font-weight: 900;
color: var(--accent); margin-bottom: 1rem;
letter-spacing: -.03em;
}
.below p {
color: rgba(255,248,220,.5);
max-width: 50ch; margin: 0 auto;
line-height: 1.75; font-size: .95rem;
}
</style>
</head>
<body>
<!-- ══════════════════════════════════════════════════════════════
HERO
══════════════════════════════════════════════════════════════ -->
<section class="hero" id="hero">
<div class="hero-bg" id="hero-bg"></div>
<!-- Carbonation streams -->
<div class="streams" id="streams"></div>
<!-- Bubble canvas -->
<canvas id="bubble-canvas"></canvas>
<!-- Foam top layer -->
<div class="foam-layer">
<svg viewBox="0 0 1200 90" preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg" id="foam-svg">
<defs>
<radialGradient id="fg" cx="50%" cy="40%" r="60%">
<stop offset="0%" stop-color="#fffdf0" stop-opacity="1"/>
<stop offset="100%" stop-color="#f5e090" stop-opacity="0.7"/>
</radialGradient>
</defs>
<!-- Foam blob path — organic bumpy shape -->
<path id="foam-path"
d="M-20,90
C30,90 20,55 60,52
C80,50 85,62 110,58
C135,54 130,38 165,35
C195,32 200,50 230,48
C260,46 255,28 295,25
C330,22 335,42 365,40
C395,38 390,18 430,15
C465,12 470,35 505,33
C540,31 535,10 575,8
C610,6 615,28 650,26
C685,24 680,5 720,3
C755,1 760,22 795,20
C830,18 825,0 865,0
C900,0 905,20 940,18
C975,16 970,2 1010,0
C1045,0 1050,18 1085,16
C1115,14 1110,4 1140,2
C1165,0 1175,14 1220,12
L1220,90 Z"
fill="url(#fg)" opacity="0.92"/>
<!-- Second foam layer for depth -->
<path
d="M-20,90
C10,90 5,68 45,65
C70,63 75,75 100,72
C125,69 120,52 155,49
C185,46 190,62 220,60
C250,58 245,42 280,39
C315,36 320,55 350,53
C380,51 375,35 410,32
C445,29 450,48 485,46
C520,44 515,28 550,25
C585,22 590,42 625,40
C660,38 655,22 690,19
C725,16 730,36 765,34
C800,32 795,16 830,13
C865,10 870,30 905,28
C940,26 935,10 970,8
C1005,6 1010,24 1045,22
C1080,20 1075,6 1110,4
C1145,2 1150,18 1220,15
L1220,90 Z"
fill="white" opacity="0.35"/>
</svg>
</div>
<!-- Content -->
<div class="hero-content">
<div class="hero-eyebrow">
<div class="eyebrow-dot"></div>
Craft Brewed Since 1987
</div>
<h1 class="hero-title">
Brewed with<br/><em>Passion</em> & Hops
</h1>
<p class="hero-sub">
Small-batch ales crafted from the finest malts and hand-selected hops.
Every pour tells a story — from grain to glass.
</p>
<div class="hero-btns">
<button class="btn btn-primary">🍺 Explore Our Beers</button>
<button class="btn btn-ghost">Our Brewery Story</button>
</div>
</div>
<div class="scroll-hint">
<span>Scroll</span>
<div class="scroll-arrow"></div>
</div>
</section>
<!-- BELOW FOLD -->
<section class="below">
<h2>🌾 From Field to Foam</h2>
<p>We source our barley from local farms, roast our own malts, and dry-hop
every batch by hand. The result? A pint that's worth every sip.</p>
</section>
<!-- ══════════════════════════════════════════════════════════════
CONTROLS
══════════════════════════════════════════════════════════════ -->
<div class="controls">
<!-- Density -->
<div class="ctrl-group">
<div class="ctrl-label">🫧 Foam Density</div>
<div class="density-wrap">
<input type="range" class="density-slider"
id="density" min="5" max="60" value="28"/>
<span class="density-val" id="density-val">28</span>
</div>
</div>
<div class="ctrl-sep"></div>
<!-- Beer style / colour -->
<div class="ctrl-group">
<div class="ctrl-label">🍺 Beer Style</div>
<div class="swatches" id="swatches"></div>
</div>
<div class="ctrl-sep"></div>
<!-- Speed -->
<div class="ctrl-group">
<div class="ctrl-label">⏱ Rise Speed</div>
<div class="speed-btns">
<button class="speed-btn" data-speed="0.4">Slow</button>
<button class="speed-btn active" data-speed="1">Normal</button>
<button class="speed-btn" data-speed="1.8">Fast</button>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════
JAVASCRIPT
══════════════════════════════════════════════════════════════ -->
<script>
(() => {
'use strict';
/* ── BEER STYLES ──────────────────────────────────────────── */
const STYLES = [
{
name: 'Pale Ale',
bg: ['#d4a017','#c8860a','#a06808'],
foam: 'rgba(255,252,230,0.88)',
foamEdge: 'rgba(255,255,255,0.96)',
bubble: { r:220, g:200, b:140 },
swatch: '#f5c842',
},
{
name: 'IPA',
bg: ['#c49010','#b07808','#8a5c06'],
foam: 'rgba(255,248,210,0.85)',
foamEdge: 'rgba(255,255,240,0.95)',
bubble: { r:210, g:185, b:110 },
swatch: '#e8a820',
},
{
name: 'Wheat',
bg: ['#e8c060','#d4a840','#b88828'],
foam: 'rgba(255,255,245,0.92)',
foamEdge: 'rgba(255,255,255,0.98)',
bubble: { r:240, g:230, b:190 },
swatch: '#f0d060',
},
{
name: 'Amber',
bg: ['#b06010','#904808','#703206'],
foam: 'rgba(255,235,195,0.85)',
foamEdge: 'rgba(255,248,220,0.95)',
bubble: { r:200, g:155, b:90 },
swatch: '#c06820',
},
{
name: 'Red Ale',
bg: ['#8c3010','#702008','#541406'],
foam: 'rgba(255,220,185,0.82)',
foamEdge: 'rgba(255,240,210,0.93)',
bubble: { r:185, g:125, b:80 },
swatch: '#a03818',
},
{
name: 'Porter',
bg: ['#3a2010','#281408','#180c04'],
foam: 'rgba(220,195,155,0.80)',
foamEdge: 'rgba(240,215,175,0.92)',
bubble: { r:160, g:120, b:70 },
swatch: '#4a2a10',
},
{
name: 'Stout',
bg: ['#1a0e06','#120804','#0a0402'],
foam: 'rgba(195,170,130,0.78)',
foamEdge: 'rgba(215,190,150,0.90)',
bubble: { r:130, g:100, b:60 },
swatch: '#2a1608',
},
];
/* ── STATE ────────────────────────────────────────────────── */
let density = 28;
let speedMult = 1.0;
let styleIdx = 0;
let bubbles = [];
let animId = null;
let lastTime = 0;
let spawnAcc = 0;
/* ── CANVAS SETUP ─────────────────────────────────────────── */
const canvas = document.getElementById('bubble-canvas');
const ctx = canvas.getContext('2d');
function resizeCanvas() {
const hero = document.getElementById('hero');
canvas.width = hero.offsetWidth;
canvas.height = hero.offsetHeight;
}
resizeCanvas();
window.addEventListener('resize', () => { resizeCanvas(); buildStreams(); });
/* ── BUBBLE CLASS ─────────────────────────────────────────── */
class Bubble {
constructor() { this.reset(true); }
reset(fromBottom = false) {
const style = STYLES[styleIdx];
this.x = Math.random() * canvas.width;
this.y = fromBottom
? canvas.height + Math.random() * 60
: canvas.height * (0.3 + Math.random() * 0.7);
this.r = 3 + Math.pow(Math.random(), 1.8) * 18; // skew small
this.baseSpeed = (0.4 + Math.random() * 1.2) * speedMult;
this.speed = this.baseSpeed;
this.wobbleAmp = 0.4 + Math.random() * 1.2;
this.wobbleFreq = 0.015 + Math.random() * 0.025;
this.wobbleOff = Math.random() * Math.PI * 2;
this.alpha = 0.25 + Math.random() * 0.55;
this.alphaDecay = 0.0004 + Math.random() * 0.0008;
this.born = performance.now();
this.popping = false;
this.popProgress = 0;
this.c = style.bubble;
// Slight colour variation per bubble
this.cr = this.c.r + Math.round((Math.random()-.5)*30);
this.cg = this.c.g + Math.round((Math.random()-.5)*30);
this.cb = this.c.b + Math.round((Math.random()-.5)*30);
}
update(dt) {
if (this.popping) {
this.popProgress += dt * 0.006;
this.alpha -= dt * 0.008;
this.r += dt * 0.04;
return this.alpha > 0;
}
// Slow down as bubble rises (drag increases near top)
const progress = 1 - (this.y / canvas.height); // 0=bottom, 1=top
const drag = 1 - progress * 0.55;
this.speed = this.baseSpeed * drag * speedMult;
this.y -= this.speed * dt * 0.06;
this.x += Math.sin(this.y * this.wobbleFreq + this.wobbleOff)
* this.wobbleAmp * 0.4;
// Fade in near bottom, fade out near top
if (progress > 0.75) {
this.alpha -= this.alphaDecay * dt * (progress - 0.75) * 8;
}
// Pop near top
if (this.y < canvas.height * 0.08 || this.alpha <= 0.02) {
this.popping = true;
return true;
}
return true;
}
draw() {
if (this.alpha <= 0) return;
const a = Math.max(0, Math.min(1, this.alpha));
ctx.save();
if (this.popping) {
// Pop burst
const pa = Math.max(0, 1 - this.popProgress);
ctx.globalAlpha = pa * 0.6;
ctx.strokeStyle = `rgba(${this.cr},${this.cg},${this.cb},${pa})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.stroke();
// Tiny pop droplets
for (let i = 0; i < 5; i++) {
const ang = (i / 5) * Math.PI * 2;
const dist = this.r * (1 + this.popProgress * 2);
ctx.beginPath();
ctx.arc(
this.x + Math.cos(ang) * dist,
this.y + Math.sin(ang) * dist,
1, 0, Math.PI * 2
);
ctx.fillStyle = `rgba(${this.cr},${this.cg},${this.cb},${pa * .5})`;
ctx.fill();
}
ctx.restore();
return;
}
// Bubble body — radial gradient for glass-sphere look
const grad = ctx.createRadialGradient(
this.x - this.r * 0.3, this.y - this.r * 0.35, this.r * 0.05,
this.x, this.y, this.r
);
grad.addColorStop(0, `rgba(255,255,255,${a * 0.7})`);
grad.addColorStop(0.3, `rgba(${this.cr},${this.cg},${this.cb},${a * 0.35})`);
grad.addColorStop(0.8, `rgba(${this.cr},${this.cg},${this.cb},${a * 0.15})`);
grad.addColorStop(1, `rgba(${this.cr},${this.cg},${this.cb},${a * 0.05})`);
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = grad;
ctx.fill();
// Rim highlight
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255,255,255,${a * 0.55})`;
ctx.lineWidth = 0.8;
ctx.stroke();
// Specular highlight dot
ctx.beginPath();
ctx.arc(
this.x - this.r * 0.28,
this.y - this.r * 0.3,
this.r * 0.22, 0, Math.PI * 2
);
ctx.fillStyle = `rgba(255,255,255,${a * 0.65})`;
ctx.fill();
ctx.restore();
}
}
/* ── ANIMATION LOOP ───────────────────────────────────────── */
function loop(ts) {
const dt = Math.min(ts - lastTime, 50); // cap delta
lastTime = ts;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Spawn new bubbles
spawnAcc += dt;
const spawnInterval = 1000 / (density * speedMult * 0.6);
while (spawnAcc >= spawnInterval) {
spawnAcc -= spawnInterval;
// Spawn multiple per tick for higher density
const count = Math.max(1, Math.floor(density / 20));
for (let i = 0; i < count; i++) bubbles.push(new Bubble());
}
// Update & draw
bubbles = bubbles.filter(b => {
const alive = b.update(dt);
if (alive) b.draw();
return alive;
});
// Cap bubble count
const maxBubbles = density * 12;
if (bubbles.length > maxBubbles) {
bubbles.splice(0, bubbles.length - maxBubbles);
}
animId = requestAnimationFrame(loop);
}
animId = requestAnimationFrame(ts => { lastTime = ts; loop(ts); });
/* ── CARBONATION STREAMS ──────────────────────────────────── */
function buildStreams() {
const el = document.getElementById('streams');
el.innerHTML = '';
const hero = document.getElementById('hero');
const W = hero.offsetWidth;
const count = Math.floor(W / 80);
for (let i = 0; i < count; i++) {
const s = document.createElement('div');
s.className = 'stream';
s.style.left = (5 + Math.random() * 90) + '%';
s.style.width = (0.5 + Math.random()) + 'px';
s.style.animationDuration = (3 + Math.random() * 5) + 's';
s.style.animationDelay = -(Math.random() * 6) + 's';
s.style.opacity = (0.04 + Math.random() * 0.1).toString();
el.appendChild(s);
}
}
buildStreams();
/* ── APPLY STYLE ──────────────────────────────────────────── */
function applyStyle(idx) {
styleIdx = idx;
const s = STYLES[idx];
// Update hero background
const hero = document.getElementById('hero');
hero.style.background = s.bg[0];
const heroBg = document.getElementById('hero-bg');
heroBg.style.background = `
radial-gradient(ellipse 80% 60% at 50% 110%,
rgba(0,0,0,0.4) 0%, transparent 70%),
radial-gradient(ellipse 60% 40% at 30% 20%,
rgba(255,220,100,0.12) 0%, transparent 60%),
linear-gradient(180deg,
${s.bg[2]} 0%,
${s.bg[0]} 40%,
${s.bg[1]} 100%)
`;
// Update foam SVG colour
const foamPath = document.getElementById('foam-path');
if (foamPath) {
foamPath.setAttribute('fill', s.foam);
}
// Update existing bubbles' colour
bubbles.forEach(b => {
b.c = s.bubble;
b.cr = s.bubble.r + Math.round((Math.random()-.5)*30);
b.cg = s.bubble.g + Math.round((Math.random()-.5)*30);
b.cb = s.bubble.b + Math.round((Math.random()-.5)*30);
});
// Update swatch active state
document.querySelectorAll('.swatch').forEach((sw, i) => {
sw.classList.toggle('active', i === idx);
});
}
/* ── BUILD SWATCHES ───────────────────────────────────────── */
const swatchWrap = document.getElementById('swatches');
STYLES.forEach((style, i) => {
const sw = document.createElement('div');
sw.className = 'swatch' + (i === 0 ? ' active' : '');
sw.style.background = style.swatch;
sw.title = style.name;
sw.addEventListener('click', () => applyStyle(i));
swatchWrap.appendChild(sw);
});
/* ── DENSITY CONTROL ──────────────────────────────────────── */
const densitySlider = document.getElementById('density');
const densityVal = document.getElementById('density-val');
densitySlider.addEventListener('input', () => {
density = parseInt(densitySlider.value);
densityVal.textContent = density;
});
/* ── SPEED CONTROL ────────────────────────────────────────── */
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.speed-btn')
.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
speedMult = parseFloat(btn.dataset.speed);
// Reset bubble speeds
bubbles.forEach(b => {
b.baseSpeed = (0.4 + Math.random() * 1.2) * speedMult;
});
});
});
/* ── INIT ─────────────────────────────────────────────────── */
applyStyle(0);
// Pre-populate some bubbles so screen isn't empty on load
for (let i = 0; i < density * 4; i++) {
const b = new Bubble();
b.y = Math.random() * canvas.height;
bubbles.push(b);
}
})();
</script>
</body>
</html>
Live Preview