x-data
Alpine
Alpine.js · Canvas API · Animation · Hyvä
Konfetti-Effekt mit Alpine.js
Canvas-Animation ohne externes Framework

Eine Konfetti-Animation klingt nach einer Aufgabe für eine fertige Bibliothek – doch wer die Canvas-API einmal verstanden hat, braucht nichts weiter als Alpine.js. Partikel-Physik, Farbpaletten, requestAnimationFrame und sauberes Speicher-Cleanup lassen sich vollständig in einer x-data-Komponente kapseln.

12 Min. Lesezeit Canvas · requestAnimationFrame · Partikel · Alpine.js Lifecycle Alpine.js 3.x · Hyvä Themes · Magento 2

1. Warum Canvas und Alpine.js – und kein Plugin?

Wenn Projekte einen Konfetti-Effekt brauchen, greift man reflexartig nach einer fertigen Bibliothek wie canvas-confetti. Das Problem dabei: In Hyvä-Projekten gibt es eine strikte Vorgabe gegen das Laden zusätzlicher JavaScript-Dateien. Jedes externe Skript muss durch CSP-Regeln freigegeben, in require.js registriert oder als ES-Modul eingebunden werden – all das ist Overhead, den man sich sparen kann. Die Canvas-API ist in jedem modernen Browser nativ vorhanden, und Alpine.js ist in Hyvä bereits geladen. Die Kombination aus beiden ist damit die einzige konsequente Wahl für ein Hyvä-Projekt, das keinen Framework-Ballast aufbauen will.

Ein zweiter Grund für die eigene Implementierung: Vollständige Kontrolle über das Verhalten. Externe Bibliotheken haben fixe Partikel-Formen, fixe Physik-Parameter und fixe Auslösemechanismen. Eine eigene Implementierung kann exakt auf das Design-System abgestimmt werden – gleiche Farbpalette wie das Theme, Konfetti in Brand-Farben, Partikel in Form des Firmenlogos. All das ist mit einer Canvas-Komponente in Alpine.js ohne weiteres realisierbar. Der dritte Vorteil: Wenn etwas nicht stimmt, lässt sich der Code debuggen, anpassen und verstehen – kein Blick in minimierte Bibliotheks-Builds nötig.

2. Canvas-Grundlagen: Kontext, Koordinatensystem und Auflösung

Das Canvas-Element ist ein Bitmap-Zeichenbereich, der über JavaScript-APIs beschrieben wird. Der Einstieg erfolgt immer über canvas.getContext('2d'), das ein CanvasRenderingContext2D-Objekt zurückgibt. Dieses Objekt enthält alle Zeichen-Methoden: fillRect, arc, beginPath, save, restore und viele weitere. Das Koordinatensystem beginnt links oben bei (0, 0) und wächst nach rechts und unten – das ist der Standard für Web-Canvas und muss beim Berechnen von Partikel-Positionen immer berücksichtigt werden.

Ein häufig übersehenes Detail: Auf High-DPI-Displays (Retina, 4K) sieht Canvas-Inhalt unscharf aus, wenn man die physische Pixeldichte nicht berücksichtigt. Das devicePixelRatio gibt das Verhältnis zwischen CSS-Pixeln und physischen Pixeln an. Die Lösung ist, die Canvas-Dimensionen mit devicePixelRatio zu multiplizieren und den Kontext anschließend zu skalieren: ctx.scale(dpr, dpr). So rechnet man weiterhin in CSS-Pixeln, das Ergebnis ist aber auf Retina-Displays scharf. Dieser Schritt sollte in der Init-Phase der Alpine-Komponente passieren, bevor der erste Frame gezeichnet wird.


// Alpine.js component: canvas setup with devicePixelRatio support
function konfettiComponent() {
  return {
    canvas: null,
    ctx: null,
    dpr: window.devicePixelRatio || 1,

    init() {
      this.canvas = this.$refs.konfettiCanvas;
      this.ctx = this.canvas.getContext('2d');
      this.resize();
      window.addEventListener('resize', () => this.resize());
    },

    resize() {
      const rect = this.canvas.parentElement.getBoundingClientRect();
      // Physical pixels for sharpness on HiDPI displays
      this.canvas.width  = rect.width  * this.dpr;
      this.canvas.height = rect.height * this.dpr;
      // CSS size stays logical pixels
      this.canvas.style.width  = rect.width  + 'px';
      this.canvas.style.height = rect.height + 'px';
      this.ctx.scale(this.dpr, this.dpr);
    }
  };
}

3. Das Partikel-Modell: Position, Geschwindigkeit und Rotation

Jedes Konfetti-Partikel ist ein einfaches JavaScript-Objekt mit wenigen Eigenschaften. Die wichtigsten davon sind Position (x, y), Geschwindigkeit (vx, vy), Rotation (angle, rotationSpeed), Farbe und Größe. In jedem Frame werden die Geschwindigkeitswerte auf die Position addiert und gleichzeitig eine leichte Schwerkraft auf vy angewendet. Das Ergebnis ist eine parabolische Flugbahn, die realistische Konfetti-Physik simuliert. Luftwiderstand lässt sich durch das Multiplizieren von vx mit einem Dämpfungsfaktor kleiner 1 simulieren.

Die Rotation wird pro Frame um rotationSpeed erhöht. Um einen dreidimensionalen Taumel-Effekt zu simulieren, kann man statt der Breite des Rechtecks width * Math.cos(angle) zeichnen – das lässt das Partikel flach werden und wieder aufgehen wie ein echtes Papierstück, das durch die Luft fällt. Partikel, die den unteren Bildschirmrand verlassen oder deren alpha-Wert unter einen Schwellenwert gesunken ist, werden aus dem Array entfernt. Wenn das Array leer ist, wird der Animations-Loop gestoppt.

4. Der Animations-Loop mit requestAnimationFrame

requestAnimationFrame ist der moderne Standard für flüssige Web-Animationen. Im Gegensatz zu setInterval synchronisiert requestAnimationFrame mit dem Browser-Repaint-Zyklus, was zu stabileren 60 fps (oder der jeweiligen Bildschirmfrequenz) führt. Außerdem pausiert der Browser den Loop automatisch, wenn der Tab nicht sichtbar ist – ein wesentlicher Unterschied zu setInterval, der auch im Hintergrund weiterläuft und CPU belastet. Das Ergebnis von requestAnimationFrame(callback) ist eine numerische ID, die man für cancelAnimationFrame speichern muss.

Der typische Aufbau eines Animation-Loops: Zuerst den Canvas mit ctx.clearRect löschen, dann alle Partikel aktualisieren (Physik anwenden), dann alle Partikel zeichnen, dann die ausgelaufenen Partikel filtern und abschließend den nächsten Frame mit requestAnimationFrame anfordern – aber nur, wenn noch Partikel vorhanden sind. Diese Bedingung ist wichtig: Ohne sie läuft der Loop unendlich weiter und belastet die CPU auch wenn längst keine Konfetti mehr zu sehen sind.


// Particle physics and animation loop
function konfettiComponent() {
  return {
    particles: [],
    animationId: null,

    createParticle(x, y) {
      return {
        x, y,
        vx: (Math.random() - 0.5) * 8,
        vy: -(Math.random() * 6 + 4),
        gravity: 0.25,
        drag: 0.995,
        angle: Math.random() * Math.PI * 2,
        rotationSpeed: (Math.random() - 0.5) * 0.2,
        width: Math.random() * 10 + 5,
        height: Math.random() * 6 + 3,
        color: this.randomColor(),
        alpha: 1
      };
    },

    tick() {
      const { ctx, canvas } = this;
      const w = canvas.width / this.dpr;
      const h = canvas.height / this.dpr;

      ctx.clearRect(0, 0, w, h);

      this.particles = this.particles.filter(p => p.alpha > 0.05 && p.y < h + 20);

      for (const p of this.particles) {
        p.vy += p.gravity;
        p.vx *= p.drag;
        p.x  += p.vx;
        p.y  += p.vy;
        p.angle += p.rotationSpeed;
        if (p.y > h * 0.6) p.alpha -= 0.012;

        ctx.save();
        ctx.globalAlpha = p.alpha;
        ctx.translate(p.x, p.y);
        ctx.rotate(p.angle);
        ctx.fillStyle = p.color;
        ctx.fillRect(
          -p.width / 2,
          -p.height / 2,
          p.width * Math.abs(Math.cos(p.angle)),
          p.height
        );
        ctx.restore();
      }

      if (this.particles.length > 0) {
        this.animationId = requestAnimationFrame(() => this.tick());
      }
    }
  };
}

5. Farbpaletten und Partikel-Formen

Farbpaletten machen den Unterschied zwischen einem generischen Konfetti-Effekt und einer Brand-spezifischen Feier-Animation. Man definiert ein Array mit Hex-Farbwerten, die zum Theme des Projekts passen, und wählt in der createParticle-Funktion zufällig einen davon aus. Für Hyvä-Projekte empfiehlt sich die Tailwind-Farbpalette: Teal, Cyan, Emerald und Amber ergeben ein frisches, modernes Konfetti. Alternativ können Unternehmensfarben direkt als Hex-Werte hinterlegt werden.

Neben einfachen Rechtecken lassen sich auch andere Formen zeichnen. Kreise werden mit ctx.arc realisiert, Dreiecke mit ctx.lineTo-Sequenzen. Für einen realistischeren Papier-Look kann man Rechtecke mit leicht abgerundeten Ecken über roundRect zeichnen (verfügbar ab Chrome 99, Firefox 112). Die Performance-Empfehlung: Alle Partikel eines Frame-Passes in einem einzigen beginPath-Block zusammenfassen, wenn sie dieselbe Farbe haben – das reduziert die Anzahl der Kontext-Wechsel erheblich. In der Praxis sind unterschiedliche Farben aber meist wichtiger als maximale Performance.

6. Integration in Alpine.js: x-data und x-ref

Die Alpine.js-Komponente kapselt den gesamten Canvas-Code als x-data-Funktion. Das Canvas-Element selbst wird mit x-ref="konfettiCanvas" markiert und ist damit über this.$refs.konfettiCanvas in allen Methoden der Komponente erreichbar. Im init()-Hook – der automatisch beim Mounten der Komponente ausgeführt wird – wird der Canvas-Kontext geholt, die Größe gesetzt und der Resize-Listener registriert. Der destroy()-Hook – verfügbar ab Alpine.js 3.x – kümmert sich um das saubere Entfernen des Event Listeners und das Stoppen des Animations-Loops.

Das HTML-Markup ist minimal: Ein Container-Div mit x-data="konfettiComponent()", darin ein canvas-Element mit x-ref="konfettiCanvas" und ein Trigger-Button mit @click="starte()". Alle Komplexität der Animation ist in der JavaScript-Komponente verborgen. Das Template bleibt sauber und deklarativ. Wichtig für Hyvä-Projekte: Nach dem script-Block, der die Komponente definiert, muss $hyvaCsp->registerInlineScript() im PHP-Template aufgerufen werden, damit die CSP-Policy das Inline-Script erlaubt.


// Full Alpine.js component with init/destroy lifecycle
function konfettiComponent() {
  return {
    particles: [],
    animationId: null,
    dpr: window.devicePixelRatio || 1,
    colors: ['#5eead4','#0f766e','#06b6d4','#fbbf24','#f472b6','#a3e635'],
    _resizeHandler: null,

    init() {
      this.canvas = this.$refs.konfettiCanvas;
      this.ctx = this.canvas.getContext('2d');
      this._resizeHandler = () => this.resize();
      window.addEventListener('resize', this._resizeHandler);
      this.resize();
    },

    destroy() {
      window.removeEventListener('resize', this._resizeHandler);
      if (this.animationId) cancelAnimationFrame(this.animationId);
    },

    resize() {
      const { canvas, dpr } = this;
      const parent = canvas.parentElement;
      const w = parent.clientWidth;
      const h = parent.clientHeight;
      canvas.width  = w * dpr;
      canvas.height = h * dpr;
      canvas.style.width  = w + 'px';
      canvas.style.height = h + 'px';
      this.ctx.scale(dpr, dpr);
    },

    randomColor() {
      return this.colors[Math.floor(Math.random() * this.colors.length)];
    },

    starte(count = 120) {
      if (this.animationId) cancelAnimationFrame(this.animationId);
      const cx = this.canvas.clientWidth  / 2;
      const cy = this.canvas.clientHeight * 0.35;
      for (let i = 0; i < count; i++) {
        this.particles.push(this.createParticle(cx, cy));
      }
      this.tick();
    }
  };
}

7. Trigger: Animation bei Benutzeraktion starten

Eine Konfetti-Animation macht am meisten Sinn als Reaktion auf eine Benutzeraktion: Formular erfolgreich abgeschickt, Produkt in den Warenkorb gelegt, Bestellung abgeschlossen. In Alpine.js lassen sich solche Trigger sauber über @click oder über das Custom-Event-System realisieren. Wenn die Konfetti-Komponente auf einer anderen Ebene im DOM sitzt als der Trigger, kann man $dispatch('konfetti-start') im Trigger und @konfetti-start.window="starte()" in der Komponente verwenden – das entkoppelt die beiden Komponenten vollständig.

Für Magento-Projekte ist ein typischer Anwendungsfall das Erfolgs-Popup nach dem Checkout. Alpine.js hat in Hyvä Zugriff auf Magento-Events über den private-content-loaded-Event-Bus. Wenn die Bestellung abgeschlossen ist und Magento das entsprechende Customer-Segment lädt, kann die Konfetti-Komponente darauf reagieren. Eine weitere Möglichkeit: Den Konfetti-Effekt an die Alpine-Komponente des Mini-Carts hängen und immer dann auslösen, wenn ein Produkt erfolgreich hinzugefügt wurde – @product-added-to-cart.window="starte(50)".

8. Cleanup: cancelAnimationFrame und Speicher-Management

Memory Leaks in Canvas-Animationen entstehen auf drei Wegen: ein nicht gecancelter Animation-Loop, ein nicht entfernter Resize-Event-Listener, und ein Canvas-Kontext, der auf ein bereits aus dem DOM entferntes Element zeigt. Alpine.js 3.x löst das Problem des Lifecycles sauber: Der destroy()-Hook wird aufgerufen, wenn die Komponente aus dem DOM entfernt wird – sei es durch x-if, durch Navigation in einer SPA oder durch direktes DOM-Manipulation. Im destroy()-Hook werden cancelAnimationFrame(this.animationId) und window.removeEventListener aufgerufen.

Ein subtiles Problem beim Resize-Handler: Wenn man als Listener direkt () => this.resize() übergibt, entsteht bei jedem Aufruf eine neue Funktion-Referenz. removeEventListener kann diese nicht deregistrieren, weil es keine Referenz auf die ursprüngliche Funktion hat. Die Lösung: Den Listener in einer Instanz-Variable speichern (this._resizeHandler = () => this.resize()) und diese Referenz sowohl bei addEventListener als auch bei removeEventListener verwenden. Dieses Pattern ist für jeden Event-Listener in Alpine.js-Komponenten obligatorisch.

9. Vergleichsübersicht: Canvas vs. CSS vs. Bibliothek

Kriterium CSS-Animationen Externe Bibliothek Alpine.js + Canvas
Partikel-Anzahl ~30–50 sinnvoll 200–500+ 200–500+, kontrollierbar
Bundle-Größe 0 KB extra ~15–50 KB 0 KB extra
CSP-Kompatibilität Vollständig CDN-Freigabe nötig Vollständig
Anpassbarkeit Begrenzt API-abhängig Vollständig
Implementierungsaufwand Gering Gering Mittel (~80 Zeilen)

Die Tabelle zeigt: Für einfache, wenige Partikel sind CSS-Animationen die einfachste Lösung. Für reiche, physikalisch korrekte Animationen ohne externe Abhängigkeiten ist die Alpine.js-Canvas-Kombination das einzige Mittel, das alle Anforderungen eines modernen Hyvä-Projekts erfüllt. Externe Bibliotheken haben in einem CSP-strikten Magento-Projekt eine hohe Einstiegshürde und bringen Abhängigkeiten mit, die gewartet werden müssen.

Mironsoft

Alpine.js-Komponenten, Hyvä Themes und Magento 2 Frontend-Entwicklung

Hyvä-Projekt braucht individuelle Alpine.js-Animationen?

Wir bauen performante, CSP-konforme Alpine.js-Komponenten für Magento 2 mit Hyvä – von der Canvas-Animation bis zur komplexen Multi-Step-Interaktion, immer ohne externe Framework-Abhängigkeiten.

Canvas-Animationen

Konfetti, Partikel-Systeme, Progress-Visualisierungen – nativ und performant

Lifecycle-Sicherheit

Memory-Leak-freie Komponenten mit sauberem init/destroy-Management

CSP-Konformität

Alle Inline-Skripte korrekt über $hyvaCsp registriert und CSP-kompatibel

10. Zusammenfassung

Eine Konfetti-Animation mit Alpine.js und Canvas ist kein großes Projekt – rund 80 Zeilen JavaScript reichen für einen vollständigen, physikalisch glaubwürdigen Effekt. Der Schlüssel liegt im Verständnis von drei Bausteinen: dem Canvas-API-Setup mit devicePixelRatio-Skalierung, dem Partikel-Modell mit Physik-Parametern, und dem Animation-Loop mit requestAnimationFrame. Alpine.js bringt den Lifecycle-Rahmen: init() für Setup und destroy() für sauberes Aufräumen. Wer dieses Muster einmal verstanden hat, kann es auf beliebige Canvas-Animationen übertragen.

Für Hyvä-Projekte ist dieser Ansatz die natürliche Wahl: keine externen Abhängigkeiten, keine CDN-Einbindungen, keine CSP-Probleme, kein Overhead im JavaScript-Bundle. Die Komponente lässt sich in jede Hyvä-Seite einbinden, über Layout-XML steuern und über das Alpine.js-Event-System mit anderen Komponenten verbinden. Das sauberste Ergebnis erzielt man, wenn die Canvas-Komponente als eigenständige Alpine-Komponente in einer eigenen Phtml-Datei lebt und über $dispatch / Custom Events kommuniziert.

Alpine.js Konfetti-Effekt — Das Wichtigste auf einen Blick

Canvas-Setup

devicePixelRatio multiplizieren, dann ctx.scale(dpr, dpr) – sonst unscharfe Grafik auf HiDPI-Displays. Im init()-Hook erledigen.

Animation-Loop

requestAnimationFrame pausiert automatisch bei unsichtbaren Tabs. Loop stoppen, wenn Partikel-Array leer – sonst CPU-Verschwendung.

Cleanup

cancelAnimationFrame und removeEventListener im destroy()-Hook. Listener-Referenz in Instanzvariable speichern.

Hyvä-Integration

Kein externes JS, kein CDN. $hyvaCsp->registerInlineScript() nach dem Script-Block. Events über $dispatch entkoppeln.

11. FAQ: Alpine.js Konfetti-Effekt mit Canvas

1Brauche ich eine externe Bibliothek für Konfetti?
Nein. Native Canvas-API plus Alpine.js reicht vollständig. In Hyvä ist das sogar die bevorzugte Lösung – keine externen Skripte, keine CDN-Einbindungen.
2Warum ist Canvas auf Retina unscharf?
devicePixelRatio nicht berücksichtigt. Canvas-Dimensionen mit dpr multiplizieren und ctx.scale(dpr, dpr) aufrufen – dann ist die Grafik scharf.
3Wie stoppe ich den Animation-Loop korrekt?
cancelAnimationFrame(animationId) im destroy()-Hook. ID von requestAnimationFrame speichern. Loop zusätzlich stoppen wenn Partikel-Array leer ist.
4Memory Leaks vermeiden?
Loop canceln, Event-Listener mit gespeicherter Referenz entfernen, und Loop stoppen wenn keine Partikel mehr aktiv sind. Alles im destroy()-Hook.
5Wie viele Partikel gleichzeitig?
200–500 bei 60 fps auf modernen Geräten. Über 1000 kann auf älteren Mobilgeräten zu Drops führen. JS-Update ist der Engpass, nicht das Drawing.
6Trigger von anderer Alpine-Komponente?
$dispatch('konfetti-start') im Trigger, @konfetti-start.window='starte()' in der Konfetti-Komponente. Vollständige Entkopplung ohne direkte Referenzen.
7requestAnimationFrame im Hintergrund-Tab?
Wird automatisch pausiert. Großer Vorteil gegenüber setInterval, der im Hintergrund läuft und CPU verbraucht.
8CSP-konforme Integration in Hyvä?
Script in .phtml-Datei, danach $hyvaCsp->registerInlineScript() aufrufen. Generiert den korrekten nonce-Wert für die CSP-Policy.
9Eigene Konfetti-Formen statt Rechtecken?
ctx.arc() für Kreise, ctx.lineTo() für Polygone, ctx.drawImage() für Sprites. roundRect() ab Chrome 99 für abgerundete Ecken.
10Taumel-Effekt wie echtes Konfetti?
Math.abs(Math.cos(angle)) * width als Rechteck-Breite verwenden. Lässt das Partikel flach werden und wieder aufgehen – wirkt wie taumelndes Papier.