mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add support for gradients with midpoints and add draggable diamonds to the color picker dialog (#3813)
* Refactor GradientStops to use struct-of-arrays and include midpoint * Implement interaction and rendering * Make color picker saturation-value color picking snap to original position and show both axis lines Make color picker saturation-value color picking snap to original position and show both axis lines * Add graphite:midpoint attribute to SVG exports * Add graphite:midpoint parsing to SVG importer
This commit is contained in:
@@ -847,6 +847,18 @@ impl Color {
|
||||
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
||||
}
|
||||
|
||||
/// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn to_rgba_hex_srgb_from_gamma(&self) -> String {
|
||||
format!(
|
||||
"{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||
(self.r() * 255.) as u8,
|
||||
(self.g() * 255.) as u8,
|
||||
(self.b() * 255.) as u8,
|
||||
(self.a() * 255.) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space.
|
||||
///
|
||||
/// # Examples
|
||||
|
||||
@@ -16,15 +16,18 @@ impl RenderExt for Gradient {
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: DAffine2, transformed_bounds: DAffine2, _render_params: &RenderParams) -> Self::Output {
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
for (position, color, original_midpoint) in self.stops.interpolated_samples() {
|
||||
stop.push_str("<stop");
|
||||
if *position != 0. {
|
||||
if position != 0. {
|
||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||
}
|
||||
let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(midpoint) = original_midpoint {
|
||||
let _ = write!(stop, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
||||
}
|
||||
stop.push_str(" />")
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ impl SvgRender {
|
||||
let (x, y) = bounds_min.into();
|
||||
let (size_x, size_y) = (bounds_max - bounds_min).into();
|
||||
let defs = &self.svg_defs;
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" viewBox="{x} {y} {size_x} {size_y}"><defs>{defs}</defs>"#,);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</svg>".into());
|
||||
}
|
||||
@@ -99,7 +99,7 @@ impl SvgRender {
|
||||
let matrix = format_transform_matrix(transform);
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
|
||||
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:graphite="https://graphite.art" {view_box}><defs>{defs}</defs><g{transform}>"#);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</g></svg>".into());
|
||||
}
|
||||
@@ -997,9 +997,9 @@ impl Render for Table<Vector> {
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
for (position, color, _) in gradient.stops.interpolated_samples() {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
offset: position as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
@@ -1557,11 +1557,14 @@ impl Render for Table<GradientStops> {
|
||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
|
||||
let mut stop_string = String::new();
|
||||
for (position, color) in row.element.0.iter() {
|
||||
for (position, color, original_midpoint) in row.element.interpolated_samples() {
|
||||
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
||||
}
|
||||
if let Some(midpoint) = original_midpoint {
|
||||
let _ = write!(stop_string, r#" graphite:midpoint="{}""#, (midpoint * 1000.).round() / 1000.);
|
||||
}
|
||||
stop_string.push_str(" />");
|
||||
}
|
||||
|
||||
@@ -1619,7 +1622,7 @@ impl Render for Table<GradientStops> {
|
||||
let blend_mode = alpha_blending.blend_mode.to_peniko();
|
||||
let opacity = alpha_blending.opacity(render_params.for_mask);
|
||||
|
||||
let color = row.element.0.first().map(|stop| stop.1).unwrap_or(Color::MAGENTA);
|
||||
let color = row.element.color.first().copied().unwrap_or(Color::MAGENTA);
|
||||
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
|
||||
|
||||
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
||||
|
||||
@@ -13,22 +13,69 @@ pub enum GradientType {
|
||||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||
// TODO: Use linear not gamma colors
|
||||
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub struct GradientStops(pub Vec<(f64, Color)>);
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, DynAny, specta::Type)]
|
||||
pub struct GradientStops {
|
||||
/// The position of this stop, a factor from 0-1 along the length of the full gradient.
|
||||
pub position: Vec<f64>,
|
||||
/// The midpoint to the right of this stop, a factor from 0-1 along the distance to the next stop. The final stop's midpoint is ignored.
|
||||
pub midpoint: Vec<f64>,
|
||||
/// The color at this stop.
|
||||
pub color: Vec<Color>,
|
||||
}
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
impl<'de> serde::Deserialize<'de> for GradientStops {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct NewFormat {
|
||||
position: Vec<f64>,
|
||||
midpoint: Vec<f64>,
|
||||
color: Vec<Color>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum GradientStopsFormat {
|
||||
New(NewFormat),
|
||||
Old(Vec<(f64, Color)>),
|
||||
}
|
||||
|
||||
Ok(match GradientStopsFormat::deserialize(deserializer)? {
|
||||
GradientStopsFormat::New(new) => Self {
|
||||
position: new.position,
|
||||
midpoint: new.midpoint,
|
||||
color: new.color,
|
||||
},
|
||||
GradientStopsFormat::Old(stops) => {
|
||||
let count = stops.len();
|
||||
Self {
|
||||
position: stops.iter().map(|(p, _)| *p).collect(),
|
||||
midpoint: vec![0.5; count],
|
||||
color: stops.into_iter().map(|(_, c)| c).collect(),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for GradientStops {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.len().hash(state);
|
||||
self.0.iter().for_each(|(position, color)| {
|
||||
position.to_bits().hash(state);
|
||||
color.hash(state);
|
||||
});
|
||||
self.position.len().hash(state);
|
||||
for i in 0..self.position.len() {
|
||||
self.position[i].to_bits().hash(state);
|
||||
self.midpoint[i].to_bits().hash(state);
|
||||
self.color[i].hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GradientStops {
|
||||
fn default() -> Self {
|
||||
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
|
||||
Self {
|
||||
position: vec![0., 1.],
|
||||
midpoint: vec![0.5, 0.5],
|
||||
color: vec![Color::BLACK, Color::WHITE],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,71 +85,145 @@ impl RenderComplexity for GradientStops {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
/// Apply the midpoint curve to a normalized parameter `t` (0 to 1) given a `midpoint` (0 to 1, where 0.5 is linear).
|
||||
fn apply_midpoint(t: f64, midpoint: f64) -> f64 {
|
||||
if (midpoint - 0.5).abs() < 1e-6 {
|
||||
return t;
|
||||
}
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
let midpoint = midpoint.clamp(f64::EPSILON, 1. - f64::EPSILON);
|
||||
|
||||
if midpoint < 0.5 {
|
||||
let q = -1. / (1. - midpoint).log2();
|
||||
1. - (1. - t).powf(q)
|
||||
} else {
|
||||
let p = -1. / midpoint.log2();
|
||||
t.powf(p)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GradientStop {
|
||||
pub position: f64,
|
||||
pub midpoint: f64,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
pub struct GradientStopsIter<'a> {
|
||||
stops: &'a GradientStops,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GradientStopsIter<'a> {
|
||||
type Item = GradientStop;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index >= self.stops.position.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stop = GradientStop {
|
||||
position: self.stops.position[self.index],
|
||||
midpoint: self.stops.midpoint[self.index],
|
||||
color: self.stops.color[self.index],
|
||||
};
|
||||
self.index += 1;
|
||||
Some(stop)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let remaining = self.stops.position.len() - self.index;
|
||||
(remaining, Some(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for GradientStopsIter<'_> {}
|
||||
|
||||
impl<'a> IntoIterator for &'a GradientStops {
|
||||
type Item = &'a (f64, Color);
|
||||
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
|
||||
type Item = GradientStop;
|
||||
type IntoIter = GradientStopsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
GradientStopsIter { stops: self, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for GradientStops {
|
||||
type Output = (f64, Color);
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = GradientStop;
|
||||
type IntoIter = std::vec::IntoIter<GradientStop>;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.0[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for GradientStops {
|
||||
type Target = Vec<(f64, Color)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for GradientStops {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.position
|
||||
.into_iter()
|
||||
.zip(self.midpoint)
|
||||
.zip(self.color)
|
||||
.map(|((position, midpoint), color)| GradientStop { position, midpoint, color })
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops {
|
||||
pub fn new(stops: Vec<(f64, Color)>) -> Self {
|
||||
let mut stops = Self(stops);
|
||||
stops.sort();
|
||||
stops
|
||||
pub fn new(stops: impl IntoIterator<Item = GradientStop>) -> Self {
|
||||
let mut position = Vec::new();
|
||||
let mut midpoint = Vec::new();
|
||||
let mut color = Vec::new();
|
||||
|
||||
for stop in stops {
|
||||
position.push(stop.position);
|
||||
midpoint.push(stop.midpoint);
|
||||
color.push(stop.color);
|
||||
}
|
||||
|
||||
Self { position, midpoint, color }
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.position.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.position.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> GradientStopsIter<'_> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
/// Remove a stop at the given index.
|
||||
pub fn remove(&mut self, index: usize) {
|
||||
self.position.remove(index);
|
||||
self.midpoint.remove(index);
|
||||
self.color.remove(index);
|
||||
}
|
||||
|
||||
/// Remove and return the last stop's color, or `None` if empty.
|
||||
pub fn pop(&mut self) -> Option<Color> {
|
||||
self.position.pop();
|
||||
self.midpoint.pop();
|
||||
self.color.pop()
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.0.is_empty() {
|
||||
if self.position.is_empty() {
|
||||
return Color::BLACK;
|
||||
}
|
||||
|
||||
if t <= self.0[0].0 {
|
||||
return self.0[0].1;
|
||||
if t <= self.position[0] {
|
||||
return self.color[0];
|
||||
}
|
||||
if t >= self.0[self.0.len() - 1].0 {
|
||||
return self.0[self.0.len() - 1].1;
|
||||
let last = self.position.len() - 1;
|
||||
if t >= self.position[last] {
|
||||
return self.color[last];
|
||||
}
|
||||
|
||||
for i in 0..self.0.len() - 1 {
|
||||
let (t1, c1) = self.0[i];
|
||||
let (t2, c2) = self.0[i + 1];
|
||||
for i in 0..self.position.len() - 1 {
|
||||
let (t1, c1) = (self.position[i], self.color[i]);
|
||||
let (t2, c2) = (self.position[i + 1], self.color[i + 1]);
|
||||
if t >= t1 && t <= t2 {
|
||||
let normalized_t = (t - t1) / (t2 - t1);
|
||||
return c1.lerp(&c2, normalized_t as f32);
|
||||
let adjusted_t = apply_midpoint(normalized_t, self.midpoint[i]);
|
||||
return c1.lerp(&c2, adjusted_t as f32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,15 +231,104 @@ impl GradientStops {
|
||||
}
|
||||
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
let mut indices: Vec<usize> = (0..self.position.len()).collect();
|
||||
indices.sort_unstable_by(|&a, &b| self.position[a].partial_cmp(&self.position[b]).unwrap());
|
||||
self.position = indices.iter().map(|&i| self.position[i]).collect();
|
||||
self.midpoint = indices.iter().map(|&i| self.midpoint[i]).collect();
|
||||
self.color = indices.iter().map(|&i| self.color[i]).collect();
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
|
||||
let position: Vec<f64> = self.position.iter().rev().map(|&p| 1. - p).collect();
|
||||
|
||||
let count = self.midpoint.len();
|
||||
let midpoint = (0..count).map(|i| if i < count - 1 { 1. - self.midpoint[count - 2 - i] } else { 0.5 }).collect::<Vec<_>>();
|
||||
|
||||
let color: Vec<Color> = self.color.iter().rev().cloned().collect();
|
||||
|
||||
Self { position, midpoint, color }
|
||||
}
|
||||
|
||||
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
|
||||
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
|
||||
Self {
|
||||
position: self.position.clone(),
|
||||
midpoint: self.midpoint.clone(),
|
||||
color: self.color.iter().map(f).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce a set of linearly-interpolated color samples that approximate the gradient's midpoint curves.
|
||||
///
|
||||
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
|
||||
/// midpoint for actual gradient stops, and `None` for interpolated samples added to approximate midpoint curves.
|
||||
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
|
||||
/// Controls accuracy vs. number of samples tradeoff.
|
||||
/// 2/255 means the linear approximation will deviate by no more than 2 gradations of 8-bit color from the theoretically perfect curve with this midpoint bias.
|
||||
const THRESHOLD: f64 = 2. / 255.;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn subdivide(left: f64, right: f64, midpoint: f64, pos_a: f64, pos_b: f64, color_a: Color, color_b: Color, result: &mut Vec<(f64, Color, Option<f64>)>, depth: u32) {
|
||||
const MAX_DEPTH: u32 = 20;
|
||||
if depth >= MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
|
||||
let mid = (left + right) / 2.;
|
||||
|
||||
let y_actual = apply_midpoint(mid, midpoint);
|
||||
let y_left = apply_midpoint(left, midpoint);
|
||||
let y_right = apply_midpoint(right, midpoint);
|
||||
let y_linear = (y_left + y_right) / 2.;
|
||||
|
||||
if (y_actual - y_linear).abs() > THRESHOLD {
|
||||
subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
||||
|
||||
let global_pos = pos_a + mid * (pos_b - pos_a);
|
||||
let color = color_a.lerp(&color_b, y_actual as f32);
|
||||
result.push((global_pos, color, None));
|
||||
|
||||
subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if self.position.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
if self.position.len() == 1 {
|
||||
return vec![(self.position[0], self.color[0], Some(self.midpoint[0]))];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for i in 0..self.position.len() - 1 {
|
||||
let pos_a = self.position[i];
|
||||
let pos_b = self.position[i + 1];
|
||||
let color_a = self.color[i];
|
||||
let color_b = self.color[i + 1];
|
||||
let midpoint = self.midpoint[i].clamp(0.01, 0.99);
|
||||
let next_midpoint = self.midpoint[i + 1].clamp(0.01, 0.99);
|
||||
|
||||
// Add the start stop (subsequent segments share the previous end stop)
|
||||
if i == 0 {
|
||||
result.push((pos_a, color_a, Some(midpoint)));
|
||||
}
|
||||
|
||||
// Only subdivide if midpoint deviates from linear (0.5)
|
||||
if (midpoint - 0.5).abs() >= 1e-6 {
|
||||
subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, &mut result, 0);
|
||||
}
|
||||
|
||||
// Add the end stop
|
||||
result.push((pos_b, color_b, Some(next_midpoint)));
|
||||
}
|
||||
|
||||
// If every midpoint is 0.5 (or within epsilon), turn all midpoints to None
|
||||
if result.iter().all(|(_, _, midpoint)| matches!(midpoint, Some(m) if (m - 0.5).abs() < 1e-6)) {
|
||||
result.iter_mut().for_each(|(_, _, midpoint)| *midpoint = None);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,13 +357,14 @@ impl Default for Gradient {
|
||||
|
||||
impl std::hash::Hash for Gradient {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stops.0.len().hash(state);
|
||||
self.stops.len().hash(state);
|
||||
[].iter()
|
||||
.chain(self.start.to_array().iter())
|
||||
.chain(self.end.to_array().iter())
|
||||
.chain(self.stops.0.iter().map(|(position, _)| position))
|
||||
.chain(self.stops.position.iter())
|
||||
.chain(self.stops.midpoint.iter())
|
||||
.for_each(|x| x.to_bits().hash(state));
|
||||
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
|
||||
self.stops.color.iter().for_each(|color| color.hash(state));
|
||||
self.gradient_type.hash(state);
|
||||
}
|
||||
}
|
||||
@@ -163,9 +374,8 @@ impl std::fmt::Display for Gradient {
|
||||
let round = |x: f64| (x * 1e3).round() / 1e3;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
|
||||
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), stop.color.to_rgba_hex_srgb()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||
@@ -175,7 +385,18 @@ impl std::fmt::Display for Gradient {
|
||||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, gradient_type: GradientType) -> Self {
|
||||
let stops = GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]);
|
||||
let stops = GradientStops::new([
|
||||
GradientStop {
|
||||
position: 0.,
|
||||
midpoint: 0.5,
|
||||
color: start_color.to_gamma_srgb(),
|
||||
},
|
||||
GradientStop {
|
||||
position: 1.,
|
||||
midpoint: 0.5,
|
||||
color: end_color.to_gamma_srgb(),
|
||||
},
|
||||
]);
|
||||
|
||||
Self { start, end, stops, gradient_type }
|
||||
}
|
||||
@@ -183,17 +404,11 @@ impl Gradient {
|
||||
pub fn lerp(&self, other: &Self, time: f64) -> Self {
|
||||
let start = self.start + (other.start - self.start) * time;
|
||||
let end = self.end + (other.end - self.end) * time;
|
||||
let stops = self
|
||||
.stops
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.stops.0.iter())
|
||||
.map(|((a_pos, a_color), (b_pos, b_color))| {
|
||||
let position = a_pos + (b_pos - a_pos) * time;
|
||||
let color = a_color.lerp(b_color, time as f32);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = self.stops.iter().zip(other.stops.iter()).map(|(a, b)| {
|
||||
let position = a.position + (b.position - a.position) * time;
|
||||
let color = a.color.lerp(&b.color, time as f32);
|
||||
GradientStop { position, midpoint: 0.5, color }
|
||||
});
|
||||
let stops = GradientStops::new(stops);
|
||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||
|
||||
@@ -213,27 +428,19 @@ impl Gradient {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Compute the color of the inserted stop
|
||||
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
|
||||
// Lerp between the nearest colors if applicable
|
||||
(a, Some(b)) => a.lerp(
|
||||
&b,
|
||||
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
|
||||
),
|
||||
// Use the start or the end color if applicable
|
||||
(v, _) => v,
|
||||
};
|
||||
// Compute the color of the inserted stop using evaluate (which respects midpoints)
|
||||
let new_color = self.stops.evaluate(new_position);
|
||||
|
||||
// Compute the correct index to keep the positions in order
|
||||
let mut index = 0;
|
||||
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
|
||||
while self.stops.len() > index && self.stops.position[index] <= new_position {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let new_color = get_color(index - 1, new_position);
|
||||
|
||||
// Insert the new stop
|
||||
self.stops.0.insert(index, (new_position, new_color));
|
||||
self.stops.position.insert(index, new_position);
|
||||
self.stops.midpoint.insert(index, 0.5);
|
||||
self.stops.color.insert(index, new_color);
|
||||
|
||||
Some(index)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod vector;
|
||||
|
||||
// Re-export commonly used types at the crate root
|
||||
pub use core_types as gcore;
|
||||
pub use gradient::{GradientStops, GradientType};
|
||||
pub use gradient::{GradientStop, GradientStops, GradientType};
|
||||
pub use math::{QuadExt, RectExt};
|
||||
pub use subpath::Subpath;
|
||||
pub use vector::Vector;
|
||||
|
||||
@@ -51,7 +51,13 @@ impl Fill {
|
||||
Self::None => Color::BLACK,
|
||||
Self::Solid(color) => *color,
|
||||
// TODO: Should correctly sample the gradient the equation here: https://svgwg.org/svg2-draft/pservers.html#Gradients
|
||||
Self::Gradient(Gradient { stops, .. }) => stops.0[0].1,
|
||||
Self::Gradient(Gradient { stops, .. }) => {
|
||||
if stops.is_empty() {
|
||||
Color::BLACK
|
||||
} else {
|
||||
stops.color[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,13 +70,13 @@ impl Fill {
|
||||
(Self::Solid(a), Self::Solid(b)) => Self::Solid(a.lerp(b, time as f32)),
|
||||
(Self::Solid(a), Self::Gradient(b)) => {
|
||||
let mut solid_to_gradient = b.clone();
|
||||
solid_to_gradient.stops.0.iter_mut().for_each(|(_, color)| *color = *a);
|
||||
solid_to_gradient.stops.color.iter_mut().for_each(|color| *color = *a);
|
||||
let a = &solid_to_gradient;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
(Self::Gradient(a), Self::Solid(b)) => {
|
||||
let mut gradient_to_solid = a.clone();
|
||||
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
|
||||
gradient_to_solid.stops.color.iter_mut().for_each(|color| *color = *b);
|
||||
let b = &gradient_to_solid;
|
||||
Self::Gradient(a.lerp(b, time))
|
||||
}
|
||||
@@ -99,7 +105,7 @@ impl Fill {
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::Gradient(gradient) => gradient.stops.color.iter().all(|color| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user