Refactor many usages of Color to natively store linear not gamma (#2457)

This commit is contained in:
Keavon Chambers 2025-03-18 05:37:20 -07:00 committed by GitHub
parent 056020a56c
commit 6292dea103
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 232 additions and 183 deletions

View File

@ -82,6 +82,7 @@ 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 => {
// Decodes the colors in gamma, not linear
let decode_color = |color: &serde_json::map::Map<String, serde_json::value::Value>| -> Option<Color> { let decode_color = |color: &serde_json::map::Map<String, serde_json::value::Value>| -> Option<Color> {
let red = color.get("red").and_then(|x| x.as_f64()).map(|x| x as f32); let red = color.get("red").and_then(|x| x.as_f64()).map(|x| x as f32);
let green = color.get("green").and_then(|x| x.as_f64()).map(|x| x as f32); let green = color.get("green").and_then(|x| x.as_f64()).map(|x| x as f32);
@ -120,20 +121,13 @@ impl LayoutMessageHandler {
.filter_map(|stop| { .filter_map(|stop| {
stop.as_object().and_then(|stop| { stop.as_object().and_then(|stop| {
let position = stop.get("position").and_then(|x| x.as_f64()); let position = stop.get("position").and_then(|x| x.as_f64());
let color = stop.get("color").and_then(|x| x.as_object()); let color = stop.get("color").and_then(|x| x.as_object()).and_then(decode_color);
if let (Some(position), Some(color)) = (position, color) { Some((position, color)) } else { None }
if let (Some(position), Some(color_object)) = (position, color) {
if let Some(color) = decode_color(color_object) {
return Some((position, color));
}
}
None
}) })
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
color_button.value = FillChoice::Gradient(GradientStops(gradient_stops)); color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops));
return (color_button.on_update.callback)(color_button); return (color_button.on_update.callback)(color_button);
} }

View File

@ -147,6 +147,7 @@ pub struct ImageButton {
#[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] #[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct ColorInput { pub struct ColorInput {
/// WARNING: The colors are gamma, not linear!
#[widget_builder(constructor)] #[widget_builder(constructor)]
pub value: FillChoice, pub value: FillChoice,

View File

@ -410,7 +410,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])]; let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops); let stops = GradientStops::new(stops);
Fill::Gradient(Gradient { Fill::Gradient(Gradient {
start, start,
@ -437,7 +437,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, t
let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])]; let [start, end] = [bounds_transform.inverse().transform_point2(layer[0]), bounds_transform.inverse().transform_point2(layer[1])];
let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
let stops = GradientStops(stops); let stops = GradientStops::new(stops);
Fill::Gradient(Gradient { Fill::Gradient(Gradient {
start, start,

View File

@ -1087,7 +1087,10 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize,
return LayoutGroup::Row { widgets }; return LayoutGroup::Row { widgets };
}; };
// Add a separator
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
// Add the color input
match &**tagged_value { match &**tagged_value {
TaggedValue::Color(color) => widgets.push( TaggedValue::Color(color) => widgets.push(
color_button color_button

View File

@ -9,7 +9,7 @@ use graphene_std::vector::style::FillChoice;
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;
let grid_color = document.snapping_state.grid.grid_color; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else {
return; return;
}; };
@ -36,11 +36,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
} else { } else {
DVec2::new(secondary_pos, primary_end) DVec2::new(secondary_pos, primary_end)
}; };
overlay_context.line( overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
);
} }
} }
} }
@ -52,7 +48,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context:
// TODO: Implement this with a dashed line (`set_line_dash`), with integer spacing which is continuously adjusted to correct the accumulated error. // TODO: Implement this with a dashed line (`set_line_dash`), with integer spacing which is continuously adjusted to correct the accumulated error.
fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
let origin = document.snapping_state.grid.origin; let origin = document.snapping_state.grid.origin;
let grid_color = document.snapping_state.grid.grid_color; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else { let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.document_ptz) else {
return; return;
}; };
@ -80,16 +76,13 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte
let x_per_dot = (end.x - start.x) / total_dots; let x_per_dot = (end.x - start.x) / total_dots;
for dot_index in 0..=total_dots as usize { for dot_index in 0..=total_dots as usize {
let exact_x = x_per_dot * dot_index as f64; let exact_x = x_per_dot * dot_index as f64;
overlay_context.pixel( overlay_context.pixel(document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), Some(&grid_color))
document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(),
Some(&("#".to_string() + &grid_color.rgba_hex())),
)
} }
} }
} }
fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
let grid_color = document.snapping_state.grid.grid_color; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin; let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
@ -112,11 +105,7 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x; let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
let start = DVec2::new(x_pos, min_y); let start = DVec2::new(x_pos, min_y);
let end = DVec2::new(x_pos, max_y); let end = DVec2::new(x_pos, max_y);
overlay_context.line( overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
);
} }
for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] { for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] {
@ -130,17 +119,13 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y; let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y;
let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos))); let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos)));
let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos))); let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos)));
overlay_context.line( overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color));
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
);
} }
} }
} }
fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) { fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
let grid_color = document.snapping_state.grid.grid_color; let grid_color = "#".to_string() + &document.snapping_state.grid.grid_color.to_rgba_hex_srgb();
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap(); let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin; let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz); let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
@ -180,7 +165,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
overlay_context.dashed_line( overlay_context.dashed_line(
document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end), document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())), Some(&grid_color),
Some(1.), Some(1.),
Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 1.), Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 1.),
None, None,
@ -228,10 +213,8 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
}; };
let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| { let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| {
update_val::<ColorInput, _>(grid, move |grid, color| { update_val::<ColorInput, _>(grid, move |grid, color| {
if let FillChoice::Solid(color) = color.value { if let (Some(color), Some(update_color)) = (color.value.as_solid(), update(grid)) {
if let Some(update_color) = update(grid) { *update_color = color.to_linear_srgb();
*update_color = color;
}
} }
}) })
}; };
@ -278,7 +261,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
Separator::new(SeparatorType::Related).widget_holder(), Separator::new(SeparatorType::Related).widget_holder(),
]); ]);
color_widgets.push( color_widgets.push(
ColorInput::new(FillChoice::Solid(grid.grid_color)) ColorInput::new(FillChoice::Solid(grid.grid_color.to_gamma_srgb()))
.tooltip("Grid display color") .tooltip("Grid display color")
.allow_none(false) .allow_none(false)
.on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) .on_update(update_color(grid, |grid| Some(&mut grid.grid_color)))

View File

@ -320,7 +320,7 @@ impl OverlayContext {
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()) let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap())
.unwrap() .unwrap()
.with_alpha(0.05) .with_alpha(0.05)
.rgba_hex(); .to_rgba_hex_srgb();
fill_color.insert(0, '#'); fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str()); let fill_color = Some(fill_color.as_str());
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None); self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None);
@ -357,7 +357,10 @@ impl OverlayContext {
// Hover ring // Hover ring
if show_hover_ring { if show_hover_ring {
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).rgba_hex(); let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.5)
.to_rgba_hex_srgb();
fill_color.insert(0, '#'); fill_color.insert(0, '#');
self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH); self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH);

View File

@ -217,10 +217,7 @@ impl Default for GridSnapping {
Self { Self {
origin: DVec2::ZERO, origin: DVec2::ZERO,
grid_type: Default::default(), grid_type: Default::default(),
grid_color: COLOR_OVERLAY_GRAY grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY.strip_prefix('#').unwrap()).unwrap(),
.strip_prefix('#')
.and_then(Color::from_rgb_str)
.expect("Should create Color from prefixed hex string"),
dot_display: false, dot_display: false,
} }
} }

View File

@ -60,14 +60,14 @@ impl ToolColorOptions {
pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) { pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() { if let Some(color) = self.active_color() {
let fill = graphene_core::vector::style::Fill::solid(color); let fill = graphene_core::vector::style::Fill::solid(color.to_gamma_srgb());
responses.add(GraphOperationMessage::FillSet { layer, fill }); responses.add(GraphOperationMessage::FillSet { layer, fill });
} }
} }
pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) { pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() { if let Some(color) = self.active_color() {
let stroke = graphene_core::vector::style::Stroke::new(Some(color), weight); let stroke = graphene_core::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke }); responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
} }
} }
@ -111,9 +111,11 @@ impl ToolColorOptions {
widgets.push(radio); widgets.push(radio);
widgets.push(Separator::new(SeparatorType::Related).widget_holder()); widgets.push(Separator::new(SeparatorType::Related).widget_holder());
let color_button = ColorInput::new(FillChoice::from_optional_color(self.active_color())) let fill_choice = match self.active_color() {
.allow_none(color_allow_none) Some(color) => FillChoice::Solid(color.to_gamma_srgb()),
.on_update(color_callback); None => FillChoice::None,
};
let color_button = ColorInput::new(fill_choice).allow_none(color_allow_none).on_update(color_callback);
widgets.push(color_button.widget_holder()); widgets.push(color_button.widget_holder());
widgets widgets

View File

@ -275,7 +275,7 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else { let TaggedValue::Fill(graphene_std::vector::style::Fill::Solid(color)) = inputs.get(fill_index)?.as_value()? else {
return None; return None;
}; };
Some(*color) Some(color.to_linear_srgb())
} }
/// Get the current blend mode of a layer from the closest Blend Mode node /// Get the current blend mode of a layer from the closest Blend Mode node

View File

@ -155,7 +155,7 @@ impl LayoutHolder for BrushTool {
false, false,
|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(), |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()),
|color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid())).into(), |color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Related).widget_holder()); widgets.push(Separator::new(SeparatorType::Related).widget_holder());

View File

@ -91,7 +91,7 @@ impl LayoutHolder for EllipseTool {
true, true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(), |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -101,7 +101,7 @@ impl LayoutHolder for EllipseTool {
true, true,
|_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(), |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -159,8 +159,8 @@ fn disable_cursor_preview(responses: &mut VecDeque<Message>) {
fn update_cursor_preview(responses: &mut VecDeque<Message>, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option<String>) { fn update_cursor_preview(responses: &mut VecDeque<Message>, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option<String>) {
responses.add(FrontendMessage::UpdateEyedropperSamplingState { responses.add(FrontendMessage::UpdateEyedropperSamplingState {
mouse_position: Some(input.mouse.position.into()), mouse_position: Some(input.mouse.position.into()),
primary_color: "#".to_string() + global_tool_data.primary_color.rgb_hex().as_str(), primary_color: "#".to_string() + global_tool_data.primary_color.to_rgb_hex_srgb().as_str(),
secondary_color: "#".to_string() + global_tool_data.secondary_color.rgb_hex().as_str(), secondary_color: "#".to_string() + global_tool_data.secondary_color.to_rgb_hex_srgb().as_str(),
set_color_choice, set_color_choice,
}); });
} }

View File

@ -91,8 +91,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), FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color.to_gamma_srgb()),
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color), FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()),
_ => return self, _ => return self,
}; };
@ -167,7 +167,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], Fill::Solid(Color::GREEN)); assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb());
} }
#[tokio::test] #[tokio::test]
@ -180,6 +180,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], Fill::Solid(color)); assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), color.to_rgba8_srgb());
} }
} }

View File

@ -97,7 +97,7 @@ impl LayoutHolder for FreehandTool {
true, true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(), |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -107,7 +107,7 @@ impl LayoutHolder for FreehandTool {
true, true,
|_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(), |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -184,11 +184,11 @@ impl SelectedGradient {
// Should not go off end but can swap // Should not go off end but can swap
let clamped = new_pos.clamp(0., 1.); let clamped = new_pos.clamp(0., 1.);
self.gradient.stops.0[s].0 = clamped; self.gradient.stops.get_mut(s).unwrap().0 = clamped;
let new_pos = self.gradient.stops.0[s]; let new_pos = self.gradient.stops[s];
self.gradient.stops.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); self.gradient.stops.sort();
self.dragging = GradientDragTarget::Step(self.gradient.stops.0.iter().position(|x| *x == new_pos).unwrap()); self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|x| *x == new_pos).unwrap());
} }
} }
self.render_gradient(responses); self.render_gradient(responses);
@ -259,7 +259,7 @@ impl Fsm for GradientToolFsmState {
overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None); overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None);
overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None); overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None);
for (index, (position, _)) in stops.0.into_iter().enumerate() { for (index, (position, _)) in stops.into_iter().enumerate() {
if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. {
continue; continue;
} }
@ -276,7 +276,7 @@ impl Fsm for GradientToolFsmState {
}; };
// Skip if invalid gradient // Skip if invalid gradient
if selected_gradient.gradient.stops.0.len() < 2 { if selected_gradient.gradient.stops.len() < 2 {
return self; return self;
} }
@ -284,25 +284,31 @@ impl Fsm for GradientToolFsmState {
// Remove the selected point // Remove the selected point
match selected_gradient.dragging { match selected_gradient.dragging {
GradientDragTarget::Start => selected_gradient.gradient.stops.0.remove(0), GradientDragTarget::Start => {
GradientDragTarget::End => selected_gradient.gradient.stops.0.pop().unwrap(), selected_gradient.gradient.stops.remove(0);
GradientDragTarget::Step(index) => selected_gradient.gradient.stops.0.remove(index), }
GradientDragTarget::End => {
let _ = selected_gradient.gradient.stops.pop();
}
GradientDragTarget::Step(index) => {
selected_gradient.gradient.stops.remove(index);
}
}; };
// The gradient has only one point and so should become a fill // The gradient has only one point and so should become a fill
if selected_gradient.gradient.stops.0.len() == 1 { if selected_gradient.gradient.stops.len() == 1 {
if let Some(layer) = selected_gradient.layer { if let Some(layer) = selected_gradient.layer {
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
layer, layer,
fill: Fill::Solid(selected_gradient.gradient.stops.0[0].1), fill: Fill::Solid(selected_gradient.gradient.stops[0].1),
}); });
} }
return self; return self;
} }
// Find the minimum and maximum positions // Find the minimum and maximum positions
let min_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min"); let min_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min");
let max_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max"); let max_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max");
// Recompute the start and end position of the gradient (in viewport transform) // Recompute the start and end position of the gradient (in viewport transform)
let transform = selected_gradient.transform; let transform = selected_gradient.transform;
@ -312,7 +318,7 @@ impl Fsm for GradientToolFsmState {
selected_gradient.gradient.end = transform.inverse().transform_point2(new_end); selected_gradient.gradient.end = transform.inverse().transform_point2(new_end);
// Remap the positions // Remap the positions
for (position, _) in selected_gradient.gradient.stops.0.iter_mut() { for (position, _) in selected_gradient.gradient.stops.iter_mut() {
*position = (*position - min_position) / (max_position - min_position); *position = (*position - min_position) / (max_position - min_position);
} }
@ -365,7 +371,7 @@ impl Fsm for GradientToolFsmState {
let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue };
let transform = gradient_space_transform(layer, document); let transform = gradient_space_transform(layer, document);
// Check for dragging step // Check for dragging step
for (index, (pos, _)) in gradient.stops.0.iter().enumerate() { for (index, (pos, _)) in gradient.stops.iter().enumerate() {
let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos)); let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos));
if pos.distance_squared(mouse) < tolerance { if pos.distance_squared(mouse) < tolerance {
dragging = true; dragging = true;

View File

@ -86,7 +86,7 @@ impl LayoutHolder for LineTool {
true, true,
|_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(), |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -933,7 +933,7 @@ impl Fsm for PathToolFsmState {
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap() .unwrap()
.with_alpha(0.05) .with_alpha(0.05)
.rgba_hex(); .to_rgba_hex_srgb();
fill_color.insert(0, '#'); fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str()); let fill_color = Some(fill_color.as_str());
@ -961,7 +961,10 @@ impl Fsm for PathToolFsmState {
let origin = tool_data.drag_start_pos; let origin = tool_data.drag_start_pos;
let viewport_diagonal = input.viewport_bounds.size().length(); let viewport_diagonal = input.viewport_bounds.size().length();
let mut faded_blue = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); let mut faded_blue = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.25)
.to_rgba_hex_srgb();
faded_blue.insert(0, '#'); faded_blue.insert(0, '#');
let other = faded_blue.as_str(); let other = faded_blue.as_str();

View File

@ -145,7 +145,7 @@ impl LayoutHolder for PenTool {
true, true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(), |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -155,7 +155,7 @@ impl LayoutHolder for PenTool {
true, true,
|_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(), |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());

View File

@ -135,7 +135,7 @@ impl LayoutHolder for PolygonTool {
true, true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(), |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -145,7 +145,7 @@ impl LayoutHolder for PolygonTool {
true, true,
|_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(), |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -79,7 +79,7 @@ impl LayoutHolder for RectangleTool {
true, true,
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(), |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -89,7 +89,7 @@ impl LayoutHolder for RectangleTool {
true, true,
|_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(), |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -692,7 +692,7 @@ impl Fsm for SelectToolFsmState {
let color = if !hover { let color = if !hover {
color color
} else { } else {
let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).to_rgba_hex_srgb();
&format!("#{}", color_string) &format!("#{}", color_string)
}; };
let line_center = tool_data.line_center; let line_center = tool_data.line_center;
@ -707,7 +707,10 @@ impl Fsm for SelectToolFsmState {
let angle = -mouse_position.angle_to(DVec2::X); let angle = -mouse_position.angle_to(DVec2::X);
let snapped_angle = (angle / snap_resolution).round() * snap_resolution; let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
let mut other = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex(); let mut other = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap()
.with_alpha(0.25)
.to_rgba_hex_srgb();
other.insert(0, '#'); other.insert(0, '#');
let other = other.as_str(); let other = other.as_str();
@ -758,7 +761,7 @@ impl Fsm for SelectToolFsmState {
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap() .unwrap()
.with_alpha(0.05) .with_alpha(0.05)
.rgba_hex(); .to_rgba_hex_srgb();
fill_color.insert(0, '#'); fill_color.insert(0, '#');
let fill_color = Some(fill_color.as_str()); let fill_color = Some(fill_color.as_str());

View File

@ -103,7 +103,7 @@ impl LayoutHolder for SplineTool {
true, true,
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(), |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
); );
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
@ -113,7 +113,7 @@ impl LayoutHolder for SplineTool {
true, true,
|_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(), |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColorType(color_type.clone())).into()),
|color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid())).into(), |color: &ColorInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(create_weight_widget(self.options.line_weight)); widgets.push(create_weight_widget(self.options.line_weight));

View File

@ -162,7 +162,7 @@ impl LayoutHolder for TextTool {
true, true,
|_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(), |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).into(),
|color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()), |color_type: ToolColorType| WidgetCallback::new(move |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColorType(color_type.clone())).into()),
|color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid())).into(), |color: &ColorInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(),
)); ));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
@ -378,7 +378,11 @@ impl TextToolData {
responses.add(Message::StartBuffer); responses.add(Message::StartBuffer);
responses.add(GraphOperationMessage::FillSet { responses.add(GraphOperationMessage::FillSet {
layer: self.layer, layer: self.layer,
fill: if editing_text.color.is_some() { Fill::Solid(editing_text.color.unwrap()) } else { Fill::None }, fill: if editing_text.color.is_some() {
Fill::Solid(editing_text.color.unwrap().to_gamma_srgb())
} else {
Fill::None
},
}); });
responses.add(GraphOperationMessage::TransformSet { responses.add(GraphOperationMessage::TransformSet {
layer: self.layer, layer: self.layer,
@ -450,7 +454,7 @@ impl Fsm for TextToolFsmState {
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
.unwrap() .unwrap()
.with_alpha(0.05) .with_alpha(0.05)
.rgba_hex(); .to_rgba_hex_srgb();
let ToolMessage::Text(event) = event else { return self }; let ToolMessage::Text(event) = event else { return self };
match (self, event) { match (self, event) {

View File

@ -401,7 +401,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
} }
} }
} else { } else {
// TODO: Fix handle snap to anchor issue, see <https://github.com/GraphiteEditor/Graphite/issues/2451>
let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); let handle_length = point.as_handle().map(|handle| handle.length(&vector_data));
if handle_length == Some(0.) { if handle_length == Some(0.) {

View File

@ -116,7 +116,7 @@ impl DocumentToolData {
pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) { pub fn update_working_colors(&self, responses: &mut VecDeque<Message>) {
let layout = WidgetLayout::new(vec![ let layout = WidgetLayout::new(vec![
LayoutGroup::Row { LayoutGroup::Row {
widgets: vec![WorkingColorsInput::new(self.primary_color, self.secondary_color).widget_holder()], widgets: vec![WorkingColorsInput::new(self.primary_color.to_gamma_srgb(), self.secondary_color.to_gamma_srgb()).widget_holder()],
}, },
LayoutGroup::Row { LayoutGroup::Row {
widgets: vec![ widgets: vec![

View File

@ -454,7 +454,7 @@ export class Gradient {
} }
} }
// All channels range from 0 to 1 // All channels range are represented by 0-1, sRGB, gamma.
export class Color { export class Color {
readonly red!: number; readonly red!: number;

View File

@ -481,12 +481,13 @@ impl EditorHandle {
/// Update primary color with values on a scale from 0 to 1. /// Update primary color with values on a scale from 0 to 1.
#[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, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let primary_color = match Color::from_rgbaf32(red, green, blue, alpha) { let Some(primary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
Some(color) => color, return Err(Error::new("Invalid color").into());
None => return Err(Error::new("Invalid color").into()),
}; };
let message = ToolMessage::SelectPrimaryColor { color: primary_color }; let message = ToolMessage::SelectPrimaryColor {
color: primary_color.to_linear_srgb(),
};
self.dispatch(message); self.dispatch(message);
Ok(()) Ok(())
@ -495,12 +496,13 @@ impl EditorHandle {
/// Update secondary color with values on a scale from 0 to 1. /// Update secondary color with values on a scale from 0 to 1.
#[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, red: f32, green: f32, blue: f32, alpha: f32) -> Result<(), JsValue> {
let secondary_color = match Color::from_rgbaf32(red, green, blue, alpha) { let Some(secondary_color) = Color::from_rgbaf32(red, green, blue, alpha) else {
Some(color) => color, return Err(Error::new("Invalid color").into());
None => return Err(Error::new("Invalid color").into()),
}; };
let message = ToolMessage::SelectSecondaryColor { color: secondary_color }; let message = ToolMessage::SelectSecondaryColor {
color: secondary_color.to_linear_srgb(),
};
self.dispatch(message); self.dispatch(message);
Ok(()) Ok(())

View File

@ -91,7 +91,7 @@ impl PartialEq for ImageTexture {
} }
#[cfg(not(feature = "wgpu"))] #[cfg(not(feature = "wgpu"))]
{ {
true // Unit values are always equal self.texture == other.texture
} }
} }
} }

View File

@ -503,7 +503,7 @@ impl GraphicElementRendered for VectorDataTable {
} }
Fill::Gradient(gradient) => { Fill::Gradient(gradient) => {
let mut stops = peniko::ColorStops::new(); let mut stops = peniko::ColorStops::new();
for &(offset, color) in &gradient.stops.0 { for &(offset, color) in &gradient.stops {
stops.push(peniko::ColorStop { stops.push(peniko::ColorStop {
offset: offset as f32, offset: offset 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(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
@ -664,7 +664,7 @@ impl GraphicElementRendered for Artboard {
if !render_params.hide_artboards { if !render_params.hide_artboards {
// Background // Background
render.leaf_tag("rect", |attributes| { render.leaf_tag("rect", |attributes| {
attributes.push("fill", format!("#{}", self.background.rgb_hex())); attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma()));
if self.background.a() < 1. { if self.background.a() < 1. {
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string()); attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
} }
@ -1059,13 +1059,13 @@ impl GraphicElementRendered for Option<Color> {
render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color")); render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color"));
return; return;
}; };
let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb()); let color_info = format!("{:?} #{} {:?}", color, color.to_rgba_hex_srgb(), color.to_rgba8_srgb());
render.leaf_tag("rect", |attributes| { render.leaf_tag("rect", |attributes| {
attributes.push("width", "100"); attributes.push("width", "100");
attributes.push("height", "100"); attributes.push("height", "100");
attributes.push("y", "40"); attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgb_hex())); attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
if color.a() < 1. { 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());
} }
@ -1086,7 +1086,7 @@ impl GraphicElementRendered for Vec<Color> {
attributes.push("height", "100"); attributes.push("height", "100");
attributes.push("x", (index * 120).to_string()); attributes.push("x", (index * 120).to_string());
attributes.push("y", "40"); attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgb_hex())); attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
if color.a() < 1. { 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());
} }

View File

@ -1,10 +1,9 @@
use crate::Context;
use crate::Ctx;
use crate::vector::VectorDataTable; use crate::vector::VectorDataTable;
use crate::{Color, Context, Ctx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
#[node_macro::node(category("Debug"))] #[node_macro::node(category("Debug"))]
fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> T { fn log_to_console<T: core::fmt::Debug>(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option<Color>)] value: T) -> T {
#[cfg(not(target_arch = "spirv"))] #[cfg(not(target_arch = "spirv"))]
// KEEP THIS `debug!()` - It acts as the output for the debug node itself // KEEP THIS `debug!()` - It acts as the output for the debug node itself
debug!("{:#?}", value); debug!("{:#?}", value);

View File

@ -614,21 +614,21 @@ impl Blend<Color> for ImageFrameTable<Color> {
} }
impl Blend<Color> for GradientStops { impl Blend<Color> for GradientStops {
fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self {
let mut combined_stops = self.0.iter().map(|(position, _)| position).chain(under.0.iter().map(|(position, _)| position)).collect::<Vec<_>>(); let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::<Vec<_>>();
combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6); combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6);
combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
let stops = combined_stops let stops = combined_stops
.into_iter() .into_iter()
.map(|&position| { .map(|&position| {
let over_color = self.evalute(position); let over_color = self.evaluate(position);
let under_color = under.evalute(position); let under_color = under.evaluate(position);
let color = blend_fn(over_color, under_color); let color = blend_fn(over_color, under_color);
(position, color) (position, color)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
GradientStops(stops) GradientStops::new(stops)
} }
} }
@ -721,7 +721,7 @@ impl Adjust<Color> for Option<Color> {
} }
impl Adjust<Color> for GradientStops { impl Adjust<Color> for GradientStops {
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
for (_pos, c) in self.0.iter_mut() { for (_pos, c) in self.iter_mut() {
*c = map_fn(c); *c = map_fn(c);
} }
} }
@ -770,7 +770,7 @@ async fn gradient_map<T: Adjust<Color>>(
image.adjust(|color| { image.adjust(|color| {
let intensity = color.luminance_srgb(); let intensity = color.luminance_srgb();
let intensity = if reverse { 1. - intensity } else { intensity }; let intensity = if reverse { 1. - intensity } else { intensity };
gradient.evalute(intensity as f64) gradient.evaluate(intensity as f64)
}); });
image image

View File

@ -9,7 +9,6 @@ use spirv_std::num_traits::Euclid;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[cfg(target_arch = "spirv")] #[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float; use spirv_std::num_traits::float::Float;
use std::fmt::Write;
#[repr(C)] #[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -388,7 +387,7 @@ impl Color {
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha) Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
} }
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`. /// Return an opaque SDR `Color` given RGB channels from `0` to `255`, premultiplied by alpha.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -402,7 +401,8 @@ impl Color {
Color::from_rgba8_srgb(red, green, blue, 255) Color::from_rgba8_srgb(red, green, blue, 255)
} }
/// Return an SDR `Color` given RGBA channels from `0` to `255`. // TODO: Should this be premult?
/// Return an SDR `Color` given RGBA channels from `0` to `255`, premultiplied by alpha.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -411,16 +411,13 @@ impl Color {
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color { pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color {
let alpha = alpha as f32 / 255.;
let map_range = |int_color| int_color as f32 / 255.; let map_range = |int_color| int_color as f32 / 255.;
Color {
red: map_range(red), let red = map_range(red);
green: map_range(green), let green = map_range(green);
blue: map_range(blue), let blue = map_range(blue);
alpha, let alpha = map_range(alpha);
} Color { red, green, blue, alpha }.to_linear_srgb().map_rgb(|channel| channel * alpha)
.to_linear_srgb()
.map_rgb(|channel| channel * alpha)
} }
/// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1) /// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1)
@ -788,56 +785,49 @@ impl Color {
(self.red, self.green, self.blue, self.alpha) (self.red, self.green, self.blue, self.alpha)
} }
/// Return an 8-character RGBA hex string (without a # prefix). /// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in linear space.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color.rgba_hex()) /// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ``` /// ```
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub fn rgba_hex(&self) -> String { pub fn to_rgba_hex_srgb(&self) -> String {
let gamma = self.to_gamma_srgb();
format!( format!(
"{:02x?}{:02x?}{:02x?}{:02x?}", "{:02x?}{:02x?}{:02x?}{:02x?}",
(self.r() * 255.) as u8, (gamma.r() * 255.) as u8,
(self.g() * 255.) as u8, (gamma.g() * 255.) as u8,
(self.b() * 255.) as u8, (gamma.b() * 255.) as u8,
(self.a() * 255.) as u8, (gamma.a() * 255.) as u8,
) )
} }
/// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1. /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
///
/// # Examples
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::raster::color::Color;
/// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color1.rgb_optional_a_hex()); /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0xFF).to_gamma_srgb();
/// assert_eq!("5267fa", color2.rgb_optional_a_hex());
/// ``` /// ```
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub fn rgb_optional_a_hex(&self) -> String { pub fn to_rgb_hex_srgb(&self) -> String {
let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8); self.to_gamma_srgb().to_rgb_hex_srgb_from_gamma()
if self.a() < 1. {
let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8);
}
result
} }
/// Return a 6-character RGB hex string (without a # prefix). /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.rgb_hex()) /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ``` /// ```
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub fn rgb_hex(&self) -> String { pub fn to_rgb_hex_srgb_from_gamma(&self) -> String {
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
} }
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -908,6 +898,7 @@ impl Color {
} }
/// Creates a color from a 6-character RGB hex string (without a # prefix). /// Creates a color from a 6-character RGB hex string (without a # prefix).
///
/// ``` /// ```
/// use graphene_core::raster::color::Color; /// use graphene_core::raster::color::Color;
/// let color = Color::from_rgb_str("7C67FA").unwrap(); /// let color = Color::from_rgb_str("7C67FA").unwrap();

View File

@ -10,20 +10,21 @@ use std::sync::{LazyLock, Mutex};
pub mod types { pub mod types {
/// 0% - 100% /// 0% - 100%
pub type Percentage = f64; pub type Percentage = f64;
/// -180° - 180°
pub type Angle = f64;
/// -100% - 100% /// -100% - 100%
pub type SignedPercentage = f64; pub type SignedPercentage = f64;
/// Non negative integer, px unit /// -180° - 180°
pub type Angle = f64;
/// Non-negative integer with px unit
pub type PixelLength = f64; pub type PixelLength = f64;
/// Non negative /// Non-negative
pub type Length = f64; pub type Length = f64;
/// 0 to 1 /// 0 to 1
pub type Fraction = f64; pub type Fraction = f64;
/// Unsigned integer
pub type IntegerCount = u32; pub type IntegerCount = u32;
/// Int input with randomization button /// Unsigned integer to be used for random seeds
pub type SeedValue = u32; pub type SeedValue = u32;
/// Non Negative integer vec with px unit /// Non-negative integer vector2 with px unit
pub type Resolution = glam::UVec2; pub type Resolution = glam::UVec2;
} }

View File

@ -15,9 +15,10 @@ 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 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.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(pub Vec<(f64, Color)>); pub struct GradientStops(Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops { impl std::hash::Hash for GradientStops {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) { fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
@ -35,8 +36,54 @@ impl Default for GradientStops {
} }
} }
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops { impl GradientStops {
pub fn evalute(&self, t: f64) -> Color { pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() { if self.0.is_empty() {
return Color::BLACK; return Color::BLACK;
} }
@ -60,9 +107,17 @@ impl GradientStops {
Color::BLACK Color::BLACK
} }
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self { pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect()) Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
} }
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
} }
/// A gradient fill. /// A gradient fill.
@ -110,7 +165,7 @@ impl Gradient {
Gradient { Gradient {
start, start,
end, end,
stops: GradientStops(vec![(0., start_color), (1., end_color)]), stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform, transform,
gradient_type, gradient_type,
} }
@ -131,7 +186,7 @@ impl Gradient {
(position, color) (position, color)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let stops = GradientStops(stops); let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self { Self {
@ -156,7 +211,7 @@ impl 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.rgb_hex()); let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma());
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.);
} }
@ -242,7 +297,7 @@ impl Gradient {
/// ///
/// 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. This will probably be named "Paint" in the future.
#[repr(C)] #[repr(C)]
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)]
pub enum Fill { pub enum Fill {
@ -305,7 +360,7 @@ impl 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.rgb_hex()); let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma());
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.);
} }
@ -325,6 +380,14 @@ impl Fill {
_ => None, _ => None,
} }
} }
/// Extract a solid color from the fill
pub fn as_solid(&self) -> Option<Color> {
match self {
Self::Solid(color) => Some(*color),
_ => None,
}
}
} }
impl From<Color> for Fill { impl From<Color> for Fill {
@ -355,18 +418,13 @@ impl From<Gradient> for Fill {
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),
} }
impl FillChoice { impl FillChoice {
pub fn from_optional_color(color: Option<Color>) -> Self {
match color {
Some(color) => Self::Solid(color),
None => Self::None,
}
}
pub fn as_solid(&self) -> Option<Color> { pub fn as_solid(&self) -> Option<Color> {
let Self::Solid(color) = self else { return None }; let Self::Solid(color) = self else { return None };
Some(*color) Some(*color)
@ -575,7 +633,7 @@ impl Stroke {
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit); let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
// Render the needed stroke attributes // Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex()); let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
if color.a() < 1. { 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.);
} }

View File

@ -78,7 +78,7 @@ where
}, },
}; };
let color = gradient.evalute(factor); let color = gradient.evaluate(factor);
if fill { if fill {
vector_data.instance.style.set_fill(Fill::Solid(color)); vector_data.instance.style.set_fill(Fill::Solid(color));

View File

@ -131,7 +131,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen
let mut context = wgpu_executor::RenderContext::default(); let mut context = wgpu_executor::RenderContext::default();
data.render_to_vello(&mut child, Default::default(), &mut context); data.render_to_vello(&mut child, Default::default(), &mut context);
// TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(n) cost
scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array()))); scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array())));
let mut background = Color::from_rgb8_srgb(0x22, 0x22, 0x22); let mut background = Color::from_rgb8_srgb(0x22, 0x22, 0x22);