Skip to main content

game_toolkit_gfx/
painter.rs

1use crate::frame::Frame;
2use crate::graphics::Graphics;
3use crate::mesh::{MeshId, MeshInstance};
4use crate::primitives::CircleInstance;
5use crate::sprite::{BlendMode, SpriteInstance};
6use crate::texture::TextureId;
7use crate::transform::Mat4;
8
9pub struct Painter<'a> {
10    frame: &'a mut Frame,
11    gfx: &'a mut Graphics,
12}
13
14impl<'a> Painter<'a> {
15    pub(crate) fn new(frame: &'a mut Frame, gfx: &'a mut Graphics) -> Self {
16        Self { frame, gfx }
17    }
18
19    pub fn clear(&mut self, color: [f32; 4]) {
20        self.frame.clear_color = color;
21    }
22
23    /// World-space rectangle currently visible through the 2D camera, as `(min, max)`.
24    pub fn visible_rect(&self) -> ([f32; 2], [f32; 2]) {
25        self.gfx.camera.visible_rect()
26    }
27
28    pub fn sprite(&mut self, tex: TextureId, pos: [f32; 2], size: [f32; 2]) {
29        self.gfx
30            .sprites
31            .draw(tex, 0, BlendMode::Alpha, SpriteInstance::at(pos, size));
32    }
33
34    pub fn sprite_ex(
35        &mut self,
36        tex: TextureId,
37        inst: SpriteInstance,
38        layer: i16,
39        blend: BlendMode,
40    ) {
41        self.gfx.sprites.draw(tex, layer, blend, inst);
42    }
43
44    /// Filled rectangle (uses the internal 1×1 white texture).
45    pub fn rect(&mut self, pos: [f32; 2], size: [f32; 2], color: [f32; 4]) {
46        let tex = self.gfx.white_texture();
47        self.gfx.sprites.draw(
48            tex,
49            0,
50            BlendMode::Alpha,
51            SpriteInstance::at(pos, size).with_color(color),
52        );
53    }
54
55    /// 4 thin lines around the rectangle.
56    pub fn rect_outline(&mut self, pos: [f32; 2], size: [f32; 2], thickness: f32, color: [f32; 4]) {
57        let [x, y] = pos;
58        let [w, h] = size;
59        self.line([x, y], [x + w, y], thickness, color);
60        self.line([x + w, y], [x + w, y + h], thickness, color);
61        self.line([x + w, y + h], [x, y + h], thickness, color);
62        self.line([x, y + h], [x, y], thickness, color);
63    }
64
65    /// Line segment, drawn as a rotated thin quad through the sprite batcher.
66    pub fn line(&mut self, a: [f32; 2], b: [f32; 2], thickness: f32, color: [f32; 4]) {
67        let dx = b[0] - a[0];
68        let dy = b[1] - a[1];
69        let len = (dx * dx + dy * dy).sqrt();
70        if len < 1e-4 {
71            return;
72        }
73        let angle = dy.atan2(dx);
74        let half_t = thickness * 0.5;
75        // SpriteInstance positions its quad by top-left; we want the quad centered on the
76        // midpoint with width=len, height=thickness and rotation=angle. The shader rotates
77        // around the quad's own center, so set `pos` so center lands on the midpoint.
78        let cx = (a[0] + b[0]) * 0.5;
79        let cy = (a[1] + b[1]) * 0.5;
80        let tex = self.gfx.white_texture();
81        let inst = SpriteInstance {
82            pos: [cx - len * 0.5, cy - half_t],
83            size: [len, thickness],
84            uv_min: [0.0, 0.0],
85            uv_max: [1.0, 1.0],
86            color,
87            rotation: angle,
88            _pad: [0.0; 3],
89        };
90        self.gfx.sprites.draw(tex, 0, BlendMode::Alpha, inst);
91    }
92
93    /// Filled disk (layer 0).
94    pub fn circle(&mut self, center: [f32; 2], radius: f32, color: [f32; 4]) {
95        self.circle_ex(center, radius, 0.0, color, 0);
96    }
97
98    /// Ring (outline circle) with the given pixel thickness (layer 0).
99    pub fn circle_outline(
100        &mut self,
101        center: [f32; 2],
102        radius: f32,
103        thickness: f32,
104        color: [f32; 4],
105    ) {
106        self.circle_ex(center, radius, thickness.max(1.0), color, 0);
107    }
108
109    /// Circle on an explicit layer. `thickness == 0.0` is a filled disk; `> 0.0` is a ring of
110    /// that pixel thickness. Lower layers draw under higher ones across all batchers.
111    pub fn circle_ex(
112        &mut self,
113        center: [f32; 2],
114        radius: f32,
115        thickness: f32,
116        color: [f32; 4],
117        layer: i16,
118    ) {
119        self.gfx.primitives.push(
120            layer,
121            CircleInstance {
122                center,
123                radius,
124                thickness,
125                color,
126            },
127        );
128    }
129
130    /// Draw text. Text always renders on top of sprites and circles (it is not layered).
131    pub fn text(&mut self, pos: [f32; 2], s: &str, size_px: f32, color: [f32; 4]) {
132        self.gfx.text.queue(s, pos, size_px, color);
133    }
134
135    /// Draw an instance of a 3D `mesh` with the given column-major `model` transform and
136    /// tint. Meshes are depth-tested against [`crate::Graphics::camera3d`] and render under
137    /// all 2D content.
138    pub fn mesh(&mut self, mesh: MeshId, model: Mat4, color: [f32; 4]) {
139        self.gfx.draw_mesh(mesh, MeshInstance::new(model, color));
140    }
141
142    /// Draw vector graphics with vello (feature `vector`). The closure receives the frame's
143    /// [`vello::Scene`]; vector content composites on top of all other layers.
144    #[cfg(feature = "vector")]
145    pub fn vector<F: FnOnce(&mut vello::Scene)>(&mut self, f: F) {
146        f(self.gfx.vector.scene());
147    }
148}
149
150impl<'a> Drop for Painter<'a> {
151    fn drop(&mut self) {
152        self.gfx.flush_into(self.frame);
153    }
154}