tiny-skia 0.8.2

A tiny Skia subset ported to Rust.
Documentation
// Copyright 2006 The Android Open Source Project
// Copyright 2020 Yevhenii Reizner
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use alloc::vec::Vec;

use tiny_skia_path::Scalar;

use crate::{Color, GradientStop, Point, Shader, SpreadMode, Transform};

use super::gradient::{Gradient, DEGENERATE_THRESHOLD};
use crate::pipeline::RasterPipelineBuilder;

/// A linear gradient shader.
#[derive(Clone, Debug)]
pub struct LinearGradient {
    pub(crate) base: Gradient,
}

impl LinearGradient {
    /// Creates a new linear gradient shader.
    ///
    /// Returns `Shader::SolidColor` when:
    /// - `stops.len()` == 1
    /// - `start` and `end` are very close
    ///
    /// Returns `None` when:
    ///
    /// - `stops` is empty
    /// - `start` == `end`
    /// - `transform` is not invertible
    #[allow(clippy::new_ret_no_self)]
    pub fn new(
        start: Point,
        end: Point,
        stops: Vec<GradientStop>,
        mode: SpreadMode,
        transform: Transform,
    ) -> Option<Shader<'static>> {
        if stops.is_empty() {
            return None;
        }

        if stops.len() == 1 {
            return Some(Shader::SolidColor(stops[0].color));
        }

        let length = (end - start).length();
        if !length.is_finite() {
            return None;
        }

        if length.is_nearly_zero_within_tolerance(DEGENERATE_THRESHOLD) {
            // Degenerate gradient, the only tricky complication is when in clamp mode,
            // the limit of the gradient approaches two half planes of solid color
            // (first and last). However, they are divided by the line perpendicular
            // to the start and end point, which becomes undefined once start and end
            // are exactly the same, so just use the end color for a stable solution.

            // Except for special circumstances of clamped gradients,
            // every gradient shape (when degenerate) can be mapped to the same fallbacks.
            // The specific shape factories must account for special clamped conditions separately,
            // this will always return the last color for clamped gradients.
            match mode {
                SpreadMode::Pad => {
                    // Depending on how the gradient shape degenerates,
                    // there may be a more specialized fallback representation
                    // for the factories to use, but this is a reasonable default.
                    return Some(Shader::SolidColor(stops.last().unwrap().color));
                }
                SpreadMode::Reflect | SpreadMode::Repeat => {
                    // repeat and mirror are treated the same: the border colors are never visible,
                    // but approximate the final color as infinite repetitions of the colors, so
                    // it can be represented as the average color of the gradient.
                    return Some(Shader::SolidColor(average_gradient_color(&stops)));
                }
            }
        }

        transform.invert()?;

        let unit_ts = points_to_unit_ts(start, end)?;
        Some(Shader::LinearGradient(LinearGradient {
            base: Gradient::new(stops, mode, transform, unit_ts),
        }))
    }

    pub(crate) fn is_opaque(&self) -> bool {
        self.base.colors_are_opaque
    }

    pub(crate) fn push_stages(&self, p: &mut RasterPipelineBuilder) -> Option<()> {
        self.base.push_stages(p, &|_| {}, &|_| {})
    }
}

fn points_to_unit_ts(start: Point, end: Point) -> Option<Transform> {
    let mut vec = end - start;
    let mag = vec.length();
    let inv = if mag != 0.0 { mag.invert() } else { 0.0 };

    vec.scale(inv);

    let mut ts = ts_from_sin_cos_at(-vec.y, vec.x, start.x, start.y);
    ts = ts.post_translate(-start.x, -start.y);
    ts = ts.post_scale(inv, inv);
    Some(ts)
}

fn average_gradient_color(points: &[GradientStop]) -> Color {
    use crate::wide::f32x4;

    fn load_color(c: Color) -> f32x4 {
        f32x4::from([c.red(), c.green(), c.blue(), c.alpha()])
    }

    fn store_color(c: f32x4) -> Color {
        let c: [f32; 4] = c.into();
        Color::from_rgba(c[0], c[1], c[2], c[3]).unwrap()
    }

    assert!(!points.is_empty());

    // The gradient is a piecewise linear interpolation between colors. For a given interval,
    // the integral between the two endpoints is 0.5 * (ci + cj) * (pj - pi), which provides that
    // intervals average color. The overall average color is thus the sum of each piece. The thing
    // to keep in mind is that the provided gradient definition may implicitly use p=0 and p=1.
    let mut blend = f32x4::splat(0.0);

    // Bake 1/(colorCount - 1) uniform stop difference into this scale factor
    let w_scale = f32x4::splat(0.5);

    for i in 0..points.len() - 1 {
        // Calculate the average color for the interval between pos(i) and pos(i+1)
        let c0 = load_color(points[i].color);
        let c1 = load_color(points[i + 1].color);
        // when pos == null, there are colorCount uniformly distributed stops, going from 0 to 1,
        // so pos[i + 1] - pos[i] = 1/(colorCount-1)
        let w = points[i + 1].position.get() - points[i].position.get();
        blend += w_scale * f32x4::splat(w) * (c1 + c0);
    }

    // Now account for any implicit intervals at the start or end of the stop definitions
    if points[0].position.get() > 0.0 {
        // The first color is fixed between p = 0 to pos[0], so 0.5 * (ci + cj) * (pj - pi)
        // becomes 0.5 * (c + c) * (pj - 0) = c * pj
        let c = load_color(points[0].color);
        blend += f32x4::splat(points[0].position.get()) * c;
    }

    let last_idx = points.len() - 1;
    if points[last_idx].position.get() < 1.0 {
        // The last color is fixed between pos[n-1] to p = 1, so 0.5 * (ci + cj) * (pj - pi)
        // becomes 0.5 * (c + c) * (1 - pi) = c * (1 - pi)
        let c = load_color(points[last_idx].color);
        blend += (f32x4::splat(1.0) - f32x4::splat(points[last_idx].position.get())) * c;
    }

    store_color(blend)
}

fn ts_from_sin_cos_at(sin: f32, cos: f32, px: f32, py: f32) -> Transform {
    let cos_inv = 1.0 - cos;
    Transform::from_row(
        cos,
        sin,
        -sin,
        cos,
        sdot(sin, py, cos_inv, px),
        sdot(-sin, px, cos_inv, py),
    )
}

fn sdot(a: f32, b: f32, c: f32, d: f32) -> f32 {
    a * b + c * d
}