2D Trail¶
fading ribbons behind moving objects.
▶ Run in browserTags: 2d
Trail2D records where it has been and draws a ribbon through those points:
width wide and coloured colour at the head, tapering to nothing and blending
to colour_end at the tail (width_curve reshapes the taper).
Add it as a child of anything that moves and it follows on its own:
mover.add_child(Trail2D(length=40, width=8, colour=(0.2, 0.8, 1, 1), colour_end=(0.2, 0.8, 1, 0)))
length caps how many points the ribbon keeps (one is recorded per frame) and
lifetime is how many seconds a point lasts before it drops off the tail, so
together they set how long the ribbon is. Setting emit to False stops recording
new points and lets the ribbon fade out; setting it back resumes recording.
Two dots move here, each carrying its own trail: a cyan figure eight with a thin, long trail, and a red orbit with a short, wide trail fading to yellow. Their paths are fractions of the viewport, so they stay on screen at any window size.
Press Space, or click or tap, to toggle both trails.
Source¶
1# /// simvx
2# web = { width = 1024, height = 768 }
3# ///
4"""2D Trail: fading ribbons behind moving objects.
5
6``Trail2D`` records where it has been and draws a ribbon through those points:
7``width`` wide and coloured ``colour`` at the head, tapering to nothing and blending
8to ``colour_end`` at the tail (``width_curve`` reshapes the taper).
9Add it as a child of anything that moves and it follows on its own:
10
11 mover.add_child(Trail2D(length=40, width=8, colour=(0.2, 0.8, 1, 1), colour_end=(0.2, 0.8, 1, 0)))
12
13``length`` caps how many points the ribbon keeps (one is recorded per frame) and
14``lifetime`` is how many seconds a point lasts before it drops off the tail, so
15together they set how long the ribbon is. Setting ``emit`` to False stops recording
16new points and lets the ribbon fade out; setting it back resumes recording.
17
18Two dots move here, each carrying its own trail: a cyan figure eight with a thin,
19long trail, and a red orbit with a short, wide trail fading to yellow. Their paths
20are fractions of the viewport, so they stay on screen at any window size.
21
22Press Space, or click or tap, to toggle both trails.
23"""
24
25import math
26from collections.abc import Callable
27
28from simvx.core import Anchor, CanvasLayer, Key, Label, Node2D, Pointer, Trail2D, on_input
29from simvx.graphics import App
30
31CYAN = (0.2, 0.8, 1.0, 1.0)
32RED = (1.0, 0.2, 0.1, 1.0)
33
34
35def figure_eight(t: float) -> tuple[float, float]:
36 return 0.5 + 0.2 * math.sin(2.0 * t), 0.35 + 0.115 * math.sin(4.0 * t)
37
38
39def orbit(t: float) -> tuple[float, float]:
40 return 0.5 + 0.16 * math.cos(1.8 * t), 0.7 + 0.1 * math.sin(1.8 * t)
41
42
43class Mover(Node2D):
44 """A white dot that follows ``route(seconds)``, a point in fractions of the viewport."""
45
46 def __init__(self, route: Callable[[float], tuple[float, float]], **kwargs):
47 super().__init__(**kwargs)
48 self.route = route
49 self.time = 0.0
50
51 def on_update(self, dt: float):
52 self.time += dt
53 fx, fy = self.route(self.time)
54 w, h = self.viewport_size
55 self.position = (w * fx, h * fy)
56
57 def on_draw(self, renderer):
58 renderer.circle((0, 0), 5, fill=(1.0, 1.0, 1.0, 1.0))
59
60
61class TrailDemo(Node2D):
62 input_context = {"toggle_trails": [Key.SPACE, Pointer.PRIMARY]}
63
64 def on_ready(self):
65 cyan = self.add_child(Mover(figure_eight))
66 cyan.add_child(Trail2D(length=40, width=8.0, lifetime=1.0, colour=CYAN, colour_end=(*CYAN[:3], 0.0)))
67 fire = self.add_child(Mover(orbit))
68 fire.add_child(Trail2D(length=25, width=16.0, lifetime=0.6, colour=RED, colour_end=(1.0, 0.9, 0.1, 0.0)))
69 self.trails = self.find_all(Trail2D)
70 self.help = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=12))
71 self.show_help()
72
73 @on_input("toggle_trails")
74 def toggle(self, event):
75 for trail in self.trails:
76 trail.emit = not trail.emit
77 self.show_help()
78
79 # Example help overlay: not part of the technique shown.
80 def show_help(self):
81 state = "ON" if self.trails[0].emit else "OFF"
82 self.help.text = f"Trails {state} (Space, click or tap to toggle)"
83
84
85if __name__ == "__main__":
86 App(width=1024, height=768, title="Trail2D Demo").run(TrailDemo())