Rotierendes Kreistext-Siegel (Badge, Stempel, Scroll-Hinweis)
Ein Stempel als Reservieren-Knopf: Der Text läuft einmal nahtlos um den Ring und dreht sich langsam, beim Hover hält er an, beim Scrollen folgt er Tempo und Richtung.
- Ein kurzer Text mit Trennpunkten läuft einmal ganz um einen Kreis und schließt nahtlos, etwa „Tisch reservieren • Seit 1874 •“.
- In der Mitte sitzt ein Icon, ein Pfeil, ein Logo oder ein Foto.
- Der Ring dreht sich langsam und endlos (10 bis 20 s pro Umdrehung) oder ist an den Scroll gekoppelt: Er dreht mit der Scrollgeschwindigkeit und wechselt mit der Scrollrichtung.
- Umsetzung per SVG-textPath auf einem Kreispfad oder per CSS mit einzeln gedrehten Buchstaben (je 360° / n).
- Häufig als Call-to-Action (Kontakt, Reservierung, Showreel) oder als Qualitätssiegel im Hero.
Die Stempel-Anmutung gibt Persönlichkeit und Handschrift. Die ruhige Drehung bringt Leben ins Layout, ohne abzulenken, und zieht den Blick auf den Call-to-Action.
Wann einsetzen
Als Scroll-Hinweis oder Kontakt-CTA auf Portfolio- und Studioseiten, als Siegel für Gastronomie und Manufakturen, als Badge auf Produktseiten („Neu • Neu •“), in Templates als austauschbarer Akzent.
Wann nicht
Als reine Deko ohne Funktion, denn das Muster ist in Framer- und Webflow-Templates sehr verbreitet und wirkt schnell generisch. Nicht mehrere rotierende Elemente auf einer Seite. Nicht für lange Texte, die werden auf dem Ring unlesbar. Nicht als Siegel mit Umwelt-, Bio- oder Sozialaussage („nachhaltig“, „regional“, „fair“): Eigensiegel ohne Zertifizierungssystem sind seit 27.09.2026 unzulässig (UWG Anhang Nr. 2a), vom Kunden prüfen lassen. Für echte Siegel die Variante „Siegel mit Nachweis“ in logo-wand-social-proof verwenden.
Varianten
- Endlos rotierendes SVG-Badge mit Icon in der Mitte
- Scroll-gekoppelte Rotation: Tempo und Richtung folgen dem Scroll
- Hover hält die Drehung an oder beschleunigt sie
- Radiales Marquee mit mehreren Ringen (Osmo „Radial Text Marquee“)
- Nur CSS mit gesplitteten Zeichen und rotate(calc(var(--i) * 360deg / var(--n)))
- Statisches Siegel ohne Rotation als Qualitätsmarke
Umsetzung
SVG: <svg viewBox='0 0 200 200' aria-hidden='true'><defs><path id='c' d='M100,100 m-74,0 a74,74 0 1,1 148,0 a74,74 0 1,1 -148,0'/></defs><text><textPath href='#c' textLength='465' lengthAdjust='spacing'>Tisch reservieren • Seit 1874 • </textPath></text></svg>. textLength = 2πr setzen, damit der Text exakt schließt; das letzte Leerzeichen als geschütztes Leerzeichen schreiben, sonst kürzt SVG es weg und die Naht klebt. Nur die Gruppe mit dem Text drehen, die Mitte bleibt stehen. Dauerrotation per CSS: .ring { animation: spin 16s linear infinite } @keyframes spin { to { rotate: 1turn } }. Für Tempowechsel (Hover, Scroll) besser per requestAnimationFrame einen Winkel aufaddieren und das Tempo weich dem Ziel annähern lassen, dann springt nichts. Scroll-Kopplung: Geschwindigkeit in px/s messen und auf ein Tempo abbilden, zum Beispiel 1 + |v| / 150, höchstens 10-fach; mit GSAP geht das über ScrollTrigger und getVelocity(), ohne Bibliothek über animation-timeline: scroll(). Funktion als aria-label am umgebenden Link.
Robust umsetzen
Telefon. Kleiner setzen (etwa 110 bis 120 px) und so platzieren, dass es keinen Text überlagert. Hover gibt es nicht, die Dauerrotation läuft weiter. Die Scroll-Kopplung funktioniert auch mobil.
Ohne Bewegung. Rotation anhalten und das Siegel statisch zeigen, mit dem Anfang des Textes gut lesbar oben.
Barrierefrei. SVG-Text wird von Screenreadern stückweise oder doppelt gelesen. Deshalb das SVG mit aria-hidden ausblenden und die Funktion als aria-label am umgebenden Link oder Button nennen. Fokus wie Hover behandeln (anhalten). Dauerbewegung über 5 s muss pausierbar sein (WCAG 2.2.2), prefers-reduced-motion beachten.
Code
<!DOCTYPE html>
<!--
Rotierendes Kreistext-Siegel (Badge, Stempel, Scroll-Hinweis)
Fundus · Schrift-Animationen · https://cecodesign.de/lab/fundus/schrift-animationen/kreistext-siegel-rotierend/
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>Rotierendes Kreistext-Siegel (Badge, Stempel, Scroll-Hinweis) · 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: 'Fraunces'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fraunces:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Welt: Rauchfang, Hausbrauerei und Wirtschaft in Bamberg (Rezept gastro-warm-appetitlich).
Warmes Papier, Fraunces mit weichen Kanten, Commissioner im Text, Dunkelgrün und Senf aus dem Haus. */
:root {
--papier: #F0ECE0; --tinte: #353839; --leise: #6b6a63; --linie: rgba(53, 56, 57, .16);
--gruen: #293E3A; --salbei: #7F937A; --senf: #EAC16E; --rose: #E1A894;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out, Rezept: Hover 200 ms, Bild 700 ms */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Commissioner', system-ui, sans-serif; font-variation-settings: 'FLAR' 0; -webkit-font-smoothing: antialiased; }
body { background: var(--papier); overflow-x: hidden; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--gruen); outline-offset: 3px; border-radius: 4px; }
.label { font: 600 12px/1.3 'Commissioner', sans-serif; font-variation-settings: 'FLAR' 80; letter-spacing: .15em; text-transform: uppercase; color: var(--leise); }
.serif { font-family: 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100, 'WONK' 0; }
.nur-vorlesen { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* Kopf */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px clamp(16px, 4vw, 56px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 14px; text-decoration: none; }
.marke b { font: 560 28px/1 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100, 'WONK' 1; letter-spacing: -.01em; color: var(--gruen); }
.marke small { font-size: 13px; color: var(--leise); }
.nav { display: flex; gap: 28px; font-size: 15px; font-weight: 450; }
.nav a { text-decoration: none; transition: color .2s var(--kurve); }
.nav a:hover { color: var(--gruen); text-decoration: underline; text-underline-offset: .3em; text-decoration-thickness: 1px; }
.rechts { display: flex; align-items: center; gap: 18px; }
.offen { display: flex; align-items: center; gap: 8px; font-size: 14px; white-space: nowrap; }
.offen i { width: 8px; height: 8px; border-radius: 50%; background: var(--salbei); box-shadow: 0 0 0 3px rgba(127, 147, 122, .25); }
.offen.zu i { background: var(--rose); box-shadow: 0 0 0 3px rgba(225, 168, 148, .3); }
.knopf { display: inline-flex; align-items: center; height: 44px; padding: 0 20px; border-radius: 999px; background: var(--gruen); color: var(--papier); font: 550 15px/1 'Commissioner', sans-serif; text-decoration: none; white-space: nowrap; transition: background-color .2s var(--kurve); }
.knopf:hover { background: #1d2d2a; }
/* Bühne */
.held { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .92fr); gap: clamp(40px, 7vw, 120px); align-items: center; padding: clamp(28px, 5vh, 64px) clamp(16px, 4vw, 56px) clamp(24px, 4vh, 48px); min-height: calc(100svh - 81px - 58px); }
.text h1 { margin-top: 22px; font-weight: 500; font-size: clamp(2.5rem, 1.6rem + 4vw, 5rem); line-height: 1.02; letter-spacing: -.01em; color: var(--gruen); text-wrap: balance; }
.text h1 em { font-style: normal; color: #8c4a22; }
.text .lauf { margin-top: 24px; max-width: 34em; font-size: 1.0625rem; line-height: 1.55; text-wrap: pretty; }
.fakten { margin-top: 32px; display: grid; grid-template-columns: repeat(3, auto); justify-content: start; gap: 12px 40px; padding-top: 18px; border-top: 1px solid var(--linie); }
.fakten p + p { margin-top: 6px; font-size: 15px; line-height: 1.4; white-space: nowrap; }
.fakten a { text-decoration: none; border-bottom: 1px solid var(--linie); }
.bild { position: relative; justify-self: end; width: 100%; max-width: 560px; }
.tafel { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; max-height: calc(100svh - 230px); border-radius: 6px; background: var(--gruen); opacity: 0; scale: .96; transition: opacity .7s var(--kurve), scale .7s var(--kurve); }
.bereit .tafel { opacity: 1; scale: 1; }
.unterschrift { margin-top: 10px; display: flex; justify-content: space-between; gap: 16px; white-space: nowrap; }
/* Das Siegel: Ring mit Text auf Kreis-Pfad, Mitte steht */
.siegel { position: absolute; z-index: 2; left: -80px; bottom: 15%; width: 160px; height: 160px; border-radius: 50%; text-decoration: none; transition: scale .2s var(--kurve); }
.siegel svg { display: block; width: 100%; height: 100%; overflow: visible; }
.siegel .ring text { font: 600 15px 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100, 'WONK' 0, 'opsz' 12; fill: var(--gruen); text-transform: uppercase; }
.siegel .pfeil { transition: translate .2s var(--kurve); }
.siegel:hover, .siegel:focus-visible { scale: 1.04; }
.siegel:hover .pfeil, .siegel:focus-visible .pfeil { translate: 3px -3px; }
.siegel:focus-visible { outline: 2px solid var(--gruen); outline-offset: 6px; border-radius: 50%; }
/* Leiste: Umschalter und Werte */
.leiste { display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap; padding: 12px clamp(16px, 4vw, 56px) 16px; border-top: 1px solid var(--linie); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: color .2s var(--kurve), background-color .2s var(--kurve), border-color .2s var(--kurve); }
.modi button:hover { color: var(--gruen); border-color: rgba(41, 62, 58, .45); }
.modi button[aria-pressed='true'] { background: var(--gruen); color: var(--papier); border-color: var(--gruen); }
.werte { display: flex; flex-wrap: wrap; gap: 4px 18px; justify-content: flex-end; font-size: 11px; letter-spacing: .1em; }
.werte b { color: var(--gruen); font-weight: 650; }
/* Karte darunter: gibt dem Scroll-Modus Strecke */
.karte { padding: clamp(64px, 10vh, 120px) clamp(16px, 4vw, 56px); display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: clamp(32px, 6vw, 96px); border-top: 1px solid var(--linie); }
.karte h2 { margin-top: 16px; font-weight: 500; font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem); line-height: 1.08; color: var(--gruen); text-wrap: balance; }
.karte .lauf { margin-top: 16px; font-size: 1.0625rem; line-height: 1.55; max-width: 32em; }
.gerichte { list-style: none; padding: 0; }
.gerichte li { display: grid; grid-template-columns: 1fr auto; gap: 4px 24px; padding: 18px 0; border-top: 1px solid var(--linie); }
.gerichte li:last-child { border-bottom: 1px solid var(--linie); }
.gerichte b { font-size: 1.125rem; font-weight: 550; }
.gerichte span { grid-column: 1; font-size: .9375rem; line-height: 1.45; color: var(--leise); }
.gerichte em { grid-column: 2; grid-row: 1; font-style: normal; font-size: 1.125rem; font-variant-numeric: tabular-nums; }
.gerichte sup { font-size: 11px; color: var(--leise); margin-left: 4px; letter-spacing: .04em; }
.fuss { background: var(--gruen); color: var(--papier); padding: clamp(56px, 9vh, 96px) clamp(16px, 4vw, 56px) 64px; }
.fuss .gross { max-width: 20ch; font-weight: 450; font-size: clamp(1.75rem, 1.3rem + 1.6vw, 2.75rem); line-height: 1.12; }
.fuss .spalten { margin-top: 44px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 28px; }
.fuss .label { color: var(--senf); margin-bottom: 10px; }
.fuss p { font-size: 1rem; line-height: 1.6; }
/* Vorführung: gezeichneter Zeiger und Scroll-Hinweis */
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 20px; height: 20px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
.rad { position: absolute; z-index: 3; left: 96px; bottom: calc(15% + 38px); display: none; align-items: center; gap: 8px; height: 30px; padding: 0 12px; border-radius: 999px; background: var(--papier); color: var(--gruen); box-shadow: 0 0 0 1px var(--linie); white-space: nowrap; }
.rad svg { width: 12px; height: 16px; }
.vorfuehrung .rad.an { display: inline-flex; }
@media (max-width: 1180px) { .nav { display: none; } }
@media (max-width: 1100px) { .fakten { gap: 12px 28px; } .unterschrift span + span { display: none; } }
@media (min-width: 861px) and (max-width: 1100px) { .fakten { grid-template-columns: auto auto; } .fakten > div:last-child { display: none; } }
@media (max-width: 860px) {
.held { grid-template-columns: 1fr; min-height: 0; gap: 36px; }
.bild { justify-self: stretch; max-width: none; padding-left: 0; }
.tafel { aspect-ratio: 4 / 3.4; max-height: none; }
.siegel { width: 116px; height: 116px; left: auto; right: 16px; bottom: auto; top: -58px; }
.rad { left: auto; right: 140px; top: -16px; bottom: auto; }
.karte { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.kopf { padding-block: 14px; }
.marke small, .offen { display: none; }
.marke b { font-size: 24px; }
.knopf { height: 40px; padding: 0 16px; font-size: 14px; }
.fakten { grid-template-columns: 1fr 1fr; gap: 16px 24px; }
.fakten > div:last-child { grid-column: 1 / -1; }
.werte { justify-content: flex-start; }
.werte .lang { display: none; }
}
/* Touch: kein Hover. Der Ring dreht weiter, Hover-Varianten fallen auf die Dauerrotation zurück */
@media (hover: none) {
.modi button { height: 36px; padding: 0 14px; }
.siegel:hover { scale: 1; }
.siegel:hover .pfeil { translate: 0 0; }
}
/* Reduzierte Bewegung: statisches Siegel, Bild ohne Einblendung */
:root.reduziert .tafel { opacity: 1; scale: 1; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .leiste, .vorschau .karte, .vorschau .fuss, .vorschau .nav, .vorschau .text .lauf { display: none; }
.vorschau .held { min-height: calc(100svh - 81px); }
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#" aria-label="Rauchfang, Startseite"><b>Rauchfang</b><small>Hausbrauerei & Wirtschaft · Bamberg</small></a>
<nav class="nav" aria-label="Hauptnavigation"><a href="#karte">Karte</a><a href="#">Unser Bier</a><a href="#">Biergarten</a><a href="#">Gruppen & Feiern</a></nav>
<div class="rechts"><p class="offen" data-offen><i aria-hidden="true"></i><span data-offen-text>Heute geöffnet</span></p><a class="knopf" href="#reservieren">Reservieren</a></div>
</header>
<main>
<section class="held" data-held>
<div class="text">
<p class="label">Obere Sandstraße 14 · am Fuß des Dombergs</p>
<h1 class="serif">Rauchbier vom Holzfass, Schäufele aus dem <em>Holzofen.</em></h1>
<p class="lauf">Seit 1874 darren wir unser Malz über Buchenholz und brauen daraus ein dunkles Märzen, das nach Lagerfeuer riecht. Dazu fränkische Küche, die nach Sonntag schmeckt.</p>
<div class="fakten">
<div><p class="label">Wirtschaft</p><p>Di bis Sa 11 bis 23 Uhr<br>So 11 bis 21 Uhr</p></div>
<div><p class="label">Küche</p><p>bis 21:30 Uhr<br>Montag Ruhetag</p></div>
<div><p class="label">Tisch</p><p><a href="tel:+49951208470">0951 20 84 70</a><br>ab 8 Personen Stube</p></div>
</div>
</div>
<div class="bild">
<svg class="tafel" viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Zeichnung: ein Seidla dunkles Rauchbier am Ufer der Regnitz, dahinter Michaelsberg und Dom mit seinen vier Türmen, aus dem Kamin des Brauhauses steigt Rauch">
<defs>
<linearGradient id="himmel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#34504a"/><stop offset=".62" stop-color="#293E3A"/></linearGradient>
<linearGradient id="bier" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#3f1709"/><stop offset=".38" stop-color="#7d3312"/><stop offset=".6" stop-color="#a54d1b"/><stop offset="1" stop-color="#36130a"/></linearGradient>
<linearGradient id="glanz" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#F0ECE0" stop-opacity=".6"/><stop offset="1" stop-color="#F0ECE0" stop-opacity="0"/></linearGradient>
<linearGradient id="fluss" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#22342f"/><stop offset="1" stop-color="#1b2926"/></linearGradient>
<radialGradient id="schein" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#EAC16E" stop-opacity=".3"/><stop offset="1" stop-color="#EAC16E" stop-opacity="0"/></radialGradient>
</defs>
<rect width="400" height="500" fill="url(#himmel)"/>
<circle cx="312" cy="112" r="120" fill="url(#schein)"/>
<circle cx="312" cy="112" r="28" fill="#EAC16E"/>
<!-- Michaelsberg mit Kirche (links) -->
<g fill="#2a403b">
<path d="M-10 318 C20 290 50 268 92 262 C130 258 160 280 190 318 Z"/>
<path d="M64 266 V226 L70 210 L76 226 V266 Z M82 266 V226 L88 210 L94 226 V266 Z M60 266 V244 H100 V266 Z"/>
</g>
<!-- Domberg mit Dom: vier Türme, spitze Helme -->
<g fill="#22352f">
<path d="M150 320 C170 300 190 292 220 290 L330 290 C360 292 390 304 410 320 Z"/>
<path d="M214 292 V260 H306 V292 Z"/>
<path d="M212 292 V214 H226 V292 Z M232 292 V214 H246 V292 Z M276 292 V214 H290 V292 Z M296 292 V214 H310 V292 Z"/>
<path d="M212 214 L219 186 L226 214 Z M232 214 L239 186 L246 214 Z M276 214 L283 186 L290 214 Z M296 214 L303 186 L310 214 Z"/>
<path d="M318 292 V268 L340 254 L362 268 V292 Z"/>
</g>
<!-- Häuserzeile am Fluss, eines davon das Brauhaus mit Kamin -->
<g fill="#1d2d29">
<path d="M-4 340 V312 L12 298 L28 312 V340 Z M28 340 V316 L42 304 L56 316 V340 Z M56 340 V306 L76 288 L96 306 V340 Z"/>
<path d="M96 340 V300 L122 278 L148 300 V340 Z M134 286 V266 H142 V293 Z"/>
<path d="M148 340 V314 L162 302 L176 314 V340 Z M176 340 V318 L192 306 L208 318 V340 Z M208 340 V310 L226 296 L244 310 V340 Z"/>
<path d="M244 340 V316 L258 304 L272 316 V340 Z M272 340 V308 L292 292 L312 308 V340 Z M312 340 V318 L328 306 L344 318 V340 Z M344 340 V312 L364 298 L384 312 V340 Z M384 340 V318 L398 306 L412 318 V340 Z"/>
</g>
<g fill="#EAC16E">
<rect x="72" y="314" width="5" height="7" opacity=".85"/><rect x="114" y="306" width="5" height="7" opacity=".9"/><rect x="126" y="306" width="5" height="7" opacity=".9"/>
<rect x="114" y="322" width="5" height="7" opacity=".55"/><rect x="186" y="324" width="4" height="6" opacity=".6"/><rect x="222" y="318" width="5" height="7" opacity=".75"/>
<rect x="356" y="320" width="5" height="7" opacity=".7"/><rect x="40" y="322" width="4" height="6" opacity=".5"/>
</g>
<!-- Rauch aus dem Kamin -->
<g fill="none" stroke="#F0ECE0" stroke-linecap="round" stroke-width="2.2">
<path d="M138 262 C128 240 152 228 138 204 C126 182 150 168 140 140 C132 118 150 104 146 80" stroke-opacity=".26"/>
<path d="M140 258 C154 238 134 222 150 200 C164 180 146 160 160 136" stroke-opacity=".14"/>
</g>
<!-- Regnitz mit Spiegelungen -->
<rect y="340" width="400" height="44" fill="url(#fluss)"/>
<g stroke="#EAC16E" stroke-linecap="round" stroke-width="1.6">
<path d="M70 350 h12 M114 348 h10 M120 358 h8 M220 352 h10 M352 350 h12 M300 362 h22 M292 372 h12 M40 364 h8" stroke-opacity=".38"/>
</g>
<path d="M0 384 H400" stroke="#EAC16E" stroke-opacity=".35"/>
<rect y="384" width="400" height="116" fill="#17221f"/>
<!-- Seidla im Vordergrund -->
<ellipse cx="282" cy="470" rx="64" ry="7" fill="#0f1714" opacity=".7"/>
<g>
<path d="M240 300 L244 456 C244 463 250 468 257 468 L307 468 C314 468 320 463 320 456 L324 300 Z" fill="url(#bier)"/>
<path d="M324 330 C352 330 360 344 360 374 C360 404 350 420 322 424" fill="none" stroke="#F0ECE0" stroke-opacity=".42" stroke-width="9" stroke-linecap="round"/>
<path d="M240 300 L244 456 C244 463 250 468 257 468 L307 468 C314 468 320 463 320 456 L324 300" fill="none" stroke="#F0ECE0" stroke-opacity=".45" stroke-width="1.4"/>
<path d="M236 292 C238 280 252 276 260 282 C266 272 282 272 288 280 C296 272 314 274 318 284 C328 282 332 292 328 302 L238 302 C234 300 234 296 236 292 Z" fill="#F0ECE0"/>
<path d="M240 302 L324 302 L323 312 C298 317 266 317 241 312 Z" fill="#e2d6bd"/>
<path d="M252 322 L255 446" stroke="url(#glanz)" stroke-width="5" stroke-linecap="round"/>
<g fill="#F0ECE0" opacity=".38"><circle cx="292" cy="440" r="1.5"/><circle cx="300" cy="408" r="1.1"/><circle cx="286" cy="376" r="1.3"/><circle cx="304" cy="348" r="1"/><circle cx="294" cy="328" r="1.2"/></g>
</g>
<text x="24" y="482" fill="#F0ECE0" fill-opacity=".6" font-family="Commissioner, sans-serif" font-size="10" letter-spacing="1.5">MÄRZEN · 5,1 % · 0,5 L · 4,60 €</text>
</svg>
<a class="siegel" href="#reservieren" aria-label="Tisch reservieren" data-siegel>
<svg viewBox="0 0 200 200" aria-hidden="true" focusable="false">
<defs><path id="kreis" d="M100,100 m-74,0 a74,74 0 1,1 148,0 a74,74 0 1,1 -148,0"/></defs>
<circle cx="100" cy="100" r="98" fill="#EAC16E"/>
<circle cx="100" cy="100" r="94" fill="none" stroke="#293E3A" stroke-width="1.2"/>
<g class="ring" data-ring>
<text dy="5.4"><textPath href="#kreis" data-textpfad lengthAdjust="spacing">Tisch reservieren <tspan fill="#8c4a22">•</tspan> Seit 1874 im Sand <tspan fill="#8c4a22">•</tspan> </textPath></text>
</g>
<circle cx="100" cy="100" r="56" fill="none" stroke="#293E3A" stroke-width="1.2"/>
<circle cx="100" cy="100" r="48" fill="#293E3A"/>
<g class="pfeil"><path d="M86 114 L114 86 M94 86 H114 V106" fill="none" stroke="#F0ECE0" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></g>
</svg>
</a>
<span class="rad label" data-rad aria-hidden="true"><svg viewBox="0 0 12 16"><rect x="1" y="1" width="10" height="14" rx="5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M6 4v3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg><span data-rad-text>Scroll ↓</span></span>
<p class="unterschrift label"><span>Abb. Unser Märzen im Seidla</span><span>Bamberg 49°53′ N</span></p>
</div>
</section>
<div class="leiste">
<div class="modi label" role="group" aria-label="Variante">
<button type="button" data-modus="endlos" aria-pressed="true">Endlos · Hover hält</button>
<button type="button" data-modus="scroll" aria-pressed="false">Scroll-gekoppelt</button>
<button type="button" data-modus="schneller" aria-pressed="false">Hover × 3</button>
</div>
<p class="werte label" data-werte></p>
</div>
<section class="karte" id="karte">
<div>
<p class="label">Heute auf der Karte</p>
<h2 class="serif">Was aus dem Holzofen kommt, steht auf der Tafel.</h2>
<p class="lauf">Wir kochen, bis es aus ist. Allergene stehen als Kürzel hinter jedem Gericht, die Legende liegt am Tisch.</p>
</div>
<ul class="gerichte">
<li><b>Schäufele mit Kloß und Krautsalat</b><em>17,90</em><span>Schweineschulter, sechs Stunden im Holzofen, Rauchbier-Soße <sup>A · G · L</sup></span></li>
<li><b>Bamberger Zwiebel</b><em>13,50</em><span>gefüllt mit Hackfleisch und Speck, dazu Kartoffelpüree <sup>A · C · G</sup></span></li>
<li><b>Hörnla mit Pilzrahm</b><em>12,80</em><span>Bamberger Hörnla aus der Pfanne, Steinpilze, Schnittlauch <sup>G · L</sup></span></li>
<li><b>Seidla Märzen vom Holzfass</b><em>4,60</em><span>0,5 l, um 11 Uhr frisch angestochen <sup>A</sup></span></li>
</ul>
</section>
<footer class="fuss">
<p class="label">Rauchfang · seit 1874</p>
<p class="gross serif">Kommen Sie vorbei. Den Rauch riechen Sie schon an der Brücke.</p>
<div class="spalten">
<div><p class="label">Adresse</p><p>Obere Sandstraße 14<br>96049 Bamberg</p></div>
<div><p class="label">Öffnungszeiten</p><p>Di bis Sa 11 bis 23 Uhr<br>So 11 bis 21 Uhr, Mo Ruhetag</p></div>
<div><p class="label">Reservieren</p><p>0951 20 84 70<br>tisch@rauchfang-bamberg.de</p></div>
</div>
</footer>
</main>
<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="#293E3A" stroke="#F0ECE0" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const siegel = $('[data-siegel]'), ring = $('[data-ring]'), textpfad = $('[data-textpfad]');
const werte = $('[data-werte]'), zeiger = $('[data-zeiger]'), rad = $('[data-rad]'), radText = $('[data-rad-text]');
// Werte (Messwerte: linear, 10–20 s pro Umdrehung, Ø 96–160 px, Text 10–14 px versal, 0,1–0,2 em)
const UMDREHUNG_MS = 16000; // Grundtempo
const RADIUS = 74; // Textpfad im viewBox 200
const UMFANG = 2 * Math.PI * RADIUS; // ≈ 465 → textLength, damit der Ring nahtlos schließt
const HOVER_SCHNELL = 3; // Variante „Hover × 3“
const SCROLL_TEILER = 150; // Faktor = 1 + |v| / 150 (v in px/s)
const SCROLL_MAX = 10; // höchstens 10-faches Tempo
const GLAETTUNG = 0.08; // Tempo nähert sich pro Bild um 8 % an (bei 60 fps)
textpfad.setAttribute('textLength', UMFANG.toFixed(1));
let modus = 'endlos', winkel = 0, tempo = 1, zielTempo = 1, richtung = 1, ueber = false;
let letzte = performance.now(), scrollAlt = scrollY, scrollZeit = performance.now(), scrollV = 0;
/* Öffnungszeiten echt berechnen (Mo Ruhetag, Di–Sa 11–23, So 11–21) */
(function oeffnung() {
const z = { 0: [11, 21], 2: [11, 23], 3: [11, 23], 4: [11, 23], 5: [11, 23], 6: [11, 23] };
const d = new Date(), t = z[d.getDay()], h = d.getHours() + d.getMinutes() / 60;
const el = $('[data-offen]'), tx = $('[data-offen-text]');
if (t && h >= t[0] && h < t[1]) tx.textContent = `Jetzt geöffnet bis ${t[1]} Uhr`;
else { el.classList.add('zu'); tx.textContent = t && h < t[0] ? `Heute ab ${t[0]} Uhr geöffnet` : 'Gerade geschlossen'; }
})();
function zeigeWerte() {
const basis = `<span>Umdrehung <b>${UMDREHUNG_MS / 1000} s linear</b></span><span class="lang">Ø <b>${siegel.offsetWidth} px</b></span><span class="lang">Ring <b>textLength 2πr = ${UMFANG.toFixed(0)}</b></span>`;
const teil = reduziert() ? '<span>Reduziert: <b>Siegel steht</b></span>' : {
endlos: '<span>Hover <b>Tempo → 0</b></span>',
scroll: `<span>Tempo <b>1 + |v| / ${SCROLL_TEILER}, max. ${SCROLL_MAX} ×</b></span><span class="lang">Richtung <b>folgt dem Scroll</b></span>`,
schneller: `<span>Hover <b>Tempo × ${HOVER_SCHNELL}</b></span>`,
}[modus];
werte.innerHTML = basis + teil + (reduziert() ? '' : '<span aria-hidden="true">jetzt <b data-tempo>1,0 ×</b></span>');
}
function setzeModus(neu) {
modus = neu; richtung = 1;
document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === modus)));
zeigeWerte();
}
document.querySelector('.modi').addEventListener('click', e => {
const b = e.target.closest('[data-modus]'); if (!b) return;
stopVorfuehrung(); setzeModus(b.dataset.modus);
});
// Hover nur mit echter Maus; Fokus verhält sich wie Hover
const an = e => { if (!e || e.pointerType !== 'touch') ueber = true; };
const aus = () => { ueber = false; };
siegel.addEventListener('pointerenter', an); siegel.addEventListener('pointerleave', aus);
siegel.addEventListener('focus', () => an()); siegel.addEventListener('blur', aus);
siegel.addEventListener('click', e => e.preventDefault());
// Scroll-Geschwindigkeit in px/s
addEventListener('scroll', () => {
if (vorf) return;
const t = performance.now(), dt = Math.max(16, t - scrollZeit);
const v = (scrollY - scrollAlt) / dt * 1000;
scrollV = v; scrollAlt = scrollY; scrollZeit = t;
}, { passive: true });
function zielBerechnen() {
if (modus === 'endlos') return ueber ? 0 : 1;
if (modus === 'schneller') return ueber ? HOVER_SCHNELL : 1;
// Scroll: Richtung folgt der letzten Scrollrichtung, Tempo der Geschwindigkeit
if (Math.abs(scrollV) > 5) richtung = Math.sign(scrollV);
return richtung * Math.min(SCROLL_MAX, 1 + Math.abs(scrollV) / SCROLL_TEILER);
}
function takt(jetzt) {
requestAnimationFrame(takt);
const dt = Math.min(64, jetzt - letzte); letzte = jetzt;
if (reduziert()) { ring.removeAttribute('transform'); return; }
if (vorf && !vorfPause) vorfuehrSchritt(jetzt);
if (vorfPause) return;
if (!vorf && jetzt - scrollZeit > 120) scrollV *= Math.pow(0.85, dt / 16.667); // Scroll ruht → abklingen
zielTempo = zielBerechnen();
tempo += (zielTempo - tempo) * (1 - Math.pow(1 - GLAETTUNG, dt / 16.667));
winkel = (winkel + tempo * dt * 360 / UMDREHUNG_MS) % 360;
ring.setAttribute('transform', `rotate(${winkel.toFixed(2)} 100 100)`);
const tz = document.querySelector('[data-tempo]');
if (tz) tz.textContent = tempo.toFixed(1).replace('.', ',') + ' ×';
}
/* Vorführung: drei Phasen, gezeichneter Zeiger bzw. simulierte Scroll-Geschwindigkeit */
let vorf = false, vorfPause = false, vorfStart = 0, pausiertBei = 0;
const PHASEN = [{ modus: 'endlos', dauer: 5200 }, { modus: 'scroll', dauer: 6000 }, { modus: 'schneller', dauer: 5200 }];
const GESAMT = PHASEN.reduce((s, p) => s + p.dauer, 0);
const glatt = x => x * x * (3 - 2 * x);
function zeigerAuf(t) {
// t 0 → 1: von unten rechts zum Siegel, kurz halten, wieder weg
const r = siegel.getBoundingClientRect(), zx = r.left + r.width * 0.62, zy = r.top + r.height * 0.58;
const ax = Math.min(innerWidth - 40, r.right + 260), ay = Math.min(innerHeight - 30, r.bottom + 160);
let k;
if (t < 0.25) k = 0; else if (t < 0.4) k = glatt((t - 0.25) / 0.15); else if (t < 0.82) k = 1; else if (t < 0.95) k = 1 - glatt((t - 0.82) / 0.13); else k = 0;
const x = ax + (zx - ax) * k, y = ay + (zy - ay) * k;
zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`;
ueber = k > 0.98;
siegel.classList.toggle('schwebt', ueber);
}
function vorfuehrSchritt(jetzt) {
let t = (jetzt - vorfStart) % GESAMT, i = 0;
while (t > PHASEN[i].dauer) { t -= PHASEN[i].dauer; i++; }
const ph = PHASEN[i], q = t / ph.dauer;
if (modus !== ph.modus) setzeModus(ph.modus);
if (ph.modus === 'scroll') {
zeiger.style.transform = 'translate(-40px, -40px)'; ueber = false;
// erst nach unten, dann zurück nach oben scrollen (simulierte Geschwindigkeit in px/s)
const v = q < 0.12 ? 0 : q < 0.48 ? 1300 * Math.sin((q - 0.12) / 0.36 * Math.PI) : q < 0.58 ? 0 : q < 0.92 ? -1100 * Math.sin((q - 0.58) / 0.34 * Math.PI) : 0;
scrollV = v; if (Math.abs(v) > 5) richtung = Math.sign(v);
rad.classList.toggle('an', q > 0.1 && q < 0.95);
radText.textContent = q < 0.53 ? 'Scroll ↓' : 'Scroll ↑';
} else {
rad.classList.remove('an'); scrollV = 0;
zeigerAuf(q);
}
}
function vorfuehren() {
stopVorfuehrung();
if (reduziert()) { setzeModus('endlos'); return; }
vorf = true; vorfPause = false; vorfStart = performance.now();
html.classList.add('vorfuehrung');
setzeModus('endlos');
}
function stopVorfuehrung() {
if (!vorf) return;
vorf = false; vorfPause = false; ueber = false; scrollV = 0; richtung = 1;
html.classList.remove('vorfuehrung'); rad.classList.remove('an');
}
function pause() { if (vorf && !vorfPause) { vorfPause = true; pausiertBei = performance.now(); } }
function weiter() {
if (vorf && vorfPause) { vorfStart += performance.now() - pausiertBei; vorfPause = false; letzte = performance.now(); }
else if (!vorf && html.classList.contains('vorschau')) vorfuehren();
}
function neustart() {
stopVorfuehrung();
scrollTo(0, 0);
winkel = 0; tempo = 1; scrollV = 0;
setzeModus('endlos');
document.body.classList.remove('bereit');
requestAnimationFrame(() => requestAnimationFrame(() => document.body.classList.add('bereit')));
}
['wheel', 'touchstart', 'keydown'].forEach(t => addEventListener(t, () => { if (vorf && !html.classList.contains('vorschau')) stopVorfuehrung(); }, { passive: true }));
document.fonts.ready.then(() => {
zeigeWerte();
document.body.classList.add('bereit');
requestAnimationFrame(takt);
});
addEventListener('resize', zeigeWerte);
})();
</script>
</body>
</html>