*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root { --bg: #0a0c14; --text: #f0f4fb; --muted: #8a95a8; --accent: #86e8ff; --border: #263249; }
.hidden-svg { position: absolute; width: 0; height: 0; }
body { background: var(--bg); color: var(--text); font-family: 'Inter', 'SF Pro Display', system-ui, sans-serif; min-height: 100vh; }
/* Overlays */
.overlay { position: fixed; inset: 0; pointer-events: none; z-index: 100; }
.noise-overlay {
filter: url(#noise-filter);
opacity: 0.06;
mix-blend-mode: overlay;
animation: noise-shift 0.5s steps(4) infinite;
}
@keyframes noise-shift {
0% { transform: translate(0, 0); }
25% { transform: translate(-2px, 1px); }
50% { transform: translate(1px, -1px); }
75% { transform: translate(-1px, 2px); }
}
.grain-canvas-overlay { opacity: 0.08; mix-blend-mode: overlay; }
.grain-canvas-overlay canvas { width: 100%; height: 100%; image-rendering: pixelated; }
.scanlines-overlay {
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.15) 2px, rgba(0,0,0,0.15) 4px);
opacity: 0; /* off by default */
}
.vignette-overlay {
background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.6) 100%);
opacity: 1;
}
.overlay.off { opacity: 0 !important; }
.reduced-motion .noise-overlay { animation: none; }
/* Page */
.page { position: relative; z-index: 2; max-width: 800px; margin: 0 auto; padding: 5rem 2rem 4rem; }
.header { text-align: center; margin-bottom: 2rem; }
.eyebrow { display: inline-block; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.18em; color: var(--accent); margin-bottom: 1rem; }
h1 { font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 700; letter-spacing: -0.02em; }
.subtitle { font-size: clamp(0.9rem, 2vw, 1.05rem); color: var(--muted); max-width: 480px; margin: 0.75rem auto 0; line-height: 1.6; }
/* Controls */
.controls { display: flex; justify-content: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 2.5rem; }
.toggle {
display: flex; align-items: center; gap: 0.5rem;
padding: 0.45rem 0.9rem; border-radius: 8px;
background: rgba(134,232,255,0.05); border: 1px solid var(--border);
font-size: 0.78rem; font-weight: 600; color: var(--muted); cursor: pointer;
transition: border-color 0.2s;
}
.toggle:has(input:checked) { border-color: var(--accent); color: var(--accent); }
.toggle input { accent-color: var(--accent); }
/* Sample content */
.sample-hero {
aspect-ratio: 16/7; border-radius: 18px; margin-bottom: 1rem;
background: linear-gradient(135deg, hsl(var(--hue,200) 45% 12%), hsl(var(--hue,200) 60% 22%), hsl(calc(var(--hue,200)+40) 50% 16%));
display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center;
padding: 2rem;
}
.sample-hero h2 { font-size: clamp(1.5rem, 3vw, 2.2rem); font-weight: 700; margin-bottom: 0.5rem; }
.sample-hero p { font-size: 0.9rem; color: rgba(255,255,255,0.6); }
.sample-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
.sample-card {
aspect-ratio: 1.2; border-radius: 14px;
background: linear-gradient(135deg, hsl(var(--hue,200) 40% 12%), hsl(var(--hue,200) 55% 20%));
border: 1px solid hsl(var(--hue,200) 30% 20%);
display: flex; align-items: center; justify-content: center;
font: 600 0.85rem/1 'Inter', system-ui, sans-serif;
color: hsl(var(--hue,200) 60% 75%);
}
.btn-back {
display: inline-block; padding: 0.7rem 2rem; border-radius: 999px;
border: 1px solid rgba(134,232,255,0.3); color: var(--accent); text-decoration: none;
font: 600 0.85rem/1 'Inter', system-ui, sans-serif; transition: all 0.25s;
}
.btn-back:hover { background: rgba(134,232,255,0.08); border-color: var(--accent); }
@media (max-width: 640px) {
.sample-row { grid-template-columns: 1fr; }
.page { padding: 3rem 1rem 3rem; }
}
if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
initDemoShell({ title: 'Noise & Grain Overlay', category: 'css-canvas', tech: ['svg-filter', 'canvas', 'css'] });
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add('reduced-motion');
// Refs
const noiseOverlay = document.getElementById('noise-overlay');
const scanlinesOverlay = document.getElementById('scanlines-overlay');
const vignetteOverlay = document.querySelector('.vignette-overlay');
const grainCanvas = document.getElementById('grain-canvas');
const grainCtx = grainCanvas.getContext('2d');
// Canvas grain — tiny resolution, scaled up with pixelated rendering
const GRAIN_SIZE = 128;
grainCanvas.width = GRAIN_SIZE;
grainCanvas.height = GRAIN_SIZE;
let grainRAF;
let grainFrame = 0;
function renderGrain() {
grainFrame++;
// Only update every 3 frames for flickering effect
if (grainFrame % 3 === 0) {
const imageData = grainCtx.createImageData(GRAIN_SIZE, GRAIN_SIZE);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = Math.random() * 255;
data[i] = v; // R
data[i+1] = v; // G
data[i+2] = v; // B
data[i+3] = 255; // A
}
grainCtx.putImageData(imageData, 0, 0);
}
grainRAF = requestAnimationFrame(renderGrain);
}
if (!reduced) {
renderGrain();
}
// Toggle controls
function setupToggle(id, overlay) {
const checkbox = document.getElementById(id);
const update = () => overlay.classList.toggle('off', !checkbox.checked);
checkbox.addEventListener('change', update);
update();
}
setupToggle('tog-noise', noiseOverlay);
setupToggle('tog-grain', grainCanvas.parentElement);
setupToggle('tog-scanlines', scanlinesOverlay);
setupToggle('tog-vignette', vignetteOverlay);
// Motion preference
window.addEventListener('motion-preference', (e) => {
reduced = e.detail.reduced;
document.documentElement.classList.toggle('reduced-motion', reduced);
if (reduced) {
cancelAnimationFrame(grainRAF);
} else {
renderGrain();
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Noise & Grain Overlay — stealthisdesign</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- SVG noise filter (hidden) -->
<svg class="hidden-svg">
<filter id="noise-filter">
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" id="turbulence"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
</svg>
<!-- Overlays -->
<div class="overlay noise-overlay" id="noise-overlay"></div>
<div class="overlay grain-canvas-overlay"><canvas id="grain-canvas" aria-hidden="true"></canvas></div>
<div class="overlay scanlines-overlay" id="scanlines-overlay"></div>
<div class="overlay vignette-overlay"></div>
<!-- Content -->
<main class="page">
<header class="header">
<span class="eyebrow">Demo 18</span>
<h1>Noise & Grain</h1>
<p class="subtitle">Multiple overlay techniques composited on top of content: SVG noise filter, canvas grain, scanlines, and vignette.</p>
</header>
<div class="controls">
<label class="toggle"><input type="checkbox" id="tog-noise" checked><span>SVG Noise</span></label>
<label class="toggle"><input type="checkbox" id="tog-grain" checked><span>Canvas Grain</span></label>
<label class="toggle"><input type="checkbox" id="tog-scanlines"><span>Scanlines</span></label>
<label class="toggle"><input type="checkbox" id="tog-vignette" checked><span>Vignette</span></label>
</div>
<div class="sample-content">
<div class="sample-hero" style="--hue: 220;">
<h2>Cinematic Texture</h2>
<p>Film grain adds warmth and analog character to digital interfaces.</p>
</div>
<div class="sample-row">
<div class="sample-card" style="--hue: 280;">Visual Depth</div>
<div class="sample-card" style="--hue: 340;">Atmosphere</div>
<div class="sample-card" style="--hue: 50;">Nostalgia</div>
</div>
</div>
<a href="/" class="btn-back">Back to Showcase</a>
</main>
<script src="script.js"></script>
</body>
</html>