Skip to main content

macroquad_ply/
shapes.rs

1//! 2D shapes rendering.
2
3use crate::{color::Color, get_context};
4
5use crate::quad_gl::{DrawMode, Vertex};
6use glam::{vec2, vec3, vec4, Mat4, Vec2};
7
8/// Draws a solid triangle between points `v1`, `v2`, and `v3` with a given `color`.
9pub fn draw_triangle(v1: Vec2, v2: Vec2, v3: Vec2, color: Color) {
10    let context = get_context();
11
12    let vertices = [
13        Vertex::new(v1.x, v1.y, 0., 0., 0., color),
14        Vertex::new(v2.x, v2.y, 0., 0., 0., color),
15        Vertex::new(v3.x, v3.y, 0., 0., 0., color),
16    ];
17
18    let indices: [u16; 3] = [0, 1, 2];
19
20    context.gl.texture(None);
21    context.gl.draw_mode(DrawMode::Triangles);
22    context.gl.geometry(&vertices, &indices);
23}
24
25/// Draws a triangle outline between points `v1`, `v2`, and `v3` with a given line `thickness` and `color`.
26pub fn draw_triangle_lines(v1: Vec2, v2: Vec2, v3: Vec2, thickness: f32, color: Color) {
27    draw_line(v1.x, v1.y, v2.x, v2.y, thickness, color);
28    draw_line(v2.x, v2.y, v3.x, v3.y, thickness, color);
29    draw_line(v3.x, v3.y, v1.x, v1.y, thickness, color);
30}
31
32/// Draws a solid rectangle with its top-left corner at `[x, y]` with size `[w, h]` (width going to
33/// the right, height going down), with a given `color`.
34pub fn draw_rectangle(x: f32, y: f32, w: f32, h: f32, color: Color) {
35    let context = get_context();
36
37    #[rustfmt::skip]
38    let vertices = [
39        Vertex::new(x    , y    , 0., 0.0, 0.0, color),
40        Vertex::new(x + w, y    , 0., 1.0, 0.0, color),
41        Vertex::new(x + w, y + h, 0., 1.0, 1.0, color),
42        Vertex::new(x    , y + h, 0., 0.0, 1.0, color),
43    ];
44    let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
45
46    context.gl.texture(None);
47    context.gl.draw_mode(DrawMode::Triangles);
48    context.gl.geometry(&vertices, &indices);
49}
50
51/// Draws a rectangle outline with its top-left corner at `[x, y]` with size `[w, h]` (width going to
52/// the right, height going down), with a given line `thickness` and `color`.
53pub fn draw_rectangle_lines(x: f32, y: f32, w: f32, h: f32, thickness: f32, color: Color) {
54    let context = get_context();
55    let t = thickness / 2.;
56
57    #[rustfmt::skip]
58    let vertices = [
59        Vertex::new(x    , y    , 0., 0.0, 1.0, color),
60        Vertex::new(x + w, y    , 0., 1.0, 0.0, color),
61        Vertex::new(x + w, y + h, 0., 1.0, 1.0, color),
62        Vertex::new(x    , y + h, 0., 0.0, 0.0, color),
63        //inner rectangle
64        Vertex::new(x + t    , y + t    , 0., 0.0, 0.0, color),
65        Vertex::new(x + w - t, y + t    , 0., 0.0, 0.0, color),
66        Vertex::new(x + w - t, y + h - t, 0., 0.0, 0.0, color),
67        Vertex::new(x + t    , y + h - t, 0., 0.0, 0.0, color),
68    ];
69    let indices: [u16; 24] = [
70        0, 1, 4, 1, 4, 5, 1, 5, 6, 1, 2, 6, 3, 7, 2, 2, 7, 6, 0, 4, 3, 3, 4, 7,
71    ];
72
73    context.gl.texture(None);
74    context.gl.draw_mode(DrawMode::Triangles);
75    context.gl.geometry(&vertices, &indices);
76}
77
78pub fn draw_rectangle_lines_ex(
79    x: f32,
80    y: f32,
81    w: f32,
82    h: f32,
83    thickness: f32,
84    params: DrawRectangleParams,
85) {
86    let context = get_context();
87    let tx = thickness / w;
88    let ty = thickness / h;
89
90    let transform_matrix = Mat4::from_translation(vec3(x, y, 0.0))
91        * Mat4::from_axis_angle(vec3(0.0, 0.0, 1.0), params.rotation)
92        * Mat4::from_scale(vec3(w, h, 1.0));
93
94    #[rustfmt::skip]
95    let v = [
96        transform_matrix * vec4( 0.0 - params.offset.x,  0.0 - params.offset.y, 0.0, 1.0),
97        transform_matrix * vec4( 0.0 - params.offset.x,  1.0 - params.offset.y, 0.0, 1.0),
98        transform_matrix * vec4( 1.0 - params.offset.x,  1.0 - params.offset.y, 0.0, 1.0),
99        transform_matrix * vec4( 1.0 - params.offset.x,  0.0 - params.offset.y, 0.0, 1.0),
100
101        transform_matrix * vec4( 0.0 - params.offset.x + tx,  0.0 - params.offset.y + ty, 0.0, 1.0),
102        transform_matrix * vec4( 0.0 - params.offset.x + tx,  1.0 - params.offset.y - ty, 0.0, 1.0),
103        transform_matrix * vec4( 1.0 - params.offset.x - tx,  1.0 - params.offset.y - ty, 0.0, 1.0),
104        transform_matrix * vec4( 1.0 - params.offset.x - tx,  0.0 - params.offset.y + ty, 0.0, 1.0),
105    ];
106
107    // TODO: fix UVs
108    #[rustfmt::skip]
109    let vertices = [
110        Vertex::new(v[0].x, v[0].y, v[0].z, 0.0, 1.0, params.color),
111        Vertex::new(v[1].x, v[1].y, v[1].z, 1.0, 0.0, params.color),
112        Vertex::new(v[2].x, v[2].y, v[2].z, 1.0, 1.0, params.color),
113        Vertex::new(v[3].x, v[3].y, v[3].z, 1.0, 0.0, params.color),
114
115        Vertex::new(v[4].x, v[4].y, v[4].z, 0.0, 0.0, params.color),
116        Vertex::new(v[5].x, v[5].y, v[5].z, 0.0, 0.0, params.color),
117        Vertex::new(v[6].x, v[6].y, v[6].z, 0.0, 0.0, params.color),
118        Vertex::new(v[7].x, v[7].y, v[7].z, 0.0, 0.0, params.color),
119    ];
120    #[rustfmt::skip]
121    let indices: [u16; 24] = [
122        0, 4, 3,
123        4, 3, 7,
124        4, 0, 1,
125        4, 5, 1,
126        1, 5, 6,
127        1, 6, 2,
128        2, 3, 6,
129        3, 6, 7,
130    ];
131
132    context.gl.texture(None);
133    context.gl.draw_mode(DrawMode::Triangles);
134    context.gl.geometry(&vertices, &indices);
135}
136
137#[derive(Debug, Clone)]
138pub struct DrawRectangleParams {
139    /// Adds an offset to the position
140    /// E.g. offset (0,0) positions the rectangle at the top left corner of the screen, while offset
141    /// (0.5, 0.5) centers it
142    pub offset: Vec2,
143
144    /// Rotation in radians
145    pub rotation: f32,
146
147    pub color: Color,
148}
149
150impl Default for DrawRectangleParams {
151    fn default() -> Self {
152        Self {
153            offset: vec2(0.0, 0.0),
154            rotation: 0.0,
155            color: Color::from_rgba(255, 255, 255, 255),
156        }
157    }
158}
159
160/// Draws a solid rectangle with its position at `[x, y]` with size `[w, h]`,
161/// with parameters.
162pub fn draw_rectangle_ex(x: f32, y: f32, w: f32, h: f32, params: DrawRectangleParams) {
163    let context = get_context();
164    let transform_matrix = Mat4::from_translation(vec3(x, y, 0.0))
165        * Mat4::from_axis_angle(vec3(0.0, 0.0, 1.0), params.rotation)
166        * Mat4::from_scale(vec3(w, h, 1.0));
167
168    #[rustfmt::skip]
169    let v = [
170        transform_matrix * vec4( 0.0 - params.offset.x,  0.0 - params.offset.y, 0.0, 1.0),
171        transform_matrix * vec4( 0.0 - params.offset.x,  1.0 - params.offset.y, 0.0, 1.0),
172        transform_matrix * vec4( 1.0 - params.offset.x,  1.0 - params.offset.y, 0.0, 1.0),
173        transform_matrix * vec4( 1.0 - params.offset.x,  0.0 - params.offset.y, 0.0, 1.0),
174    ];
175
176    #[rustfmt::skip]
177    let vertices = [
178        Vertex::new(v[0].x, v[0].y, v[0].z, 0.0, 0.0, params.color),
179        Vertex::new(v[1].x, v[1].y, v[1].z, 1.0, 0.0, params.color),
180        Vertex::new(v[2].x, v[2].y, v[2].z, 1.0, 1.0, params.color),
181        Vertex::new(v[3].x, v[3].y, v[3].z, 0.0, 1.0, params.color),
182    ];
183    let indices: [u16; 6] = [0, 1, 2, 0, 2, 3];
184
185    context.gl.texture(None);
186    context.gl.draw_mode(DrawMode::Triangles);
187    context.gl.geometry(&vertices, &indices);
188}
189
190/// Draws an outlined solid hexagon centered at `[x, y]` with a radius `size`, outline thickness
191/// defined by `border`, orientation defined by `vertical` (when `true`, the hexagon points along
192/// the `y` axis), and colors for outline given by `border_color` and fill by `fill_color`.
193pub fn draw_hexagon(
194    x: f32,
195    y: f32,
196    size: f32,
197    border: f32,
198    vertical: bool,
199    border_color: Color,
200    fill_color: Color,
201) {
202    let rotation = if vertical { 90. } else { 0. };
203    draw_poly(x, y, 6, size, rotation, fill_color);
204    if border > 0. {
205        draw_poly_lines(x, y, 6, size, rotation, border, border_color);
206    }
207}
208
209/// Draws a solid regular polygon centered at `[x, y]` with a given number of `sides`, `radius`,
210/// clockwise `rotation` (in degrees) and `color`.
211pub fn draw_poly(x: f32, y: f32, sides: u8, radius: f32, rotation: f32, color: Color) {
212    let context = get_context();
213
214    let mut vertices = Vec::<Vertex>::with_capacity(sides as usize + 2);
215    let mut indices = Vec::<u16>::with_capacity(sides as usize * 3);
216
217    let rot = rotation.to_radians();
218    vertices.push(Vertex::new(x, y, 0., 0., 0., color));
219    for i in 0..=sides {
220        let rx = (i as f32 / sides as f32 * std::f32::consts::PI * 2. + rot).cos();
221        let ry = (i as f32 / sides as f32 * std::f32::consts::PI * 2. + rot).sin();
222
223        let vertex = Vertex::new(x + radius * rx, y + radius * ry, 0., rx, ry, color);
224
225        vertices.push(vertex);
226
227        if i != sides {
228            indices.extend_from_slice(&[0, i as u16 + 1, i as u16 + 2]);
229        }
230    }
231
232    context.gl.texture(None);
233    context.gl.draw_mode(DrawMode::Triangles);
234    context.gl.geometry(&vertices, &indices);
235}
236
237/// Draws a regular polygon outline centered at `[x, y]` with a given number of `sides`, `radius`,
238/// clockwise `rotation` (in degrees), line `thickness`, and `color`.
239pub fn draw_poly_lines(
240    x: f32,
241    y: f32,
242    sides: u8,
243    radius: f32,
244    rotation: f32,
245    thickness: f32,
246    color: Color,
247) {
248    draw_arc(x, y, sides, radius, rotation, thickness, 360.0, color);
249}
250
251/// Draws a solid circle centered at `[x, y]` with a given radius `r` and `color`.
252///
253/// This is not a perfect circle, but only a polygon approximation.
254/// If this is an issue for you, consider using `draw_poly(x, y, 255, r, 0., color)` instead.
255pub fn draw_circle(x: f32, y: f32, r: f32, color: Color) {
256    draw_poly(x, y, 20, r, 0., color);
257}
258
259/// Draws a circle outline centered at `[x, y]` with a given radius, line `thickness` and `color`.
260///
261/// This is not a perfect circle, but only a polygon approximation.
262/// If this is an issue for you, consider using `draw_poly_lines(x, y, 255, r, 0., thickness, color)` instead.
263pub fn draw_circle_lines(x: f32, y: f32, r: f32, thickness: f32, color: Color) {
264    draw_poly_lines(x, y, 30, r, 0., thickness, color);
265}
266
267/// Draws a solid ellipse centered at `[x, y]` with a given size `[w, h]`,
268/// clockwise `rotation` (in degrees) and `color`.
269pub fn draw_ellipse(x: f32, y: f32, w: f32, h: f32, rotation: f32, color: Color) {
270    let sides = 20;
271    let context = get_context();
272
273    let mut vertices = Vec::<Vertex>::with_capacity(sides as usize + 2);
274    let mut indices = Vec::<u16>::with_capacity(sides as usize * 3);
275
276    let rot = rotation.to_radians();
277    let sr = rot.sin();
278    let cr = rot.cos();
279    vertices.push(Vertex::new(x, y, 0., 0., 0., color));
280    for i in 0..=sides {
281        let rx = (i as f32 / sides as f32 * std::f32::consts::PI * 2.).cos();
282        let ry = (i as f32 / sides as f32 * std::f32::consts::PI * 2.).sin();
283
284        let px = w * rx;
285        let py = h * ry;
286        let rotated_x = px * cr - py * sr;
287        let rotated_y = py * cr + px * sr;
288        let vertex = Vertex::new(x + rotated_x, y + rotated_y, 0., rx, ry, color);
289
290        vertices.push(vertex);
291
292        if i != sides {
293            indices.extend_from_slice(&[0, i as u16 + 1, i as u16 + 2]);
294        }
295    }
296
297    context.gl.texture(None);
298    context.gl.draw_mode(DrawMode::Triangles);
299    context.gl.geometry(&vertices, &indices);
300}
301
302/// Draws an ellipse outline centered at `[x, y]` with a given size `[w, h]`,
303/// clockwise `rotation` (in degrees), line `thickness` and `color`.
304pub fn draw_ellipse_lines(
305    x: f32,
306    y: f32,
307    w: f32,
308    h: f32,
309    rotation: f32,
310    thickness: f32,
311    color: Color,
312) {
313    let sides = 20;
314
315    let rot = rotation.to_radians();
316    let sr = rot.sin();
317    let cr = rot.cos();
318    for i in 0..sides {
319        let rx = (i as f32 / sides as f32 * std::f32::consts::PI * 2.).cos();
320        let ry = (i as f32 / sides as f32 * std::f32::consts::PI * 2.).sin();
321        let px = w * rx;
322        let py = h * ry;
323        let rotated_x = px * cr - py * sr;
324        let rotated_y = py * cr + px * sr;
325
326        let p0 = vec2(x + rotated_x, y + rotated_y);
327
328        let rx = ((i + 1) as f32 / sides as f32 * std::f32::consts::PI * 2.).cos();
329        let ry = ((i + 1) as f32 / sides as f32 * std::f32::consts::PI * 2.).sin();
330        let px = w * rx;
331        let py = h * ry;
332        let rotated_x = px * cr - py * sr;
333        let rotated_y = py * cr + px * sr;
334
335        let p1 = vec2(x + rotated_x, y + rotated_y);
336
337        draw_line(p0.x, p0.y, p1.x, p1.y, thickness, color);
338    }
339}
340
341/// Draws a line between points `[x1, y1]` and `[x2, y2]` with a given `thickness` and `color`.
342pub fn draw_line(x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: Color) {
343    let context = get_context();
344    let dx = x2 - x1;
345    let dy = y2 - y1;
346
347    // https://stackoverflow.com/questions/1243614/how-do-i-calculate-the-normal-vector-of-a-line-segment
348
349    let nx = -dy;
350    let ny = dx;
351
352    let tlen = (nx * nx + ny * ny).sqrt() / (thickness * 0.5);
353    if tlen < f32::EPSILON {
354        return;
355    }
356    let tx = nx / tlen;
357    let ty = ny / tlen;
358
359    context.gl.texture(None);
360    context.gl.draw_mode(DrawMode::Triangles);
361    context.gl.geometry(
362        &[
363            Vertex::new(x1 + tx, y1 + ty, 0., 0., 0., color),
364            Vertex::new(x1 - tx, y1 - ty, 0., 0., 0., color),
365            Vertex::new(x2 + tx, y2 + ty, 0., 0., 0., color),
366            Vertex::new(x2 - tx, y2 - ty, 0., 0., 0., color),
367        ],
368        &[0, 1, 2, 2, 1, 3],
369    );
370}
371
372/// Draw arc from `rotation`(in degrees) to `arc + rotation` (`arc` in degrees),
373/// centered at `[x, y]` with a given number of `sides`, `radius`, line `thickness`, and `color`.
374pub fn draw_arc(
375    x: f32,
376    y: f32,
377    sides: u8,
378    radius: f32,
379    rotation: f32,
380    thickness: f32,
381    arc: f32,
382    color: Color,
383) {
384    let rot = rotation.to_radians();
385    let part = arc.to_radians();
386
387    let sides = (sides as f32 * part / std::f32::consts::TAU)
388        .ceil()
389        .max(1.0);
390    let span = part / sides;
391    let sides = sides as usize;
392
393    let context = get_context();
394    context.gl.texture(None);
395    context.gl.draw_mode(DrawMode::Triangles);
396
397    let mut verticies = Vec::<Vertex>::with_capacity(sides * 2);
398    let mut indicies = Vec::<u16>::with_capacity(sides * 2);
399
400    for i in 0..sides {
401        let start_angle = i as f32 * span + rot;
402        let end_angle = start_angle + span;
403
404        indicies.extend([0, 1, 2, 2, 1, 3].map(|k| k + (verticies.len() as u16)));
405
406        for (angle, radius) in [
407            (start_angle, radius),
408            (start_angle, radius + thickness),
409            (end_angle, radius),
410            (end_angle, radius + thickness),
411        ] {
412            let point = Vec2::new(x, y) + radius * Vec2::from_angle(angle);
413            verticies.push(Vertex::new(point.x, point.y, 0., 0., 0., color));
414        }
415    }
416
417    context.gl.geometry(&verticies, &indicies);
418}