Projektziel: Animierte SVG-Szenen im Browser. Bei der Szene mit Taube, Hase und Hirsch werden die Linien gezeichnet, als würde jemand mit dem Stift darüberfahren, danach füllen sich die Formen. Die zweite Szene zeigt eine Mühle bei Nacht mit Mond, Wolken, Licht und rotierenden Rädern. SVG-Grafiken bleiben dabei in jeder Größe scharf und lassen sich mit CSS animieren.

Die erste Version (August 2024) lud vier SVG-Grafiken aus einer einzigen großen JSON-Datei. Die Projektseite wurde inzwischen neu aufgebaut und ist am 20.09.2026 von micha-it.de auf diese Seite umgezogen. Dieser Beitrag beschreibt die neue Version.

Animierte SVG-Szene mit Taube, Hase und Hirsch
Bild 1 - Die Szene „Taube, Hase und Hirsch“ nach dem Zeichnen

Projektlink: Animierte SVG-Szenen

Der Zeichen-Effekt

Die Animation simuliert das Zeichnen der Konturen. Dafür genügen zwei SVG-Eigenschaften und eine CSS-Animation:

  • stroke-dasharray legt das Muster der gestrichelten Linie fest: Strichlänge und Lücke. Ist die Strichlänge mindestens so groß wie der Pfad, ist die Linie durchgehend.
  • stroke-dashoffset verschiebt den Anfang des Musters. Ist der Wert groß genug, liegt der sichtbare Strich komplett außerhalb des Pfades, und die Linie ist unsichtbar.
  • Eine @keyframes-Animation setzt stroke-dashoffset allmählich auf 0. Der Strich wandert in den Pfad hinein, und die Linie erscheint, als würde sie gezeichnet.

Im SVG steckt jede Figur in einer Gruppe mit einer ID (<g id='taube1'>), darin liegt der Pfad mit einer gleichnamigen Klasse. Die Gruppe trägt den Startzustand, der Pfad die Animation. Die Zeiten im CSS bestimmen die Regie: Hase und Hirsch starten früher als die Taube, und die Füllung läuft parallel weiter.


@keyframes taube1{
   to{
      stroke-dashoffset: 0;
   }
}

@keyframes svg-fill-white{
   to{
      fill: hsl(0, 0%, 100%);
   }
}

#taube1{stroke-dasharray:296;stroke-dashoffset:296;}
.taube1{animation:taube1 5s 7s linear forwards, svg-fill-white 12s 5s forwards;}

Welchen Wert braucht stroke-dasharray?

Am gleichmäßigsten läuft die Zeichnung, wenn der Wert der Länge des Pfades entspricht. Die Länge liefert der Browser mit getTotalLength(), zum Beispiel in der Konsole: document.querySelector('path.hirsch').getTotalLength(). Ein zu kleiner Wert zeichnet nur einen Teil der Linie oder erzeugt Lücken. Ein zu großer Wert verzögert den sichtbaren Beginn, denn der Strich muss erst durch den Überhang laufen, bevor er im Pfad ankommt. Im Projekt sind die Werte großzügig gewählt (296 bis 387). Die Pfade selbst messen zwischen etwa 50 und 378 Einheiten. Wer die Zeichnung über die ganze Dauer gleichmäßig haben möchte, trägt dort die gemessenen Längen ein.

Die Mühle: Drehung statt Zeichnen

In der zweiten Szene drehen sich die Räder endlos. Das erledigt eine Keyframe-Animation, die den Winkel einmal herum laufen lässt. Mit transform-box: fill-box und transform-origin: center dreht sich jedes Element um die eigene Mitte statt um den Nullpunkt der Zeichnung.


@keyframes raeder{
   from{transform: rotateZ(360deg);}
   to{transform: rotateZ(0deg);}
}

#raeder{
   animation: raeder 12s linear infinite;
   transform-origin: center;
   transform-box: fill-box;
}

Szenen laden

Die Szenen liegen als einzelne SVG-Dateien im Ordner assets/. Eine kleine JSON-Datei beschreibt sie (ID, Titel, Beschreibung, Datei):


{
  "svg4": {
    "id": "svg4",
    "title": "Taube, Hase und Hirsch",
    "description": "Linienzeichnung mit Zeichenanimation und organischen Formen.",
    "file": "assets/svg4.svg"
  },
  "svg6": {
    "id": "svg6",
    "title": "Mühle bei Nacht",
    "description": "Szene mit Mond, Wolken, Licht und rotierenden Rädern.",
    "file": "assets/svg6.svg"
  }
}

get_svg.php liefert die gewünschte Szene aus. Es kennt nur IDs aus der JSON-Datei und prüft mit realpath(), dass die Datei wirklich im Ordner assets/ liegt. Damit kann niemand über die Adresse beliebige Dateien vom Server abrufen:


<?php

$data_file = __DIR__ . '/data.json';
$json_data = file_get_contents($data_file);
$svg_data = json_decode($json_data, true);

if (!isset($_GET['id'])) {
    http_response_code(400);
    exit('Keine ID angegeben.');
}

$id = $_GET['id'];

if (!is_array($svg_data) || !array_key_exists($id, $svg_data)) {
    http_response_code(404);
    exit('SVG nicht gefunden.');
}

$file = $svg_data[$id]['file'] ?? '';
$path = realpath(__DIR__ . '/' . $file);
$assets_dir = realpath(__DIR__ . '/assets');

if (!$path || !$assets_dir || strpos($path, $assets_dir) !== 0 || !is_file($path)) {
    http_response_code(404);
    exit('SVG-Datei nicht gefunden.');
}

header('Content-Type: image/svg+xml; charset=UTF-8');
echo file_get_contents($path);
?>

Im Browser holt loadSvg die Szene mit der Fetch-API und setzt das SVG in den Container. Weil das SVG dabei neu eingefügt wird, startet die CSS-Animation von vorn. Genau das macht auch der Knopf „Animation neu starten“. Ein Statusfeld zeigt „Lädt“, „Bereit“ oder „Fehler“, bei einem Fehler erscheint eine Meldung.


async function loadSvg(id) {
   const item = svgData[id];
   if (!item) {
      showError('Die ausgewählte Szene existiert nicht.');
      setStatus('Fehler');
      return;
   }

   currentSvgId = id;
   updateSelection(id);
   clearError();
   setStatus('Lädt');
   svgTitle.textContent = item.title;
   svgDescription.textContent = item.description;

   try {
      const response = await fetch(`get_svg.php?id=${encodeURIComponent(id)}`, { cache: 'no-store' });

      if (!response.ok) {
         throw new Error(`HTTP ${response.status}`);
      }

      const markup = await response.text();
      svgContainer.innerHTML = markup;
      setStatus('Bereit');
   } catch (error) {
      svgContainer.innerHTML = '';
      showError('Die SVG-Datei konnte nicht geladen werden.');
      setStatus('Fehler');
      console.error(error);
   }
}

Die Auswahl der Szenen (svg.php) wird aus derselben JSON-Datei erzeugt. Eine dritte Szene braucht deshalb nur eine SVG-Datei, einen Eintrag in der JSON-Datei und, falls sie animiert sein soll, ein paar Zeilen CSS.