Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ In diesem Ordner werden die Anforderungen für neue Features dokumentiert.
## Enthaltene Dateien

- [`feature-vorlage.md`](./feature-vorlage.md) – Vorlage für neue Feature-Anforderungen.
- [`signale-erstellen-und-gleiskante-zuordnen.md`](./signale-erstellen-und-gleiskante-zuordnen.md) – Signale anlegen und einer Gleiskante zuordnen.
- [`lustige-giraffe-animation.md`](./lustige-giraffe-animation.md) – Lustige Giraffe zeichnet neue Elemente animiert.
83 changes: 83 additions & 0 deletions Features/lustige-giraffe-animation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Feature: Lustige Giraffe zeichnet neue Elemente

## Übersicht

- **Status:** Abgeschlossen
- **Autor:** Claude Code
- **Erstellt am:** 2026-07-09
- **Letzte Änderung:** 2026-07-09

## Ziel / Problemstellung

Das Anlegen neuer Elemente (Knoten, Kanten, Signale) soll spielerischer und
sichtbarer werden. Statt dass ein Element schlagartig erscheint, taucht eine
kleine, lustige Cartoon-Giraffe auf und „zeichnet" das neue Element vor den
Augen der Nutzerin animiert in die Fläche. Das gibt visuelles Feedback darüber,
was gerade entstanden ist, und macht den Editor sympathischer.

## Anforderungen

### Funktionale Anforderungen

- [x] Beim Hinzufügen eines **Knotens** erscheint die Giraffe und zeichnet den
Knoten (wachsender Kreis) an der Klickposition.
- [x] Beim Hinzufügen einer **Kante** wird die Linie vom Start- zum Zielknoten
animiert gezogen.
- [x] Beim Hinzufügen eines **Signals** wird der Signalarm von der Gleiskante
bis zum Marker animiert gezeichnet.
- [x] Die Giraffe steht neben der „Stiftspitze" und wackelt leicht mit dem Kopf,
eine gestrichelte Linie verbindet ihre Schnauze mit der wachsenden Spitze.
- [x] Das Element wird erst nach Abschluss der Animation ins Modell übernommen,
sodass es tatsächlich gezeichnet erscheint statt sofort dazustehen.
- [x] Wird während einer laufenden Animation eine weitere Aktion ausgelöst
(schnelles Hinzufügen, Werkzeugwechsel, „Leeren", „Beispielnetz"), wird
die aktuelle Animation sauber abgeschlossen; kein Element geht verloren.

### Nicht-funktionale Anforderungen

- [x] Die Animation lebt vollständig im `view`-Paket; `model` und `geometry`
bleiben JavaFX-frei und testbar.
- [x] Die reine Zeit-/Interpolationsmathematik (`Easing.smoothStep`,
`Vec2.lerp`) liegt im `geometry`-Paket und ist per Unit-Test abgedeckt.
- [x] Architektur-Guardrails eingehalten (max. 3 Parameter, max. 500 LOC/Klasse);
`mvn verify` (Checkstyle, PMD, SpotBugs, JaCoCo) läuft grün.

## Nutzer / Zielgruppe

Alle, die den Gleisnetz-Editor bedienen und neue Netzelemente anlegen.

## Ein- und Ausgaben

- **Eingaben:** Mausklicks der Werkzeuge Knoten / Kante / Signal.
- **Ausgaben:** Kurze Zeichen-Animation (~1,1 s) auf dem Canvas; danach ist das
neue Element Teil des Netzes.

## Akzeptanzkriterien

- [x] Nach dem Anlegen jedes Elementtyps ist für kurze Zeit eine Giraffe zu
sehen, die das Element zeichnet, danach ist das Element vorhanden.
- [x] Die Domänenlogik bleibt ohne JavaFX-Abhängigkeit; alle Tests grün.
- [x] `mvn verify` ist erfolgreich.

## Umsetzung (Kurzüberblick)

- `geometry/Easing.java` – `smoothStep` für sanftes Ein-/Ausblenden der Bewegung.
- `geometry/Vec2.java` – `lerp` für das Wachsen entlang einer Strecke.
- `view/DrawTarget.java` – beschreibt Geometrie des neuen Elements (Kind + Start/Ende).
- `view/Giraffe.java` – zeichnet die Cartoon-Giraffe (Körper, Flecken, Hals, Kopf).
- `view/GiraffeAnimator.java` – `AnimationTimer`, der pro Frame das Netz neu
zeichnet, das wachsende Element und die Giraffe darüber legt und das Element am
Ende via Callback ins Modell übernimmt.
- `view/NetworkEditorController.java` – löst die Animation beim Anlegen von
Knoten/Kante/Signal aus.

## Offene Fragen / Annahmen

- Annahme: Eine feste Animationsdauer (~1,1 s) ist angenehm; bei Bedarf leicht
über `GiraffeAnimator.DURATION_NANOS` anpassbar.
- Die Animation ist rein kosmetisch und wird von JaCoCo (wie `view`/`App`) nicht
auf Coverage geprüft; die zugrunde liegende Mathematik ist getestet.

## Referenzen

- [`AGENTS.md`](../AGENTS.md) – Architektur, Guardrails, Definition of Done.
22 changes: 22 additions & 0 deletions src/main/java/com/example/vibeapp/geometry/Easing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.vibeapp.geometry;

/**
* Pure easing helpers for time-based animations. UI-free (like the rest of this
* package) so the timing math stays unit-testable without the JavaFX toolkit.
*/
public final class Easing {

private Easing() {
}

/**
* Smooth ease-in/ease-out of a normalized progress value. The input is
* clamped to [0,1] and the classic smoothstep curve {@code 3t²-2t³} is
* applied: it returns 0 at 0, 1 at 1, is symmetric around 0.5 and has zero
* slope at both ends, so an animation accelerates and decelerates gently.
*/
public static double smoothStep(double t) {
double c = Math.max(0.0, Math.min(1.0, t));
return c * c * (3.0 - 2.0 * c);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/example/vibeapp/geometry/Vec2.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ public Vec2 subtract(Vec2 other) {
public double length() {
return Math.hypot(x, y);
}

/**
* Point fraction {@code t} of the way from this vector toward {@code target}
* ({@code t} = 0 returns this point, {@code t} = 1 returns {@code target}).
* Used to animate an element growing along a straight path.
*/
public Vec2 lerp(Vec2 target, double t) {
return new Vec2(x + (target.x - x) * t, y + (target.y - y) * t);
}
Comment on lines +22 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programmierung (Null-Prüfung):

Um eine NullPointerException bei der Dereferenzierung von target zu verhindern, sollte ein expliziter Null-Check hinzugefügt werden, wie es in den Qualitätsrichtlinien für Java-Projekte empfohlen wird.

    public Vec2 lerp(Vec2 target, double t) {
        java.util.Objects.requireNonNull(target, "target must not be null");
        return new Vec2(x + (target.x - x) * t, y + (target.y - y) * t);
    }

}
26 changes: 26 additions & 0 deletions src/main/java/com/example/vibeapp/view/DrawTarget.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.vibeapp.view;

import java.util.Objects;

import com.example.vibeapp.geometry.Vec2;

/**
* Geometry of a freshly created element, handed to the {@link GiraffeAnimator}
* so the giraffe can trace it into existence.
*
* <p>{@code start} and {@code end} bound the stroke that gets drawn: a
* {@link Kind#NODE} uses the same point for both, a {@link Kind#EDGE} spans its
* two nodes, and a {@link Kind#SIGNAL} runs from its base on the edge to its
* offset marker.
*/
record DrawTarget(Kind kind, Vec2 start, Vec2 end) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programmierung (Validierung im Record):

Da Java-Records standardmäßig null-Werte für ihre Felder erlauben, sollten wir einen kompakten Konstruktor hinzufügen, um sicherzustellen, dass kind, start und end niemals null sind. Dies verhindert unerwartete NullPointerExceptions zur Laufzeit im GiraffeAnimator.

record DrawTarget(Kind kind, Vec2 start, Vec2 end) {

    DrawTarget {
        java.util.Objects.requireNonNull(kind, "kind must not be null");
        java.util.Objects.requireNonNull(start, "start must not be null");
        java.util.Objects.requireNonNull(end, "end must not be null");
    }


DrawTarget {
Objects.requireNonNull(kind, "kind must not be null");
Objects.requireNonNull(start, "start must not be null");
Objects.requireNonNull(end, "end must not be null");
}

/** The kind of element being drawn, which selects how it is animated. */
enum Kind { NODE, EDGE, SIGNAL }
}
116 changes: 116 additions & 0 deletions src/main/java/com/example/vibeapp/view/Giraffe.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.example.vibeapp.view;

import java.util.Objects;

import com.example.vibeapp.geometry.Vec2;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
* Draws a small cartoon giraffe on a canvas — the friendly mascot that appears
* and "draws" newly added elements. Pure rendering: it holds a graphics context
* and paints on request; a bob {@code phase} gives its head a bit of life. The
* giraffe faces right, toward the element it is drawing.
*/
final class Giraffe {

private static final Color COAT = Color.web("#f2c14e");
private static final Color COAT_DARK = Color.web("#e0a92e");
private static final Color SPOT = Color.web("#a86a2c");
private static final Color HOOF = Color.web("#5b4327");
private static final Color MANE = Color.web("#b5651d");
private static final Color EYE = Color.web("#2b2118");

private final GraphicsContext gc;

Giraffe(GraphicsContext gc) {
this.gc = Objects.requireNonNull(gc, "gc must not be null");
}
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programmierung (Null-Prüfung):

Der GraphicsContext sollte im Konstruktor auf null geprüft werden, um einen schnellen Ausfall (Fail-Fast) zu garantieren.

Suggested change
Giraffe(GraphicsContext gc) {
this.gc = gc;
}
Giraffe(GraphicsContext gc) {
this.gc = java.util.Objects.requireNonNull(gc, "gc must not be null");
}


/**
* Draws the giraffe standing on {@code feet} (its bottom-centre point), with
* {@code phase} (radians) driving a gentle head bob so it looks alive while
* it is drawing. The snout ends up near {@code feet} + (23, -48).
*/
void draw(Vec2 feet, double phase) {
double bob = Math.sin(phase) * 2.0;
double bodyCy = feet.y() - 26.0;
drawLegs(feet, bodyCy);
drawTail(feet.x() - 12.0, bodyCy);
drawBody(feet.x(), bodyCy);
drawNeckAndHead(feet.x(), bodyCy, bob);
}

private void drawLegs(Vec2 feet, double bodyCy) {
double top = bodyCy + 6.0;
double footY = feet.y();
double[] xs = {feet.x() - 8.0, feet.x() - 3.0, feet.x() + 4.0, feet.x() + 9.0};
gc.setLineWidth(3.5);
gc.setStroke(COAT);
for (double x : xs) {
gc.strokeLine(x, top, x, footY);
}
gc.setStroke(HOOF);
for (double x : xs) {
gc.strokeLine(x, footY - 2.5, x, footY);
}
}

private void drawBody(double cx, double cy) {
gc.setFill(COAT);
gc.fillOval(cx - 13.0, cy - 8.0, 26.0, 16.0);
gc.setFill(SPOT);
gc.fillOval(cx - 8.0, cy - 4.0, 4.0, 4.0);
gc.fillOval(cx - 1.0, cy - 1.0, 5.0, 5.0);
gc.fillOval(cx + 5.0, cy - 5.0, 4.0, 4.0);
gc.fillOval(cx + 3.0, cy + 2.0, 3.5, 3.5);
}

private void drawTail(double x, double cy) {
gc.setStroke(COAT_DARK);
gc.setLineWidth(1.5);
gc.strokeLine(x, cy, x - 5.0, cy + 8.0);
gc.setFill(MANE);
gc.fillOval(x - 7.0, cy + 7.0, 3.5, 4.5);
}

private void drawNeckAndHead(double cx, double cy, double bob) {
double neckBaseX = cx + 7.0;
double neckBaseY = cy - 6.0;
double headX = cx + 15.0 + bob;
double headY = cy - 24.0 + bob * 0.5;
gc.setStroke(COAT);
gc.setLineWidth(6.0);
gc.strokeLine(neckBaseX, neckBaseY, headX, headY + 4.0);
gc.setStroke(MANE);
gc.setLineWidth(2.0);
gc.strokeLine(neckBaseX - 2.0, neckBaseY, headX - 2.0, headY + 4.0);
drawHead(headX, headY);
}

private void drawHead(double x, double y) {
// Ossicones (the little horns) with knobbed tips.
gc.setStroke(COAT_DARK);
gc.setLineWidth(1.5);
gc.strokeLine(x - 2.0, y - 2.0, x - 3.0, y - 7.0);
gc.strokeLine(x + 3.0, y - 2.0, x + 3.0, y - 7.0);
gc.setFill(MANE);
gc.fillOval(x - 4.5, y - 9.0, 3.0, 3.0);
gc.fillOval(x + 1.5, y - 9.0, 3.0, 3.0);
// Head and snout (facing right).
gc.setFill(COAT);
gc.fillOval(x - 5.0, y - 3.0, 10.0, 8.0);
gc.fillOval(x + 2.0, y - 1.0, 8.0, 6.0);
// Ear on the far side.
gc.setFill(COAT_DARK);
gc.fillOval(x - 7.0, y - 3.0, 4.0, 2.5);
// Eye, nostril and a friendly smile.
gc.setFill(EYE);
gc.fillOval(x - 1.0, y - 1.0, 2.0, 2.0);
gc.fillOval(x + 8.0, y + 1.5, 1.3, 1.3);
gc.setStroke(EYE);
gc.setLineWidth(0.8);
gc.strokeLine(x + 6.0, y + 3.5, x + 9.0, y + 3.0);
}
}
Loading
Loading