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
20from simvx.core import Curve2D, Input, InputMap, Key, Node2D, Path2D, PathFollow2D, Property, Text2D, Vec2
21from simvx.graphics import App
22
23WIDTH, HEIGHT = 1024, 640
24
25
26class PathDemo(Node2D):
27 dynamic = True # follower circle advances along the curve every frame
28
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 actual window
37 cx, cy = self.app.width / 2, self.app.height / 2
38 curve = Curve2D(bake_interval=5.0)
39 # Right loop: leave the centre below the axis, wrap the tip, return above it
40 curve.add_point(Vec2(cx, cy), handle_in=Vec2(-140, 60), handle_out=Vec2(140, -60))
41 curve.add_point(Vec2(cx + 200, cy + 150), handle_in=Vec2(90, -120), handle_out=Vec2(-90, 120))
42 curve.add_point(Vec2(cx, cy), handle_in=Vec2(-20, 150), handle_out=Vec2(20, -150))
43 # Left loop (mirrors the right)
44 curve.add_point(Vec2(cx - 200, cy - 150), handle_in=Vec2(90, -120), handle_out=Vec2(-90, 120))
45 curve.add_point(Vec2(cx, cy), handle_in=Vec2(-140, 60), handle_out=Vec2(140, -60))
46
47 self._path = self.add_child(Path2D(name="Path"))
48 self._path.curve = curve
49
50 self._follower = self._path.add_child(PathFollow2D(name="Follower"))
51 self._follower.loop = True
52 self._follower.rotates = True
53 self._follower.loop_completed.connect(self._on_loop)
54 self._loops = 0
55
56 self._hud = self.add_child(Text2D(name="HUD", text="", position=(10, 10), font_scale=1.5))
57
58 def _on_loop(self):
59 self._loops += 1
60
61 def on_update(self, dt: float):
62 # Speed adjustment: Property(range=...) clamps every assignment, so the
63 # 50..500 bounds declared above need no manual min()/max() here.
64 if Input.is_action_pressed("speed_up"):
65 self.speed += 150.0 * dt
66 if Input.is_action_pressed("speed_down"):
67 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())