2D Path Follow

an arrow rides a figure-8 bezier curve.

▶ Run in browser

Tags: 2d

A Path2D owns a Curve2D, a chain of points with a bezier in-handle and out-handle on each. The handles are offsets from their point, and every point is in the path’s local space, so moving the Path2D moves the whole route. Here the path is the scene’s root and stays centred on the window.

A PathFollow2D child of the path does the travelling: set its progress, a distance in pixels along the curve, and it places itself on the curve each frame. With rotates=True it also turns to face along the curve, which is why the arrow it draws always points the way it is going. With loop=True a progress past the end wraps to the start and fires the loop_completed signal, which the HUD counts. progress_ratio is the same position as a fraction of the curve’s length.

Path2D draws nothing at runtime, so on_draw strokes the curve from curve.get_baked_points(), the evenly spaced samples the follower moves between.

Hold Up or Down to change the speed, which Property(range=(50, 500)) keeps in bounds.

Source

 1# /// simvx
 2# web = { width = 1024, height = 640 }
 3# ///
 4"""2D Path Follow: an arrow rides a figure-8 bezier curve.
 5
 6A ``Path2D`` owns a ``Curve2D``, a chain of points with a bezier in-handle and out-handle on
 7each. The handles are offsets from their point, and every point is in the path's local space,
 8so moving the ``Path2D`` moves the whole route. Here the path is the scene's root and stays
 9centred on the window.
10
11A ``PathFollow2D`` child of the path does the travelling: set its ``progress``, a distance in
12pixels along the curve, and it places itself on the curve each frame. With ``rotates=True``
13it also turns to face along the curve, which is why the arrow it draws always points the way
14it is going. With ``loop=True`` a progress past the end wraps to the start and fires the
15``loop_completed`` signal, which the HUD counts. ``progress_ratio`` is the same position as a
16fraction of the curve's length.
17
18``Path2D`` draws nothing at runtime, so ``on_draw`` strokes the curve from
19``curve.get_baked_points()``, the evenly spaced samples the follower moves between.
20
21Hold Up or Down to change the speed, which ``Property(range=(50, 500))`` keeps in bounds.
22"""
23
24from simvx.core import Anchor, Axis1D, CanvasLayer, Input, Key, Label, Path2D, PathFollow2D, Property
25from simvx.graphics import App
26
27
28class Arrow(PathFollow2D):
29    def on_draw(self, renderer):
30        # Rotation 0 faces up the screen, so the tip points along -y.
31        renderer.polygon([(0, -14), (9, 10), (-9, 10)], fill=(1.0, 0.4, 0.2, 1.0))
32
33
34class FigureEight(Path2D):
35    input_context = {"speed": [Axis1D(low=Key.DOWN, high=Key.UP)]}
36
37    speed = Property(200.0, range=(50, 500))
38
39    def on_ready(self):
40        # Right loop: leave the centre below the axis, wrap the tip, return above it; the left mirrors it.
41        self.curve.add_point((0, 0), handle_in=(-140, 60), handle_out=(140, -60))
42        self.curve.add_point((200, 150), handle_in=(90, -120), handle_out=(-90, 120))
43        self.curve.add_point((0, 0), handle_in=(-20, 150), handle_out=(20, -150))
44        self.curve.add_point((-200, -150), handle_in=(90, -120), handle_out=(-90, 120))
45        self.curve.add_point((0, 0), handle_in=(-140, 60), handle_out=(140, -60))
46
47        self.arrow = self.add_child(Arrow(rotates=True, loop=True))
48        self.arrow.loop_completed.connect(self.on_loop)
49        self.loops = 0
50        self.hud = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=22))
51
52    def on_loop(self):
53        self.loops += 1
54
55    def on_update(self, dt):
56        self.position = self.viewport_size / 2
57        self.speed += 150.0 * Input.value("speed") * dt
58        self.arrow.progress += self.speed * dt
59        self.hud.text = (
60            f"Speed: {self.speed:.0f} (Up/Down)   Progress: {self.arrow.progress_ratio:.0%}   Loops: {self.loops}"
61        )
62
63    def on_draw(self, renderer):
64        renderer.line(self.curve.get_baked_points(), stroke=(0.5, 0.5, 0.85))
65
66
67if __name__ == "__main__":
68    App(title="2D Path Follow", width=1024, height=640).run(FigureEight())