mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Port the color picker popover to a Rust-defined layout (#4102)
* Break out VisualColorPickersInput.svelte * Break out ColorComparisonInput.svelte and ColorPresetsInput.svelte * Add backend definitions and plumbing for the 4 new widgets * Port the ColorPicker.svelte layout and business logic to Rust * Port more ColorComparisonInput.svelte logic to Rust * Port more SpectrumInput.svelte logic to Rust * Port more frontend logic to Rust * Code review * Code review * Fix some CSS
This commit is contained in:
@@ -188,7 +188,7 @@ pub enum NodeInput {
|
||||
/// Input that is extracted from the parent scopes the node resides in. The string argument is the key.
|
||||
Scope(Cow<'static, str>),
|
||||
|
||||
/// Input that is extracted from the parent scopes the node resides in. The string argument is the key.
|
||||
/// Input that is replaced at graph compilation with introspective metadata about this node's location.
|
||||
Reflection(DocumentNodeMetadata),
|
||||
|
||||
/// A Rust source code string. Allows us to insert literal Rust code. Only used for GPU compilation.
|
||||
|
||||
@@ -220,7 +220,7 @@ impl Pixel for Luma {}
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))]
|
||||
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, BufferStruct)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable, BufferStruct)]
|
||||
pub struct Color {
|
||||
red: f32,
|
||||
green: f32,
|
||||
@@ -228,12 +228,7 @@ pub struct Color {
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl PartialEq for Color {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.red == other.red && self.green == other.green && self.blue == other.blue && self.alpha == other.alpha
|
||||
}
|
||||
}
|
||||
|
||||
// `f32` channels mean `Color` doesn't qualify for a derived `Eq`, but in practice we never store NaN here, and the renderer's `HashMap<CacheHashWrapper<Image<Color>>, _>` deduplication needs `Color: Eq` to propagate up through the wrapper.
|
||||
impl Eq for Color {}
|
||||
|
||||
impl RGB for Color {
|
||||
@@ -869,6 +864,15 @@ impl Color {
|
||||
)
|
||||
}
|
||||
|
||||
/// [`Color::BLACK`] or [`Color::WHITE`], whichever gives more legible text against this color (alpha composited over white, WCAG-style luminance threshold). Use this if this [`Color`] is in gamma space.
|
||||
pub fn contrasting_text_color_from_gamma(&self) -> Color {
|
||||
let composited = Self::WHITE.alpha_blend(Self::from_unassociated_alpha(self.r(), self.g(), self.b(), self.a()));
|
||||
let luminance = composited.to_linear_srgb().luminance_srgb();
|
||||
// WCAG-derived perceptual midpoint between black and white (~0.179)
|
||||
let threshold = (1.05_f32 * 0.05).sqrt() - 0.05;
|
||||
if luminance > threshold { Self::BLACK } else { Self::WHITE }
|
||||
}
|
||||
|
||||
/// 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 gamma space.
|
||||
#[inline(always)]
|
||||
pub fn to_rgba8(&self) -> [u8; 4] {
|
||||
|
||||
@@ -197,6 +197,47 @@ impl GradientStops {
|
||||
self.color.pop()
|
||||
}
|
||||
|
||||
/// Move the stop at `index` to a new position, re-sorting the stops by position. Returns the new index of the moved stop.
|
||||
pub fn move_stop(&mut self, index: usize, position: f64) -> usize {
|
||||
if index >= self.position.len() {
|
||||
return index;
|
||||
}
|
||||
self.position[index] = position;
|
||||
self.sort_returning_new_index(index)
|
||||
}
|
||||
|
||||
/// Insert a new stop at the given position, sampling the gradient at that position to determine the new stop's color.
|
||||
/// The new stop's midpoint is inherited from the interval it splits (or `0.5` if inserting at the very start).
|
||||
/// Returns the index where the new stop was inserted.
|
||||
pub fn insert_stop(&mut self, position: f64) -> usize {
|
||||
let color = self.evaluate(position);
|
||||
let index = self.position.iter().position(|p| *p > position).unwrap_or(self.position.len());
|
||||
let midpoint = index.checked_sub(1).and_then(|i| self.midpoint.get(i).copied()).unwrap_or(0.5);
|
||||
self.position.insert(index, position);
|
||||
self.midpoint.insert(index, midpoint);
|
||||
self.color.insert(index, color);
|
||||
index
|
||||
}
|
||||
|
||||
/// Reset the midpoint for the interval starting at `index` to its default `0.5`.
|
||||
pub fn reset_midpoint(&mut self, index: usize) {
|
||||
if let Some(midpoint) = self.midpoint.get_mut(index) {
|
||||
*midpoint = 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort the stops in place by position; returns the new index of the stop that was at `previous_index` before sorting.
|
||||
fn sort_returning_new_index(&mut self, previous_index: usize) -> usize {
|
||||
let len = self.position.len();
|
||||
let mut indices: Vec<usize> = (0..len).collect();
|
||||
indices.sort_by(|&a, &b| self.position[a].total_cmp(&self.position[b]));
|
||||
let new_index = indices.iter().position(|&i| i == previous_index).unwrap_or(previous_index);
|
||||
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();
|
||||
new_index
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.position.is_empty() {
|
||||
return Color::BLACK;
|
||||
@@ -250,6 +291,24 @@ impl GradientStops {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a CSS `linear-gradient(...)` string suitable for use as a `background-image`. Samples the midpoint curves so the rendered gradient matches Graphite's interpolation rather than browser defaults.
|
||||
pub fn to_css_linear_gradient(&self) -> String {
|
||||
if self.position.len() <= 1 {
|
||||
let hex = self.color.first().map(|c| c.to_rgba_hex_srgb_from_gamma()).unwrap_or_else(|| "000000ff".to_string());
|
||||
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
||||
}
|
||||
let pieces = self
|
||||
.interpolated_samples()
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| {
|
||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||
format!("#{} {percent}%", color.to_rgba_hex_srgb_from_gamma())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("linear-gradient(to right, {pieces})")
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -185,6 +185,18 @@ impl FillChoice {
|
||||
Some(gradient)
|
||||
}
|
||||
|
||||
/// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoice::None`]. Solid colors become a degenerate gradient between the same color so the CSS variable can always be assigned to a `background-image`.
|
||||
pub fn to_css_background_image(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Solid(color) => {
|
||||
let hex = color.to_rgba_hex_srgb_from_gamma();
|
||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||
}
|
||||
Self::Gradient(stops) => Some(stops.to_css_linear_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 {
|
||||
|
||||
Reference in New Issue
Block a user