valo_dl/shader.rs
1use valo_geometry::{Color, Matrix, Point};
2
3/// `Shader` determines the color painted at each point of a drawing operation.
4///
5/// Shader coordinates begin in the draw's local coordinate space, so shaders
6/// follow the same transforms as their geometry.
7// Serialize ONLY, now that a pattern can hold an `Image`: the dump records a
8// texture's identity, and no deserializer can turn that back into a live GPU
9// handle. Same call `Path` already makes.
10#[derive(Clone, Debug, PartialEq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize))]
12pub enum Shader {
13 /// `Linear` interpolates colors along the line from `start` to `end`.
14 Linear {
15 /// `start` is the zero-offset point in local coordinates.
16 start: Point,
17 /// `end` is the one-offset point in local coordinates.
18 end: Point,
19 /// `stops` defines the colors along the gradient.
20 stops: Vec<GradientStop>,
21 /// `spread` controls colors outside the zero-to-one span.
22 spread: SpreadMode,
23 /// `local` transforms the shader independently of the drawn geometry.
24 ///
25 /// Use [`Matrix::IDENTITY`] when no additional transform is needed.
26 local: Matrix,
27 },
28 /// `Radial` interpolates between a start circle and an end circle.
29 Radial {
30 /// `center` is the end circle's center in local coordinates.
31 center: Point,
32 /// `radius` is the end circle's radius in local coordinates.
33 radius: f32,
34 /// `stops` defines the colors along the gradient.
35 stops: Vec<GradientStop>,
36 /// `spread` controls colors outside the zero-to-one span.
37 spread: SpreadMode,
38 /// `focus` is the optional start circle.
39 ///
40 /// `None` starts at a zero-radius circle centered on `center`.
41 focus: Option<FocalCircle>,
42 /// `local` transforms the shader independently of the drawn geometry.
43 local: Matrix,
44 },
45 /// `Sweep` interpolates colors around one full clockwise turn.
46 Sweep {
47 /// `center` is the sweep origin in local coordinates.
48 center: Point,
49 /// `start_angle` is the zero-offset angle in radians clockwise from +x.
50 start_angle: f32,
51 /// `stops` defines the colors around the sweep.
52 stops: Vec<GradientStop>,
53 /// `local` transforms the shader independently of the drawn geometry.
54 local: Matrix,
55 },
56 /// `Image` samples an image across the drawn geometry.
57 Image {
58 /// `image` supplies the sampled pixels.
59 image: crate::Image,
60 /// `sampling` controls filtering, mipmaps, and tiling.
61 sampling: crate::Sampling,
62 /// `local` transforms the image pattern independently of the geometry.
63 local: Matrix,
64 },
65}
66
67/// `SpreadMode` controls a gradient outside its zero-to-one span.
68#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub enum SpreadMode {
71 /// `Pad` extends the nearest edge color.
72 #[default]
73 Pad,
74 /// `Repeat` repeats the gradient in the same direction.
75 Repeat,
76 /// `Reflect` repeats the gradient with alternating direction.
77 Reflect,
78}
79
80/// `GradientStop` assigns a color to one position along a gradient.
81#[derive(Clone, Copy, Debug, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83pub struct GradientStop {
84 /// `offset` is the position from zero to one.
85 ///
86 /// Supply stops in nondecreasing offset order.
87 pub offset: f32,
88 /// `color` is the straight-alpha sRGB color at this offset.
89 pub color: Color,
90}
91
92/// `FocalCircle` defines the start circle of a radial gradient.
93#[derive(Clone, Copy, Debug, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct FocalCircle {
96 /// `center` is the start circle's center in local coordinates.
97 pub center: Point,
98 /// `radius` is the start circle's radius.
99 ///
100 /// Zero represents a focal point.
101 pub radius: f32,
102}
103
104impl FocalCircle {
105 /// `point` creates a zero-radius focal circle.
106 pub fn point(center: Point) -> Self {
107 Self {
108 center,
109 radius: 0.0,
110 }
111 }
112}
113
114/// `MAX_GRADIENT_STOPS` is the largest gradient stored directly in uniforms.
115///
116/// Gradients with more stops use a cached texture ramp.
117pub const MAX_GRADIENT_STOPS: usize = 8;
118
119impl Shader {
120 /// `stops` returns this gradient's stops or an empty slice for image shaders.
121 pub fn stops(&self) -> &[GradientStop] {
122 match self {
123 Shader::Linear { stops, .. }
124 | Shader::Radial { stops, .. }
125 | Shader::Sweep { stops, .. } => stops,
126 Shader::Image { .. } => &[],
127 }
128 }
129
130 /// `fold_color_filter` applies a color filter directly to gradient stops.
131 ///
132 /// It returns `false` for image shaders, whose colors must be filtered
133 /// during sampling.
134 pub fn fold_color_filter(&mut self, filter: &crate::ColorFilter) -> bool {
135 let stops = match self {
136 Shader::Linear { stops, .. }
137 | Shader::Radial { stops, .. }
138 | Shader::Sweep { stops, .. } => stops,
139 // A pattern's colours live in texels; the image fragment applies
140 // the filter as it samples.
141 Shader::Image { .. } => return false,
142 };
143 let mut folded = Vec::with_capacity(stops.len());
144 for stop in stops.iter() {
145 match filter.folded_into(stop.color) {
146 Some(color) => folded.push(GradientStop { color, ..*stop }),
147 None => return false,
148 }
149 }
150 *stops = folded;
151 true
152 }
153
154 /// `linear` creates a two-color padded linear gradient.
155 pub fn linear(start: Point, end: Point, from: Color, to: Color) -> Self {
156 Shader::Linear {
157 start,
158 end,
159 stops: vec![
160 GradientStop {
161 offset: 0.0,
162 color: from,
163 },
164 GradientStop {
165 offset: 1.0,
166 color: to,
167 },
168 ],
169 spread: SpreadMode::Pad,
170 local: Matrix::IDENTITY,
171 }
172 }
173}