Skip to main content

pax_runtime_api/
drawing.rs

1//! Vector drawing primitives: paths, fills, strokes, caps, and gradients.
2
3use super::*;
4
5pub mod path_smoothing;
6pub mod path_trim;
7pub mod stroke_utils;
8
9/// Describes a single element of a vector path, such as a line, a point, or curve segment.
10#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
11#[serde(crate = "crate::serde")]
12pub enum PathElement {
13    /// No-op path element.
14    #[default]
15    Empty,
16    /// Moves the current point to the provided coordinate.
17    Point(Size, Size),
18    /// Draws a straight line to the following `Point`.
19    Line,
20    /// Draws a quadratic Bézier segment with one control point, ending at the following `Point`.
21    Quadratic(Size, Size),
22    /// Draws a cubic Bézier segment with two control points, ending at the following `Point`.
23    Cubic(Size, Size, Size, Size),
24    /// Closes the current contour.
25    Close,
26}
27
28impl Interpolatable for PathElement {}
29impl HelperFunctions for PathElement {}
30
31/// Controls optional curve smoothing for path geometry before tessellation.
32///
33/// `PathSmoothing` is intended for authored or imported paths whose source data
34/// approximates curves with many short line segments, such as single-stroke SVG
35/// fonts. Existing paths keep their exact geometry by default.
36#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
37#[serde(crate = "crate::serde")]
38pub enum PathSmoothing {
39    /// Preserve the authored path exactly.
40    #[default]
41    None,
42    /// Lightly smooth polyline runs while preserving sharp corners.
43    Light,
44    /// More aggressively smooth polyline runs for pen-like paths.
45    Strong,
46}
47
48impl Interpolatable for PathSmoothing {
49    fn interpolate(&self, other: &Self, t: f64) -> Self {
50        if t < 1.0 {
51            *self
52        } else {
53            *other
54        }
55    }
56}
57
58/// Controls how an open stroke terminates at the exposed endpoints of a path.
59///
60/// `StrokeCap` affects primitives such as `Line` and open `Path` subpaths.
61/// Closed geometry ignores cap style because it has no exposed endpoints.
62#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
63#[serde(crate = "crate::serde")]
64pub enum StrokeCap {
65    /// Ends exactly at the path endpoint without extending past it.
66    #[default]
67    Butt,
68    /// Adds a semicircular cap whose radius is half the stroke width.
69    Round,
70    /// Adds a square cap that extends half the stroke width past the endpoint.
71    Square,
72}
73
74impl Interpolatable for StrokeCap {}
75
76/// Controls how stroke segments are joined at path vertices.
77#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
78#[serde(crate = "crate::serde")]
79pub enum StrokeJoin {
80    /// Extends outer edges to a point, subject to the renderer's miter limit.
81    #[default]
82    Miter,
83    /// Rounds the outside of each join.
84    Round,
85    /// Cuts joins off with a straight edge.
86    Bevel,
87}
88
89impl Interpolatable for StrokeJoin {}
90
91/// Describes the outline drawn around vector geometry.
92///
93/// Pax currently renders strokes centered on the underlying path. For open
94/// geometry, `cap` controls how the stroke terminates at the start and end of
95/// the path, while `join` controls how adjacent segments meet.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(crate = "crate::serde")]
98pub struct Stroke {
99    /// The stroke color, including alpha.
100    pub color: Property<Color>,
101    /// The stroke width.
102    ///
103    /// The type is [`Size`] for consistency with the wider property system, but
104    /// current vector renderers interpret this value in pixels.
105    pub width: Property<Size>,
106    /// The cap style used for exposed endpoints on open paths.
107    pub cap: Property<StrokeCap>,
108    /// The join style used where adjacent stroke segments meet.
109    pub join: Property<StrokeJoin>,
110}
111
112impl Default for Stroke {
113    fn default() -> Self {
114        Self {
115            color: Default::default(),
116            width: Property::new(Size::Pixels(Numeric::F64(0.0))),
117            cap: Property::new(StrokeCap::default()),
118            join: Property::new(StrokeJoin::default()),
119        }
120    }
121}
122
123impl PartialEq for Stroke {
124    fn eq(&self, other: &Self) -> bool {
125        self.color.get() == other.color.get()
126            && self.width.get() == other.width.get()
127            && self.cap.get() == other.cap.get()
128            && self.join.get() == other.join.get()
129    }
130}
131
132impl Interpolatable for Stroke {
133    fn interpolate(&self, other: &Self, t: f64) -> Self {
134        Self {
135            color: Property::new(self.color.get().interpolate(&other.color.get(), t)),
136            width: Property::new(self.width.get().interpolate(&other.width.get(), t)),
137            cap: Property::new(if t < 1.0 {
138                self.cap.get()
139            } else {
140                other.cap.get()
141            }),
142            join: Property::new(if t < 1.0 {
143                self.join.get()
144            } else {
145                other.join.get()
146            }),
147        }
148    }
149}
150
151/// Describes where to open new windows or tabs when navigating to a URL from a `Link` node.
152pub enum NavigationTarget {
153    /// Navigate in the current window or tab.
154    Current,
155    /// Navigate in a new window or tab.
156    New,
157}
158
159/// Describes how to fill vector geometry.
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
161#[serde(crate = "crate::serde")]
162pub enum Fill {
163    /// A single solid color.
164    Solid(Color),
165    /// A linear gradient.
166    LinearGradient(LinearGradient),
167    /// A radial gradient.
168    RadialGradient(RadialGradient),
169}
170
171impl Hash for Fill {
172    fn hash<H: Hasher>(&self, state: &mut H) {
173        match self {
174            Fill::Solid(color) => {
175                state.write_u8(0);
176                color.hash(state);
177            }
178            Fill::LinearGradient(linear) => {
179                state.write_u8(1);
180                linear.start.hash(state);
181                linear.end.hash(state);
182                linear.stops.hash(state);
183            }
184            Fill::RadialGradient(radial) => {
185                state.write_u8(2);
186                radial.start.hash(state);
187                radial.end.hash(state);
188                radial.radius.to_bits().hash(state);
189                radial.stops.hash(state);
190            }
191        }
192    }
193}
194
195impl Hash for Stroke {
196    fn hash<H: Hasher>(&self, state: &mut H) {
197        self.width.get().hash(state);
198        self.color.get().hash(state);
199        self.cap.get().hash(state);
200        self.join.get().hash(state);
201    }
202}
203
204/// Logical scene depth for lighting calculations.
205///
206/// `Depth` is expressed in logical pixels. Unlike [`Size`], it is not anchored
207/// to a viewport or parent box, so percent and combined units are intentionally
208/// rejected during value coercion.
209#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default, PartialEq, PartialOrd)]
210#[serde(crate = "crate::serde")]
211pub struct Depth(pub Numeric);
212
213impl Depth {
214    /// Returns the depth as a floating-point logical pixel value.
215    pub fn to_float(&self) -> f64 {
216        self.0.to_float()
217    }
218}
219
220impl From<Numeric> for Depth {
221    fn from(value: Numeric) -> Self {
222        Self(value)
223    }
224}
225
226impl From<f64> for Depth {
227    fn from(value: f64) -> Self {
228        Self(Numeric::F64(value))
229    }
230}
231
232impl Hash for Depth {
233    fn hash<H: Hasher>(&self, state: &mut H) {
234        self.0.hash(state);
235    }
236}
237
238impl Interpolatable for Depth {
239    fn interpolate(&self, other: &Self, t: f64) -> Self {
240        Self(self.0.interpolate(&other.0, t))
241    }
242}
243
244impl HelperFunctions for Depth {}
245
246/// A three-dimensional vector in logical scene space.
247#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
248#[serde(crate = "crate::serde")]
249pub struct Vector3 {
250    /// X component.
251    pub x: f64,
252    /// Y component.
253    pub y: f64,
254    /// Z component.
255    pub z: f64,
256}
257
258impl Vector3 {
259    /// Constructs a 3D vector.
260    pub fn new(x: f64, y: f64, z: f64) -> Self {
261        Self { x, y, z }
262    }
263}
264
265impl Default for Vector3 {
266    fn default() -> Self {
267        Self {
268            x: 0.0,
269            y: 0.0,
270            z: 0.0,
271        }
272    }
273}
274
275impl Hash for Vector3 {
276    fn hash<H: Hasher>(&self, state: &mut H) {
277        self.x.to_bits().hash(state);
278        self.y.to_bits().hash(state);
279        self.z.to_bits().hash(state);
280    }
281}
282
283impl Interpolatable for Vector3 {
284    fn interpolate(&self, other: &Self, t: f64) -> Self {
285        Self {
286            x: self.x.interpolate(&other.x, t),
287            y: self.y.interpolate(&other.y, t),
288            z: self.z.interpolate(&other.z, t),
289        }
290    }
291}
292
293impl HelperFunctions for Vector3 {
294    fn register_all_functions() {
295        register_function(
296            "Vector3".to_string(),
297            "new".to_string(),
298            std::sync::Arc::new(|args| {
299                if args.len() != 3 {
300                    return Err("Expected 3 arguments for function Vector3::new".to_string());
301                }
302                let mut itr = args.into_iter();
303                let x = f64::try_coerce(itr.next().unwrap())?;
304                let y = f64::try_coerce(itr.next().unwrap())?;
305                let z = f64::try_coerce(itr.next().unwrap())?;
306                Ok(Vector3::new(x, y, z).to_pax_value())
307            }),
308        );
309    }
310}
311
312/// Shape of a light contribution in logical scene space.
313#[derive(Debug, Copy, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
314#[serde(crate = "crate::serde")]
315pub enum LightShape {
316    /// A positional light with radius-based attenuation.
317    #[default]
318    Point,
319    /// A light with direction but no position or attenuation.
320    Directional,
321}
322
323impl Interpolatable for LightShape {}
324impl HelperFunctions for LightShape {}
325
326/// Tunable response parameters for a light-reactive vector material.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[serde(crate = "crate::serde")]
329pub struct MaterialParams {
330    /// Ambient contribution multiplier.
331    pub ambient: Property<f64>,
332    /// Diffuse contribution multiplier.
333    pub diffuse: Property<f64>,
334    /// Specular contribution multiplier.
335    pub specular: Property<f64>,
336    /// Surface roughness in the `[0.0, 1.0]` range.
337    pub roughness: Property<f64>,
338    /// Metallic response in the `[0.0, 1.0]` range.
339    pub metallic: Property<f64>,
340    /// Additive emissive color.
341    pub emissive: Property<Color>,
342    /// Additive emissive intensity.
343    pub emissive_intensity: Property<f64>,
344}
345
346impl Default for MaterialParams {
347    fn default() -> Self {
348        Self {
349            ambient: Property::new(1.0),
350            diffuse: Property::new(0.82),
351            specular: Property::new(0.08),
352            roughness: Property::new(0.78),
353            metallic: Property::new(0.0),
354            emissive: Property::new(Color::BLACK),
355            emissive_intensity: Property::new(0.0),
356        }
357    }
358}
359
360impl PartialEq for MaterialParams {
361    fn eq(&self, other: &Self) -> bool {
362        self.ambient.get() == other.ambient.get()
363            && self.diffuse.get() == other.diffuse.get()
364            && self.specular.get() == other.specular.get()
365            && self.roughness.get() == other.roughness.get()
366            && self.metallic.get() == other.metallic.get()
367            && self.emissive.get() == other.emissive.get()
368            && self.emissive_intensity.get() == other.emissive_intensity.get()
369    }
370}
371
372impl Hash for MaterialParams {
373    fn hash<H: Hasher>(&self, state: &mut H) {
374        self.ambient.get().to_bits().hash(state);
375        self.diffuse.get().to_bits().hash(state);
376        self.specular.get().to_bits().hash(state);
377        self.roughness.get().to_bits().hash(state);
378        self.metallic.get().to_bits().hash(state);
379        self.emissive.get().hash(state);
380        self.emissive_intensity.get().to_bits().hash(state);
381    }
382}
383
384impl Interpolatable for MaterialParams {
385    fn interpolate(&self, other: &Self, t: f64) -> Self {
386        Self {
387            ambient: Property::new(self.ambient.get().interpolate(&other.ambient.get(), t)),
388            diffuse: Property::new(self.diffuse.get().interpolate(&other.diffuse.get(), t)),
389            specular: Property::new(self.specular.get().interpolate(&other.specular.get(), t)),
390            roughness: Property::new(self.roughness.get().interpolate(&other.roughness.get(), t)),
391            metallic: Property::new(self.metallic.get().interpolate(&other.metallic.get(), t)),
392            emissive: Property::new(self.emissive.get().interpolate(&other.emissive.get(), t)),
393            emissive_intensity: Property::new(
394                self.emissive_intensity
395                    .get()
396                    .interpolate(&other.emissive_intensity.get(), t),
397            ),
398        }
399    }
400}
401
402impl HelperFunctions for MaterialParams {}
403
404/// Light-reactive surface response for vector primitives.
405///
406/// This is intentionally named `Material`; `texture` is reserved for future
407/// bitmap-backed texture maps and pattern data.
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409#[serde(crate = "crate::serde")]
410pub enum Material {
411    /// Responds to scene lighting using parameterized material coefficients.
412    Lit(MaterialParams),
413    /// Ignores scene lighting and preserves legacy unlit rendering behavior.
414    Unlit,
415}
416
417impl Default for Material {
418    fn default() -> Self {
419        Self::matte()
420    }
421}
422
423impl Material {
424    /// A soft, low-specular material suitable as the default lit response.
425    pub fn matte() -> Self {
426        Self::Lit(MaterialParams::default())
427    }
428
429    /// A higher-specular material with lower roughness.
430    pub fn glossy(specular: f64) -> Self {
431        let mut params = MaterialParams::default();
432        params.specular = Property::new(specular.clamp(0.0, 1.0));
433        params.roughness = Property::new(0.28);
434        Self::Lit(params)
435    }
436
437    /// A metallic material response.
438    pub fn metallic(metallic: f64) -> Self {
439        let mut params = MaterialParams::default();
440        params.metallic = Property::new(metallic.clamp(0.0, 1.0));
441        params.diffuse = Property::new(0.42);
442        params.specular = Property::new(0.55);
443        params.roughness = Property::new(0.36);
444        Self::Lit(params)
445    }
446
447    /// An emissive material that adds color independent of lights.
448    pub fn emissive(color: Color, intensity: f64) -> Self {
449        let mut params = MaterialParams::default();
450        params.emissive = Property::new(color);
451        params.emissive_intensity = Property::new(intensity.max(0.0));
452        Self::Lit(params)
453    }
454
455    /// Creates a lit material from explicit coefficients.
456    pub fn custom(params: MaterialParams) -> Self {
457        Self::Lit(params)
458    }
459
460    /// A material that ignores authored lights.
461    pub fn unlit() -> Self {
462        Self::Unlit
463    }
464}
465
466impl Hash for Material {
467    fn hash<H: Hasher>(&self, state: &mut H) {
468        match self {
469            Material::Lit(params) => {
470                state.write_u8(0);
471                params.hash(state);
472            }
473            Material::Unlit => {
474                state.write_u8(1);
475            }
476        }
477    }
478}
479
480impl Interpolatable for Material {
481    fn interpolate(&self, other: &Self, t: f64) -> Self {
482        match (self, other) {
483            (Self::Lit(lhs), Self::Lit(rhs)) => Self::Lit(lhs.interpolate(rhs, t)),
484            _ if t < 1.0 => self.clone(),
485            _ => other.clone(),
486        }
487    }
488}
489
490impl HelperFunctions for Material {
491    fn register_all_functions() {
492        register_function(
493            "Material".to_string(),
494            "matte".to_string(),
495            std::sync::Arc::new(|args| {
496                if !args.is_empty() {
497                    return Err("Expected 0 arguments for function Material::matte".to_string());
498                }
499                Ok(Material::matte().to_pax_value())
500            }),
501        );
502        register_function(
503            "Material".to_string(),
504            "glossy".to_string(),
505            std::sync::Arc::new(|args| {
506                if args.len() != 1 {
507                    return Err("Expected 1 argument for function Material::glossy".to_string());
508                }
509                let specular = f64::try_coerce(args.into_iter().next().unwrap())?;
510                Ok(Material::glossy(specular).to_pax_value())
511            }),
512        );
513        register_function(
514            "Material".to_string(),
515            "metallic".to_string(),
516            std::sync::Arc::new(|args| {
517                if args.len() != 1 {
518                    return Err("Expected 1 argument for function Material::metallic".to_string());
519                }
520                let metallic = f64::try_coerce(args.into_iter().next().unwrap())?;
521                Ok(Material::metallic(metallic).to_pax_value())
522            }),
523        );
524        register_function(
525            "Material".to_string(),
526            "emissive".to_string(),
527            std::sync::Arc::new(|args| {
528                if args.len() != 2 {
529                    return Err("Expected 2 arguments for function Material::emissive".to_string());
530                }
531                let mut itr = args.into_iter();
532                let color = Color::try_coerce(itr.next().unwrap())?;
533                let intensity = f64::try_coerce(itr.next().unwrap())?;
534                Ok(Material::emissive(color, intensity).to_pax_value())
535            }),
536        );
537        register_function(
538            "Material".to_string(),
539            "custom".to_string(),
540            std::sync::Arc::new(|args| {
541                if args.len() != 1 {
542                    return Err("Expected 1 argument for function Material::custom".to_string());
543                }
544                let params = MaterialParams::try_coerce(args.into_iter().next().unwrap())?;
545                Ok(Material::custom(params).to_pax_value())
546            }),
547        );
548        register_function(
549            "Material".to_string(),
550            "unlit".to_string(),
551            std::sync::Arc::new(|args| {
552                if !args.is_empty() {
553                    return Err("Expected 0 arguments for function Material::unlit".to_string());
554                }
555                Ok(Material::unlit().to_pax_value())
556            }),
557        );
558    }
559}
560
561/// Resolved ambient light for one logical canvas layer.
562#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
563#[serde(crate = "crate::serde")]
564pub struct SceneAmbientLight {
565    /// Ambient color.
566    pub color: Color,
567    /// Ambient intensity.
568    pub intensity: f64,
569}
570
571impl Default for SceneAmbientLight {
572    fn default() -> Self {
573        Self {
574            color: Color::WHITE,
575            intensity: 1.0,
576        }
577    }
578}
579
580impl Interpolatable for SceneAmbientLight {
581    fn interpolate(&self, other: &Self, t: f64) -> Self {
582        Self {
583            color: self.color.interpolate(&other.color, t),
584            intensity: self.intensity.interpolate(&other.intensity, t),
585        }
586    }
587}
588
589impl HelperFunctions for SceneAmbientLight {}
590
591/// Resolved light contribution for one logical canvas layer.
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593#[serde(crate = "crate::serde")]
594pub struct SceneLight {
595    /// Positional or directional light shape.
596    pub shape: LightShape,
597    /// Position in logical canvas pixels for point lights.
598    pub position: Vector3,
599    /// Direction in scene space for directional lights.
600    pub direction: Vector3,
601    /// Light color.
602    pub color: Color,
603    /// Light intensity.
604    pub intensity: f64,
605    /// Point light radius in logical pixels.
606    pub radius: f64,
607    /// Whether this light contributes.
608    pub enabled: bool,
609}
610
611impl Default for SceneLight {
612    fn default() -> Self {
613        Self {
614            shape: LightShape::Point,
615            position: Vector3::default(),
616            direction: Vector3::new(0.0, 0.0, -1.0),
617            color: Color::WHITE,
618            intensity: 1.0,
619            radius: 240.0,
620            enabled: true,
621        }
622    }
623}
624
625impl Interpolatable for SceneLight {
626    fn interpolate(&self, other: &Self, t: f64) -> Self {
627        Self {
628            shape: if t < 1.0 { self.shape } else { other.shape },
629            position: self.position.interpolate(&other.position, t),
630            direction: self.direction.interpolate(&other.direction, t),
631            color: self.color.interpolate(&other.color, t),
632            intensity: self.intensity.interpolate(&other.intensity, t),
633            radius: self.radius.interpolate(&other.radius, t),
634            enabled: if t < 1.0 { self.enabled } else { other.enabled },
635        }
636    }
637}
638
639impl HelperFunctions for SceneLight {}
640
641/// Resolved lighting state for one logical canvas layer.
642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
643#[serde(crate = "crate::serde")]
644pub struct SceneLighting {
645    /// Whether authored lights or ambient overrides are present.
646    pub active: bool,
647    /// Whether `ambient` came from an enabled authored `AmbientLight`.
648    ///
649    /// The default ambient is only applied to primitives with at least one
650    /// eligible direct light. An authored ambient remains layer-wide.
651    pub ambient_is_authored: bool,
652    /// Singleton ambient contribution.
653    pub ambient: SceneAmbientLight,
654    /// Positional and directional light contributions.
655    pub lights: Vec<SceneLight>,
656}
657
658impl Default for SceneLighting {
659    fn default() -> Self {
660        Self {
661            active: false,
662            ambient_is_authored: false,
663            ambient: SceneAmbientLight::default(),
664            lights: vec![],
665        }
666    }
667}
668
669impl SceneLighting {
670    /// Maximum number of simultaneously enabled lights in one target canvas layer.
671    pub const MAX_LIGHTS: usize = 8;
672
673    /// Ambient intensity used when point/directional lights exist but no explicit ambient override exists.
674    pub const DEFAULT_AMBIENT_INTENSITY: f64 = 0.35;
675
676    /// Returns lighting that preserves unlit legacy rendering.
677    pub fn identity() -> Self {
678        Self::default()
679    }
680
681    /// Creates active lighting with Pax's default ambient term.
682    pub fn with_default_ambient(lights: Vec<SceneLight>) -> Self {
683        Self {
684            active: !lights.is_empty(),
685            ambient_is_authored: false,
686            ambient: SceneAmbientLight {
687                color: Color::WHITE,
688                intensity: Self::DEFAULT_AMBIENT_INTENSITY,
689            },
690            lights,
691        }
692    }
693}
694
695impl Interpolatable for SceneLighting {}
696impl HelperFunctions for SceneLighting {}
697
698impl Interpolatable for Fill {
699    fn interpolate(&self, _other: &Self, _t: f64) -> Self {
700        // TODO interpolation
701        self.clone()
702    }
703}
704
705/// Describes a linear gradient fill with a start and end point, and a list of color stops.
706///
707/// Pax templates canonically author each point as `[x, y]`: magic index `0`
708/// is the horizontal coordinate and index `1` is the vertical coordinate.
709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
710#[serde(crate = "crate::serde")]
711pub struct LinearGradient {
712    /// Gradient start point in the primitive's local coordinate space.
713    pub start: (Size, Size),
714    /// Gradient end point in the primitive's local coordinate space.
715    pub end: (Size, Size),
716    /// Ordered color stops along the gradient.
717    pub stops: Vec<GradientStop>,
718}
719
720/// Describes a radial gradient fill with a start and end point, a radius, and a list of color stops.
721///
722/// Pax templates canonically author each point as `[x, y]`: magic index `0`
723/// is the horizontal coordinate and index `1` is the vertical coordinate.
724#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
725#[serde(crate = "crate::serde")]
726pub struct RadialGradient {
727    /// Outer radius endpoint in the primitive's local coordinate space.
728    pub end: (Size, Size),
729    /// Gradient center point in the primitive's local coordinate space.
730    pub start: (Size, Size),
731    /// Radial gradient radius.
732    pub radius: f64,
733    /// Ordered color stops along the gradient.
734    pub stops: Vec<GradientStop>,
735}
736
737/// A color stop for a gradient fill, defined by a position (% or px) and a color.
738#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash)]
739#[serde(crate = "crate::serde")]
740pub struct GradientStop {
741    /// Stop position, conventionally expressed as a percentage along the gradient.
742    pub position: Size,
743    /// Color at this stop.
744    pub color: Color,
745}
746
747impl GradientStop {
748    /// Constructs a gradient stop at `position`.
749    pub fn get(color: Color, position: Size) -> GradientStop {
750        GradientStop { position, color }
751    }
752
753    /// Returns a copy of this stop with alpha multiplied by `factor`.
754    pub fn with_alpha_factor(&self, factor: f64) -> GradientStop {
755        GradientStop {
756            position: self.position.clone(),
757            color: self.color.with_alpha_factor(factor),
758        }
759    }
760}
761
762impl Default for Fill {
763    fn default() -> Self {
764        Self::Solid(Color::default())
765    }
766}
767
768impl Fill {
769    fn gradient_stop_position_0_1(position: &Size) -> Option<f64> {
770        match position {
771            Size::Percent(p) => Some((p.to_float() / 100.0).clamp(0.0, 1.0)),
772            Size::Pixels(_) | Size::Combined(_, _) => None,
773        }
774    }
775
776    fn integrated_gradient_alpha_0_1(stops: &[GradientStop]) -> f64 {
777        if stops.is_empty() {
778            return 0.0;
779        }
780
781        let mut positioned_stops = stops
782            .iter()
783            .filter_map(|stop| {
784                Self::gradient_stop_position_0_1(&stop.position)
785                    .map(|position| (position, stop.color.alpha_0_1()))
786            })
787            .collect::<Vec<_>>();
788
789        if positioned_stops.is_empty() {
790            return stops
791                .iter()
792                .map(|stop| stop.color.alpha_0_1())
793                .fold(0.0, f64::max);
794        }
795
796        positioned_stops.sort_by(|lhs, rhs| lhs.0.total_cmp(&rhs.0));
797
798        if positioned_stops
799            .first()
800            .map(|stop| stop.0 > 0.0)
801            .unwrap_or(false)
802        {
803            let alpha = positioned_stops[0].1;
804            positioned_stops.insert(0, (0.0, alpha));
805        }
806
807        if positioned_stops
808            .last()
809            .map(|stop| stop.0 < 1.0)
810            .unwrap_or(false)
811        {
812            let alpha = positioned_stops.last().map(|stop| stop.1).unwrap_or(0.0);
813            positioned_stops.push((1.0, alpha));
814        }
815
816        let mut integrated_alpha = 0.0;
817        for window in positioned_stops.windows(2) {
818            let [(start_pos, start_alpha), (end_pos, end_alpha)] = window else {
819                continue;
820            };
821            let span = (end_pos - start_pos).clamp(0.0, 1.0);
822            if span <= f64::EPSILON {
823                continue;
824            }
825            integrated_alpha += (start_alpha + end_alpha) * 0.5 * span;
826        }
827
828        integrated_alpha.clamp(0.0, 1.0)
829    }
830
831    // Converts a Pax point into Piet's unit-gradient coordinate system.
832    pub fn to_unit_point((x, y): (Size, Size), (width, height): (f64, f64)) -> UnitPoint {
833        let normalized_x = match x {
834            Size::Pixels(val) => val.to_float() / width,
835            Size::Percent(val) => val.to_float() / 100.0,
836            Size::Combined(pix, per) => (pix.to_float() / width) + (per.to_float() / 100.0),
837        };
838
839        let normalized_y = match y {
840            Size::Pixels(val) => val.to_float() / height,
841            Size::Percent(val) => val.to_float() / 100.0,
842            Size::Combined(pix, per) => (pix.to_float() / width) + (per.to_float() / 100.0),
843        };
844        UnitPoint::new(normalized_x, normalized_y)
845    }
846
847    // Converts Pax gradient stops into Piet's gradient-stop representation.
848    pub fn to_piet_gradient_stops(stops: Vec<GradientStop>) -> Vec<piet::GradientStop> {
849        let mut ret = Vec::new();
850        for gradient_stop in stops {
851            match gradient_stop.position {
852                Size::Pixels(_) => {
853                    panic!("Gradient stops must be specified in percentages");
854                }
855                Size::Percent(p) => {
856                    ret.push(piet::GradientStop {
857                        pos: (p.to_float() / 100.0) as f32,
858                        color: gradient_stop.color.to_piet_color(),
859                    });
860                }
861                Size::Combined(_, _) => {
862                    panic!("Gradient stops must be specified in percentages");
863                }
864            }
865        }
866        ret
867    }
868
869    #[allow(non_snake_case)]
870    /// Constructs a linear gradient fill.
871    ///
872    /// Pax templates should normally use `@gradient`. When this helper is
873    /// needed explicitly, pass `start` and `end` as `[x, y]` lists.
874    pub fn linearGradient(
875        start: (Size, Size),
876        end: (Size, Size),
877        stops: Vec<GradientStop>,
878    ) -> Fill {
879        Fill::LinearGradient(LinearGradient { start, end, stops })
880    }
881
882    /// Returns a copy of this fill with alpha multiplied by `factor`.
883    pub fn with_alpha_factor(&self, factor: f64) -> Fill {
884        match self {
885            Fill::Solid(color) => Fill::Solid(color.with_alpha_factor(factor)),
886            Fill::LinearGradient(gradient) => Fill::LinearGradient(LinearGradient {
887                start: gradient.start.clone(),
888                end: gradient.end.clone(),
889                stops: gradient
890                    .stops
891                    .iter()
892                    .map(|stop| stop.with_alpha_factor(factor))
893                    .collect(),
894            }),
895            Fill::RadialGradient(gradient) => Fill::RadialGradient(RadialGradient {
896                start: gradient.start.clone(),
897                end: gradient.end.clone(),
898                radius: gradient.radius,
899                stops: gradient
900                    .stops
901                    .iter()
902                    .map(|stop| stop.with_alpha_factor(factor))
903                    .collect(),
904            }),
905        }
906    }
907
908    /// Returns the maximum alpha used by this fill.
909    pub fn max_alpha_0_1(&self) -> f64 {
910        match self {
911            Fill::Solid(color) => color.alpha_0_1(),
912            Fill::LinearGradient(gradient) => gradient
913                .stops
914                .iter()
915                .map(|stop| stop.color.alpha_0_1())
916                .fold(0.0, f64::max),
917            Fill::RadialGradient(gradient) => gradient
918                .stops
919                .iter()
920                .map(|stop| stop.color.alpha_0_1())
921                .fold(0.0, f64::max),
922        }
923    }
924
925    /// Estimates the alpha coverage contributed by this fill.
926    pub fn coverage_alpha_0_1(&self) -> f64 {
927        match self {
928            Fill::Solid(color) => color.alpha_0_1(),
929            Fill::LinearGradient(gradient) => Self::integrated_gradient_alpha_0_1(&gradient.stops),
930            Fill::RadialGradient(gradient) => Self::integrated_gradient_alpha_0_1(&gradient.stops),
931        }
932    }
933}
934
935#[cfg(test)]
936mod fill_coverage_tests {
937    use super::{Color, ColorChannel, Fill, GradientStop, LinearGradient, Numeric, Size};
938
939    fn rgba_alpha(alpha: u8) -> Color {
940        Color::rgba(
941            ColorChannel::Integer(0),
942            ColorChannel::Integer(0),
943            ColorChannel::Integer(0),
944            ColorChannel::Integer(alpha),
945        )
946    }
947
948    #[test]
949    fn coverage_alpha_matches_solid_alpha() {
950        let fill = Fill::Solid(rgba_alpha(128));
951        assert!((fill.coverage_alpha_0_1() - (128.0 / 255.0)).abs() < 1e-6);
952    }
953
954    #[test]
955    fn coverage_alpha_integrates_gradient_stops() {
956        let fill = Fill::LinearGradient(LinearGradient {
957            start: (
958                Size::Percent(Numeric::F64(0.0)),
959                Size::Percent(Numeric::F64(50.0)),
960            ),
961            end: (
962                Size::Percent(Numeric::F64(100.0)),
963                Size::Percent(Numeric::F64(50.0)),
964            ),
965            stops: vec![
966                GradientStop::get(rgba_alpha(0), Size::Percent(Numeric::F64(0.0))),
967                GradientStop::get(rgba_alpha(255), Size::Percent(Numeric::F64(50.0))),
968                GradientStop::get(rgba_alpha(0), Size::Percent(Numeric::F64(100.0))),
969            ],
970        });
971        assert!((fill.coverage_alpha_0_1() - 0.5).abs() < 1e-6);
972    }
973}