2D Path Follow

A circle follows a figure-8 bezier curve.

▶ Run in browser

Tags: 2d

Demonstrates:

  • Curve2D with bezier control points

  • Path2D / PathFollow2D for automatic motion along a curve

  • on_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 examples/features/2d/path_follow.py

Source

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