use super::Colour;
use leptos::prelude::*;
pub type SequentialGradient = (Colour, &'static [Colour]);
pub type DivergingGradient = (SequentialGradient, SequentialGradient);
#[derive(Clone, Debug, PartialEq)]
pub struct ColourScheme {
swatches: Vec<Colour>,
zero: Option<usize>,
}
impl ColourScheme {
pub fn new(first: Colour, rest: impl IntoIterator<Item = Colour>) -> Self {
Self {
swatches: std::iter::once(first).chain(rest).collect(),
zero: None,
}
}
pub fn diverging_gradient(below_zero: Self, above_zero: Self) -> Self {
let zero = below_zero.swatches.len();
Self {
swatches: below_zero
.swatches
.into_iter()
.chain(above_zero.swatches)
.collect(),
zero: Some(zero),
}
}
fn get_index(&self, index: usize) -> usize {
index.rem_euclid(self.swatches.len())
}
pub fn by_index(&self, index: usize) -> Colour {
let index = self.get_index(index);
self.swatches[index]
}
pub fn set_by_index(&mut self, index: usize, colour: Colour) {
let index = self.get_index(index);
self.swatches[index] = colour;
}
pub fn invert(self) -> Self {
let mut swatches = self.swatches.clone();
swatches.reverse();
Self {
swatches,
zero: self.zero,
}
}
fn line_to_prior_swatch_index(&self, line: usize, total: usize) -> usize {
if total <= 1 {
return 0;
}
let line = line.clamp(0, total - 1);
let swatches = self.swatches.len();
if line == total - 1 {
return swatches - 1;
}
(line as f64 / (total as f64 - 1.0) * swatches as f64) as usize
}
pub fn interpolate(&self, line: usize, total: usize) -> Colour {
let before_i = self.line_to_prior_swatch_index(line, total);
if before_i == self.swatches.len() - 1 {
return self.swatches[before_i];
}
let before = self.swatches[before_i];
let after = self.swatches[before_i + 1];
let lines_per_swatch = total as f64 / self.swatches.len() as f64;
let before_line = (before_i as f64 * lines_per_swatch) as usize;
let ratio = ((line - before_line) as f64) / lines_per_swatch;
before.interpolate(after, ratio)
}
}
#[component]
pub fn LinearGradientSvg(
#[prop(into)] id: String,
scheme: Signal<ColourScheme>,
range_y: Signal<Option<(f64, f64)>>,
) -> impl IntoView {
view! {
<linearGradient id=Some(id) x1="0%" y1="100%" x2="0%" y2="0%">
{move || scheme.get().stops(range_y.get().unwrap_or_default())}
</linearGradient>
}
.into_any()
}
impl ColourScheme {
fn stops(&self, range_y: (f64, f64)) -> impl IntoView {
if self.zero.is_some() {
self.diverging_stops(range_y).into_any()
} else {
self.sequential_stops().into_any()
}
}
fn sequential_stops(&self) -> impl IntoView {
let step = 1.0 / self.swatches.len().saturating_sub(1) as f64;
generate_stops(&self.swatches, 0.0, step)
}
fn diverging_stops(&self, (bottom_y, top_y): (f64, f64)) -> impl IntoView {
let zero = (1.0 - top_y / (top_y - bottom_y)).clamp(0.0, 1.0);
let (below_zero, above_zero) = self.diverging_swatches();
let below_step = zero * 1.0 / below_zero.len() as f64;
let above_step = (1.0 - zero) * 1.0 / above_zero.len() as f64;
let below_start = below_step / 2.0;
let above_start = above_step / 2.0 + below_zero.len() as f64 * below_step;
view! {
{generate_stops(below_zero, below_start, below_step)}
{generate_stops(above_zero, above_start, above_step)}
}
}
fn diverging_swatches(&self) -> (&[Colour], &[Colour]) {
if let Some(zero_index) = self.zero {
self.swatches.split_at(zero_index)
} else {
(&[], &self.swatches)
}
}
}
fn generate_stops(swatches: &[Colour], from: f64, step: f64) -> impl IntoView {
swatches
.iter()
.enumerate()
.map(|(i, colour)| (from + i as f64 * step, colour))
.filter(|(percent, _)| (0.0..1.0).contains(percent))
.map(|(percent, colour)| {
let offset = format!("{:.2}%", percent * 100.0);
view! {
<stop offset=offset stop-color=colour.to_string() />
}
})
.collect_view()
}
impl From<SequentialGradient> for ColourScheme {
fn from((first, rest): (Colour, &[Colour])) -> Self {
Self::new(first, rest.to_vec())
}
}
impl From<DivergingGradient> for ColourScheme {
fn from((below_zero, above_zero): DivergingGradient) -> Self {
let below_zero = below_zero.into();
let above_zero = above_zero.into();
Self::diverging_gradient(below_zero, above_zero)
}
}
macro_rules! from_array_to_colour_scheme {
($($n:literal),*) => {
$(
impl From<[Colour; $n]> for ColourScheme {
fn from(colours: [Colour; $n]) -> Self {
Self::new(colours[0], (&colours[1..]).to_vec())
}
}
)*
};
}
from_array_to_colour_scheme!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
#[cfg(test)]
mod tests {
use super::*;
fn scheme3() -> ColourScheme {
ColourScheme::from([
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
Colour::from_rgb(0, 0, 0),
])
}
const SCHEME10: [Colour; 10] = [
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
Colour::from_rgb(0, 0, 0),
Colour::from_rgb(255, 255, 255),
];
fn scheme10() -> ColourScheme {
ColourScheme::from(SCHEME10)
}
#[test]
fn test_colour_scheme() {
let scheme = scheme3();
assert_eq!(scheme.by_index(0), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme.by_index(1), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme.by_index(2), Colour::from_rgb(0, 0, 0));
}
#[test]
fn test_line_to_swatch_index() {
let scheme3 = scheme3();
let scheme10 = scheme10();
assert_eq!(scheme10.line_to_prior_swatch_index(0, 2), 0);
assert_eq!(scheme10.line_to_prior_swatch_index(1, 2), 9);
assert_eq!(scheme10.line_to_prior_swatch_index(0, 1), 0); assert_eq!(scheme10.line_to_prior_swatch_index(0, 5), 0);
assert_eq!(scheme10.line_to_prior_swatch_index(1, 5), 2);
assert_eq!(scheme10.line_to_prior_swatch_index(2, 5), 5);
assert_eq!(scheme10.line_to_prior_swatch_index(3, 5), 7);
assert_eq!(scheme10.line_to_prior_swatch_index(4, 5), 9);
assert_eq!(scheme3.line_to_prior_swatch_index(0, 1), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(1, 2), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(0, 3), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(1, 3), 1);
assert_eq!(scheme3.line_to_prior_swatch_index(2, 3), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(0, 5), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(1, 5), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(2, 5), 1);
assert_eq!(scheme3.line_to_prior_swatch_index(3, 5), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(4, 5), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(0, 9), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(1, 9), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(2, 9), 0);
assert_eq!(scheme3.line_to_prior_swatch_index(3, 9), 1);
assert_eq!(scheme3.line_to_prior_swatch_index(4, 9), 1);
assert_eq!(scheme3.line_to_prior_swatch_index(5, 9), 1);
assert_eq!(scheme3.line_to_prior_swatch_index(6, 9), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(7, 9), 2);
assert_eq!(scheme3.line_to_prior_swatch_index(8, 9), 2);
assert_eq!(scheme10.line_to_prior_swatch_index(37, 100), 3);
assert_eq!(scheme10.line_to_prior_swatch_index(3, 3), 9);
assert_eq!(scheme10.line_to_prior_swatch_index(4, 3), 9);
assert_eq!(scheme10.line_to_prior_swatch_index(0, 0), 0);
assert_eq!(scheme10.line_to_prior_swatch_index(1, 0), 0);
}
#[test]
fn test_scheme_interpolation() {
let scheme3 = scheme3();
let scheme10 = scheme10();
assert_eq!(scheme3.interpolate(0, 3), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme3.interpolate(1, 3), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme3.interpolate(2, 3), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme3.interpolate(0, 9), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme3.interpolate(1, 9), Colour::from_rgb(85, 85, 85));
assert_eq!(scheme3.interpolate(2, 9), Colour::from_rgb(170, 170, 170));
assert_eq!(scheme3.interpolate(3, 9), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme3.interpolate(4, 9), Colour::from_rgb(170, 170, 170));
assert_eq!(scheme3.interpolate(5, 9), Colour::from_rgb(85, 85, 85));
assert_eq!(scheme3.interpolate(6, 9), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme3.interpolate(7, 9), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme3.interpolate(8, 9), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme10.interpolate(0, 1), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme10.interpolate(0, 2), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme10.interpolate(1, 2), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme10.interpolate(0, 3), Colour::from_rgb(0, 0, 0));
assert_eq!(scheme10.interpolate(1, 3), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme10.interpolate(2, 3), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme10.interpolate(2, 5), Colour::from_rgb(255, 255, 255));
assert_eq!(scheme10.interpolate(2, 8), Colour::from_rgb(255, 255, 255));
}
}