dinamika_cpu/paint/mod.rs
1//! Brushes: [`Paint`], shaders ([`Shader`]) with gradients and blend modes
2//! ([`BlendMode`]).
3//!
4//! The submodules split the responsibilities:
5//! - [`gradient`] — linear/radial/conic gradients and stop sampling;
6//! - [`pattern`] — a texture/[`Pattern`] shader from an image;
7//! - [`blend`] — blend modes and Porter–Duff / W3C compositing.
8//!
9//! # Known limitation: colors are computed in sRGB, not in linear space
10//!
11//! Both blending and gradient stop interpolation operate directly on the sRGB
12//! (gamma-encoded) components, without converting to linear light and back.
13//! This is a deliberate trade-off — it is what most 8-bit engines do by
14//! default — but it has visible consequences: gradients look slightly "dirtier"
15//! in the middle, and semi-transparent edges darken a little. A mathematically
16//! correct result would require decoding sRGB into linear space before the
17//! arithmetic and encoding it back afterward.
18
19use crate::color::Color;
20use crate::geometry::Point;
21
22mod blend;
23mod gradient;
24mod pattern;
25
26pub use blend::BlendMode;
27pub use gradient::{ConicGradient, GradientStop, LinearGradient, RadialGradient, SpreadMode};
28pub use pattern::{FilterQuality, Pattern};
29
30pub(crate) use blend::blend;
31
32/// A color source.
33#[derive(Clone, Debug)]
34pub enum Shader {
35 /// Solid color.
36 SolidColor(Color),
37 Linear(LinearGradient),
38 Radial(RadialGradient),
39 /// A conic (sweep) gradient by angle.
40 Conic(ConicGradient),
41 /// A texture/pattern from an image.
42 Pattern(Pattern),
43}
44
45impl Shader {
46 /// The source color at point `(x, y)` (the pixel center).
47 pub fn color_at(&self, x: f32, y: f32) -> Color {
48 match self {
49 Shader::SolidColor(c) => *c,
50 Shader::Linear(g) => g.color_at(Point::new(x, y)),
51 Shader::Radial(g) => g.color_at(Point::new(x, y)),
52 Shader::Conic(g) => g.color_at(Point::new(x, y)),
53 Shader::Pattern(p) => p.color_at(Point::new(x, y)),
54 }
55 }
56
57 /// Shades a horizontal run of `len` pixels — pixel centers
58 /// `(x + 0.5, y + 0.5)`, `(x + 1.5, y + 0.5)`, … — appending one [`Color`]
59 /// per pixel to `out`.
60 ///
61 /// This is the batched counterpart of [`Shader::color_at`]. Because every
62 /// shader's coordinate mapping is affine, the run is evaluated by mapping
63 /// the first pixel once and then advancing by a constant per-pixel delta,
64 /// instead of recomputing `inv_transform.map_point` for every pixel. `out`
65 /// is appended to (not cleared), so reuse a buffer across rows and clear it
66 /// yourself between runs.
67 pub(crate) fn shade_span(&self, x: usize, y: usize, len: usize, out: &mut Vec<Color>) {
68 match self {
69 Shader::SolidColor(c) => out.extend(std::iter::repeat_n(*c, len)),
70 Shader::Linear(g) => g.shade_span(x, y, len, out),
71 Shader::Radial(g) => g.shade_span(x, y, len, out),
72 Shader::Conic(g) => g.shade_span(x, y, len, out),
73 Shader::Pattern(p) => p.shade_span(x, y, len, out),
74 }
75 }
76}
77
78/// A fill description: the color source, blend mode and anti-aliasing.
79#[derive(Clone, Debug)]
80pub struct Paint {
81 pub shader: Shader,
82 pub blend_mode: BlendMode,
83 pub anti_alias: bool,
84 /// Overall transparency multiplier `0..=1`.
85 pub opacity: f32,
86}
87
88impl Default for Paint {
89 fn default() -> Self {
90 Paint {
91 shader: Shader::SolidColor(Color::BLACK),
92 blend_mode: BlendMode::SourceOver,
93 anti_alias: true,
94 opacity: 1.0,
95 }
96 }
97}
98
99impl Paint {
100 /// A brush with a solid color.
101 pub fn from_color(color: Color) -> Self {
102 Paint { shader: Shader::SolidColor(color), ..Paint::default() }
103 }
104
105 /// Sets a solid color.
106 pub fn set_color(&mut self, color: Color) -> &mut Self {
107 self.shader = Shader::SolidColor(color);
108 self
109 }
110
111 /// Sets the shader.
112 pub fn set_shader(&mut self, shader: Shader) -> &mut Self {
113 self.shader = shader;
114 self
115 }
116}