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(
 63            FigureEightMover(0.5, 0.35, 0.2, 0.23, name="Mover1")
 64        )
 65        trail1 = self._mover1.add_child(Trail2D(name="CyanTrail"))
 66        trail1.length = 40
 67        trail1.width = 8.0
 68        trail1.colour = (0.2, 0.8, 1.0, 1.0)
 69        trail1.colour_end = (0.2, 0.8, 1.0, 0.0)
 70        trail1.lifetime = 1.0
 71
 72        # --- Red-to-yellow mover on a circular orbit ---
 73        self._mover2 = self.add_child(Node2D(name="Mover2"))
 74        trail2 = self._mover2.add_child(Trail2D(name="FireTrail"))
 75        trail2.length = 25
 76        trail2.width = 16.0
 77        trail2.colour = (1.0, 0.2, 0.1, 1.0)
 78        trail2.colour_end = (1.0, 0.9, 0.1, 0.0)
 79        trail2.lifetime = 0.6
 80
 81        self._trails = [trail1, trail2]
 82        self._time = 0.0
 83
 84    def on_update(self, dt: float):
 85        self._time += dt
 86
 87        # Circular orbit for the second mover, sized to the live window
 88        w, h = self.app.width, self.app.height
 89        angle = self._time * 1.8
 90        self._mover2.position = Vec2(
 91            w * 0.5 + math.cos(angle) * w * 0.16,
 92            h * 0.7 + math.sin(angle) * h * 0.1,
 93        )
 94
 95        # Toggle trails with Space
 96        if Input.is_action_just_pressed("toggle_trails"):
 97            for trail in self._trails:
 98                trail.emit = not trail.emit
 99        if Input.is_action_just_pressed("quit"):
100            self.app.quit()
101
102    def on_draw(self, renderer):
103        # Each Trail2D renders its own ribbon via on_draw; the scene only
104        # decorates with mover dots and a HUD.
105        white = (1.0, 1.0, 1.0, 1.0)
106        renderer.draw_circle(self._mover1.position, 5, colour=white)
107        renderer.draw_circle(self._mover2.position, 5, colour=white)
108
109        # HUD
110        renderer.draw_text("TRAIL2D DEMO", (10, 10), colour=(0.78, 0.78, 0.78), scale=2)
111        state = "ON" if self._trails[0].emit else "OFF"
112        renderer.draw_text(
113            f"Space or click = toggle [{state}] | Cyan: figure-8 | Red: orbit",
114            (10, 35),
115            colour=(0.59, 0.59, 0.59),
116        )
117
118
119if __name__ == "__main__":
120    App(width=WIDTH, height=HEIGHT, title="Trail2D Demo").run(TrailDemo())