mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Refactor many usages of Color to natively store linear not gamma (#2457)
This commit is contained in:
parent
056020a56c
commit
6292dea103
36 changed files with 232 additions and 183 deletions
|
@ -91,7 +91,7 @@ impl PartialEq for ImageTexture {
|
|||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
{
|
||||
true // Unit values are always equal
|
||||
self.texture == other.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -503,7 +503,7 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops.0 {
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
|
@ -664,7 +664,7 @@ impl GraphicElementRendered for Artboard {
|
|||
if !render_params.hide_artboards {
|
||||
// Background
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("fill", format!("#{}", self.background.rgb_hex()));
|
||||
attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma()));
|
||||
if self.background.a() < 1. {
|
||||
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
|
@ -1059,13 +1059,13 @@ impl GraphicElementRendered for Option<Color> {
|
|||
render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color"));
|
||||
return;
|
||||
};
|
||||
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb());
|
||||
let color_info = format!("{:?} #{} {:?}", color, color.to_rgba_hex_srgb(), color.to_rgba8_srgb());
|
||||
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("width", "100");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("y", "40");
|
||||
attributes.push("fill", format!("#{}", color.rgb_hex()));
|
||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||
if color.a() < 1. {
|
||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
|
@ -1086,7 +1086,7 @@ impl GraphicElementRendered for Vec<Color> {
|
|||
attributes.push("height", "100");
|
||||
attributes.push("x", (index * 120).to_string());
|
||||
attributes.push("y", "40");
|
||||
attributes.push("fill", format!("#{}", color.rgb_hex()));
|
||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||
if color.a() < 1. {
|
||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use crate::Context;
|
||||
use crate::Ctx;
|
||||
use crate::vector::VectorDataTable;
|
||||
use crate::{Color, Context, Ctx};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[node_macro::node(category("Debug"))]
|
||||
fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> T {
|
||||
fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
// KEEP THIS `debug!()` - It acts as the output for the debug node itself
|
||||
debug!("{:#?}", value);
|
||||
|
|
|
@ -614,21 +614,21 @@ impl Blend<Color> for ImageFrameTable<Color> {
|
|||
}
|
||||
impl Blend<Color> for GradientStops {
|
||||
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
|
||||
let mut combined_stops = self.0.iter().map(|(position, _)| position).chain(under.0.iter().map(|(position, _)| position)).collect::<Vec<_>>();
|
||||
let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::<Vec<_>>();
|
||||
combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6);
|
||||
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
|
||||
|
||||
let stops = combined_stops
|
||||
.into_iter()
|
||||
.map(|&position| {
|
||||
let over_color = self.evalute(position);
|
||||
let under_color = under.evalute(position);
|
||||
let over_color = self.evaluate(position);
|
||||
let under_color = under.evaluate(position);
|
||||
let color = blend_fn(over_color, under_color);
|
||||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
GradientStops(stops)
|
||||
GradientStops::new(stops)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -721,7 +721,7 @@ impl Adjust<Color> for Option<Color> {
|
|||
}
|
||||
impl Adjust<Color> for GradientStops {
|
||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||
for (_pos, c) in self.0.iter_mut() {
|
||||
for (_pos, c) in self.iter_mut() {
|
||||
*c = map_fn(c);
|
||||
}
|
||||
}
|
||||
|
@ -770,7 +770,7 @@ async fn gradient_map<T: Adjust<Color>>(
|
|||
image.adjust(|color| {
|
||||
let intensity = color.luminance_srgb();
|
||||
let intensity = if reverse { 1. - intensity } else { intensity };
|
||||
gradient.evalute(intensity as f64)
|
||||
gradient.evaluate(intensity as f64)
|
||||
});
|
||||
|
||||
image
|
||||
|
|
|
@ -9,7 +9,6 @@ use spirv_std::num_traits::Euclid;
|
|||
#[cfg(feature = "serde")]
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
use std::fmt::Write;
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
|
@ -388,7 +387,7 @@ impl Color {
|
|||
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
|
||||
}
|
||||
|
||||
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`.
|
||||
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`, premultiplied by alpha.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
|
@ -402,7 +401,8 @@ impl Color {
|
|||
Color::from_rgba8_srgb(red, green, blue, 255)
|
||||
}
|
||||
|
||||
/// Return an SDR `Color` given RGBA channels from `0` to `255`.
|
||||
// TODO: Should this be premult?
|
||||
/// Return an SDR `Color` given RGBA channels from `0` to `255`, premultiplied by alpha.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
|
@ -411,16 +411,13 @@ impl Color {
|
|||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
|
||||
let alpha = alpha as f32 / 255.;
|
||||
let map_range = |int_color| int_color as f32 / 255.;
|
||||
Color {
|
||||
red: map_range(red),
|
||||
green: map_range(green),
|
||||
blue: map_range(blue),
|
||||
alpha,
|
||||
}
|
||||
.to_linear_srgb()
|
||||
.map_rgb(|channel| channel * alpha)
|
||||
|
||||
let red = map_range(red);
|
||||
let green = map_range(green);
|
||||
let blue = map_range(blue);
|
||||
let alpha = map_range(alpha);
|
||||
Color { red, green, blue, alpha }.to_linear_srgb().map_rgb(|channel| channel * alpha)
|
||||
}
|
||||
|
||||
/// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1)
|
||||
|
@ -788,56 +785,49 @@ impl Color {
|
|||
(self.red, self.green, self.blue, self.alpha)
|
||||
}
|
||||
|
||||
/// Return an 8-character RGBA hex string (without a # prefix).
|
||||
/// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in linear space.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240a261", color.rgba_hex())
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgba_hex(&self) -> String {
|
||||
pub fn to_rgba_hex_srgb(&self) -> String {
|
||||
let gamma = self.to_gamma_srgb();
|
||||
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,
|
||||
(gamma.r() * 255.) as u8,
|
||||
(gamma.g() * 255.) as u8,
|
||||
(gamma.b() * 255.) as u8,
|
||||
(gamma.a() * 255.) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1.
|
||||
///
|
||||
/// # Examples
|
||||
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240a261", color1.rgb_optional_a_hex());
|
||||
/// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0xFF).to_gamma_srgb();
|
||||
/// assert_eq!("5267fa", color2.rgb_optional_a_hex());
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgb_optional_a_hex(&self) -> String {
|
||||
let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8);
|
||||
if self.a() < 1. {
|
||||
let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8);
|
||||
}
|
||||
result
|
||||
pub fn to_rgb_hex_srgb(&self) -> String {
|
||||
self.to_gamma_srgb().to_rgb_hex_srgb_from_gamma()
|
||||
}
|
||||
|
||||
/// Return a 6-character RGB hex string (without a # prefix).
|
||||
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240a2", color.rgb_hex())
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgb_hex(&self) -> String {
|
||||
pub fn to_rgb_hex_srgb_from_gamma(&self) -> String {
|
||||
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
||||
}
|
||||
|
||||
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha.
|
||||
/// 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
|
||||
/// ```
|
||||
|
@ -908,6 +898,7 @@ impl Color {
|
|||
}
|
||||
|
||||
/// Creates a color from a 6-character RGB hex string (without a # prefix).
|
||||
///
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color = Color::from_rgb_str("7C67FA").unwrap();
|
||||
|
|
|
@ -10,20 +10,21 @@ use std::sync::{LazyLock, Mutex};
|
|||
pub mod types {
|
||||
/// 0% - 100%
|
||||
pub type Percentage = f64;
|
||||
/// -180° - 180°
|
||||
pub type Angle = f64;
|
||||
/// -100% - 100%
|
||||
pub type SignedPercentage = f64;
|
||||
/// Non negative integer, px unit
|
||||
/// -180° - 180°
|
||||
pub type Angle = f64;
|
||||
/// Non-negative integer with px unit
|
||||
pub type PixelLength = f64;
|
||||
/// Non negative
|
||||
/// Non-negative
|
||||
pub type Length = f64;
|
||||
/// 0 to 1
|
||||
pub type Fraction = f64;
|
||||
/// Unsigned integer
|
||||
pub type IntegerCount = u32;
|
||||
/// Int input with randomization button
|
||||
/// Unsigned integer to be used for random seeds
|
||||
pub type SeedValue = u32;
|
||||
/// Non Negative integer vec with px unit
|
||||
/// Non-negative integer vector2 with px unit
|
||||
pub type Resolution = glam::UVec2;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@ 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)>);
|
||||
pub struct GradientStops(Vec<(f64, Color)>);
|
||||
|
||||
impl std::hash::Hash for GradientStops {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
|
@ -35,8 +36,54 @@ impl Default for GradientStops {
|
|||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a GradientStops {
|
||||
type Item = &'a (f64, Color);
|
||||
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for GradientStops {
|
||||
type Output = (f64, Color);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops {
|
||||
pub fn evalute(&self, t: f64) -> Color {
|
||||
pub fn new(stops: Vec<(f64, Color)>) -> Self {
|
||||
let mut stops = Self(stops);
|
||||
stops.sort();
|
||||
stops
|
||||
}
|
||||
|
||||
pub fn evaluate(&self, t: f64) -> Color {
|
||||
if self.0.is_empty() {
|
||||
return Color::BLACK;
|
||||
}
|
||||
|
@ -60,9 +107,17 @@ impl GradientStops {
|
|||
Color::BLACK
|
||||
}
|
||||
|
||||
pub fn sort(&mut self) {
|
||||
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
}
|
||||
|
||||
pub fn reversed(&self) -> Self {
|
||||
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
|
||||
}
|
||||
|
||||
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
|
||||
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A gradient fill.
|
||||
|
@ -110,7 +165,7 @@ impl Gradient {
|
|||
Gradient {
|
||||
start,
|
||||
end,
|
||||
stops: GradientStops(vec![(0., start_color), (1., end_color)]),
|
||||
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
|
||||
transform,
|
||||
gradient_type,
|
||||
}
|
||||
|
@ -131,7 +186,7 @@ impl Gradient {
|
|||
(position, color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let stops = GradientStops(stops);
|
||||
let stops = GradientStops::new(stops);
|
||||
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
|
||||
|
||||
Self {
|
||||
|
@ -156,7 +211,7 @@ impl Gradient {
|
|||
if *position != 0. {
|
||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||
}
|
||||
let _ = write!(stop, r##" stop-color="#{}""##, color.rgb_hex());
|
||||
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.);
|
||||
}
|
||||
|
@ -242,7 +297,7 @@ impl Gradient {
|
|||
///
|
||||
/// Can be None, a solid [Color], or a linear/radial [Gradient].
|
||||
///
|
||||
/// In the future we'll probably also add a pattern fill.
|
||||
/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future.
|
||||
#[repr(C)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
|
||||
pub enum Fill {
|
||||
|
@ -305,7 +360,7 @@ impl Fill {
|
|||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
let mut result = format!(r##" fill="#{}""##, color.rgb_hex());
|
||||
let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
|
@ -325,6 +380,14 @@ impl Fill {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a solid color from the fill
|
||||
pub fn as_solid(&self) -> Option<Color> {
|
||||
match self {
|
||||
Self::Solid(color) => Some(*color),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
|
@ -355,18 +418,13 @@ impl From<Gradient> for Fill {
|
|||
pub enum FillChoice {
|
||||
#[default]
|
||||
None,
|
||||
/// WARNING: Color is gamma, not linear!
|
||||
Solid(Color),
|
||||
/// WARNING: Color stops are gamma, not linear!
|
||||
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)
|
||||
|
@ -575,7 +633,7 @@ impl Stroke {
|
|||
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
|
||||
|
||||
// Render the needed stroke attributes
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex());
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ where
|
|||
},
|
||||
};
|
||||
|
||||
let color = gradient.evalute(factor);
|
||||
let color = gradient.evaluate(factor);
|
||||
|
||||
if fill {
|
||||
vector_data.instance.style.set_fill(Fill::Solid(color));
|
||||
|
|
|
@ -131,7 +131,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
|
|||
let mut context = wgpu_executor::RenderContext::default();
|
||||
data.render_to_vello(&mut child, Default::default(), &mut context);
|
||||
|
||||
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost
|
||||
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(n) cost
|
||||
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
|
||||
|
||||
let mut background = Color::from_rgb8_srgb(0x22, 0x22, 0x22);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue