2D Trail

Colour-gradient trails behind moving objects.

▶ Run in browser

Tags: 2d

Demonstrates:

  • Trail2D as a child of a moving Node2D

  • Configurable length, width, colour gradient, and lifetime

  • Multiple trails with different visual settings

  • Toggle emission on/off with Space or a click/tap

Run: uv run python examples/features/2d/trail.py

Controls: Space / Click - Toggle trails on/off Escape - Quit

Source

  1"""2D Trail: Colour-gradient trails behind moving objects.
  2
  3# /// simvx
  4# web = { width = 1024, height = 768 }
  5# ///
  6
  7Demonstrates:
  8  - Trail2D as a child of a moving Node2D
  9  - Configurable length, width, colour gradient, and lifetime
 10  - Multiple trails with different visual settings
 11  - Toggle emission on/off with Space or a click/tap
 12
 13Run: uv run python examples/features/2d/trail.py
 14
 15Controls:
 16    Space / Click - Toggle trails on/off
 17    Escape        - Quit
 18"""
 19
 20import math
 21
 22from simvx.core import Input, InputMap, Key, MouseButton, Node2D, Property, Trail2D, Vec2
 23from simvx.graphics import App
 24
 25WIDTH, HEIGHT = 1024, 768
 26
 27
 28class FigureEightMover(Node2D):
 29    """Moves in a figure-8 (lemniscate) pattern sized to the live window.
 30
 31    Centre and radii are fractions of the window size, so the path stays
 32    on screen at any resolution.
 33    """
 34
 35    speed = Property(2.0)
 36
 37    def __init__(self, cx: float, cy: float, rx: float, ry: float, **kwargs):
 38        super().__init__(**kwargs)
 39        self._cx, self._cy = cx, cy
 40        self._rx, self._ry = rx, ry
 41        self._time = 0.0
 42
 43    def on_update(self, dt: float):
 44        self._time += dt * self.speed
 45        t = self._time
 46        w, h = self.app.width, self.app.height
 47        x = w * self._cx + math.sin(t) * w * self._rx
 48        y = h * self._cy + math.sin(t * 2) * h * self._ry * 0.5
 49        self.position = Vec2(x, y)
 50
 51
 52class TrailDemo(Node2D):
 53    """Root scene with two moving objects, each with a Trail2D child."""
 54
 55    dynamic = True  # the dots + HUD read live positions every frame
 56
 57    def on_ready(self):
 58        InputMap.add_action("toggle_trails", [Key.SPACE, MouseButton.LEFT])
 59        InputMap.add_action("quit", [Key.ESCAPE])
 60
 61        # --- Cyan figure-8 mover with a thin, long trail ---
 62        self._mover1 = self.add_child(FigureEightMover(0.5, 0.35, 0.2, 0.23, name="Mover1"))
 63        trail1 = self._mover1.add_child(Trail2D(name="CyanTrail"))
 64        trail1.length = 40
 65        trail1.width = 8.0
 66        trail1.colour = (0.2, 0.8, 1.0, 1.0)
 67        trail1.colour_end = (0.2, 0.8, 1.0, 0.0)
 68        trail1.lifetime = 1.0
 69
 70        # --- Red-to-yellow mover on a circular orbit ---
 71        self._mover2 = self.add_child(Node2D(name="Mover2"))
 72        trail2 = self._mover2.add_child(Trail2D(name="FireTrail"))
 73        trail2.length = 25
 74        trail2.width = 16.0
 75        trail2.colour = (1.0, 0.2, 0.1, 1.0)
 76        trail2.colour_end = (1.0, 0.9, 0.1, 0.0)
 77        trail2.lifetime = 0.6
 78
 79        self._trails = [trail1, trail2]
 80        self._time = 0.0
 81
 82    def on_update(self, dt: float):
 83        self._time += dt
 84
 85        # Circular orbit for the second mover, sized to the live window
 86        w, h = self.app.width, self.app.height
 87        angle = self._time * 1.8
 88        self._mover2.position = Vec2(
 89            w * 0.5 + math.cos(angle) * w * 0.16,
 90            h * 0.7 + math.sin(angle) * h * 0.1,
 91        )
 92
 93        # Toggle trails with Space
 94        if Input.is_action_just_pressed("toggle_trails"):
 95            for trail in self._trails:
 96                trail.emit = not trail.emit
 97        if Input.is_action_just_pressed("quit"):
 98            self.app.quit()
 99
100    def on_draw(self, renderer):
101        # Each Trail2D renders its own ribbon via on_draw; the scene only
102        # decorates with mover dots and a HUD.
103        white = (1.0, 1.0, 1.0, 1.0)
104        renderer.draw_circle(self._mover1.position, 5, colour=white, filled=True)
105        renderer.draw_circle(self._mover2.position, 5, colour=white, filled=True)
106
107        # HUD
108        renderer.draw_text("TRAIL2D DEMO", (10, 10), colour=(0.78, 0.78, 0.78), scale=2)
109        state = "ON" if self._trails[0].emit else "OFF"
110        renderer.draw_text(
111            f"Space or click = toggle [{state}] | Cyan: figure-8 | Red: orbit",
112            (10, 35),
113            colour=(0.59, 0.59, 0.59),
114        )
115
116
117if __name__ == "__main__":
118    App(width=WIDTH, height=HEIGHT, title="Trail2D Demo").run(TrailDemo())