pride_overlay/effects/
ring.rs

1#[cfg(not(target_arch = "wasm32"))]
2use crate::flags::Flag;
3#[cfg(target_arch = "wasm32")]
4use crate::flags::wasm::CustomFlag;
5use crate::{effects::overlay_flag, prelude::*};
6use core::f32::consts::PI;
7use image::{GenericImageView, Rgba, RgbaImage, imageops::overlay};
8use imageproc::{drawing::draw_antialiased_polygon_mut, pixelops::interpolate, point::Point};
9
10/// Effect that draws a ring around an image using pride [Flag] colors.
11#[derive(bon::Builder)]
12#[builder(
13    const,
14    builder_type(doc {
15        /// Builder for the [Ring] effect.
16    })
17)]
18pub struct Ring {
19    /// Opacity of the ring, from 0.0 (transparent) to 1.0 (opaque).
20    #[builder(default = Ring::DEFAULT_OPACITY, with = |percent: f32| percent.clamp(0., 1.))]
21    opacity: f32,
22    /// Thickness of the ring as a percentage of the image width, from 0.0 to 1.0.
23    ///
24    /// You probably want this to be fairly small!
25    #[builder(default = Ring::DEFAULT_THICKNESS, with = |percent: f32| percent.clamp(0., 1.))]
26    thickness: f32,
27}
28
29impl Ring {
30    /// Default opacity for the [Ring] effect.
31    pub const DEFAULT_OPACITY: f32 = 1.;
32
33    /// Default thickness for the [Ring] effect.
34    pub const DEFAULT_THICKNESS: f32 = 0.1;
35}
36
37impl Effect for Ring {
38    fn apply<F>(&self, image: &mut image::DynamicImage, flag: F)
39    where
40        F: FlagData,
41    {
42        if self.opacity == 0. {
43            // no-op for zero opacity
44        } else if self.thickness >= 0.99 {
45            // full thickness is just an overlay
46            let effect = Overlay::builder().opacity(self.opacity).build();
47            effect.apply(image, flag)
48        } else {
49            let (width, height) = image.dimensions();
50            #[cfg(not(target_arch = "wasm32"))]
51            let ring_flag = Flag {
52                name: flag.name(),
53                colours: flag.colours(),
54                ..Default::default()
55            };
56            #[cfg(target_arch = "wasm32")]
57            let ring_flag = CustomFlag {
58                colours: flag.colours().into(),
59                ..Default::default()
60            };
61            let mut ring_overlay = overlay_flag(ring_flag, width, height, self.opacity);
62
63            let center = ((width / 2) as i32, (height / 2) as i32);
64            let radius =
65                (width / 2).saturating_sub(((width / 2) as f32 * self.thickness) as u32) as i32;
66
67            draw_circle(&mut ring_overlay, center, radius as f32, Rgba([0, 0, 0, 0]));
68            overlay(image, &ring_overlay, 0, 0);
69        }
70    }
71}
72
73/// Draws a smooth circle on the image using anti-aliasing.
74fn draw_circle(image: &mut RgbaImage, center: (i32, i32), radius: f32, color: Rgba<u8>) {
75    const MIN_SIDES: f32 = 32.;
76    const MAX_SIDES: f32 = 256.;
77    const PIXELS_PER_SIDE: f32 = 4.;
78
79    // determine the number of sides to use for a the polygon
80    // that approximates the circle.
81    let circumference = 2.0 * PI * radius;
82    let sides = (circumference / PIXELS_PER_SIDE).clamp(MIN_SIDES, MAX_SIDES);
83
84    // compute the points of the polygon
85    let points: Vec<Point<i32>> = (0..(sides as usize))
86        .map(|i| {
87            let theta = 2.0 * PI * (i as f32) / sides;
88            let (x, y) = center;
89            let dx = radius * theta.cos();
90            let dy = radius * theta.sin();
91            Point::new(x + dx.round() as i32, y + dy.round() as i32)
92        })
93        .collect();
94
95    draw_antialiased_polygon_mut(image, &points, color, interpolate);
96}