Camera2D

follow a target with smoothing, zoom, and edge limits.

▶ Run in browser

Tags: 2d camera

Add a Camera2D and point its target at a node; the view then follows that node. smoothing adds lag so the camera eases after the player instead of snapping, zoom scales the view, and the limit_* properties stop the camera from showing past the edges of your world. Move the cube with WASD, the arrows, or by holding the left mouse button (tap and hold on touch screens) and watch the grid scroll underneath it while the cube stays near the centre.

What it demonstrates

  • Camera2D + camera.target = node – the renderer tracks the target’s position.

  • smoothing – ease toward the target instead of snapping (0 = instant).

  • zoom – scale the whole view.

  • limit_left/right/top/bottom – clamp the camera so it never shows past the world edges.

  • World-space drawing (the grid, camera-affected) vs a screen-fixed HUD (a Text2D node).

Source

  1"""Camera2D: follow a target with smoothing, zoom, and edge limits.
  2
  3Add a `Camera2D` and point its `target` at a node; the view then follows that node.
  4`smoothing` adds lag so the camera eases after the player instead of snapping, `zoom`
  5scales the view, and the `limit_*` properties stop the camera from showing past the
  6edges of your world. Move the cube with WASD, the arrows, or by holding the left mouse
  7button (tap and hold on touch screens) and watch the grid scroll underneath it while
  8the cube stays near the centre.
  9
 10# /// simvx
 11# tags = ["2d", "camera"]
 12# web = { root = "CameraDemo", width = 800, height = 600, responsive = true }
 13# ///
 14
 15## What it demonstrates
 16
 17- `Camera2D` + `camera.target = node` -- the renderer tracks the target's position.
 18- `smoothing` -- ease toward the target instead of snapping (0 = instant).
 19- `zoom` -- scale the whole view.
 20- `limit_left/right/top/bottom` -- clamp the camera so it never shows past the world edges.
 21- World-space drawing (the grid, camera-affected) vs a screen-fixed HUD (a `Text2D` node).
 22"""
 23
 24import math
 25
 26from simvx.core import Camera2D, Input, Key, MouseButton, Node2D, Text2D, Vec2
 27from simvx.graphics import App
 28
 29WIDTH, HEIGHT = 800, 600
 30WORLD = 700  # half-size of the playable world
 31
 32# World-space landmarks (x, y, colour) so the scroll has something to read against.
 33LANDMARKS = [
 34    (-400, -300, (0.9, 0.4, 0.4, 1)),
 35    (400, 300, (0.4, 0.9, 0.5, 1)),
 36    (400, -300, (0.9, 0.8, 0.3, 1)),
 37    (-400, 300, (0.7, 0.5, 0.95, 1)),
 38]
 39
 40
 41class Player(Node2D):
 42    SPEED = 320.0
 43
 44    camera: Camera2D | None = None  # set by CameraDemo; used to map the pointer into world space
 45
 46    def on_update(self, dt: float):
 47        dx = Input.get_strength("move_right") - Input.get_strength("move_left")
 48        dy = Input.get_strength("move_down") - Input.get_strength("move_up")
 49        self.position += Vec2(dx, dy) * self.SPEED * dt
 50        # Mouse/touch: hold the left button (a touch on the web) to steer toward the
 51        # pointer; the camera converts the screen position into world coordinates.
 52        if self.camera is not None and Input.is_mouse_button_pressed(MouseButton.LEFT):
 53            target = self.camera.screen_to_world(Input.mouse_position, Vec2(self.app.width, self.app.height))
 54            offset = target - self.position
 55            dist = math.hypot(offset.x, offset.y)
 56            if dist > 4.0:  # dead zone so the cube settles under the pointer
 57                self.position += offset * (min(self.SPEED * dt, dist) / dist)
 58        self.position.x = max(-WORLD, min(WORLD, self.position.x))
 59        self.position.y = max(-WORLD, min(WORLD, self.position.y))
 60
 61    def on_draw(self, renderer):
 62        renderer.draw_rect(
 63            (self.position.x - 20, self.position.y - 20),
 64            (40, 40),
 65            colour=(0.4, 0.8, 1.0, 1.0),
 66            filled=True,
 67        )
 68
 69
 70class CameraDemo(Node2D):
 71    dynamic = True  # world-space grid scrolls as the followed camera moves
 72
 73    input_actions = {
 74        "move_left": [Key.A, Key.LEFT],
 75        "move_right": [Key.D, Key.RIGHT],
 76        "move_up": [Key.W, Key.UP],
 77        "move_down": [Key.S, Key.DOWN],
 78    }
 79
 80    def on_ready(self):
 81        self.player = self.add_child(Player(position=Vec2(0, 0)))
 82
 83        cam = self.add_child(Camera2D())
 84        cam.target = self.player  # follow the player
 85        cam.smoothing = 6.0  # ease after it (0 = snap)
 86        cam.zoom = 1.0
 87        # Stop the camera at the world edges (view half-size is screen/2 / zoom).
 88        # These are computed from the launch size, so a resized window shows a little
 89        # past the edge; recompute them from the live size if that matters to you.
 90        cam.limit_left = -WORLD + WIDTH / 2
 91        cam.limit_right = WORLD - WIDTH / 2
 92        cam.limit_top = -WORLD + HEIGHT / 2
 93        cam.limit_bottom = WORLD - HEIGHT / 2
 94        self.player.camera = cam  # lets the player map pointer clicks into world space
 95
 96        # Screen-fixed HUD: a Text2D node renders as an overlay, ignoring the camera.
 97        self.add_child(
 98            Text2D(
 99                text="Camera2D: WASD/arrows or click/tap to move (camera follows)",
100                position=Vec2(20, 20),
101                font_scale=1.5,
102                colour=(1, 1, 1, 1),
103            )
104        )
105
106    def on_draw(self, renderer):
107        # World-space grid + landmarks: these are drawn in world coordinates, so the
108        # camera transform scrolls them as the player moves.
109        for gx in range(-WORLD, WORLD + 1, 100):
110            for gy in range(-WORLD, WORLD + 1, 100):
111                renderer.draw_circle((gx, gy), 3, colour=(0.3, 0.3, 0.36, 1.0), filled=True)
112        for lx, ly, col in LANDMARKS:
113            renderer.draw_rect((lx - 30, ly - 30), (60, 60), colour=col, filled=True)
114
115
116if __name__ == "__main__":
117    App(title="Camera2D", width=WIDTH, height=HEIGHT).run(CameraDemo())