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
25
26
27class PathDemo(Node2D):
28    dynamic = True  # follower circle advances along the curve every frame
29
30    speed = Property(200.0, range=(50, 500))
31
32    def on_ready(self):
33        InputMap.add_action("speed_up", [Key.UP])
34        InputMap.add_action("speed_down", [Key.DOWN])
35        InputMap.add_action("quit", [Key.ESCAPE])
36
37        # Build a figure-8 curve centred on the actual window
38        cx, cy = self.app.width / 2, self.app.height / 2
39        curve = Curve2D(bake_interval=5.0)
40        # Right loop: leave the centre below the axis, wrap the tip, return above it
41        curve.add_point(Vec2(cx, cy), handle_in=Vec2(-140, 60), handle_out=Vec2(140, -60))
42        curve.add_point(Vec2(cx + 200, cy + 150), handle_in=Vec2(90, -120), handle_out=Vec2(-90, 120))
43        curve.add_point(Vec2(cx, cy), handle_in=Vec2(-20, 150), handle_out=Vec2(20, -150))
44        # Left loop (mirrors the right)
45        curve.add_point(Vec2(cx - 200, cy - 150), handle_in=Vec2(90, -120), handle_out=Vec2(-90, 120))
46        curve.add_point(Vec2(cx, cy), handle_in=Vec2(-140, 60), handle_out=Vec2(140, -60))
47
48        self._path = self.add_child(Path2D(name="Path"))
49        self._path.curve = curve
50
51        self._follower = self._path.add_child(PathFollow2D(name="Follower"))
52        self._follower.loop = True
53        self._follower.rotates = True
54        self._follower.loop_completed.connect(self._on_loop)
55        self._loops = 0
56
57        self._hud = self.add_child(Text2D(name="HUD", text="", position=(10, 10), font_scale=1.5))
58
59    def _on_loop(self):
60        self._loops += 1
61
62    def on_update(self, dt: float):
63        # Speed adjustment
64        if Input.is_action_pressed("speed_up"):
65            self.speed = min(500.0, self.speed + 150.0 * dt)
66        if Input.is_action_pressed("speed_down"):
67            self.speed = max(50.0, self.speed - 150.0 * dt)
68        if Input.is_action_just_pressed("quit"):
69            self.app.quit()
70            return
71
72        self._follower.progress += self.speed * dt
73
74        ratio = self._follower.progress_ratio
75        self._hud.text = f"Speed: {self.speed:.0f} (Up/Down)  Progress: {ratio:.0%}  Loops: {self._loops}"
76
77    def on_draw(self, renderer):
78        # Draw the baked curve as connected line segments
79        points = self._path.curve.get_baked_points()
80        if len(points) >= 2:
81            renderer.draw_lines(points, closed=False, colour=(0.5, 0.5, 0.85))
82
83        # Draw follower as a filled circle
84        pos = self._follower.world_position
85        renderer.draw_circle(pos, 10, colour=(1.0, 0.4, 0.2, 1.0), filled=True)
86        renderer.draw_circle(pos, 3, colour=(1.0, 1.0, 0.8, 1.0), filled=True)
87
88
89if __name__ == "__main__":
90    App(title="2D Path Follow", width=WIDTH, height=HEIGHT).run(PathDemo())