mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-01 06:58:11 +08:00
Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs (#4149)
* Deprecate all usages of the Color struct representing gamma space values, fixing round-trip precision bugs * Code review fixes
This commit is contained in:
Generated
+2
@@ -887,6 +887,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"color",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
@@ -2216,6 +2217,7 @@ dependencies = [
|
|||||||
name = "graphite-wasm-wrapper"
|
name = "graphite-wasm-wrapper"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
"graph-craft",
|
"graph-craft",
|
||||||
"graphene-std",
|
"graphene-std",
|
||||||
"graphite-editor",
|
"graphite-editor",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub enum HsvChannel {
|
|||||||
#[impl_message(Message, ColorPicker)]
|
#[impl_message(Message, ColorPicker)]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ColorPickerMessage {
|
pub enum ColorPickerMessage {
|
||||||
/// Initialize the picker state from an external color/gradient and announce its options. Called by the frontend when a `<ColorPicker>` opens.
|
/// Initialize the picker state from an external color/gradient and announce its options. Called by the frontend when a `<ColorPicker />` opens.
|
||||||
Open { initial_value: FillChoice, allow_none: bool, disabled: bool },
|
Open { initial_value: FillChoice, allow_none: bool, disabled: bool },
|
||||||
/// Clear the picker state. Called by the frontend when the popover closes.
|
/// Clear the picker state. Called by the frontend when the popover closes.
|
||||||
Close,
|
Close,
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ use crate::messages::color_picker::color_picker_message::{HsvChannel, RgbChannel
|
|||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorPresetsInputUpdate, SpectrumInputUpdate, SpectrumMarker, VisualColorPickersInputUpdate};
|
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorPresetsInputUpdate, SpectrumInputUpdate, SpectrumMarker, VisualColorPickersInputUpdate};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use color::{AlphaColor, Srgb};
|
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::vector::style::{FillChoice, GradientStops};
|
use graphene_std::color::SRGBA8;
|
||||||
|
use graphene_std::core_types::misc::parse_css_color;
|
||||||
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI, GradientStops, GradientStopsUI};
|
||||||
|
|
||||||
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
/// Bounds for a midpoint position (relative to the interval between two adjacent gradient stops).
|
||||||
const MIN_MIDPOINT: f64 = 0.01;
|
const MIN_MIDPOINT: f64 = 0.01;
|
||||||
@@ -105,10 +106,13 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
ColorPickerMessage::SetChannelRgb { channel, value } => {
|
ColorPickerMessage::SetChannelRgb { channel, value } => {
|
||||||
let Some(strength) = value else { return };
|
let Some(strength) = value else { return };
|
||||||
let Some(current) = self.current_color() else { return };
|
let Some(current) = self.current_color() else { return };
|
||||||
|
// The RGB inputs are 0..255 sRGB display values; substitute the new channel into the gamma triple and lift back to linear for storage.
|
||||||
|
let new_gamma_channel = (strength / 255.) as f32;
|
||||||
|
let [cur_r, cur_g, cur_b, cur_a] = current.to_gamma_srgb_channels();
|
||||||
let updated = match channel {
|
let updated = match channel {
|
||||||
RgbChannel::Red => Color::from_rgbaf32_unchecked((strength / 255.) as f32, current.g(), current.b(), current.a()),
|
RgbChannel::Red => Color::from_gamma_srgb_channels(new_gamma_channel, cur_g, cur_b, cur_a),
|
||||||
RgbChannel::Green => Color::from_rgbaf32_unchecked(current.r(), (strength / 255.) as f32, current.b(), current.a()),
|
RgbChannel::Green => Color::from_gamma_srgb_channels(cur_r, new_gamma_channel, cur_b, cur_a),
|
||||||
RgbChannel::Blue => Color::from_rgbaf32_unchecked(current.r(), current.g(), (strength / 255.) as f32, current.a()),
|
RgbChannel::Blue => Color::from_gamma_srgb_channels(cur_r, cur_g, new_gamma_channel, cur_a),
|
||||||
};
|
};
|
||||||
self.adopt_color(updated);
|
self.adopt_color(updated);
|
||||||
self.emit_color(responses);
|
self.emit_color(responses);
|
||||||
@@ -150,7 +154,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
match preset {
|
match preset {
|
||||||
FillChoice::None => {
|
FillChoice::None => {
|
||||||
self.set_new_hsva(0., 0., 0., 1., true);
|
self.set_new_hsva(0., 0., 0., 1., true);
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::None });
|
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
||||||
}
|
}
|
||||||
FillChoice::Solid(color) => {
|
FillChoice::Solid(color) => {
|
||||||
self.adopt_color(color);
|
self.adopt_color(color);
|
||||||
@@ -175,7 +179,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
|
self.set_old_hsva(temp.0, temp.1, temp.2, temp.3, temp.4);
|
||||||
|
|
||||||
if self.is_none {
|
if self.is_none {
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::None });
|
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoiceUI::None });
|
||||||
} else {
|
} else {
|
||||||
self.emit_color(responses);
|
self.emit_color(responses);
|
||||||
}
|
}
|
||||||
@@ -197,6 +201,7 @@ impl MessageHandler<ColorPickerMessage, ()> for ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ColorPickerMessageHandler {
|
impl ColorPickerMessageHandler {
|
||||||
|
// The picker's internal HSV state is HSV of sRGB display values
|
||||||
fn current_color(&self) -> Option<Color> {
|
fn current_color(&self) -> Option<Color> {
|
||||||
if self.is_none {
|
if self.is_none {
|
||||||
None
|
None
|
||||||
@@ -239,7 +244,8 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
/// Set HSV state from a Color, preserving hue and saturation in degenerate cases.
|
/// Set HSV state from a Color, preserving hue and saturation in degenerate cases.
|
||||||
fn adopt_color(&mut self, color: Color) {
|
fn adopt_color(&mut self, color: Color) {
|
||||||
let [target_h, target_s, target_v] = rgb_to_hsv(color.r() as f64, color.g() as f64, color.b() as f64);
|
let [target_h, target_s, target_v, target_a] = color.to_hsva();
|
||||||
|
let (target_h, target_s, target_v, target_a) = (target_h as f64, target_s as f64, target_v as f64, target_a as f64);
|
||||||
|
|
||||||
// Preserve hue: avoid jumping from 360° (top) to 0° (bottom) and don't reset hue when the color is desaturated or fully dark.
|
// Preserve hue: avoid jumping from 360° (top) to 0° (bottom) and don't reset hue when the color is desaturated or fully dark.
|
||||||
if !(target_h == 0. && self.hue == 1.) && target_s > 0. && target_v > 0. {
|
if !(target_h == 0. && self.hue == 1.) && target_s > 0. && target_v > 0. {
|
||||||
@@ -250,7 +256,7 @@ impl ColorPickerMessageHandler {
|
|||||||
self.saturation = target_s;
|
self.saturation = target_s;
|
||||||
}
|
}
|
||||||
self.value = target_v;
|
self.value = target_v;
|
||||||
self.alpha = color.a() as f64;
|
self.alpha = target_a;
|
||||||
self.is_none = false;
|
self.is_none = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,9 +270,15 @@ impl ColorPickerMessageHandler {
|
|||||||
{
|
{
|
||||||
*stop_color = color;
|
*stop_color = color;
|
||||||
let stops = gradient.clone();
|
let stops = gradient.clone();
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::Gradient(stops) });
|
let fill_choice = FillChoice::Gradient(stops);
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoiceUI::from(&fill_choice),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged { value: FillChoice::Solid(color) });
|
let fill_choice = FillChoice::Solid(color);
|
||||||
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
|
value: FillChoiceUI::from(&fill_choice),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,8 +376,9 @@ impl ColorPickerMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.gradient = Some(gradient.clone());
|
self.gradient = Some(gradient.clone());
|
||||||
|
let fill_choice = FillChoice::Gradient(gradient);
|
||||||
responses.add(FrontendMessage::ColorPickerColorChanged {
|
responses.add(FrontendMessage::ColorPickerColorChanged {
|
||||||
value: FillChoice::Gradient(gradient),
|
value: FillChoiceUI::from(&fill_choice),
|
||||||
});
|
});
|
||||||
self.send_layouts(responses);
|
self.send_layouts(responses);
|
||||||
}
|
}
|
||||||
@@ -392,7 +405,7 @@ impl ColorPickerMessageHandler {
|
|||||||
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
// For gradient editing, the markers' handle colors mirror their gradient stop colors
|
||||||
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
let markers = gradient.iter().map(|stop| SpectrumMarker::new(stop.position, stop.midpoint, stop.color)).collect();
|
||||||
let mut row_widgets = vec![
|
let mut row_widgets = vec![
|
||||||
SpectrumInput::new(gradient.clone())
|
SpectrumInput::new(GradientStopsUI::from(gradient))
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.active_marker_index(self.active_marker_index)
|
.active_marker_index(self.active_marker_index)
|
||||||
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
.active_marker_is_midpoint(self.active_marker_is_midpoint)
|
||||||
@@ -453,7 +466,11 @@ impl ColorPickerMessageHandler {
|
|||||||
let old_color = self.old_color();
|
let old_color = self.old_color();
|
||||||
|
|
||||||
let hex_value = new_color.map(|c| color_to_hex_optional_alpha(&c)).unwrap_or_else(|| "-".to_string());
|
let hex_value = new_color.map(|c| color_to_hex_optional_alpha(&c)).unwrap_or_else(|| "-".to_string());
|
||||||
let rgb_255 = new_color.map(|c| (c.r() as f64 * 255., c.g() as f64 * 255., c.b() as f64 * 255.));
|
// RGB readouts display sRGB byte values to the user, so we convert from linear-light to gamma here before quantizing.
|
||||||
|
let rgb_255 = new_color.map(|c| {
|
||||||
|
let [r, g, b, _] = c.to_gamma_srgb_channels();
|
||||||
|
(r as f64 * 255., g as f64 * 255., b as f64 * 255.)
|
||||||
|
});
|
||||||
|
|
||||||
// Epsilon comparison since the picker round-trips through HSV
|
// Epsilon comparison since the picker round-trips through HSV
|
||||||
let differs = match (new_color, old_color) {
|
let differs = match (new_color, old_color) {
|
||||||
@@ -470,7 +487,7 @@ impl ColorPickerMessageHandler {
|
|||||||
|
|
||||||
// New/old comparison swatch with swap button
|
// New/old comparison swatch with swap button
|
||||||
groups.push(LayoutGroup::row(vec![
|
groups.push(LayoutGroup::row(vec![
|
||||||
ColorComparisonInput::new(new_color, old_color)
|
ColorComparisonInput::new(new_color.map(SRGBA8::from), old_color.map(SRGBA8::from))
|
||||||
.is_none(self.is_none)
|
.is_none(self.is_none)
|
||||||
.old_is_none(self.old_is_none)
|
.old_is_none(self.old_is_none)
|
||||||
.disabled(self.disabled)
|
.disabled(self.disabled)
|
||||||
@@ -566,7 +583,10 @@ impl ColorPickerMessageHandler {
|
|||||||
.disabled(self.disabled)
|
.disabled(self.disabled)
|
||||||
.show_none_option(self.allow_none && self.gradient.is_none())
|
.show_none_option(self.allow_none && self.gradient.is_none())
|
||||||
.on_update(|update: &ColorPresetsInputUpdate| match update {
|
.on_update(|update: &ColorPresetsInputUpdate| match update {
|
||||||
ColorPresetsInputUpdate::Preset(fill_choice) => ColorPickerMessage::PickPreset { preset: fill_choice.clone() }.into(),
|
ColorPresetsInputUpdate::Preset(fill_choice) => ColorPickerMessage::PickPreset {
|
||||||
|
preset: FillChoice::from(fill_choice),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
ColorPresetsInputUpdate::EyedropperColorCode(code) => ColorPickerMessage::EyedropperColorCode { code: code.clone() }.into(),
|
ColorPresetsInputUpdate::EyedropperColorCode(code) => ColorPickerMessage::EyedropperColorCode { code: code.clone() }.into(),
|
||||||
})
|
})
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
@@ -611,33 +631,10 @@ const SATURATION_DESCRIPTION: &str = "The vividness from grayscale to full color
|
|||||||
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
const VALUE_DESCRIPTION: &str = "The brightness from black to full color.";
|
||||||
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
const ALPHA_DESCRIPTION: &str = "The level of translucency, from transparent (0%) to opaque (100%).";
|
||||||
|
|
||||||
/// Convert an `rgb(0..1)` triple to `hsv(0..1)`. Mirrors the legacy frontend `colorToHSV`.
|
/// The popover's background color as sRGB gamma-encoded channels (the `--color-2-mildblack` design token, `#222`).
|
||||||
fn rgb_to_hsv(red: f64, green: f64, blue: f64) -> [f64; 3] {
|
/// Used by the comparison swatch's outline computation to brighten the inset border for colors close to this background.
|
||||||
let max = red.max(green).max(blue);
|
const POPOVER_BACKGROUND_GAMMA_CHANNELS: [f32; 4] = [0x22 as f32 / 255., 0x22 as f32 / 255., 0x22 as f32 / 255., 1.];
|
||||||
let min = red.min(green).min(blue);
|
/// The luminance window within which a color is considered close enough to the popover background
|
||||||
let delta = max - min;
|
|
||||||
|
|
||||||
let mut hue = if delta == 0. {
|
|
||||||
0.
|
|
||||||
} else if max == red {
|
|
||||||
((green - blue) / delta).rem_euclid(6.)
|
|
||||||
} else if max == green {
|
|
||||||
(blue - red) / delta + 2.
|
|
||||||
} else {
|
|
||||||
(red - green) / delta + 4.
|
|
||||||
};
|
|
||||||
hue = (hue * 60. + 360.).rem_euclid(360.) / 360.;
|
|
||||||
|
|
||||||
let saturation = if max == 0. { 0. } else { delta / max };
|
|
||||||
let value = max;
|
|
||||||
|
|
||||||
[hue, saturation, value]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The popover's background color (the `--color-2-mildblack` design token, `#222`). Used by the comparison swatch's
|
|
||||||
/// outline computation to brighten the inset border for colors close to this background.
|
|
||||||
const POPOVER_BACKGROUND: Color = Color::from_rgbaf32_unchecked(0x22 as f32 / 255., 0x22 as f32 / 255., 0x22 as f32 / 255., 1.);
|
|
||||||
/// The luminance window (in linear-light) within which a color is considered close enough to the popover background
|
|
||||||
/// to warrant an outline. Mirrors the `proximityRange` argument the legacy frontend passed to `contrastingOutlineFactor`.
|
/// to warrant an outline. Mirrors the `proximityRange` argument the legacy frontend passed to `contrastingOutlineFactor`.
|
||||||
const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
|
const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
|
||||||
|
|
||||||
@@ -646,61 +643,22 @@ const OUTLINE_PROXIMITY_RANGE: f64 = 0.01;
|
|||||||
fn contrasting_outline_factor(color: Option<Color>) -> f64 {
|
fn contrasting_outline_factor(color: Option<Color>) -> f64 {
|
||||||
let Some(color) = color else { return 0. };
|
let Some(color) = color else { return 0. };
|
||||||
|
|
||||||
// WCAG-style relative luminance, with alpha composited over white in gamma space
|
// WCAG-style relative luminance, with alpha composited over white in sRGB gamma space (matching the perceptual intent of `SRGBA8::contrasting_text_color`).
|
||||||
let luminance = |color: Color| {
|
let luminance_from_gamma_channels = |[r, g, b, a]: [f32; 4]| -> f64 {
|
||||||
// TODO: Remove the `.to_linear_srgb()` once we move to correctly treating `Color` as linear.
|
let inv_a = 1. - a;
|
||||||
Color::WHITE
|
Color::from_gamma_srgb_channels(inv_a + r * a, inv_a + g * a, inv_a + b * a, 1.).luminance_rec_709() as f64
|
||||||
.alpha_blend(Color::from_unassociated_alpha(color.r(), color.g(), color.b(), color.a()))
|
|
||||||
.to_linear_srgb()
|
|
||||||
.luminance_srgb() as f64
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let distance = (luminance(POPOVER_BACKGROUND) - luminance(color)).abs().max(0.);
|
let color_gamma_channels = color.to_gamma_srgb_channels();
|
||||||
|
let distance = (luminance_from_gamma_channels(POPOVER_BACKGROUND_GAMMA_CHANNELS) - luminance_from_gamma_channels(color_gamma_channels))
|
||||||
|
.abs()
|
||||||
|
.max(0.);
|
||||||
let proximity = 1. - (distance / OUTLINE_PROXIMITY_RANGE).min(1.);
|
let proximity = 1. - (distance / OUTLINE_PROXIMITY_RANGE).min(1.);
|
||||||
let [_, saturation, _] = rgb_to_hsv(color.r() as f64, color.g() as f64, color.b() as f64);
|
let [_, saturation, _, _] = color.to_hsva();
|
||||||
proximity * (1. - saturation)
|
proximity * (1. - saturation as f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a Color as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
|
/// Format a linear `Color` as a `#`-prefixed hex string, including the alpha component only if it's not fully opaque.
|
||||||
fn color_to_hex_optional_alpha(color: &Color) -> String {
|
fn color_to_hex_optional_alpha(color: &Color) -> String {
|
||||||
format!(
|
SRGBA8::from(*color).to_css_hex()
|
||||||
"#{}",
|
|
||||||
if color.a() >= 1. {
|
|
||||||
color.to_rgb_hex_srgb_from_gamma()
|
|
||||||
} else {
|
|
||||||
color.to_rgba_hex_srgb_from_gamma()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a CSS color string (named color, hex, `rgb(...)`, etc.) into a `Color` using the `color` crate's CSS Color 4 parser.
|
|
||||||
/// Tries the input as-is first (catches CSS named colors like `red`, `rgb(...)`, and well-formed hex like `#abcdef`), then falls back to treating the input as bare hex with length-based expansion to a CSS-parseable form:
|
|
||||||
/// - 1 char `f` → `#fff` (CSS 3-char shorthand)
|
|
||||||
/// - 2 char `ab` → `#ababab` (repeated to 6 chars)
|
|
||||||
/// - 4 char `abcd` → `#00abcd` (left-padded with `00`)
|
|
||||||
/// - 5 char `abcde` → `#0abcde` (left-padded with `0`)
|
|
||||||
/// - 3, 6, 8 char inputs are passed through with a `#` prefix.
|
|
||||||
fn parse_css_color(input: &str) -> Option<Color> {
|
|
||||||
let trimmed = input.trim();
|
|
||||||
|
|
||||||
let parsed = color::parse_color(trimmed).ok().or_else(|| {
|
|
||||||
let bare = trimmed.strip_prefix('#').unwrap_or(trimmed);
|
|
||||||
if bare.is_empty() || !bare.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let expanded = match bare.len() {
|
|
||||||
1 => bare.repeat(3),
|
|
||||||
2 => bare.repeat(3),
|
|
||||||
4 => format!("00{bare}"),
|
|
||||||
5 => format!("0{bare}"),
|
|
||||||
_ => bare.to_string(),
|
|
||||||
};
|
|
||||||
let candidate = format!("#{expanded}");
|
|
||||||
// Avoid retrying the exact same string we just failed to parse.
|
|
||||||
(candidate != trimmed).then(|| color::parse_color(&candidate).ok()).flatten()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let srgb: AlphaColor<Srgb> = parsed.to_alpha_color();
|
|
||||||
let [red, green, blue, alpha] = srgb.components;
|
|
||||||
Color::from_rgbaf32(red, green, blue, alpha)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
use crate::messages::tool::tool_messages::eyedropper_tool::PrimarySecondary;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::raster::color::Color;
|
|
||||||
use graphene_std::text::Font;
|
use graphene_std::text::Font;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoiceUI;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
@@ -167,16 +167,16 @@ pub enum FrontendMessage {
|
|||||||
document_id: DocumentId,
|
document_id: DocumentId,
|
||||||
},
|
},
|
||||||
UpdateGradientStopColorPickerPosition {
|
UpdateGradientStopColorPickerPosition {
|
||||||
color: Color, // TODO: Color (without `none`) -> Color (with `none`)
|
color: SRGBA8, // TODO: Color (without `none`) -> Color (with `none`)
|
||||||
position: (f64, f64),
|
position: (f64, f64),
|
||||||
},
|
},
|
||||||
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker>` forwards this as its `colorOrGradient` event.
|
/// The Rust color picker handler picked a new color/gradient. The frontend `<ColorPicker />` forwards this as its `colorOrGradient` event.
|
||||||
ColorPickerColorChanged {
|
ColorPickerColorChanged {
|
||||||
value: FillChoice,
|
value: FillChoiceUI,
|
||||||
},
|
},
|
||||||
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker>` forwards this as its `startHistoryTransaction` event.
|
/// The Rust color picker handler is starting an undo transaction. The frontend `<ColorPicker />` forwards this as its `startHistoryTransaction` event.
|
||||||
ColorPickerStartHistoryTransaction,
|
ColorPickerStartHistoryTransaction,
|
||||||
/// The Rust color picker handler is committing the in-flight undo transaction. The frontend `<ColorPicker>` forwards this as its `commitHistoryTransaction` event.
|
/// The Rust color picker handler is committing the in-flight undo transaction. The frontend `<ColorPicker />` forwards this as its `commitHistoryTransaction` event.
|
||||||
ColorPickerCommitHistoryTransaction,
|
ColorPickerCommitHistoryTransaction,
|
||||||
UpdateImportsExports {
|
UpdateImportsExports {
|
||||||
/// If the primary import is not visible, then it is None.
|
/// If the primary import is not visible, then it is None.
|
||||||
@@ -241,7 +241,7 @@ pub enum FrontendMessage {
|
|||||||
svg: String,
|
svg: String,
|
||||||
},
|
},
|
||||||
UpdateImageData {
|
UpdateImageData {
|
||||||
image_data: Vec<(u64, Image<Color>)>,
|
image_data: Vec<(u64, Image<SRGBA8>)>,
|
||||||
},
|
},
|
||||||
UpdateDocumentLayerDetails {
|
UpdateDocumentLayerDetails {
|
||||||
data: LayerPanelEntry,
|
data: LayerPanelEntry,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub struct DocumentInfo {
|
|||||||
pub is_saved: bool,
|
pub is_saved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints, from_wasm_abi))]
|
||||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct PersistedState {
|
pub struct PersistedState {
|
||||||
pub documents: Vec<DocumentInfo>,
|
pub documents: Vec<DocumentInfo>,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::color::SRGBA8;
|
||||||
|
use graphene_std::vector::style::FillChoiceUI;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -179,11 +180,11 @@ impl LayoutMessageHandler {
|
|||||||
let callback_message = match action {
|
let callback_message = match action {
|
||||||
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
WidgetValueAction::Commit => (color_button.on_commit.callback)(&()),
|
||||||
WidgetValueAction::Update => {
|
WidgetValueAction::Update => {
|
||||||
let Ok(fill_choice) = serde_json::from_value::<FillChoice>(value) else {
|
let Ok(fill_choice_ui) = serde_json::from_value::<FillChoiceUI>(value) else {
|
||||||
warn!("ColorInput update was not able to be parsed as FillChoice: {color_button:?}");
|
warn!("ColorInput update was not able to be parsed as FillChoiceUI: {color_button:?}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
color_button.value = fill_choice;
|
color_button.value = fill_choice_ui;
|
||||||
(color_button.on_update.callback)(color_button)
|
(color_button.on_update.callback)(color_button)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -496,25 +497,14 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
|||||||
}
|
}
|
||||||
Widget::SpectrumInput(spectrum_input) => {
|
Widget::SpectrumInput(spectrum_input) => {
|
||||||
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
|
spectrum_input.track_css = spectrum_input.track.to_css_linear_gradient();
|
||||||
spectrum_input.track_start_css = spectrum_input
|
spectrum_input.track_start_css = spectrum_input.track.color.first().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
||||||
.track
|
spectrum_input.track_end_css = spectrum_input.track.color.last().map(|color| color.to_css_hex()).unwrap_or_else(|| "black".to_string());
|
||||||
.color
|
|
||||||
.first()
|
|
||||||
.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma()))
|
|
||||||
.unwrap_or_else(|| "black".to_string());
|
|
||||||
spectrum_input.track_end_css = spectrum_input
|
|
||||||
.track
|
|
||||||
.color
|
|
||||||
.last()
|
|
||||||
.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma()))
|
|
||||||
.unwrap_or_else(|| "black".to_string());
|
|
||||||
}
|
}
|
||||||
Widget::ColorComparisonInput(comparison) => {
|
Widget::ColorComparisonInput(comparison) => {
|
||||||
use graphene_std::Color;
|
let contrasting = |color: Option<SRGBA8>| color.map_or(SRGBA8::BLACK, |color| color.contrasting_text_color()).to_css_hex();
|
||||||
let contrasting = |color: Option<Color>| format!("#{}", color.map_or(Color::BLACK, |color| color.contrasting_text_color_from_gamma()).to_rgba_hex_srgb_from_gamma());
|
comparison.new_color_css = comparison.new_color.map(|color| color.to_css_hex()).unwrap_or_default();
|
||||||
comparison.new_color_css = comparison.new_color.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma())).unwrap_or_default();
|
|
||||||
comparison.new_color_contrasting = contrasting(comparison.new_color);
|
comparison.new_color_contrasting = contrasting(comparison.new_color);
|
||||||
comparison.old_color_css = comparison.old_color.map(|color| format!("#{}", color.to_rgba_hex_srgb_from_gamma())).unwrap_or_default();
|
comparison.old_color_css = comparison.old_color.map(|color| color.to_css_hex()).unwrap_or_default();
|
||||||
comparison.old_color_contrasting = contrasting(comparison.old_color);
|
comparison.old_color_contrasting = contrasting(comparison.old_color);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl core::fmt::Display for WidgetId {
|
|||||||
|
|
||||||
macro_rules! define_layout_target {
|
macro_rules! define_layout_target {
|
||||||
($($(#[$attr:meta])* $variant:ident),* $(,)?) => {
|
($($(#[$attr:meta])* $variant:ident),* $(,)?) => {
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum LayoutTarget {
|
pub enum LayoutTarget {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
|
||||||
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
|
use crate::messages::tool::tool_messages::tool_prelude::WidgetCallback;
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoiceUI;
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -187,11 +187,10 @@ pub struct ImageButton {
|
|||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
pub struct ColorInput {
|
pub struct ColorInput {
|
||||||
// Content
|
// Content
|
||||||
/// WARNING: The colors are gamma, not linear!
|
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub value: FillChoice,
|
pub value: FillChoiceUI,
|
||||||
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
||||||
/// `None` when `value` is `FillChoice::None`, in which case the frontend uses its "none" fallback styling.
|
/// `None` when `value` is `FillChoiceUI::None`, in which case the frontend uses its "none" fallback styling.
|
||||||
#[serde(rename = "chosenGradient")]
|
#[serde(rename = "chosenGradient")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
pub chosen_gradient: Option<String>,
|
pub chosen_gradient: Option<String>,
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use crate::messages::layout::utility_types::widget_prelude::*;
|
|||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
|
||||||
use derivative::*;
|
use derivative::*;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::transform::ReferencePoint;
|
use graphene_std::transform::ReferencePoint;
|
||||||
use graphene_std::vector::style::{FillChoice, GradientStops};
|
use graphene_std::vector::style::{FillChoiceUI, GradientStopsUI};
|
||||||
use graphite_proc_macros::WidgetBuilder;
|
use graphite_proc_macros::WidgetBuilder;
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -349,9 +350,9 @@ pub struct RadioEntryData {
|
|||||||
pub struct WorkingColorsInput {
|
pub struct WorkingColorsInput {
|
||||||
// Content
|
// Content
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub primary: Color,
|
pub primary: SRGBA8,
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub secondary: Color,
|
pub secondary: SRGBA8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
@@ -484,10 +485,10 @@ pub struct ColorComparisonInput {
|
|||||||
// Content
|
// Content
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
#[serde(rename = "newColor")]
|
#[serde(rename = "newColor")]
|
||||||
pub new_color: Option<Color>,
|
pub new_color: Option<SRGBA8>,
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
#[serde(rename = "oldColor")]
|
#[serde(rename = "oldColor")]
|
||||||
pub old_color: Option<Color>,
|
pub old_color: Option<SRGBA8>,
|
||||||
#[serde(rename = "isNone")]
|
#[serde(rename = "isNone")]
|
||||||
pub is_none: bool,
|
pub is_none: bool,
|
||||||
#[serde(rename = "oldIsNone")]
|
#[serde(rename = "oldIsNone")]
|
||||||
@@ -544,7 +545,7 @@ pub struct ColorPresetsInput {
|
|||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ColorPresetsInputUpdate {
|
pub enum ColorPresetsInputUpdate {
|
||||||
Preset(FillChoice),
|
Preset(FillChoiceUI),
|
||||||
EyedropperColorCode(String),
|
EyedropperColorCode(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +556,7 @@ pub struct SpectrumInput {
|
|||||||
// Content
|
// Content
|
||||||
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
/// The colored gradient drawn behind the markers (display-only, caller-owned).
|
||||||
#[widget_builder(constructor)]
|
#[widget_builder(constructor)]
|
||||||
pub track: GradientStops,
|
pub track: GradientStopsUI,
|
||||||
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
/// CSS `linear-gradient(...)` string for the track strip's `background-image`. Auto-populated from `track` at layout-send time.
|
||||||
#[serde(rename = "trackCSS")]
|
#[serde(rename = "trackCSS")]
|
||||||
#[widget_builder(skip)]
|
#[widget_builder(skip)]
|
||||||
@@ -607,14 +608,14 @@ pub struct SpectrumMarker {
|
|||||||
position: f64,
|
position: f64,
|
||||||
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true. The last marker's value is ignored.
|
/// Position (0..1) of the midpoint between this marker and the next, used only if `show_midpoints` is true. The last marker's value is ignored.
|
||||||
midpoint: f64,
|
midpoint: f64,
|
||||||
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a [`Color`] (gamma space).
|
/// CSS color string for the marker handle's fill. Set via `SpectrumMarker::new` from a linear [`Color`].
|
||||||
#[serde(rename = "handleColorCSS")]
|
#[serde(rename = "handleColorCSS")]
|
||||||
handle_color_css: String,
|
handle_color_css: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpectrumMarker {
|
impl SpectrumMarker {
|
||||||
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
pub fn new(position: f64, midpoint: f64, handle_color: Color) -> Self {
|
||||||
let handle_color_css = format!("#{}", handle_color.to_rgba_hex_srgb_from_gamma());
|
let handle_color_css = SRGBA8::from(handle_color).to_css_hex();
|
||||||
Self { position, midpoint, handle_color_css }
|
Self { position, midpoint, handle_color_css }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ use crate::messages::tool::tool_messages::tool_prelude::*;
|
|||||||
use glam::{Affine2, DAffine2, Vec2};
|
use glam::{Affine2, DAffine2, Vec2};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::blending::BlendMode;
|
use graphene_std::blending::BlendMode;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::gradient::GradientStops;
|
use graphene_std::gradient::GradientStops;
|
||||||
use graphene_std::list::List;
|
use graphene_std::list::List;
|
||||||
use graphene_std::memo::IORecord;
|
use graphene_std::memo::IORecord;
|
||||||
use graphene_std::raster_types::{CPU, GPU, Raster};
|
use graphene_std::raster_types::{CPU, GPU, Raster};
|
||||||
use graphene_std::vector::Vector;
|
use graphene_std::vector::Vector;
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientType};
|
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientType};
|
||||||
use graphene_std::{Artboard, Color, Context, Graphic};
|
use graphene_std::{Artboard, Color, Context, Graphic};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -385,11 +386,15 @@ impl TableItemLayout for Vector {
|
|||||||
match self.style.fill.clone() {
|
match self.style.fill.clone() {
|
||||||
Fill::None => table_rows.push(vec![
|
Fill::None => table_rows.push(vec![
|
||||||
TextLabel::new("Fill").narrow(true).widget_instance(),
|
TextLabel::new("Fill").narrow(true).widget_instance(),
|
||||||
ColorInput::new(FillChoice::None).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
|
ColorInput::new(FillChoiceUI::None)
|
||||||
|
.disabled(true)
|
||||||
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
|
.narrow(true)
|
||||||
|
.widget_instance(),
|
||||||
]),
|
]),
|
||||||
Fill::Solid(color) => table_rows.push(vec![
|
Fill::Solid(color) => table_rows.push(vec![
|
||||||
TextLabel::new("Fill").narrow(true).widget_instance(),
|
TextLabel::new("Fill").narrow(true).widget_instance(),
|
||||||
ColorInput::new(FillChoice::Solid(color))
|
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(color)))
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.menu_direction(Some(MenuDirection::Top))
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
@@ -398,7 +403,7 @@ impl TableItemLayout for Vector {
|
|||||||
Fill::Gradient(gradient) => {
|
Fill::Gradient(gradient) => {
|
||||||
table_rows.push(vec![
|
table_rows.push(vec![
|
||||||
TextLabel::new("Fill").narrow(true).widget_instance(),
|
TextLabel::new("Fill").narrow(true).widget_instance(),
|
||||||
ColorInput::new(FillChoice::Gradient(gradient.stops))
|
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(gradient.stops)))
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.menu_direction(Some(MenuDirection::Top))
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
@@ -423,7 +428,11 @@ impl TableItemLayout for Vector {
|
|||||||
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
|
let color = if let Some(color) = stroke.color { FillChoice::Solid(color) } else { FillChoice::None };
|
||||||
table_rows.push(vec![
|
table_rows.push(vec![
|
||||||
TextLabel::new("Stroke").narrow(true).widget_instance(),
|
TextLabel::new("Stroke").narrow(true).widget_instance(),
|
||||||
ColorInput::new(color).disabled(true).menu_direction(Some(MenuDirection::Top)).narrow(true).widget_instance(),
|
ColorInput::new(FillChoiceUI::from(&color))
|
||||||
|
.disabled(true)
|
||||||
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
|
.narrow(true)
|
||||||
|
.widget_instance(),
|
||||||
]);
|
]);
|
||||||
table_rows.push(vec![
|
table_rows.push(vec![
|
||||||
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
|
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
|
||||||
@@ -561,10 +570,10 @@ impl TableItemLayout for Color {
|
|||||||
"Color"
|
"Color"
|
||||||
}
|
}
|
||||||
fn identifier(&self) -> String {
|
fn identifier(&self) -> String {
|
||||||
format!("Color (#{})", self.to_gamma_srgb().to_rgba_hex_srgb())
|
format!("Color (#{})", SRGBA8::from(*self).to_rgba_hex())
|
||||||
}
|
}
|
||||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||||
ColorInput::new(FillChoice::Solid(*self))
|
ColorInput::new(FillChoiceUI::from(&FillChoice::Solid(*self)))
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.menu_direction(Some(MenuDirection::Top))
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
@@ -584,7 +593,7 @@ impl TableItemLayout for GradientStops {
|
|||||||
format!("Gradient ({} stops)", self.len())
|
format!("Gradient ({} stops)", self.len())
|
||||||
}
|
}
|
||||||
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
fn value_widget(&self, _target: PathStep, _data: &LayoutData) -> WidgetInstance {
|
||||||
ColorInput::new(FillChoice::Gradient(self.clone()))
|
ColorInput::new(FillChoiceUI::from(&FillChoice::Gradient(self.clone())))
|
||||||
.menu_direction(Some(MenuDirection::Top))
|
.menu_direction(Some(MenuDirection::Top))
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use graphene_std::text_nodes::StringCapitalization;
|
|||||||
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, GradientSpreadMethod, GradientStops, GradientStopsUI, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
|
||||||
|
|
||||||
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
||||||
@@ -1164,19 +1164,19 @@ pub fn color_widget(parameter_widgets_info: ParameterWidgetsInfo, color_button:
|
|||||||
match &**tagged_value {
|
match &**tagged_value {
|
||||||
TaggedValue::Color(color) => widgets.push(
|
TaggedValue::Color(color) => widgets.push(
|
||||||
color_button
|
color_button
|
||||||
.value(match color {
|
.value(FillChoiceUI::from(&match color {
|
||||||
Some(color) => FillChoice::Solid(*color),
|
Some(color) => FillChoice::Solid(*color),
|
||||||
None => FillChoice::None,
|
None => FillChoice::None,
|
||||||
})
|
}))
|
||||||
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid()), node_id, index))
|
.on_update(update_value(|input: &ColorInput| TaggedValue::Color(input.value.as_solid().map(Color::from)), node_id, index))
|
||||||
.on_commit(commit_value)
|
.on_commit(commit_value)
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
),
|
),
|
||||||
TaggedValue::Gradient(stops) => widgets.push(
|
TaggedValue::Gradient(stops) => widgets.push(
|
||||||
color_button
|
color_button
|
||||||
.value(FillChoice::Gradient(stops.clone()))
|
.value(FillChoiceUI::from(&FillChoice::Gradient(stops.clone())))
|
||||||
.on_update(update_value(
|
.on_update(update_value(
|
||||||
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().cloned().unwrap_or_default()),
|
|input: &ColorInput| TaggedValue::Gradient(input.value.as_gradient().map(GradientStops::from).unwrap_or_default()),
|
||||||
node_id,
|
node_id,
|
||||||
index,
|
index,
|
||||||
))
|
))
|
||||||
@@ -1417,7 +1417,7 @@ fn build_shared_spectrum_section(node_id: NodeId, context: &mut NodePropertiesCo
|
|||||||
|
|
||||||
// Build the shared spectrum widget (placed on the first non-exposed row)
|
// Build the shared spectrum widget (placed on the first non-exposed row)
|
||||||
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
let spectrum_widget = (!spectrum_markers.is_empty()).then(|| {
|
||||||
SpectrumInput::new(bw_track())
|
SpectrumInput::new(GradientStopsUI::from(&bw_track()))
|
||||||
.markers(spectrum_markers)
|
.markers(spectrum_markers)
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
.allow_insert(false)
|
.allow_insert(false)
|
||||||
@@ -1591,7 +1591,7 @@ fn spectrum_slider_row(
|
|||||||
|
|
||||||
let position_to_value = move |position: f64| value_min + position * value_range;
|
let position_to_value = move |position: f64| value_min + position * value_range;
|
||||||
row.push(
|
row.push(
|
||||||
SpectrumInput::new(track)
|
SpectrumInput::new(GradientStopsUI::from(&track))
|
||||||
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
.markers(vec![SpectrumMarker::new(position, 0.5, handle_color)])
|
||||||
.show_midpoints(false)
|
.show_midpoints(false)
|
||||||
.allow_insert(false)
|
.allow_insert(false)
|
||||||
@@ -2482,7 +2482,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
widgets_first_row.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
widgets_first_row.push(
|
widgets_first_row.push(
|
||||||
ColorInput::default()
|
ColorInput::default()
|
||||||
.value(fill.clone().into())
|
.value(FillChoiceUI::from(&FillChoice::from(fill.clone())))
|
||||||
.on_update(move |x: &ColorInput| Message::Batched {
|
.on_update(move |x: &ColorInput| Message::Batched {
|
||||||
messages: Box::new([
|
messages: Box::new([
|
||||||
match &fill2 {
|
match &fill2 {
|
||||||
@@ -2508,7 +2508,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||||||
NodeGraphMessage::SetInputValue {
|
NodeGraphMessage::SetInputValue {
|
||||||
node_id,
|
node_id,
|
||||||
input_index: FillInput::<Color>::INDEX,
|
input_index: FillInput::<Color>::INDEX,
|
||||||
value: TaggedValue::Fill(x.value.to_fill(fill2.as_gradient())),
|
value: TaggedValue::Fill(FillChoice::from(&x.value).to_fill(fill2.as_gradient())),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
|
|||||||
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
|
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::FillChoiceUI;
|
||||||
|
|
||||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||||
let origin = document.snapping_state.grid.origin;
|
let origin = document.snapping_state.grid.origin;
|
||||||
@@ -274,12 +274,12 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
]);
|
]);
|
||||||
color_widgets.push(
|
color_widgets.push(
|
||||||
ColorInput::new(FillChoice::Solid(Color::from_hex_str(&grid.color).unwrap_or(Color::BLACK)))
|
ColorInput::new(FillChoiceUI::Solid(SRGBA8::from_hex_str(&grid.color).unwrap_or(SRGBA8::BLACK)))
|
||||||
.tooltip_label("Grid Display Color")
|
.tooltip_label("Grid Display Color")
|
||||||
.allow_none(false)
|
.allow_none(false)
|
||||||
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
|
.on_update(update_val::<ColorInput, _>(grid, |grid, color| {
|
||||||
if let Some(color) = color.value.as_solid() {
|
if let Some(srgba) = color.value.as_solid() {
|
||||||
grid.color = format!("#{}", color.to_rgba_hex_srgb_from_gamma());
|
grid.color = srgba.to_css_hex();
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.widget_instance(),
|
.widget_instance(),
|
||||||
|
|||||||
@@ -999,13 +999,10 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add default fill and stroke to the layer
|
// Add default fill and stroke to the layer
|
||||||
let fill_color = Color::WHITE;
|
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE);
|
||||||
let stroke_color = Color::BLACK;
|
|
||||||
|
|
||||||
let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb());
|
|
||||||
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
||||||
|
|
||||||
let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH);
|
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH);
|
||||||
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
||||||
|
|
||||||
// Create new point ids and add those into the existing Vector path
|
// Create new point ids and add those into the existing Vector path
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ impl FontCatalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct FontCatalogFamily {
|
pub struct FontCatalogFamily {
|
||||||
/// The font family name.
|
/// The font family name.
|
||||||
@@ -55,6 +56,7 @@ impl FontCatalogFamily {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct FontCatalogStyle {
|
pub struct FontCatalogStyle {
|
||||||
pub weight: u32,
|
pub weight: u32,
|
||||||
@@ -88,7 +90,7 @@ impl FontCatalogStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum PanelType {
|
pub enum PanelType {
|
||||||
Welcome,
|
Welcome,
|
||||||
@@ -124,7 +126,7 @@ impl PanelType {
|
|||||||
pub struct PanelGroupId(pub u64);
|
pub struct PanelGroupId(pub u64);
|
||||||
|
|
||||||
/// Which edge of a panel group to split on when docking a dragged panel.
|
/// Which edge of a panel group to split on when docking a dragged panel.
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum DockingSplitDirection {
|
pub enum DockingSplitDirection {
|
||||||
Left,
|
Left,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::messages::prelude::*;
|
|||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::utility_types::DocumentToolData;
|
use crate::messages::tool::utility_types::DocumentToolData;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::vector::style::{FillChoice, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
|
||||||
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
/// Color selector widgets seen in [`LayoutTarget::ToolOptions`] bar.
|
||||||
pub struct ToolColorOptions {
|
pub struct ToolColorOptions {
|
||||||
@@ -47,13 +47,12 @@ impl ToolColorOptions {
|
|||||||
self.enabled == Some(true)
|
self.enabled == Some(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The active solid color in linear sRGB, suitable for storing in a working color or downstream rendering input.
|
/// The active solid color, suitable for storing in a working color or downstream rendering input.
|
||||||
/// `fill_choice` is stored in gamma space (per [`FillChoice`]'s contract), so this method converts to linear before returning.
|
|
||||||
pub fn active_color(&self) -> Option<Color> {
|
pub fn active_color(&self) -> Option<Color> {
|
||||||
if !self.is_active() {
|
if !self.is_active() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(self.fill_choice.as_ref()?.as_solid()?.to_linear_srgb())
|
self.fill_choice.as_ref()?.as_solid()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
||||||
@@ -86,7 +85,8 @@ impl ToolColorOptions {
|
|||||||
// In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position.
|
// In the mixed state (`fill_choice` is `None`) the dash overlay covers the swatch, so the underlying widget value just drives the picker's initial position.
|
||||||
// `FillChoice::None` gives it a neutral starting point.
|
// `FillChoice::None` gives it a neutral starting point.
|
||||||
let mixed_color = self.fill_choice.is_none();
|
let mixed_color = self.fill_choice.is_none();
|
||||||
let widget_value = self.fill_choice.clone().unwrap_or(FillChoice::None);
|
// Convert the internal linear-light `FillChoice` to the JS-boundary `FillChoiceUI` (with `SRGBA8` colors) for the widget value.
|
||||||
|
let widget_value = FillChoiceUI::from(self.fill_choice.as_ref().unwrap_or(&FillChoice::None));
|
||||||
let mixed_enabled = self.enabled.is_none();
|
let mixed_enabled = self.enabled.is_none();
|
||||||
// In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash.
|
// In the mixed-enabled state the underlying `checked` value is hidden behind the indeterminate dash.
|
||||||
// The frontend's click handler sends `true` when the user resolves the mixed state by clicking.
|
// The frontend's click handler sends `true` when the user resolves the mixed state by clicking.
|
||||||
@@ -191,10 +191,9 @@ impl DrawingToolState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `FillChoice::Solid` from a linear-space color, applying gamma conversion to display sRGB.
|
/// Builds a `FillChoice::Solid` from a color.
|
||||||
/// Common helper used throughout the color-syncing code where working colors (linear) flow into swatches that store gamma-encoded colors.
|
pub fn solid(color: Color) -> FillChoice {
|
||||||
pub fn solid_gamma(color: Color) -> FillChoice {
|
FillChoice::Solid(color)
|
||||||
FillChoice::Solid(color.to_gamma_srgb())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fill working color (the source for the fill swatch when nothing is selected).
|
/// The fill working color (the source for the fill swatch when nothing is selected).
|
||||||
@@ -219,8 +218,8 @@ pub fn sync_color_options(
|
|||||||
document: &DocumentMessageHandler,
|
document: &DocumentMessageHandler,
|
||||||
selection_changed: bool,
|
selection_changed: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let fill_fallback = solid_gamma(fill_working_color(global, drawing.colors_swapped));
|
let fill_fallback = solid(fill_working_color(global, drawing.colors_swapped));
|
||||||
let stroke_fallback = solid_gamma(stroke_working_color(global, drawing.colors_swapped));
|
let stroke_fallback = solid(stroke_working_color(global, drawing.colors_swapped));
|
||||||
|
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
|
|
||||||
@@ -364,7 +363,7 @@ fn sync_stroke_options(drawing: &mut DrawingToolState, document: &DocumentMessag
|
|||||||
|
|
||||||
/// Same as [`sync_color_options`] but for tools that only have a fill option (e.g., text). The fill follows the given working color when nothing is selected.
|
/// Same as [`sync_color_options`] but for tools that only have a fill option (e.g., text). The fill follows the given working color when nothing is selected.
|
||||||
pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, fill_color: Color, document: &DocumentMessageHandler, selection_changed: bool) -> bool {
|
pub fn sync_fill_only(fill: &mut ToolColorOptions, natural_fill_enabled: bool, fill_color: Color, document: &DocumentMessageHandler, selection_changed: bool) -> bool {
|
||||||
let fill_fallback = solid_gamma(fill_color);
|
let fill_fallback = solid(fill_color);
|
||||||
|
|
||||||
let new_fill = if let Some(state) = graph_modification_utils::selected_fill_state(document) {
|
let new_fill = if let Some(state) = graph_modification_utils::selected_fill_state(document) {
|
||||||
let active = state.enabled == Some(true);
|
let active = state.enabled == Some(true);
|
||||||
@@ -417,11 +416,7 @@ pub fn apply_fill_only_color_pick(fill: &mut ToolColorOptions, fill_choice: Fill
|
|||||||
}
|
}
|
||||||
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
||||||
} else if let FillChoice::Solid(color) = fill_choice {
|
} else if let FillChoice::Solid(color) = fill_choice {
|
||||||
// Swatch is gamma; working colors are linear.
|
responses.add(ToolMessage::SelectWorkingColor { color, primary: slot_is_primary });
|
||||||
responses.add(ToolMessage::SelectWorkingColor {
|
|
||||||
color: color.to_linear_srgb(),
|
|
||||||
primary: slot_is_primary,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,9 +431,8 @@ pub fn apply_stroke_color_pick(drawing: &mut DrawingToolState, color: Option<Col
|
|||||||
}
|
}
|
||||||
graph_modification_utils::set_stroke_color_for_selected_layers(color, drawing.effective_line_weight(), document, responses);
|
graph_modification_utils::set_stroke_color_for_selected_layers(color, drawing.effective_line_weight(), document, responses);
|
||||||
} else if let Some(color) = color {
|
} else if let Some(color) = color {
|
||||||
// Swatch is gamma; working colors are linear.
|
|
||||||
responses.add(ToolMessage::SelectWorkingColor {
|
responses.add(ToolMessage::SelectWorkingColor {
|
||||||
color: color.to_linear_srgb(),
|
color,
|
||||||
primary: !drawing.colors_swapped,
|
primary: !drawing.colors_swapped,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -459,14 +453,14 @@ pub fn apply_fill_only_enabled(fill: &mut ToolColorOptions, enabled: bool, worki
|
|||||||
// Mixed re-tick has no per-layer color to restore; fall back to the working color and keep tracking it.
|
// Mixed re-tick has no per-layer color to restore; fall back to the working color and keep tracking it.
|
||||||
let fill_choice = fill.fill_choice.clone().unwrap_or_else(|| {
|
let fill_choice = fill.fill_choice.clone().unwrap_or_else(|| {
|
||||||
fill.tracks_working_color = true;
|
fill.tracks_working_color = true;
|
||||||
solid_gamma(working_color)
|
solid(working_color)
|
||||||
});
|
});
|
||||||
fill.fill_choice = Some(fill_choice.clone());
|
fill.fill_choice = Some(fill_choice.clone());
|
||||||
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
graph_modification_utils::set_fill_for_selected_layers(fill_choice, document, responses);
|
||||||
} else {
|
} else {
|
||||||
// Unticking from mixed: capture the working color as the saved value so the swatch keeps following the link.
|
// Unticking from mixed: capture the working color as the saved value so the swatch keeps following the link.
|
||||||
if fill.fill_choice.is_none() {
|
if fill.fill_choice.is_none() {
|
||||||
fill.fill_choice = Some(solid_gamma(working_color));
|
fill.fill_choice = Some(solid(working_color));
|
||||||
fill.tracks_working_color = true;
|
fill.tracks_working_color = true;
|
||||||
}
|
}
|
||||||
graph_modification_utils::remove_fill_for_selected_layers(document, responses);
|
graph_modification_utils::remove_fill_for_selected_layers(document, responses);
|
||||||
@@ -482,13 +476,13 @@ pub fn apply_stroke_enabled(drawing: &mut DrawingToolState, enabled: bool, globa
|
|||||||
if enabled {
|
if enabled {
|
||||||
let stroke_choice = drawing.stroke.fill_choice.clone().unwrap_or_else(|| {
|
let stroke_choice = drawing.stroke.fill_choice.clone().unwrap_or_else(|| {
|
||||||
drawing.stroke.tracks_working_color = true;
|
drawing.stroke.tracks_working_color = true;
|
||||||
solid_gamma(stroke_working_color(global, drawing.colors_swapped))
|
solid(stroke_working_color(global, drawing.colors_swapped))
|
||||||
});
|
});
|
||||||
drawing.stroke.fill_choice = Some(stroke_choice.clone());
|
drawing.stroke.fill_choice = Some(stroke_choice.clone());
|
||||||
graph_modification_utils::set_stroke_color_for_selected_layers(stroke_choice.as_solid(), drawing.effective_line_weight(), document, responses);
|
graph_modification_utils::set_stroke_color_for_selected_layers(stroke_choice.as_solid(), drawing.effective_line_weight(), document, responses);
|
||||||
} else {
|
} else {
|
||||||
if drawing.stroke.fill_choice.is_none() {
|
if drawing.stroke.fill_choice.is_none() {
|
||||||
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
|
drawing.stroke.fill_choice = Some(solid(stroke_working_color(global, drawing.colors_swapped)));
|
||||||
drawing.stroke.tracks_working_color = true;
|
drawing.stroke.tracks_working_color = true;
|
||||||
}
|
}
|
||||||
graph_modification_utils::remove_stroke_for_selected_layers(document, responses);
|
graph_modification_utils::remove_stroke_for_selected_layers(document, responses);
|
||||||
@@ -513,14 +507,14 @@ pub fn apply_working_colors(drawing: &mut DrawingToolState, global: &DocumentToo
|
|||||||
/// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`].
|
/// Refreshes a single swatch from the given working color, subject to the rules in [`apply_working_colors`].
|
||||||
pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) {
|
pub fn refresh_slot_working_color(slot: &mut ToolColorOptions, working_color: Color, document: &DocumentMessageHandler) {
|
||||||
if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) {
|
if slot.fill_choice.is_some() && (!has_selection(document) || slot.tracks_working_color) {
|
||||||
slot.fill_choice = Some(solid_gamma(working_color));
|
slot.fill_choice = Some(solid(working_color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the tool's swatches to the working colors. Called on tool deactivation and shape-mode changes.
|
/// Resets the tool's swatches to the working colors. Called on tool deactivation and shape-mode changes.
|
||||||
pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &DocumentToolData) {
|
pub fn reset_colors_on_deactivation(drawing: &mut DrawingToolState, global: &DocumentToolData) {
|
||||||
drawing.fill.fill_choice = Some(solid_gamma(fill_working_color(global, drawing.colors_swapped)));
|
drawing.fill.fill_choice = Some(solid(fill_working_color(global, drawing.colors_swapped)));
|
||||||
drawing.stroke.fill_choice = Some(solid_gamma(stroke_working_color(global, drawing.colors_swapped)));
|
drawing.stroke.fill_choice = Some(solid(stroke_working_color(global, drawing.colors_swapped)));
|
||||||
drawing.fill.tracks_working_color = true;
|
drawing.fill.tracks_working_color = true;
|
||||||
drawing.stroke.tracks_working_color = true;
|
drawing.stroke.tracks_working_color = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,10 +329,10 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
|||||||
let fill_index = 1;
|
let fill_index = 1;
|
||||||
|
|
||||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER))?;
|
||||||
let TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
|
let &TaggedValue::Fill(Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(color.to_linear_srgb())
|
Some(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
/// Get the current blend mode of a layer from the closest upstream "Blend Mode" node.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::messages::prelude::*;
|
|||||||
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
use crate::messages::tool::transform_layer::transform_layer_message_handler::TransformLayerMessageContext;
|
||||||
use crate::messages::tool::utility_types::{HintData, ToolType};
|
use crate::messages::tool::utility_types::{HintData, ToolType};
|
||||||
use crate::node_graph_executor::NodeGraphExecutor;
|
use crate::node_graph_executor::NodeGraphExecutor;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
|
|
||||||
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays { context }.into();
|
const ARTBOARD_OVERLAY_PROVIDER: OverlayProvider = |context| DocumentMessage::DrawArtboardOverlays { context }.into();
|
||||||
@@ -280,7 +281,7 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
|
|||||||
let r = (random_number >> 16) as u8;
|
let r = (random_number >> 16) as u8;
|
||||||
let g = (random_number >> 8) as u8;
|
let g = (random_number >> 8) as u8;
|
||||||
let b = random_number as u8;
|
let b = random_number as u8;
|
||||||
let random_color = Color::from_rgba8_srgb(r, g, b, 255);
|
let random_color = Color::from(SRGBA8::new(r, g, b, 255));
|
||||||
|
|
||||||
if primary {
|
if primary {
|
||||||
document_data.primary_color = random_color;
|
document_data.primary_color = random_color;
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use crate::messages::portfolio::document::graph_operation::transform_utils::get_
|
|||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_proto_node_type};
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_proto_node_type};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
|
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid_gamma};
|
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid};
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
||||||
use graphene_std::raster::BlendMode;
|
use graphene_std::raster::BlendMode;
|
||||||
use graphene_std::vector::style::FillChoice;
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI};
|
||||||
|
|
||||||
const BRUSH_MAX_SIZE: f64 = 5000.;
|
const BRUSH_MAX_SIZE: f64 = 5000.;
|
||||||
|
|
||||||
@@ -104,13 +104,12 @@ impl ToolMetadata for BrushTool {
|
|||||||
impl LayoutHolder for BrushTool {
|
impl LayoutHolder for BrushTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let mut widgets = vec![
|
let mut widgets = vec![
|
||||||
ColorInput::new(self.options.color.fill_choice.clone().unwrap_or(FillChoice::None))
|
ColorInput::new(FillChoiceUI::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
|
||||||
.mixed(self.options.color.fill_choice.is_none())
|
.mixed(self.options.color.fill_choice.is_none())
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
.on_update(|color: &ColorInput| {
|
.on_update(|color: &ColorInput| {
|
||||||
BrushToolMessage::UpdateOptions {
|
BrushToolMessage::UpdateOptions {
|
||||||
// The picker emits gamma-space colors; working colors are stored in linear sRGB.
|
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(Color::from)),
|
||||||
options: BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|c| c.to_linear_srgb())),
|
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
@@ -245,7 +244,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
|
BrushToolMessageOptionsUpdate::WorkingColorsChanged => {
|
||||||
self.options.color.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
self.options.color.fill_choice = Some(solid(context.global_tool_data.primary_color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use super::tool_prelude::*;
|
use super::tool_prelude::*;
|
||||||
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
|
use crate::messages::frontend::utility_types::EyedropperPreviewImage;
|
||||||
use crate::messages::tool::utility_types::DocumentToolData;
|
use crate::messages::tool::utility_types::DocumentToolData;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::vector::style::RenderMode;
|
use graphene_std::vector::style::RenderMode;
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
@@ -233,8 +234,8 @@ fn update_cursor_preview_common(
|
|||||||
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
|
responses.add(FrontendMessage::UpdateEyedropperSamplingState {
|
||||||
image,
|
image,
|
||||||
mouse_position: Some(input.mouse.position.into()),
|
mouse_position: Some(input.mouse.position.into()),
|
||||||
primary_color: "#".to_string() + global_tool_data.primary_color.to_rgb_hex_srgb().as_str(),
|
primary_color: SRGBA8::from(global_tool_data.primary_color).to_css_hex(),
|
||||||
secondary_color: "#".to_string() + global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(),
|
secondary_color: SRGBA8::from(global_tool_data.secondary_color).to_css_hex(),
|
||||||
set_color_choice,
|
set_color_choice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::tool_prelude::*;
|
use super::tool_prelude::*;
|
||||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||||
use crate::messages::tool::common_functionality::color_selector::solid_gamma;
|
use crate::messages::tool::common_functionality::color_selector::solid;
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
use graphene_std::vector::style::Fill;
|
use graphene_std::vector::style::{Fill, FillChoiceUI};
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
pub struct FillTool {
|
pub struct FillTool {
|
||||||
@@ -43,11 +44,11 @@ impl ToolMetadata for FillTool {
|
|||||||
impl LayoutHolder for FillTool {
|
impl LayoutHolder for FillTool {
|
||||||
fn layout(&self) -> Layout {
|
fn layout(&self) -> Layout {
|
||||||
let widgets = vec![
|
let widgets = vec![
|
||||||
ColorInput::new(solid_gamma(self.primary_color))
|
ColorInput::new(FillChoiceUI::from(&solid(self.primary_color)))
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
.on_update(|color: &ColorInput| {
|
.on_update(|color: &ColorInput| {
|
||||||
FillToolMessage::SetColor {
|
FillToolMessage::SetColor {
|
||||||
color: color.value.as_solid().map(|c| c.to_linear_srgb()),
|
color: color.value.as_solid().map(Color::from),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
@@ -141,7 +142,7 @@ impl Fsm for FillToolFsmState {
|
|||||||
|
|
||||||
// Get the layer the user is hovering over
|
// Get the layer the user is hovering over
|
||||||
if let Some(layer) = document.click(input, viewport) {
|
if let Some(layer) = document.click(input, viewport) {
|
||||||
let color_hex = format!("#{}", preview_color.to_rgba_hex_srgb());
|
let color_hex = SRGBA8::from(preview_color).to_css_hex();
|
||||||
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex);
|
overlay_context.fill_path_pattern(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer), &color_hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +162,8 @@ impl Fsm for FillToolFsmState {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
let fill = match color_event {
|
let fill = match color_event {
|
||||||
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color.to_gamma_srgb()),
|
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color),
|
||||||
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()),
|
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color),
|
||||||
_ => return self,
|
_ => return self,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,6 +202,7 @@ impl Fsm for FillToolFsmState {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_fill {
|
mod test_fill {
|
||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::vector::fill;
|
use graphene_std::vector::fill;
|
||||||
use graphene_std::vector::style::Fill;
|
use graphene_std::vector::style::Fill;
|
||||||
|
|
||||||
@@ -240,7 +242,7 @@ mod test_fill {
|
|||||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
|
||||||
let fills = get_fills(&mut editor).await;
|
let fills = get_fills(&mut editor).await;
|
||||||
assert_eq!(fills.len(), 1);
|
assert_eq!(fills.len(), 1);
|
||||||
assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
|
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::GREEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -252,6 +254,6 @@ mod test_fill {
|
|||||||
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
|
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await;
|
||||||
let fills = get_fills(&mut editor).await;
|
let fills = get_fills(&mut editor).await;
|
||||||
assert_eq!(fills.len(), 1);
|
assert_eq!(fills.len(), 1);
|
||||||
assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::YELLOW.to_rgba8_srgb());
|
assert_eq!(SRGBA8::from(fills[0].as_solid().unwrap()), SRGBA8::from(Color::YELLOW));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ impl LayoutHolder for FreehandTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
FreehandToolMessage::UpdateOptions {
|
FreehandToolMessage::UpdateOptions {
|
||||||
options: FreehandOptionsUpdate::FillColor(color.value.clone()),
|
options: FreehandOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -127,7 +127,7 @@ impl LayoutHolder for FreehandTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
FreehandToolMessage::UpdateOptions {
|
FreehandToolMessage::UpdateOptions {
|
||||||
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid()),
|
options: FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
|||||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops};
|
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer, get_gradient_stops};
|
||||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType};
|
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientStopsUI, GradientType};
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
pub struct GradientTool {
|
pub struct GradientTool {
|
||||||
@@ -49,7 +50,7 @@ pub enum GradientToolMessage {
|
|||||||
CommitTransactionForColorStop,
|
CommitTransactionForColorStop,
|
||||||
CloseStopColorPicker,
|
CloseStopColorPicker,
|
||||||
UpdateStopColor { color: Color },
|
UpdateStopColor { color: Color },
|
||||||
UpdateStops { stops: GradientStops },
|
UpdateStops { stops: GradientStopsUI },
|
||||||
UpdateOptions { options: GradientOptionsUpdate },
|
UpdateOptions { options: GradientOptionsUpdate },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Grad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
ToolMessage::Gradient(GradientToolMessage::UpdateStops { stops }) => {
|
||||||
apply_stops_update(&mut self.data, context, responses, stops);
|
apply_stops_update(&mut self.data, context, responses, GradientStops::from(&stops));
|
||||||
}
|
}
|
||||||
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
ToolMessage::Gradient(GradientToolMessage::CloseStopColorPicker) => {
|
||||||
if self.data.color_picker_transaction_open {
|
if self.data.color_picker_transaction_open {
|
||||||
@@ -243,7 +244,7 @@ impl LayoutHolder for GradientTool {
|
|||||||
},
|
},
|
||||||
]))
|
]))
|
||||||
});
|
});
|
||||||
let stops_widget = ColorInput::new(stops_value)
|
let stops_widget = ColorInput::new(FillChoiceUI::from(&stops_value))
|
||||||
.allow_none(false)
|
.allow_none(false)
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
.tooltip_label("Gradient Stops")
|
.tooltip_label("Gradient Stops")
|
||||||
@@ -856,7 +857,7 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let (start, end) = (transform.transform_point2(*start), transform.transform_point2(*end));
|
let (start, end) = (transform.transform_point2(*start), transform.transform_point2(*end));
|
||||||
|
|
||||||
fn color_to_hex(color: graphene_std::Color) -> String {
|
fn color_to_hex(color: graphene_std::Color) -> String {
|
||||||
format!("#{}", color.to_rgb_hex_srgb_from_gamma())
|
SRGBA8::from(color).to_css_hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE));
|
||||||
@@ -1024,10 +1025,10 @@ impl Fsm for GradientToolFsmState {
|
|||||||
let transform = gradient_space_transform(layer, document);
|
let transform = gradient_space_transform(layer, document);
|
||||||
let gradient = &selected_gradient.gradient;
|
let gradient = &selected_gradient.gradient;
|
||||||
if stop_index < gradient.stops.position.len() {
|
if stop_index < gradient.stops.position.len() {
|
||||||
let color = gradient.stops.color[stop_index].to_gamma_srgb();
|
let color = gradient.stops.color[stop_index];
|
||||||
let position = gradient.stops.position[stop_index];
|
let position = gradient.stops.position[stop_index];
|
||||||
let position = transform.transform_point2(gradient.start.lerp(gradient.end, position)).into();
|
let position = transform.transform_point2(gradient.start.lerp(gradient.end, position)).into();
|
||||||
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position });
|
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color: color.into(), position });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1081,9 +1082,9 @@ impl Fsm for GradientToolFsmState {
|
|||||||
.transform
|
.transform
|
||||||
.transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos));
|
.transform_point2(selected_gradient.gradient.start.lerp(selected_gradient.gradient.end, stop_pos));
|
||||||
let position = viewport_pos.into();
|
let position = viewport_pos.into();
|
||||||
let color = selected_gradient.gradient.stops.color[stop_index].to_gamma_srgb();
|
let color = selected_gradient.gradient.stops.color[stop_index];
|
||||||
tool_data.color_picker_editing_color_stop = Some(stop_index);
|
tool_data.color_picker_editing_color_stop = Some(stop_index);
|
||||||
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color, position });
|
responses.add(FrontendMessage::UpdateGradientStopColorPickerPosition { color: color.into(), position });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1910,6 +1911,7 @@ mod test_gradient {
|
|||||||
pub use crate::test_utils::test_prelude::*;
|
pub use crate::test_utils::test_prelude::*;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::vector::style::{Fill, Gradient};
|
use graphene_std::vector::style::{Fill, Gradient};
|
||||||
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
use graphene_std::vector::{GradientStop, GradientStops, fill};
|
||||||
|
|
||||||
@@ -2022,8 +2024,8 @@ mod test_gradient {
|
|||||||
let (gradient, transform) = get_gradient(&mut editor).await;
|
let (gradient, transform) = get_gradient(&mut editor).await;
|
||||||
|
|
||||||
// Gradient goes from primary color to secondary color
|
// Gradient goes from primary color to secondary color
|
||||||
let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::<Vec<_>>();
|
let stops = gradient.stops.iter().map(|stop| (stop.position, SRGBA8::from(stop.color))).collect::<Vec<_>>();
|
||||||
assert_eq!(stops, vec![(0., Color::GREEN.to_rgba8_srgb()), (1., Color::BLUE.to_rgba8_srgb())]);
|
assert_eq!(stops, vec![(0., SRGBA8::from(Color::GREEN)), (1., SRGBA8::from(Color::BLUE))]);
|
||||||
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10));
|
||||||
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10));
|
||||||
}
|
}
|
||||||
@@ -2215,7 +2217,7 @@ mod test_gradient {
|
|||||||
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
let positions: Vec<f64> = stops.iter().map(|stop| stop.position).collect();
|
||||||
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
|
assert_stops_at_positions(&positions, &[0., 0.25, 1.], 0.1);
|
||||||
|
|
||||||
let middle_color = stops.color[1].to_rgba8_srgb();
|
let middle_color = SRGBA8::from(stops.color[1]);
|
||||||
|
|
||||||
// Simulate dragging the middle stop to position 0.8
|
// Simulate dragging the middle stop to position 0.8
|
||||||
let click_position = DVec2::new(25., 0.);
|
let click_position = DVec2::new(25., 0.);
|
||||||
@@ -2256,9 +2258,9 @@ mod test_gradient {
|
|||||||
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
|
assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1);
|
||||||
|
|
||||||
// Colors should maintain their associations with the stop points
|
// Colors should maintain their associations with the stop points
|
||||||
assert_eq!(updated_stops.color[0].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
|
assert_eq!(SRGBA8::from(updated_stops.color[0]), SRGBA8::from(Color::GREEN));
|
||||||
assert_eq!(updated_stops.color[1].to_rgba8_srgb(), middle_color);
|
assert_eq!(SRGBA8::from(updated_stops.color[1]), middle_color);
|
||||||
assert_eq!(updated_stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb());
|
assert_eq!(SRGBA8::from(updated_stops.color[2]), SRGBA8::from(Color::BLUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -2495,8 +2497,8 @@ mod test_gradient {
|
|||||||
|
|
||||||
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
assert_eq!(updated.stops.len(), 3, "Stop count should be preserved");
|
||||||
assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10);
|
assert_stops_at_positions(&updated.stops.position, &[0., 0.5, 1.], 1e-10);
|
||||||
assert_eq!(updated.stops.color[0].to_rgba8_srgb(), Color::RED.to_rgba8_srgb(), "First stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color[0]), SRGBA8::from(Color::RED), "First stop color should be preserved");
|
||||||
assert_eq!(updated.stops.color[1].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb(), "Middle stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color[1]), SRGBA8::from(Color::GREEN), "Middle stop color should be preserved");
|
||||||
assert_eq!(updated.stops.color[2].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb(), "Last stop color should be preserved");
|
assert_eq!(SRGBA8::from(updated.stops.color[2]), SRGBA8::from(Color::BLUE), "Last stop color should be preserved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2850,13 +2850,10 @@ impl Fsm for PathToolFsmState {
|
|||||||
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses);
|
||||||
|
|
||||||
// Defaults chosen because the pasted geometry has no inherent associated style
|
// Defaults chosen because the pasted geometry has no inherent associated style
|
||||||
let stroke_color = Color::BLACK;
|
let stroke = graphene_std::vector::style::Stroke::new(Some(Color::BLACK), DEFAULT_STROKE_WIDTH);
|
||||||
let fill_color = Color::WHITE;
|
|
||||||
|
|
||||||
let stroke = graphene_std::vector::style::Stroke::new(Some(stroke_color.to_gamma_srgb()), DEFAULT_STROKE_WIDTH);
|
|
||||||
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
|
||||||
|
|
||||||
let fill = graphene_std::vector::style::Fill::solid(fill_color.to_gamma_srgb());
|
let fill = graphene_std::vector::style::Fill::solid(Color::WHITE);
|
||||||
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
responses.add(GraphOperationMessage::FillSet { layer, fill });
|
||||||
|
|
||||||
new_layers.push(layer);
|
new_layers.push(layer);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ impl LayoutHolder for PenTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
PenToolMessage::UpdateOptions {
|
PenToolMessage::UpdateOptions {
|
||||||
options: PenOptionsUpdate::FillColor(color.value.clone()),
|
options: PenOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -184,7 +184,7 @@ impl LayoutHolder for PenTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
PenToolMessage::UpdateOptions {
|
PenToolMessage::UpdateOptions {
|
||||||
options: PenOptionsUpdate::StrokeColor(color.value.as_solid()),
|
options: PenOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ impl LayoutHolder for SelectTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
SelectToolMessage::SelectOptions {
|
SelectToolMessage::SelectOptions {
|
||||||
options: SelectOptionsUpdate::FillColor(color.value.clone()),
|
options: SelectOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -278,7 +278,7 @@ impl LayoutHolder for SelectTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
SelectToolMessage::SelectOptions {
|
SelectToolMessage::SelectOptions {
|
||||||
options: SelectOptionsUpdate::StrokeColor(color.value.as_solid()),
|
options: SelectOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ impl LayoutHolder for ShapeTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
ShapeToolMessage::UpdateOptions {
|
ShapeToolMessage::UpdateOptions {
|
||||||
options: ShapeOptionsUpdate::FillColor(color.value.clone()),
|
options: ShapeOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -498,7 +498,7 @@ impl LayoutHolder for ShapeTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
ShapeToolMessage::UpdateOptions {
|
ShapeToolMessage::UpdateOptions {
|
||||||
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid()),
|
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ impl LayoutHolder for SplineTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
SplineToolMessage::UpdateOptions {
|
SplineToolMessage::UpdateOptions {
|
||||||
options: SplineOptionsUpdate::FillColor(color.value.clone()),
|
options: SplineOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
@@ -135,7 +135,7 @@ impl LayoutHolder for SplineTool {
|
|||||||
},
|
},
|
||||||
|color: &ColorInput| {
|
|color: &ColorInput| {
|
||||||
SplineToolMessage::UpdateOptions {
|
SplineToolMessage::UpdateOptions {
|
||||||
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid()),
|
options: SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(Color::from)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Inpu
|
|||||||
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
use crate::messages::portfolio::utility_types::{CachedData, FontCatalog, FontCatalogStyle};
|
||||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{
|
use crate::messages::tool::common_functionality::color_selector::{
|
||||||
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid_gamma, sync_fill_only,
|
ToolColorOptions, apply_fill_only_color_pick, apply_fill_only_enabled, refresh_slot_working_color, selection_changed_since_last_sync, solid, sync_fill_only,
|
||||||
};
|
};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||||
use crate::messages::tool::common_functionality::resize::Resize;
|
use crate::messages::tool::common_functionality::resize::Resize;
|
||||||
@@ -20,9 +20,10 @@ use crate::messages::tool::utility_types::ToolRefreshOptions;
|
|||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{NodeId, NodeInput};
|
use graph_craft::document::{NodeId, NodeInput};
|
||||||
use graphene_std::choice_type::ChoiceTypeStatic;
|
use graphene_std::choice_type::ChoiceTypeStatic;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
|
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig, lines_clipping};
|
||||||
use graphene_std::vector::style::{Fill, FillChoice};
|
use graphene_std::vector::style::{Fill, FillChoice, FillChoiceUI};
|
||||||
use graphene_std::{Color, NodeInputDecleration};
|
use graphene_std::{Color, NodeInputDecleration};
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
@@ -269,12 +270,12 @@ impl TextTool {
|
|||||||
|
|
||||||
fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout {
|
fn layout(&self, font_catalog: &FontCatalog, document: &DocumentMessageHandler) -> Layout {
|
||||||
let mut widgets = vec![
|
let mut widgets = vec![
|
||||||
ColorInput::new(self.options.fill.fill_choice.clone().unwrap_or(graphene_std::vector::style::FillChoice::None))
|
ColorInput::new(FillChoiceUI::from(self.options.fill.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
|
||||||
.mixed(self.options.fill.fill_choice.is_none())
|
.mixed(self.options.fill.fill_choice.is_none())
|
||||||
.narrow(true)
|
.narrow(true)
|
||||||
.on_update(|color: &ColorInput| {
|
.on_update(|color: &ColorInput| {
|
||||||
TextToolMessage::UpdateOptions {
|
TextToolMessage::UpdateOptions {
|
||||||
options: TextOptionsUpdate::FillColor(color.value.clone()),
|
options: TextOptionsUpdate::FillColor(FillChoice::from(&color.value)),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
@@ -295,7 +296,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
// reset the displayed fill color so the next activation starts fresh from the current working color.
|
// reset the displayed fill color so the next activation starts fresh from the current working color.
|
||||||
// Guarded on `Ready` so Esc-mid-editing (which also fires Abort) doesn't wipe the user's customized fill option.
|
// Guarded on `Ready` so Esc-mid-editing (which also fires Abort) doesn't wipe the user's customized fill option.
|
||||||
if matches!(&message, ToolMessage::Text(TextToolMessage::Abort)) && self.fsm_state == TextToolFsmState::Ready {
|
if matches!(&message, ToolMessage::Text(TextToolMessage::Abort)) && self.fsm_state == TextToolFsmState::Ready {
|
||||||
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
self.options.fill.fill_choice = Some(solid(context.global_tool_data.primary_color));
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = match message {
|
let options = match message {
|
||||||
@@ -319,7 +320,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Text
|
|||||||
if can_edit_selected(context.document).is_some() {
|
if can_edit_selected(context.document).is_some() {
|
||||||
sync_fill_only(&mut self.options.fill, true, context.global_tool_data.primary_color, context.document, selection_changed);
|
sync_fill_only(&mut self.options.fill, true, context.global_tool_data.primary_color, context.document, selection_changed);
|
||||||
} else if selection_changed {
|
} else if selection_changed {
|
||||||
self.options.fill.fill_choice = Some(solid_gamma(context.global_tool_data.primary_color));
|
self.options.fill.fill_choice = Some(solid(context.global_tool_data.primary_color));
|
||||||
self.options.fill.tracks_working_color = true;
|
self.options.fill.tracks_working_color = true;
|
||||||
}
|
}
|
||||||
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
|
// Text tool has no fill checkbox; keep enabled so new text never starts with `None`
|
||||||
@@ -497,7 +498,7 @@ impl TextToolData {
|
|||||||
text: editing_text.text.clone(),
|
text: editing_text.text.clone(),
|
||||||
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
line_height_ratio: editing_text.typesetting.line_height_ratio,
|
||||||
font_size: editing_text.typesetting.font_size,
|
font_size: editing_text.typesetting.font_size,
|
||||||
color: editing_text.color.map_or("#000000".to_string(), |color| format!("#{}", color.to_rgba_hex_srgb())),
|
color: editing_text.color.map_or("#000000".to_string(), |color| SRGBA8::from(color).to_css_hex()),
|
||||||
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
|
font_data: font_cache.get(&editing_text.font).map(|(data, _)| data.clone()).unwrap_or_default().into(),
|
||||||
transform: editing_text.transform.to_cols_array(),
|
transform: editing_text.transform.to_cols_array(),
|
||||||
max_width: editing_text.typesetting.max_width,
|
max_width: editing_text.typesetting.max_width,
|
||||||
@@ -574,7 +575,7 @@ impl TextToolData {
|
|||||||
});
|
});
|
||||||
responses.add(GraphOperationMessage::FillSet {
|
responses.add(GraphOperationMessage::FillSet {
|
||||||
layer: self.layer,
|
layer: self.layer,
|
||||||
fill: if let Some(color) = editing_text.color { Fill::Solid(color.to_gamma_srgb()) } else { Fill::None },
|
fill: if let Some(color) = editing_text.color { Fill::Solid(color) } else { Fill::None },
|
||||||
});
|
});
|
||||||
let transform = editing_text.transform;
|
let transform = editing_text.transform;
|
||||||
self.editing_text = Some(editing_text);
|
self.editing_text = Some(editing_text);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::messages::preferences::PreferencesMessageHandler;
|
|||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeType;
|
||||||
use crate::node_graph_executor::NodeGraphExecutor;
|
use crate::node_graph_executor::NodeGraphExecutor;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
@@ -128,9 +129,7 @@ pub struct DocumentToolData {
|
|||||||
impl DocumentToolData {
|
impl DocumentToolData {
|
||||||
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
|
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
|
||||||
let layout = Layout(vec![
|
let layout = Layout(vec![
|
||||||
LayoutGroup::row(vec![
|
LayoutGroup::row(vec![WorkingColorsInput::new(SRGBA8::from(self.primary_color), SRGBA8::from(self.secondary_color)).widget_instance()]),
|
||||||
WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_instance(),
|
|
||||||
]),
|
|
||||||
LayoutGroup::row(vec![
|
LayoutGroup::row(vec![
|
||||||
IconButton::new("SwapVertical", 16)
|
IconButton::new("SwapVertical", 16)
|
||||||
.tooltip_label("Swap Working Colors")
|
.tooltip_label("Swap Working Colors")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue};
|
|||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation};
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::{CPU, Raster};
|
use graphene_std::raster::{CPU, Raster};
|
||||||
use graphene_std::renderer::RenderMetadata;
|
use graphene_std::renderer::RenderMetadata;
|
||||||
use graphene_std::text::FontCache;
|
use graphene_std::text::FontCache;
|
||||||
@@ -398,7 +399,21 @@ impl NodeGraphExecutor {
|
|||||||
|
|
||||||
match render_output.data {
|
match render_output.data {
|
||||||
RenderOutputType::Svg { svg, image_data } => {
|
RenderOutputType::Svg { svg, image_data } => {
|
||||||
// Send to frontend
|
// Convert each linear-light `Image<Color>` into the JS-boundary `Image<SRGBA8>` form (gamma byte channels) before dispatching.
|
||||||
|
let image_data = image_data
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, image)| {
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
graphene_std::raster::Image {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
data: image.data.iter().map(|&c| SRGBA8::from(c)).collect(),
|
||||||
|
base64_string: image.base64_string,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
responses.add(FrontendMessage::UpdateImageData { image_data });
|
responses.add(FrontendMessage::UpdateImageData { image_data });
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import WidgetLayout from "/src/components/widgets/WidgetLayout.svelte";
|
import WidgetLayout from "/src/components/widgets/WidgetLayout.svelte";
|
||||||
import type { ColorPickerCallbacks, ColorPickerStore } from "/src/stores/color-picker";
|
import type { ColorPickerCallbacks, ColorPickerStore } from "/src/stores/color-picker";
|
||||||
import type { EditorWrapper, FillChoice, MenuDirection } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { EditorWrapper, FillChoiceUI, MenuDirection } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
|
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoiceUI; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
|
||||||
|
|
||||||
const editor = getContext<EditorWrapper>("editor");
|
const editor = getContext<EditorWrapper>("editor");
|
||||||
const colorPickerStore = getContext<ColorPickerStore>("colorPicker");
|
const colorPickerStore = getContext<ColorPickerStore>("colorPicker");
|
||||||
|
|
||||||
export let colorOrGradient: FillChoice;
|
export let colorOrGradient: FillChoiceUI;
|
||||||
export let allowNone = false;
|
export let allowNone = false;
|
||||||
// export let allowTransparency = false; // TODO: Implement
|
// export let allowTransparency = false; // TODO: Implement
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
import type { DocumentStore } from "/src/stores/document";
|
import type { DocumentStore } from "/src/stores/document";
|
||||||
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
||||||
import type { MessageBody } from "/src/subscriptions-router";
|
import type { MessageBody } from "/src/subscriptions-router";
|
||||||
import { fillChoiceColor, createColor } from "/src/utility-functions/colors";
|
import { fillChoiceUIColor, createSRgba8 } from "/src/utility-functions/colors";
|
||||||
import { pasteFile } from "/src/utility-functions/files";
|
import { pasteFile } from "/src/utility-functions/files";
|
||||||
import { textInputCleanup } from "/src/utility-functions/keyboard-entry";
|
import { textInputCleanup } from "/src/utility-functions/keyboard-entry";
|
||||||
import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization";
|
import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization";
|
||||||
import { setupViewportResizeObserver } from "/src/utility-functions/viewports";
|
import { setupViewportResizeObserver } from "/src/utility-functions/viewports";
|
||||||
import type { Color, EditorWrapper, MenuDirection, MouseCursorIcon } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { EditorWrapper, MenuDirection, MouseCursorIcon, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
let rulerHorizontal: RulerInput | undefined;
|
let rulerHorizontal: RulerInput | undefined;
|
||||||
let rulerVertical: RulerInput | undefined;
|
let rulerVertical: RulerInput | undefined;
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
let cursorEyedropperPreviewColorSecondary = "";
|
let cursorEyedropperPreviewColorSecondary = "";
|
||||||
|
|
||||||
// Gradient stop color picker
|
// Gradient stop color picker
|
||||||
let gradientStopPickerColor: Color | undefined = undefined;
|
let gradientStopPickerColor: SRGBA8 | undefined = undefined;
|
||||||
let gradientStopPickerPosition: { x: number; y: number } | undefined = undefined;
|
let gradientStopPickerPosition: { x: number; y: number } | undefined = undefined;
|
||||||
|
|
||||||
// Canvas dimensions
|
// Canvas dimensions
|
||||||
@@ -223,7 +223,7 @@
|
|||||||
mousePosition: [number, number] | undefined,
|
mousePosition: [number, number] | undefined,
|
||||||
colorPrimary: string,
|
colorPrimary: string,
|
||||||
colorSecondary: string,
|
colorSecondary: string,
|
||||||
): Promise<[number, number, number] | undefined> {
|
): Promise<SRGBA8 | undefined> {
|
||||||
if (mousePosition === undefined) {
|
if (mousePosition === undefined) {
|
||||||
cursorEyedropper = false;
|
cursorEyedropper = false;
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -275,14 +275,14 @@
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
const hex = [centerPixel.r, centerPixel.g, centerPixel.b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
const hex = [centerPixel.r, centerPixel.g, centerPixel.b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
||||||
const rgb: [number, number, number] = [centerPixel.r / 255, centerPixel.g / 255, centerPixel.b / 255];
|
const sRgba8: SRGBA8 = { red: centerPixel.r, green: centerPixel.g, blue: centerPixel.b, alpha: 255 };
|
||||||
|
|
||||||
cursorEyedropperPreviewColorChoice = "#" + hex;
|
cursorEyedropperPreviewColorChoice = "#" + hex;
|
||||||
cursorEyedropperPreviewColorPrimary = colorPrimary;
|
cursorEyedropperPreviewColorPrimary = colorPrimary;
|
||||||
cursorEyedropperPreviewColorSecondary = colorSecondary;
|
cursorEyedropperPreviewColorSecondary = colorSecondary;
|
||||||
cursorEyedropperPreviewImageData = preview;
|
cursorEyedropperPreviewImageData = preview;
|
||||||
|
|
||||||
return rgb;
|
return sRgba8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update scrollbars and rulers
|
// Update scrollbars and rulers
|
||||||
@@ -477,11 +477,11 @@
|
|||||||
|
|
||||||
const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data;
|
const { image, mousePosition, primaryColor, secondaryColor, setColorChoice } = data;
|
||||||
const imageData = image !== undefined ? new ImageData(new Uint8ClampedArray(image.data), image.width, image.height) : undefined;
|
const imageData = image !== undefined ? new ImageData(new Uint8ClampedArray(image.data), image.width, image.height) : undefined;
|
||||||
const rgb = await updateEyedropperSamplingState(imageData, mousePosition, primaryColor, secondaryColor);
|
const sRgba8 = await updateEyedropperSamplingState(imageData, mousePosition, primaryColor, secondaryColor);
|
||||||
|
|
||||||
if (setColorChoice && rgb) {
|
if (setColorChoice && sRgba8) {
|
||||||
if (setColorChoice === "Primary") editor.updatePrimaryColor(...rgb, 1);
|
if (setColorChoice === "Primary") editor.updatePrimaryColor(sRgba8);
|
||||||
if (setColorChoice === "Secondary") editor.updateSecondaryColor(...rgb, 1);
|
if (setColorChoice === "Secondary") editor.updateSecondaryColor(sRgba8);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -668,10 +668,10 @@
|
|||||||
gradientStopPickerColor = undefined;
|
gradientStopPickerColor = undefined;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
colorOrGradient={{ Solid: gradientStopPickerColor || createColor(0, 0, 0, 1) }}
|
colorOrGradient={{ Solid: gradientStopPickerColor || createSRgba8(0, 0, 0, 255) }}
|
||||||
on:colorOrGradient={({ detail }) => {
|
on:colorOrGradient={({ detail }) => {
|
||||||
const color = fillChoiceColor(detail);
|
const color = fillChoiceUIColor(detail);
|
||||||
if (color) editor.updateGradientStopColor(color.red, color.green, color.blue, color.alpha);
|
if (color) editor.updateGradientStopColor(color);
|
||||||
}}
|
}}
|
||||||
on:startHistoryTransaction={() => editor.startGradientStopColorTransaction()}
|
on:startHistoryTransaction={() => editor.startGradientStopColorTransaction()}
|
||||||
on:commitHistoryTransaction={() => editor.commitGradientStopColorTransaction()}
|
on:commitHistoryTransaction={() => editor.commitGradientStopColorTransaction()}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
import ShortcutLabel from "/src/components/widgets/labels/ShortcutLabel.svelte";
|
import ShortcutLabel from "/src/components/widgets/labels/ShortcutLabel.svelte";
|
||||||
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
||||||
import type { ColorPickerStore } from "/src/stores/color-picker";
|
import type { ColorPickerStore } from "/src/stores/color-picker";
|
||||||
import { parseFillChoice } from "/src/utility-functions/colors";
|
import { parseFillChoiceUI } from "/src/utility-functions/colors";
|
||||||
import type { EditorWrapper, LayoutTarget, Widget, WidgetInstance } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { EditorWrapper, LayoutTarget, Widget, WidgetInstance } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
// Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...)
|
// Extract the discriminant key names from the Widget tagged enum union (e.g. "TextButton" | "CheckboxInput" | ...)
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
component: ColorInput,
|
component: ColorInput,
|
||||||
getProps: (props, index) => ({
|
getProps: (props, index) => ({
|
||||||
...props,
|
...props,
|
||||||
value: parseFillChoice(props.value),
|
value: parseFillChoiceUI(props.value),
|
||||||
$$events: {
|
$$events: {
|
||||||
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
||||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
getProps: (props, index) => ({
|
getProps: (props, index) => ({
|
||||||
...props,
|
...props,
|
||||||
$$events: {
|
$$events: {
|
||||||
// The widget dispatches `"None"` or a bare `Color`, wrap the color in `{ Solid: ... }` so the payload matches Rust's `FillChoice` shape (which the `Preset` variant expects).
|
// The widget dispatches `"None"` or a bare `SRGBA8`, wrap the color in `{ Solid: ... }` so the payload matches Rust's `FillChoiceUI` shape (which the `Preset` variant expects).
|
||||||
preset: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { Preset: e.detail === "None" ? "None" : { Solid: e.detail } }, true),
|
preset: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { Preset: e.detail === "None" ? "None" : { Solid: e.detail } }, true),
|
||||||
eyedropperColorCode: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { EyedropperColorCode: e.detail }, true),
|
eyedropperColorCode: (e: CustomEvent) => widgetValueCommitAndUpdate(index, { EyedropperColorCode: e.detail }, true),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
||||||
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
||||||
import type { Color } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ swap: undefined }>();
|
const dispatch = createEventDispatcher<{ swap: undefined }>();
|
||||||
|
|
||||||
export let newColor: Color | undefined;
|
export let newColor: SRGBA8 | undefined;
|
||||||
export let oldColor: Color | undefined;
|
export let oldColor: SRGBA8 | undefined;
|
||||||
export let newColorCSS: string;
|
export let newColorCSS: string;
|
||||||
export let newColorContrasting: string;
|
export let newColorContrasting: string;
|
||||||
export let oldColorCSS: string;
|
export let oldColorCSS: string;
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
export let outlineAmount: number;
|
export let outlineAmount: number;
|
||||||
|
|
||||||
$: outlined = outlineAmount > 0.0001;
|
$: outlined = outlineAmount > 0.0001;
|
||||||
$: transparency = (newColor?.alpha ?? 1) < 1 || (oldColor?.alpha ?? 1) < 1;
|
$: transparency = (newColor?.alpha ?? 255) < 255 || (oldColor?.alpha ?? 255) < 255;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||||
import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradientStops } from "/src/utility-functions/colors";
|
import { contrastingOutlineFactor, fillChoiceUIColor, fillChoiceUIGradientStops } from "/src/utility-functions/colors";
|
||||||
import type { FillChoice, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FillChoiceUI, MenuDirection, ActionShortcut } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
const dispatch = createEventDispatcher<{ value: FillChoiceUI; startHistoryTransaction: undefined }>();
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
export let value: FillChoice;
|
export let value: FillChoiceUI;
|
||||||
export let chosenGradient: string | undefined = undefined;
|
export let chosenGradient: string | undefined = undefined;
|
||||||
export let allowNone = false;
|
export let allowNone = false;
|
||||||
// export let allowTransparency = false; // TODO: Implement
|
// export let allowTransparency = false; // TODO: Implement
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
|
|
||||||
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
|
$: outlineFactor = contrastingOutlineFactor(value, "--color-3-darkgray", 0.01);
|
||||||
$: outlined = outlineFactor > 0.0001;
|
$: outlined = outlineFactor > 0.0001;
|
||||||
$: gradientStops = fillChoiceGradientStops(value);
|
$: gradientStops = fillChoiceUIGradientStops(value);
|
||||||
$: solidColor = fillChoiceColor(value);
|
$: solidColor = fillChoiceUIColor(value);
|
||||||
$: none = value === "None";
|
$: none = value === "None";
|
||||||
$: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 1) : solidColor ? solidColor.alpha < 1 : false;
|
$: transparency = gradientStops ? gradientStops.color.some((color) => color.alpha < 255) : solidColor ? solidColor.alpha < 255 : false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol
|
<LayoutCol
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
|
||||||
import Separator from "/src/components/widgets/labels/Separator.svelte";
|
import Separator from "/src/components/widgets/labels/Separator.svelte";
|
||||||
import { createColor } from "/src/utility-functions/colors";
|
import { createSRgba8 } from "/src/utility-functions/colors";
|
||||||
import type { Color } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
type PresetColor = "Black" | "White" | "Red" | "Yellow" | "Green" | "Cyan" | "Blue" | "Magenta";
|
type PresetColor = "Black" | "White" | "Red" | "Yellow" | "Green" | "Cyan" | "Blue" | "Magenta";
|
||||||
|
|
||||||
const PURE_COLORS: Record<PresetColor, [number, number, number]> = {
|
const PURE_COLORS: Record<PresetColor, [number, number, number]> = {
|
||||||
Black: [0, 0, 0],
|
Black: [0, 0, 0],
|
||||||
White: [1, 1, 1],
|
White: [255, 255, 255],
|
||||||
Red: [1, 0, 0],
|
Red: [255, 0, 0],
|
||||||
Yellow: [1, 1, 0],
|
Yellow: [255, 255, 0],
|
||||||
Green: [0, 1, 0],
|
Green: [0, 255, 0],
|
||||||
Cyan: [0, 1, 1],
|
Cyan: [0, 255, 255],
|
||||||
Blue: [0, 0, 1],
|
Blue: [0, 0, 255],
|
||||||
Magenta: [1, 0, 1],
|
Magenta: [255, 0, 255],
|
||||||
};
|
};
|
||||||
const PURE_COLORS_GRAYABLE: [PresetColor, string, string][] = [
|
const PURE_COLORS_GRAYABLE: [PresetColor, string, string][] = [
|
||||||
["Red", "#ff0000", "#4c4c4c"],
|
["Red", "#ff0000", "#4c4c4c"],
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
preset: Color | "None";
|
preset: SRGBA8 | "None";
|
||||||
eyedropperColorCode: string;
|
eyedropperColorCode: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
function pickPreset(preset: PresetColor | "None") {
|
function pickPreset(preset: PresetColor | "None") {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
dispatch("preset", preset === "None" ? "None" : createColor(...PURE_COLORS[preset], 1));
|
dispatch("preset", preset === "None" ? "None" : createSRgba8(...PURE_COLORS[preset], 255));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace this temporary usage of the browser eyedropper API, that only works in Chromium-based browsers, with the custom color sampler system used by the Eyedropper tool
|
// TODO: Replace this temporary usage of the browser eyedropper API, that only works in Chromium-based browsers, with the custom color sampler system used by the Eyedropper tool
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import type { TooltipStore } from "/src/stores/tooltip";
|
import type { TooltipStore } from "/src/stores/tooltip";
|
||||||
import { colorContrastingColor, colorOpaque, colorToHexNoAlpha, colorToRgbCSS, createColor, createColorFromHSVA } from "/src/utility-functions/colors";
|
import { sRgba8ContrastingColor, sRgba8Opaque, sRgba8ToHexNoAlpha, sRgba8ToRgbCSS, createSRgba8, createSRgba8FromHsva } from "/src/utility-functions/colors";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
update: { hue: number; saturation: number; value: number; alpha: number };
|
update: { hue: number; saturation: number; value: number; alpha: number };
|
||||||
@@ -184,20 +184,20 @@
|
|||||||
removeEvents();
|
removeEvents();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: newColor = isNone ? undefined : createColorFromHSVA(hue, saturation, value, alpha);
|
$: newColor = isNone ? undefined : createSRgba8FromHsva(hue, saturation, value, alpha);
|
||||||
$: opaqueHueColor = createColorFromHSVA(hue, 1, 1, 1);
|
$: opaqueHueColor = createSRgba8FromHsva(hue, 1, 1, 1);
|
||||||
$: opaqueColorOnly = newColor ? colorOpaque(newColor) : createColor(0, 0, 0, 1);
|
$: opaqueColorOnly = newColor ? sRgba8Opaque(newColor) : createSRgba8(0, 0, 0, 255);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
class="visual-color-pickers-input"
|
class="visual-color-pickers-input"
|
||||||
classes={{ disabled }}
|
classes={{ disabled }}
|
||||||
styles={{
|
styles={{
|
||||||
"--hue-color": colorToRgbCSS(opaqueHueColor),
|
"--hue-color": sRgba8ToRgbCSS(opaqueHueColor),
|
||||||
"--hue-color-contrasting": colorContrastingColor(opaqueHueColor),
|
"--hue-color-contrasting": sRgba8ContrastingColor(opaqueHueColor),
|
||||||
"--opaque-color": colorToHexNoAlpha(opaqueColorOnly),
|
"--opaque-color": sRgba8ToHexNoAlpha(opaqueColorOnly),
|
||||||
"--opaque-color-contrasting": colorContrastingColor(opaqueColorOnly),
|
"--opaque-color-contrasting": sRgba8ContrastingColor(opaqueColorOnly),
|
||||||
"--new-color-contrasting": colorContrastingColor(newColor),
|
"--new-color-contrasting": sRgba8ContrastingColor(newColor),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@const hueDescription = "The shade along the spectrum of the rainbow."}
|
{@const hueDescription = "The shade along the spectrum of the rainbow."}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
||||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import { fillChoiceColor, colorToRgbaCSS } from "/src/utility-functions/colors";
|
import { fillChoiceUIColor, sRgba8ToRgbaCSS } from "/src/utility-functions/colors";
|
||||||
import type { Color, EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { SRGBA8, EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const editor = getContext<EditorWrapper>("editor");
|
const editor = getContext<EditorWrapper>("editor");
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
export let primary: Color;
|
export let primary: SRGBA8;
|
||||||
export let secondary: Color;
|
export let secondary: SRGBA8;
|
||||||
|
|
||||||
let primaryOpen = false;
|
let primaryOpen = false;
|
||||||
let secondaryOpen = false;
|
let secondaryOpen = false;
|
||||||
@@ -25,37 +25,37 @@
|
|||||||
secondaryOpen = true;
|
secondaryOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function primaryColorChanged(color: Color) {
|
function primaryColorChanged(color: SRGBA8) {
|
||||||
editor.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
|
editor.updatePrimaryColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
function secondaryColorChanged(color: Color) {
|
function secondaryColorChanged(color: SRGBA8) {
|
||||||
editor.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
|
editor.updateSecondaryColor(color);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol class="working-colors-button">
|
<LayoutCol class="working-colors-button">
|
||||||
<LayoutRow class="primary swatch">
|
<LayoutRow class="primary swatch">
|
||||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={colorToRgbaCSS(primary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={sRgba8ToRgbaCSS(primary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
open={primaryOpen}
|
open={primaryOpen}
|
||||||
on:open={({ detail }) => (primaryOpen = detail)}
|
on:open={({ detail }) => (primaryOpen = detail)}
|
||||||
colorOrGradient={{ Solid: primary }}
|
colorOrGradient={{ Solid: primary }}
|
||||||
on:colorOrGradient={({ detail }) => {
|
on:colorOrGradient={({ detail }) => {
|
||||||
const color = fillChoiceColor(detail);
|
const color = fillChoiceUIColor(detail);
|
||||||
if (color) primaryColorChanged(color);
|
if (color) primaryColorChanged(color);
|
||||||
}}
|
}}
|
||||||
direction="Right"
|
direction="Right"
|
||||||
/>
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
<LayoutRow class="secondary swatch">
|
<LayoutRow class="secondary swatch">
|
||||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={colorToRgbaCSS(secondary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={sRgba8ToRgbaCSS(secondary)} data-floating-menu-spawner data-block-hover-transfer tabindex="0"></button>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
open={secondaryOpen}
|
open={secondaryOpen}
|
||||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||||
colorOrGradient={{ Solid: secondary }}
|
colorOrGradient={{ Solid: secondary }}
|
||||||
on:colorOrGradient={({ detail }) => {
|
on:colorOrGradient={({ detail }) => {
|
||||||
const color = fillChoiceColor(detail);
|
const color = fillChoiceUIColor(detail);
|
||||||
if (color) secondaryColorChanged(color);
|
if (color) secondaryColorChanged(color);
|
||||||
}}
|
}}
|
||||||
direction="Right"
|
direction="Right"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
|
||||||
import { panelDrag, startCrossPanelDrag, endCrossPanelDrag, updateCrossPanelHover, updateDockingHover } from "/src/stores/panel-drag";
|
import { panelDrag, startCrossPanelDrag, endCrossPanelDrag, updateCrossPanelHover, updateDockingHover } from "/src/stores/panel-drag";
|
||||||
import type { DockingEdge } from "/src/stores/panel-drag";
|
import type { DockingEdge } from "/src/stores/panel-drag";
|
||||||
import type { EditorWrapper, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { DockingSplitDirection, EditorWrapper, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const PANEL_COMPONENTS = {
|
const PANEL_COMPONENTS = {
|
||||||
Welcome,
|
Welcome,
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
export let emptySpaceAction: (() => void) | undefined = undefined;
|
export let emptySpaceAction: (() => void) | undefined = undefined;
|
||||||
export let crossPanelDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
export let crossPanelDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
||||||
export let groupDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
export let groupDropAction: ((sourcePanelId: string, targetPanelId: string, insertIndex: number) => void) | undefined = undefined;
|
||||||
export let splitDropAction: ((targetPanelId: string, direction: DockingEdge, tabs: PanelType[], activeTabIndex: number) => void) | undefined = undefined;
|
export let splitDropAction: ((targetPanelId: string, direction: DockingSplitDirection, tabs: PanelType[], activeTabIndex: number) => void) | undefined = undefined;
|
||||||
|
|
||||||
let className = "";
|
let className = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
dropAction?.(panelId, crossPanelState.hoverDockingPanelId, Number.MAX_SAFE_INTEGER);
|
dropAction?.(panelId, crossPanelState.hoverDockingPanelId, Number.MAX_SAFE_INTEGER);
|
||||||
}
|
}
|
||||||
// Edge docking drop: create a new split adjacent to the target panel
|
// Edge docking drop: create a new split adjacent to the target panel
|
||||||
else if (crossPanelState.active && crossPanelState.hoverDockingPanelId && crossPanelState.hoverDockingEdge) {
|
else if (crossPanelState.active && crossPanelState.hoverDockingPanelId && crossPanelState.hoverDockingEdge && crossPanelState.hoverDockingEdge !== "Center") {
|
||||||
splitDropAction?.(
|
splitDropAction?.(
|
||||||
crossPanelState.hoverDockingPanelId,
|
crossPanelState.hoverDockingPanelId,
|
||||||
crossPanelState.hoverDockingEdge,
|
crossPanelState.hoverDockingEdge,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||||
import Panel from "/src/components/window/Panel.svelte";
|
import Panel from "/src/components/window/Panel.svelte";
|
||||||
import type { PortfolioStore } from "/src/stores/portfolio";
|
import type { PortfolioStore } from "/src/stores/portfolio";
|
||||||
import type { DocumentInfo, EditorWrapper, PanelGroupState, PanelLayoutSubdivision } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { DockingSplitDirection, DocumentInfo, EditorWrapper, PanelGroupState, PanelLayoutSubdivision, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
const MIN_PANEL_SIZE = 100;
|
const MIN_PANEL_SIZE = 100;
|
||||||
const DOUBLE_CLICK_MILLISECONDS = 500;
|
const DOUBLE_CLICK_MILLISECONDS = 500;
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
sizeOverrides = sizeOverrides;
|
sizeOverrides = sizeOverrides;
|
||||||
|
|
||||||
const allSizes = children.map((child, i) => sizeOverrides[i] ?? child.size);
|
const allSizes = children.map((child, i) => sizeOverrides[i] ?? child.size);
|
||||||
editor.setPanelGroupSizes(splitPath, allSizes);
|
editor.setPanelGroupSizes(new Uint32Array(splitPath), new Float64Array(allSizes));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
// Persist the resized sizes to the backend
|
// Persist the resized sizes to the backend
|
||||||
if ("Split" in subdivision) {
|
if ("Split" in subdivision) {
|
||||||
const allSizes = subdivision.Split.children.map((child, i) => sizeOverrides[i] ?? child.size);
|
const allSizes = subdivision.Split.children.map((child, i) => sizeOverrides[i] ?? child.size);
|
||||||
editor.setPanelGroupSizes(splitPath, allSizes);
|
editor.setPanelGroupSizes(new Uint32Array(splitPath), new Float64Array(allSizes));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
editor.moveAllPanelTabs(BigInt(sourcePanelId), BigInt(targetPanelId), insertIndex);
|
editor.moveAllPanelTabs(BigInt(sourcePanelId), BigInt(targetPanelId), insertIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitDrop(targetPanelId: string, direction: string, tabs: string[], activeTabIndex: number) {
|
function splitDrop(targetPanelId: string, direction: DockingSplitDirection, tabs: PanelType[], activeTabIndex: number) {
|
||||||
editor.splitPanelGroup(BigInt(targetPanelId), direction, tabs, activeTabIndex);
|
editor.splitPanelGroup(BigInt(targetPanelId), direction, tabs, activeTabIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { writable } from "svelte/store";
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
import type { SubscriptionsRouter } from "/src/subscriptions-router";
|
||||||
import { patchLayout } from "/src/utility-functions/widgets";
|
import { patchLayout } from "/src/utility-functions/widgets";
|
||||||
import type { FillChoice, Layout } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FillChoiceUI, Layout } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
export type ColorPickerCallbacks = {
|
export type ColorPickerCallbacks = {
|
||||||
onColorChanged?: (value: FillChoice) => void;
|
onColorChanged?: (value: FillChoiceUI) => void;
|
||||||
onStartTransaction?: () => void;
|
onStartTransaction?: () => void;
|
||||||
onCommitTransaction?: () => void;
|
onCommitTransaction?: () => void;
|
||||||
};
|
};
|
||||||
@@ -40,9 +40,9 @@ export type ColorPickerStore = {
|
|||||||
setDragging: (dragging: boolean) => void;
|
setDragging: (dragging: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The Rust handler keeps a single shared layout per target, but multiple `<ColorPicker>` Svelte instances may be mounted across
|
// The Rust handler keeps a single shared layout per target, but multiple `<ColorPicker />` Svelte instances may be mounted across
|
||||||
// the app (one per `ColorInput`/`WorkingColorsInput`/etc.). Subscribing to the layout target from each instance is destructive,
|
// the app (one per `ColorInput`/`WorkingColorsInput`/etc.). Subscribing to the layout target from each instance is destructive,
|
||||||
// only the last-registered callback wins. So we maintain a single global subscription here and let each `<ColorPicker>` instance
|
// only the last-registered callback wins. So we maintain a single global subscription here and let each `<ColorPicker />` instance
|
||||||
// read from the resulting store and register its own per-open callbacks for color/transaction events.
|
// read from the resulting store and register its own per-open callbacks for color/transaction events.
|
||||||
export function createColorPickerStore(subscriptions: SubscriptionsRouter): ColorPickerStore {
|
export function createColorPickerStore(subscriptions: SubscriptionsRouter): ColorPickerStore {
|
||||||
destroyColorPickerStore();
|
destroyColorPickerStore();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { DockingSplitDirection, PanelType } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
export type DockingEdge = "Left" | "Right" | "Top" | "Bottom" | "Center";
|
export type DockingEdge = DockingSplitDirection | "Center";
|
||||||
|
|
||||||
export type PanelDragState = {
|
export type PanelDragState = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Color, FillChoice, GradientStops } from "/wrapper/pkg/graphite_wasm_wrapper";
|
import type { FillChoiceUI, GradientStopsUI, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||||
|
|
||||||
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
// Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers
|
||||||
export type HSV = { h: number; s: number; v: number };
|
export type HSV = { h: number; s: number; v: number };
|
||||||
@@ -6,26 +6,33 @@ export type RGB = { r: number; g: number; b: number };
|
|||||||
|
|
||||||
// COLOR FACTORY FUNCTIONS
|
// COLOR FACTORY FUNCTIONS
|
||||||
|
|
||||||
export function createColor(red: number, green: number, blue: number, alpha: number): Color {
|
export function createSRgba8(red: number, green: number, blue: number, alpha: number): SRGBA8 {
|
||||||
return { red, green, blue, alpha };
|
return { red, green, blue, alpha };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color {
|
// Build an `SRGBA8` from HSVA components on the 0..1 range.
|
||||||
|
export function createSRgba8FromHsva(h: number, s: number, v: number, a: number): SRGBA8 {
|
||||||
const convert = (n: number): number => {
|
const convert = (n: number): number => {
|
||||||
const k = (n + h * 6) % 6;
|
const k = (n + h * 6) % 6;
|
||||||
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { red: convert(5), green: convert(3), blue: convert(1), alpha: a };
|
return {
|
||||||
|
red: Math.round(convert(5) * 255),
|
||||||
|
green: Math.round(convert(3) * 255),
|
||||||
|
blue: Math.round(convert(1) * 255),
|
||||||
|
alpha: Math.round(a * 255),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// COLOR UTILITY FUNCTIONS
|
// COLOR UTILITY FUNCTIONS
|
||||||
|
|
||||||
export function isColor(value: unknown): value is Color {
|
export function isSRgba8(value: unknown): value is SRGBA8 {
|
||||||
return typeof value === "object" && value !== null && "red" in value;
|
return typeof value === "object" && value !== null && "red" in value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorFromCSS(colorCode: string): Color | undefined {
|
// Parse a CSS color string into an `SRGBA8`. Uses a canvas to delegate parsing to the browser.
|
||||||
|
export function sRgba8FromCSS(colorCode: string): SRGBA8 | undefined {
|
||||||
// Allow single-digit hex value inputs
|
// Allow single-digit hex value inputs
|
||||||
let colorValue = colorCode.trim();
|
let colorValue = colorCode.trim();
|
||||||
if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) {
|
if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) {
|
||||||
@@ -52,52 +59,40 @@ export function colorFromCSS(colorCode: string): Color | undefined {
|
|||||||
// Invalid color
|
// Invalid color
|
||||||
if (comparisonA !== comparisonB) {
|
if (comparisonA !== comparisonB) {
|
||||||
// If this color code didn't start with a #, add it and try again
|
// If this color code didn't start with a #, add it and try again
|
||||||
if (colorValue.trim().charAt(0) !== "#") return colorFromCSS(`#${colorValue.trim()}`);
|
if (colorValue.trim().charAt(0) !== "#") return sRgba8FromCSS(`#${colorValue.trim()}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.fillRect(0, 0, 1, 1);
|
context.fillRect(0, 0, 1, 1);
|
||||||
|
|
||||||
const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data];
|
const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data];
|
||||||
return createColor(r / 255, g / 255, b / 255, a / 255);
|
return createSRgba8(r, g, b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorToHexNoAlpha(color: Color): string {
|
export function sRgba8ToHexNoAlpha(color: SRGBA8): string {
|
||||||
const r = Math.round(color.red * 255)
|
const r = color.red.toString(16).padStart(2, "0");
|
||||||
.toString(16)
|
const g = color.green.toString(16).padStart(2, "0");
|
||||||
.padStart(2, "0");
|
const b = color.blue.toString(16).padStart(2, "0");
|
||||||
const g = Math.round(color.green * 255)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
const b = Math.round(color.blue * 255)
|
|
||||||
.toString(16)
|
|
||||||
.padStart(2, "0");
|
|
||||||
|
|
||||||
return `#${r}${g}${b}`;
|
return `#${r}${g}${b}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorToRgb255(color: Color): RGB {
|
export function sRgba8ToRgb255(color: SRGBA8): RGB {
|
||||||
return {
|
return { r: color.red, g: color.green, b: color.blue };
|
||||||
r: Math.round(color.red * 255),
|
|
||||||
g: Math.round(color.green * 255),
|
|
||||||
b: Math.round(color.blue * 255),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorToRgbCSS(color: Color): string {
|
export function sRgba8ToRgbCSS(color: SRGBA8): string {
|
||||||
const rgb = colorToRgb255(color);
|
return `rgb(${color.red}, ${color.green}, ${color.blue})`;
|
||||||
|
|
||||||
return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorToRgbaCSS(color: Color): string {
|
export function sRgba8ToRgbaCSS(color: SRGBA8): string {
|
||||||
const rgb = colorToRgb255(color);
|
return `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255})`;
|
||||||
|
|
||||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorToHSV(color: Color): HSV {
|
export function sRgba8ToHSV(color: SRGBA8): HSV {
|
||||||
const { red: r, green: g, blue: b } = color;
|
const r = color.red / 255;
|
||||||
|
const g = color.green / 255;
|
||||||
|
const b = color.blue / 255;
|
||||||
|
|
||||||
const max = Math.max(r, g, b);
|
const max = Math.max(r, g, b);
|
||||||
const min = Math.min(r, g, b);
|
const min = Math.min(r, g, b);
|
||||||
@@ -126,15 +121,17 @@ export function colorToHSV(color: Color): HSV {
|
|||||||
return { h, s, v };
|
return { h, s, v };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorOpaque(color: Color): Color {
|
export function sRgba8Opaque(color: SRGBA8): SRGBA8 {
|
||||||
return createColor(color.red, color.green, color.blue, 1);
|
return createSRgba8(color.red, color.green, color.blue, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorLuminance(color: Color): number {
|
// WCAG-style relative luminance computed from an `SRGBA8` (alpha composited over white).
|
||||||
|
export function sRgba8Luminance(color: SRGBA8): number {
|
||||||
|
const a = color.alpha / 255;
|
||||||
// Convert alpha into white
|
// Convert alpha into white
|
||||||
const r = color.red * color.alpha + (1 - color.alpha);
|
const r = (color.red / 255) * a + (1 - a);
|
||||||
const g = color.green * color.alpha + (1 - color.alpha);
|
const g = (color.green / 255) * a + (1 - a);
|
||||||
const b = color.blue * color.alpha + (1 - color.alpha);
|
const b = (color.blue / 255) * a + (1 - a);
|
||||||
|
|
||||||
// https://stackoverflow.com/a/3943023/775283
|
// https://stackoverflow.com/a/3943023/775283
|
||||||
|
|
||||||
@@ -145,32 +142,32 @@ export function colorLuminance(color: Color): number {
|
|||||||
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorContrastingColor(color: Color | undefined): "black" | "white" {
|
export function sRgba8ContrastingColor(color: SRGBA8 | undefined): "black" | "white" {
|
||||||
if (!color) return "black";
|
if (!color) return "black";
|
||||||
|
|
||||||
const luminance = colorLuminance(color);
|
const luminance = sRgba8Luminance(color);
|
||||||
|
|
||||||
return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number {
|
export function contrastingOutlineFactor(value: FillChoiceUI, proximityColor: string | [string, string], proximityRange: number): number {
|
||||||
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor];
|
||||||
const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color)));
|
const [range1, range2] = pair.map((color) => sRgba8FromCSS(window.getComputedStyle(document.body).getPropertyValue(color)));
|
||||||
|
|
||||||
const contrast = (color: Color | undefined): number => {
|
const contrast = (color: SRGBA8 | undefined): number => {
|
||||||
if (!color) return 0;
|
if (!color) return 0;
|
||||||
|
|
||||||
const lum = colorLuminance(color);
|
const lum = sRgba8Luminance(color);
|
||||||
let rangeLuminance1 = range1 ? colorLuminance(range1) : 0;
|
let rangeLuminance1 = range1 ? sRgba8Luminance(range1) : 0;
|
||||||
let rangeLuminance2 = range2 ? colorLuminance(range2) : 0;
|
let rangeLuminance2 = range2 ? sRgba8Luminance(range2) : 0;
|
||||||
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
[rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)];
|
||||||
|
|
||||||
const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2);
|
const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2);
|
||||||
|
|
||||||
return (1 - Math.min(distance / proximityRange, 1)) * (1 - colorToHSV(color).s);
|
return (1 - Math.min(distance / proximityRange, 1)) * (1 - sRgba8ToHSV(color).s);
|
||||||
};
|
};
|
||||||
|
|
||||||
const gradientStops = fillChoiceGradientStops(value);
|
const gradientStops = fillChoiceUIGradientStops(value);
|
||||||
if (gradientStops) {
|
if (gradientStops) {
|
||||||
if (gradientStops.color.length === 0) return 0;
|
if (gradientStops.color.length === 0) return 0;
|
||||||
|
|
||||||
@@ -180,30 +177,30 @@ export function contrastingOutlineFactor(value: FillChoice, proximityColor: stri
|
|||||||
return Math.min(first, last);
|
return Math.min(first, last);
|
||||||
}
|
}
|
||||||
|
|
||||||
return contrast(fillChoiceColor(value));
|
return contrast(fillChoiceUIColor(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// GRADIENT UTILITY FUNCTIONS
|
// GRADIENT UTILITY FUNCTIONS
|
||||||
|
|
||||||
export function isGradientStops(value: unknown): value is GradientStops {
|
export function isGradientStopsUI(value: unknown): value is GradientStopsUI {
|
||||||
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value;
|
return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FILL CHOICE UTILITY FUNCTIONS
|
// FILL CHOICE UTILITY FUNCTIONS
|
||||||
|
|
||||||
export function fillChoiceColor(value: FillChoice): Color | undefined {
|
export function fillChoiceUIColor(value: FillChoiceUI): SRGBA8 | undefined {
|
||||||
if (typeof value === "object" && "Solid" in value) return value.Solid;
|
if (typeof value === "object" && "Solid" in value) return value.Solid;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillChoiceGradientStops(value: FillChoice): GradientStops | undefined {
|
export function fillChoiceUIGradientStops(value: FillChoiceUI): GradientStopsUI | undefined {
|
||||||
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
|
if (typeof value === "object" && "Gradient" in value) return value.Gradient;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFillChoice(value: unknown): FillChoice {
|
export function parseFillChoiceUI(value: unknown): FillChoiceUI {
|
||||||
if (value === "None" || value === undefined || value === null) return "None";
|
if (value === "None" || value === undefined || value === null) return "None";
|
||||||
if (typeof value === "object" && value !== null && "Solid" in value && isColor(value.Solid)) return { Solid: value.Solid };
|
if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid };
|
||||||
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient };
|
if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStopsUI(value.Gradient)) return { Gradient: value.Gradient };
|
||||||
return "None";
|
return "None";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ editor = { path = "../../editor", package = "graphite-editor", features = ["gpu"
|
|||||||
graphene-std = { workspace = true }
|
graphene-std = { workspace = true }
|
||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
|
bytemuck = { workspace = true }
|
||||||
graph-craft = { workspace = true }
|
graph-craft = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|||||||
@@ -16,14 +16,17 @@ use editor::consts::FILE_EXTENSION;
|
|||||||
use editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
use editor::messages::clipboard::utility_types::ClipboardContentRaw;
|
||||||
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
|
||||||
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
|
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
|
||||||
|
use editor::messages::layout::utility_types::layout_widget::LayoutTarget;
|
||||||
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
|
||||||
use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType};
|
use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType};
|
||||||
use editor::messages::prelude::*;
|
use editor::messages::prelude::*;
|
||||||
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::graphene_hash::CacheHashWrapper;
|
use graphene_std::graphene_hash::CacheHashWrapper;
|
||||||
use graphene_std::raster::color::Color;
|
use graphene_std::raster::color::Color;
|
||||||
|
use graphene_std::vector::style::{FillChoice, FillChoiceUI};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_wasm_bindgen::{self, from_value};
|
use serde_wasm_bindgen::{self, from_value};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -319,20 +322,20 @@ impl EditorWrapper {
|
|||||||
|
|
||||||
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
/// Update the value of a given UI widget, but don't commit it to the history (unless `commit_layout()` is called, which handles that)
|
||||||
#[wasm_bindgen(js_name = widgetValueUpdate)]
|
#[wasm_bindgen(js_name = widgetValueUpdate)]
|
||||||
pub fn widget_value_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
pub fn widget_value_update(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)
|
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the value of a given UI widget to the history
|
/// Commit the value of a given UI widget to the history
|
||||||
#[wasm_bindgen(js_name = widgetValueCommit)]
|
#[wasm_bindgen(js_name = widgetValueCommit)]
|
||||||
pub fn widget_value_commit(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
pub fn widget_value_commit(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||||
self.widget_value_commit_helper(layout_target, widget_id, value)
|
self.widget_value_commit_helper(layout_target, widget_id, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the value of a given UI widget, and commit it to the history
|
/// Update the value of a given UI widget, and commit it to the history
|
||||||
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
#[wasm_bindgen(js_name = widgetValueCommitAndUpdate)]
|
||||||
pub fn widget_value_commit_and_update(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
pub fn widget_value_commit_and_update(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||||
self.widget_value_commit_helper(layout_target.clone(), widget_id, value.clone())?;
|
self.widget_value_commit_helper(layout_target, widget_id, value.clone())?;
|
||||||
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
|
self.widget_value_update_helper(layout_target, widget_id, value, resend_widget)?;
|
||||||
// Close out a transaction that the widget's `on_commit` opened (if any), so a single click on widgets like the
|
// Close out a transaction that the widget's `on_commit` opened (if any), so a single click on widgets like the
|
||||||
// NumberInput's increment buttons collapses into one history step instead of leaving the transaction in `Modified`
|
// NumberInput's increment buttons collapses into one history step instead of leaving the transaction in `Modified`
|
||||||
@@ -346,34 +349,24 @@ impl EditorWrapper {
|
|||||||
self.dispatch(DocumentMessage::EndTransaction);
|
self.dispatch(DocumentMessage::EndTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget_value_update_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
pub fn widget_value_update_helper(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue, resend_widget: bool) -> Result<(), JsValue> {
|
||||||
let widget_id = WidgetId(widget_id);
|
let widget_id = WidgetId(widget_id);
|
||||||
match (from_value(layout_target), from_value(value)) {
|
let value: serde_json::Value = from_value(value).map_err(|e| Error::new(&format!("Could not update UI: {e}")))?;
|
||||||
(Ok(layout_target), Ok(value)) => {
|
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
|
||||||
let message = LayoutMessage::WidgetValueUpdate { layout_target, widget_id, value };
|
self.dispatch(message);
|
||||||
self.dispatch(message);
|
if resend_widget {
|
||||||
|
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
|
||||||
if resend_widget {
|
self.dispatch(resend_message);
|
||||||
let resend_message = LayoutMessage::ResendActiveWidget { layout_target, widget_id };
|
|
||||||
self.dispatch(resend_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
(target, val) => Err(Error::new(&format!("Could not update UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget_value_commit_helper(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
pub fn widget_value_commit_helper(&self, layout_target: LayoutTarget, widget_id: u64, value: JsValue) -> Result<(), JsValue> {
|
||||||
let widget_id = WidgetId(widget_id);
|
let widget_id = WidgetId(widget_id);
|
||||||
match (from_value(layout_target), from_value(value)) {
|
let value: serde_json::Value = from_value(value).map_err(|e| Error::new(&format!("Could not commit UI: {e}")))?;
|
||||||
(Ok(layout_target), Ok(value)) => {
|
let message = LayoutMessage::WidgetValueCommit { layout_target, widget_id, value };
|
||||||
let message = LayoutMessage::WidgetValueCommit { layout_target, widget_id, value };
|
self.dispatch(message);
|
||||||
self.dispatch(message);
|
Ok(())
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
(target, val) => Err(Error::new(&format!("Could not commit UI\nDetails:\nTarget: {target:?}\nValue: {val:?}")).into()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = loadPreferences)]
|
#[wasm_bindgen(js_name = loadPreferences)]
|
||||||
@@ -389,14 +382,8 @@ impl EditorWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = loadPersistedState)]
|
#[wasm_bindgen(js_name = loadPersistedState)]
|
||||||
pub fn load_persisted_state(&self, state: JsValue) {
|
pub fn load_persisted_state(&self, state: editor::messages::frontend::utility_types::PersistedState) {
|
||||||
let Ok(state) = serde_wasm_bindgen::from_value(state) else {
|
self.dispatch(PersistentStateMessage::LoadState { state });
|
||||||
log::error!("Failed to deserialize persisted state");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = PersistentStateMessage::LoadState { state };
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = loadDocumentContent)]
|
#[wasm_bindgen(js_name = loadDocumentContent)]
|
||||||
@@ -494,9 +481,7 @@ impl EditorWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = splitPanelGroup)]
|
#[wasm_bindgen(js_name = splitPanelGroup)]
|
||||||
pub fn split_panel_group(&self, target_group: u64, direction: String, tabs: JsValue, active_tab_index: usize) {
|
pub fn split_panel_group(&self, target_group: u64, direction: DockingSplitDirection, tabs: Vec<PanelType>, active_tab_index: usize) {
|
||||||
let direction: DockingSplitDirection = serde_wasm_bindgen::from_value(JsValue::from_str(&direction)).unwrap();
|
|
||||||
let tabs: Vec<PanelType> = serde_wasm_bindgen::from_value(tabs).unwrap();
|
|
||||||
let message = PortfolioMessage::SplitPanelGroup {
|
let message = PortfolioMessage::SplitPanelGroup {
|
||||||
target_group: PanelGroupId(target_group),
|
target_group: PanelGroupId(target_group),
|
||||||
direction,
|
direction,
|
||||||
@@ -507,9 +492,8 @@ impl EditorWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = setPanelGroupSizes)]
|
#[wasm_bindgen(js_name = setPanelGroupSizes)]
|
||||||
pub fn set_panel_group_sizes(&self, split_path: JsValue, sizes: JsValue) {
|
pub fn set_panel_group_sizes(&self, split_path: Vec<u32>, sizes: Vec<f64>) {
|
||||||
let split_path: Vec<usize> = serde_wasm_bindgen::from_value(split_path).unwrap();
|
let split_path = split_path.into_iter().map(|i| i as usize).collect();
|
||||||
let sizes: Vec<f64> = serde_wasm_bindgen::from_value(sizes).unwrap();
|
|
||||||
let message = PortfolioMessage::SetPanelGroupSizes { split_path, sizes };
|
let message = PortfolioMessage::SetPanelGroupSizes { split_path, sizes };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
@@ -645,13 +629,8 @@ impl EditorWrapper {
|
|||||||
|
|
||||||
/// The font catalog has been loaded
|
/// The font catalog has been loaded
|
||||||
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
#[wasm_bindgen(js_name = onFontCatalogLoad)]
|
||||||
pub fn on_font_catalog_load(&self, catalog: JsValue) -> Result<(), JsValue> {
|
pub fn on_font_catalog_load(&self, catalog: Vec<FontCatalogFamily>) {
|
||||||
// Deserializing from TS type: `{ name: string; styles: { weight: number, italic: boolean, url: string }[] }[]`
|
self.dispatch(PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(catalog) });
|
||||||
let families = serde_wasm_bindgen::from_value::<Vec<FontCatalogFamily>>(catalog)?;
|
|
||||||
let message = PortfolioMessage::FontCatalogLoaded { catalog: FontCatalog(families) };
|
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A font has been downloaded
|
/// A font has been downloaded
|
||||||
@@ -679,44 +658,29 @@ impl EditorWrapper {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update primary color with values on a scale from 0 to 1.
|
/// Update primary color from sRGB bytes (the wire format at the JS boundary).
|
||||||
#[wasm_bindgen(js_name = updatePrimaryColor)]
|
#[wasm_bindgen(js_name = updatePrimaryColor)]
|
||||||
pub fn update_primary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
|
pub fn update_primary_color(&self, color: SRGBA8) {
|
||||||
let Some(primary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
|
self.dispatch(ToolMessage::SelectWorkingColor {
|
||||||
return Err(Error::new("Invalid color").into());
|
color: Color::from(color),
|
||||||
};
|
|
||||||
|
|
||||||
let message = ToolMessage::SelectWorkingColor {
|
|
||||||
color: primary_color.to_linear_srgb(),
|
|
||||||
primary: true,
|
primary: true,
|
||||||
};
|
});
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update secondary color with values on a scale from 0 to 1.
|
/// Update secondary color from sRGB bytes (the wire format at the JS boundary).
|
||||||
#[wasm_bindgen(js_name = updateSecondaryColor)]
|
#[wasm_bindgen(js_name = updateSecondaryColor)]
|
||||||
pub fn update_secondary_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
|
pub fn update_secondary_color(&self, color: SRGBA8) {
|
||||||
let Some(secondary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
|
self.dispatch(ToolMessage::SelectWorkingColor {
|
||||||
return Err(Error::new("Invalid color").into());
|
color: Color::from(color),
|
||||||
};
|
|
||||||
|
|
||||||
let message = ToolMessage::SelectWorkingColor {
|
|
||||||
color: secondary_color.to_linear_srgb(),
|
|
||||||
primary: false,
|
primary: false,
|
||||||
};
|
});
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the Rust color picker handler with a starting value (used when the frontend `<ColorPicker>` opens).
|
/// Initialize the Rust color picker handler with a starting value (used when the frontend `<ColorPicker />` opens).
|
||||||
#[wasm_bindgen(js_name = openColorPicker)]
|
#[wasm_bindgen(js_name = openColorPicker)]
|
||||||
pub fn open_color_picker(&self, initial_value: JsValue, allow_none: bool, disabled: bool) -> Result<(), JsValue> {
|
pub fn open_color_picker(&self, initial_value: FillChoiceUI, allow_none: bool, disabled: bool) {
|
||||||
let initial_value = serde_wasm_bindgen::from_value(initial_value).map_err(|e| Error::new(&format!("Invalid initial picker value: {e}")))?;
|
let initial_value = FillChoice::from(&initial_value);
|
||||||
self.dispatch(ColorPickerMessage::Open { initial_value, allow_none, disabled });
|
self.dispatch(ColorPickerMessage::Open { initial_value, allow_none, disabled });
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tell the Rust color picker handler that the popover is closing.
|
/// Tell the Rust color picker handler that the popover is closing.
|
||||||
@@ -725,14 +689,10 @@ impl EditorWrapper {
|
|||||||
self.dispatch(ColorPickerMessage::Close);
|
self.dispatch(ColorPickerMessage::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the color of the currently-edited gradient stop
|
/// Update the color of the currently-edited gradient stop, from sRGB bytes (the wire format at the JS boundary).
|
||||||
#[wasm_bindgen(js_name = updateGradientStopColor)]
|
#[wasm_bindgen(js_name = updateGradientStopColor)]
|
||||||
pub fn update_gradient_stop_color(&self, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
|
pub fn update_gradient_stop_color(&self, color: SRGBA8) {
|
||||||
let Some(color) = Color::from_rgbaf32(red, green, blue, alpha) else {
|
self.dispatch(GradientToolMessage::UpdateStopColor { color: Color::from(color) });
|
||||||
return Err(Error::new("Invalid color").into());
|
|
||||||
};
|
|
||||||
self.dispatch(GradientToolMessage::UpdateStopColor { color: color.to_linear_srgb() });
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a new undo transaction for gradient stop color editing
|
/// Start a new undo transaction for gradient stop color editing
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use crate::{EDITOR_HAS_CRASHED, EDITOR_WRAPPER};
|
|||||||
use editor::application::Editor;
|
use editor::application::Editor;
|
||||||
use editor::messages::input_mapper::utility_types::input_keyboard::Key;
|
use editor::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||||
use editor::messages::prelude::*;
|
use editor::messages::prelude::*;
|
||||||
|
use graphene_std::color::SRGBA8;
|
||||||
use graphene_std::raster::Image;
|
use graphene_std::raster::Image;
|
||||||
use graphene_std::raster::color::Color;
|
|
||||||
use js_sys::{Object, Reflect};
|
use js_sys::{Object, Reflect};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -119,7 +119,7 @@ pub(crate) fn auto_save_all_documents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)]) {
|
pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<SRGBA8>)]) {
|
||||||
let window = match window() {
|
let window = match window() {
|
||||||
Some(window) => window,
|
Some(window) => window,
|
||||||
None => {
|
None => {
|
||||||
@@ -167,8 +167,9 @@ pub(crate) fn render_image_data_to_canvases(image_data: &[(u64, Image<Color>)])
|
|||||||
.expect("2d context was not found")
|
.expect("2d context was not found")
|
||||||
.dyn_into::<CanvasRenderingContext2d>()
|
.dyn_into::<CanvasRenderingContext2d>()
|
||||||
.expect("Failed to cast context to CanvasRenderingContext2d");
|
.expect("Failed to cast context to CanvasRenderingContext2d");
|
||||||
let u8_data: Vec<u8> = image.data.iter().flat_map(|color| color.to_rgba8_srgb()).collect();
|
// `SRGBA8` is `#[repr(C)]` of four `u8`s, so the data buffer is already the byte format the canvas expects
|
||||||
let clamped_u8_data = wasm_bindgen::Clamped(&u8_data[..]);
|
let u8_data: &[u8] = bytemuck::cast_slice(&image.data);
|
||||||
|
let clamped_u8_data = wasm_bindgen::Clamped(u8_data);
|
||||||
match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) {
|
match ImageData::new_with_u8_clamped_array_and_sh(clamped_u8_data, image.width, image.height) {
|
||||||
Ok(image_data_obj) => {
|
Ok(image_data_obj) => {
|
||||||
if context.put_image_data(&image_data_obj, 0., 0.).is_err() {
|
if context.put_image_data(&image_data_obj, 0., 0.).is_err() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::DocumentNode;
|
|||||||
use crate::application_io::PlatformEditorApi;
|
use crate::application_io::PlatformEditorApi;
|
||||||
use crate::proto::{Any as DAny, FutureAny};
|
use crate::proto::{Any as DAny, FutureAny};
|
||||||
use brush_nodes::brush_stroke::BrushStroke;
|
use brush_nodes::brush_stroke::BrushStroke;
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::transform::Footprint;
|
use core_types::transform::Footprint;
|
||||||
use core_types::uuid::NodeId;
|
use core_types::uuid::NodeId;
|
||||||
@@ -464,7 +465,7 @@ impl TaggedValue {
|
|||||||
// String syntax (e.g. "000000ff")
|
// String syntax (e.g. "000000ff")
|
||||||
if input.starts_with('"') && input.ends_with('"') {
|
if input.starts_with('"') && input.ends_with('"') {
|
||||||
let hex = input.trim().trim_matches('"').trim().trim_start_matches('#');
|
let hex = input.trim().trim_matches('"').trim().trim_start_matches('#');
|
||||||
let color = Color::from_hex_str(hex);
|
let color = SRGBA8::from_hex_str(hex).map(Color::from);
|
||||||
if color.is_none() {
|
if color.is_none() {
|
||||||
log::error!("Invalid default value color string: {input}");
|
log::error!("Invalid default value color string: {input}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ no-std-types = { workspace = true, features = ["std"] }
|
|||||||
graphene-hash = { workspace = true, features = ["derive"] }
|
graphene-hash = { workspace = true, features = ["derive"] }
|
||||||
|
|
||||||
# Workspace dependencies
|
# Workspace dependencies
|
||||||
|
color = { workspace = true }
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
bytemuck = { workspace = true }
|
bytemuck = { workspace = true }
|
||||||
node-macro = { workspace = true }
|
node-macro = { workspace = true }
|
||||||
|
|||||||
@@ -102,3 +102,37 @@ pub fn migrate_to_f64_array<'de, D: serde::Deserializer<'de>>(deserializer: D) -
|
|||||||
F64ArrayFormat::List(list) => list.element,
|
F64ArrayFormat::List(list) => list.element,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a CSS color string (named color, hex, `rgb(...)`, `hsl(...)`, etc.) into a linear-light [`Color`] using the `color` crate's CSS Color 4 parser.
|
||||||
|
/// Tries the input as-is first (catches CSS named colors like `red`, `rgb(...)`, and well-formed hex like `#abcdef`), then falls back to treating the input as bare hex with length-based expansion to a CSS-parseable form:
|
||||||
|
/// - 1 char `f` → `#fff` (CSS 3-char shorthand)
|
||||||
|
/// - 2 char `ab` → `#ababab` (repeated to 6 chars)
|
||||||
|
/// - 4 char `abcd` → `#00abcd` (left-padded with `00`)
|
||||||
|
/// - 5 char `abcde` → `#0abcde` (left-padded with `0`)
|
||||||
|
/// - 3, 6, 8 char inputs are passed through with a `#` prefix.
|
||||||
|
pub fn parse_css_color(input: &str) -> Option<crate::Color> {
|
||||||
|
let trimmed = input.trim();
|
||||||
|
|
||||||
|
let parsed = color::parse_color(trimmed).ok().or_else(|| {
|
||||||
|
let bare = trimmed.strip_prefix('#').unwrap_or(trimmed);
|
||||||
|
if bare.is_empty() || !bare.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let expanded = match bare.len() {
|
||||||
|
1 => bare.repeat(3),
|
||||||
|
2 => bare.repeat(3),
|
||||||
|
4 => format!("00{bare}"),
|
||||||
|
5 => format!("0{bare}"),
|
||||||
|
_ => bare.to_string(),
|
||||||
|
};
|
||||||
|
let candidate = format!("#{expanded}");
|
||||||
|
// Avoid retrying the exact same string we just failed to parse.
|
||||||
|
(candidate != trimmed).then(|| color::parse_color(&candidate).ok()).flatten()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let srgb: color::AlphaColor<color::Srgb> = parsed.to_alpha_color();
|
||||||
|
let [red, green, blue, alpha] = srgb.components;
|
||||||
|
// Reject out-of-gamut values that `color::parse_color` accepts for newer CSS syntax (e.g., `rgb(300 -50 200)`).
|
||||||
|
let in_gamut = alpha <= 1. && ![red, green, blue, alpha].iter().any(|c| c.is_sign_negative() || !c.is_finite());
|
||||||
|
in_gamut.then(|| crate::Color::from_gamma_srgb_channels(red, green, blue, alpha))
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,17 +92,112 @@ impl Alpha for RGBA16F {
|
|||||||
|
|
||||||
impl Pixel for RGBA16F {}
|
impl Pixel for RGBA16F {}
|
||||||
|
|
||||||
|
/// An sRGB color with 8-bit unassociated-alpha channels. Used as the wire format at the DOM boundary:
|
||||||
|
/// bijective with hex codes, byte-identical to CSS/SVG/PNG/peniko conventions. Internal computations use
|
||||||
|
/// the linear-light [`Color`] type. Convert via [`From<SRGBA8> for Color`] and [`From<Color> for SRGBA8`].
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
|
#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))]
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Pod, Zeroable)]
|
||||||
pub struct SRGBA8 {
|
pub struct SRGBA8 {
|
||||||
red: u8,
|
pub red: u8,
|
||||||
green: u8,
|
pub green: u8,
|
||||||
blue: u8,
|
pub blue: u8,
|
||||||
alpha: u8,
|
pub alpha: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SRGBA8 {
|
||||||
|
pub const TRANSPARENT: Self = Self::new(0, 0, 0, 0);
|
||||||
|
pub const BLACK: Self = Self::new(0, 0, 0, 255);
|
||||||
|
pub const WHITE: Self = Self::new(255, 255, 255, 255);
|
||||||
|
|
||||||
|
/// Construct from raw 8-bit channels. Alpha is unassociated (not premultiplied), matching CSS/SVG/PNG convention.
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
|
||||||
|
Self { red, green, blue, alpha }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct an opaque (alpha = 255) color from raw 8-bit RGB channels.
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn new_opaque(red: u8, green: u8, blue: u8) -> Self {
|
||||||
|
Self::new(red, green, blue, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse `RRGGBB` or `RRGGBBAA` (with or without a leading `#`). Returns `None` for any other format.
|
||||||
|
/// For full CSS Color 4 parsing (named colors, shorthand hex, `rgb(...)`, `hsl(...)`), parse in the caller and construct via [`Self::new`].
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn from_hex_str(hex: &str) -> Option<Self> {
|
||||||
|
let hex = hex.trim().trim_start_matches('#');
|
||||||
|
if hex.len() != 6 && hex.len() != 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let red = u8::from_str_radix(&hex[0..2], 16).ok()?;
|
||||||
|
let green = u8::from_str_radix(&hex[2..4], 16).ok()?;
|
||||||
|
let blue = u8::from_str_radix(&hex[4..6], 16).ok()?;
|
||||||
|
let alpha = if hex.len() == 8 { u8::from_str_radix(&hex[6..8], 16).ok()? } else { 255 };
|
||||||
|
|
||||||
|
Some(Self::new(red, green, blue, alpha))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `rrggbb` (lowercase, no `#` prefix, alpha discarded). Use where alpha is specified separately, e.g. SVG `fill="#..." fill-opacity="..."`.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn to_rgb_hex(self) -> String {
|
||||||
|
format!("{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `rrggbbaa` (lowercase, no `#` prefix).
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn to_rgba_hex(self) -> String {
|
||||||
|
format!("{:02x}{:02x}{:02x}{:02x}", self.red, self.green, self.blue, self.alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `#rrggbb` if fully opaque, `#rrggbbaa` otherwise. Suitable for direct insertion into a CSS property or SVG attribute.
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub fn to_css_hex(self) -> String {
|
||||||
|
if self.alpha == 255 {
|
||||||
|
format!("#{}", self.to_rgb_hex())
|
||||||
|
} else {
|
||||||
|
format!("#{}", self.to_rgba_hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns [`Self::BLACK`] or [`Self::WHITE`], whichever gives more legible text against this color
|
||||||
|
/// (alpha composited over white in gamma space, WCAG-style relative-luminance threshold).
|
||||||
|
pub fn contrasting_text_color(self) -> Self {
|
||||||
|
// Composite over white in gamma space, then convert to linear for the luminance test.
|
||||||
|
let r = self.red as f32 / 255.;
|
||||||
|
let g = self.green as f32 / 255.;
|
||||||
|
let b = self.blue as f32 / 255.;
|
||||||
|
let a = self.alpha as f32 / 255.;
|
||||||
|
let composited = Color::from_gamma_srgb_channels(1. - a + r * a, 1. - a + g * a, 1. - a + b * a, 1.);
|
||||||
|
let luminance = composited.luminance_rec_709();
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 4]> for SRGBA8 {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(bytes: [u8; 4]) -> Self {
|
||||||
|
let [red, green, blue, alpha] = bytes;
|
||||||
|
Self::new(red, green, blue, alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SRGBA8> for [u8; 4] {
|
||||||
|
#[inline(always)]
|
||||||
|
fn from(c: SRGBA8) -> Self {
|
||||||
|
let SRGBA8 { red, green, blue, alpha } = c;
|
||||||
|
[red, green, blue, alpha]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lets `Image<SRGBA8>` cross the wasm boundary as gamma bytes, since `Color` (linear-light) isn't exposed with Tsify.
|
||||||
|
impl Pixel for SRGBA8 {}
|
||||||
|
|
||||||
impl From<Color> for SRGBA8 {
|
impl From<Color> for SRGBA8 {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn from(c: Color) -> Self {
|
fn from(c: Color) -> Self {
|
||||||
@@ -127,53 +222,6 @@ impl From<SRGBA8> for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Luminance for SRGBA8 {
|
|
||||||
type LuminanceChannel = f32;
|
|
||||||
#[inline(always)]
|
|
||||||
fn luminance(&self) -> f32 {
|
|
||||||
// TODO: verify this is correct for sRGB
|
|
||||||
0.2126 * self.red() + 0.7152 * self.green() + 0.0722 * self.blue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RGB for SRGBA8 {
|
|
||||||
type ColorChannel = f32;
|
|
||||||
#[inline(always)]
|
|
||||||
fn red(&self) -> f32 {
|
|
||||||
self.red as f32 / 255.
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn green(&self) -> f32 {
|
|
||||||
self.green as f32 / 255.
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn blue(&self) -> f32 {
|
|
||||||
self.blue as f32 / 255.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rec709Primaries for SRGBA8 {}
|
|
||||||
impl SRGB for SRGBA8 {}
|
|
||||||
|
|
||||||
impl Alpha for SRGBA8 {
|
|
||||||
type AlphaChannel = f32;
|
|
||||||
#[inline(always)]
|
|
||||||
fn alpha(&self) -> f32 {
|
|
||||||
self.alpha as f32 / 255.
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRANSPARENT: Self = SRGBA8 { red: 0, green: 0, blue: 0, alpha: 0 };
|
|
||||||
|
|
||||||
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
|
|
||||||
let alpha = alpha * 255.;
|
|
||||||
let mut result = *self;
|
|
||||||
result.alpha = (alpha * self.alpha()) as u8;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Pixel for SRGBA8 {}
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
||||||
@@ -216,9 +264,13 @@ impl Pixel for Luma {}
|
|||||||
/// Internally alpha is stored as `f32` that ranges from `0.0` (transparent) to `1.0` (opaque).
|
/// Internally alpha is stored as `f32` that ranges from `0.0` (transparent) to `1.0` (opaque).
|
||||||
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
|
/// The other components (RGB) are stored as `f32` that range from `0.0` up to `f32::MAX`,
|
||||||
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
|
/// the values encode the brightness of each channel proportional to the light intensity in cd/m² (nits) in HDR, and `0.0` (black) to `1.0` (white) in SDR color.
|
||||||
|
/// Linear-light sRGB color with `f32` channels (alpha unassociated for swatch/UI colors, associated/premultiplied for pixel data inside [`Image<Color>`]).
|
||||||
|
///
|
||||||
|
/// Channels range from `0.0` to `f32::MAX`, encoding brightness proportional to light intensity (cd/m² nits in HDR, or `0..=1` mapped to white for SDR).
|
||||||
|
///
|
||||||
|
/// Anything crossing the Wasm/JS boundary must go through [`SRGBA8`] instead.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
#[cfg_attr(feature = "std", derive(dyn_any::DynAny))]
|
||||||
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize))]
|
|
||||||
#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))]
|
#[cfg_attr(feature = "std", derive(graphene_hash::CacheHash))]
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable, BufferStruct)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable, BufferStruct)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
@@ -231,6 +283,52 @@ pub struct Color {
|
|||||||
// `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.
|
// `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 Eq for Color {}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl serde::Serialize for Color {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
// Persist linear-light floats directly and tag with `"linear": true` so legacy gamma-encoded values (which lack this marker) can be detected and upgraded on load.
|
||||||
|
let mut state = serializer.serialize_struct("Color", 5)?;
|
||||||
|
state.serialize_field("red", &self.red)?;
|
||||||
|
state.serialize_field("green", &self.green)?;
|
||||||
|
state.serialize_field("blue", &self.blue)?;
|
||||||
|
state.serialize_field("alpha", &self.alpha)?;
|
||||||
|
// TODO: Remove the `linear` marker when switching to the new document format and Ctrl-C node serialization format
|
||||||
|
state.serialize_field("linear", &true)?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for Color {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
// Documents from before the linear-storage migration lack the `linear` marker and stored gamma-encoded floats; convert them on load.
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct MigrationColor {
|
||||||
|
red: f32,
|
||||||
|
green: f32,
|
||||||
|
blue: f32,
|
||||||
|
alpha: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
// TODO: Remove the `linear` marker when switching to the new document format and Ctrl-C node serialization format
|
||||||
|
linear: bool,
|
||||||
|
}
|
||||||
|
let raw = MigrationColor::deserialize(deserializer)?;
|
||||||
|
Ok(if raw.linear {
|
||||||
|
Color {
|
||||||
|
red: raw.red,
|
||||||
|
green: raw.green,
|
||||||
|
blue: raw.blue,
|
||||||
|
alpha: raw.alpha,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Color::from_gamma_srgb_channels(raw.red, raw.green, raw.blue, raw.alpha)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RGB for Color {
|
impl RGB for Color {
|
||||||
type ColorChannel = f32;
|
type ColorChannel = f32;
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
@@ -266,11 +364,14 @@ impl AlphaMut for Color {
|
|||||||
impl Pixel for Color {
|
impl Pixel for Color {
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
fn to_bytes(&self) -> Vec<u8> {
|
fn to_bytes(&self) -> Vec<u8> {
|
||||||
self.to_rgba8_srgb().to_vec()
|
let SRGBA8 { red, green, blue, alpha } = (*self).into();
|
||||||
|
[red, green, blue, alpha].to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_bytes(bytes: &[u8]) -> Self {
|
fn from_bytes(bytes: &[u8]) -> Self {
|
||||||
Color::from_rgba8_srgb(bytes[0], bytes[1], bytes[2], bytes[3])
|
// `Image<Color>` pixel convention is linear-light with associated (premultiplied) alpha.
|
||||||
|
let srgba = SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]);
|
||||||
|
Color::from(srgba).apply_opacity(bytes[3] as f32 / 255.)
|
||||||
}
|
}
|
||||||
fn byte_size() -> usize {
|
fn byte_size() -> usize {
|
||||||
4
|
4
|
||||||
@@ -372,56 +473,26 @@ impl Color {
|
|||||||
Some(color)
|
Some(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an opaque `Color` from given `f32` RGB channels.
|
/// Construct an opaque `Color` from `f32` RGB channels, with no value validation (use [`Self::from_rgbaf32`] for validation).
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn from_rgbf32_unchecked(red: f32, green: f32, blue: f32) -> Color {
|
pub const fn from_rgbf32_unchecked(red: f32, green: f32, blue: f32) -> Color {
|
||||||
Color { red, green, blue, alpha: 1. }
|
Color { red, green, blue, alpha: 1. }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an opaque `Color` from given `f32` RGB channels.
|
/// Construct a `Color` from `f32` RGBA channels, with no value validation (use [`Self::from_rgbaf32`] for validation).
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn from_rgbaf32_unchecked(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
|
pub const fn from_rgbaf32_unchecked(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
|
||||||
Color { red, green, blue, alpha }
|
Color { red, green, blue, alpha }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an opaque `Color` from given `f32` RGB channels.
|
/// Construct a `Color` from unassociated (straight) RGBA channels, premultiplying the RGB channels by alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_unassociated_alpha(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
|
pub fn new_from_unassociated_rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
|
||||||
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
|
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`, premultiplied by alpha.
|
/// Create a linear-light `Color` from HSL coordinates (all between 0 and 1).
|
||||||
///
|
/// HSL is defined on sRGB display values, so the RGB produced by the HSL math is gamma-encoded and decoded to linear before being wrapped in `Color`.
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// use core_types::color::Color;
|
|
||||||
/// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
|
|
||||||
/// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
|
|
||||||
/// assert_eq!(color, color2)
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_rgb8_srgb(red: u8, green: u8, blue: u8) -> Color {
|
|
||||||
Color::from_rgba8_srgb(red, green, blue, 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Should this be premult?
|
|
||||||
/// Return an SDR `Color` given RGBA channels from `0` to `255`, premultiplied by alpha.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// use core_types::color::Color;
|
|
||||||
/// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
|
|
||||||
let red = red as f32 / 255.;
|
|
||||||
let green = green as f32 / 255.;
|
|
||||||
let blue = blue as f32 / 255.;
|
|
||||||
let alpha = alpha as f32 / 255.;
|
|
||||||
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)
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@@ -460,10 +531,11 @@ impl Color {
|
|||||||
map_channel(&mut green, temp2, temp1);
|
map_channel(&mut green, temp2, temp1);
|
||||||
map_channel(&mut blue, temp2, temp1);
|
map_channel(&mut blue, temp2, temp1);
|
||||||
|
|
||||||
Color { red, green, blue, alpha }
|
Color::from_gamma_srgb_channels(red, green, blue, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a [Color] from hue, saturation, value, and alpha (all between 0 and 1).
|
/// Create a linear-light `Color` from HSV coordinates (all between 0 and 1).
|
||||||
|
/// HSV is defined on sRGB display values, so the RGB produced by the HSV math is gamma-encoded and decoded to linear before being wrapped in `Color`.
|
||||||
pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Color {
|
pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Color {
|
||||||
let h_prime = (hue * 6.) % 6.;
|
let h_prime = (hue * 6.) % 6.;
|
||||||
let i = h_prime as i32;
|
let i = h_prime as i32;
|
||||||
@@ -479,7 +551,7 @@ impl Color {
|
|||||||
4 => (t, p, value),
|
4 => (t, p, value),
|
||||||
_ => (value, p, q),
|
_ => (value, p, q),
|
||||||
};
|
};
|
||||||
Color { red, green, blue, alpha }
|
Color::from_gamma_srgb_channels(red, green, blue, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the `red` component.
|
/// Return the `red` component.
|
||||||
@@ -534,48 +606,56 @@ impl Color {
|
|||||||
self.alpha
|
self.alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the alpha channel is at (or within an epsilon of) fully opaque.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_opaque(&self) -> bool {
|
pub fn is_opaque(&self) -> bool {
|
||||||
self.alpha > 1. - f32::EPSILON
|
self.alpha > 1. - f32::EPSILON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mean of the three RGB channels.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn average_rgb_channels(&self) -> f32 {
|
pub fn average_rgb_channels(&self) -> f32 {
|
||||||
(self.red + self.green + self.blue) / 3.
|
(self.red + self.green + self.blue) / 3.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Minimum of the three RGB channels.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn minimum_rgb_channels(&self) -> f32 {
|
pub fn minimum_rgb_channels(&self) -> f32 {
|
||||||
self.red.min(self.green).min(self.blue)
|
self.red.min(self.green).min(self.blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum of the three RGB channels.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn maximum_rgb_channels(&self) -> f32 {
|
pub fn maximum_rgb_channels(&self) -> f32 {
|
||||||
self.red.max(self.green).max(self.blue)
|
self.red.max(self.green).max(self.blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Relative luminance using Rec.709 / sRGB-primary weights, computed on linear-light RGB.
|
||||||
// From https://stackoverflow.com/a/56678483/775283
|
// From https://stackoverflow.com/a/56678483/775283
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn luminance_srgb(&self) -> f32 {
|
pub fn luminance_rec_709(&self) -> f32 {
|
||||||
0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue
|
0.2126 * self.red + 0.7152 * self.green + 0.0722 * self.blue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Luma using Rec.601 SDTV coefficients.
|
||||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn luminance_rec_601(&self) -> f32 {
|
pub fn luminance_rec_601(&self) -> f32 {
|
||||||
0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
|
0.299 * self.red + 0.587 * self.green + 0.114 * self.blue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Luma using rounded Rec.601 coefficients (`0.3 / 0.59 / 0.11`), as used by some legacy image processing.
|
||||||
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
// From https://en.wikipedia.org/wiki/Luma_(video)#Rec._601_luma_versus_Rec._709_luma_coefficients
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn luminance_rec_601_rounded(&self) -> f32 {
|
pub fn luminance_rec_601_rounded(&self) -> f32 {
|
||||||
0.3 * self.red + 0.59 * self.green + 0.11 * self.blue
|
0.3 * self.red + 0.59 * self.green + 0.11 * self.blue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perceptual lightness (CIE L*) of the Rec.709 luminance, normalized to 0..1.
|
||||||
// From https://stackoverflow.com/a/56678483/775283
|
// From https://stackoverflow.com/a/56678483/775283
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn luminance_perceptual(&self) -> f32 {
|
pub fn luminance_perceptual(&self) -> f32 {
|
||||||
let luminance = self.luminance_srgb();
|
let luminance = self.luminance_rec_709();
|
||||||
|
|
||||||
if luminance <= 0.008856 {
|
if luminance <= 0.008856 {
|
||||||
(luminance * 903.3) / 100.
|
(luminance * 903.3) / 100.
|
||||||
@@ -584,6 +664,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct an opaque grayscale color where R = G = B = `luminance`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn from_luminance(luminance: f32) -> Color {
|
pub fn from_luminance(luminance: f32) -> Color {
|
||||||
Color {
|
Color {
|
||||||
@@ -594,26 +675,30 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shift all RGB channels by the offset that moves Rec.601-rounded luma to `luminance`, clamping channels to 0..1. Approximate; channels above 1 are lost.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn with_luminance(&self, luminance: f32) -> Color {
|
pub fn with_luminance(&self, luminance: f32) -> Color {
|
||||||
let delta = luminance - self.luminance_rec_601_rounded();
|
let delta = luminance - self.luminance_rec_601_rounded();
|
||||||
self.map_rgb(|c| (c + delta).clamp(0., 1.))
|
self.map_rgb(|c| (c + delta).clamp(0., 1.))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The RGB chroma range, `max - min` across the three channels. Not the HSL/HSV saturation (use [`Self::to_hsla`] or [`Self::to_hsva`] for those).
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn saturation(&self) -> f32 {
|
pub fn chroma_range(&self) -> f32 {
|
||||||
let max = (self.red).max(self.green).max(self.blue);
|
let max = (self.red).max(self.green).max(self.blue);
|
||||||
let min = (self.red).min(self.green).min(self.blue);
|
let min = (self.red).min(self.green).min(self.blue);
|
||||||
|
|
||||||
max - min
|
max - min
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace HSL saturation with the given value, preserving hue, lightness, and alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn with_saturation(&self, saturation: f32) -> Color {
|
pub fn with_saturation(&self, saturation: f32) -> Color {
|
||||||
let [hue, _, lightness, alpha] = self.to_hsla();
|
let [hue, _, lightness, alpha] = self.to_hsla();
|
||||||
Color::from_hsla(hue, saturation, lightness, alpha)
|
Color::from_hsla(hue, saturation, lightness, alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the alpha channel, leaving RGB unchanged.
|
||||||
pub fn with_alpha(&self, alpha: f32) -> Color {
|
pub fn with_alpha(&self, alpha: f32) -> Color {
|
||||||
Color {
|
Color {
|
||||||
red: self.red,
|
red: self.red,
|
||||||
@@ -623,6 +708,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the red channel, leaving the others unchanged.
|
||||||
pub fn with_red(&self, red: f32) -> Color {
|
pub fn with_red(&self, red: f32) -> Color {
|
||||||
Color {
|
Color {
|
||||||
red,
|
red,
|
||||||
@@ -632,6 +718,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the green channel, leaving the others unchanged.
|
||||||
pub fn with_green(&self, green: f32) -> Color {
|
pub fn with_green(&self, green: f32) -> Color {
|
||||||
Color {
|
Color {
|
||||||
red: self.red,
|
red: self.red,
|
||||||
@@ -641,6 +728,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the blue channel, leaving the others unchanged.
|
||||||
pub fn with_blue(&self, blue: f32) -> Color {
|
pub fn with_blue(&self, blue: f32) -> Color {
|
||||||
Color {
|
Color {
|
||||||
red: self.red,
|
red: self.red,
|
||||||
@@ -650,21 +738,25 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Normal" blend: returns the source channel unchanged.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_normal(_c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_normal(_c_b: f32, c_s: f32) -> f32 {
|
||||||
c_s
|
c_s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Multiply" blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_multiply(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_multiply(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_s * c_b
|
c_s * c_b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Darken" blend: the smaller of the two.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_darken(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_darken(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_s.min(c_b)
|
c_s.min(c_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Color Burn" blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_color_burn(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_color_burn(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_b == 1. {
|
if c_b == 1. {
|
||||||
@@ -676,41 +768,49 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Linear Burn" blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_linear_burn(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_linear_burn(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_b + c_s - 1.
|
c_b + c_s - 1.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_darker_color(&self, other: Color) -> Color {
|
pub fn blend_darker_color(&self, other: Color) -> Color {
|
||||||
if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other }
|
if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Screen" blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_screen(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_screen(c_b: f32, c_s: f32) -> f32 {
|
||||||
1. - (1. - c_s) * (1. - c_b)
|
1. - (1. - c_s) * (1. - c_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Lighten" blend: the larger of the two.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_lighten(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_lighten(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_s.max(c_b)
|
c_s.max(c_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Color Dodge" blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_color_dodge(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_color_dodge(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s == 1. { 1. } else { (c_b / (1. - c_s)).min(1.) }
|
if c_s == 1. { 1. } else { (c_b / (1. - c_s)).min(1.) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Linear Dodge" (Add) blend.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_linear_dodge(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_linear_dodge(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_b + c_s
|
c_b + c_s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
||||||
if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other }
|
if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Soft Light" blend.
|
||||||
pub fn blend_softlight(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_softlight(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s <= 0.5 {
|
if c_s <= 0.5 {
|
||||||
c_b - (1. - 2. * c_s) * c_b * (1. - c_b)
|
c_b - (1. - 2. * c_s) * c_b * (1. - c_b)
|
||||||
@@ -720,6 +820,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Hard Light" blend.
|
||||||
pub fn blend_hardlight(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_hardlight(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s <= 0.5 {
|
if c_s <= 0.5 {
|
||||||
Color::blend_multiply(2. * c_s, c_b)
|
Color::blend_multiply(2. * c_s, c_b)
|
||||||
@@ -728,6 +829,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Vivid Light" blend.
|
||||||
pub fn blend_vivid_light(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_vivid_light(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s <= 0.5 {
|
if c_s <= 0.5 {
|
||||||
Color::blend_color_burn(2. * c_s, c_b)
|
Color::blend_color_burn(2. * c_s, c_b)
|
||||||
@@ -736,6 +838,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Linear Light" blend.
|
||||||
pub fn blend_linear_light(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_linear_light(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s <= 0.5 {
|
if c_s <= 0.5 {
|
||||||
Color::blend_linear_burn(2. * c_s, c_b)
|
Color::blend_linear_burn(2. * c_s, c_b)
|
||||||
@@ -744,6 +847,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Pin Light" blend.
|
||||||
pub fn blend_pin_light(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_pin_light(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_s <= 0.5 {
|
if c_s <= 0.5 {
|
||||||
Color::blend_darken(2. * c_s, c_b)
|
Color::blend_darken(2. * c_s, c_b)
|
||||||
@@ -752,45 +856,54 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Hard Mix" blend: thresholds Linear Light at 0.5.
|
||||||
pub fn blend_hard_mix(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_hard_mix(c_b: f32, c_s: f32) -> f32 {
|
||||||
if Color::blend_linear_light(c_b, c_s) < 0.5 { 0. } else { 1. }
|
if Color::blend_linear_light(c_b, c_s) < 0.5 { 0. } else { 1. }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Difference" blend.
|
||||||
pub fn blend_difference(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_difference(c_b: f32, c_s: f32) -> f32 {
|
||||||
(c_b - c_s).abs()
|
(c_b - c_s).abs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Exclusion" blend.
|
||||||
pub fn blend_exclusion(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_exclusion(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_b + c_s - 2. * c_b * c_s
|
c_b + c_s - 2. * c_b * c_s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Subtract" blend.
|
||||||
pub fn blend_subtract(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_subtract(c_b: f32, c_s: f32) -> f32 {
|
||||||
c_b - c_s
|
c_b - c_s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-channel "Divide" blend.
|
||||||
pub fn blend_divide(c_b: f32, c_s: f32) -> f32 {
|
pub fn blend_divide(c_b: f32, c_s: f32) -> f32 {
|
||||||
if c_b == 0. { 1. } else { c_b / c_s }
|
if c_b == 0. { 1. } else { c_b / c_s }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Hue" blend: source hue with this color's saturation and Rec.601 luma.
|
||||||
pub fn blend_hue(&self, c_s: Color) -> Color {
|
pub fn blend_hue(&self, c_s: Color) -> Color {
|
||||||
let sat_b = self.saturation();
|
let sat_b = self.chroma_range();
|
||||||
let lum_b = self.luminance_rec_601();
|
let lum_b = self.luminance_rec_601();
|
||||||
c_s.with_saturation(sat_b).with_luminance(lum_b)
|
c_s.with_saturation(sat_b).with_luminance(lum_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Saturation" blend: this color's hue/luma with source saturation.
|
||||||
pub fn blend_saturation(&self, c_s: Color) -> Color {
|
pub fn blend_saturation(&self, c_s: Color) -> Color {
|
||||||
let sat_s = c_s.saturation();
|
let sat_s = c_s.chroma_range();
|
||||||
let lum_b = self.luminance_rec_601();
|
let lum_b = self.luminance_rec_601();
|
||||||
|
|
||||||
self.with_saturation(sat_s).with_luminance(lum_b)
|
self.with_saturation(sat_s).with_luminance(lum_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Color" blend: source hue/saturation with this color's luma.
|
||||||
pub fn blend_color(&self, c_s: Color) -> Color {
|
pub fn blend_color(&self, c_s: Color) -> Color {
|
||||||
let lum_b = self.luminance_rec_601();
|
let lum_b = self.luminance_rec_601();
|
||||||
|
|
||||||
c_s.with_luminance(lum_b)
|
c_s.with_luminance(lum_b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whole-color "Luminosity" blend: this color's hue/saturation with source luma.
|
||||||
pub fn blend_luminosity(&self, c_s: Color) -> Color {
|
pub fn blend_luminosity(&self, c_s: Color) -> Color {
|
||||||
let lum_s = c_s.luminance_rec_601();
|
let lum_s = c_s.luminance_rec_601();
|
||||||
|
|
||||||
@@ -810,98 +923,43 @@ impl Color {
|
|||||||
(self.red, self.green, self.blue, self.alpha)
|
(self.red, self.green, self.blue, self.alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in linear space.
|
/// Convert this color to HSV coordinates (all between 0 and 1).
|
||||||
///
|
/// HSV is defined on sRGB display values, so this color's linear RGB is gamma-encoded before the HSV math.
|
||||||
/// # Examples
|
pub fn to_hsva(&self) -> [f32; 4] {
|
||||||
/// ```
|
#[cfg(feature = "std")]
|
||||||
/// use core_types::color::Color;
|
let rem = |x: f32, m: f32| x.rem_euclid(m);
|
||||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
#[cfg(not(feature = "std"))]
|
||||||
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
let rem = |x: f32, m: f32| x.rem_euclid(&m);
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn to_rgba_hex_srgb(&self) -> String {
|
|
||||||
let gamma = self.to_gamma_srgb();
|
|
||||||
format!(
|
|
||||||
"{:02x?}{:02x?}{:02x?}{:02x?}",
|
|
||||||
(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 hex string (without a # prefix). Use this if the [`Color`] is in linear space.
|
let [red, green, blue, alpha] = self.to_gamma_srgb_channels();
|
||||||
/// ```
|
let max = red.max(green).max(blue);
|
||||||
/// use core_types::color::Color;
|
let min = red.min(green).min(blue);
|
||||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
let delta = max - min;
|
||||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
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). Use this if the [`Color`] is in gamma space.
|
let mut hue = if delta == 0. {
|
||||||
/// ```
|
0.
|
||||||
/// use core_types::color::Color;
|
} else if max == red {
|
||||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
|
rem((green - blue) / delta, 6.)
|
||||||
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
|
} else if max == green {
|
||||||
/// ```
|
(blue - red) / delta + 2.
|
||||||
#[cfg(feature = "std")]
|
} else {
|
||||||
pub fn to_rgb_hex_srgb_from_gamma(&self) -> String {
|
(red - green) / delta + 4.
|
||||||
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
};
|
||||||
}
|
hue = rem(hue * 60. + 360., 360.) / 360.;
|
||||||
|
|
||||||
/// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
|
let saturation = if max == 0. { 0. } else { delta / max };
|
||||||
#[cfg(feature = "std")]
|
let value = max;
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [`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.
|
[hue, saturation, value, alpha]
|
||||||
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] {
|
|
||||||
[(self.red * 255.) as u8, (self.green * 255.) as u8, (self.blue * 255.) as u8, (self.alpha * 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.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn to_rgba8_srgb(&self) -> [u8; 4] {
|
|
||||||
self.to_gamma_srgb().to_rgba8()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the all RGB components as a u8 slice, first component is red, followed by green, followed by blue. Use this if the [`Color`] is in gamma space.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn to_rgb8(&self) -> [u8; 3] {
|
|
||||||
[(self.red * 255.) as u8, (self.green * 255.) as u8, (self.blue * 255.) as u8]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the all RGB components as a u8 slice, first component is red, followed by green, followed by blue. Use this if the [`Color`] is in linear space.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn to_rgb8_srgb(&self) -> [u8; 3] {
|
|
||||||
self.to_gamma_srgb().to_rgb8()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
|
||||||
/// Convert a [Color] to a hue, saturation, lightness and alpha (all between 0 and 1)
|
/// Convert this color to HSL coordinates (all between 0 and 1).
|
||||||
|
/// HSL is defined on sRGB display values, so this color's linear RGB is gamma-encoded before the HSL math.
|
||||||
pub fn to_hsla(&self) -> [f32; 4] {
|
pub fn to_hsla(&self) -> [f32; 4] {
|
||||||
let min_channel = self.red.min(self.green).min(self.blue);
|
let [red, green, blue, alpha] = self.to_gamma_srgb_channels();
|
||||||
let max_channel = self.red.max(self.green).max(self.blue);
|
let min_channel = red.min(green).min(blue);
|
||||||
|
let max_channel = red.max(green).max(blue);
|
||||||
|
|
||||||
let lightness = (min_channel + max_channel) / 2.;
|
let lightness = (min_channel + max_channel) / 2.;
|
||||||
let saturation = if min_channel == max_channel {
|
let saturation = if min_channel == max_channel {
|
||||||
@@ -911,39 +969,22 @@ impl Color {
|
|||||||
} else {
|
} else {
|
||||||
(max_channel - min_channel) / (2. - max_channel - min_channel)
|
(max_channel - min_channel) / (2. - max_channel - min_channel)
|
||||||
};
|
};
|
||||||
let hue = if self.red >= self.green && self.red >= self.blue {
|
let hue = if red >= green && red >= blue {
|
||||||
(self.green - self.blue) / (max_channel - min_channel)
|
(green - blue) / (max_channel - min_channel)
|
||||||
} else if self.green >= self.red && self.green >= self.blue {
|
} else if green >= red && green >= blue {
|
||||||
2. + (self.blue - self.red) / (max_channel - min_channel)
|
2. + (blue - red) / (max_channel - min_channel)
|
||||||
} else {
|
} else {
|
||||||
4. + (self.red - self.green) / (max_channel - min_channel)
|
4. + (red - green) / (max_channel - min_channel)
|
||||||
} / 6.;
|
} / 6.;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
let hue = hue.rem_euclid(1.);
|
let hue = hue.rem_euclid(1.);
|
||||||
#[cfg(not(feature = "std"))]
|
#[cfg(not(feature = "std"))]
|
||||||
let hue = hue.rem_euclid(&1.);
|
let hue = hue.rem_euclid(&1.);
|
||||||
|
|
||||||
[hue, saturation, lightness, self.alpha]
|
[hue, saturation, lightness, alpha]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a color from a hex color code string with an optional `#` prefix, such as `#RRGGBB`, `RRGGBB`, `#RRGGBBAA`, or `RRGGBBAA`.
|
/// Linearly interpolate each RGBA channel between `self` (`t = 0`) and `other` (`t = 1`); `t` must be in 0..=1.
|
||||||
/// Returns `None` for invalid or unrecognized strings.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn from_hex_str(hex: &str) -> Option<Color> {
|
|
||||||
let hex = hex.trim().trim_start_matches('#');
|
|
||||||
if hex.len() != 6 && hex.len() != 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let red = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
|
|
||||||
let green = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
|
|
||||||
let blue = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
|
|
||||||
let alpha = if hex.len() == 8 { u8::from_str_radix(&hex[6..8], 16).ok()? as f32 / 255. } else { 1. };
|
|
||||||
Some(Color { red, green, blue, alpha })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linearly interpolates between two colors based on t.
|
|
||||||
///
|
|
||||||
/// T must be between 0 and 1.
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn lerp(&self, other: &Color, t: f32) -> Self {
|
pub fn lerp(&self, other: &Color, t: f32) -> Self {
|
||||||
assert!((0. ..=1.).contains(&t));
|
assert!((0. ..=1.).contains(&t));
|
||||||
@@ -955,70 +996,62 @@ impl Color {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generic power curve `c.powf(1 / exponent)` applied per RGB channel. Distinct from the sRGB transfer curve (see [`Self::to_gamma_srgb_channels`]).
|
||||||
|
/// The expected output must still be treated as linear-light.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn gamma(&self, gamma: f32) -> Color {
|
pub fn apply_gamma_exponent(&self, exponent: f32) -> Color {
|
||||||
let gamma = gamma.max(0.0001);
|
let exponent = exponent.max(0.0001);
|
||||||
|
|
||||||
// From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
|
// From https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-6-gamma-correction/
|
||||||
let inverse_gamma = 1. / gamma;
|
let inverse = 1. / exponent;
|
||||||
self.map_rgb(|c: f32| c.powf(inverse_gamma))
|
self.map_rgb(|c: f32| c.powf(inverse))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decompose into the four channel components after sRGB gamma encoding (linear → gamma). Alpha is unchanged.
|
||||||
|
/// Use [`Self::from_gamma_srgb_channels`] to wrap these gamma-encoded channels back into a linear-light `Color`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_linear_srgb(&self) -> Self {
|
pub fn to_gamma_srgb_channels(&self) -> [f32; 4] {
|
||||||
Self {
|
[super::linear_to_srgb(self.red), super::linear_to_srgb(self.green), super::linear_to_srgb(self.blue), self.alpha]
|
||||||
red: Self::srgb_to_linear(self.red),
|
}
|
||||||
green: Self::srgb_to_linear(self.green),
|
|
||||||
blue: Self::srgb_to_linear(self.blue),
|
/// Construct a `Color` from sRGB gamma-encoded channel components, decoding RGB to linear-light. Alpha is unchanged.
|
||||||
alpha: self.alpha,
|
#[inline(always)]
|
||||||
|
pub fn from_gamma_srgb_channels(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
|
||||||
|
Color {
|
||||||
|
red: super::srgb_to_linear(red),
|
||||||
|
green: super::srgb_to_linear(green),
|
||||||
|
blue: super::srgb_to_linear(blue),
|
||||||
|
alpha,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply `f` to each RGB channel after sRGB gamma encoding, returning a linear-light `Color`. Alpha is unchanged.
|
||||||
|
/// Equivalent to unpacking via [`Self::to_gamma_srgb_channels`], mapping per channel, and rewrapping via [`Self::from_gamma_srgb_channels`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_gamma_srgb(&self) -> Self {
|
pub fn map_gamma_rgb<F: Fn(f32) -> f32>(&self, f: F) -> Color {
|
||||||
Self {
|
let [r, g, b, a] = self.to_gamma_srgb_channels();
|
||||||
red: Self::linear_to_srgb(self.red),
|
Color::from_gamma_srgb_channels(f(r), f(g), f(b), a)
|
||||||
green: Self::linear_to_srgb(self.green),
|
|
||||||
blue: Self::linear_to_srgb(self.blue),
|
|
||||||
alpha: self.alpha,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn srgb_to_linear(channel: f32) -> f32 {
|
|
||||||
if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn linear_to_srgb(channel: f32) -> f32 {
|
|
||||||
if channel <= 0.0031308 { channel * 12.92 } else { 1.055 * channel.powf(1. / 2.4) - 0.055 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply `f` to each of the four RGBA channels independently.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn map_rgba<F: Fn(f32) -> f32>(&self, f: F) -> Self {
|
pub fn map_rgba<F: Fn(f32) -> f32>(&self, f: F) -> Self {
|
||||||
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), f(self.a()))
|
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), f(self.a()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply `f` to each of the three RGB channels; alpha is unchanged.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn map_rgb<F: Fn(f32) -> f32>(&self, f: F) -> Self {
|
pub fn map_rgb<F: Fn(f32) -> f32>(&self, f: F) -> Self {
|
||||||
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a())
|
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multiply all four channels (including alpha) by `opacity`, applying an additional premultiplication factor to this Color.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn apply_opacity(&self, opacity: f32) -> Self {
|
pub fn apply_opacity(&self, opacity: f32) -> Self {
|
||||||
Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity)
|
Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
/// Divide RGB by alpha to recover unassociated (straight-alpha) channels; no-op if alpha is zero.
|
||||||
pub fn to_associated_alpha(&self, alpha: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
red: self.red * alpha,
|
|
||||||
green: self.green * alpha,
|
|
||||||
blue: self.blue * alpha,
|
|
||||||
alpha: self.alpha * alpha,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_unassociated_alpha(&self) -> Self {
|
pub fn to_unassociated_alpha(&self) -> Self {
|
||||||
if self.alpha == 0. {
|
if self.alpha == 0. {
|
||||||
@@ -1033,6 +1066,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a per-channel blend function to this color (unmultiplied) and `other`, returning a color with `other`'s alpha; channels are clamped to 0..1.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
|
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
|
||||||
let background = self.to_unassociated_alpha();
|
let background = self.to_unassociated_alpha();
|
||||||
@@ -1044,6 +1078,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Porter-Duff "source over" composite of `other` over `self`. Both colors must use associated (premultiplied) alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn alpha_blend(&self, other: Color) -> Self {
|
pub fn alpha_blend(&self, other: Color) -> Self {
|
||||||
let inv_alpha = 1. - other.alpha;
|
let inv_alpha = 1. - other.alpha;
|
||||||
@@ -1055,6 +1090,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace alpha with `self.alpha + other.alpha`, clamped to 0..1; RGB is unchanged.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn alpha_add(&self, other: Color) -> Self {
|
pub fn alpha_add(&self, other: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1063,6 +1099,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace alpha with `self.alpha - other.alpha`, clamped to 0..1; RGB is unchanged.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn alpha_subtract(&self, other: Color) -> Self {
|
pub fn alpha_subtract(&self, other: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1071,6 +1108,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace alpha with `self.alpha * other.alpha`, clamped to 0..1; RGB is unchanged.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn alpha_multiply(&self, other: Color) -> Self {
|
pub fn alpha_multiply(&self, other: Color) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1079,6 +1117,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct from a `glam::Vec4` where `(x, y, z, w)` map to `(red, green, blue, alpha)`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn from_vec4(vec: Vec4) -> Self {
|
pub const fn from_vec4(vec: Vec4) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1089,6 +1128,7 @@ impl Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pack into a `glam::Vec4` as `(red, green, blue, alpha)`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn to_vec4(&self) -> Vec4 {
|
pub fn to_vec4(&self) -> Vec4 {
|
||||||
Vec4::new(self.red, self.green, self.blue, self.alpha)
|
Vec4::new(self.red, self.green, self.blue, self.alpha)
|
||||||
@@ -1123,7 +1163,7 @@ mod tests {
|
|||||||
(82, 84, 84),
|
(82, 84, 84),
|
||||||
(255, 255, 178),
|
(255, 255, 178),
|
||||||
] {
|
] {
|
||||||
let col = Color::from_rgb8_srgb(red, green, blue);
|
let col: Color = SRGBA8::new(red, green, blue, 255).into();
|
||||||
let [hue, saturation, lightness, alpha] = col.to_hsla();
|
let [hue, saturation, lightness, alpha] = col.to_hsla();
|
||||||
let result = Color::from_hsla(hue, saturation, lightness, alpha);
|
let result = Color::from_hsla(hue, saturation, lightness, alpha);
|
||||||
assert!((col.r() - result.r()) < f32::EPSILON * 100.);
|
assert!((col.r() - result.r()) < f32::EPSILON * 100.);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
mod color_traits;
|
mod color_traits;
|
||||||
mod color_types;
|
mod color_types;
|
||||||
mod discrete_srgb;
|
mod discrete_srgb;
|
||||||
|
mod transfer;
|
||||||
|
|
||||||
pub use color_traits::*;
|
pub use color_traits::*;
|
||||||
pub use color_types::*;
|
pub use color_types::*;
|
||||||
pub use discrete_srgb::*;
|
pub use discrete_srgb::*;
|
||||||
|
pub use transfer::*;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//! Analytic per-channel sRGB transfer functions (gamma encoding/decoding).
|
||||||
|
//!
|
||||||
|
//! These work in `f32` at full precision. For round-trip-exact `u8` ⇄ `f32` conversion at the
|
||||||
|
//! display byte boundary, use the lookup tables in [`super::discrete_srgb`] instead.
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use num_traits::float::Float;
|
||||||
|
|
||||||
|
/// Decode an sRGB gamma-encoded channel value to linear-light.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn srgb_to_linear(channel: f32) -> f32 {
|
||||||
|
if channel <= 0.04045 { channel / 12.92 } else { ((channel + 0.055) / 1.055).powf(2.4) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a linear-light channel value to sRGB gamma-encoded.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn linear_to_srgb(channel: f32) -> f32 {
|
||||||
|
if channel <= 0.0031308 { channel * 12.92 } else { 1.055 * channel.powf(1. / 2.4) - 0.055 }
|
||||||
|
}
|
||||||
@@ -144,7 +144,14 @@ impl<P: Pixel> Image<P> {
|
|||||||
impl Image<Color> {
|
impl Image<Color> {
|
||||||
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
||||||
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
||||||
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8_srgb(v[0], v[1], v[2], v[3])).collect();
|
let data = image_data
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|v| {
|
||||||
|
// `Image<Color>` pixels are stored linear-light with premultiplied alpha
|
||||||
|
let srgba = SRGBA8::new(v[0], v[1], v[2], v[3]);
|
||||||
|
Color::from(srgba).apply_opacity(v[3] as f32 / 255.)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Image {
|
Image {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -263,30 +270,6 @@ impl<P: Pixel> AsRef<Image<P>> for Image<P> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Image<Color>> for Image<SRGBA8> {
|
|
||||||
fn from(image: Image<Color>) -> Self {
|
|
||||||
let data = image.data.into_iter().map(|x| x.into()).collect();
|
|
||||||
Self {
|
|
||||||
data,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
base64_string: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Image<SRGBA8>> for Image<Color> {
|
|
||||||
fn from(image: Image<SRGBA8>) -> Self {
|
|
||||||
let data = image.data.into_iter().map(|x| x.into()).collect();
|
|
||||||
Self {
|
|
||||||
data,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
base64_string: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::renderer::{RenderParams, format_transform_matrix};
|
use crate::renderer::{RenderParams, format_transform_matrix};
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use core_types::uuid::generate_uuid;
|
use core_types::uuid::generate_uuid;
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graphic_types::vector_types::gradient::{Gradient, GradientType};
|
use graphic_types::vector_types::gradient::{Gradient, GradientType};
|
||||||
@@ -22,7 +23,7 @@ impl RenderExt for Gradient {
|
|||||||
if position != 0. {
|
if position != 0. {
|
||||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
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());
|
let _ = write!(stop, r##" stop-color="#{}""##, SRGBA8::from(color).to_rgb_hex());
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||||
}
|
}
|
||||||
@@ -86,7 +87,7 @@ impl RenderExt for Fill {
|
|||||||
match self {
|
match self {
|
||||||
Self::None => r#" fill="none""#.to_string(),
|
Self::None => r#" fill="none""#.to_string(),
|
||||||
Self::Solid(color) => {
|
Self::Solid(color) => {
|
||||||
let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
let mut result = format!(r##" fill="#{}""##, SRGBA8::from(*color).to_rgb_hex());
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,7 @@ impl RenderExt for Stroke {
|
|||||||
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);
|
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || render_params.override_paint_order).then_some(PaintOrder::StrokeBelow);
|
||||||
|
|
||||||
// Render the needed stroke attributes
|
// Render the needed stroke attributes
|
||||||
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
let mut attributes = format!(r##" stroke="#{}""##, SRGBA8::from(color).to_rgb_hex());
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use crate::render_ext::RenderExt;
|
use crate::render_ext::RenderExt;
|
||||||
use crate::to_peniko::BlendModeExt;
|
use crate::to_peniko::{BlendModeExt, ToPenikoColor};
|
||||||
use core_types::CacheHash;
|
use core_types::CacheHash;
|
||||||
use core_types::blending::BlendMode;
|
use core_types::blending::BlendMode;
|
||||||
use core_types::bounds::BoundingBox;
|
use core_types::bounds::BoundingBox;
|
||||||
use core_types::bounds::RenderBoundingBox;
|
use core_types::bounds::RenderBoundingBox;
|
||||||
use core_types::color::Color;
|
use core_types::color::Color;
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{Item, List};
|
use core_types::list::{Item, List};
|
||||||
use core_types::math::quad::Quad;
|
use core_types::math::quad::Quad;
|
||||||
use core_types::render_complexity::RenderComplexity;
|
use core_types::render_complexity::RenderComplexity;
|
||||||
@@ -273,19 +274,20 @@ pub fn black_or_white_for_best_contrast(background: Option<Color>) -> Color {
|
|||||||
|
|
||||||
let alpha = bg.a();
|
let alpha = bg.a();
|
||||||
|
|
||||||
// Un-premultiply, then convert to gamma sRGB
|
// Un-premultiply, then encode to gamma sRGB to do the composite in display space.
|
||||||
let srgb = if alpha > f32::EPSILON {
|
let (gamma_r, gamma_g, gamma_b) = if alpha > f32::EPSILON {
|
||||||
Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb()
|
let [r, g, b, _] = Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb_channels();
|
||||||
|
(r, g, b)
|
||||||
} else {
|
} else {
|
||||||
Color::TRANSPARENT
|
(0., 0., 0.)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Composite over black in sRGB space, then convert back to linear for luminance
|
// Composite over black in sRGB space (premultiplied by alpha), then decode to linear for the luminance test.
|
||||||
let composited = Color::from_rgbaf32_unchecked(srgb.r() * alpha, srgb.g() * alpha, srgb.b() * alpha, 1.).to_linear_srgb();
|
let composited = Color::from_gamma_srgb_channels(gamma_r * alpha, gamma_g * alpha, gamma_b * alpha, 1.);
|
||||||
|
|
||||||
let threshold = (1.05 * 0.05f32).sqrt() - 0.05;
|
let threshold = (1.05 * 0.05f32).sqrt() - 0.05;
|
||||||
|
|
||||||
if composited.luminance_srgb() > threshold { Color::BLACK } else { Color::WHITE }
|
if composited.luminance_rec_709() > threshold { Color::BLACK } else { Color::WHITE }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
||||||
@@ -311,7 +313,7 @@ fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::C
|
|||||||
};
|
};
|
||||||
|
|
||||||
let outline_color = black_or_white_for_best_contrast(render_params.artboard_background);
|
let outline_color = black_or_white_for_best_contrast(render_params.artboard_background);
|
||||||
let outline_color_peniko = peniko::Color::new([outline_color.r(), outline_color.g(), outline_color.b(), outline_color.a()]);
|
let outline_color_peniko = SRGBA8::from(outline_color).to_peniko_color();
|
||||||
|
|
||||||
(outline_stroke, outline_color_peniko)
|
(outline_stroke, outline_color_peniko)
|
||||||
}
|
}
|
||||||
@@ -573,7 +575,7 @@ impl Render for List<Artboard> {
|
|||||||
|
|
||||||
// Background
|
// Background
|
||||||
render.leaf_tag("rect", |attributes| {
|
render.leaf_tag("rect", |attributes| {
|
||||||
attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma()));
|
attributes.push("fill", format!("#{}", SRGBA8::from(background).to_rgb_hex()));
|
||||||
if background.a() < 1. {
|
if background.a() < 1. {
|
||||||
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
|
attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string());
|
||||||
}
|
}
|
||||||
@@ -629,7 +631,7 @@ impl Render for List<Artboard> {
|
|||||||
|
|
||||||
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
|
let artboard_transform = kurbo::Affine::new(transform.to_cols_array());
|
||||||
|
|
||||||
let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]);
|
let color = SRGBA8::from(background).to_peniko_color();
|
||||||
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
|
scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect);
|
||||||
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
|
scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect);
|
||||||
scene.pop_layer();
|
scene.pop_layer();
|
||||||
@@ -1182,7 +1184,7 @@ impl Render for List<Vector> {
|
|||||||
// Closures to avoid duplicated fill/stroke drawing logic
|
// Closures to avoid duplicated fill/stroke drawing logic
|
||||||
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match element.style.fill() {
|
let do_fill_path = |scene: &mut Scene, path: &kurbo::BezPath, fill_rule: peniko::Fill| match element.style.fill() {
|
||||||
Fill::Solid(color) => {
|
Fill::Solid(color) => {
|
||||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
let fill = peniko::Brush::Solid(SRGBA8::from(*color).to_peniko_color());
|
||||||
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
|
scene.fill(fill_rule, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, path);
|
||||||
}
|
}
|
||||||
Fill::Gradient(gradient) => {
|
Fill::Gradient(gradient) => {
|
||||||
@@ -1190,7 +1192,7 @@ impl Render for List<Vector> {
|
|||||||
for (position, color, _) in gradient.stops.interpolated_samples() {
|
for (position, color, _) in gradient.stops.interpolated_samples() {
|
||||||
stops.push(peniko::ColorStop {
|
stops.push(peniko::ColorStop {
|
||||||
offset: position as f32,
|
offset: position as f32,
|
||||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1267,7 +1269,7 @@ impl Render for List<Vector> {
|
|||||||
let do_stroke = |scene: &mut Scene, width_scale: f64| {
|
let do_stroke = |scene: &mut Scene, width_scale: f64| {
|
||||||
if let Some(stroke) = element.style.stroke() {
|
if let Some(stroke) = element.style.stroke() {
|
||||||
let color = match stroke.color {
|
let color = match stroke.color {
|
||||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
Some(color) => SRGBA8::from(color).to_peniko_color(),
|
||||||
None => peniko::Color::TRANSPARENT,
|
None => peniko::Color::TRANSPARENT,
|
||||||
};
|
};
|
||||||
let cap = match stroke.cap {
|
let cap = match stroke.cap {
|
||||||
@@ -1811,7 +1813,7 @@ impl Render for List<Color> {
|
|||||||
const MAX: f64 = 1e7;
|
const MAX: f64 = 1e7;
|
||||||
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
attributes.push("points", format!("{MAX},{MAX} -{MAX},{MAX} -{MAX},-{MAX} {MAX},-{MAX}"));
|
||||||
|
|
||||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
attributes.push("fill", format!("#{}", SRGBA8::from(*color).to_rgb_hex()));
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||||
}
|
}
|
||||||
@@ -1838,7 +1840,7 @@ impl Render for List<Color> {
|
|||||||
let blend_mode = blend_mode_attr.to_peniko();
|
let blend_mode = blend_mode_attr.to_peniko();
|
||||||
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
|
||||||
|
|
||||||
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
|
let vello_color = SRGBA8::from(*color).to_peniko_color();
|
||||||
|
|
||||||
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
|
||||||
|
|
||||||
@@ -1895,7 +1897,7 @@ impl Render for List<GradientStops> {
|
|||||||
|
|
||||||
let mut stop_string = String::new();
|
let mut stop_string = String::new();
|
||||||
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
||||||
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, color.to_rgb_hex_srgb_from_gamma());
|
let _ = write!(stop_string, r##"<stop offset="{}" stop-color="#{}""##, position, SRGBA8::from(color).to_rgb_hex());
|
||||||
if color.a() < 1. {
|
if color.a() < 1. {
|
||||||
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
let _ = write!(stop_string, r#" stop-opacity="{}""#, color.a());
|
||||||
}
|
}
|
||||||
@@ -1977,7 +1979,7 @@ impl Render for List<GradientStops> {
|
|||||||
for (position, color, _) in gradient.interpolated_samples() {
|
for (position, color, _) in gradient.interpolated_samples() {
|
||||||
stops.push(peniko::ColorStop {
|
stops.push(peniko::ColorStop {
|
||||||
offset: position as f32,
|
offset: position as f32,
|
||||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
color: peniko::color::DynamicColor::from_alpha_color(SRGBA8::from(color).to_peniko_color()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
use core_types::BlendMode;
|
use core_types::BlendMode;
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use vello::peniko;
|
use vello::peniko;
|
||||||
|
|
||||||
pub trait BlendModeExt {
|
pub trait BlendModeExt {
|
||||||
fn to_peniko(&self) -> peniko::Mix;
|
fn to_peniko(&self) -> peniko::Mix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ToPenikoColor {
|
||||||
|
fn to_peniko_color(&self) -> peniko::Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToPenikoColor for SRGBA8 {
|
||||||
|
#[inline(always)]
|
||||||
|
fn to_peniko_color(&self) -> peniko::Color {
|
||||||
|
peniko::Color::from_rgba8(self.red, self.green, self.blue, self.alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BlendModeExt for BlendMode {
|
impl BlendModeExt for BlendMode {
|
||||||
fn to_peniko(&self) -> peniko::Mix {
|
fn to_peniko(&self) -> peniko::Mix {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use core_types::{Color, render_complexity::RenderComplexity};
|
use core_types::Color;
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
|
use core_types::render_complexity::RenderComplexity;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
@@ -13,9 +15,9 @@ pub enum GradientType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
|
||||||
// TODO: Use linear not gamma colors
|
/// A list of colors (linear, unassociated alpha) associated with positions (in the range 0 to 1) along a gradient.
|
||||||
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
|
///
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
/// Not exposed via Tsify; use [`GradientStopsUI`] at the JS boundary.
|
||||||
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
#[derive(Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
pub struct GradientStops {
|
pub struct GradientStops {
|
||||||
@@ -27,6 +29,59 @@ pub struct GradientStops {
|
|||||||
pub color: Vec<Color>,
|
pub color: Vec<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// JS-boundary version of [`GradientStops`] where stop colors are [`SRGBA8`] byte triples instead of linear-light [`Color`].
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default, DynAny)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct GradientStopsUI {
|
||||||
|
pub position: Vec<f64>,
|
||||||
|
pub midpoint: Vec<f64>,
|
||||||
|
pub color: Vec<SRGBA8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&GradientStops> for GradientStopsUI {
|
||||||
|
fn from(s: &GradientStops) -> Self {
|
||||||
|
Self {
|
||||||
|
position: s.position.clone(),
|
||||||
|
midpoint: s.midpoint.clone(),
|
||||||
|
color: s.color.iter().map(|c| SRGBA8::from(*c)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&GradientStopsUI> for GradientStops {
|
||||||
|
fn from(s: &GradientStopsUI) -> Self {
|
||||||
|
Self {
|
||||||
|
position: s.position.clone(),
|
||||||
|
midpoint: s.midpoint.clone(),
|
||||||
|
color: s.color.iter().map(|c| Color::from(*c)).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GradientStopsUI {
|
||||||
|
/// CSS `linear-gradient(...)` string. Stops are emitted as `#rrggbbaa` hex (already gamma-encoded bytes).
|
||||||
|
pub fn to_css_linear_gradient(&self) -> String {
|
||||||
|
if self.position.len() <= 1 {
|
||||||
|
let hex = self.color.first().map(|c| c.to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
|
||||||
|
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
||||||
|
}
|
||||||
|
// Sample via the midpoint-aware subdivision used for SVG/Vello stops so browser interpolation matches
|
||||||
|
let stops: GradientStops = self.into();
|
||||||
|
let pieces = stops
|
||||||
|
.interpolated_samples()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(position, color, _)| {
|
||||||
|
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||||
|
let hex = SRGBA8::from(color).to_rgba_hex();
|
||||||
|
format!("#{hex} {percent}%")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!("linear-gradient(to right, {pieces})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Eventually remove this migration document upgrade code
|
// TODO: Eventually remove this migration document upgrade code
|
||||||
impl<'de> serde::Deserialize<'de> for GradientStops {
|
impl<'de> serde::Deserialize<'de> for GradientStops {
|
||||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
@@ -294,7 +349,7 @@ 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.
|
/// 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 {
|
pub fn to_css_linear_gradient(&self) -> String {
|
||||||
if self.position.len() <= 1 {
|
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());
|
let hex = self.color.first().map(|c| SRGBA8::from(*c).to_rgba_hex()).unwrap_or_else(|| "000000ff".to_string());
|
||||||
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
return format!("linear-gradient(to right, #{hex} 0%, #{hex} 100%)");
|
||||||
}
|
}
|
||||||
let pieces = self
|
let pieces = self
|
||||||
@@ -302,7 +357,7 @@ impl GradientStops {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(position, color, _)| {
|
.map(|(position, color, _)| {
|
||||||
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
let percent = ((position * 100.) * 1e2).round() / 1e2;
|
||||||
format!("#{} {percent}%", color.to_rgba_hex_srgb_from_gamma())
|
format!("#{} {percent}%", SRGBA8::from(color).to_rgba_hex())
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
@@ -313,13 +368,17 @@ impl GradientStops {
|
|||||||
///
|
///
|
||||||
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
|
/// 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.
|
/// midpoint for actual gradient stops, and `None` for interpolated samples added to approximate midpoint curves.
|
||||||
|
///
|
||||||
|
/// Interpolation is performed in sRGB gamma space (then lifted back to linear-light for output) because the downstream SVG/CSS
|
||||||
|
/// renderer interpolates between adjacent `<stop>` colors in gamma space; doing the subdivision math in the same space ensures
|
||||||
|
/// the chosen samples actually match the curve the browser will draw.
|
||||||
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
|
pub fn interpolated_samples(&self) -> Vec<(f64, Color, Option<f64>)> {
|
||||||
/// Controls accuracy vs. number of samples tradeoff.
|
/// 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.
|
/// 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.;
|
const THRESHOLD: f64 = 2. / 255.;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[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) {
|
fn subdivide(left: f64, right: f64, midpoint: f64, pos_a: f64, pos_b: f64, color_a_gamma: [f32; 4], color_b_gamma: [f32; 4], result: &mut Vec<(f64, Color, Option<f64>)>, depth: u32) {
|
||||||
const MAX_DEPTH: u32 = 20;
|
const MAX_DEPTH: u32 = 20;
|
||||||
if depth >= MAX_DEPTH {
|
if depth >= MAX_DEPTH {
|
||||||
return;
|
return;
|
||||||
@@ -333,13 +392,18 @@ impl GradientStops {
|
|||||||
let y_linear = (y_left + y_right) / 2.;
|
let y_linear = (y_left + y_right) / 2.;
|
||||||
|
|
||||||
if (y_actual - y_linear).abs() > THRESHOLD {
|
if (y_actual - y_linear).abs() > THRESHOLD {
|
||||||
subdivide(left, mid, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
subdivide(left, mid, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
|
||||||
|
|
||||||
let global_pos = pos_a + mid * (pos_b - pos_a);
|
let global_pos = pos_a + mid * (pos_b - pos_a);
|
||||||
let color = color_a.lerp(&color_b, y_actual as f32);
|
let t = y_actual as f32;
|
||||||
|
let r = color_a_gamma[0] + (color_b_gamma[0] - color_a_gamma[0]) * t;
|
||||||
|
let g = color_a_gamma[1] + (color_b_gamma[1] - color_a_gamma[1]) * t;
|
||||||
|
let b = color_a_gamma[2] + (color_b_gamma[2] - color_a_gamma[2]) * t;
|
||||||
|
let a = color_a_gamma[3] + (color_b_gamma[3] - color_a_gamma[3]) * t;
|
||||||
|
let color = Color::from_gamma_srgb_channels(r, g, b, a);
|
||||||
result.push((global_pos, color, None));
|
result.push((global_pos, color, None));
|
||||||
|
|
||||||
subdivide(mid, right, midpoint, pos_a, pos_b, color_a, color_b, result, depth + 1);
|
subdivide(mid, right, midpoint, pos_a, pos_b, color_a_gamma, color_b_gamma, result, depth + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +432,7 @@ impl GradientStops {
|
|||||||
|
|
||||||
// Only subdivide if midpoint deviates from linear (0.5)
|
// Only subdivide if midpoint deviates from linear (0.5)
|
||||||
if (midpoint - 0.5).abs() >= 1e-6 {
|
if (midpoint - 0.5).abs() >= 1e-6 {
|
||||||
subdivide(0., 1., midpoint, pos_a, pos_b, color_a, color_b, &mut result, 0);
|
subdivide(0., 1., midpoint, pos_a, pos_b, color_a.to_gamma_srgb_channels(), color_b.to_gamma_srgb_channels(), &mut result, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the end stop
|
// Add the end stop
|
||||||
@@ -440,7 +504,7 @@ impl std::fmt::Display for Gradient {
|
|||||||
let stops = self
|
let stops = self
|
||||||
.stops
|
.stops
|
||||||
.iter()
|
.iter()
|
||||||
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), stop.color.to_rgba_hex_srgb()))
|
.map(|stop| format!("[{}%: #{}]", round(stop.position * 100.), SRGBA8::from(stop.color).to_rgba_hex()))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
write!(f, "{} Gradient: {stops}", self.gradient_type)
|
||||||
@@ -454,12 +518,12 @@ impl Gradient {
|
|||||||
GradientStop {
|
GradientStop {
|
||||||
position: 0.,
|
position: 0.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: start_color.to_gamma_srgb(),
|
color: start_color,
|
||||||
},
|
},
|
||||||
GradientStop {
|
GradientStop {
|
||||||
position: 1.,
|
position: 1.,
|
||||||
midpoint: 0.5,
|
midpoint: 0.5,
|
||||||
color: end_color.to_gamma_srgb(),
|
color: end_color,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
pub use crate::gradient::*;
|
pub use crate::gradient::*;
|
||||||
use core_types::ATTR_OPACITY;
|
use core_types::ATTR_OPACITY;
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::color::Alpha;
|
use core_types::color::{Alpha, SRGBA8};
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::transform::Transform;
|
use core_types::transform::Transform;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
@@ -30,7 +30,7 @@ impl std::fmt::Display for Fill {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::None => write!(f, "None"),
|
Self::None => write!(f, "None"),
|
||||||
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", color.to_rgb_hex_srgb(), color.a() * 100.),
|
Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", SRGBA8::from(*color).to_rgb_hex(), color.a() * 100.),
|
||||||
Self::Gradient(gradient) => write!(f, "{gradient}"),
|
Self::Gradient(gradient) => write!(f, "{gradient}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,19 +161,75 @@ impl From<Gradient> for Fill {
|
|||||||
/// Can be None, a solid [Color], or a linear/radial [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.
|
||||||
|
///
|
||||||
|
/// Use [`FillChoiceUI`] at the JS boundary.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
#[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum FillChoice {
|
pub enum FillChoice {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
/// WARNING: Color is gamma, not linear!
|
|
||||||
Solid(Color),
|
Solid(Color),
|
||||||
/// WARNING: Color stops are gamma, not linear!
|
|
||||||
Gradient(GradientStops),
|
Gradient(GradientStops),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Deprecate [`FillChoice`] and keep this, renamed, as the main widget-controlling type
|
||||||
|
/// JS-boundary version of [`FillChoice`] where the solid color is [`SRGBA8`] and the gradient is [`GradientStopsUI`].
|
||||||
|
#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(from_wasm_abi))]
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, DynAny)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub enum FillChoiceUI {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Solid(SRGBA8),
|
||||||
|
Gradient(GradientStopsUI),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FillChoice> for FillChoiceUI {
|
||||||
|
fn from(value: &FillChoice) -> Self {
|
||||||
|
match value {
|
||||||
|
FillChoice::None => Self::None,
|
||||||
|
FillChoice::Solid(color) => Self::Solid(SRGBA8::from(*color)),
|
||||||
|
FillChoice::Gradient(stops) => Self::Gradient(GradientStopsUI::from(stops)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&FillChoiceUI> for FillChoice {
|
||||||
|
fn from(value: &FillChoiceUI) -> Self {
|
||||||
|
match value {
|
||||||
|
FillChoiceUI::None => Self::None,
|
||||||
|
FillChoiceUI::Solid(srgba) => Self::Solid(Color::from(*srgba)),
|
||||||
|
FillChoiceUI::Gradient(stops) => Self::Gradient(GradientStops::from(stops)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FillChoiceUI {
|
||||||
|
pub fn as_solid(&self) -> Option<SRGBA8> {
|
||||||
|
let Self::Solid(c) = self else { return None };
|
||||||
|
Some(*c)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_gradient(&self) -> Option<&GradientStopsUI> {
|
||||||
|
let Self::Gradient(g) = self else { return None };
|
||||||
|
Some(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a CSS `background-image` string (always a `linear-gradient(...)`) representing this fill, or `None` if the fill is [`FillChoiceUI::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(srgba) => {
|
||||||
|
let hex = srgba.to_rgba_hex();
|
||||||
|
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||||
|
}
|
||||||
|
Self::Gradient(stops) => Some(stops.to_css_linear_gradient()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FillChoice {
|
impl FillChoice {
|
||||||
pub fn as_solid(&self) -> Option<Color> {
|
pub fn as_solid(&self) -> Option<Color> {
|
||||||
let Self::Solid(color) = self else { return None };
|
let Self::Solid(color) = self else { return None };
|
||||||
@@ -190,7 +246,7 @@ impl FillChoice {
|
|||||||
match self {
|
match self {
|
||||||
Self::None => None,
|
Self::None => None,
|
||||||
Self::Solid(color) => {
|
Self::Solid(color) => {
|
||||||
let hex = color.to_rgba_hex_srgb_from_gamma();
|
let hex = SRGBA8::from(*color).to_rgba_hex();
|
||||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||||
}
|
}
|
||||||
Self::Gradient(stops) => Some(stops.to_css_linear_gradient()),
|
Self::Gradient(stops) => Some(stops.to_css_linear_gradient()),
|
||||||
@@ -526,7 +582,7 @@ impl Default for Stroke {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
weight: 0.,
|
weight: 0.,
|
||||||
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
|
color: Some(Color::BLACK),
|
||||||
dash_lengths: Vec::new(),
|
dash_lengths: Vec::new(),
|
||||||
dash_offset: 0.,
|
dash_offset: 0.,
|
||||||
cap: StrokeCap::Butt,
|
cap: StrokeCap::Butt,
|
||||||
@@ -553,7 +609,7 @@ impl std::fmt::Display for PathStyle {
|
|||||||
let fill = &self.fill;
|
let fill = &self.fill;
|
||||||
|
|
||||||
let stroke = match &self.stroke {
|
let stroke = match &self.stroke {
|
||||||
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight),
|
Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| SRGBA8::from(c).to_rgba_hex()), stroke.weight),
|
||||||
None => "None".to_string(),
|
None => "None".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crate::shader_runtime::ShaderRuntime;
|
|||||||
use crate::texture_cache::TextureCache;
|
use crate::texture_cache::TextureCache;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
use glam::{Affine2, UVec2};
|
use glam::{Affine2, UVec2};
|
||||||
use graphene_application_io::{ApplicationIo, EditorApi};
|
use graphene_application_io::{ApplicationIo, EditorApi};
|
||||||
@@ -55,9 +56,9 @@ impl WgpuExecutor {
|
|||||||
|
|
||||||
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
let [r, g, b, a] = background.unwrap_or(Color::TRANSPARENT).to_rgba8();
|
let SRGBA8 { red, green, blue, alpha } = background.unwrap_or(Color::TRANSPARENT).into();
|
||||||
let render_params = RenderParams {
|
let render_params = RenderParams {
|
||||||
base_color: vello::peniko::Color::from_rgba8(r, g, b, a),
|
base_color: vello::peniko::Color::from_rgba8(red, green, blue, alpha),
|
||||||
width: size.x,
|
width: size.x,
|
||||||
height: size.y,
|
height: size.y,
|
||||||
antialiasing_method: AaConfig::Msaa16,
|
antialiasing_method: AaConfig::Msaa16,
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ impl RasterGpuToRasterCpuConverter {
|
|||||||
let start = row * row_stride;
|
let start = row * row_stride;
|
||||||
let row_slice = &view[start..start + row_bytes];
|
let row_slice = &view[start..start + row_bytes];
|
||||||
for px in row_slice.chunks_exact(4) {
|
for px in row_slice.chunks_exact(4) {
|
||||||
cpu_data.push(Color::from_rgba8_srgb(px[0], px[1], px[2], px[3]));
|
// `Image<Color>` pixels are stored linear-light with associated (premultiplied) alpha
|
||||||
|
let srgba = SRGBA8::new(px[0], px[1], px[2], px[3]);
|
||||||
|
cpu_data.push(Color::from(srgba).apply_opacity(px[3] as f32 / 255.));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use canvas_utils::{Canvas, CanvasHandle};
|
use canvas_utils::{Canvas, CanvasHandle};
|
||||||
|
use core_types::color::SRGBA8;
|
||||||
use core_types::list::{Item, List};
|
use core_types::list::{Item, List};
|
||||||
#[cfg(target_family = "wasm")]
|
#[cfg(target_family = "wasm")]
|
||||||
use core_types::math::bbox::Bbox;
|
use core_types::math::bbox::Bbox;
|
||||||
@@ -123,7 +124,15 @@ fn string_to_bytes(_: impl Ctx, string: String) -> List<u8> {
|
|||||||
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
|
||||||
fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
|
fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
|
||||||
let Some(image) = image.element(0) else { return List::new() };
|
let Some(image) = image.element(0) else { return List::new() };
|
||||||
image.data.iter().flat_map(|color| color.to_rgba8_srgb()).map(Item::new_from_element).collect()
|
image
|
||||||
|
.data
|
||||||
|
.iter()
|
||||||
|
.flat_map(|color| {
|
||||||
|
let SRGBA8 { red, green, blue, alpha } = (*color).into();
|
||||||
|
[red, green, blue, alpha]
|
||||||
|
})
|
||||||
|
.map(Item::new_from_element)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
|
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
|
||||||
@@ -154,7 +163,11 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> List<Raster<CPU>> {
|
|||||||
let image = Image {
|
let image = Image {
|
||||||
data: image
|
data: image
|
||||||
.chunks(4)
|
.chunks(4)
|
||||||
.map(|pixel| Color::from_unassociated_alpha(pixel[0], pixel[1], pixel[2], pixel[3]).to_linear_srgb())
|
.map(|pixel| {
|
||||||
|
// Decoded bytes are unassociated gamma sRGB; premultiply in gamma then lift to linear
|
||||||
|
let a = pixel[3];
|
||||||
|
Color::from_gamma_srgb_channels(pixel[0] * a, pixel[1] * a, pixel[2] * a, a)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
width: image.width(),
|
width: image.width(),
|
||||||
height: image.height(),
|
height: image.height(),
|
||||||
|
|||||||
@@ -823,7 +823,8 @@ fn rgba_to_color(_: impl Ctx, _primary: (), red: Fraction, green: Fraction, blue
|
|||||||
let blue = (blue as f32).clamp(0., 1.);
|
let blue = (blue as f32).clamp(0., 1.);
|
||||||
let alpha = (alpha as f32).clamp(0., 1.);
|
let alpha = (alpha as f32).clamp(0., 1.);
|
||||||
|
|
||||||
List::new_from_element(Color::from_rgbaf32_unchecked(red, green, blue, alpha))
|
// RGB user inputs are interpreted as sRGB display values; lift to linear-light for the internal `Color`
|
||||||
|
List::new_from_element(Color::from_gamma_srgb_channels(red, green, blue, alpha))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a color value from hue, saturation, value, and alpha components given as numbers from 0 to 1.
|
/// Constructs a color value from hue, saturation, value, and alpha components given as numbers from 0 to 1.
|
||||||
@@ -848,11 +849,11 @@ fn hsla_to_color(_: impl Ctx, _primary: (), hue: Fraction, #[default(1.)] satura
|
|||||||
List::new_from_element(Color::from_hsla(hue, saturation, lightness, alpha))
|
List::new_from_element(Color::from_hsla(hue, saturation, lightness, alpha))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a color value from an sRGB color code string, such as `#RRGGBB` or `#RRGGBBAA`. Invalid hex code strings produce no color.
|
/// Constructs a color value from a CSS color string. Accepts hex (`#RRGGBB`, `#RRGGBBAA`, plus bare and shorthand variants), CSS named colors (like `red`), and functional notations (`rgb(...)`, `hsl(...)`, etc.). Invalid inputs produce no color.
|
||||||
#[node_macro::node(category("Color"), name("Hex to Color"))]
|
#[node_macro::node(category("Color"), name("Hex to Color"))]
|
||||||
fn hex_to_color(_: impl Ctx, hex_code: String) -> List<Color> {
|
fn hex_to_color(_: impl Ctx, hex_code: String) -> List<Color> {
|
||||||
match Color::from_hex_str(&hex_code) {
|
match core_types::misc::parse_css_color(&hex_code) {
|
||||||
Some(c) => List::new_from_element(c),
|
Some(color) => List::new_from_element(color),
|
||||||
None => List::new(),
|
None => List::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::cubic_spline::CubicSplines;
|
|||||||
use core::fmt::Debug;
|
use core::fmt::Debug;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use glam::{Vec3, Vec4};
|
use glam::Vec3;
|
||||||
use no_std_types::color::Color;
|
use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear};
|
||||||
use no_std_types::context::Ctx;
|
use no_std_types::context::Ctx;
|
||||||
use no_std_types::registry::types::{AngleF32, PercentageF32, SignedPercentageF32};
|
use no_std_types::registry::types::{AngleF32, PercentageF32, SignedPercentageF32};
|
||||||
use node_macro::BufferStruct;
|
use node_macro::BufferStruct;
|
||||||
@@ -63,7 +63,7 @@ fn luminance<T: Adjust<Color>>(
|
|||||||
) -> T {
|
) -> T {
|
||||||
input.adjust(|color| {
|
input.adjust(|color| {
|
||||||
let luminance = match luminance_calc {
|
let luminance = match luminance_calc {
|
||||||
LuminanceCalculation::SRGB => color.luminance_srgb(),
|
LuminanceCalculation::SRGB => color.luminance_rec_709(),
|
||||||
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
||||||
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
||||||
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
||||||
@@ -91,7 +91,7 @@ fn gamma_correction<T: Adjust<Color>>(
|
|||||||
inverse: bool,
|
inverse: bool,
|
||||||
) -> T {
|
) -> T {
|
||||||
let exponent = if inverse { 1. / gamma } else { gamma };
|
let exponent = if inverse { 1. / gamma } else { gamma };
|
||||||
input.adjust(|color| color.gamma(exponent));
|
input.adjust(|color| color.apply_gamma_exponent(exponent));
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ fn brightness_contrast_classic<T: Adjust<Color>>(
|
|||||||
|
|
||||||
let offset = brightness * contrast + brightness - contrast / 2.;
|
let offset = brightness * contrast + brightness - contrast / 2.;
|
||||||
|
|
||||||
input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)).to_linear_srgb());
|
input.adjust(|color| color.map_gamma_rgb(|c| (c + c * contrast + offset).clamp(0., 1.)));
|
||||||
|
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
@@ -240,7 +240,7 @@ fn brightness_contrast<T: Adjust<Color>>(
|
|||||||
});
|
});
|
||||||
let lut_max = (combined_lut.len() - 1) as f32;
|
let lut_max = (combined_lut.len() - 1) as f32;
|
||||||
|
|
||||||
input.adjust(|color| color.to_gamma_srgb().map_rgb(|c| combined_lut[(c * lut_max).round() as usize]).to_linear_srgb());
|
input.adjust(|color| color.map_gamma_rgb(|c| combined_lut[(c * lut_max).round() as usize]));
|
||||||
|
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,8 @@ fn levels<T: Adjust<Color>>(
|
|||||||
#[default(100.)] output_maximums: PercentageF32,
|
#[default(100.)] output_maximums: PercentageF32,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
// Levels math operates in gamma space
|
||||||
|
let [mut r, mut g, mut b, a] = color.to_gamma_srgb_channels();
|
||||||
|
|
||||||
// Input Range (Range: 0-1)
|
// Input Range (Range: 0-1)
|
||||||
let input_shadows = shadows / 100.;
|
let input_shadows = shadows / 100.;
|
||||||
@@ -301,15 +302,24 @@ fn levels<T: Adjust<Color>>(
|
|||||||
|
|
||||||
// Input levels (Range: 0-1)
|
// Input levels (Range: 0-1)
|
||||||
let highlights_minus_shadows = (input_highlights - input_shadows).clamp(f32::EPSILON, 1.);
|
let highlights_minus_shadows = (input_highlights - input_shadows).clamp(f32::EPSILON, 1.);
|
||||||
let color = color.map_rgb(|c| ((c - input_shadows).max(0.) / highlights_minus_shadows).min(1.));
|
let input_map = |c: f32| ((c - input_shadows).max(0.) / highlights_minus_shadows).min(1.);
|
||||||
|
r = input_map(r);
|
||||||
|
g = input_map(g);
|
||||||
|
b = input_map(b);
|
||||||
|
|
||||||
// Midtones (Range: 0-1)
|
// Midtones gamma curve (Range: 0-1)
|
||||||
let color = color.gamma(gamma);
|
let inverse_gamma = 1. / gamma.max(0.0001);
|
||||||
|
r = r.powf(inverse_gamma);
|
||||||
|
g = g.powf(inverse_gamma);
|
||||||
|
b = b.powf(inverse_gamma);
|
||||||
|
|
||||||
// Output levels (Range: 0-1)
|
// Output levels (Range: 0-1)
|
||||||
let color = color.map_rgb(|c| c * (output_maximums - output_minimums) + output_minimums);
|
let output_map = |c: f32| c * (output_maximums - output_minimums) + output_minimums;
|
||||||
|
r = output_map(r);
|
||||||
|
g = output_map(g);
|
||||||
|
b = output_map(b);
|
||||||
|
|
||||||
color.to_linear_srgb()
|
Color::from_gamma_srgb_channels(r, g, b, a)
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
@@ -353,7 +363,8 @@ fn black_and_white<T: Adjust<Color>>(
|
|||||||
magentas: PercentageF32,
|
magentas: PercentageF32,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
// Black & White channel weights are tuned for gamma-space values
|
||||||
|
let [r, g, b, alpha_part] = color.to_gamma_srgb_channels();
|
||||||
|
|
||||||
let reds = reds / 100.;
|
let reds = reds / 100.;
|
||||||
let yellows = yellows / 100.;
|
let yellows = yellows / 100.;
|
||||||
@@ -362,12 +373,11 @@ fn black_and_white<T: Adjust<Color>>(
|
|||||||
let blues = blues / 100.;
|
let blues = blues / 100.;
|
||||||
let magentas = magentas / 100.;
|
let magentas = magentas / 100.;
|
||||||
|
|
||||||
let gray_base = color.r().min(color.g()).min(color.b());
|
let gray_base = r.min(g).min(b);
|
||||||
|
|
||||||
let red_part = color.r() - gray_base;
|
let red_part = r - gray_base;
|
||||||
let green_part = color.g() - gray_base;
|
let green_part = g - gray_base;
|
||||||
let blue_part = color.b() - gray_base;
|
let blue_part = b - gray_base;
|
||||||
let alpha_part = color.a();
|
|
||||||
|
|
||||||
let additional = if red_part == 0. {
|
let additional = if red_part == 0. {
|
||||||
let cyan_part = green_part.min(blue_part);
|
let cyan_part = green_part.min(blue_part);
|
||||||
@@ -383,11 +393,15 @@ fn black_and_white<T: Adjust<Color>>(
|
|||||||
let luminance = gray_base + additional;
|
let luminance = gray_base + additional;
|
||||||
|
|
||||||
// TODO: Fix "Color" blend mode implementation so it matches the expected behavior perfectly (it's currently close)
|
// TODO: Fix "Color" blend mode implementation so it matches the expected behavior perfectly (it's currently close)
|
||||||
let color = tint.with_luminance(luminance);
|
// Apply luminance substitution in gamma space
|
||||||
|
let [tr, tg, tb, _] = tint.to_gamma_srgb_channels();
|
||||||
|
let tint_luma_rec_601 = 0.3 * tr + 0.59 * tg + 0.11 * tb;
|
||||||
|
let delta = luminance - tint_luma_rec_601;
|
||||||
|
let result_r = (tr + delta).clamp(0., 1.);
|
||||||
|
let result_g = (tg + delta).clamp(0., 1.);
|
||||||
|
let result_b = (tb + delta).clamp(0., 1.);
|
||||||
|
|
||||||
let color = Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), alpha_part);
|
Color::from_gamma_srgb_channels(result_r, result_g, result_b, alpha_part)
|
||||||
|
|
||||||
color.to_linear_srgb()
|
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
@@ -410,20 +424,17 @@ fn hue_saturation<T: Adjust<Color>>(
|
|||||||
lightness_shift: SignedPercentageF32,
|
lightness_shift: SignedPercentageF32,
|
||||||
) -> T {
|
) -> T {
|
||||||
input.adjust(|color| {
|
input.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
// HSL operates on gamma-space channels
|
||||||
|
|
||||||
let [hue, saturation, lightness, alpha] = color.to_hsla();
|
let [hue, saturation, lightness, alpha] = color.to_hsla();
|
||||||
|
|
||||||
let color = Color::from_hsla(
|
Color::from_hsla(
|
||||||
(hue + hue_shift / 360.) % 1.,
|
(hue + hue_shift / 360.) % 1.,
|
||||||
// TODO: Improve the way saturation works (it's slightly off)
|
// TODO: Improve the way saturation works (it's slightly off)
|
||||||
(saturation + saturation_shift / 100.).clamp(0., 1.),
|
(saturation + saturation_shift / 100.).clamp(0., 1.),
|
||||||
// TODO: Fix the way lightness works (it's very off)
|
// TODO: Fix the way lightness works (it's very off)
|
||||||
(lightness + lightness_shift / 100.).clamp(0., 1.),
|
(lightness + lightness_shift / 100.).clamp(0., 1.),
|
||||||
alpha,
|
alpha,
|
||||||
);
|
)
|
||||||
|
|
||||||
color.to_linear_srgb()
|
|
||||||
});
|
});
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
@@ -442,11 +453,9 @@ fn invert<T: Adjust<Color>>(
|
|||||||
mut input: T,
|
mut input: T,
|
||||||
) -> T {
|
) -> T {
|
||||||
input.adjust(|color| {
|
input.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
// Invert in gamma space relative to alpha
|
||||||
|
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||||
let color = color.map_rgb(|c| color.a() - c);
|
Color::from_gamma_srgb_channels(a - r, a - g, a - b, a)
|
||||||
|
|
||||||
color.to_linear_srgb()
|
|
||||||
});
|
});
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
@@ -468,11 +477,11 @@ fn threshold<T: Adjust<Color>>(
|
|||||||
luminance_calc: LuminanceCalculation,
|
luminance_calc: LuminanceCalculation,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let min_luminance = Color::srgb_to_linear(min_luminance / 100.);
|
let min_luminance = srgb_to_linear(min_luminance / 100.);
|
||||||
let max_luminance = Color::srgb_to_linear(max_luminance / 100.);
|
let max_luminance = srgb_to_linear(max_luminance / 100.);
|
||||||
|
|
||||||
let luminance = match luminance_calc {
|
let luminance = match luminance_calc {
|
||||||
LuminanceCalculation::SRGB => color.luminance_srgb(),
|
LuminanceCalculation::SRGB => color.luminance_rec_709(),
|
||||||
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
LuminanceCalculation::Perceptual => color.luminance_perceptual(),
|
||||||
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
LuminanceCalculation::AverageChannels => color.average_rgb_channels(),
|
||||||
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
LuminanceCalculation::MinimumChannels => color.minimum_rgb_channels(),
|
||||||
@@ -512,17 +521,22 @@ fn vibrance<T: Adjust<Color>>(
|
|||||||
vibrance: SignedPercentageF32,
|
vibrance: SignedPercentageF32,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
|
let r_raw = color.r();
|
||||||
|
let g_raw = color.g();
|
||||||
|
let b_raw = color.b();
|
||||||
|
let alpha_in = color.a();
|
||||||
|
|
||||||
let vibrance = vibrance / 100.;
|
let vibrance = vibrance / 100.;
|
||||||
// Slow the effect down by half when it's negative, since artifacts begin appearing past -50%.
|
// Slow the effect down by half when it's negative, since artifacts begin appearing past -50%.
|
||||||
// So this scales the 0% to -50% range to 0% to -100%.
|
// So this scales the 0% to -50% range to 0% to -100%.
|
||||||
let slowed_vibrance = if vibrance >= 0. { vibrance } else { vibrance * 0.5 };
|
let slowed_vibrance = if vibrance >= 0. { vibrance } else { vibrance * 0.5 };
|
||||||
|
|
||||||
let channel_max = color.r().max(color.g()).max(color.b());
|
let channel_max = r_raw.max(g_raw).max(b_raw);
|
||||||
let channel_min = color.r().min(color.g()).min(color.b());
|
let channel_min = r_raw.min(g_raw).min(b_raw);
|
||||||
let channel_difference = channel_max - channel_min;
|
let channel_difference = channel_max - channel_min;
|
||||||
|
|
||||||
let scale_multiplier = if channel_max == color.r() {
|
let scale_multiplier = if channel_max == r_raw {
|
||||||
let green_blue_difference = (color.g() - color.b()).abs();
|
let green_blue_difference = (g_raw - b_raw).abs();
|
||||||
let t = (green_blue_difference / channel_difference).min(1.);
|
let t = (green_blue_difference / channel_difference).min(1.);
|
||||||
t * 0.5 + 0.5
|
t * 0.5 + 0.5
|
||||||
} else {
|
} else {
|
||||||
@@ -532,30 +546,48 @@ fn vibrance<T: Adjust<Color>>(
|
|||||||
let channel_reduction = channel_min * scale;
|
let channel_reduction = channel_min * scale;
|
||||||
let scale = 1. + scale * (1. - channel_difference);
|
let scale = 1. + scale * (1. - channel_difference);
|
||||||
|
|
||||||
let luminance_initial = color.to_linear_srgb().luminance_srgb();
|
let r_lin0 = srgb_to_linear(r_raw);
|
||||||
let altered_color = color.map_rgb(|c| c * scale - channel_reduction).to_linear_srgb();
|
let g_lin0 = srgb_to_linear(g_raw);
|
||||||
let luminance = altered_color.luminance_srgb();
|
let b_lin0 = srgb_to_linear(b_raw);
|
||||||
let altered_color = altered_color.map_rgb(|c| c * luminance_initial / luminance);
|
let luminance_initial = 0.2126 * r_lin0 + 0.7152 * g_lin0 + 0.0722 * b_lin0;
|
||||||
|
|
||||||
let channel_max = altered_color.r().max(altered_color.g()).max(altered_color.b());
|
let mut alt_r = srgb_to_linear(r_raw * scale - channel_reduction);
|
||||||
let altered_color = if Color::linear_to_srgb(channel_max) > 1. {
|
let mut alt_g = srgb_to_linear(g_raw * scale - channel_reduction);
|
||||||
|
let mut alt_b = srgb_to_linear(b_raw * scale - channel_reduction);
|
||||||
|
let luminance = 0.2126 * alt_r + 0.7152 * alt_g + 0.0722 * alt_b;
|
||||||
|
// Skip the luminance-preservation scaling when the result is black (e.g. black input pixel), avoiding division by zero.
|
||||||
|
if luminance > 0. {
|
||||||
|
alt_r *= luminance_initial / luminance;
|
||||||
|
alt_g *= luminance_initial / luminance;
|
||||||
|
alt_b *= luminance_initial / luminance;
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel_max = alt_r.max(alt_g).max(alt_b);
|
||||||
|
if linear_to_srgb(channel_max) > 1. {
|
||||||
let scale = (1. - luminance) / (channel_max - luminance);
|
let scale = (1. - luminance) / (channel_max - luminance);
|
||||||
altered_color.map_rgb(|c| (c - luminance) * scale + luminance)
|
alt_r = (alt_r - luminance) * scale + luminance;
|
||||||
} else {
|
alt_g = (alt_g - luminance) * scale + luminance;
|
||||||
altered_color
|
alt_b = (alt_b - luminance) * scale + luminance;
|
||||||
};
|
}
|
||||||
let altered_color = altered_color.to_gamma_srgb();
|
|
||||||
|
alt_r = linear_to_srgb(alt_r);
|
||||||
|
alt_g = linear_to_srgb(alt_g);
|
||||||
|
alt_b = linear_to_srgb(alt_b);
|
||||||
|
|
||||||
if vibrance >= 0. {
|
if vibrance >= 0. {
|
||||||
altered_color
|
Color::from_rgbaf32_unchecked(alt_r, alt_g, alt_b, alpha_in)
|
||||||
} else {
|
} else {
|
||||||
// TODO: The result ends up a bit darker than it should be, further investigation is needed
|
// TODO: The result ends up a bit darker than it should be, further investigation is needed.
|
||||||
let luminance = color.luminance_rec_601();
|
// Mix in gamma space (matching `alt_*`), so the luminance is computed from gamma channels too.
|
||||||
|
let [gr, gg, gb, _] = color.to_gamma_srgb_channels();
|
||||||
// Near -0% vibrance we mostly use `altered_color`.
|
let luminance = 0.299 * gr + 0.587 * gg + 0.114 * gb;
|
||||||
// Near -100% vibrance, we mostly use half the desaturated luminance color and half `altered_color`.
|
|
||||||
let factor = -slowed_vibrance;
|
let factor = -slowed_vibrance;
|
||||||
altered_color.map_rgb(|c| c * (1. - factor) + luminance * factor)
|
Color::from_rgbaf32_unchecked(
|
||||||
|
alt_r * (1. - factor) + luminance * factor,
|
||||||
|
alt_g * (1. - factor) + luminance * factor,
|
||||||
|
alt_b * (1. - factor) + luminance * factor,
|
||||||
|
alpha_in,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
@@ -747,16 +779,14 @@ fn channel_mixer<T: Adjust<Color>>(
|
|||||||
_output_channel: RedGreenBlue,
|
_output_channel: RedGreenBlue,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||||
|
|
||||||
let (r, g, b, a) = color.components();
|
let (out_r, out_g, out_b) = if monochrome {
|
||||||
|
|
||||||
let color = if monochrome {
|
|
||||||
let (monochrome_r, monochrome_g, monochrome_b, monochrome_c) = (monochrome_r / 100., monochrome_g / 100., monochrome_b / 100., monochrome_c / 100.);
|
let (monochrome_r, monochrome_g, monochrome_b, monochrome_c) = (monochrome_r / 100., monochrome_g / 100., monochrome_b / 100., monochrome_c / 100.);
|
||||||
|
|
||||||
let gray = (r * monochrome_r + g * monochrome_g + b * monochrome_b + monochrome_c).clamp(0., 1.);
|
let gray = (r * monochrome_r + g * monochrome_g + b * monochrome_b + monochrome_c).clamp(0., 1.);
|
||||||
|
|
||||||
Color::from_rgbaf32_unchecked(gray, gray, gray, a)
|
(gray, gray, gray)
|
||||||
} else {
|
} else {
|
||||||
let (red_r, red_g, red_b, red_c) = (red_r / 100., red_g / 100., red_b / 100., red_c / 100.);
|
let (red_r, red_g, red_b, red_c) = (red_r / 100., red_g / 100., red_b / 100., red_c / 100.);
|
||||||
let (green_r, green_g, green_b, green_c) = (green_r / 100., green_g / 100., green_b / 100., green_c / 100.);
|
let (green_r, green_g, green_b, green_c) = (green_r / 100., green_g / 100., green_b / 100., green_c / 100.);
|
||||||
@@ -766,10 +796,10 @@ fn channel_mixer<T: Adjust<Color>>(
|
|||||||
let green = (r * green_r + g * green_g + b * green_b + green_c).clamp(0., 1.);
|
let green = (r * green_r + g * green_g + b * green_b + green_c).clamp(0., 1.);
|
||||||
let blue = (r * blue_r + g * blue_g + b * blue_b + blue_c).clamp(0., 1.);
|
let blue = (r * blue_r + g * blue_g + b * blue_b + blue_c).clamp(0., 1.);
|
||||||
|
|
||||||
Color::from_rgbaf32_unchecked(red, green, blue, a)
|
(red, green, blue)
|
||||||
};
|
};
|
||||||
|
|
||||||
color.to_linear_srgb()
|
Color::from_gamma_srgb_channels(out_r, out_g, out_b, a)
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
@@ -873,9 +903,7 @@ fn selective_color<T: Adjust<Color>>(
|
|||||||
_colors: SelectiveColorChoice,
|
_colors: SelectiveColorChoice,
|
||||||
) -> T {
|
) -> T {
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
let [r, g, b, a] = color.to_gamma_srgb_channels();
|
||||||
|
|
||||||
let (r, g, b, a) = color.components();
|
|
||||||
|
|
||||||
let min = |a: f32, b: f32, c: f32| a.min(b).min(c);
|
let min = |a: f32, b: f32, c: f32| a.min(b).min(c);
|
||||||
let max = |a: f32, b: f32, c: f32| a.max(b).max(c);
|
let max = |a: f32, b: f32, c: f32| a.max(b).max(c);
|
||||||
@@ -945,9 +973,9 @@ fn selective_color<T: Adjust<Color>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rgb = Vec3::new(r, g, b);
|
let rgb = Vec3::new(r, g, b);
|
||||||
let color = Color::from_vec4(Vec4::from(((sum + rgb).clamp(Vec3::ZERO, Vec3::ONE), a)));
|
let out = (sum + rgb).clamp(Vec3::ZERO, Vec3::ONE);
|
||||||
|
|
||||||
color.to_linear_srgb()
|
Color::from_gamma_srgb_channels(out.x, out.y, out.z, a)
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
@@ -973,15 +1001,11 @@ fn posterize<T: Adjust<Color>>(
|
|||||||
levels: u32,
|
levels: u32,
|
||||||
) -> T {
|
) -> T {
|
||||||
input.adjust(|color| {
|
input.adjust(|color| {
|
||||||
let color = color.to_gamma_srgb();
|
// `hard_min(2)` constrains the widget but doesn't bind the data-flow input (a saved doc or upstream node could still feed 0 or 1, producing inf/NaN below).
|
||||||
|
let levels = (levels as f32).max(2.);
|
||||||
let levels = levels as f32;
|
|
||||||
let number_of_areas = levels.recip();
|
let number_of_areas = levels.recip();
|
||||||
let size_of_areas = (levels - 1.).recip();
|
let size_of_areas = (levels - 1.).recip();
|
||||||
let channel = |channel: f32| (channel / number_of_areas).floor() * size_of_areas;
|
color.map_gamma_rgb(|c| (c / number_of_areas).floor() * size_of_areas)
|
||||||
let color = color.map_rgb(channel);
|
|
||||||
|
|
||||||
color.to_linear_srgb()
|
|
||||||
});
|
});
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1040,7 @@ fn exposure<T: Adjust<Color>>(
|
|||||||
// Offset
|
// Offset
|
||||||
.map_rgb(|c: f32| c + offset)
|
.map_rgb(|c: f32| c + offset)
|
||||||
// Gamma correction
|
// Gamma correction
|
||||||
.gamma(gamma_correction);
|
.apply_gamma_exponent(gamma_correction);
|
||||||
|
|
||||||
adjusted.map_rgb(|c: f32| c.clamp(0., 1.))
|
adjusted.map_rgb(|c: f32| c.clamp(0., 1.))
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode,
|
|||||||
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
||||||
};
|
};
|
||||||
|
|
||||||
background.alpha_blend(target_color.to_associated_alpha(opacity))
|
background.alpha_blend(target_color.apply_opacity(opacity))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color {
|
pub fn apply_blend_mode(foreground: Color, background: Color, blend_mode: BlendMode) -> Color {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use core_types::color::Color;
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use core_types::color::{Alpha, Color, Pixel, RGB};
|
||||||
use core_types::context::Ctx;
|
use core_types::context::Ctx;
|
||||||
use core_types::list::List;
|
use core_types::list::List;
|
||||||
use core_types::registry::types::PixelLength;
|
use core_types::registry::types::PixelLength;
|
||||||
@@ -6,6 +7,84 @@ use raster_types::Image;
|
|||||||
use raster_types::{Bitmap, BitmapMut};
|
use raster_types::{Bitmap, BitmapMut};
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
|
|
||||||
|
/// Working-buffer pixel for the blur algorithms' `gamma` mode: premultiplied sRGB-gamma `f32` channels.
|
||||||
|
/// Only used internally so the working buffer's color space is reflected in the type instead of stuffed into `Color` (which is linear-light by invariant).
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Pod, Zeroable)]
|
||||||
|
struct PremultipliedGammaPixel {
|
||||||
|
r: f32,
|
||||||
|
g: f32,
|
||||||
|
b: f32,
|
||||||
|
a: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pixel for PremultipliedGammaPixel {}
|
||||||
|
|
||||||
|
impl RGB for PremultipliedGammaPixel {
|
||||||
|
type ColorChannel = f32;
|
||||||
|
fn red(&self) -> f32 {
|
||||||
|
self.r
|
||||||
|
}
|
||||||
|
fn green(&self) -> f32 {
|
||||||
|
self.g
|
||||||
|
}
|
||||||
|
fn blue(&self) -> f32 {
|
||||||
|
self.b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Alpha for PremultipliedGammaPixel {
|
||||||
|
type AlphaChannel = f32;
|
||||||
|
const TRANSPARENT: Self = Self { r: 0., g: 0., b: 0., a: 0. };
|
||||||
|
fn alpha(&self) -> f32 {
|
||||||
|
self.a
|
||||||
|
}
|
||||||
|
fn multiplied_alpha(&self, mult: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
r: self.r * mult,
|
||||||
|
g: self.g * mult,
|
||||||
|
b: self.b * mult,
|
||||||
|
a: self.a * mult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn premultiply_gamma(buffer: Image<Color>) -> Image<PremultipliedGammaPixel> {
|
||||||
|
Image {
|
||||||
|
width: buffer.width,
|
||||||
|
height: buffer.height,
|
||||||
|
data: buffer
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|px| {
|
||||||
|
let [r, g, b, a] = px.to_gamma_srgb_channels();
|
||||||
|
PremultipliedGammaPixel { r: r * a, g: g * a, b: b * a, a }
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
base64_string: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpremultiply_gamma_to_linear(buffer: Image<PremultipliedGammaPixel>) -> Image<Color> {
|
||||||
|
Image {
|
||||||
|
width: buffer.width,
|
||||||
|
height: buffer.height,
|
||||||
|
data: buffer
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|px| {
|
||||||
|
if px.a > 0. {
|
||||||
|
let inv_a = 1. / px.a;
|
||||||
|
Color::from_gamma_srgb_channels(px.r * inv_a, px.g * inv_a, px.b * inv_a, px.a)
|
||||||
|
} else {
|
||||||
|
Color::TRANSPARENT
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
base64_string: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Blurs the image with a Gaussian or box blur kernel filter.
|
/// Blurs the image with a Gaussian or box blur kernel filter.
|
||||||
#[node_macro::node(category("Raster: Filter"))]
|
#[node_macro::node(category("Raster: Filter"))]
|
||||||
async fn blur(
|
async fn blur(
|
||||||
@@ -98,26 +177,49 @@ fn gaussian_kernel(radius: f64) -> Vec<f64> {
|
|||||||
gaussian_kernel
|
gaussian_kernel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||||
if gamma {
|
|
||||||
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
|
|
||||||
} else {
|
|
||||||
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (width, height) = original_buffer.dimensions();
|
|
||||||
|
|
||||||
// Create 1D gaussian kernel
|
|
||||||
let kernel = gaussian_kernel(radius);
|
let kernel = gaussian_kernel(radius);
|
||||||
|
if gamma {
|
||||||
|
let working = premultiply_gamma(buffer);
|
||||||
|
let blurred = gaussian_separable(working, &kernel, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a });
|
||||||
|
unpremultiply_gamma_to_linear(blurred)
|
||||||
|
} else {
|
||||||
|
let mut working = buffer;
|
||||||
|
working.map_pixels(|px| px.apply_opacity(px.a()));
|
||||||
|
let mut blurred = gaussian_separable(working, &kernel, Color::from_rgbaf32_unchecked);
|
||||||
|
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||||
|
blurred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
||||||
|
if gamma {
|
||||||
|
let working = premultiply_gamma(buffer);
|
||||||
|
let blurred = box_separable(working, radius, |r, g, b, a| PremultipliedGammaPixel { r, g, b, a });
|
||||||
|
unpremultiply_gamma_to_linear(blurred)
|
||||||
|
} else {
|
||||||
|
let mut working = buffer;
|
||||||
|
working.map_pixels(|px| px.apply_opacity(px.a()));
|
||||||
|
let mut blurred = box_separable(working, radius, Color::from_rgbaf32_unchecked);
|
||||||
|
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||||
|
blurred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gaussian_separable<P, F>(buffer: Image<P>, kernel: &[f64], construct: F) -> Image<P>
|
||||||
|
where
|
||||||
|
P: Pixel + Copy + RGB<ColorChannel = f32> + Alpha<AlphaChannel = f32>,
|
||||||
|
F: Fn(f32, f32, f32, f32) -> P,
|
||||||
|
{
|
||||||
|
let (width, height) = buffer.dimensions();
|
||||||
let half_kernel = kernel.len() / 2;
|
let half_kernel = kernel.len() / 2;
|
||||||
|
|
||||||
// Intermediate buffer for horizontal and vertical passes
|
let mut x_axis = Image::new(width, height, P::default());
|
||||||
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
|
let mut y_axis = Image::new(width, height, P::default());
|
||||||
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
|
|
||||||
|
|
||||||
for pass in [false, true] {
|
for pass in [false, true] {
|
||||||
let (max, old_buffer, current_buffer) = match pass {
|
let (max, old_buffer, current_buffer) = match pass {
|
||||||
false => (width, &original_buffer, &mut x_axis),
|
false => (width, &buffer, &mut x_axis),
|
||||||
true => (height, &x_axis, &mut y_axis),
|
true => (height, &x_axis, &mut y_axis),
|
||||||
};
|
};
|
||||||
let pass = pass as usize;
|
let pass = pass as usize;
|
||||||
@@ -140,41 +242,32 @@ fn gaussian_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize
|
|
||||||
let (r, g, b, a) = if weight_sum > 0. {
|
let (r, g, b, a) = if weight_sum > 0. {
|
||||||
((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32)
|
((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32)
|
||||||
} else {
|
} else {
|
||||||
let px = old_buffer.get_pixel(x, y).unwrap();
|
let px = old_buffer.get_pixel(x, y).unwrap();
|
||||||
(px.r(), px.g(), px.b(), px.a())
|
(px.r(), px.g(), px.b(), px.a())
|
||||||
};
|
};
|
||||||
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
|
current_buffer.set_pixel(x, y, construct(r, g, b, a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gamma {
|
|
||||||
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
|
|
||||||
} else {
|
|
||||||
y_axis.map_pixels(|px| px.to_unassociated_alpha());
|
|
||||||
}
|
|
||||||
|
|
||||||
y_axis
|
y_axis
|
||||||
}
|
}
|
||||||
|
|
||||||
fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: bool) -> Image<Color> {
|
fn box_separable<P, F>(buffer: Image<P>, radius: f64, construct: F) -> Image<P>
|
||||||
if gamma {
|
where
|
||||||
original_buffer.map_pixels(|px| px.to_gamma_srgb().to_associated_alpha(px.a()));
|
P: Pixel + Copy + RGB<ColorChannel = f32> + Alpha<AlphaChannel = f32>,
|
||||||
} else {
|
F: Fn(f32, f32, f32, f32) -> P,
|
||||||
original_buffer.map_pixels(|px| px.to_associated_alpha(px.a()));
|
{
|
||||||
}
|
let (width, height) = buffer.dimensions();
|
||||||
|
let mut x_axis = Image::new(width, height, P::default());
|
||||||
let (width, height) = original_buffer.dimensions();
|
let mut y_axis = Image::new(width, height, P::default());
|
||||||
let mut x_axis = Image::new(width, height, Color::TRANSPARENT);
|
|
||||||
let mut y_axis = Image::new(width, height, Color::TRANSPARENT);
|
|
||||||
|
|
||||||
for pass in [false, true] {
|
for pass in [false, true] {
|
||||||
let (max, old_buffer, current_buffer) = match pass {
|
let (max, old_buffer, current_buffer) = match pass {
|
||||||
false => (width, &original_buffer, &mut x_axis),
|
false => (width, &buffer, &mut x_axis),
|
||||||
true => (height, &x_axis, &mut y_axis),
|
true => (height, &x_axis, &mut y_axis),
|
||||||
};
|
};
|
||||||
let pass = pass as usize;
|
let pass = pass as usize;
|
||||||
@@ -196,17 +289,11 @@ fn box_blur_algorithm(mut original_buffer: Image<Color>, radius: f64, gamma: boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (r, g, b, a) = ((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32);
|
let (r, g, b, a) = ((r_sum / weight_sum) as f32, (g_sum / weight_sum) as f32, (b_sum / weight_sum) as f32, (a_sum / weight_sum) as f32);
|
||||||
current_buffer.set_pixel(x, y, Color::from_rgbaf32_unchecked(r, g, b, a));
|
current_buffer.set_pixel(x, y, construct(r, g, b, a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gamma {
|
|
||||||
y_axis.map_pixels(|px| px.to_linear_srgb().to_unassociated_alpha());
|
|
||||||
} else {
|
|
||||||
y_axis.map_pixels(|px| px.to_unassociated_alpha());
|
|
||||||
}
|
|
||||||
|
|
||||||
y_axis
|
y_axis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ async fn gradient_map<T: Adjust<Color>>(
|
|||||||
let Some(gradient) = gradient.element(0) else { return image };
|
let Some(gradient) = gradient.element(0) else { return image };
|
||||||
|
|
||||||
image.adjust(|color| {
|
image.adjust(|color| {
|
||||||
let intensity = color.luminance_srgb();
|
let intensity = color.luminance_rec_709();
|
||||||
let intensity = if reverse { 1. - intensity } else { intensity };
|
let intensity = if reverse { 1. - intensity } else { intensity };
|
||||||
gradient.evaluate(intensity as f64).to_linear_srgb()
|
gradient.evaluate(intensity as f64)
|
||||||
});
|
});
|
||||||
|
|
||||||
image
|
image
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ async fn image_color_palette(
|
|||||||
let bins = GRID * GRID * GRID;
|
let bins = GRID * GRID * GRID;
|
||||||
|
|
||||||
let mut histogram = vec![0; (bins + 1.) as usize];
|
let mut histogram = vec![0; (bins + 1.) as usize];
|
||||||
let mut color_bins = vec![Vec::new(); (bins + 1.) as usize];
|
// Each bin stores `(red, green, blue, alpha)` tuples in sRGB gamma space; averaging in gamma space gives perceptually-uniform binning.
|
||||||
|
let mut color_bins: Vec<Vec<[f32; 4]>> = vec![Vec::new(); (bins + 1.) as usize];
|
||||||
|
|
||||||
for element in image.iter_element_values() {
|
for element in image.iter_element_values() {
|
||||||
for pixel in element.data.iter() {
|
for pixel in element.data.iter() {
|
||||||
@@ -27,7 +28,7 @@ async fn image_color_palette(
|
|||||||
let bin = (r * GRID + g * GRID + b * GRID) as usize;
|
let bin = (r * GRID + g * GRID + b * GRID) as usize;
|
||||||
|
|
||||||
histogram[bin] += 1;
|
histogram[bin] += 1;
|
||||||
color_bins[bin].push(pixel.to_gamma_srgb());
|
color_bins[bin].push(pixel.to_gamma_srgb_channels());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,24 +40,21 @@ async fn image_color_palette(
|
|||||||
.flat_map(|&i| {
|
.flat_map(|&i| {
|
||||||
let list = &color_bins[i];
|
let list = &color_bins[i];
|
||||||
|
|
||||||
let mut r = 0.;
|
let [mut r, mut g, mut b, mut a] = [0.; 4];
|
||||||
let mut g = 0.;
|
|
||||||
let mut b = 0.;
|
|
||||||
let mut a = 0.;
|
|
||||||
|
|
||||||
for color in list.iter() {
|
for &[cr, cg, cb, ca] in list.iter() {
|
||||||
r += color.r();
|
r += cr;
|
||||||
g += color.g();
|
g += cg;
|
||||||
b += color.b();
|
b += cb;
|
||||||
a += color.a();
|
a += ca;
|
||||||
}
|
}
|
||||||
|
|
||||||
r /= list.len() as f32;
|
let len = list.len() as f32;
|
||||||
g /= list.len() as f32;
|
let [r, g, b, a] = [r / len, g / len, b / len, a / len];
|
||||||
b /= list.len() as f32;
|
|
||||||
a /= list.len() as f32;
|
|
||||||
|
|
||||||
Color::from_rgbaf32(r, g, b, a).map(Item::new_from_element).into_iter()
|
// Reject NaN/out-of-range averages, then lift the gamma-space bin centroid to linear-light
|
||||||
|
let in_gamut = a <= 1. && ![r, g, b, a].iter().any(|c| c.is_sign_negative() || !c.is_finite());
|
||||||
|
in_gamut.then(|| Color::from_gamma_srgb_channels(r, g, b, a)).map(Item::new_from_element).into_iter()
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user