1. Mechanisme — kopieer 1-op-1, geen styling-keuzes
// Mechanisme: footer-noise-bg-quiet (canvas2D simplex noise, sub-2% ink)
import { createNoise2D } from 'https://esm.sh/simplex-noise@4.0.3';
import gsap from 'https://esm.sh/gsap@3.12.5';
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const cv = document.querySelector('.noise-canvas-quiet');
if (cv) {
const ctx = cv.getContext('2d');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const resize = () => {
cv.width = Math.floor(cv.offsetWidth * dpr);
cv.height = Math.floor(cv.offsetHeight * dpr);
};
resize();
window.addEventListener('resize', resize);
const noise2D = createNoise2D();
const draw = (t) => {
const w = cv.width, h = cv.height;
const img = ctx.createImageData(w, h);
const data = img.data;
const scale = 0.012;
for (let y = 0; y < h; y += 2) {
for (let x = 0; x < w; x += 2) {
const n = (noise2D(x * scale, y * scale + t) + 1) * 0.5;
const v = (n * 255) | 0;
const a = 5; // sub-2% ink
for (let oy = 0; oy < 2; oy++) {
for (let ox = 0; ox < 2; ox++) {
const i = ((y + oy) * w + (x + ox)) * 4;
data[i] = data[i+1] = data[i+2] = v;
data[i+3] = a;
}
}
}
}
ctx.putImageData(img, 0, 0);
};
if (reduce) {
draw(0);
} else {
let t = 0;
(function tick(){
t += 0.0015;
draw(t);
requestAnimationFrame(tick);
})();
}
}
if (!reduce) {
gsap.from('.fnq-reveal', {
autoAlpha: 0,
y: 12,
duration: 1.2,
ease: 'power2.out',
stagger: 0.07
});
} 2. Skeleton — DOM + class-namen, mag herschikken
<!-- Skeleton: footer-noise-bg-quiet -->
<footer class="fnq">
<canvas class="noise-canvas-quiet" aria-hidden="true"></canvas>
<div class="fnq-inner">
<p class="fnq-eyebrow fnq-reveal">Studio note</p>
<p class="fnq-manifest fnq-reveal">A quiet practice. Made with care, slowly, for work that lasts.</p>
<ul class="fnq-links fnq-reveal">
<li><a href="#">Work</a></li>
<li><a href="#">Studio</a></li>
<li><a href="#">Contact</a></li>
<li><a href="#">Index</a></li>
</ul>
<p class="fnq-meta fnq-reveal">© 2026</p>
</div>
</footer> 3. Styling-template — verplicht eigen invulling per merk
/* Styling: footer-noise-bg-quiet */
.fnq {
--bg: #F6F4EF;
--fg: #161513;
--muted: #6B6862;
position: relative;
background: var(--bg);
color: var(--fg);
padding: clamp(5rem, 12vw, 10rem) clamp(1.5rem, 4vw, 4rem);
min-height: 70vh;
overflow: hidden;
}
.noise-canvas-quiet {
position: absolute; inset: 0;
width: 100%; height: 100%;
pointer-events: none;
}
.fnq-inner { position: relative; max-width: 540px; }
.fnq-eyebrow {
font-family: 'Archivo', sans-serif;
font-size: 0.75rem; letter-spacing: 0.18em;
text-transform: uppercase; color: var(--muted);
margin: 0 0 2rem;
}
.fnq-manifest {
font-family: 'Fraunces', Georgia, serif;
font-weight: 300; font-size: clamp(1.5rem, 2.4vw, 2rem);
line-height: 1.35; letter-spacing: -0.01em;
margin: 0 0 3.5rem;
}
.fnq-links {
list-style: none; padding: 0; margin: 0 0 2rem;
display: flex; flex-wrap: wrap; gap: 1.5rem;
}
.fnq-links a {
font-family: 'Archivo', sans-serif;
font-size: 0.875rem; color: var(--fg);
text-decoration: none; border-bottom: 1px solid transparent;
padding-bottom: 2px; transition: border-color .3s ease;
}
.fnq-links a:hover { border-color: var(--fg); }
.fnq-meta {
font-family: 'Archivo', sans-serif;
font-size: 0.75rem; color: var(--muted);
margin: 0;
}