Zum Inhalt
← Cursor

Nachzieh-Cursor (Lerp-Follower)

Studio-Startseite aus Wien-Neubau: Ein Punkt sitzt am Zeiger, ein 30-px-Ring zieht per Lerp 1/12 nach, wächst über Links und füllt sich über den Arbeiten in Signalorange.

Ein kleiner Punkt sitzt exakt am Zeiger. Ein größerer Ring folgt mit Verzögerung: Pro Bild rückt er um einen festen Anteil des Abstands nach, typisch 0,075 bis 0,15. Über Links wächst der Ring oder füllt sich. Optional dehnt er sich nach Geschwindigkeit in Bewegungsrichtung (Squash and Stretch).

Der Nachlauf gibt der Bewegung Gewicht. Zwischen 2019 und 2023 war er ein Erkennungszeichen von Award-Seiten.

Wann einsetzen

Nur noch als Basis für Kontext-Cursor und Labels oder wenn die Marke ein Cursor-Konzept trägt, etwa Pinsel, Fadenkreuz oder ein Zeichen aus Punkten.

Wann nicht

Als Standard-Deko auf der ganzen Seite. Das wirkt 2026 generisch und verzögert die Bedienung. Nie den System-Cursor durch einen verzögerten ersetzen. In Shops und auf Seiten öffentlicher Häuser ohne Nutzen.

Varianten

  • Punkt und Ring
  • Dehnung nach Geschwindigkeit (Velocity-Stretch)
  • Mehrere Kreise mit gestaffelter Verzögerung
  • CSS-Cursor per cursor: url() ohne Verzögerung, sehr leicht

Umsetzung

rAF-Schleife: x += (mx − x) × k, dann transform: translate3d(x, y, 0) translate(-50%, -50%). Den Faktor auf die Bildrate umrechnen, sonst läuft der Ring auf 120-Hz-Bildschirmen doppelt so schnell. Mausposition in einer Variablen halten, nicht im State eines Frameworks. pointer-events: none. Nur bei (hover: hover) and (pointer: fine) starten. Alternative ohne Lerp: bei jedem mousemove einen Tween mit expo.out und rund 550 ms neu starten. System-Cursor sichtbar lassen; beim Verlassen des Fensters und bei Tab-Wechsel ausblenden.

Robust umsetzen

Telefon. Entfällt komplett.

Ohne Bewegung. Abschalten, nur der System-Cursor bleibt.

Barrierefrei. System-Cursor nicht ausblenden, viele Nutzer arbeiten mit vergrößertem Zeiger. Cursor-Elemente aria-hidden. Hover-Zustände der Links müssen auch ohne eigenen Cursor erkennbar sein.

Code

<!DOCTYPE html>
<!--
  Nachzieh-Cursor (Lerp-Follower)
  Fundus · Cursor · https://cecodesign.de/lab/fundus/cursor/cursor-follower-lerp/
  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>Nachzieh-Cursor (Lerp-Follower) · 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: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

:root {
  --dunkel: #0b0b0b; --hell: #f2f2f2; --weiss: #ffffff; --meta: #8a8a8a; --signal: #ff5a1f;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out, eine Kurve für alles */
  --form: 550ms;                            /* Cuberto: 550 ms expo.out */
  --ui: 300ms;
  --rand: clamp(18px, 3vw, 44px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--dunkel); color: var(--weiss); font-family: 'TikTok Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.mono { font: 500 .75rem/1.35 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; }

/* Kopf */
.kopf { display: flex; align-items: center; gap: 28px; padding: 22px var(--rand); }
.marke { font-weight: 800; font-stretch: 75%; font-size: 1.5rem; letter-spacing: -.01em; text-transform: uppercase; display: flex; align-items: center; gap: 10px; }
.marke svg { width: 22px; height: 22px; }
.navi { display: flex; gap: 26px; margin-left: auto; }
.navi a { font-size: 1rem; font-weight: 500; padding: 8px 0; position: relative; }
.navi a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform var(--ui) var(--kurve); }
.navi a:hover::after, .navi a:focus-visible::after { transform: scaleX(1); transform-origin: left; }
.cta { background: var(--signal); color: var(--dunkel); font-weight: 600; font-size: 1rem; padding: 13px 20px; border-radius: 999px; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.cta:hover, .cta:focus-visible { background: var(--weiss); }

/* Hero: riesige Schrift, schmale Breite */
.held { padding: clamp(28px, 6vh, 72px) var(--rand) clamp(36px, 7vh, 88px); display: grid; grid-template-columns: 1fr minmax(220px, 300px); gap: 32px; align-items: end; }
h1 { font-size: clamp(3.5rem, 10.4vw, 11rem); font-weight: 800; font-stretch: 75%; line-height: .98; letter-spacing: -.03em; text-transform: uppercase; }
h1 .signal { color: var(--signal); }
.held p { font-size: 1.125rem; line-height: 1.4; color: #cfcfcf; max-width: 30ch; }
.held .mono { color: var(--meta); display: block; margin-bottom: 14px; }

/* Ausgewählte Arbeiten: helle, invertierte Sektion */
.arbeiten { background: var(--hell); color: var(--dunkel); padding: clamp(28px, 5vh, 56px) var(--rand) clamp(40px, 8vh, 96px); }
.arbeiten .kopfzeile { display: flex; justify-content: space-between; color: #5c5c5c; padding-bottom: 14px; }
.liste { list-style: none; border-bottom: 1px solid rgba(11, 11, 11, .2); }
.liste a { display: grid; grid-template-columns: 3.5em 1fr minmax(8em, 16em) 4em; align-items: baseline; gap: 18px; padding: clamp(14px, 2.2vh, 24px) 0; border-top: 1px solid rgba(11, 11, 11, .2); }
.liste .name { font-size: clamp(1.75rem, 4.6vw, 4.5rem); font-weight: 600; letter-spacing: -.02em; line-height: 1; transition: translate var(--form) var(--kurve); }
.liste .art { font-size: 1rem; color: #5c5c5c; }
.liste .jahr { text-align: right; color: #5c5c5c; }
.liste a:hover .name, .liste a:focus-visible .name, .liste a.an .name { translate: .18em 0; }
.liste a:focus-visible { outline: 2px solid var(--dunkel); outline-offset: 4px; }

.abschluss { padding: clamp(48px, 10vh, 120px) var(--rand) 40px; display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 28px; }
.abschluss h2 { font-size: clamp(3rem, 9vw, 9rem); font-weight: 800; font-stretch: 75%; line-height: .9; letter-spacing: -.03em; text-transform: uppercase; }
.abschluss a.mail { font-size: clamp(1.25rem, 2.2vw, 2rem); font-weight: 500; border-bottom: 1px solid currentColor; padding-bottom: 4px; }
.fuss { display: flex; justify-content: space-between; gap: 16px; padding: 0 var(--rand) 28px; color: var(--meta); }

/* Der Nachzieh-Cursor: Punkt exakt am Zeiger, Ring folgt per Lerp. System-Cursor bleibt sichtbar. */
.folger { position: fixed; inset: 0; pointer-events: none; z-index: 50; opacity: 0; transition: opacity var(--ui) var(--kurve); --farbe: var(--weiss); mix-blend-mode: difference; }
.folger.an { opacity: 1; }
.folger.hell { --farbe: var(--dunkel); mix-blend-mode: normal; }
.folger.hell.fuellen { mix-blend-mode: multiply; }
.folger > * { position: absolute; left: 0; top: 0; will-change: transform; }
.punkt { width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%; background: var(--farbe); transition: background-color 200ms var(--kurve); }
.dehn { width: 0; height: 0; }
.form { position: absolute; left: 0; top: 0; width: 30px; height: 30px; translate: -50% -50%; border-radius: 50%; border: 1px solid var(--farbe); background: transparent;
  transition: width var(--form) var(--kurve), height var(--form) var(--kurve), background-color var(--form) var(--kurve), border-color 200ms var(--kurve); }
.folger.wachsen .form { width: 64px; height: 64px; }
.folger.fuellen .form { width: 96px; height: 96px; background: var(--signal); border-color: var(--signal); }
.folger.fuellen .punkt { background: var(--dunkel); }
.stern { width: 0; height: 0; display: none; }
.stern i { position: absolute; left: 0; top: 0; translate: -50% -50%; border-radius: 50%; border: 1px solid var(--farbe); transition: border-color 200ms var(--kurve); }
.stern:first-of-type i { border-color: var(--signal); }
[data-variante="sieben"] .stern { display: block; }
[data-variante="sieben"] .dehn { display: none; }

/* Demo-Pult: fest unten links */
.pult { position: absolute; right: var(--rand); top: 96px; z-index: 60; display: grid; gap: 8px; padding: 12px 14px; background: rgba(20, 20, 20, .86); color: var(--weiss); border: 1px solid rgba(255, 255, 255, .14); border-radius: 12px; -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); font: 500 10.5px/1.3 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; max-width: calc(100vw - 28px); }
.pult .zeile { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pult .zeile > span { color: var(--meta); min-width: 64px; }
.pult .wahl { display: flex; gap: 2px; padding: 2px; border-radius: 999px; background: rgba(255, 255, 255, .07); }
.pult .wahl button { padding: 6px 10px; border-radius: 999px; font: inherit; letter-spacing: inherit; text-transform: inherit; color: #bdbdbd; transition: background-color 200ms var(--kurve), color 200ms var(--kurve); }
.pult .wahl button[aria-pressed="true"] { background: var(--weiss); color: var(--dunkel); }
.pult .wahl button:focus-visible { outline: 1px solid var(--signal); }
.pult .werte { color: var(--meta); text-transform: none; letter-spacing: .02em; max-width: 46ch; }

/* Vorführung: gezeichneter System-Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 70; width: 18px; height: 18px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
.vorschau body { overflow: hidden; }
.vorschau .held p, .vorschau .navi a:nth-child(n+3) { display: none; }

@media (max-width: 860px) {
  .held { grid-template-columns: 1fr; }
  .liste a { grid-template-columns: 2.4em 1fr auto; }
  .liste .art { display: none; }
}
@media (max-width: 560px) {
  .navi { display: none; }
  .cta { margin-left: auto; padding: 11px 16px; font-size: .9375rem; }
  .marke { font-size: 1.25rem; }
  h1 { font-size: 3.75rem; }
  .liste .name { font-size: 1.75rem; }
  .fuss { flex-direction: column; }
}
/* Touch: kein eigener Cursor, Zeilen zeigen einen Pfeil */
.liste .pfeil { display: none; }
@media (hover: none) {
  .folger { display: none; }
  .liste a { grid-template-columns: 2.4em 1fr auto; }
  .liste .jahr, .liste .art { display: none; }
  .liste .pfeil { display: block; width: 34px; height: 34px; border-radius: 50%; background: var(--signal); align-self: center; display: grid; place-items: center; }
  .liste .pfeil svg { width: 14px; height: 14px; }
}
/* Reduzierte Bewegung: Cursor ganz aus, nur der System-Cursor */
.reduziert .folger { display: none; }
.reduziert *, .reduziert *::after { transition-duration: 0ms !important; }
.reduziert .liste a:hover .name, .reduziert .liste a.an .name { translate: none; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: .12em; }
</style>
</head>
<body data-variante="ring">
<header class="kopf" data-ton="dunkel">
  <a class="marke" href="#start" data-cursor="wachsen"><svg viewBox="0 0 22 22" aria-hidden="true"><g fill="#ff5a1f"><circle cx="11" cy="3" r="2.2"/><circle cx="17.3" cy="6" r="2.2"/><circle cx="18.8" cy="12.8" r="2.2"/><circle cx="14.5" cy="18.2" r="2.2"/><circle cx="7.5" cy="18.2" r="2.2"/><circle cx="3.2" cy="12.8" r="2.2"/><circle cx="4.7" cy="6" r="2.2"/></g></svg>Siebenstern</a>
  <nav class="navi" aria-label="Hauptnavigation">
    <a href="#arbeiten" data-cursor="wachsen">Arbeiten</a><a href="#studio" data-cursor="wachsen">Studio</a><a href="#journal" data-cursor="wachsen">Journal</a>
  </nav>
  <a class="cta" href="#kontakt" data-cursor="weg">Projekt anfragen</a>
</header>

<main id="start">
  <section class="held" data-ton="dunkel">
    <h1>Ernst<br>gemeint,<br>mit <span class="signal">Schmäh.</span></h1>
    <p><span class="mono">Wien-Neubau · seit 2017 · 9 Leute</span>Studio für Marken, Websites und Bewegtbild. Wir arbeiten für Kultur, Handel und alle, die lieber auffallen als gefallen.</p>
  </section>

  <section class="arbeiten" id="arbeiten" data-ton="hell" aria-labelledby="arbeiten-titel">
    <div class="kopfzeile mono"><h2 class="mono" id="arbeiten-titel">Ausgewählte Arbeiten</h2><span>(05)</span></div>
    <ul class="liste" data-liste>
      <li><a href="#halle" data-cursor="fuellen"><span class="mono">01</span><span class="name">Halle Neubau</span><span class="art">Spielzeit 26/27, Kampagne</span><span class="mono jahr">2026</span><span class="pfeil" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M4 12 12 4M5.5 4H12v6.5" fill="none" stroke="#0b0b0b" stroke-width="1.8"/></svg></span></a></li>
      <li><a href="#strudlwerk" data-cursor="fuellen"><span class="mono">02</span><span class="name">Strudlwerk</span><span class="art">Marke und Verpackung</span><span class="mono jahr">2025</span><span class="pfeil" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M4 12 12 4M5.5 4H12v6.5" fill="none" stroke="#0b0b0b" stroke-width="1.8"/></svg></span></a></li>
      <li><a href="#speichenwerk" data-cursor="fuellen"><span class="mono">03</span><span class="name">Speichenwerk Schottenfeld</span><span class="art">Website und Shop</span><span class="mono jahr">2025</span><span class="pfeil" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M4 12 12 4M5.5 4H12v6.5" fill="none" stroke="#0b0b0b" stroke-width="1.8"/></svg></span></a></li>
      <li><a href="#donaustrom" data-cursor="fuellen"><span class="mono">04</span><span class="name">Donaustrom</span><span class="art">Markenauftritt</span><span class="mono jahr">2024</span><span class="pfeil" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M4 12 12 4M5.5 4H12v6.5" fill="none" stroke="#0b0b0b" stroke-width="1.8"/></svg></span></a></li>
      <li><a href="#lerchenfeld" data-cursor="fuellen"><span class="mono">05</span><span class="name">Kunstraum Lerchenfeld</span><span class="art">Ausstellungsdesign</span><span class="mono jahr">2024</span><span class="pfeil" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M4 12 12 4M5.5 4H12v6.5" fill="none" stroke="#0b0b0b" stroke-width="1.8"/></svg></span></a></li>
    </ul>
  </section>

  <section class="abschluss" id="kontakt" data-ton="dunkel">
    <h2>Habt ihr<br>was vor?</h2>
    <a class="mail" href="#kontakt" data-cursor="wachsen">studio@siebenstern.wien</a>
  </section>
  <footer class="fuss mono" data-ton="dunkel"><span>Siebenstern Studio GmbH · Siebensterngasse 7 · 1070 Wien</span><span>Impressum · Datenschutz</span></footer>
</main>

<div class="folger" data-folger aria-hidden="true">
  <div class="dehn" data-dehn><div class="form"></div></div>
  <div class="stern"><i></i></div><div class="stern"><i></i></div><div class="stern"><i></i></div><div class="stern"><i></i></div><div class="stern"><i></i></div><div class="stern"><i></i></div><div class="stern"><i></i></div>
  <div class="punkt" data-punkt></div>
</div>

<div class="pult">
  <div class="zeile"><span>Variante</span><div class="wahl" role="group" aria-label="Variante"><button type="button" data-variante-wahl="ring" aria-pressed="true">Punkt + Ring</button><button type="button" data-variante-wahl="dehnen" aria-pressed="false">Geschwindigkeit</button><button type="button" data-variante-wahl="sieben" aria-pressed="false">Sieben Kreise</button></div></div>
  <div class="zeile"><span>Antrieb</span><div class="wahl" role="group" aria-label="Antrieb"><button type="button" data-antrieb-wahl="lerp" aria-pressed="true">Lerp 1/12</button><button type="button" data-antrieb-wahl="tween" aria-pressed="false">expo.out 550 ms</button></div></div>
  <div class="werte" data-werte></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="#ffffff" stroke="#0b0b0b" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement, body = document.body;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const folger = $('[data-folger]'), punkt = $('[data-punkt]'), dehn = $('[data-dehn]'), zeiger = $('[data-zeiger]');
  const sterne = [...document.querySelectorAll('.stern')];
  const GROESSEN = [30, 24, 19, 15, 12, 9, 7];
  sterne.forEach((s, i) => { const k = s.firstElementChild; k.style.width = k.style.height = GROESSEN[i] + 'px'; });

  const LERP = 1 / 12;          // Dennis Snellenberg: pos += (maus − pos) / 12
  const DAUER = 550;            // Cuberto: gsap.to(pos, { duration: .55, ease: 'expo.out' }) je mousemove
  const KETTE = 0.38;           // Sieben Kreise: jeder Kreis rückt um 0,38 an den vorigen heran
  const DEHN_FAKTOR = 0.0045, DEHN_MAX = 0.42, DEHN_GLATT = 0.2;
  const WERTE = {
    lerp: 'Ring: pos += (maus − pos) × 1/12 pro Bild (60 Hz, auf Bildrate umgerechnet). Punkt 6 px ohne Verzögerung, Ring 30 px.',
    tween: 'Ring: bei jedem mousemove neuer Tween zur Maus, 550 ms expo.out cubic-bezier(0.16, 1, 0.3, 1). Punkt 6 px ohne Verzögerung.'
  };
  const ZUSATZ = { ring: ' Über Links 64 px, über Arbeiten 96 px gefüllt (550 ms expo.out).', dehnen: ' Dehnung: Abstand × 0,0045, höchstens 0,42, in Laufrichtung gedreht.', sieben: ' Sieben Kreise 30 → 7 px, jeder folgt dem vorigen mit 0,38.' };

  let variante = 'ring', antrieb = 'lerp';
  const maus = { x: innerWidth / 2, y: innerHeight / 2 };
  const ist = { x: maus.x, y: maus.y };
  const tw = { vx: 0, vy: 0, zx: 0, zy: 0, t0: 0 };
  const kette = GROESSEN.map(() => ({ x: maus.x, y: maus.y }));
  let dehnS = 0, dehnW = 0, sichtbar = false, letzte = performance.now(), laeuft = false, zustand = '';

  const expoOut = p => p >= 1 ? 1 : 1 - Math.pow(2, -10 * p);

  function ziel(x, y) {
    if (!sichtbar) {               // nicht von der alten Stelle hereinfliegen
      ist.x = x; ist.y = y; kette.forEach(k => { k.x = x; k.y = y; });
    }
    if (antrieb === 'tween') { tw.vx = ist.x; tw.vy = ist.y; tw.zx = x; tw.zy = y; tw.t0 = performance.now(); }
    maus.x = x; maus.y = y;
    zeigen(true);
    kontext(document.elementFromPoint(x, y));
    starten();
  }
  function zeigen(v) { sichtbar = v; folger.classList.toggle('an', v); }

  // Kontext: wachsen über Links, füllen über Arbeiten, weg über dem CTA; Farbe nach Sektion
  function kontext(el) {
    const z = el && el.closest('[data-cursor]');
    const neu = z ? z.dataset.cursor : '';
    if (neu !== zustand) {
      zustand = neu;
      folger.classList.toggle('wachsen', neu === 'wachsen');
      folger.classList.toggle('fuellen', neu === 'fuellen' && variante !== 'sieben');
      folger.style.opacity = neu === 'weg' ? '0' : '';
    }
    if (vorf && !reduziert()) { const zeile = el && el.closest('.liste a'); zeilen().forEach(a => a.classList.toggle('an', a === zeile)); }
    const ton = el && el.closest('[data-ton]');
    folger.classList.toggle('hell', !!ton && ton.dataset.ton === 'hell');
  }

  function bild(t) {
    const dt = Math.min(64, t - letzte) / 16.667; letzte = t;
    const k = 1 - Math.pow(1 - LERP, dt);
    if (antrieb === 'tween') {
      const e = expoOut((t - tw.t0) / DAUER);
      ist.x = tw.vx + (tw.zx - tw.vx) * e; ist.y = tw.vy + (tw.zy - tw.vy) * e;
    } else {
      ist.x += (maus.x - ist.x) * k; ist.y += (maus.y - ist.y) * k;
    }
    punkt.style.transform = `translate3d(${maus.x}px, ${maus.y}px, 0)`;
    // Dehnung nach Geschwindigkeit (Abstand Maus–Ring), geglättet
    const dx = maus.x - ist.x, dy = maus.y - ist.y, d = Math.hypot(dx, dy);
    const sZiel = variante === 'dehnen' ? Math.min(d * DEHN_FAKTOR, DEHN_MAX) : 0;
    const g = 1 - Math.pow(1 - DEHN_GLATT, dt);
    dehnS += (sZiel - dehnS) * g;
    if (d > 0.5) dehnW = Math.atan2(dy, dx) * 180 / Math.PI;
    dehn.style.transform = `translate3d(${ist.x}px, ${ist.y}px, 0) rotate(${dehnW}deg) scale(${1 + dehnS}, ${1 - dehnS * 0.6})`;
    // Sieben Kreise: Kette
    if (variante === 'sieben') {
      const kk = 1 - Math.pow(1 - KETTE, dt);
      kette.forEach((p, i) => {
        const vor = i === 0 ? ist : kette[i - 1];
        if (i === 0) { p.x = ist.x; p.y = ist.y; } else { p.x += (vor.x - p.x) * kk; p.y += (vor.y - p.y) * kk; }
        sterne[i].style.transform = `translate3d(${p.x}px, ${p.y}px, 0)`;
      });
    }
    const letzter = kette[kette.length - 1];
    const ruhig = Math.abs(d) < 0.1 && dehnS < 0.001 && (variante !== 'sieben' || Math.hypot(letzter.x - ist.x, letzter.y - ist.y) < 0.1);
    if (ruhig && !vorf) { laeuft = false; return; }
    requestAnimationFrame(bild);
  }
  function starten() { if (!laeuft) { laeuft = true; letzte = performance.now(); requestAnimationFrame(bild); } }

  // Nur mit Feinzeiger und Hover
  const fein = matchMedia('(hover: hover) and (pointer: fine)');
  addEventListener('pointermove', e => { if (e.pointerType !== 'mouse' || vorf || reduziert() || !fein.matches) return; ziel(e.clientX, e.clientY); }, { passive: true });
  document.addEventListener('pointerleave', () => { if (!vorf) zeigen(false); });
  document.documentElement.addEventListener('mouseleave', () => { if (!vorf) zeigen(false); });
  addEventListener('blur', () => { if (!vorf) zeigen(false); });
  document.addEventListener('visibilitychange', () => { if (document.hidden && !vorf) zeigen(false); });
  addEventListener('scroll', () => { if (sichtbar) kontext(document.elementFromPoint(maus.x, maus.y)); }, { passive: true });
  document.querySelectorAll('a[href^="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));

  // Pult
  const vWahl = [...document.querySelectorAll('[data-variante-wahl]')], aWahl = [...document.querySelectorAll('[data-antrieb-wahl]')];
  function setzeVariante(v) {
    variante = v; body.dataset.variante = v;
    vWahl.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.varianteWahl === v)));
    kette.forEach(k => { k.x = ist.x; k.y = ist.y; });
    zustand = '__'; kontext(document.elementFromPoint(maus.x, maus.y));
    werte();
  }
  function setzeAntrieb(a) {
    antrieb = a; aWahl.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.antriebWahl === a)));
    tw.vx = tw.zx = ist.x; tw.vy = tw.zy = ist.y; tw.t0 = 0;
    werte();
  }
  function werte() { $('[data-werte]').textContent = reduziert() ? 'Reduzierte Bewegung: eigener Cursor aus, nur der System-Cursor. Arbeiten unterstreichen sich bei Hover.' : WERTE[antrieb] + ZUSATZ[variante]; }
  vWahl.forEach(b => b.addEventListener('click', () => { stoppVorf(); setzeVariante(b.dataset.varianteWahl); }));
  aWahl.forEach(b => b.addEventListener('click', () => { stoppVorf(); setzeAntrieb(b.dataset.antriebWahl); }));
  werte();
  addEventListener('load', werte);

  // Vorführung: gedachter Zeiger über Claim, Navigation und Arbeiten; jede Runde andere Variante
  let uhr = null, vorf = false, t0 = 0, stand = 0, runde = -1;
  const RUNDE = 7000, FOLGE = ['ring', 'dehnen', 'sieben'];
  const zeilen = () => [...document.querySelectorAll('.liste a')];
  function bahn(p) {
    // Stationen in Bildschirm-Koordinaten, weich verbunden
    const h1 = $('h1').getBoundingClientRect(), z = zeilen(), nav = $('.navi a') || $('.marke');
    const zr = i => { const r = z[Math.min(i, z.length - 1)].getBoundingClientRect(); return [r.left + Math.min(r.width * .32, 420), r.top + r.height / 2]; };
    const nr = nav.getBoundingClientRect();
    const S = [
      [h1.left + h1.width * .08, h1.top + h1.height * .2],
      [h1.left + h1.width * .55, h1.top + h1.height * .5],
      zr(0), [innerWidth * .72, zr(0)[1] + 4], zr(1),
      [nr.left + nr.width / 2, nr.top + nr.height / 2],
      [h1.left + h1.width * .3, h1.top + h1.height * .82],
      [h1.left + h1.width * .08, h1.top + h1.height * .2]
    ].map(([x, y]) => [Math.max(20, Math.min(innerWidth - 20, x)), Math.max(20, Math.min(innerHeight - 20, y))]);
    const n = S.length - 1, f = p * n, i = Math.min(n - 1, Math.floor(f)), q = f - i;
    const e = q < .5 ? 4 * q * q * q : 1 - Math.pow(-2 * q + 2, 3) / 2;     // Zeiger bewegt sich wie eine Hand: an- und abschwellend
    return [S[i][0] + (S[i + 1][0] - S[i][0]) * e, S[i][1] + (S[i + 1][1] - S[i][1]) * e];
  }
  function vorfuehren(ab) {
    pause();
    vorf = true; html.classList.add('vorfuehrung');
    t0 = performance.now() - (ab || 0); runde = -1;
    if (reduziert()) {           // ohne eigenen Cursor: Zeilen nacheinander hervorheben
      zeiger.style.display = 'none';
      let i = 0;
      const tick = () => { zeilen().forEach((a, j) => a.classList.toggle('an', j === i % 5)); i++; uhr = setTimeout(tick, 1400); };
      tick(); return;
    }
    zeiger.style.display = '';
    const schritt = t => {
      const z = Math.max(0, t - t0), r = Math.floor(z / RUNDE);
      if (r !== runde) { runde = r; setzeVariante(FOLGE[r % FOLGE.length]); }
      const [x, y] = bahn((z % RUNDE) / RUNDE);
      zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`;
      sichtbar = true; folger.classList.add('an');
      if (antrieb === 'tween') { tw.vx = ist.x; tw.vy = ist.y; tw.zx = x; tw.zy = y; tw.t0 = t; }
      maus.x = x; maus.y = y;
      kontext(document.elementFromPoint(x, y));
      starten();
      uhr = requestAnimationFrame(schritt);
    };
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { if (uhr) stand = performance.now() - t0; cancelAnimationFrame(uhr); clearTimeout(uhr); uhr = null; }
  function stoppVorf() { if (!vorf) return; pause(); vorf = false; html.classList.remove('vorfuehrung'); zeilen().forEach(a => a.classList.remove('an')); }
  function neustart() { stoppVorf(); setzeVariante('ring'); setzeAntrieb('lerp'); zeigen(false); }
  function weiter() { if (!uhr && vorf) vorfuehren(stand); }

})();
</script>
</body>
</html>