2D Path Follow¶
A circle follows a figure-8 bezier curve.
▶ Run in browserTags: 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())