Icon-Squash-and-Stretch (Pfeil dehnt sich)
Der Pfeil im Button streckt sich beim Hover: Der Schaft wird länger, die Spitze rückt vor und wird schmaler. Das wirkt gezeichnet statt mechanisch.
Ein Icon im Button, meist ein Pfeil, reagiert beim Hover wie ein elastischer Gegenstand. Statt nur nach rechts zu rutschen, wird der Schaft länger, die Spitze rückt vor und wird schmaler. Das folgt dem Prinzip Squash and Stretch aus dem Zeichentrick: Das Volumen bleibt erhalten, wer sich streckt, wird schmaler. Umsetzen lässt es sich als Pfad-Morph (d-Transition), mit Transforms auf Schaft und Spitze oder als kurzer Feder-Impuls.
Der Pfeil wirkt lebendig und gezeichnet statt mechanisch. Er nimmt die Richtung der Aktion vorweg, bevor man klickt. Ein kleines Detail, das generische Pfeil-Buttons sofort aufwertet.
Wann einsetzen
CTA-Pfeile, Weiter-Links, Listen mit Detailseiten, Download-Icons, Links nach außen, Chevrons in Akkordeons.
Wann nicht
Bei Icons ohne klare Bewegungsrichtung. Zu starke Werte oder eine lange Feder wirken cartoonhaft. In ruhigen Branchen den Pfad-Morph ohne Überschwingen wählen.
Varianten
- Pfad-Morph: transition: d 300 ms, Schaft h14 → h17, Spitze 3 nach rechts und schmaler (Josh W. Comeau). Nicht in Safari.
- Schaft + Spitze: kurzer Schaft scaleX 2 (extern 1.2), Spitze translateX 200 %, 200 ms quad.out nach 200 ms Verzögerung (14islands). Läuft in allen Browsern.
- Feder-Impuls: bei pointerenter dehnen, nach 150 ms zurückfedern, Feder stiffness 300 / damping 12. Der Pfeil schwingt kurz über und staucht sich dabei.
- Ganzes Bedienelement staucht und dehnt sich: Badge scale(1.5, .8) ↔ scale(.667, 1.25); bei Fiddle.Digital scaleX 1.45 / scaleY 1.55 mit linear()-Feder über 900–1050 ms.
Umsetzung
SVG mit zwei Pfaden, Schaft und Spitze, in einer 24er-ViewBox. Pfad-Morph: .btn .schaft { d: path('M 5,12 h 14'); transition: d 300ms cubic-bezier(0.5, 1, 0.89, 1) } .btn:hover .schaft { d: path('M 5,12 h 17') } .btn .spitze { d: path('M 12,5 l 7,7 l -7,7'); transition: d 300ms cubic-bezier(0.5, 1, 0.89, 1) } .btn:hover .spitze { d: path('M 15,7 l 7,5 l -7,5') }. Ruhe- und Dehnform brauchen dieselben Pfadbefehle, sonst springt die Form. Safari kennt d nicht als CSS-Eigenschaft: dort die Transform-Variante nehmen (Schaft mit transform-box: fill-box, transform-origin links, scale: 2 1; Spitze translate: 200% 0) oder d per JS mit einer Feder setzen. Dasselbe SVG dient mit rotate(90) als Download-Pfeil und mit rotate(-45) als Link nach außen. Hover-Regeln in @media (hover: hover), :focus-visible gleich behandeln. Bewegung nur bei prefers-reduced-motion: no-preference.
Robust umsetzen
Telefon. Kein Hover: beim Tippen kurz abspielen. pointerdown dehnt, nach mindestens 450 ms federt der Pfeil zurück. Hover-Regeln nur unter @media (hover: hover), damit nach dem Tippen nichts gedehnt hängen bleibt.
Ohne Bewegung. Keine Pfadänderung. Als Rückmeldung bleibt der Farbwechsel von Button oder Link.
Barrierefrei. Icon mit aria-hidden="true". Der Text von Button oder Link trägt die Bedeutung, reine Icon-Buttons brauchen ein aria-label. :focus-visible löst dieselbe Dehnung aus, der Fokusring bleibt sichtbar.
Code
<!DOCTYPE html>
<!--
Icon-Squash-and-Stretch (Pfeil dehnt sich)
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/icon-squash-stretch/
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>Icon-Squash-and-Stretch (Pfeil dehnt sich) · 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: 'Hedvig Letters Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hedvig-letters-serif:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; 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; }
/* Welt: Therapiehaus Saggen, Physiotherapie und Osteopathie in Innsbruck (Rezept praxis-gesundheit-ruhig) */
:root {
--grund: #f5f7f7; --flaeche: #e2e0de; --marke: #005450; --marke-tief: #003f3c;
--text: #363b3b; --leise: #596666; --linie: rgba(54, 59, 59, .14); --akzent: #ffc776;
--kurve: cubic-bezier(0.5, 1, 0.89, 1); /* quad.out, gemessen (Pfad-Morph und 14islands) */
--ui: cubic-bezier(0.2, 0, 0.38, 0.9); /* Rezept: UI-Feedback 150 ms */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 'Google Sans Flex', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
a { color: inherit; text-decoration: none; }
a:focus-visible, button:focus-visible { outline: 3px solid var(--marke); outline-offset: 4px; border-radius: 6px; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.serif { font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; font-optical-sizing: auto; }
/* ---------- Der Effekt: Pfeil aus Schaft und Spitze ---------- */
.pfeil { width: 1.25em; height: 1.25em; flex: none; overflow: visible; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.pfeil.lade { height: 1.45em; } /* Download: Platz für die Ablage unter dem Pfeil */
/* Variante 1 · Pfad-Morph: d per CSS überblenden, 300 ms quad.out */
[data-variante='morph'] .schaft { d: path('M 5,12 h 14'); transition: d 300ms var(--kurve); }
[data-variante='morph'] .spitze { d: path('M 12,5 l 7,7 l -7,7'); transition: d 300ms var(--kurve); }
[data-variante='morph'] .dehnt.gedehnt .schaft,
[data-variante='morph'] .dehnt:focus-visible .schaft { d: path('M 5,12 h 17'); }
[data-variante='morph'] .dehnt.gedehnt .spitze,
[data-variante='morph'] .dehnt:focus-visible .spitze { d: path('M 15,7 l 7,5 l -7,5'); }
/* Variante 2 · Schaft + Spitze (wie 14islands): kurzer Schaft scaleX 2, Spitze wandert 200 %,
200 ms quad.out nach 200 ms Verzögerung; Transforms laufen auch in Safari */
[data-variante='teile'] .schaft { d: path('M 5,12 h 8'); transform-box: fill-box; transform-origin: left center; transition: scale 200ms var(--kurve); }
[data-variante='teile'] .spitze { d: path('M 9,8 l 4,4 l -4,4'); transform-box: fill-box; transition: translate 200ms var(--kurve); }
[data-variante='teile'] .dehnt.gedehnt .schaft,
[data-variante='teile'] .dehnt:focus-visible .schaft { scale: 2 1; transition-delay: 200ms; }
[data-variante='teile'] .dehnt.gedehnt .spitze,
[data-variante='teile'] .dehnt:focus-visible .spitze { translate: 200% 0; transition-delay: 200ms; }
[data-variante='teile'] .extern.gedehnt .schaft,
[data-variante='teile'] .extern:focus-visible .schaft { scale: 1.2 1; }
[data-variante='teile'] .extern.gedehnt .spitze,
[data-variante='teile'] .extern:focus-visible .spitze { translate: 40% 0; }
/* Variante 3 · Feder-Impuls: JS setzt d pro Bild (Feder 300/12), CSS greift hier nicht ein */
/* Echter Hover nur bei Geräten mit Zeiger; Touch bekommt .gedehnt per Tippen */
@media (hover: hover) {
[data-variante='morph'] .dehnt:hover .schaft { d: path('M 5,12 h 17'); }
[data-variante='morph'] .dehnt:hover .spitze { d: path('M 15,7 l 7,5 l -7,5'); }
[data-variante='teile'] .dehnt:hover .schaft { scale: 2 1; transition-delay: 200ms; }
[data-variante='teile'] .dehnt:hover .spitze { translate: 200% 0; transition-delay: 200ms; }
[data-variante='teile'] .extern:hover .schaft { scale: 1.2 1; }
[data-variante='teile'] .extern:hover .spitze { translate: 40% 0; }
}
/* Ablage des Download-Pfeils: staucht minimal, wenn der Pfeil ankommt */
.ablage { transform-box: fill-box; transform-origin: center; transition: scale 300ms var(--kurve); }
[data-variante='morph'] .dehnt.gedehnt .ablage, [data-variante='teile'] .dehnt.gedehnt .ablage { scale: 1.12 1; }
@media (hover: hover) { [data-variante='morph'] .dehnt:hover .ablage, [data-variante='teile'] .dehnt:hover .ablage { scale: 1.12 1; } }
/* ---------- Seite ---------- */
.seite { max-width: 1320px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 56px); min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto auto; }
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: clamp(16px, 2.4vh, 26px) 0; }
.marke { display: flex; align-items: center; gap: 12px; }
.marke svg { width: 34px; height: 34px; flex: none; }
.marke b { display: block; font: 400 21px/1.05 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.01em; color: var(--marke-tief); }
.marke small { display: block; font-size: 13px; line-height: 1.3; color: var(--leise); }
.nav { display: flex; gap: clamp(16px, 2vw, 30px); font-size: 15px; font-weight: 500; }
.nav a { color: var(--leise); transition: color 150ms var(--ui); }
.nav a:hover { color: var(--marke); }
.kopf-rechts { display: flex; align-items: center; gap: 18px; font-size: 15px; font-weight: 500; }
.tel { color: var(--marke); white-space: nowrap; }
.knopf { display: inline-flex; align-items: center; gap: .6em; height: 52px; padding: 0 22px 0 26px; border-radius: 999px; background: var(--marke); color: #fff; font: 500 16px/1 'Google Sans Flex', sans-serif; white-space: nowrap; transition: background-color 150ms var(--ui); }
.knopf:hover { background: var(--marke-tief); }
.knopf.klein { height: 44px; padding: 0 16px 0 20px; font-size: 15px; }
.held { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: clamp(28px, 5vw, 80px); align-items: center; padding: clamp(12px, 3vh, 40px) 0 clamp(24px, 5vh, 56px); }
.ort { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 500; color: var(--marke); margin-bottom: clamp(14px, 2.4vh, 26px); }
.ort::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--akzent); box-shadow: 0 0 0 4px rgba(255, 199, 118, .35); }
h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.5rem)/1.06 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.015em; color: var(--marke-tief); text-wrap: balance; }
h1 span { display: block; color: var(--leise); }
.lead { max-width: 40ch; margin-top: clamp(14px, 2.4vh, 26px); color: var(--text); text-wrap: pretty; }
.aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 28px; margin-top: clamp(20px, 3.6vh, 36px); }
.lade-link { display: inline-flex; align-items: center; gap: 12px; font-weight: 500; color: var(--marke); transition: color 150ms var(--ui); }
.lade-link .pfeil { width: 22px; height: 26px; }
.lade-link small { display: block; font-size: 13px; font-weight: 400; color: var(--leise); line-height: 1.2; }
.lade-link span { line-height: 1.25; }
.kasse { margin-top: clamp(18px, 3.4vh, 34px); padding-top: 16px; border-top: 1px solid var(--linie); max-width: 46ch; font-size: 15px; line-height: 1.5; color: var(--leise); }
/* Terminkarte mit eigener Bildwelt: Nordkette in Petrol */
.karte { position: relative; aspect-ratio: 1.12; max-height: min(54vh, 520px); justify-self: stretch; border-radius: 28px; overflow: hidden; background: var(--marke); color: #fff; isolation: isolate; }
.karte > svg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; }
.karte-text { position: absolute; left: clamp(20px, 2.6vw, 36px); top: clamp(20px, 2.6vw, 34px); right: 20px; }
.karte-text .label { font-size: 14px; font-weight: 500; color: var(--akzent); }
.karte-text .tag { font: 400 clamp(24px, 2.3vw, 36px)/1.1 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.01em; margin-top: 8px; }
.karte-text .wer { margin-top: 10px; font-size: 15px; line-height: 1.45; color: rgba(255, 255, 255, .78); }
.uebernehmen { position: absolute; right: clamp(16px, 2.2vw, 30px); bottom: clamp(16px, 2.2vw, 30px); display: flex; align-items: center; gap: 16px; font-weight: 500; font-size: 16px; color: #fff; }
.rund { width: clamp(76px, 8.4vw, 124px); aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; background: #fff; color: var(--marke); transition: background-color 150ms var(--ui); }
.rund .pfeil { width: 46%; height: 46%; stroke-width: 1.5; }
[data-variante='teile'] .rund .pfeil { translate: 12.5% 0; } /* kurzer Ruhepfeil sitzt mittig im Kreis */
.uebernehmen:hover .rund { background: var(--akzent); }
.uebernehmen:focus-visible { outline-color: var(--akzent); border-radius: 999px; }
.hilfe { border-top: 1px solid var(--linie); padding: clamp(14px, 2vh, 22px) 0 clamp(14px, 2.4vh, 26px); }
.hilfe h2 { font-size: 14px; font-weight: 500; color: var(--leise); margin-bottom: clamp(6px, 1vh, 12px); }
.hilfe ul { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0 clamp(16px, 2.4vw, 36px); }
.hilfe a { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 4px 16px; padding: 12px 0; border-bottom: 1px solid transparent; transition: border-color 150ms var(--ui); }
.hilfe a b { font: 400 clamp(19px, 1.4vw, 21px)/1.25 'Hedvig Letters Serif', Georgia, serif; color: var(--marke-tief); }
.hilfe a small { grid-column: 1; font-size: 14px; line-height: 1.35; color: var(--leise); }
.hilfe a .pfeil { grid-row: 1; grid-column: 2; align-self: center; width: 26px; height: 26px; color: var(--marke); stroke-width: 1.8; }
.hilfe a:hover { border-bottom-color: var(--marke); }
.fuss { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 28px; padding: 14px 0; border-top: 1px solid var(--linie); font-size: 14px; color: var(--leise); }
.fuss .extern { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; color: var(--marke); }
.fuss .extern .pfeil { width: 18px; height: 18px; }
.fuss .notfall { margin-left: auto; }
.fuss .notfall b { color: var(--text); font-weight: 500; }
/* Demo-Regler: dezent, unter der Seite */
.regler { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 24px; padding: 12px 0 18px; border-top: 1px dashed var(--linie); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.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 150ms var(--ui), background-color 150ms var(--ui), border-color 150ms var(--ui); }
.modi button:hover { color: var(--text); border-color: rgba(54, 59, 59, .3); }
.modi button[aria-pressed='true'] { background: var(--marke); color: #fff; border-color: var(--marke); }
.werte { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--text); font-weight: 500; text-transform: none; letter-spacing: .02em; }
.leiste { display: none; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 20px; height: 20px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .regler .modi { display: none; }
.vorschau .regler { position: fixed; left: 0; right: 0; bottom: 0; z-index: 7; padding: 12px clamp(16px, 4vw, 56px); background: var(--grund); border-top: 1px solid var(--linie); }
/* Reduzierte Bewegung: keine Pfadänderung, nur der Farbwechsel bleibt */
.reduziert .schaft, .reduziert .spitze, .reduziert .ablage { transition: none !important; scale: none !important; translate: none !important; }
.reduziert [data-variante] .dehnt .schaft { d: path('M 5,12 h 14') !important; }
.reduziert [data-variante] .dehnt .spitze { d: path('M 12,5 l 7,7 l -7,7') !important; }
.reduziert .zeiger { transition: none; }
@media (max-width: 1080px) {
.nav { display: none; }
}
@media (max-width: 860px) {
.held { grid-template-columns: 1fr; }
.karte { max-height: none; aspect-ratio: 1.25; }
.hilfe ul { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 600px) {
html { font-size: 17px; }
.seite { grid-template-rows: auto; padding-bottom: 84px; }
.kopf-rechts { display: none; } /* Termin und Telefon stehen mobil in der unteren Leiste */
.marke small { display: none; }
.knopf.klein { height: 40px; padding: 0 12px 0 16px; }
.held { padding-top: 8px; }
.karte { aspect-ratio: auto; min-height: 330px; border-radius: 22px; }
.karte-text { right: 20px; }
.hilfe ul { grid-template-columns: 1fr; }
.hilfe li + li a { border-top: 1px solid var(--linie); }
.hilfe a { padding: 14px 0; }
.fuss .notfall { margin-left: 0; }
.regler { flex-direction: column; align-items: flex-start; }
/* Untere Leiste wie im Rezept: Anrufen / Termin / Anfahrt */
.leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 8; display: grid; grid-template-columns: 1fr 1.4fr 1fr; gap: 8px; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: rgba(245, 247, 247, .94); backdrop-filter: blur(10px); border-top: 1px solid var(--linie); }
.leiste a { display: flex; align-items: center; justify-content: center; gap: 8px; height: 48px; border-radius: 14px; font-size: 15px; font-weight: 500; color: var(--marke); background: #fff; border: 1px solid var(--linie); }
.leiste a.termin { background: var(--marke); color: #fff; border-color: var(--marke); }
.leiste .pfeil { width: 20px; height: 20px; }
}
</style>
</head>
<body data-variante="morph">
<div class="seite">
<header class="kopf">
<a class="marke" href="#start" aria-label="Therapiehaus Saggen, Startseite">
<svg viewBox="0 0 34 34" aria-hidden="true"><circle cx="17" cy="17" r="17" fill="#005450"/><path d="M6 23.5 13 13l4.2 5.6L20.5 15 28 23.5" fill="none" stroke="#fff" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"/><circle cx="23.5" cy="10.5" r="2.4" fill="#ffc776"/></svg>
<span><b>Therapiehaus Saggen</b><small>Physiotherapie · Osteopathie</small></span>
</a>
<nav class="nav" aria-label="Hauptnavigation"><a href="#behandlungen">Behandlungen</a><a href="#team">Team</a><a href="#kosten">Kosten & Kassen</a><a href="#anfahrt">Anfahrt</a></nav>
<div class="kopf-rechts">
<a class="tel" href="#anrufen">0512 58 47 20</a>
<a class="knopf klein dehnt" href="#termin" data-ziel>Termin<svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a>
</div>
</header>
<main class="held" id="start">
<div class="held-text">
<p class="ort">Innsbruck-Saggen · fünf Minuten vom Hofgarten</p>
<h1>Wieder gut gehen. <span>In Ihrem Tempo.</span></h1>
<p class="lead">Wir behandeln Rücken, Gelenke und alles, was nach einer Operation wieder in Gang kommen soll. Neue Patientinnen und Patienten bekommen meist innerhalb einer Woche einen Termin.</p>
<div class="aktionen">
<a class="knopf dehnt" href="#termin" data-ziel="1">Termin vereinbaren<svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a>
<a class="lade-link dehnt" href="#anamnesebogen" data-ziel="2"><svg class="pfeil lade" viewBox="0 0 24 28" aria-hidden="true"><g transform="rotate(90 12 12)"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></g><path class="ablage" d="M5 26.5h14"/></svg><span>Anamnesebogen herunterladen<small>PDF · 2 Seiten · vorab ausfüllen</small></span></a>
</div>
<p class="kasse" id="kosten">Mit ärztlicher Verordnung erstattet die ÖGK einen Teil der Kosten. Wir helfen beim Antrag.</p>
</div>
<aside class="karte" aria-label="Nächster freier Termin">
<svg viewBox="0 0 560 500" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<defs>
<linearGradient id="himmel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#005450"/><stop offset="1" stop-color="#0d6b63"/></linearGradient>
</defs>
<rect width="560" height="500" fill="url(#himmel)"/>
<circle cx="420" cy="250" r="150" fill="none" stroke="rgba(255,255,255,.16)" stroke-width="1.2"/>
<circle cx="430" cy="262" r="44" fill="#ffc776"/>
<path d="M0 370 L58 336 96 348 150 302 186 316 232 276 262 292 300 254 336 280 372 262 410 296 452 278 500 310 560 292 V500 H0Z" fill="#2f7f76"/>
<path d="M0 370 L58 336 96 348 150 302 186 316 232 276 262 292 300 254 336 280 372 262 410 296 452 278 500 310 560 292" fill="none" stroke="#e8f1ee" stroke-width="2" stroke-linejoin="round" opacity=".55"/>
<path d="M96 440 L300 254" stroke="rgba(255,255,255,.5)" stroke-width="1"/>
<g transform="translate(196 349)"><path d="M0 0v7" stroke="rgba(255,255,255,.6)" stroke-width="1"/><rect x="-7" y="7" width="14" height="10" rx="2.5" fill="#ffc776"/></g>
<path d="M0 404 C70 380 120 392 180 372 S300 352 360 372 S470 392 560 366 V500 H0Z" fill="#14625b"/>
<path d="M0 446 C90 432 170 442 260 428 S430 422 560 436 V500 H0Z" fill="#0a4e4a"/>
<g fill="#0a4e4a"><path d="M40 446 48 418 56 446Z"/><path d="M58 444 68 410 78 444Z"/></g>
</svg>
<div class="karte-text">
<p class="label">Nächster freier Termin</p>
<p class="tag">Mittwoch, 4. November<br>08:30 Uhr</p>
<p class="wer">Physiotherapie, 50 Minuten<br>bei Theresa Gruber</p>
</div>
<a class="uebernehmen dehnt" href="#termin-uebernehmen" data-ziel="0"><span>Termin übernehmen</span><span class="rund"><svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></span></a>
</aside>
</main>
<section class="hilfe" id="behandlungen" aria-labelledby="hilfe-titel">
<h2 id="hilfe-titel">Wobei wir helfen</h2>
<ul>
<li><a class="dehnt" href="#ruecken" data-ziel="3"><b>Rückenschmerzen</b><small>Lumbalgie, Bandscheibenvorfall</small><svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a></li>
<li><a class="dehnt" href="#operation"><b>Nach einer Operation</b><small>Reha nach Knie- und Hüft-OP</small><svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a></li>
<li><a class="dehnt" href="#kiefer" data-ziel="4"><b>Kiefer und Kopfschmerz</b><small>Craniomandibuläre Dysfunktion</small><svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a></li>
<li><a class="dehnt" href="#sport"><b>Sportverletzungen</b><small>Bänder, Sehnen, Muskeln</small><svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a></li>
</ul>
</section>
<footer class="fuss" id="anfahrt">
<span>Conradstraße 14, 6020 Innsbruck</span>
<a class="extern dehnt" href="#route" data-ziel="5">Route planen<svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><g transform="rotate(-45 12 12)"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></g></svg></a>
<span>Mo–Fr 7–19 Uhr · Lift, ebenerdiger Eingang</span>
<span class="notfall">Notfall <b>144</b> · Ärztenotdienst <b>141</b></span>
</footer>
<div class="regler">
<div class="modi mono" role="group" aria-label="Variante">
<button type="button" data-modus="morph" aria-pressed="true">Pfad-Morph</button>
<button type="button" data-modus="teile" aria-pressed="false">Schaft + Spitze</button>
<button type="button" data-modus="feder" aria-pressed="false">Feder-Impuls</button>
</div>
<p class="werte mono" data-werte aria-live="polite"></p>
</div>
</div>
<nav class="leiste" aria-label="Schnellzugriff">
<a href="#anrufen">Anrufen</a>
<a class="termin dehnt" href="#termin">Termin<svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></svg></a>
<a class="dehnt extern" href="#route">Anfahrt<svg class="pfeil" viewBox="0 0 24 24" aria-hidden="true"><g transform="rotate(-45 12 12)"><path class="schaft" d="M5 12h14"/><path class="spitze" d="M12 5l7 7-7 7"/></g></svg></a>
</nav>
<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="#363b3b" stroke="#f5f7f7" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const ausloeser = [...document.querySelectorAll('.dehnt')];
const werte = document.querySelector('[data-werte]');
const knoepfe = [...document.querySelectorAll('[data-modus]')];
const zeiger = document.querySelector('[data-zeiger]');
let variante = 'morph';
// Ruhe- und Dehnform des Pfeils in der 24er-ViewBox (gemessen bei Josh W. Comeau):
// Schaft h14 → h17, Spitze 3 nach rechts, halbe Öffnung 7 → 5. s = 0 Ruhe, 1 gedehnt,
// s < 0 gestaucht (kürzer und breiter: das Volumen bleibt erhalten).
const schaftD = s => `M5 12h${(14 + 3 * s).toFixed(2)}`;
const spitzeD = s => `M${(12 + 3 * s).toFixed(2)} ${(5 + 2 * s).toFixed(2)}l7 ${(7 - 2 * s).toFixed(2)}-7 ${(7 - 2 * s).toFixed(2)}`;
const WERTE = {
morph: '<span>Pfad-Morph <b>transition: d 300 ms</b></span><span>Kurve <b>cubic-bezier(0.5, 1, 0.89, 1)</b> quad.out</span><span>Schaft <b>h14 → h17</b></span><span>Spitze <b>+3, Öffnung 7 → 5</b></span>',
teile: '<span>Schaft <b>scaleX 2</b> (extern 1.2)</span><span>Spitze <b>translateX 200 %</b></span><span><b>200 ms quad.out</b> nach <b>200 ms</b></span>',
feder: '<span>Feder <b>stiffness 300 · damping 12 · mass 1</b></span><span>Rückfederung nach <b>150 ms</b></span><span>Ausschlag <b>+1.24 / −0.39</b>, Ruhe nach ca. 1 s</span>',
};
function zeigeWerte() {
werte.innerHTML = reduziert()
? '<span>Reduzierte Bewegung: <b>keine Pfadänderung</b></span><span>nur Farbwechsel <b>150 ms</b></span>'
: WERTE[variante];
}
// ---------- Feder-Impuls: Feder 300/12 auf s, Ziel 1 für 150 ms, dann 0 ----------
const FEDER = { k: 300, c: 12, m: 1, halten: 150 };
const zustand = new Map(); // svg → { s, v, ziel, bis }
let federLaeuft = false, letzte = 0;
function malen(svg, s) {
svg.querySelector('.schaft').setAttribute('d', schaftD(s));
svg.querySelector('.spitze').setAttribute('d', spitzeD(s));
}
function impuls(el) {
if (reduziert() || variante !== 'feder') return;
const t = performance.now();
el.querySelectorAll('.pfeil').forEach(svg => {
const z = zustand.get(svg) || { s: 0, v: 0 };
z.ziel = 1; z.bis = t + FEDER.halten;
zustand.set(svg, z);
});
if (!federLaeuft) { federLaeuft = true; letzte = t; requestAnimationFrame(federSchritt); }
}
function federSchritt(t) {
const dt = Math.min(48, t - letzte) / 1000; letzte = t;
const n = Math.max(1, Math.ceil(dt / 0.002)), h = dt / n; // feste Teilschritte, stabil bei jeder Bildrate
zustand.forEach((z, svg) => {
if (t >= z.bis) z.ziel = 0;
for (let i = 0; i < n; i++) { const a = (-FEDER.k * (z.s - z.ziel) - FEDER.c * z.v) / FEDER.m; z.v += a * h; z.s += z.v * h; }
if (z.ziel === 0 && Math.abs(z.s) < 0.002 && Math.abs(z.v) < 0.02) { malen(svg, 0); zustand.delete(svg); }
else malen(svg, z.s);
});
if (zustand.size) requestAnimationFrame(federSchritt); else federLaeuft = false;
}
function federRuhe() { zustand.forEach((z, svg) => malen(svg, 0)); zustand.clear(); }
// ---------- Auslöser ----------
ausloeser.forEach(el => {
el.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') impuls(el); });
el.addEventListener('focus', () => { if (el.matches(':focus-visible')) impuls(el); });
// Touch: kurz abspielen. .gedehnt hält mindestens 450 ms (300 ms Weg + 150 ms Stand)
let seit = 0, aus = null;
el.addEventListener('pointerdown', e => {
if (e.pointerType === 'mouse') return;
clearTimeout(aus); seit = performance.now();
if (variante === 'feder') impuls(el); else if (!reduziert()) el.classList.add('gedehnt');
});
const los = e => {
if (e.pointerType === 'mouse') return;
const rest = Math.max(0, 450 + (variante === 'teile' ? 200 : 0) - (performance.now() - seit));
aus = setTimeout(() => el.classList.remove('gedehnt'), rest);
};
el.addEventListener('pointerup', los);
el.addEventListener('pointercancel', los);
el.addEventListener('click', e => e.preventDefault()); // Demo: Links führen nirgendwohin
});
function setzeVariante(v) {
variante = v;
federRuhe();
// Attribute auf die Ruheform zurück; CSS übernimmt in morph/teile die Geometrie
document.querySelectorAll('.pfeil').forEach(svg => malen(svg, 0));
document.body.dataset.variante = v;
knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === v)));
zeigeWerte();
}
knoepfe.forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.modus)));
// ---------- Vorführung: ein gedachter Zeiger fährt die Pfeile ab ----------
const REIHE = [...document.querySelectorAll('[data-ziel]')]
.filter(el => el.dataset.ziel !== '')
.sort((a, b) => a.dataset.ziel - b.dataset.ziel);
const VARIANTEN = ['morph', 'teile', 'feder'];
let uhr = null, runde = 0, laeuft = false;
const zp = { x: 0, y: 0 };
const warte = ms => new Promise(r => { uhr = setTimeout(r, ms); });
function zeigerZu(x, y) { zp.x = x; zp.y = y; zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`; }
function fahre(x, y, ms) {
return new Promise(r => {
const x0 = zp.x, y0 = zp.y, t0 = performance.now();
const schritt = t => {
if (!laeuft) return r();
const p = Math.min(1, (t - t0) / ms), e = 1 - Math.pow(1 - p, 3);
zeigerZu(x0 + (x - x0) * e, y0 + (y - y0) * e);
if (p < 1) requestAnimationFrame(schritt); else r();
};
requestAnimationFrame(schritt);
});
}
async function schleife() {
const meine = ++runde;
while (laeuft && meine === runde) {
for (const el of REIHE) {
if (!laeuft || meine !== runde) return;
const r = el.querySelector('.pfeil').getBoundingClientRect();
if (r.bottom > innerHeight || r.top < 0 || !r.width) continue;
// Zeiger links unter den Pfeil, damit er die Dehnung nicht verdeckt
const rund = el.querySelector('.rund');
const x = rund ? rund.getBoundingClientRect().left + 10 : r.left - r.width * 0.7 - 6;
await fahre(x, r.top + r.height * (rund ? 1.25 : 0.8), 650);
if (!laeuft || meine !== runde) return;
el.classList.add('gedehnt'); impuls(el);
await warte(variante === 'teile' ? 1150 : 950);
el.classList.remove('gedehnt');
await warte(380);
}
if (!laeuft || meine !== runde) return;
// nächste Runde mit der nächsten Variante
setzeVariante(VARIANTEN[(VARIANTEN.indexOf(variante) + 1) % VARIANTEN.length]);
await warte(500);
}
}
function aufraeumen() {
laeuft = false; runde++; clearTimeout(uhr);
ausloeser.forEach(el => el.classList.remove('gedehnt'));
}
function vorfuehren() {
aufraeumen();
html.classList.add('vorfuehrung');
zeigerZu(innerWidth * 0.3, innerHeight * 0.92);
laeuft = true;
schleife();
}
function pause() { aufraeumen(); }
function weiter() { if (html.classList.contains('vorfuehrung')) vorfuehren(); }
function neustart() {
aufraeumen();
html.classList.remove('vorfuehrung');
setzeVariante(variante);
}
zeigeWerte();
// Wechselt die Detailseite auf reduzierte Bewegung, Werte nachziehen
new MutationObserver(zeigeWerte).observe(html, { attributes: true, attributeFilter: ['class'] });
})();
</script>
</body>
</html>