From 6292dea1034566a2d13c5c2b879e7679f4f3fceb Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 18 Mar 2025 05:37:20 -0700 Subject: [PATCH] Refactor many usages of Color to natively store linear not gamma (#2457) --- .../messages/layout/layout_message_handler.rs | 14 +-- .../utility_types/widgets/button_widgets.rs | 1 + .../graph_operation_message_handler.rs | 4 +- .../document/node_graph/node_properties.rs | 3 + .../document/overlays/grid_overlays.rs | 41 +++------ .../document/overlays/utility_types.rs | 7 +- .../portfolio/document/utility_types/misc.rs | 5 +- .../common_functionality/color_selector.rs | 12 +-- .../graph_modification_utils.rs | 2 +- .../messages/tool/tool_messages/brush_tool.rs | 2 +- .../tool/tool_messages/ellipse_tool.rs | 4 +- .../tool/tool_messages/eyedropper_tool.rs | 4 +- .../messages/tool/tool_messages/fill_tool.rs | 8 +- .../tool/tool_messages/freehand_tool.rs | 4 +- .../tool/tool_messages/gradient_tool.rs | 36 ++++---- .../messages/tool/tool_messages/line_tool.rs | 2 +- .../messages/tool/tool_messages/path_tool.rs | 7 +- .../messages/tool/tool_messages/pen_tool.rs | 4 +- .../tool/tool_messages/polygon_tool.rs | 4 +- .../tool/tool_messages/rectangle_tool.rs | 4 +- .../tool/tool_messages/select_tool.rs | 9 +- .../tool/tool_messages/spline_tool.rs | 4 +- .../messages/tool/tool_messages/text_tool.rs | 10 ++- .../transform_layer_message_handler.rs | 1 - editor/src/messages/tool/utility_types.rs | 2 +- frontend/src/messages.ts | 2 +- frontend/wasm/src/editor_api.rs | 18 ++-- node-graph/gcore/src/application_io.rs | 2 +- .../gcore/src/graphic_element/renderer.rs | 10 +-- node-graph/gcore/src/logic.rs | 5 +- node-graph/gcore/src/raster/adjustments.rs | 12 +-- node-graph/gcore/src/raster/color.rs | 67 ++++++-------- node-graph/gcore/src/registry.rs | 13 +-- node-graph/gcore/src/vector/style.rs | 88 +++++++++++++++---- node-graph/gcore/src/vector/vector_nodes.rs | 2 +- node-graph/gstd/src/wasm_application_io.rs | 2 +- 36 files changed, 232 insertions(+), 183 deletions(-) diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 4fbe4b27..88b4c7c5 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -82,6 +82,7 @@ impl LayoutMessageHandler { let callback_message = match action { WidgetValueAction::Commit => (color_button.on_commit.callback)(&()), WidgetValueAction::Update => { + // Decodes the colors in gamma, not linear let decode_color = |color: &serde_json::map::Map| -> Option { 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); @@ -120,20 +121,13 @@ impl LayoutMessageHandler { .filter_map(|stop| { stop.as_object().and_then(|stop| { let position = stop.get("position").and_then(|x| x.as_f64()); - let color = stop.get("color").and_then(|x| x.as_object()); - - if let (Some(position), Some(color_object)) = (position, color) { - if let Some(color) = decode_color(color_object) { - return Some((position, color)); - } - } - - None + 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 } }) }) .collect::>(); - 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); } diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 6c5d28d6..8025d7da 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -147,6 +147,7 @@ pub struct ImageButton { #[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] #[derivative(Debug, PartialEq, Default)] pub struct ColorInput { + /// WARNING: The colors are gamma, not linear! #[widget_builder(constructor)] pub value: FillChoice, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index cea12c3e..b65968fc 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -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 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 { 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 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 { start, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 40dc2f96..908751bf 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1087,7 +1087,10 @@ pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, return LayoutGroup::Row { widgets }; }; + // Add a separator widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + + // Add the color input match &**tagged_value { TaggedValue::Color(color) => widgets.push( color_button diff --git a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs index 8a685a05..d9c9e2a3 100644 --- a/editor/src/messages/portfolio/document/overlays/grid_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/grid_overlays.rs @@ -9,7 +9,7 @@ use graphene_std::vector::style::FillChoice; fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { 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 { return; }; @@ -36,11 +36,7 @@ fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: } else { DVec2::new(secondary_pos, primary_end) }; - overlay_context.line( - document_to_viewport.transform_point2(start), - document_to_viewport.transform_point2(end), - Some(&("#".to_string() + &grid_color.rgba_hex())), - ); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color)); } } } @@ -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. fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) { 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 { return; }; @@ -80,16 +76,13 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte let x_per_dot = (end.x - start.x) / total_dots; for dot_index in 0..=total_dots as usize { let exact_x = x_per_dot * dot_index as f64; - overlay_context.pixel( - document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), - Some(&("#".to_string() + &grid_color.rgba_hex())), - ) + overlay_context.pixel(document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(), Some(&grid_color)) } } } 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 origin = document.snapping_state.grid.origin; 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 start = DVec2::new(x_pos, min_y); let end = DVec2::new(x_pos, max_y); - overlay_context.line( - document_to_viewport.transform_point2(start), - document_to_viewport.transform_point2(end), - Some(&("#".to_string() + &grid_color.rgba_hex())), - ); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color)); } 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 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))); - overlay_context.line( - document_to_viewport.transform_point2(start), - document_to_viewport.transform_point2(end), - Some(&("#".to_string() + &grid_color.rgba_hex())), - ); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(&grid_color)); } } } 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 origin = document.snapping_state.grid.origin; 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( document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), - Some(&("#".to_string() + &grid_color.rgba_hex())), + Some(&grid_color), Some(1.), Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() - 1.), None, @@ -228,10 +213,8 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { }; let update_color = |grid, update: fn(&mut GridSnapping) -> Option<&mut Color>| { update_val::(grid, move |grid, color| { - if let FillChoice::Solid(color) = color.value { - if let Some(update_color) = update(grid) { - *update_color = color; - } + if let (Some(color), Some(update_color)) = (color.value.as_solid(), update(grid)) { + *update_color = color.to_linear_srgb(); } }) }; @@ -278,7 +261,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec { Separator::new(SeparatorType::Related).widget_holder(), ]); color_widgets.push( - ColorInput::new(FillChoice::Solid(grid.grid_color)) + ColorInput::new(FillChoice::Solid(grid.grid_color.to_gamma_srgb())) .tooltip("Grid display color") .allow_none(false) .on_update(update_color(grid, |grid| Some(&mut grid.grid_color))) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index bac8d683..d13fa300 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -320,7 +320,7 @@ impl OverlayContext { let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()) .unwrap() .with_alpha(0.05) - .rgba_hex(); + .to_rgba_hex_srgb(); fill_color.insert(0, '#'); let fill_color = Some(fill_color.as_str()); self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None); @@ -357,7 +357,10 @@ impl OverlayContext { // 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, '#'); self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH); diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index c24682ff..3b7821f6 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -217,10 +217,7 @@ impl Default for GridSnapping { Self { origin: DVec2::ZERO, grid_type: Default::default(), - grid_color: COLOR_OVERLAY_GRAY - .strip_prefix('#') - .and_then(Color::from_rgb_str) - .expect("Should create Color from prefixed hex string"), + grid_color: Color::from_rgb_str(COLOR_OVERLAY_GRAY.strip_prefix('#').unwrap()).unwrap(), dot_display: false, } } diff --git a/editor/src/messages/tool/common_functionality/color_selector.rs b/editor/src/messages/tool/common_functionality/color_selector.rs index e1ec64a1..ed14ba56 100644 --- a/editor/src/messages/tool/common_functionality/color_selector.rs +++ b/editor/src/messages/tool/common_functionality/color_selector.rs @@ -60,14 +60,14 @@ impl ToolColorOptions { pub fn apply_fill(&self, layer: LayerNodeIdentifier, responses: &mut VecDeque) { 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 }); } } pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque) { 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 }); } } @@ -111,9 +111,11 @@ impl ToolColorOptions { widgets.push(radio); widgets.push(Separator::new(SeparatorType::Related).widget_holder()); - let color_button = ColorInput::new(FillChoice::from_optional_color(self.active_color())) - .allow_none(color_allow_none) - .on_update(color_callback); + let fill_choice = match self.active_color() { + Some(color) => FillChoice::Solid(color.to_gamma_srgb()), + 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 diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 0b55ed94..3220eed0 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -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 { return None; }; - Some(*color) + Some(color.to_linear_srgb()) } /// Get the current blend mode of a layer from the closest Blend Mode node diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index ab6be8ab..ac7e3883 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -155,7 +155,7 @@ impl LayoutHolder for BrushTool { false, |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).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()); diff --git a/editor/src/messages/tool/tool_messages/ellipse_tool.rs b/editor/src/messages/tool/tool_messages/ellipse_tool.rs index 35603426..e86a3763 100644 --- a/editor/src/messages/tool/tool_messages/ellipse_tool.rs +++ b/editor/src/messages/tool/tool_messages/ellipse_tool.rs @@ -91,7 +91,7 @@ impl LayoutHolder for EllipseTool { true, |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::FillColor(None)).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()); @@ -101,7 +101,7 @@ impl LayoutHolder for EllipseTool { true, |_| EllipseToolMessage::UpdateOptions(EllipseOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs index 1f815e8b..3b3f3249 100644 --- a/editor/src/messages/tool/tool_messages/eyedropper_tool.rs +++ b/editor/src/messages/tool/tool_messages/eyedropper_tool.rs @@ -159,8 +159,8 @@ fn disable_cursor_preview(responses: &mut VecDeque) { fn update_cursor_preview(responses: &mut VecDeque, input: &InputPreprocessorMessageHandler, global_tool_data: &DocumentToolData, set_color_choice: Option) { responses.add(FrontendMessage::UpdateEyedropperSamplingState { mouse_position: Some(input.mouse.position.into()), - primary_color: "#".to_string() + global_tool_data.primary_color.rgb_hex().as_str(), - secondary_color: "#".to_string() + global_tool_data.secondary_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.to_rgb_hex_srgb().as_str(), set_color_choice, }); } diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index f3103540..fe616213 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -91,8 +91,8 @@ impl Fsm for FillToolFsmState { return self; } let fill = match color_event { - FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color), - FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color), + FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color.to_gamma_srgb()), + FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()), _ => return self, }; @@ -167,7 +167,7 @@ mod test_fill { editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; let fills = get_fills(&mut editor).await; 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] @@ -180,6 +180,6 @@ mod test_fill { editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; let fills = get_fills(&mut editor).await; 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()); } } diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index 4a67fb3e..ec5e2b8c 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -97,7 +97,7 @@ impl LayoutHolder for FreehandTool { true, |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::FillColor(None)).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()); @@ -107,7 +107,7 @@ impl LayoutHolder for FreehandTool { true, |_| FreehandToolMessage::UpdateOptions(FreehandOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 164b06ae..5efb22f6 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -184,11 +184,11 @@ impl SelectedGradient { // Should not go off end but can swap let clamped = new_pos.clamp(0., 1.); - self.gradient.stops.0[s].0 = clamped; - let new_pos = self.gradient.stops.0[s]; + self.gradient.stops.get_mut(s).unwrap().0 = clamped; + let new_pos = self.gradient.stops[s]; - self.gradient.stops.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - self.dragging = GradientDragTarget::Step(self.gradient.stops.0.iter().position(|x| *x == new_pos).unwrap()); + self.gradient.stops.sort(); + self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|x| *x == new_pos).unwrap()); } } 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(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. { continue; } @@ -276,7 +276,7 @@ impl Fsm for GradientToolFsmState { }; // Skip if invalid gradient - if selected_gradient.gradient.stops.0.len() < 2 { + if selected_gradient.gradient.stops.len() < 2 { return self; } @@ -284,25 +284,31 @@ impl Fsm for GradientToolFsmState { // Remove the selected point match selected_gradient.dragging { - GradientDragTarget::Start => selected_gradient.gradient.stops.0.remove(0), - GradientDragTarget::End => selected_gradient.gradient.stops.0.pop().unwrap(), - GradientDragTarget::Step(index) => selected_gradient.gradient.stops.0.remove(index), + GradientDragTarget::Start => { + selected_gradient.gradient.stops.remove(0); + } + 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 - if selected_gradient.gradient.stops.0.len() == 1 { + if selected_gradient.gradient.stops.len() == 1 { if let Some(layer) = selected_gradient.layer { responses.add(GraphOperationMessage::FillSet { layer, - fill: Fill::Solid(selected_gradient.gradient.stops.0[0].1), + fill: Fill::Solid(selected_gradient.gradient.stops[0].1), }); } return self; } // 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 max_position = selected_gradient.gradient.stops.0.iter().map(|(pos, _)| *pos).reduce(f64::max).expect("No max"); + let min_position = selected_gradient.gradient.stops.iter().map(|(pos, _)| *pos).reduce(f64::min).expect("No min"); + 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) let transform = selected_gradient.transform; @@ -312,7 +318,7 @@ impl Fsm for GradientToolFsmState { selected_gradient.gradient.end = transform.inverse().transform_point2(new_end); // 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); } @@ -365,7 +371,7 @@ impl Fsm for GradientToolFsmState { let Some(gradient) = get_gradient(layer, &document.network_interface) else { continue }; let transform = gradient_space_transform(layer, document); // 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)); if pos.distance_squared(mouse) < tolerance { dragging = true; diff --git a/editor/src/messages/tool/tool_messages/line_tool.rs b/editor/src/messages/tool/tool_messages/line_tool.rs index 1d22a0fb..9006c73e 100644 --- a/editor/src/messages/tool/tool_messages/line_tool.rs +++ b/editor/src/messages/tool/tool_messages/line_tool.rs @@ -86,7 +86,7 @@ impl LayoutHolder for LineTool { true, |_| LineToolMessage::UpdateOptions(LineOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index af194fec..4e5278de 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -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()) .unwrap() .with_alpha(0.05) - .rgba_hex(); + .to_rgba_hex_srgb(); fill_color.insert(0, '#'); let fill_color = Some(fill_color.as_str()); @@ -961,7 +961,10 @@ impl Fsm for PathToolFsmState { let origin = tool_data.drag_start_pos; 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, '#'); let other = faded_blue.as_str(); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 2c8c7b38..d3c9ad23 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -145,7 +145,7 @@ impl LayoutHolder for PenTool { true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::FillColor(None)).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()); @@ -155,7 +155,7 @@ impl LayoutHolder for PenTool { true, |_| PenToolMessage::UpdateOptions(PenOptionsUpdate::StrokeColor(None)).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()); diff --git a/editor/src/messages/tool/tool_messages/polygon_tool.rs b/editor/src/messages/tool/tool_messages/polygon_tool.rs index 0f644ce4..51cce795 100644 --- a/editor/src/messages/tool/tool_messages/polygon_tool.rs +++ b/editor/src/messages/tool/tool_messages/polygon_tool.rs @@ -135,7 +135,7 @@ impl LayoutHolder for PolygonTool { true, |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::FillColor(None)).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()); @@ -145,7 +145,7 @@ impl LayoutHolder for PolygonTool { true, |_| PolygonToolMessage::UpdateOptions(PolygonOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/rectangle_tool.rs b/editor/src/messages/tool/tool_messages/rectangle_tool.rs index 7bc698e4..ee5e73e1 100644 --- a/editor/src/messages/tool/tool_messages/rectangle_tool.rs +++ b/editor/src/messages/tool/tool_messages/rectangle_tool.rs @@ -79,7 +79,7 @@ impl LayoutHolder for RectangleTool { true, |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::FillColor(None)).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()); @@ -89,7 +89,7 @@ impl LayoutHolder for RectangleTool { true, |_| RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index a3d4a30b..00af97ab 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -692,7 +692,7 @@ impl Fsm for SelectToolFsmState { let color = if !hover { color } 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) }; let line_center = tool_data.line_center; @@ -707,7 +707,10 @@ impl Fsm for SelectToolFsmState { let angle = -mouse_position.angle_to(DVec2::X); 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, '#'); 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()) .unwrap() .with_alpha(0.05) - .rgba_hex(); + .to_rgba_hex_srgb(); fill_color.insert(0, '#'); let fill_color = Some(fill_color.as_str()); diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index 9f29d340..6d4e1789 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -103,7 +103,7 @@ impl LayoutHolder for SplineTool { true, |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::FillColor(None)).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()); @@ -113,7 +113,7 @@ impl LayoutHolder for SplineTool { true, |_| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::StrokeColor(None)).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(create_weight_widget(self.options.line_weight)); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index df502837..87526192 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -162,7 +162,7 @@ impl LayoutHolder for TextTool { true, |_| TextToolMessage::UpdateOptions(TextOptionsUpdate::FillColor(None)).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 }])) @@ -378,7 +378,11 @@ impl TextToolData { responses.add(Message::StartBuffer); responses.add(GraphOperationMessage::FillSet { 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 { 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()) .unwrap() .with_alpha(0.05) - .rgba_hex(); + .to_rgba_hex_srgb(); let ToolMessage::Text(event) = event else { return self }; match (self, event) { diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index c25f3ce8..cf4c4585 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -401,7 +401,6 @@ impl MessageHandler> for TransformLayer } } } else { - // TODO: Fix handle snap to anchor issue, see let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); if handle_length == Some(0.) { diff --git a/editor/src/messages/tool/utility_types.rs b/editor/src/messages/tool/utility_types.rs index fd8420a3..12acacea 100644 --- a/editor/src/messages/tool/utility_types.rs +++ b/editor/src/messages/tool/utility_types.rs @@ -116,7 +116,7 @@ impl DocumentToolData { pub fn update_working_colors(&self, responses: &mut VecDeque) { let layout = WidgetLayout::new(vec![ 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 { widgets: vec![ diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index f6e7188d..6f3ee10b 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -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 { readonly red!: number; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 30af3d33..8bc7b6c3 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -481,12 +481,13 @@ impl EditorHandle { /// Update primary color with values on a scale from 0 to 1. #[wasm_bindgen(js_name = updatePrimaryColor)] 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) { - Some(color) => color, - None => return Err(Error::new("Invalid color").into()), + let Some(primary_color) = Color::from_rgbaf32(red, green, blue, alpha) else { + 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); Ok(()) @@ -495,12 +496,13 @@ impl EditorHandle { /// Update secondary color with values on a scale from 0 to 1. #[wasm_bindgen(js_name = updateSecondaryColor)] 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) { - Some(color) => color, - None => return Err(Error::new("Invalid color").into()), + let Some(secondary_color) = Color::from_rgbaf32(red, green, blue, alpha) else { + 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); Ok(()) diff --git a/node-graph/gcore/src/application_io.rs b/node-graph/gcore/src/application_io.rs index a47373cb..db07fc44 100644 --- a/node-graph/gcore/src/application_io.rs +++ b/node-graph/gcore/src/application_io.rs @@ -91,7 +91,7 @@ impl PartialEq for ImageTexture { } #[cfg(not(feature = "wgpu"))] { - true // Unit values are always equal + self.texture == other.texture } } } diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 7a4fa7be..c03b9c56 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -503,7 +503,7 @@ impl GraphicElementRendered for VectorDataTable { } Fill::Gradient(gradient) => { let mut stops = peniko::ColorStops::new(); - for &(offset, color) in &gradient.stops.0 { + for &(offset, color) in &gradient.stops { stops.push(peniko::ColorStop { offset: offset as f32, color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])), @@ -664,7 +664,7 @@ impl GraphicElementRendered for Artboard { if !render_params.hide_artboards { // Background render.leaf_tag("rect", |attributes| { - attributes.push("fill", format!("#{}", self.background.rgb_hex())); + attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma())); if self.background.a() < 1. { attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string()); } @@ -1059,13 +1059,13 @@ impl GraphicElementRendered for Option { render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color")); return; }; - let color_info = format!("{:?} #{} {:?}", color, color.rgba_hex(), color.to_rgba8_srgb()); + let color_info = format!("{:?} #{} {:?}", color, color.to_rgba_hex_srgb(), color.to_rgba8_srgb()); render.leaf_tag("rect", |attributes| { attributes.push("width", "100"); attributes.push("height", "100"); attributes.push("y", "40"); - attributes.push("fill", format!("#{}", color.rgb_hex())); + attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); if color.a() < 1. { attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); } @@ -1086,7 +1086,7 @@ impl GraphicElementRendered for Vec { attributes.push("height", "100"); attributes.push("x", (index * 120).to_string()); attributes.push("y", "40"); - attributes.push("fill", format!("#{}", color.rgb_hex())); + attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); if color.a() < 1. { attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); } diff --git a/node-graph/gcore/src/logic.rs b/node-graph/gcore/src/logic.rs index e3a798d2..c1e7d21f 100644 --- a/node-graph/gcore/src/logic.rs +++ b/node-graph/gcore/src/logic.rs @@ -1,10 +1,9 @@ -use crate::Context; -use crate::Ctx; use crate::vector::VectorDataTable; +use crate::{Color, Context, Ctx}; use glam::{DAffine2, DVec2}; #[node_macro::node(category("Debug"))] -fn log_to_console(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2)] value: T) -> T { +fn log_to_console(_: impl Ctx, #[implementations(String, bool, f64, u32, u64, DVec2, VectorDataTable, DAffine2, Color, Option)] value: T) -> T { #[cfg(not(target_arch = "spirv"))] // KEEP THIS `debug!()` - It acts as the output for the debug node itself debug!("{:#?}", value); diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 53cd412f..bd35b370 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -614,21 +614,21 @@ impl Blend for ImageFrameTable { } impl Blend for GradientStops { fn blend(&self, under: &Self, blend_fn: impl Fn(Color, Color) -> Color) -> Self { - let mut combined_stops = self.0.iter().map(|(position, _)| position).chain(under.0.iter().map(|(position, _)| position)).collect::>(); + let mut combined_stops = self.iter().map(|(position, _)| position).chain(under.iter().map(|(position, _)| position)).collect::>(); combined_stops.dedup_by(|&mut a, &mut b| (a - b).abs() < 1e-6); combined_stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); let stops = combined_stops .into_iter() .map(|&position| { - let over_color = self.evalute(position); - let under_color = under.evalute(position); + let over_color = self.evaluate(position); + let under_color = under.evaluate(position); let color = blend_fn(over_color, under_color); (position, color) }) .collect::>(); - GradientStops(stops) + GradientStops::new(stops) } } @@ -721,7 +721,7 @@ impl Adjust for Option { } impl Adjust for GradientStops { fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) { - for (_pos, c) in self.0.iter_mut() { + for (_pos, c) in self.iter_mut() { *c = map_fn(c); } } @@ -770,7 +770,7 @@ async fn gradient_map>( image.adjust(|color| { let intensity = color.luminance_srgb(); let intensity = if reverse { 1. - intensity } else { intensity }; - gradient.evalute(intensity as f64) + gradient.evaluate(intensity as f64) }); image diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 4fcea6de..3be18d19 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -9,7 +9,6 @@ use spirv_std::num_traits::Euclid; #[cfg(feature = "serde")] #[cfg(target_arch = "spirv")] use spirv_std::num_traits::float::Float; -use std::fmt::Write; #[repr(C)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -388,7 +387,7 @@ impl Color { Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha) } - /// Return an opaque SDR `Color` given RGB channels from `0` to `255`. + /// Return an opaque SDR `Color` given RGB channels from `0` to `255`, premultiplied by alpha. /// /// # Examples /// ``` @@ -402,7 +401,8 @@ impl Color { Color::from_rgba8_srgb(red, green, blue, 255) } - /// Return an SDR `Color` given RGBA channels from `0` to `255`. + // TODO: Should this be premult? + /// Return an SDR `Color` given RGBA channels from `0` to `255`, premultiplied by alpha. /// /// # Examples /// ``` @@ -411,16 +411,13 @@ impl Color { /// ``` #[inline(always)] pub fn from_rgba8_srgb(red: u8, green: u8, blue: u8, alpha: u8) -> Color { - let alpha = alpha as f32 / 255.; let map_range = |int_color| int_color as f32 / 255.; - Color { - red: map_range(red), - green: map_range(green), - blue: map_range(blue), - alpha, - } - .to_linear_srgb() - .map_rgb(|channel| channel * alpha) + + let red = map_range(red); + let green = map_range(green); + let blue = map_range(blue); + let alpha = map_range(alpha); + Color { red, green, blue, alpha }.to_linear_srgb().map_rgb(|channel| channel * alpha) } /// Create a [Color] from a hue, saturation, lightness and alpha (all between 0 and 1) @@ -788,56 +785,49 @@ impl Color { (self.red, self.green, self.blue, self.alpha) } - /// Return an 8-character RGBA hex string (without a # prefix). + /// Return an 8-character RGBA hex string (without a # prefix). Use this if the [`Color`] is in linear space. /// /// # Examples /// ``` /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); - /// assert_eq!("3240a261", color.rgba_hex()) + /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha + /// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` #[cfg(feature = "std")] - pub fn rgba_hex(&self) -> String { + pub fn to_rgba_hex_srgb(&self) -> String { + let gamma = self.to_gamma_srgb(); format!( "{:02x?}{:02x?}{:02x?}{:02x?}", - (self.r() * 255.) as u8, - (self.g() * 255.) as u8, - (self.b() * 255.) as u8, - (self.a() * 255.) as u8, + (gamma.r() * 255.) as u8, + (gamma.g() * 255.) as u8, + (gamma.b() * 255.) as u8, + (gamma.a() * 255.) as u8, ) } - /// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1. - /// - /// # Examples + /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space. /// ``` /// use graphene_core::raster::color::Color; - /// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); - /// assert_eq!("3240a261", color1.rgb_optional_a_hex()); - /// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0xFF).to_gamma_srgb(); - /// assert_eq!("5267fa", color2.rgb_optional_a_hex()); + /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha + /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` #[cfg(feature = "std")] - pub fn rgb_optional_a_hex(&self) -> String { - let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8); - if self.a() < 1. { - let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8); - } - result + pub fn to_rgb_hex_srgb(&self) -> String { + self.to_gamma_srgb().to_rgb_hex_srgb_from_gamma() } - /// Return a 6-character RGB hex string (without a # prefix). + /// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space. /// ``` /// use graphene_core::raster::color::Color; - /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb(); - /// assert_eq!("3240a2", color.rgb_hex()) + /// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha + /// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha /// ``` #[cfg(feature = "std")] - pub fn rgb_hex(&self) -> String { + pub fn to_rgb_hex_srgb_from_gamma(&self) -> String { format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8) } - /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. + /// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha. Use this if the [`Color`] is in linear space. /// /// # Examples /// ``` @@ -908,6 +898,7 @@ impl Color { } /// Creates a color from a 6-character RGB hex string (without a # prefix). + /// /// ``` /// use graphene_core::raster::color::Color; /// let color = Color::from_rgb_str("7C67FA").unwrap(); diff --git a/node-graph/gcore/src/registry.rs b/node-graph/gcore/src/registry.rs index 883973ef..a419ad3d 100644 --- a/node-graph/gcore/src/registry.rs +++ b/node-graph/gcore/src/registry.rs @@ -10,20 +10,21 @@ use std::sync::{LazyLock, Mutex}; pub mod types { /// 0% - 100% pub type Percentage = f64; - /// -180° - 180° - pub type Angle = f64; /// -100% - 100% pub type SignedPercentage = f64; - /// Non negative integer, px unit + /// -180° - 180° + pub type Angle = f64; + /// Non-negative integer with px unit pub type PixelLength = f64; - /// Non negative + /// Non-negative pub type Length = f64; /// 0 to 1 pub type Fraction = f64; + /// Unsigned integer pub type IntegerCount = u32; - /// Int input with randomization button + /// Unsigned integer to be used for random seeds pub type SeedValue = u32; - /// Non Negative integer vec with px unit + /// Non-negative integer vector2 with px unit pub type Resolution = glam::UVec2; } diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 03386719..3ae0e9e6 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -15,9 +15,10 @@ pub enum GradientType { } // TODO: Someday we could switch this to a Box[T] to avoid over-allocation +// TODO: Use linear not gamma colors /// A list of colors associated with positions (in the range 0 to 1) along a gradient. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)] -pub struct GradientStops(pub Vec<(f64, Color)>); +pub struct GradientStops(Vec<(f64, Color)>); impl std::hash::Hash for GradientStops { fn hash(&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 for GradientStops { + type Output = (f64, Color); + + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +impl std::ops::Deref for GradientStops { + type Target = Vec<(f64, Color)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for GradientStops { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl GradientStops { - pub fn evalute(&self, t: f64) -> Color { + pub fn new(stops: Vec<(f64, Color)>) -> Self { + let mut stops = Self(stops); + stops.sort(); + stops + } + + pub fn evaluate(&self, t: f64) -> Color { if self.0.is_empty() { return Color::BLACK; } @@ -60,9 +107,17 @@ impl GradientStops { Color::BLACK } + pub fn sort(&mut self) { + self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + } + pub fn reversed(&self) -> Self { Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect()) } + + pub fn map_colors Color>(&self, f: F) -> Self { + Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect()) + } } /// A gradient fill. @@ -110,7 +165,7 @@ impl Gradient { Gradient { start, end, - stops: GradientStops(vec![(0., start_color), (1., end_color)]), + stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]), transform, gradient_type, } @@ -131,7 +186,7 @@ impl Gradient { (position, color) }) .collect::>(); - let stops = GradientStops(stops); + let stops = GradientStops::new(stops); let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type }; Self { @@ -156,7 +211,7 @@ impl Gradient { if *position != 0. { let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.); } - let _ = write!(stop, r##" stop-color="#{}""##, color.rgb_hex()); + let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -242,7 +297,7 @@ impl Gradient { /// /// Can be None, a solid [Color], or a linear/radial [Gradient]. /// -/// In the future we'll probably also add a pattern fill. +/// In the future we'll probably also add a pattern fill. This will probably be named "Paint" in the future. #[repr(C)] #[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type)] pub enum Fill { @@ -305,7 +360,7 @@ impl Fill { match self { Self::None => r#" fill="none""#.to_string(), Self::Solid(color) => { - let mut result = format!(r##" fill="#{}""##, color.rgb_hex()); + let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -325,6 +380,14 @@ impl Fill { _ => None, } } + + /// Extract a solid color from the fill + pub fn as_solid(&self) -> Option { + match self { + Self::Solid(color) => Some(*color), + _ => None, + } + } } impl From for Fill { @@ -355,18 +418,13 @@ impl From for Fill { pub enum FillChoice { #[default] None, + /// WARNING: Color is gamma, not linear! Solid(Color), + /// WARNING: Color stops are gamma, not linear! Gradient(GradientStops), } impl FillChoice { - pub fn from_optional_color(color: Option) -> Self { - match color { - Some(color) => Self::Solid(color), - None => Self::None, - } - } - pub fn as_solid(&self) -> Option { let Self::Solid(color) = self else { return None }; Some(*color) @@ -575,7 +633,7 @@ impl Stroke { let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit); // Render the needed stroke attributes - let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex()); + let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 21f49822..275f6011 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -78,7 +78,7 @@ where }, }; - let color = gradient.evalute(factor); + let color = gradient.evaluate(factor); if fill { vector_data.instance.style.set_fill(Fill::Solid(color)); diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 0bee4e77..686ea72d 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -131,7 +131,7 @@ async fn render_canvas(render_config: RenderConfig, data: impl GraphicElementRen let mut context = wgpu_executor::RenderContext::default(); data.render_to_vello(&mut child, Default::default(), &mut context); - // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(Nr cost + // TODO: Instead of applying the transform here, pass the transform during the translation to avoid the O(n) cost scene.append(&child, Some(kurbo::Affine::new(footprint.transform.to_cols_array()))); let mut background = Color::from_rgb8_srgb(0x22, 0x22, 0x22);