Move gradient picking into the color picker (#1778)

* Gradient picker

* Fix up color picker layout CSS problems

* Begin hooking up SpectrumInput for gradient in the ColorPicker

* Working gradient picking on the frontend only

* Plumb FillColorChoice into the backend

* Hook everything else up, just with a weird bug remaining

* Fix some svelty reactivity issues

* Add and remove stops

* Cleanup

* Rename type

* Fill node document format upgrading

* Fix lint

* Polish the color picker UX and fix a bug

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers 2024-06-09 22:55:13 -07:00 committed by GitHub
parent 449729f1e1
commit a9a4b5cd19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1380 additions and 664 deletions

View file

@ -265,6 +265,9 @@ impl Color {
pub const RED: Color = Color::from_rgbf32_unchecked(1., 0., 0.);
pub const GREEN: Color = Color::from_rgbf32_unchecked(0., 1., 0.);
pub const BLUE: Color = Color::from_rgbf32_unchecked(0., 0., 1.);
pub const YELLOW: Color = Color::from_rgbf32_unchecked(1., 1., 0.);
pub const CYAN: Color = Color::from_rgbf32_unchecked(0., 1., 1.);
pub const MAGENTA: Color = Color::from_rgbf32_unchecked(1., 0., 1.);
pub const TRANSPARENT: Color = Self {
red: 0.,
green: 0.,

View file

@ -27,29 +27,62 @@ pub enum GradientType {
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
/// 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)>);
impl std::hash::Hash for GradientStops {
fn hash<H: core::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);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
pub positions: Vec<(f64, Color)>,
pub gradient_type: GradientType,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl core::hash::Hash for Gradient {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.positions.len().hash(state);
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.positions.iter().map(|(position, _)| position))
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.positions.iter().for_each(|(_, color)| color.hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
@ -60,7 +93,7 @@ impl Gradient {
Gradient {
start,
end,
positions: vec![(0., start_color), (1., end_color)],
stops: GradientStops(vec![(0., start_color), (1., end_color)]),
transform,
gradient_type,
}
@ -70,23 +103,25 @@ impl Gradient {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let positions = self
.positions
let stops = self
.stops
.0
.iter()
.zip(other.positions.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 = GradientStops(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
positions,
stops,
gradient_type,
}
}
@ -97,9 +132,9 @@ impl Gradient {
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
let updated_transform = multiplied_transform * bound_transform;
let mut positions = String::new();
for (position, color) in self.positions.iter() {
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
let mut stop = String::new();
for (position, color) in self.stops.0.iter() {
let _ = write!(stop, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
}
let mod_gradient = transformed_bound_transform.inverse();
@ -121,7 +156,7 @@ impl Gradient {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, transform, positions
gradient_id, start.x, end.x, start.y, end.y, transform, stop
);
}
GradientType::Radial => {
@ -129,7 +164,7 @@ impl Gradient {
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, transform, positions
gradient_id, start.x, start.y, radius, transform, stop
);
}
}
@ -151,11 +186,11 @@ impl Gradient {
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.positions[index].1, self.positions.get(index + 1).map(|(_, c)| *c)) {
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.positions[index].0) / self.positions.get(index + 1).map(|end| end.0 - self.positions[index].0).unwrap_or_default()) as f32,
((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,
@ -163,14 +198,14 @@ impl Gradient {
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.positions.len() > index && self.positions[index].0 <= new_position {
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.positions.insert(index, (new_position, new_color));
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
@ -178,7 +213,9 @@ impl Gradient {
/// Describes the fill of a layer.
///
/// Can be None, a solid [Color], a linear [Gradient], a radial [Gradient] or potentially some sort of image or pattern in the future
/// Can be None, a solid [Color], or a linear/radial [Gradient].
///
/// In the future we'll probably also add a pattern fill.
#[repr(C)]
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
pub enum Fill {
@ -207,8 +244,8 @@ impl Fill {
match self {
Self::None => Color::BLACK,
Self::Solid(color) => *color,
// TODO: Should correctly sample the gradient
Self::Gradient(Gradient { positions, .. }) => positions[0].1,
// 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,
}
}
@ -221,13 +258,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.positions.iter_mut().for_each(|(_, color)| *color = *a);
solid_to_gradient.stops.0.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.positions.iter_mut().for_each(|(_, color)| *color = *b);
gradient_to_solid.stops.0.iter_mut().for_each(|(_, color)| *color = *b);
let b = &gradient_to_solid;
Self::Gradient(a.lerp(b, time))
}
@ -248,22 +285,91 @@ impl Fill {
}
}
/// Check if the fill is not none
pub fn is_some(&self) -> bool {
*self != Self::None
}
/// Extract a gradient from the fill
pub fn as_gradient(&self) -> Option<&Gradient> {
if let Self::Gradient(gradient) = self {
Some(gradient)
} else {
None
match self {
Self::Gradient(gradient) => Some(gradient),
_ => None,
}
}
}
/// Enum describing the type of [Fill]
impl From<Color> for Fill {
fn from(color: Color) -> Fill {
Fill::Solid(color)
}
}
impl From<Option<Color>> for Fill {
fn from(color: Option<Color>) -> Fill {
Fill::solid_or_none(color)
}
}
impl From<Gradient> for Fill {
fn from(gradient: Gradient) -> Fill {
Fill::Gradient(gradient)
}
}
/// Describes the fill of a layer, but unlike [`Fill`], this doesn't store a [`Gradient`] directly but just its [`GradientStops`].
///
/// Can be None, a solid [Color], or a linear/radial [Gradient].
///
/// In the future we'll probably also add a pattern fill.
#[repr(C)]
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
pub enum FillChoice {
#[default]
None,
Solid(Color),
Gradient(GradientStops),
}
impl FillChoice {
pub fn from_optional_color(color: Option<Color>) -> Self {
match color {
Some(color) => Self::Solid(color),
None => Self::None,
}
}
pub fn as_solid(&self) -> Option<Color> {
let Self::Solid(color) = self else { return None };
Some(*color)
}
pub fn as_gradient(&self) -> Option<&GradientStops> {
let Self::Gradient(gradient) = self else { return None };
Some(gradient)
}
/// Convert this [`FillChoice`] to a [`Fill`] using the provided [`Gradient`] as a base for the positional information of the gradient.
/// If a gradient isn't provided, default gradient positional information is used in cases where the [`FillChoice`] is a [`Gradient`].
pub fn to_fill(&self, existing_gradient: Option<&Gradient>) -> Fill {
match self {
Self::None => Fill::None,
Self::Solid(color) => Fill::Solid(*color),
Self::Gradient(stops) => {
let mut fill = existing_gradient.cloned().unwrap_or_default();
fill.stops = stops.clone();
Fill::Gradient(fill)
}
}
}
}
impl From<Fill> for FillChoice {
fn from(fill: Fill) -> Self {
match fill {
Fill::None => FillChoice::None,
Fill::Solid(color) => FillChoice::Solid(color),
Fill::Gradient(gradient) => FillChoice::Gradient(gradient.stops),
}
}
}
/// Enum describing the type of [Fill].
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
pub enum FillType {

View file

@ -1,5 +1,5 @@
use super::misc::CentroidType;
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
use super::style::{Fill, Stroke};
use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
@ -11,37 +11,14 @@ use glam::{DAffine2, DVec2};
use rand::{Rng, SeedableRng};
#[derive(Debug, Clone, Copy)]
pub struct SetFillNode<FillType, SolidColor, GradientType, Start, End, Transform, Positions> {
fill_type: FillType,
solid_color: SolidColor,
gradient_type: GradientType,
start: Start,
end: End,
transform: Transform,
positions: Positions,
pub struct SetFillNode<Fill> {
fill: Fill,
}
#[node_macro::node_fn(SetFillNode)]
fn set_vector_data_fill(
mut vector_data: VectorData,
fill_type: FillType,
solid_color: Option<Color>,
gradient_type: GradientType,
start: DVec2,
end: DVec2,
transform: DAffine2,
positions: Vec<(f64, Color)>,
) -> VectorData {
vector_data.style.set_fill(match fill_type {
FillType::Solid => solid_color.map_or(Fill::None, Fill::Solid),
FillType::Gradient => Fill::Gradient(Gradient {
start,
end,
transform,
positions,
gradient_type,
}),
});
fn set_vector_data_fill<T: Into<Fill>>(mut vector_data: VectorData, fill: T) -> VectorData {
vector_data.style.set_fill(fill.into());
vector_data
}