2D Path Follow

Play Demo

Demonstrates:

  • Curve2D with bezier control points

  • Path2D / PathFollow2D for automatic motion along a curve

  • draw() callback for rendering the path and follower

  • Speed control via input actions

Controls: Up / Down - Increase / decrease speed Escape - Quit

Run: uv run python packages/graphics/examples/2d_path_follow.py

Source Code

 1"""2D Path Follow demo -- a circle follows a figure-8 bezier curve.
 2
 3Demonstrates:
 4  - Curve2D with bezier control points
 5  - Path2D / PathFollow2D for automatic motion along a curve
 6  - draw() callback for rendering the path and follower
 7  - Speed control via input actions
 8
 9Controls:
10    Up / Down  - Increase / decrease speed
11    Escape     - Quit
12
13Run: uv run python packages/graphics/examples/2d_path_follow.py
14"""
15
16
17from simvx.core import Curve2D, Input, InputMap, Key, Node2D, Path2D, PathFollow2D, Property, Text2D, Vec2
18from simvx.graphics import App
19
20WIDTH, HEIGHT = 1024, 640
21CX, CY = WIDTH / 2, HEIGHT / 2
22
23
24class PathDemo(Node2D):
25    speed = Property(200.0, range=(50, 500))
26
27    def ready(self):
28        InputMap.add_action("speed_up", [Key.UP])
29        InputMap.add_action("speed_down", [Key.DOWN])
30        InputMap.add_action("quit", [Key.ESCAPE])
31
32        # Build a figure-8 curve centred on the window
33        curve = Curve2D(bake_interval=5.0)
34        # Right loop
35        curve.add_point(Vec2(CX, CY), handle_in=Vec2(0, -120), handle_out=Vec2(0, 120))
36        curve.add_point(Vec2(CX + 200, CY + 150), handle_in=Vec2(-80, 0), handle_out=Vec2(80, 0))
37        curve.add_point(Vec2(CX, CY), handle_in=Vec2(0, 120), handle_out=Vec2(0, -120))
38        # Left loop (mirrors the right)
39        curve.add_point(Vec2(CX - 200, CY - 150), handle_in=Vec2(80, 0), handle_out=Vec2(-80, 0))
40        curve.add_point(Vec2(CX, CY), handle_in=Vec2(0, -120), handle_out=Vec2(0, 120))
41
42        self._path = self.add_child(Path2D(name="Path"))
43        self._path.curve = curve
44
45        self._follower = self._path.add_child(PathFollow2D(name="Follower"))
46        self._follower.loop = True
47        self._follower.rotates = True
48        self._follower.loop_completed.connect(self._on_loop)
49        self._loops = 0
50
51        self._hud = self.add_child(Text2D(name="HUD", text="", x=10, y=10, font_scale=1.5))
52
53    def _on_loop(self):
54        self._loops += 1
55
56    def process(self, dt: float):
57        # Speed adjustment
58        if Input.is_action_pressed("speed_up"):
59            self.speed = min(500.0, self.speed + 150.0 * dt)
60        if Input.is_action_pressed("speed_down"):
61            self.speed = max(50.0, self.speed - 150.0 * dt)
62        if Input.is_action_just_pressed("quit"):
63            self.app.quit()
64            return
65
66        self._follower.progress += self.speed * dt
67
68        ratio = self._follower.progress_ratio
69        length = self._path.curve.get_baked_length()
70        self._hud.text = (
71            f"Speed: {self.speed:.0f} (Up/Down)  "
72            f"Progress: {ratio:.1%}  Length: {length:.0f}  Loops: {self._loops}"
73        )
74
75    def draw(self, renderer):
76        # Draw the baked curve as connected line segments
77        points = self._path.curve.get_baked_points()
78        if len(points) >= 2:
79            renderer.draw_lines(points, closed=False, colour=(0.31, 0.31, 0.55))
80
81        # Draw follower as a filled circle
82        pos = self._follower.world_position
83        renderer.draw_circle(pos, 10, colour=(1.0, 0.4, 0.2, 1.0), filled=True)
84        renderer.draw_circle(pos, 3, colour=(1.0, 1.0, 0.8, 1.0), filled=True)
85
86
87if __name__ == "__main__":
88    App(title="2D Path Follow", width=WIDTH, height=HEIGHT).run(PathDemo())