use core::fmt;
use std::f32::consts::PI;
use crate::{Error, ErrorKind, Result, Solid, parser::parse_gradient};
#[derive(Debug, Clone, PartialOrd, PartialEq)]
pub struct Gradient {
pub direction: GradientCoordinates,
pub colors: Vec<Solid>,
}
impl fmt::Display for Gradient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let colors = self
.colors
.iter()
.map(|color| color.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"Gradient {{ start: ({}, {}), end: ({}, {}), colors: [{}] }}",
self.direction.start[0],
self.direction.start[1],
self.direction.end[0],
self.direction.end[1],
colors
)
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct GradientCoordinates {
pub start: [f32; 2],
pub end: [f32; 2],
}
impl TryFrom<&str> for GradientCoordinates {
type Error = Error;
fn try_from(color: &str) -> Result<Self> {
parse_coordinates(color)
}
}
#[derive(Debug)]
struct Line {
m: f32,
b: f32,
}
impl Line {
pub fn plug_in_x(&self, x: f32) -> f32 {
self.m * x + self.b
}
}
fn calculate_point(line: &Line, x: f32) -> [f32; 2] {
match line.plug_in_x(x) {
0.0..=1.0 => [x, line.plug_in_x(x)],
1.0.. => [(1.0 - line.b) / line.m, 1.0],
_ => [-line.b / line.m, 0.0],
}
}
fn parse_coordinates(coordinates: &str) -> Result<GradientCoordinates> {
let angle = parse_angle(coordinates);
match angle {
Some(angle) => {
let rad = -angle * PI / 180.0;
let m = match angle.abs() % 360.0 {
90.0 | 270.0 => angle.signum() * f32::MAX,
_ => rad.sin() / rad.cos(),
};
let b = -m * 0.5 + 0.5;
let line = Line { m, b };
let (x_s, x_e) = match angle.abs() % 360.0 {
0.0..90.0 => (0.0, 1.0),
90.0..270.0 => (1.0, 0.0),
270.0..360.0 => (0.0, 1.0),
_ => (0.0, 1.0),
};
let start = calculate_point(&line, x_s);
let end = calculate_point(&line, x_e);
Ok(GradientCoordinates { start, end })
}
None => match coordinates {
"to right" => Ok(GradientCoordinates {
start: [0.0, 0.5],
end: [1.0, 0.5],
}),
"to left" => Ok(GradientCoordinates {
start: [1.0, 0.5],
end: [0.0, 0.5],
}),
"to top" => Ok(GradientCoordinates {
start: [0.5, 1.0],
end: [0.5, 0.0],
}),
"to bottom" => Ok(GradientCoordinates {
start: [0.5, 0.0],
end: [0.5, 1.0],
}),
"to top right" => Ok(GradientCoordinates {
start: [0.0, 1.0],
end: [1.0, 0.0],
}),
"to top left" => Ok(GradientCoordinates {
start: [1.0, 1.0],
end: [0.0, 0.0],
}),
"to bottom right" => Ok(GradientCoordinates {
start: [0.0, 0.0],
end: [1.0, 1.0],
}),
"to bottom left" => Ok(GradientCoordinates {
start: [1.0, 0.0],
end: [0.0, 1.0],
}),
_ => Err(Error::new(
ErrorKind::InvalidGradientCoordinates,
coordinates,
)),
},
}
}
fn parse_angle(s: &str) -> Option<f32> {
s.strip_suffix("deg")
.and_then(|s| s.parse().ok())
.or_else(|| {
s.strip_suffix("grad")
.and_then(|s| s.parse().ok())
.map(|t: f32| t * 360.0 / 400.0)
})
.or_else(|| {
s.strip_suffix("rad")
.and_then(|s| s.parse().ok())
.map(|t: f32| t.to_degrees())
})
.or_else(|| {
s.strip_suffix("turn")
.and_then(|s| s.parse().ok())
.map(|t: f32| t * 360.0)
})
.or_else(|| s.parse().ok())
}
pub fn is_valid_direction(direction: &str) -> bool {
matches!(
direction,
"to right"
| "to left"
| "to top"
| "to bottom"
| "to top right"
| "to top left"
| "to bottom right"
| "to bottom left"
) || is_valid_angle(direction)
}
fn is_valid_angle(direction: &str) -> bool {
const VALID_SUFFIXES: [&str; 4] = ["deg", "grad", "rad", "turn"];
VALID_SUFFIXES.iter().any(|&suffix| {
direction
.strip_suffix(suffix) .and_then(|num| num.parse::<f32>().ok()) .is_some()
})
}
impl TryFrom<&str> for Gradient {
type Error = Error;
fn try_from(color: &str) -> Result<Self> {
parse_gradient(color)
}
}