-
Notifications
You must be signed in to change notification settings - Fork 0
Lustige Giraffe zeichnet neue Elemente animiert #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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); | ||
| } | ||
| } |
| 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defensive Programmierung (Validierung im Record): Da Java-Records standardmäßig 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 } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defensive Programmierung (Null-Prüfung): Der
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 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); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defensive Programmierung (Null-Prüfung):
Um eine
NullPointerExceptionbei der Dereferenzierung vontargetzu verhindern, sollte ein expliziter Null-Check hinzugefügt werden, wie es in den Qualitätsrichtlinien für Java-Projekte empfohlen wird.