Skip to main content

compose_rust/
brush.rs

1//! Gradients and image fills: registered once, named by id wherever a colour can go.
2//!
3//! A brush does not fit where a colour fits. A `Paint` is two words on the wire and a
4//! gradient is a list of stops, so the brush travels the way a picture does: registered
5//! before anything draws it, kept by the Renderer, and named by id after that. That is
6//! the road [`crate::asset`] already built, and this is a second kind of traffic on it
7//! rather than a second road.
8//!
9//! The bytes are this module's own fixed layout and not a file format. No platform stores
10//! a list of stops as a document, so a format would be a parser to write, to keep, and to
11//! get wrong.
12
13use crate::asset::register_owned;
14use crate::schema::{AssetKind, Color, Paint, TileMode};
15
16/// A stop: where along the run it sits, and what colour it is there.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Stop {
19    /// Zero at the start of the run, one at the end.
20    pub at: f32,
21    pub color: Color,
22}
23
24impl Stop {
25    pub const fn new(at: f32, color: Color) -> Self {
26        Self { at, color }
27    }
28}
29
30/// What a surface can be filled with beyond a flat colour.
31///
32/// The coordinates are fractions of the area being filled, never pixels. A gradient
33/// described in pixels would be a gradient that only looks right at one size, and nothing
34/// on this side of the boundary knows how large anything ended up.
35#[derive(Clone, Debug, PartialEq)]
36pub enum Brush {
37    Linear {
38        start: (f32, f32),
39        end: (f32, f32),
40        stops: Vec<Stop>,
41        tile: TileMode,
42    },
43    Radial {
44        center: (f32, f32),
45        /// As a fraction of the larger side, so a circle stays a circle.
46        radius: f32,
47        stops: Vec<Stop>,
48        tile: TileMode,
49    },
50    /// A registered picture, laid out as a fill.
51    Image { asset: u32, tile: TileMode },
52}
53
54const KIND_LINEAR: u16 = 1;
55const KIND_RADIAL: u16 = 2;
56const KIND_IMAGE: u16 = 3;
57
58impl Brush {
59    /// Top to bottom, which is the gradient almost every screen wants.
60    pub fn vertical(stops: Vec<Stop>) -> Self {
61        Self::Linear {
62            start: (0.5, 0.0),
63            end: (0.5, 1.0),
64            stops,
65            tile: TileMode::Clamp,
66        }
67    }
68
69    /// Left to right.
70    pub fn horizontal(stops: Vec<Stop>) -> Self {
71        Self::Linear {
72            start: (0.0, 0.5),
73            end: (1.0, 0.5),
74            stops,
75            tile: TileMode::Clamp,
76        }
77    }
78
79    /// Out from the middle.
80    pub fn radial(stops: Vec<Stop>) -> Self {
81        Self::Radial {
82            center: (0.5, 0.5),
83            radius: 0.5,
84            stops,
85            tile: TileMode::Clamp,
86        }
87    }
88
89    /// The bytes the Renderer reads, in this module's own layout.
90    ///
91    /// Fixed fields throughout: a kind, a stop count, four coordinates, and then eight
92    /// bytes per stop. Nothing is skipped for a brush that does not use a coordinate,
93    /// because a reader that has to know the kind before it knows where the stops start
94    /// is a reader that can be walked off the end of a short buffer.
95    pub fn to_bytes(&self) -> Vec<u8> {
96        let (kind, coordinates, stops, tile) = match self {
97            Self::Linear {
98                start,
99                end,
100                stops,
101                tile,
102            } => (
103                KIND_LINEAR,
104                [start.0, start.1, end.0, end.1],
105                stops.as_slice(),
106                *tile,
107            ),
108            Self::Radial {
109                center,
110                radius,
111                stops,
112                tile,
113            } => (
114                KIND_RADIAL,
115                [center.0, center.1, *radius, 0.0],
116                stops.as_slice(),
117                *tile,
118            ),
119            // The picture's id rides in the first coordinate's bits. It is a u32 in a
120            // four-byte field, and giving the image case a layout of its own would mean
121            // two layouts to read instead of one.
122            Self::Image { asset, tile } => (
123                KIND_IMAGE,
124                [f32::from_bits(*asset), 0.0, 0.0, 0.0],
125                &[][..],
126                *tile,
127            ),
128        };
129        let mut bytes = Vec::with_capacity(HEADER_LEN + stops.len() * STOP_LEN);
130        bytes.extend_from_slice(&kind.to_le_bytes());
131        bytes.extend_from_slice(&(stops.len() as u16).to_le_bytes());
132        bytes.extend_from_slice(&(tile as u16).to_le_bytes());
133        bytes.extend_from_slice(&0u16.to_le_bytes());
134        for coordinate in coordinates {
135            bytes.extend_from_slice(&coordinate.to_bits().to_le_bytes());
136        }
137        for stop in stops {
138            bytes.extend_from_slice(&stop.at.to_bits().to_le_bytes());
139            bytes.extend_from_slice(&stop.color.0.to_le_bytes());
140        }
141        bytes
142    }
143}
144
145/// A kind, a stop count, a tile mode, two bytes of padding and four coordinates.
146pub const HEADER_LEN: usize = 24;
147/// A position and a colour.
148pub const STOP_LEN: usize = 8;
149
150/// Registers a brush and returns the `Paint` that names it.
151///
152/// Call it wherever the brush is used, including in a component body that runs every
153/// render: the second call with the same brush returns the first call's id and registers
154/// nothing, so a screen painted with the same gradient every frame registers it once and
155/// allocates nothing after that.
156///
157/// ```ignore
158/// let sky = brush(Brush::vertical(vec![
159///     Stop::new(0.0, Color::rgb(0x4a90d9)),
160///     Stop::new(1.0, Color::rgb(0xffffff)),
161/// ]));
162/// rsx! { Surface { background: sky, .. } }
163/// ```
164pub fn brush(brush: Brush) -> Paint {
165    Paint::Asset(register_owned(AssetKind::Brush, brush.to_bytes()))
166}