diff --git a/.branding b/.branding index 7f1c9ecbe7..1e29c19dff 100644 --- a/.branding +++ b/.branding @@ -1,2 +1,2 @@ -https://github.com/Keavon/graphite-branded-assets/archive/f44aa2f362ae4fed8d634878b817a1d3948a7dcb.tar.gz -dffe2b483e491979ef57c320d61446ada5400ef73ff26582976631d9c36efefc +https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz +c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fbf8c7f1b..d1c8a4ae1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -55,7 +55,8 @@ "a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` "a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` "a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` - "a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y_no_noninteractive_element_interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y_no_static_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` }, // Git Graph config "git-graph.repository.fetchAndPrune": true, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 49a63f3d11..37d80450a7 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; use graphene_std::raster::color::Color; -use graphene_std::vector::style::{FillChoice, GradientStops}; +use graphene_std::vector::style::{FillChoice, GradientStop, GradientStops}; use serde_json::Value; use std::collections::HashMap; @@ -193,18 +193,17 @@ impl LayoutMessageHandler { } // Gradient - let gradient = update_value.get("stops").and_then(|x| x.as_array()); - if let Some(stops) = gradient { - let gradient_stops = stops - .iter() - .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()).and_then(decode_color); - if let (Some(position), Some(color)) = (position, color) { Some((position, color)) } else { None } - }) - }) - .collect::>(); + let positions = update_value.get("position").and_then(|x| x.as_array()); + let midpoints = update_value.get("midpoint").and_then(|x| x.as_array()); + let colors = update_value.get("color").and_then(|x| x.as_array()); + + if let (Some(positions), Some(midpoints), Some(colors)) = (positions, midpoints, colors) { + let gradient_stops = positions.iter().zip(midpoints.iter()).zip(colors.iter()).filter_map(|((pos, mid), col)| { + let position = pos.as_f64()?; + let midpoint = mid.as_f64()?; + let color = col.as_object().and_then(decode_color)?; + Some(GradientStop { position, midpoint, color }) + }); color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops)); return (color_button.on_update.callback)(color_button); diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 3e5e91e9d9..36a38e8658 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -564,7 +564,7 @@ impl TableRowLayout for GradientStops { "Gradient" } fn identifier(&self) -> String { - format!("Gradient ({} stops)", self.0.len()) + format!("Gradient ({} stops)", self.len()) } fn element_widget(&self, _index: usize) -> WidgetInstance { ColorInput::new(FillChoice::Gradient(self.clone())) 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 631bc1250b..d0660983c3 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 @@ -14,7 +14,7 @@ use graphene_std::renderer::Quad; use graphene_std::renderer::convert_usvg_path::convert_usvg_path; use graphene_std::table::Table; use graphene_std::text::{Font, TypesettingConfig}; -use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; +use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; #[derive(ExtractField)] pub struct GraphOperationMessageContext<'a> { @@ -337,7 +337,17 @@ impl MessageHandler> for let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.; let transform = transform * DAffine2::from_translation(offset_to_center); - import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index); + let graphite_gradient_stops = extract_graphite_gradient_stops(&svg); + + import_usvg_node( + &mut modify_inputs, + &usvg::Node::Group(Box::new(tree.root().clone())), + transform, + id, + parent, + insert_index, + &graphite_gradient_stops, + ); } } } @@ -362,7 +372,85 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 { DAffine2::from_cols_array(&[c.sx as f64, c.ky as f64, c.kx as f64, c.sy as f64, c.tx as f64, c.ty as f64]) } -fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize) { +const GRAPHITE_NAMESPACE: &str = "https://graphite.art"; + +/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes. +/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops +/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes. +/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops. +fn extract_graphite_gradient_stops(svg: &str) -> HashMap { + let mut result = HashMap::new(); + + // Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing + if !svg.contains("graphite:midpoint") { + return result; + } + + let doc = match usvg::roxmltree::Document::parse(svg) { + Ok(doc) => doc, + Err(_) => return result, + }; + + for node in doc.descendants() { + match node.tag_name().name() { + "linearGradient" | "radialGradient" => {} + _ => continue, + } + + let gradient_id = match node.attribute("id") { + Some(id) => id.to_string(), + None => continue, + }; + + let mut real_stops = Vec::new(); + let mut has_any_midpoint = false; + + for child in node.children() { + if child.tag_name().name() != "stop" { + continue; + } + + let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::().ok()); + + if let Some(midpoint) = midpoint { + has_any_midpoint = true; + + let offset = child.attribute("offset").and_then(|v| v.parse::().ok()).unwrap_or(0.); + let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::().ok()).unwrap_or(1.); + let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK); + + real_stops.push(GradientStop { position: offset, midpoint, color }); + } + } + + if has_any_midpoint && !real_stops.is_empty() { + result.insert(gradient_id, GradientStops::new(real_stops)); + } + } + + result +} + +fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option { + let hex = hex.strip_prefix('#')?; + if hex.len() != 6 { + return None; + } + let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.; + let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.; + let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.; + Some(Color::from_rgbaf32_unchecked(r, g, b, opacity)) +} + +fn import_usvg_node( + modify_inputs: &mut ModifyInputsContext, + node: &usvg::Node, + transform: DAffine2, + id: NodeId, + parent: LayerNodeIdentifier, + insert_index: usize, + graphite_gradient_stops: &HashMap, +) { let layer = modify_inputs.create_layer(id); modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); modify_inputs.layer_node = Some(layer); @@ -372,7 +460,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, match node { usvg::Node::Group(group) => { for child in group.children() { - import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0); + import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops); } modify_inputs.layer_node = Some(layer); } @@ -388,7 +476,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, if let Some(fill) = path.fill() { let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); - apply_usvg_fill(fill, modify_inputs, bounds_transform); + apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops); } if let Some(stroke) = path.stroke() { apply_usvg_stroke(stroke, modify_inputs, transform * usvg_transform(node.abs_transform())); @@ -432,7 +520,7 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont } } -fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2) { +fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap) { modify_inputs.fill_set(match &fill.paint() { usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())), usvg::Paint::LinearGradient(linear) => { @@ -443,8 +531,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b let gradient_type = GradientType::Linear; - let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); - let stops = GradientStops::new(stops); + let stops = match graphite_gradient_stops.get(linear.id()) { + Some(graphite_stops) => graphite_stops.clone(), + None => { + let stops = linear.stops().iter().map(|stop| GradientStop { + position: stop.offset().get() as f64, + midpoint: 0.5, + color: usvg_color(stop.color(), stop.opacity().get()), + }); + GradientStops::new(stops) + } + }; Fill::Gradient(Gradient { start, end, gradient_type, stops }) } @@ -457,8 +554,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b let gradient_type = GradientType::Radial; - let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect(); - let stops = GradientStops::new(stops); + let stops = match graphite_gradient_stops.get(radial.id()) { + Some(graphite_stops) => graphite_stops.clone(), + None => { + let stops = radial.stops().iter().map(|stop| GradientStop { + position: stop.offset().get() as f64, + midpoint: 0.5, + color: usvg_color(stop.color(), stop.opacity().get()), + }); + GradientStops::new(stops) + } + }; Fill::Gradient(Gradient { start, end, gradient_type, stops }) } diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index fd18cc1125..9969256b6f 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -5,7 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::{NodeGraphLayer, get_gradient}; use crate::messages::tool::common_functionality::snapping::SnapManager; -use graphene_std::vector::style::{Fill, Gradient, GradientType}; +use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType}; #[derive(Default, ExtractField)] pub struct GradientTool { @@ -182,13 +182,13 @@ struct SelectedGradient { initial_gradient: Gradient, } -fn calculate_insertion(start: DVec2, end: DVec2, stops: &[(f64, graphene_std::Color)], mouse: DVec2) -> Option { +fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: DVec2) -> Option { let distance = (end - start).angle_to(mouse - start).sin() * (mouse - start).length(); let projection = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end); if distance.abs() < SEGMENT_INSERTION_DISTANCE && (0. ..=1.).contains(&projection) { - for (position, _) in stops { - let stop_pos = start.lerp(end, *position); + for stop in stops { + let stop_pos = start.lerp(end, stop.position); if stop_pos.distance_squared(mouse) < (MANIPULATOR_GROUP_MARKER_SIZE * 2.).powi(2) { return None; } @@ -262,11 +262,12 @@ impl SelectedGradient { // Should not go off end but can swap let clamped = new_pos.clamp(0., 1.); - self.gradient.stops.get_mut(s).unwrap().0 = clamped; - let new_pos = self.gradient.stops[s]; + self.gradient.stops.position[s] = clamped; + let new_position = self.gradient.stops.position[s]; + let new_color = self.gradient.stops.color[s]; self.gradient.stops.sort(); - self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|x| *x == new_pos).unwrap()); + self.dragging = GradientDragTarget::Step(self.gradient.stops.iter().position(|s| s.position == new_position && s.color == new_color).unwrap()); } } self.render_gradient(responses); @@ -357,18 +358,18 @@ impl Fsm for GradientToolFsmState { format!("#{}", color.with_alpha(1.).to_rgba_hex_srgb()) } - let start_hex = stops.first().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); - let end_hex = stops.last().map(|(_, c)| color_to_hex(*c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); + let start_hex = stops.color.first().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); + let end_hex = stops.color.last().map(|&c| color_to_hex(c)).unwrap_or(String::from(COLOR_OVERLAY_BLUE)); overlay_context.line(start, end, None, None); overlay_context.gradient_color_stop(start, dragging == Some(GradientDragTarget::Start), &start_hex); overlay_context.gradient_color_stop(end, dragging == Some(GradientDragTarget::End), &end_hex); - for (index, (position, color)) in stops.clone().into_iter().enumerate() { - if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { + for (index, stop) in stops.iter().enumerate() { + if stop.position.abs() < f64::EPSILON * 1000. || (1. - stop.position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.gradient_color_stop(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(color)); + overlay_context.gradient_color_stop(start.lerp(end, stop.position), dragging == Some(GradientDragTarget::Step(index)), &color_to_hex(stop.color)); } if let (Some(projection), Some(dir)) = (calculate_insertion(start, end, stops, mouse), (end - start).try_normalize()) { @@ -415,7 +416,7 @@ impl Fsm for GradientToolFsmState { if let Some(layer) = selected_gradient.layer { responses.add(GraphOperationMessage::FillSet { layer, - fill: Fill::Solid(selected_gradient.gradient.stops[0].1), + fill: Fill::Solid(selected_gradient.gradient.stops.color[0]), }); } responses.add(DocumentMessage::CommitTransaction); @@ -424,8 +425,8 @@ impl Fsm for GradientToolFsmState { } // Find the minimum and maximum positions - 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"); + let min_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::min).expect("No min"); + let max_position = selected_gradient.gradient.stops.position.iter().copied().reduce(f64::max).expect("No max"); // Recompute the start and end position of the gradient (in viewport transform) let transform = selected_gradient.transform; @@ -435,7 +436,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.iter_mut() { + for position in selected_gradient.gradient.stops.position.iter_mut() { *position = (*position - min_position) / (max_position - min_position); } @@ -492,8 +493,8 @@ 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.iter().enumerate() { - let pos = transform.transform_point2(gradient.start.lerp(gradient.end, *pos)); + for (index, stop) in gradient.stops.iter().enumerate() { + let pos = transform.transform_point2(gradient.start.lerp(gradient.end, stop.position)); if pos.distance_squared(mouse) < tolerance { dragging = true; tool_data.selected_gradient = Some(SelectedGradient { @@ -787,7 +788,7 @@ mod test_gradient { let (gradient, transform) = get_gradient(&mut editor).await; // Gradient goes from secondary color to primary color - let stops = gradient.stops.iter().map(|stop| (stop.0, stop.1.to_rgba8_srgb())).collect::>(); + let stops = gradient.stops.iter().map(|stop| (stop.position, stop.color.to_rgba8_srgb())).collect::>(); assert_eq!(stops, vec![(0., Color::BLUE.to_rgba8_srgb()), (1., Color::GREEN.to_rgba8_srgb())]); assert!(transform.transform_point2(gradient.start).abs_diff_eq(DVec2::new(2., 3.), 1e-10)); assert!(transform.transform_point2(gradient.end).abs_diff_eq(DVec2::new(24., 4.), 1e-10)); @@ -879,7 +880,7 @@ mod test_gradient { let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 3, "Expected 3 stops, found {}", updated_gradient.stops.len()); - let positions: Vec = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = updated_gradient.stops.iter().map(|stop| stop.position).collect(); assert!( positions.iter().any(|pos| (pos - 0.5).abs() < 0.1), "Expected to find a stop near position 0.5, but found: {positions:?}" @@ -975,12 +976,12 @@ mod test_gradient { // Verify initial stop positions and colors let mut stops = initial_gradient.stops.clone(); - stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + stops.sort(); - let positions: Vec = stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = stops.iter().map(|stop| stop.position).collect(); assert_stops_at_positions(&positions, &[0., 0.5, 1.], 0.1); - let middle_color = stops[1].1.to_rgba8_srgb(); + let middle_color = stops.color[1].to_rgba8_srgb(); // Simulate dragging the middle stop to position 0.8 let click_position = DVec2::new(50., 0.); @@ -1014,16 +1015,16 @@ mod test_gradient { // Verify updated stop positions and colors let mut updated_stops = updated_gradient.stops.clone(); - updated_stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + updated_stops.sort(); // Check positions are now correctly ordered - let updated_positions: Vec = updated_stops.iter().map(|(pos, _)| *pos).collect(); + let updated_positions: Vec = updated_stops.iter().map(|stop| stop.position).collect(); assert_stops_at_positions(&updated_positions, &[0., 0.8, 1.], 0.1); // Colors should maintain their associations with the stop points - assert_eq!(updated_stops[0].1.to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb()); - assert_eq!(updated_stops[1].1.to_rgba8_srgb(), middle_color); - assert_eq!(updated_stops[2].1.to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); + assert_eq!(updated_stops.color[0].to_rgba8_srgb(), Color::BLUE.to_rgba8_srgb()); + assert_eq!(updated_stops.color[1].to_rgba8_srgb(), middle_color); + assert_eq!(updated_stops.color[2].to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); } #[tokio::test] @@ -1054,7 +1055,7 @@ mod test_gradient { let (updated_gradient, _) = get_gradient(&mut editor).await; assert_eq!(updated_gradient.stops.len(), 4, "Expected 4 stops, found {}", updated_gradient.stops.len()); - let positions: Vec = updated_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let positions: Vec = updated_gradient.stops.iter().map(|stop| stop.position).collect(); // Use helper function to verify positions assert_stops_at_positions(&positions, &[0., 0.25, 0.75, 1.], 0.05); @@ -1080,7 +1081,7 @@ mod test_gradient { let (final_gradient, _) = get_gradient(&mut editor).await; assert_eq!(final_gradient.stops.len(), 3, "Expected 3 stops after deletion, found {}", final_gradient.stops.len()); - let final_positions: Vec = final_gradient.stops.iter().map(|(pos, _)| *pos).collect(); + let final_positions: Vec = final_gradient.stops.iter().map(|stop| stop.position).collect(); // Verify final positions with helper function assert_stops_at_positions(&final_positions, &[0., 0.25, 1.], 0.05); diff --git a/frontend/src/components/floating-menus/ColorPicker.svelte b/frontend/src/components/floating-menus/ColorPicker.svelte index 217e2edd2e..91caa98b2b 100644 --- a/frontend/src/components/floating-menus/ColorPicker.svelte +++ b/frontend/src/components/floating-menus/ColorPicker.svelte @@ -13,7 +13,7 @@ import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte"; - import SpectrumInput from "@graphite/components/widgets/inputs/SpectrumInput.svelte"; + import SpectrumInput, { MAX_MIDPOINT, MIN_MIDPOINT } from "@graphite/components/widgets/inputs/SpectrumInput.svelte"; import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import Separator from "@graphite/components/widgets/labels/Separator.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; @@ -57,7 +57,8 @@ // Gradient color stops $: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined; let activeIndex = 0 as number | undefined; - $: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color); + let activeIndexIsMidpoint = false; + $: selectedGradientColor = (activeIndex !== undefined && gradient?.color[activeIndex]) || (Color.fromCSS("black") as Color); // Currently viewed color $: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor; // New color components @@ -191,8 +192,8 @@ alignedAxis = undefined; } else if (!shiftPressed && draggingPickerTrack) { shiftPressed = true; - saturationStartOfAxisAlign = saturation; - valueStartOfAxisAlign = value; + saturationStartOfAxisAlign = saturationBeforeDrag; + valueStartOfAxisAlign = valueBeforeDrag; } } @@ -286,8 +287,9 @@ function setColor(color?: Color) { const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha }); - const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex); - if (stop) stop.color = colorToEmit; + if (gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.position[activeIndex] !== undefined && colorOrGradient instanceof Gradient) { + colorOrGradient.color[activeIndex] = colorToEmit; + } dispatch("colorOrGradient", gradient || colorToEmit); } @@ -397,9 +399,11 @@ } } - function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent) { - activeIndex = index; - const color = index === undefined ? undefined : gradient?.colorAtIndex(index); + function gradientActiveMarkerIndexChange({ detail: { activeMarkerIndex, activeMarkerIsMidpoint } }: CustomEvent<{ activeMarkerIndex: number | undefined; activeMarkerIsMidpoint: boolean }>) { + activeIndex = activeMarkerIndex; + activeIndexIsMidpoint = activeMarkerIsMidpoint; + + const color = activeMarkerIndex === undefined ? undefined : gradient?.color[activeMarkerIndex]; const hsva = color?.toHSVA(); if (!color || !hsva) return; @@ -440,18 +444,25 @@ on:pointerdown={onPointerDown} data-saturation-value-picker > - {#if !isNone} -
- {/if} {#if alignedAxis}
+
{/if} + {#if !isNone} +
+ {/if} dispatch("colorOrGradient", gradient)} on:activeMarkerIndexChange={gradientActiveMarkerIndexChange} activeMarkerIndex={activeIndex} + activeMarkerIsMidpoint={activeIndexIsMidpoint} on:dragging={({ detail }) => (gradientSpectrumDragging = detail)} bind:this={gradientSpectrumInputWidget} /> {#if gradientSpectrumInputWidget && activeIndex !== undefined} { - if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100); + on:value={({ detail: position }) => { + if (gradientSpectrumInputWidget && activeIndex !== undefined && position !== undefined) { + gradientSpectrumInputWidget.setPosition(activeIndex, position / 100, activeIndexIsMidpoint); + } }} displayDecimalPlaces={0} - min={0} - max={100} + min={activeIndexIsMidpoint ? MIN_MIDPOINT * 100 : 0} + max={activeIndexIsMidpoint ? MAX_MIDPOINT * 100 : 100} unit="%" /> {/if} @@ -744,12 +758,12 @@ } .selection-circle { + pointer-events: none; position: absolute; left: 0; top: 0; width: 0; height: 0; - pointer-events: none; &::after { content: ""; @@ -761,56 +775,31 @@ height: calc(var(--picker-circle-radius) * 2 + 1px); border-radius: 50%; border: 2px solid var(--opaque-color-contrasting); + background: var(--opaque-color); box-sizing: border-box; } } - .selection-circle-alignment { - position: absolute; + .selection-circle-axis-snap-line { pointer-events: none; + position: absolute; + width: 1px; + height: 1px; + top: 0; + left: 0; + background: var(--opaque-color-contrasting); - &.saturation::before, - &.saturation::after, - &.value::before, - &.value::after { - content: ""; - position: absolute; - background: var(--opaque-color-contrasting); - width: 1px; - height: 1px; - } - - &.saturation { - &::before { - height: var(--picker-size); - margin-top: calc(-1 * var(--picker-size) - var(--picker-circle-radius)); - } - - &::after { - height: var(--picker-size); - margin-top: var(--picker-circle-radius); - } - } - - &.value { - &::before { - width: var(--picker-size); - margin-left: var(--picker-circle-radius); - } - - &::after { - width: var(--picker-size); - margin-left: calc(-1 * var(--picker-size) - var(--picker-circle-radius)); - } + + .selection-circle-axis-snap-line { + opacity: 0.25; } } .selection-needle { + pointer-events: none; position: absolute; top: 0; width: 100%; height: 0; - pointer-events: none; &::before { content: ""; @@ -881,13 +870,13 @@ &.outlined::after { content: ""; + pointer-events: none; position: absolute; top: 0; bottom: 0; left: 0; right: 0; box-shadow: inset 0 0 0 1px rgba(var(--color-0-black-rgb), var(--outline-amount)); - pointer-events: none; } &.transparency { diff --git a/frontend/src/components/widgets/inputs/ColorInput.svelte b/frontend/src/components/widgets/inputs/ColorInput.svelte index 51fcd37bfe..261ae5e3da 100644 --- a/frontend/src/components/widgets/inputs/ColorInput.svelte +++ b/frontend/src/components/widgets/inputs/ColorInput.svelte @@ -28,7 +28,7 @@ $: outlined = outlineFactor > 0.0001; $: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`; $: none = value instanceof Color ? value.none : false; - $: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1; + $: transparency = value instanceof Gradient ? value.color.some((color) => color.alpha < 1) : value.alpha < 1; diff --git a/frontend/src/components/widgets/inputs/SpectrumInput.svelte b/frontend/src/components/widgets/inputs/SpectrumInput.svelte index 26a3a38810..7d76da2c56 100644 --- a/frontend/src/components/widgets/inputs/SpectrumInput.svelte +++ b/frontend/src/components/widgets/inputs/SpectrumInput.svelte @@ -1,6 +1,12 @@ + +