ProjektIm Handbuch: Workspace: Grundlagen
image: feuerwerk.webp kind: projekt template: https://github.com/specht/pixelram-starter.git template_name: des PixelRAM-Starters

Feuerwerk aus Partikeln

Ein Feuerwerk besteht aus tausenden Funken, und jeder einzelne folgt denselben einfachen Regeln: Er fliegt, die Schwerkraft zieht ihn nach unten, und irgendwann verglimmt er. In diesem Projekt programmierst du diese Regeln in C mit PixelRAM, vom ersten Funken bis zu Raketen, die du mit der Maus in den Himmel schießt.

Feuerwerk über einer nächtlichen Stadt: Rote und goldene Funkenkreise leuchten über den Dächern, Raketen steigen auf, in den Häusern brennen einzelne Fenster.

Dieses Projekt baut auf Echtzeitgrafik mit C auf. Von dort kennst du die Vorlage, make PROGRAM=…, die Vorschau und die Schleife mit present(). Arbeite in einer eigenen Datei feuerwerk.c neben main.c und übersetze sie mit:

make PROGRAM=feuerwerk

Fehlt dir die Vorlage noch, klone sie wie dort beschrieben:

Stelle zuerst sicher, dass du keinen Ordner geöffnet hast. Um sicherzugehen, drücke einfach den Shortcut für »Ordner schließen«: StrgK und dann F. Dein Workspace sollte jetzt ungefähr so aussehen:

Ein leerer Workspace ohne geöffneten Ordner.

Klicke auf »Clone Repository« und füge die Adresse der Vorlage ein:

https://github.com/specht/pixelram-starter.git

Das Eingabefeld für die Adresse des Repositorys.

Bestätige mit Enter. Danach wählst du das Verzeichnis, in dem das Repository gespeichert werden soll: den Ordner /workspace.

Die Auswahl des Zielordners /workspace.

Das Repository wird jetzt in einen Unterordner von /workspace geklont, der so heißt wie das Repository. Beantworte die Frage, ob es jetzt geöffnet werden soll, mit »Open«:

Die Frage, ob das geklonte Repository geöffnet werden soll.

1. Ein Funke

Position und Geschwindigkeit

Ein Funke hat eine Position x, y und eine Geschwindigkeit vx, vy: wie viele Pixel er pro Sekunde nach rechts und nach unten fliegt. Ein negatives vy heißt: nach oben. Beides fasst ein struct zusammen.

In jedem Bild fragt das Programm, wie viel Zeit seit dem letzten Bild vergangen ist, dt, meistens etwa eine Sechzigstelsekunde. Dann gelten zwei Regeln:

  • Die Schwerkraft ändert die Geschwindigkeit: vy += GRAVITY * dt. Jede Sekunde fällt der Funke 40 Pixel pro Sekunde schneller.
  • Die Geschwindigkeit ändert die Position: x += vx * dt und y += vy * dt.

Mehr Physik braucht ein Feuerwerk nicht. Hier ein Funke, der immer wieder von links unten schräg nach oben geworfen wird:

#include "pixelram.h"
#include <stdlib.h>

#define W 320
#define H 180
#define GRAVITY 40.0

typedef struct
{
    double x, y;     // Position in Pixeln
    double vx, vy;   // Geschwindigkeit in Pixeln pro Sekunde
} Particle;

double random_between(double low, double high)
{
    return low + (high - low) * rand() / (double)RAND_MAX;
}

void throw_spark(Particle *p)
{
    p->x = 20;
    p->y = H - 1;
    p->vx = random_between(20, 60);
    p->vy = random_between(-110, -60);
}

int main(void)
{
    screen_open(W, H, pixel_indexed8, "Ein Funke");
    use_palette("sweetie_16");

    Particle spark;
    throw_spark(&spark);

    double last = seconds();
    while (!should_close())
    {
        double now = seconds();
        double dt = now - last;   // Sekunden seit dem letzten Bild
        last = now;

        spark.vy += GRAVITY * dt;   // die Schwerkraft zieht nach unten
        spark.x += spark.vx * dt;
        spark.y += spark.vy * dt;

        if (spark.x >= W || spark.y >= H)
            throw_spark(&spark);

        set_pixel((int)spark.x, (int)spark.y, 4);
        present();
    }

    screen_close();
    return 0;
}

random_between liefert eine zufällige Kommazahl zwischen zwei Grenzen: rand() gibt eine ganze Zahl von 0 bis RAND_MAX zurück, geteilt durch RAND_MAX liegt sie zwischen 0 und 1. Weil das Programm den Bildschirm nie löscht, bleibt jede Flugbahn stehen.

Der Workspace mit feuerwerk.c im Editor; im Terminal darunter hat make PROGRAM=feuerwerk die Datei feuerwerk.html erzeugt.

Übersetze das Programm mit make PROGRAM=feuerwerk und öffne feuerwerk.html in der Vorschau:

Mehrere gelbe Wurfparabeln steigen von links unten auf und fallen wieder.

Jede Bahn ist eine Parabel, wie beim Wurf eines Balls. Das Programm rechnet sie nirgends aus: Sie entsteht von selbst aus den zwei Regeln.

Aufgabe 1Der erste Funke

Schreib das Programm mit dem Funken in feuerwerk.c und übersetze es mit make PROGRAM=feuerwerk.

Vermutung 2Wie hoch steigt der Funke?

Ein Funke startet senkrecht nach oben mit vy = -100, also 100 Pixel pro Sekunde, und GRAVITY ist 40. Wie viele Pixel steigt er, bevor er umkehrt? Rechne erst, dann probier es aus: Setze im Programm vx = 0 und vy = -100.

Pixel
Tipp

Er wird jede Sekunde um 40 Pixel pro Sekunde langsamer. Wie lange dauert es bis zum Stillstand, und wie schnell war er im Mittel?

Zum Nachdenken: Warum rechnet das Programm mit dt, statt den Funken in jedem Bild um einen festen Wert zu bewegen? Was würde sonst auf einem Computer passieren, der nur 30 Bilder pro Sekunde schafft?

2. Eine Explosion

Viele Funken

Für ein Feuerwerk braucht es nicht einen Funken, sondern viele. Sie stehen in einem Array particles, und count zählt, wie viele davon gerade leben. Eine Explosion wirft 100 Funken auf einmal von derselben Stelle aus, jeden in eine zufällige Richtung. Die Richtung ist ein Winkel zwischen 0 und 2π, und mit cos und sin wird daraus der Anteil nach rechts und nach unten:

double angle = random_between(0, 2 * PI);
double speed = random_between(30, 50);
add(x, y, cos(angle) * speed, sin(angle) * speed, random_between(1.5, 2.5));

Alle Funken sind etwa gleich schnell. Deshalb liegen sie nach einem Moment auf einem Kreis um die Explosion, wie die Sterne eines echten Feuerwerkskörpers, und nicht in einer vollen Scheibe.

Funken verglimmen

Jeder Funke bekommt eine Lebenszeit life in Sekunden, die in jedem Bild um dt kleiner wird. Ist sie abgelaufen, muss der Funke aus dem Array verschwinden. Statt alle folgenden Funken eine Stelle nach vorne zu schieben, rückt einfach der letzte Funke in die Lücke. Die Reihenfolge ist egal, und es geht schnell, egal wie viele Funken es gibt:

particles[i] = particles[count - 1];
count--;
i--;   // den nachgerückten Funken auch noch bewegen
Aufgabe 3Wer rückt nach?

In particles leben 10 Funken, an den Stellen 0 bis 9. Der Funke an Stelle 3 verlischt. Was steht danach an Stelle 3?

Wer rückt nach?

Jetzt löscht das Programm den Bildschirm in jedem Bild mit clear(0) und zeichnet alle Funken neu, sonst sähe man nur noch Streifen. Hier das ganze Programm:

#include "pixelram.h"
#include <math.h>
#include <stdlib.h>

#define W 320
#define H 180
#define GRAVITY 40.0
#define PI 3.14159265358979
#define MAX_PARTICLES 2000

typedef struct
{
    double x, y;     // Position in Pixeln
    double vx, vy;   // Geschwindigkeit in Pixeln pro Sekunde
    double life;     // verbleibende Lebenszeit in Sekunden
} Particle;

Particle particles[MAX_PARTICLES];
int count = 0;

double random_between(double low, double high)
{
    return low + (high - low) * rand() / (double)RAND_MAX;
}

void add(double x, double y, double vx, double vy, double life)
{
    if (count == MAX_PARTICLES)
        return;
    particles[count] = (Particle){ x, y, vx, vy, life };
    count++;
}

void explode(double x, double y)
{
    for (int i = 0; i < 100; i++)
    {
        double angle = random_between(0, 2 * PI);
        double speed = random_between(30, 50);
        add(x, y, cos(angle) * speed, sin(angle) * speed, random_between(1.5, 2.5));
    }
}

int main(void)
{
    screen_open(W, H, pixel_indexed8, "Explosion");
    use_palette("sweetie_16");

    double last = seconds();
    double next_explosion = 0;

    while (!should_close())
    {
        double now = seconds();
        double dt = now - last;
        last = now;

        if (now > next_explosion)
        {
            explode(random_between(60, W - 60), random_between(30, 90));
            next_explosion = now + 1.0;
        }

        clear(0);
        for (int i = 0; i < count; i++)
        {
            Particle *p = &particles[i];
            p->vy += GRAVITY * dt;
            p->x += p->vx * dt;
            p->y += p->vy * dt;
            p->life -= dt;

            if (p->life <= 0)
            {
                // verloschen: der letzte Funke rückt an seinen Platz
                particles[i] = particles[count - 1];
                count--;
                i--;
                continue;
            }

            set_pixel((int)p->x, (int)p->y, 12);
        }

        present();
    }

    screen_close();
    return 0;
}

(Particle){ x, y, vx, vy, life } baut einen ganzen Funken auf einmal, die Werte in der Reihenfolge der Felder im struct. M_PI gibt es in strengem C nicht, deshalb steht π als PI oben im Programm.

Weiße Funken zweier Explosionen: eine frische, deren Funken einen Kreis bilden, und eine ältere, die schon nach unten sinkt.

Jede Sekunde explodiert es an einer neuen Stelle. Die Funken fliegen als Kreis auseinander und sinken dabei nach unten.

3. Ein richtiges Feuerwerk

Farben, die verglühen

Ein Funke soll hell aufleuchten und dann langsam dunkler werden. Dafür baut sich das Programm eine eigene Palette aus vier Farbverläufen zu je 16 Farben: Rot auf den Nummern 0 bis 15, Grün auf 16 bis 31, Blau auf 32 bis 47 und Gold auf 48 bis 63. Innerhalb eines Verlaufs ist 0 das Dunkelblau des Nachthimmels und 15 fast weiß. Die Farbe eines Funkens ist dann 16 * ramp + brightness, und brightness sinkt mit der Lebenszeit:

// Vier Farbverläufe zu je 16 Farben: 16 * ramp + Helligkeit
void make_palette(void)
{
    const int sky[3] = {10, 12, 35};
    const int colors[4][3] = { {255, 80, 60}, {80, 255, 120}, {90, 150, 255}, {255, 210, 80} };
    for (int ramp = 0; ramp < 4; ramp++)
        for (int b = 0; b < 16; b++)
        {
            double f = b / 15.0;
            // vom Nachthimmel zur Farbe, ganz hell fast weiß
            double white = f > 0.8 ? (f - 0.8) * 5 : 0;
            int rgb[3];
            for (int i = 0; i < 3; i++)
                rgb[i] = (sky[i] + (colors[ramp][i] - sky[i]) * f) * (1 - white) + 255 * white;
            set_palette(16 * ramp + b, rgb[0], rgb[1], rgb[2]);
        }
}

Nummer 0, der Himmel, ist damit Dunkelblau, und der ganze Bildschirm beginnt in dieser Farbe: PixelRAM füllt ihn am Anfang mit 0.

Leuchtspuren

Echte Funken ziehen einen Schweif hinter sich her, weil das Auge ein Leuchten noch kurz sieht, wenn es schon weg ist. Das lässt sich mit dem Bildschirm selbst nachbauen: Statt ihn mit clear(0) zu löschen, wird jedes Pixel nur ein wenig dunkler, eine Stufe in seinem Farbverlauf. Ein Funke, der weiterfliegt, lässt so eine Spur zurück, die von selbst verglüht:

// Nachleuchten: jedes Pixel wird ein wenig dunkler
void fade(void)
{
    for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
        {
            int c = get_pixel(x, y);
            if (c % 16 > 0)
                set_pixel(x, y, c - 1);
        }
}

c % 16 ist die Helligkeit innerhalb des Verlaufs. Ist sie schon 0, bleibt das Pixel schwarz, sonst rutscht es eine Farbe tiefer, im selben Verlauf.

Raketen

Bis jetzt explodieren die Funken einfach irgendwo in der Luft. Ein Feuerwerk schießt erst eine Rakete hoch, und sie explodiert am höchsten Punkt, wenn sie umkehrt, also sobald vy nicht mehr negativ ist. Eine Rakete ist auch nur ein Partikel, mit einem Merkmal rocket mehr.

Wie schnell muss die Rakete starten, damit sie in einer bestimmten Höhe umkehrt? Aus der Physik: Wer mit der Geschwindigkeit v nach oben startet, steigt bei der Schwerkraft g genau h = v² / 2g hoch. Umgekehrt ist v = √(2gh):

void launch(double x, double target_y)
{
    // so schnell, dass die Rakete genau bei target_y umkehrt
    double vy = -sqrt(2 * GRAVITY * (H - target_y));
    add(x, H - 1, random_between(-8, 8), vy, 10, rand() % 4, true);
}
Aufgabe 4Wie schnell muss die Rakete sein?

Eine Rakete soll 160 Pixel hoch steigen, bei GRAVITY 40. Mit wie vielen Pixeln pro Sekunde muss sie starten? Gib die Zahl ohne Vorzeichen an.

Pixel pro Sekunde
Tipp

Die Formel steht in launch. Was ist dort H - target_y?

Explodiert eine Rakete, bekommen ihre Funken ihren Farbverlauf und ihre Geschwindigkeit mit, sie fliegen also mit der Rakete weiter. Und weil Luft die Funken bremst, verlieren sie in jeder Sekunde einen Teil ihres Tempos:

p->vx -= p->vx * 1.5 * dt;
p->vy -= p->vy * 1.5 * dt;

Eine Stadt bei Nacht

Unten im Bild steht eine Stadt, vor der das Feuerwerk aufsteigt. Sie bekommt vier eigene Farben hinter den Farbverläufen: 64 für die Häuser, 65 bis 67 für Fenster in drei Lichtfarben. Die Stadt wird einmal am Anfang ausgedacht: Häuser mit zufälliger Breite und Höhe, nebeneinander von links nach rechts, jedes mit eigenem Fensterabstand und einem eigenen Anteil heller Fenster. Sie steht dann im zweidimensionalen Array city, eine Zahl für jedes Pixel, 0 für Himmel. In jedem Bild malt draw_skyline sie nach den Funken. So steigen die Raketen hinter den Häusern auf.

// Häuser mit zufälliger Breite und Höhe, von links nach rechts. Jedes Haus
// hat seinen eigenen Fensterabstand, und in jedem leuchtet ein anderer Teil
// der Fenster. Die Stadt wird einmal ausgedacht und in city gemalt.
void make_skyline(void)
{
    set_palette(HOUSE, 0, 0, 0);
    set_palette(WINDOW, 255, 210, 120);       // Glühbirne
    set_palette(WINDOW + 1, 255, 150, 70);    // warmes Licht
    set_palette(WINDOW + 2, 140, 180, 255);   // Fernseher

    int x = 0;
    while (x < W)
    {
        int width = 10 + rand() % 20;
        int top = H - 12 - rand() % 30;
        int step = 2 + rand() % 2;                 // Fensterabstand
        double lit = random_between(0.05, 0.4);    // Anteil der hellen Fenster

        for (int i = x; i < x + width && i < W; i++)
            for (int y = top; y < H; y++)
                city[y][i] = HOUSE;

        for (int y = top + 2; y < H - 1; y += 3)
            for (int i = x + 2; i < x + width - 1 && i < W; i += step)
                if (random_between(0, 1) < lit)
                    city[y][i] = WINDOW + rand() % 3;

        x += width;
    }
}

// die Stadt vor den Himmel malen
void draw_skyline(void)
{
    for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            if (city[y][x] != 0)
                set_pixel(x, y, city[y][x]);
}

Eine Kleinigkeit fehlt noch: Kommen zwei Raketen nacheinander auf derselben Seite hoch, drängt sich das Feuerwerk an einer Stelle. Deshalb startet die nächste Rakete abwechselnd in der linken und der rechten Hälfte.

Das ganze Feuerwerk

Mit der Maus kannst du selbst Raketen starten: mouse_button_pressed meldet einen Klick, mouse_position sagt, wohin. Die Rakete fliegt genau bis zu dieser Höhe. Hier das ganze Programm:

#include "pixelram.h"
#include <math.h>
#include <stdlib.h>

#define W 320
#define H 180
#define MAX_PARTICLES 3000
#define GRAVITY 40.0
#define PI 3.14159265358979
#define HOUSE 64     // Farbe der Häuser
#define WINDOW 65    // 65 bis 67: drei Farben für Fenster

typedef struct
{
    double x, y;     // Position in Pixeln
    double vx, vy;   // Geschwindigkeit in Pixeln pro Sekunde
    double life;     // verbleibende Lebenszeit in Sekunden
    int ramp;        // Farbverlauf 0 bis 3
    bool rocket;     // eine Rakete explodiert, wenn sie umkehrt
} Particle;

Particle particles[MAX_PARTICLES];
int count = 0;
uint8_t city[H][W];   // die Stadt: 0 ist Himmel, sonst eine Farbe

double random_between(double low, double high)
{
    return low + (high - low) * rand() / (double)RAND_MAX;
}

void add(double x, double y, double vx, double vy, double life, int ramp, bool rocket)
{
    if (count == MAX_PARTICLES)
        return;
    particles[count] = (Particle){ x, y, vx, vy, life, ramp, rocket };
    count++;
}

void launch(double x, double target_y)
{
    // so schnell, dass die Rakete genau bei target_y umkehrt
    double vy = -sqrt(2 * GRAVITY * (H - target_y));
    add(x, H - 1, random_between(-8, 8), vy, 10, rand() % 4, true);
}

void explode(Particle *p)
{
    // etwa gleich schnell: die Funken bilden eine Kugelschale, keine volle Scheibe
    for (int i = 0; i < 100; i++)
    {
        double angle = random_between(0, 2 * PI);
        double speed = random_between(30, 50);
        add(p->x, p->y, p->vx + cos(angle) * speed, p->vy + sin(angle) * speed,
            random_between(1.5, 2.5), p->ramp, false);
    }
}

// Vier Farbverläufe zu je 16 Farben: 16 * ramp + Helligkeit
void make_palette(void)
{
    const int sky[3] = {10, 12, 35};
    const int colors[4][3] = { {255, 80, 60}, {80, 255, 120}, {90, 150, 255}, {255, 210, 80} };
    for (int ramp = 0; ramp < 4; ramp++)
        for (int b = 0; b < 16; b++)
        {
            double f = b / 15.0;
            // vom Nachthimmel zur Farbe, ganz hell fast weiß
            double white = f > 0.8 ? (f - 0.8) * 5 : 0;
            int rgb[3];
            for (int i = 0; i < 3; i++)
                rgb[i] = (sky[i] + (colors[ramp][i] - sky[i]) * f) * (1 - white) + 255 * white;
            set_palette(16 * ramp + b, rgb[0], rgb[1], rgb[2]);
        }
}

// Häuser mit zufälliger Breite und Höhe, von links nach rechts. Jedes Haus
// hat seinen eigenen Fensterabstand, und in jedem leuchtet ein anderer Teil
// der Fenster. Die Stadt wird einmal ausgedacht und in city gemalt.
void make_skyline(void)
{
    set_palette(HOUSE, 0, 0, 0);
    set_palette(WINDOW, 255, 210, 120);       // Glühbirne
    set_palette(WINDOW + 1, 255, 150, 70);    // warmes Licht
    set_palette(WINDOW + 2, 140, 180, 255);   // Fernseher

    int x = 0;
    while (x < W)
    {
        int width = 10 + rand() % 20;
        int top = H - 12 - rand() % 30;
        int step = 2 + rand() % 2;                 // Fensterabstand
        double lit = random_between(0.05, 0.4);    // Anteil der hellen Fenster

        for (int i = x; i < x + width && i < W; i++)
            for (int y = top; y < H; y++)
                city[y][i] = HOUSE;

        for (int y = top + 2; y < H - 1; y += 3)
            for (int i = x + 2; i < x + width - 1 && i < W; i += step)
                if (random_between(0, 1) < lit)
                    city[y][i] = WINDOW + rand() % 3;

        x += width;
    }
}

// die Stadt vor den Himmel malen
void draw_skyline(void)
{
    for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
            if (city[y][x] != 0)
                set_pixel(x, y, city[y][x]);
}

// Nachleuchten: jedes Pixel wird ein wenig dunkler
void fade(void)
{
    for (int y = 0; y < H; y++)
        for (int x = 0; x < W; x++)
        {
            int c = get_pixel(x, y);
            if (c % 16 > 0)
                set_pixel(x, y, c - 1);
        }
}

int main(void)
{
    screen_open(W, H, pixel_indexed8, "Feuerwerk");
    make_palette();
    make_skyline();

    double last = seconds();
    double next_rocket = 0;
    bool left = false;
    int frame = 0;

    while (!should_close())
    {
        double now = seconds();
        double dt = now - last;
        last = now;

        if (now > next_rocket)
        {
            // abwechselnd links und rechts, damit sich die Raketen verteilen
            left = !left;
            double x = left ? random_between(30, W / 2) : random_between(W / 2, W - 30);
            launch(x, random_between(20, 70));
            next_rocket = now + random_between(0.3, 1.2);
        }
        if (mouse_button_pressed(pixel_mouse_left))
        {
            int mx, my;
            mouse_position(&mx, &my);
            launch(mx, my);
        }

        // jedes zweite Bild: die Spuren verblassen langsamer
        frame++;
        if (frame % 2 == 0)
            fade();

        for (int i = 0; i < count; i++)
        {
            Particle *p = &particles[i];
            p->vy += GRAVITY * dt;
            if (!p->rocket)
            {
                // Luftwiderstand bremst die Funken
                p->vx -= p->vx * 1.5 * dt;
                p->vy -= p->vy * 1.5 * dt;
            }
            p->x += p->vx * dt;
            p->y += p->vy * dt;
            p->life -= dt;

            if (p->rocket && p->vy >= 0)
            {
                explode(p);
                p->life = 0;
            }

            if (p->life <= 0 || p->y >= H)
            {
                // Lücke mit dem letzten Partikel füllen
                particles[i] = particles[count - 1];
                count--;
                i--;
                continue;
            }

            int brightness = p->rocket ? 15 : (int)(15 * fmin(1, p->life));
            set_pixel((int)p->x, (int)p->y, 16 * p->ramp + brightness);
        }

        // die Stadt vor dem Himmel: nach den Funken gezeichnet
        draw_skyline();
        present();
    }

    screen_close();
    return 0;
}

Feuerwerk über einer nächtlichen Stadt: Rote und goldene Funkenkreise leuchten über den Dächern, Raketen steigen auf, in den Häusern brennen einzelne Fenster.

Die Funken einer Explosion haben unterschiedliche Lebenszeiten zwischen 1,5 und 2,5 Sekunden. Deshalb hört eine Explosion nicht auf einen Schlag auf, sondern franst aus. Ein Funke ist hell, solange er mehr als eine Sekunde vor sich hat, und wird in der letzten Sekunde dunkler.

Bau dein eigenes Feuerwerk. Ein paar Ideen:
  • Schalen und Scheiben: Was passiert, wenn die Funken einer Explosion zwischen 0 und 50 Pixeln pro Sekunde schnell sind statt zwischen 30 und 50? Und wenn alle genau gleich schnell sind?
  • Bunte Explosionen: Jeder Funke bekommt einen zufälligen Farbverlauf statt den der Rakete.
  • Die Stadt schläft ein: Ab und zu geht ein Fenster aus oder an.
  • Wind: Ein Wert wind, der vx in jedem Bild ein wenig verändert und sich mit den Pfeiltasten einstellen lässt.
  • Doppelte Explosion: Einige Funken explodieren nach kurzer Zeit selbst noch einmal, in einer anderen Farbe.
  • Glitzern: Kurz vor dem Verlöschen flackert die Helligkeit zufällig.
  • Gleich schnell überall: fade() läuft in jedem zweiten Bild. Auf einem Bildschirm mit 120 Bildern pro Sekunde verglühen die Spuren also doppelt so schnell. Wie lässt sich das mit dt lösen?
Aufgabe 5Dein Feuerwerk

Bau mindestens eine der Ideen am Ende der Seite in dein Feuerwerk ein, oder eine eigene, und gib die Datei ab. Deine Lehrkraft sieht genau diesen Stand; du kannst jederzeit neu abgeben.