Grain-/Noise-Überlagerung
Eine kleine feTurbulence-Kachel legt Korn über Verlauf, Bild und Schrift und macht die digitale Fläche analog und greifbar.
Ganze Flächen, Verläufe und Bilder bekommen analoges Korn: statisch, Filmkorn oder Grainy Gradient, mit Vergleichslinie (Unterumschalter Korn).
Eine feine Rauschtextur liegt als Ebene über Flächen, Verläufen, Fotos oder der ganzen Seite. Das Korn entsteht mit feTurbulence (fractalNoise) als kleine, nahtlose Kachel von 128 bis 256 px. Gängig sind drei Formen: ein statisches Overlay mit mix-blend-mode overlay oder soft-light, animiertes Filmkorn, bei dem die Kachel alle 80 bis 120 ms springt, und der Grainy Gradient, bei dem Rauschen unter einem Verlauf per contrast und brightness hart gemacht wird.
Perfekt glatte digitale Flächen wirken steril. Korn bricht diese Glätte, verhindert sichtbare Stufen (Banding) in Verläufen und gibt Flächen, Glas und Fotos einen analogen, fast stofflichen Charakter.
Wann einsetzen
Bei großen Farbflächen, Verläufen und dunklen Seiten. Passt zu Editorial-, Fashion- und Studio-Auftritten. Gut auch als letzter Schliff auf Shader- oder Mesh-Hintergründen.
Wann nicht
Wenn kleine, feine Schrift auf hellem Grund steht und das Korn sie sichtbar anraut. Animiertes Korn nicht großflächig auf schwachen Geräten.
Varianten
- Statisches Overlay über der ganzen Seite (body::after)
- Animiertes Filmkorn: Kachel springt per steps(1) alle 80 bis 120 ms
- Grainy Gradient: Rauschen unter Verlauf, contrast(170%) brightness(1000%), Farbe per multiply
- Korn im Shader, z. B. als letzter Schritt eines Mesh-Gradients
- Korn nur auf Bildern oder einzelnen Flächen statt global
- Korn auf Button-Flächen: Kachel per ::before über einem Verlauf (soft-light oder multiply), Text darüber, flimmert nur bei Hover
Umsetzung
Die Kachel als SVG-Data-URL oder kleines PNG/WebP anlegen und als background-image mit fester Größe kacheln. So rendert der Browser das Rauschen einmal; feTurbulence live auf großer Fläche ist teuer. Die Ebene liegt mit position fixed bzw. absolute, inset 0 und pointer-events none über dem Inhalt, mit mix-blend-mode overlay und opacity 0.04 bis 0.9 je nach Alpha der Kachel. Filmkorn über transform auf einem übergroßen Pseudo-Element bewegen, nicht über background-position. Für den Grainy Gradient braucht der Container isolation: isolate, damit multiply nur innen mischt. Blink und WebKit mischen mix-blend-mode leicht unterschiedlich, deshalb in beiden prüfen. Im Dark Mode die Deckkraft etwas erhöhen.
Robust umsetzen
Telefon. Statisches Korn ist unproblematisch. Animiertes Korn auf Touch-Geräten abschalten.
Ohne Bewegung. Animiertes Korn anhalten, statisches Korn behalten.
Barrierefrei. Textkontrast über dem Korn prüfen. Die Korn-Ebene ist rein dekorativ: aria-hidden="true" und pointer-events: none. Auf Buttons immer eine deckende Grundfarbe als Rückfall setzen und den Kontrast an der hellsten und an der dunkelsten Stelle des Verlaufs prüfen.
Code
<!DOCTYPE html>
<!--
Grain-/Noise-Überlagerung
Fundus · Glas & Material · https://cecodesign.de/lab/fundus/glas-material/grain-noise-overlay/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Grain-/Noise-Überlagerung · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
:root {
--grund: #140e10; --text: #f5ece0; --leise: rgba(245, 236, 224, .62); --linie: rgba(245, 236, 224, .2);
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out: Auftritt */
--ui: 150ms;
/* Kachel A/B (Arc): feTurbulence fractalNoise 0.9, 2 Oktaven, nahtlos, 160 × 160, Graustufen, Alpha 0.55 */
--korn: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0.55'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
/* Kachel C (CSS-Tricks): fractalNoise 0.65, 3 Oktaven, 256 × 256, unverändert */
--korn-roh: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='256' height='256'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
/* Die Fläche: ein Abendlicht-Verlauf, wie geschaffen für Banding */
--verlauf:
radial-gradient(115% 85% at 88% 108%, #f7c27a 0%, #ec8746 20%, #c9473a 40%, #7d2338 60%, rgba(125, 35, 56, 0) 82%),
radial-gradient(70% 60% at 6% -4%, #3a4c86 0%, rgba(58, 76, 134, 0) 72%),
#140e10;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { overflow-x: hidden; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.serif { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; letter-spacing: -.02em; }
a { color: inherit; text-decoration: none; }
button { font: inherit; letter-spacing: inherit; text-transform: inherit; }
/* ---------- Bühne ---------- */
.buehne { position: relative; min-height: 100svh; overflow: hidden; isolation: isolate; touch-action: pan-y; --teil: 50%; }
.ebene { position: absolute; inset: 0; pointer-events: none; }
.flaeche { background: var(--verlauf); z-index: 0; }
/* Alles „mit Korn“ wird rechts der Trennlinie gezeigt */
.mit { clip-path: inset(0 0 0 var(--teil)); }
/* A + B: Korn-Ebene über allem (wie body::after), Overlay-Blend */
.korn { z-index: 3; overflow: hidden; mix-blend-mode: overlay; opacity: .9; transition: opacity var(--ui) linear; }
.korn::before { content: ''; position: absolute; inset: -160px; background: var(--korn); background-size: 160px 160px; }
/* B: Filmkorn – die Kachel springt alle 100 ms an eine neue Stelle */
[data-variante='filmkorn'] .korn::before { animation: flimmern 800ms steps(1) infinite; }
@keyframes flimmern {
0% { transform: translate(0, 0); }
12.5% { transform: translate(-37px, 61px); }
25% { transform: translate(53px, -22px); }
37.5% { transform: translate(-71px, -48px); }
50% { transform: translate(18px, 79px); }
62.5% { transform: translate(84px, 33px); }
75% { transform: translate(-12px, -83px); }
87.5% { transform: translate(-59px, 14px); }
}
/* C: Grainy Gradient – Rauschen unter einem Verlauf, hart gemacht, dann farbig multipliziert */
.riso { z-index: 1; isolation: isolate; opacity: 0; transition: opacity var(--ui) linear; }
[data-variante='grainy'] .riso { opacity: 1; }
.riso__rauschen {
position: absolute; inset: 0;
background:
radial-gradient(115% 85% at 88% 108%, rgba(20, 14, 16, 0) 16%, rgba(20, 14, 16, .6) 46%, #140e10 64%, rgba(20, 14, 16, 0) 90%),
var(--korn-roh);
background-size: auto, 256px 256px;
filter: contrast(170%) brightness(1000%);
}
.riso__farbe { position: absolute; inset: 0; background: var(--verlauf); mix-blend-mode: multiply; }
/* Inhalt */
.inhalt { position: relative; z-index: 2; min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; padding: clamp(20px, 3.4vw, 44px); gap: 24px; }
.kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.marke { font-size: 17px; font-weight: 600; letter-spacing: -.02em; display: flex; align-items: center; gap: 10px; }
.marke i { width: 12px; height: 12px; border-radius: 50%; background: var(--text); box-shadow: inset -3px -2px 0 rgba(20,14,16,.35); }
.nav { display: flex; gap: clamp(16px, 2.6vw, 34px); }
.nav a { color: var(--text); opacity: .78; transition: opacity var(--ui) linear; }
.nav a:hover { opacity: 1; }
.mitte { display: grid; grid-template-columns: minmax(0, 1fr) clamp(220px, 24vw, 340px); align-items: end; gap: clamp(24px, 4vw, 64px); }
.kicker { margin-bottom: clamp(16px, 2vw, 26px); color: var(--text); opacity: .8; }
.titel { font-size: clamp(54px, 9.4vw, 152px); font-weight: 600; letter-spacing: -.05em; line-height: .88; }
.titel .serif { font-size: 1.08em; display: inline-block; padding-right: .06em; }
.lead { max-width: 34ch; margin-top: clamp(18px, 2.4vw, 30px); font-size: clamp(16px, 1.25vw, 19px); line-height: 1.45; color: rgba(245, 236, 224, .84); }
.karte { display: grid; gap: 10px; }
.karte__bild { aspect-ratio: 4 / 5; border-radius: 4px; overflow: hidden; background: #2a1c1c; }
.karte__bild img { display: block; width: 100%; height: 100%; object-fit: cover; }
.karte figcaption { display: flex; justify-content: space-between; gap: 12px; }
.karte figcaption b { font-weight: 500; color: var(--text); }
.fuss { display: flex; justify-content: space-between; align-items: end; gap: 20px; flex-wrap: wrap; }
.werte { line-height: 1.6; min-height: 4.8em; display: flex; flex-direction: column; justify-content: flex-end; }
.werte span { display: block; }
.werte b { font-weight: 500; color: var(--text); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { color: var(--leise); background: rgba(20, 14, 16, .28); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: background var(--ui) linear, color var(--ui) linear; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.modi button:hover { color: var(--text); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.modi button:focus-visible, .regler:focus-visible + .linie .griff { outline: 2px solid var(--text); outline-offset: 3px; }
/* Trennlinie: links ohne, rechts mit Korn */
.linie { position: absolute; top: 0; bottom: 0; left: var(--teil); z-index: 4; width: 0; pointer-events: none; }
.linie::before { content: ''; position: absolute; top: 0; bottom: 0; left: -.5px; width: 1px; background: rgba(245, 236, 224, .7); }
.griff { position: absolute; top: 50%; left: 0; translate: -50% -50%; width: 44px; height: 44px; border-radius: 50%; border: 1px solid rgba(245, 236, 224, .75); background: rgba(20, 14, 16, .35); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); display: grid; place-items: center; pointer-events: auto; cursor: ew-resize; touch-action: none; }
.griff svg { width: 18px; height: 18px; }
.seite { position: absolute; top: clamp(88px, 13vh, 124px); white-space: nowrap; color: var(--text); padding: 6px 9px 5px; border-radius: 999px; background: rgba(20, 14, 16, .42); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
.seite--ohne { right: 10px; }
.seite--mit { left: 10px; }
.regler { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
/* Auftritt beim Laden */
.auftritt { opacity: 0; translate: 0 28px; }
.da .auftritt { opacity: 1; translate: 0 0; transition: opacity 1.2s var(--kurve), translate 1.2s var(--kurve); transition-delay: calc(var(--i, 0) * 90ms); }
/* ---------- Rezepte unter der Bühne ---------- */
.rezepte { padding: clamp(56px, 9vw, 120px) clamp(20px, 3.4vw, 44px); display: grid; gap: clamp(28px, 4vw, 48px); }
.rezepte__kopf { display: flex; justify-content: space-between; align-items: end; gap: 24px; flex-wrap: wrap; }
.rezepte h2 { font-size: clamp(32px, 4.6vw, 64px); font-weight: 600; letter-spacing: -.045em; line-height: .95; max-width: 14ch; }
.rezepte__kopf p { max-width: 42ch; color: rgba(245, 236, 224, .78); line-height: 1.5; }
.proben { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 2vw, 24px); }
.probe { display: grid; gap: 12px; }
.probe__feld { position: relative; aspect-ratio: 4 / 3; border-radius: 4px; overflow: hidden; isolation: isolate; background: var(--verlauf); background-size: 180% 180%; background-position: 80% 90%; }
.probe__feld .korn, .probe__feld .riso { position: absolute; inset: 0; }
.probe__feld .riso { opacity: 1; }
.probe__feld .riso__rauschen, .probe__feld .riso__farbe { background-size: 180% 180%, 256px 256px; background-position: 80% 90%, 0 0; }
.probe__feld .riso__farbe { background-size: 180% 180%; background-position: 80% 90%; }
.probe__feld span { position: absolute; left: 14px; bottom: 12px; z-index: 5; color: var(--text); }
.probe dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; }
.probe dt { color: var(--leise); }
.probe dd { color: var(--text); text-transform: none; letter-spacing: .04em; }
.probe h3 { font-size: 20px; font-weight: 600; letter-spacing: -.02em; }
/* ---------- Zustände ---------- */
.vorschau body { overflow: hidden; }
.vorschau .rezepte { display: none; }
.vorschau .regler { display: none; }
/* Reduzierte Bewegung: Korn steht, statisches bleibt; Auftritt nur kurz eingeblendet */
.reduziert .korn::before { animation: none !important; }
.reduziert .auftritt { translate: none; }
.reduziert .da .auftritt { transition: opacity .25s linear; transition-delay: 0s; }
/* Touch: animiertes Korn abschalten (Steckbrief), Linie per Ziehen */
@media (hover: none) {
.korn::before { animation: none !important; }
.hinweis-touch { display: inline; }
}
.hinweis-touch { display: none; }
@media (max-width: 860px) {
.mitte { grid-template-columns: 1fr; align-items: start; }
.karte { width: min(62%, 260px); justify-self: end; grid-row: 1; }
.karte__bild { aspect-ratio: 5 / 4; }
.nav { display: none; }
.breit { display: none; }
.proben { grid-template-columns: 1fr; }
.probe { grid-template-columns: 42% 1fr; align-items: start; gap: 16px; }
}
@media (max-width: 860px) and (hover: none), (max-width: 520px) {
.griff { width: 40px; height: 40px; top: 34%; }
.seite { top: calc(34% + 28px); }
.titel { font-size: clamp(50px, 15vw, 80px); }
.karte { width: 58%; }
}
/* Zugang: 320 px ohne Querscrollen */
@media (max-width: 360px) { .probe { grid-template-columns: minmax(0, 1fr); } .probe dd { overflow-wrap: anywhere; } }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
/* ---------- Varianten-Umschalter (Regel 13) ---------- */
.welten { display: flex; gap: 4px; flex-wrap: wrap; }
.welten button { color: var(--leise); background: rgba(20, 14, 16, .28); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; }
.welten button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.welten button:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.fuss__rechts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
html:not([data-v="button"]) [data-welt="button"], html[data-v="button"] [data-welt="basis"] { display: none !important; }
/* ---------- Variante „button“ (Rezept d2c-produkt-zuerst): Körnung auf Button-Flächen ----------
Dieselbe Kachel (fractalNoise 0.9, 2 Oktaven, 160 px) liegt als ::before über einem Verlauf im Button.
isolation: isolate begrenzt den Blend auf den Button, der Text liegt darüber und bleibt glatt. */
html[data-v="button"] {
--grund: #f5f4f1; --text: #212121; --leise: #5f5f5f; --linie: rgba(33, 33, 33, .16);
--d-kachel: #ece9e2; --d-gruen: #10714e; --d-kobalt: #0137ba;
background: var(--grund); color: var(--text);
}
html[data-v="button"] .mono { color: var(--leise); }
.d-seite { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
.d-kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px clamp(20px, 3.4vw, 44px); border-bottom: 1px solid var(--linie); }
.d-marke { font: 400 26px/1 'Young Serif', Georgia, serif; letter-spacing: -.01em; }
.d-nav { display: flex; gap: clamp(16px, 2.4vw, 30px); font-size: 15px; }
.d-nav a:hover { text-decoration: underline; text-underline-offset: .25em; }
.d-korb { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; font-size: 15px; font-weight: 500; }
.d-korb b { display: inline-grid; place-items: center; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px; background: var(--text); color: var(--grund); font-size: 12px; font-variant-numeric: tabular-nums; }
.d-produkt { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .92fr); gap: clamp(24px, 4.5vw, 72px); align-items: center; padding: clamp(24px, 4vw, 56px) clamp(20px, 3.4vw, 44px); }
.d-bild { margin: 0; border-radius: 6px; overflow: hidden; background: #1f51ca; aspect-ratio: 4 / 5; max-height: 76svh; }
.d-bild img { display: block; width: 100%; height: 100%; object-fit: cover; }
.d-kauf { display: grid; gap: 18px; max-width: 470px; }
.d-kauf h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.25rem)/1.05 'Young Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
.d-noten { font-size: 1.0625rem; line-height: 1.55; color: #3a3a3a; }
.d-preis { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 14px; }
.d-preis b { font-size: 1.5rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.d-preis span { font-size: .875rem; color: var(--leise); }
.d-groessen { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0; min-width: 0; }
.d-groessen legend { width: 100%; margin-bottom: 8px; font: 600 .75rem/1 'Instrument Sans', sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.d-groesse { position: relative; display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px; border: 1.5px solid var(--linie); border-radius: 999px; cursor: pointer; font-size: .9375rem; transition: border-color 150ms linear; }
.d-groesse input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.d-groesse:has(:checked) { border-color: var(--text); box-shadow: inset 0 0 0 1px var(--text); }
.d-groesse:has(:focus-visible) { outline: 2px solid var(--text); outline-offset: 3px; }
.d-groesse small { color: var(--leise); font-size: .8125rem; }
.d-knoepfe { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 10px; }
/* Der körnige Button: deckende Grundfarbe (Rückfall), Verlauf 200 %, Korn per ::before, Text darüber */
.d-knopf {
position: relative; isolation: isolate; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; gap: 10px;
min-height: 56px; padding: 0 22px; border: 0; border-radius: 999px; cursor: pointer;
font: 600 1rem/1.15 'Instrument Sans', sans-serif; letter-spacing: .005em; text-transform: none; text-align: center;
background-color: var(--d-fest); background-image: var(--d-verlauf); background-size: 200% 100%; background-position: 0% 50%;
transition: background-position 600ms var(--kurve), transform 150ms linear, box-shadow 150ms linear;
}
.d-knopf::before {
content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
background: var(--korn); background-size: 160px 160px; mix-blend-mode: var(--d-blend); opacity: var(--d-korn);
}
.d-knopf > * { position: relative; }
.d-knopf svg { width: 16px; height: 16px; flex: none; }
.d-knopf.kauf { --d-fest: #15183a; --d-verlauf: linear-gradient(105deg, #121212 0%, #15183a 35%, #0b2a8f 70%, #0137ba 100%); --d-blend: soft-light; --d-korn: .95; color: #fff; }
.d-knopf.probe { --d-fest: #e3dbd0; --d-verlauf: linear-gradient(105deg, #ece9e2 0%, #e3dbd0 50%, #d6c6b2 100%); --d-blend: multiply; --d-korn: .32; color: var(--text); box-shadow: inset 0 0 0 1.5px rgba(33, 33, 33, .22); }
.d-knopf:is(:hover, .an):not([aria-disabled="true"]) { background-position: 100% 50%; }
/* Filmkorn nur, solange der Zeiger darauf liegt: Kachel springt alle 100 ms (steps(1), 8 Lagen), per background-position statt transform */
.d-knopf:is(:hover, .an):not([aria-disabled="true"])::before { animation: d-korn 800ms steps(1) infinite; }
@keyframes d-korn { 0% { background-position: 0 0; } 12.5% { background-position: -37px 61px; } 25% { background-position: 53px -22px; } 37.5% { background-position: -71px -48px; } 50% { background-position: 18px 79px; } 62.5% { background-position: 84px 33px; } 75% { background-position: -12px -83px; } 87.5% { background-position: -59px 14px; } }
.d-knopf:focus-visible, .d-knopf.fokus { outline: 2px solid var(--text); outline-offset: 3px; }
.d-knopf:active:not([aria-disabled="true"]), .d-knopf.druck { transform: scale(.98); }
.d-knopf.probe:is(:hover, .an) { box-shadow: inset 0 0 0 1.5px rgba(33, 33, 33, .45); }
.d-knopf[aria-disabled="true"] { --d-fest: #dedcd6; --d-verlauf: none; --d-korn: .18; --d-blend: multiply; color: #4d4d4d; cursor: not-allowed; }
.d-knopf[aria-disabled="true"] svg { display: none; }
.d-knopf[data-laedt] { cursor: progress; background-position: 100% 50%; }
.d-knopf[data-laedt]::before { animation: d-korn 800ms steps(1) infinite; }
.d-kreis { display: none; width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, .35); border-top-color: #fff; animation: d-drehen 800ms linear infinite; }
.d-knopf[data-laedt] .d-kreis { display: block; }
.d-knopf[data-laedt] svg { display: none; }
@keyframes d-drehen { to { rotate: 360deg; } }
.d-status { min-height: 1.5em; font-size: .9375rem; line-height: 1.5; }
.d-status:empty { display: none; }
.d-status a { text-decoration: underline; text-underline-offset: .2em; font-weight: 500; }
.d-status svg { width: 16px; height: 16px; margin-right: 6px; vertical-align: -3px; }
.d-fakten { display: grid; gap: 6px; padding: 16px 0 0; border-top: 1px solid var(--linie); list-style: none; font-size: .9375rem; color: #3a3a3a; }
.d-fakten li::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: 10px; border-radius: 50%; background: var(--d-gruen); vertical-align: 2px; }
.d-pult { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px 20px; padding: 14px clamp(20px, 3.4vw, 44px) 22px; border-top: 1px solid var(--linie); }
html[data-v="button"] .welten button { background: transparent; color: var(--leise); }
html[data-v="button"] .welten button[aria-pressed='true'] { background: var(--text); color: var(--grund); }
.d-werte { max-width: 80ch; text-transform: none; letter-spacing: .03em; line-height: 1.6; }
.d-werte b { color: var(--text); font-weight: 500; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform .8s var(--kurve); }
.vorfuehrung .zeiger.an { display: block; }
.reduziert .d-knopf, .reduziert .d-knopf::before { transition-duration: .01ms !important; animation: none !important; }
.reduziert .d-kreis { animation-duration: 2400ms !important; }
@media (hover: none) { .d-knopf:hover:not(.an)::before { animation: none; } .d-knopf:hover:not(.an) { background-position: 0% 50%; } }
@media (forced-colors: active) { .d-knopf { border: 2px solid ButtonText; } .d-knopf[aria-disabled="true"] { border-color: GrayText; color: GrayText; } .d-groesse { border-color: ButtonText; } }
@media (max-width: 860px) {
.d-produkt { grid-template-columns: minmax(0, 1fr); }
.d-bild { aspect-ratio: 5 / 4; max-height: none; }
.d-nav { display: none; }
}
@media (max-width: 420px) { .d-knoepfe { grid-template-columns: minmax(0, 1fr); } }
.vorschau .d-fakten, .vorschau .d-pult, .vorschau .d-noten { display: none; }
.vorschau .d-seite { min-height: 0; }
</style>
</head>
<body>
<section class="buehne" data-buehne data-variante="statisch" aria-label="Grain-Überlagerung im Vergleich" data-welt="basis">
<div class="ebene flaeche"></div>
<div class="ebene riso mit" aria-hidden="true"><div class="riso__rauschen"></div><div class="riso__farbe"></div></div>
<div class="inhalt">
<header class="kopf auftritt">
<a class="marke" href="#"><i></i>Atelier Abendrot</a>
<nav class="nav mono" aria-label="Hauptnavigation"><a href="#">Arbeiten</a><a href="#">Studio</a><a href="#">Journal</a><a href="#">Kontakt</a></nav>
</header>
<div class="mitte">
<div>
<p class="mono kicker auftritt" style="--i:1">Gestaltungsbüro · Köln-Ehrenfeld<span class="breit"> · seit 2014</span></p>
<h1 class="titel auftritt" style="--i:1">Flächen<br>mit <span class="serif">Körnung.</span></h1>
<p class="lead auftritt" style="--i:2">Wir gestalten Marken, Magazine und Räume, die man anfassen möchte. Editorial, Erscheinungsbild und Ausstellung mit Liebe zum Material.</p>
</div>
<figure class="karte auftritt" style="--i:3">
<div class="karte__bild"><picture><source media="(max-width: 860px)" srcset="bilder/keramik-stillleben.jpg"><img src="bilder/keramik-stillleben-hoch.jpg" alt="Keramik-Stillleben: helle Flasche, schwarze Kugelvase, gelber Becher und Schalen auf Sockeln vor Terrakotta" width="800" height="1000"></picture></div>
<figcaption class="mono"><span><b>Gebrannt 07</b></span><span>Ausstellung</span></figcaption>
</figure>
</div>
<div class="fuss auftritt" style="--i:4">
<p class="werte mono" aria-live="polite" data-werte></p>
<div class="fuss__rechts">
<div class="welten mono" role="group" aria-label="Variante">
<button type="button" data-v-wahl="basis" aria-pressed="true">Fläche</button>
<button type="button" data-v-wahl="button" aria-pressed="false">Button</button>
</div>
<div class="modi mono" role="group" aria-label="Korn">
<button type="button" data-v="statisch" aria-pressed="true">Statisch</button>
<button type="button" data-v="filmkorn" aria-pressed="false">Filmkorn</button>
<button type="button" data-v="grainy" aria-pressed="false">Grainy Gradient</button>
</div>
</div>
</div>
</div>
<div class="ebene korn mit" aria-hidden="true"></div>
<input class="regler" type="range" min="0" max="100" value="50" aria-label="Vergleich: links ohne Korn, rechts mit Korn" data-regler>
<div class="linie" aria-hidden="true">
<div class="griff"><svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M6.5 4.5 2 9l4.5 4.5M11.5 4.5 16 9l-4.5 4.5"/></svg></div>
<span class="seite seite--ohne mono">Ohne Korn</span>
<span class="seite seite--mit mono">Mit Korn</span>
</div>
</section>
<section class="rezepte" aria-labelledby="rezepte-titel" data-welt="basis">
<div class="rezepte__kopf">
<h2 id="rezepte-titel">Drei Rezepte, ein Rauschen.</h2>
<p>Jede Variante beginnt mit derselben Zutat: feTurbulence als kleine, nahtlose Kachel. Gerendert wird sie einmal, danach liegt sie nur noch als Bild über der Fläche.</p>
</div>
<div class="proben">
<article class="probe" data-variante="statisch">
<div class="probe__feld"><div class="korn" aria-hidden="true"></div><span class="mono">A</span></div>
<div>
<h3>Statisches Overlay</h3>
<dl class="mono" style="margin-top:10px"><dt>Kachel</dt><dd>160 px · fractalNoise 0.9 · 2 Okt.</dd><dt>Blend</dt><dd>overlay · opacity 0.9</dd><dt>Kosten</dt><dd>ein Bild, kein Repaint</dd></dl>
</div>
</article>
<article class="probe" data-variante="filmkorn">
<div class="probe__feld"><div class="korn" aria-hidden="true"></div><span class="mono">B</span></div>
<div>
<h3>Animiertes Filmkorn</h3>
<dl class="mono" style="margin-top:10px"><dt>Takt</dt><dd>100 ms · steps(1) · 8 Lagen</dd><dt>Bewegung</dt><dd>transform auf ::before</dd><dt>Touch</dt><dd>steht still</dd></dl>
</div>
</article>
<article class="probe" data-variante="grainy">
<div class="probe__feld"><div class="riso" aria-hidden="true"><div class="riso__rauschen"></div><div class="riso__farbe"></div></div><span class="mono">C</span></div>
<div>
<h3>Grainy Gradient</h3>
<dl class="mono" style="margin-top:10px"><dt>Kachel</dt><dd>256 px · fractalNoise 0.65 · 3 Okt.</dd><dt>Filter</dt><dd>contrast(170%) brightness(1000%)</dd><dt>Farbe</dt><dd>multiply · isolation: isolate</dd></dl>
</div>
</article>
</div>
</section>
<div class="d-seite" data-welt="button">
<header class="d-kopf">
<a class="d-marke" href="#">Kalmus</a>
<nav class="d-nav" aria-label="Hauptnavigation"><a href="#">Düfte</a><a href="#">Kerzen</a><a href="#">Manufaktur</a><a href="#">Proben</a></nav>
<a class="d-korb" href="#warenkorb" data-d-korb>Warenkorb <b data-d-anzahl>0</b></a>
</header>
<main class="d-produkt">
<figure class="d-bild"><picture><source media="(max-width: 860px)" srcset="bilder/flakon.jpg"><img src="bilder/flakon-hoch.jpg" alt="Rechteckiger Glasflakon mit bernsteinfarbener Füllung und schwarzer Kappe auf einem runden Steinsockel, daneben eine weiße Kugel, vor kräftigem Kobaltblau" width="800" height="1000"></picture></figure>
<form class="d-kauf" data-d-form aria-labelledby="d-titel" novalidate>
<p class="mono">Eau de Parfum · Duftmanufaktur in Coburg</p>
<h1 id="d-titel">Harzlicht No. 4</h1>
<p class="d-noten">Kiefernharz, Wacholder und ein Rest Rauch vom Ofen. Warm auf der Haut, nie süß. Hält rund acht Stunden.</p>
<p class="d-preis"><b data-d-preis>89 €</b><span data-d-grund>50 ml · 178,00 € / 100 ml · inkl. MwSt., zzgl. Versand</span></p>
<fieldset class="d-groessen">
<legend>Größe</legend>
<label class="d-groesse"><input type="radio" name="groesse" value="50" checked>50 ml <small>89 €</small></label>
<label class="d-groesse"><input type="radio" name="groesse" value="100">100 ml <small>ausverkauft</small></label>
</fieldset>
<div class="d-knoepfe">
<button type="submit" class="d-knopf kauf" data-d-kauf aria-describedby="d-status"><span data-d-text>In den Warenkorb</span><span class="d-kreis" aria-hidden="true"></span><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2.5 5.5h11l-1 8h-9Z M5.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></button>
<button type="button" class="d-knopf probe" data-d-probe>Duftprobe · 4 €</button>
</div>
<p class="d-status" id="d-status" role="status" data-d-status></p>
<ul class="d-fakten"><li>Versand in 1 bis 2 Werktagen, ab 60 € kostenlos</li><li>Von Hand abgefüllt in der Werkstatt am Albertsplatz</li><li>30 Tage Rückgabe, auch geöffnet</li></ul>
</form>
</main>
<div class="d-pult">
<div class="welten mono" role="group" aria-label="Variante">
<button type="button" data-v-wahl="basis" aria-pressed="false">Fläche</button>
<button type="button" data-v-wahl="button" aria-pressed="true">Button</button>
</div>
<p class="mono d-werte" data-d-werte aria-live="polite"></p>
</div>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#212121" stroke="#f5f4f1" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const touch = matchMedia('(hover: none)').matches;
const buehne = document.querySelector('[data-buehne]');
const werteEl = document.querySelector('[data-werte]');
const regler = document.querySelector('[data-regler]');
const knoepfe = [...document.querySelectorAll('.modi [data-v]')];
const WERTE = {
statisch: ['<b>Statisch</b>', 'feTurbulence fractalNoise 0.9 · 2 Oktaven', 'Kachel 160 px · Alpha 0.55 · overlay 0.9'],
filmkorn: ['<b>Filmkorn</b>', 'dieselbe Kachel · alle 100 ms versetzt', 'steps(1) · 8 Lagen · 800 ms · overlay 0.9'],
grainy: ['<b>Grainy Gradient</b>', 'fractalNoise 0.65 · 3 Oktaven · Kachel 256 px', 'contrast(170%) brightness(1000%) · multiply'],
};
let variante = 'statisch';
function setzen(v) {
variante = v;
buehne.dataset.variante = v;
knoepfe.forEach(k => k.setAttribute('aria-pressed', String(k.dataset.v === v)));
const z = WERTE[v].slice();
if (v === 'filmkorn' && (touch || reduziert())) z[2] = 'Korn steht still (' + (touch ? 'Touch' : 'reduzierte Bewegung') + ')';
werteEl.innerHTML = z.map(x => '<span>' + x + '</span>').join('');
}
knoepfe.forEach(k => k.addEventListener('click', () => { pause(); setzen(k.dataset.v); }));
// Trennlinie: folgt dem Zeiger weich (Lerp 0.14), per Ziehen auf Touch, per Pfeiltasten über den Regler
let ist = 50, ziel = 50, lauf = null;
const zeichnen = () => { buehne.style.setProperty('--teil', ist.toFixed(2) + '%'); regler.value = Math.round(ist); };
function gleiten() {
ist += (ziel - ist) * (reduziert() ? 1 : 0.14);
if (Math.abs(ziel - ist) < 0.05) ist = ziel;
zeichnen();
lauf = ist === ziel ? null : requestAnimationFrame(gleiten);
}
const hin = p => { ziel = Math.min(100, Math.max(0, p)); if (!lauf) lauf = requestAnimationFrame(gleiten); };
const ausZeiger = e => { const r = buehne.getBoundingClientRect(); return (e.clientX - r.left) / r.width * 100; };
let zieht = false;
buehne.addEventListener('pointermove', e => {
if (e.target.closest('.modi, .kopf a')) return;
if ((e.pointerType === 'mouse' && !touch) || zieht) { stoppVorfuehrung(); hin(ausZeiger(e)); }
});
buehne.addEventListener('pointerdown', e => {
if (e.pointerType === 'mouse' || e.target.closest('button, a')) return;
stoppVorfuehrung(); zieht = !!e.target.closest('.griff');
if (zieht) buehne.setPointerCapture(e.pointerId);
hin(ausZeiger(e));
});
const loslassen = () => { zieht = false; };
buehne.addEventListener('pointerup', loslassen);
buehne.addEventListener('pointercancel', loslassen);
regler.addEventListener('input', () => { stoppVorfuehrung(); ziel = ist = +regler.value; zeichnen(); });
// Vorführung: die Linie fährt hin und her, nach jeder Fahrt kommt die nächste Variante
const FOLGE = ['grainy', 'statisch', 'filmkorn'];
const FAHRT = 3600;
let uhr = null, takt = null, t0 = 0, runde = -1;
function stoppVorfuehrung() { if (uhr || takt) { cancelAnimationFrame(uhr); clearTimeout(takt); uhr = takt = null; html.classList.remove('vorfuehrung'); } }
function vorfuehren() {
pause(); html.classList.add('vorfuehrung');
if (welt === 'button') return dRunde();
runde = -1; t0 = performance.now();
if (reduziert()) { // keine Fahrt: Linie mittig, Varianten wechseln
ziel = ist = 50; zeichnen();
const weiterschalten = () => { runde = (runde + 1) % FOLGE.length; setzen(FOLGE[runde]); takt = setTimeout(weiterschalten, 2400); };
return weiterschalten();
}
const schritt = t => {
const n = Math.floor((t - t0) / FAHRT), p = ((t - t0) % FAHRT) / FAHRT;
if (n !== runde) { runde = n; setzen(FOLGE[n % FOLGE.length]); }
// 50 → 84 → 16 → 50, weich (Sinus)
ist = ziel = 50 + 34 * Math.sin(p * Math.PI * 2);
zeichnen();
uhr = requestAnimationFrame(schritt);
};
uhr = requestAnimationFrame(schritt);
}
function pause() { cancelAnimationFrame(uhr); clearTimeout(takt); uhr = takt = null; html.classList.remove('vorfuehrung'); dStopp(); }
function weiter() { if (html.classList.contains('vorschau') && !uhr && !takt && !dUhren.length) vorfuehren(); }
function neustart() {
pause(); dZustand(); setzen('statisch'); ziel = ist = 50; zeichnen();
buehne.classList.remove('da'); void buehne.offsetWidth;
requestAnimationFrame(() => buehne.classList.add('da'));
}
// ---- Variante „button“: Kaufbox mit körnigen Buttons (Zustände Ruhe, Hover, Fokus, Druck, deaktiviert, Laden)
const VARIANTEN = ['basis', 'button'];
const vKnoepfe = [...document.querySelectorAll('[data-v-wahl]')];
let welt = 'basis';
function setzeWelt(v, url = true) {
welt = v; html.dataset.v = v;
vKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === v)));
if (url) { const u = new URL(location.href); u.searchParams.set('v', v); history.replaceState(history.state, '', u); }
dWerte();
}
vKnoepfe.forEach(b => b.addEventListener('click', () => { pause(); setzeWelt(b.dataset.vWahl); if (b.closest('[data-welt]')) document.querySelector(`[data-welt="${welt}"] [data-v-wahl="${welt}"]`)?.focus(); }));
const dForm = document.querySelector('[data-d-form]'), dKauf = document.querySelector('[data-d-kauf]'), dProbe = document.querySelector('[data-d-probe]');
const dText = document.querySelector('[data-d-text]'), dStatus = document.querySelector('[data-d-status]'), dAnzahl = document.querySelector('[data-d-anzahl]');
const zeiger = document.querySelector('[data-zeiger]');
const HAKEN = '<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="8" fill="#10714e"/><path d="m4.6 8.2 2.2 2.2 4.6-4.8" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
let dImKorb = 0, dLadeUhr = null;
function dWerte() {
const el = document.querySelector('[data-d-werte]');
el.innerHTML = reduziert()
? '<b>Reduziert</b> · Korn steht · Verlauf ohne Fahrt · Zustände per Farbe'
: 'Kachel <b>fractalNoise 0.9 · 2 Okt. · 160 px</b> · Kauf <b>soft-light .95</b> über Verlauf 105deg #121212 → #0137ba · Probe <b>multiply .32</b> · Verlauf <b>200 %</b>, Hover <b>background-position 0 → 100 % in 600 ms expo.out</b> · Korn flimmert nur bei Hover <b>100 ms steps(1)</b> · Grundfarbe deckend als Rückfall';
}
function dZustand() {
const gross = dForm.elements.groesse.value === '100';
clearTimeout(dLadeUhr); delete dKauf.dataset.laedt;
if (gross) {
dKauf.setAttribute('aria-disabled', 'true'); dText.textContent = 'Ausverkauft';
dStatus.innerHTML = '100 ml füllen wir am 20. November neu ab. <a href="#erinnern">Per E-Mail erinnern</a>';
} else {
dKauf.removeAttribute('aria-disabled'); dText.textContent = 'In den Warenkorb'; dStatus.textContent = '';
}
}
function dKaufen() {
if (dKauf.getAttribute('aria-disabled') === 'true' || dKauf.dataset.laedt !== undefined) return;
dKauf.dataset.laedt = ''; dText.textContent = 'Wird hinzugefügt …'; dStatus.textContent = '';
dLadeUhr = setTimeout(() => {
delete dKauf.dataset.laedt; dText.textContent = 'In den Warenkorb';
dImKorb++; dAnzahl.textContent = dImKorb;
dStatus.innerHTML = HAKEN + '<b>Im Warenkorb:</b> Harzlicht No. 4, 50 ml. <a href="#warenkorb">Zur Kasse</a>';
}, reduziert() ? 700 : 1400);
}
dForm.addEventListener('change', dZustand);
dForm.addEventListener('submit', e => { e.preventDefault(); dKaufen(); });
dProbe.addEventListener('click', () => { dStatus.innerHTML = HAKEN + 'Duftprobe (2 ml) liegt im Warenkorb. Die 4 € rechnen wir beim Flakon an.'; });
dStatus.addEventListener('click', e => { if (e.target.closest('a')) e.preventDefault(); });
document.querySelectorAll('.d-kopf a').forEach(a => a.addEventListener('click', e => e.preventDefault()));
// Touch: kein Hover, also fährt der Verlauf beim Antippen einmal hin und zurück
[dKauf, dProbe].forEach(k => k.addEventListener('pointerdown', e => {
if (e.pointerType === 'mouse') return;
k.classList.add('an'); clearTimeout(k._uhr); k._uhr = setTimeout(() => k.classList.remove('an'), 900);
}));
// Vorführung button: Zeiger über „In den Warenkorb“, dann „Duftprobe“, Tastaturfokus, Klick mit Laden
let dUhren = [];
const dSpaeter = (fn, ms) => dUhren.push(setTimeout(fn, ms));
function dStopp() { dUhren.forEach(clearTimeout); dUhren = []; zeiger.classList.remove('an'); [dKauf, dProbe].forEach(k => k.classList.remove('an', 'fokus', 'druck')); }
function dNach(el, fx = .55, fy = .6) { const r = el.getBoundingClientRect(); zeiger.style.transform = `translate(${r.left + r.width * fx}px, ${r.top + r.height * fy}px)`; }
function dRunde() {
dForm.elements.groesse.value = '50'; dZustand();
zeiger.style.transition = 'none'; zeiger.style.transform = `translate(${innerWidth * .92}px, ${innerHeight * .96}px)`; zeiger.getBoundingClientRect(); zeiger.style.transition = '';
dSpaeter(() => { zeiger.classList.add('an'); dNach(dKauf); }, 200);
dSpaeter(() => dKauf.classList.add('an'), 1000);
dSpaeter(() => { dKauf.classList.remove('an'); dNach(dProbe); }, 3400);
dSpaeter(() => dProbe.classList.add('an'), 4100);
dSpaeter(() => { dProbe.classList.remove('an'); dNach(dKauf, .4); }, 6200);
dSpaeter(() => dKauf.classList.add('an', 'druck'), 7000);
dSpaeter(() => { dKauf.classList.remove('druck'); dKaufen(); }, 7150);
dSpaeter(() => { dKauf.classList.remove('an'); zeiger.classList.remove('an'); }, 8800);
dSpaeter(() => { dStatus.textContent = ''; dKauf.classList.add('fokus'); }, 10200);
dSpaeter(() => dKauf.classList.remove('fokus'), 11800);
dSpaeter(() => { dUhren = []; dRunde(); }, 12400);
}
setzen('statisch'); zeichnen();
const ausUrl = new URLSearchParams(location.search).get('v');
setzeWelt(VARIANTEN.includes(ausUrl) ? ausUrl : VARIANTEN[0], false);
new MutationObserver(dWerte).observe(html, { attributes: true, attributeFilter: ['class'] });
requestAnimationFrame(() => requestAnimationFrame(() => buehne.classList.add('da')));
})();
</script>
</body>
</html>