Skip to main content

camera/
camera.rs

1//! # Camera Example
2//!
3//! Demonstrates jengine's 2D camera system.
4//!
5//! Concepts shown:
6//!   · `set_camera_pos(x, y)`   — snap the camera instantly to a world-pixel position
7//!   · `move_camera_pos(x, y)`  — set a new target; camera lerps there smoothly (8×/s)
8//!   · `set_camera_zoom(z)`     — target zoom level; lerped toward each frame
9//!   · `camera_shake(intensity)` — trigger a 0.5-second sinusoidal screen-shake
10//!   · `camera_zoom()`          — read the current (interpolated) zoom level
11//!
12//! The world is a large colourful tile grid that stays fixed; only the camera moves.
13//! UI elements (the HUD) are drawn in screen-space and are therefore unaffected by
14//! the camera transform.
15//!
16//! Controls:
17//!   Arrow keys   — pan camera (smooth lerp)
18//!   = / +        — zoom in  ×1.25
19//!   - / _        — zoom out ÷1.25
20//!   Space        — camera shake
21//!   R            — reset camera to default position and zoom
22//!   Esc          — quit
23
24use jengine::engine::{Color, Game, jEngine, KeyCode};
25use jengine::renderer::text::Font;
26use jengine::ui::modern::Panel;
27use jengine::{DEFAULT_FONT_METADATA, DEFAULT_TILE_H, DEFAULT_TILE_W, DEFAULT_TILESET};
28
29// ── World constants ───────────────────────────────────────────────────────────
30
31/// The world is WORLD_W × WORLD_H tiles regardless of window size.
32const WORLD_W: u32 = 80;
33const WORLD_H: u32 = 50;
34
35// ── Camera state (desired values, independent of the engine camera) ───────────
36
37struct CameraDemo {
38    font_loaded: bool,
39    /// Current zoom target we track independently to display it.
40    zoom_target: f32,
41    /// Camera target position in world pixels.
42    cam_x: f32,
43    cam_y: f32,
44}
45
46impl CameraDemo {
47    fn new() -> Self {
48        // Initial camera position: the centre of the world.
49        let cx = WORLD_W as f32 * DEFAULT_TILE_W as f32 * 0.5;
50        let cy = WORLD_H as f32 * DEFAULT_TILE_H as f32 * 0.5;
51        Self {
52            font_loaded: false,
53            zoom_target: 1.0,
54            cam_x: cx,
55            cam_y: cy,
56        }
57    }
58
59    fn default_camera_pos() -> (f32, f32) {
60        (
61            WORLD_W as f32 * DEFAULT_TILE_W as f32 * 0.5,
62            WORLD_H as f32 * DEFAULT_TILE_H as f32 * 0.5,
63        )
64    }
65}
66
67impl Game for CameraDemo {
68    fn update(&mut self, engine: &mut jEngine) {
69        if engine.is_key_pressed(KeyCode::Escape) {
70            engine.request_quit();
71            return;
72        }
73
74        let step = engine.tile_width() as f32 * 3.0; // pan speed in world pixels
75
76        // ── Camera pan (smooth lerp target) ──────────────────────────────────
77        if engine.is_key_held(KeyCode::ArrowLeft)  { self.cam_x -= step; }
78        if engine.is_key_held(KeyCode::ArrowRight) { self.cam_x += step; }
79        if engine.is_key_held(KeyCode::ArrowUp)    { self.cam_y -= step; }
80        if engine.is_key_held(KeyCode::ArrowDown)  { self.cam_y += step; }
81
82        // Clamp the target within world bounds so the user cannot scroll off-map.
83        let tw = engine.tile_width() as f32;
84        let th = engine.tile_height() as f32;
85        self.cam_x = self.cam_x.clamp(0.0, WORLD_W as f32 * tw);
86        self.cam_y = self.cam_y.clamp(0.0, WORLD_H as f32 * th);
87
88        // Apply the smooth-lerp target.  The camera will glide to this position.
89        engine.move_camera_pos(self.cam_x, self.cam_y);
90
91        // ── Zoom ──────────────────────────────────────────────────────────────
92        if engine.is_key_pressed(KeyCode::Equal) || engine.is_key_pressed(KeyCode::NumpadAdd) {
93            self.zoom_target = (self.zoom_target * 1.25).min(4.0);
94            engine.set_camera_zoom(self.zoom_target);
95        }
96        if engine.is_key_pressed(KeyCode::Minus) || engine.is_key_pressed(KeyCode::NumpadSubtract) {
97            self.zoom_target = (self.zoom_target / 1.25).max(0.25);
98            engine.set_camera_zoom(self.zoom_target);
99        }
100
101        // ── Shake ─────────────────────────────────────────────────────────────
102        if engine.is_key_pressed(KeyCode::Space) {
103            // Intensity in world pixels; decays over 0.5 s.
104            engine.camera_shake(12.0);
105        }
106
107        // ── Reset ─────────────────────────────────────────────────────────────
108        if engine.is_key_pressed(KeyCode::KeyR) {
109            let (cx, cy) = Self::default_camera_pos();
110            self.cam_x = cx;
111            self.cam_y = cy;
112            self.zoom_target = 1.0;
113            // `set_camera_pos` snaps immediately (no lerp).
114            engine.set_camera_pos(cx, cy);
115            engine.set_camera_zoom(1.0);
116        }
117    }
118
119    fn render(&mut self, engine: &mut jEngine) {
120        // Register the bitmap font once for ui_text.
121        if !self.font_loaded {
122            if let Ok(font) = Font::from_mtsdf_json(DEFAULT_FONT_METADATA) {
123                engine.ui.text.set_font(font);
124            }
125            self.font_loaded = true;
126        }
127
128        engine.clear();
129
130        // ── World tiles ───────────────────────────────────────────────────────
131        // Only draw tiles that fall within the viewable grid.  The engine tile
132        // grid is sized to the window; extra world tiles outside the window are
133        // clipped by the camera transform.
134        let gw = engine.grid_width().min(WORLD_W);
135        let gh = engine.grid_height().min(WORLD_H);
136
137        for y in 0..gh {
138            for x in 0..gw {
139                // Create a colourful but regular pattern so panning is obvious.
140                let color = world_color(x, y);
141                engine.set_background(x, y, color);
142
143                // Label every fifth tile with its grid coordinate.
144                if x % 5 == 0 && y % 5 == 0 {
145                    engine.set_foreground(x, y, '+', Color([0.3, 0.3, 0.35, 1.0]));
146                }
147            }
148        }
149
150        // Centre-of-world marker.
151        let mx = WORLD_W / 2;
152        let my = WORLD_H / 2;
153        if mx < gw && my < gh {
154            engine.set_background(mx, my, Color::WHITE);
155            engine.set_foreground(mx, my, 'X', Color::BLACK);
156        }
157
158        // ── HUD (screen-space — unaffected by camera) ─────────────────────────
159        let tw = engine.tile_width() as f32;
160        let th = engine.tile_height() as f32;
161        let sw = engine.grid_width() as f32 * tw;
162        let zoom_now = engine.camera_zoom();
163
164        // Top bar.
165        Panel::new(0.0, 0.0, sw, 30.0).with_color(Color([0.0, 0.0, 0.0, 0.85])).draw(engine);
166        engine.ui.ui_text(
167            20.0,
168            8.0,
169            &format!(
170                "Camera: ({:.0}, {:.0})  Zoom: {:.2}  Target zoom: {:.2}",
171                self.cam_x, self.cam_y, zoom_now, self.zoom_target
172            ),
173            Color::WHITE,
174            Color::TRANSPARENT, Some(14.0));
175
176        // Bottom hint bar.
177        let sh = engine.grid_height() as f32 * th;
178        Panel::new(0.0, sh - 30.0, sw, 30.0).with_color(Color([0.0, 0.0, 0.0, 0.85])).draw(engine);
179        engine.ui.ui_text(
180            20.0,
181            sh - 22.0,
182            "[Arrows] pan   [=/-] zoom   [Space] shake   [R] reset   [Esc] quit",
183            Color([0.6, 0.7, 0.65, 1.0]),
184            Color::TRANSPARENT, Some(14.0));
185    }
186}
187
188/// Produce a visually varied colour for world tile `(x, y)`.
189///
190/// Uses a simple diagonal stripe pattern with colour bands so that camera
191/// movement creates an obvious parallax-free scrolling effect.
192fn world_color(x: u32, y: u32) -> Color {
193    let band = (x + y) / 4 % 6;
194    match band {
195        0 => Color([0.10, 0.12, 0.18, 1.0]),
196        1 => Color([0.12, 0.18, 0.12, 1.0]),
197        2 => Color([0.18, 0.12, 0.10, 1.0]),
198        3 => Color([0.14, 0.14, 0.20, 1.0]),
199        4 => Color([0.10, 0.18, 0.18, 1.0]),
200        _ => Color([0.16, 0.16, 0.12, 1.0]),
201    }
202}
203
204fn main() {
205    jEngine::builder()
206        .with_title("jengine — Camera")
207        .with_size(800, 576)
208        .with_tileset(DEFAULT_TILESET, DEFAULT_TILE_W, DEFAULT_TILE_H)
209        .run(CameraDemo::new());
210}