Skip to content

Commit 691d965

Browse files
authored
Add support for gradients with midpoints and add draggable diamonds to the color picker dialog (#3813)
* Refactor GradientStops to use struct-of-arrays and include midpoint * Implement interaction and rendering * Make color picker saturation-value color picking snap to original position and show both axis lines Make color picker saturation-value color picking snap to original position and show both axis lines * Add graphite:midpoint attribute to SVG exports * Add graphite:midpoint parsing to SVG importer
1 parent a1c1039 commit 691d965

File tree

21 files changed

+836
-316
lines changed

21 files changed

+836
-316
lines changed

.branding

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
https://github.com/Keavon/graphite-branded-assets/archive/f44aa2f362ae4fed8d634878b817a1d3948a7dcb.tar.gz
2-
dffe2b483e491979ef57c320d61446ada5400ef73ff26582976631d9c36efefc
1+
https://github.com/Keavon/graphite-branded-assets/archive/8ae15dc9c51a3855475d8cab1d0f29d9d9bc622c.tar.gz
2+
c19abe4ac848f3c835e43dc065c59e20e60233ae023ea0a064c5fed442be2d3d

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"a11y-click-events-have-key-events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
5656
"a11y_consider_explicit_label": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
5757
"a11y_click_events_have_key_events": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
58-
"a11y_no_noninteractive_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
58+
"a11y_no_noninteractive_element_interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
59+
"a11y_no_static_element_interactions": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts`
5960
},
6061
// Git Graph config
6162
"git-graph.repository.fetchAndPrune": true,

editor/src/messages/layout/layout_message_handler.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
22
use crate::messages::layout::utility_types::widget_prelude::*;
33
use crate::messages::prelude::*;
44
use graphene_std::raster::color::Color;
5-
use graphene_std::vector::style::{FillChoice, GradientStops};
5+
use graphene_std::vector::style::{FillChoice, GradientStop, GradientStops};
66
use serde_json::Value;
77
use std::collections::HashMap;
88

@@ -193,18 +193,17 @@ impl LayoutMessageHandler {
193193
}
194194

195195
// Gradient
196-
let gradient = update_value.get("stops").and_then(|x| x.as_array());
197-
if let Some(stops) = gradient {
198-
let gradient_stops = stops
199-
.iter()
200-
.filter_map(|stop| {
201-
stop.as_object().and_then(|stop| {
202-
let position = stop.get("position").and_then(|x| x.as_f64());
203-
let color = stop.get("color").and_then(|x| x.as_object()).and_then(decode_color);
204-
if let (Some(position), Some(color)) = (position, color) { Some((position, color)) } else { None }
205-
})
206-
})
207-
.collect::<Vec<_>>();
196+
let positions = update_value.get("position").and_then(|x| x.as_array());
197+
let midpoints = update_value.get("midpoint").and_then(|x| x.as_array());
198+
let colors = update_value.get("color").and_then(|x| x.as_array());
199+
200+
if let (Some(positions), Some(midpoints), Some(colors)) = (positions, midpoints, colors) {
201+
let gradient_stops = positions.iter().zip(midpoints.iter()).zip(colors.iter()).filter_map(|((pos, mid), col)| {
202+
let position = pos.as_f64()?;
203+
let midpoint = mid.as_f64()?;
204+
let color = col.as_object().and_then(decode_color)?;
205+
Some(GradientStop { position, midpoint, color })
206+
});
208207

209208
color_button.value = FillChoice::Gradient(GradientStops::new(gradient_stops));
210209
return (color_button.on_update.callback)(color_button);

editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ impl TableRowLayout for GradientStops {
564564
"Gradient"
565565
}
566566
fn identifier(&self) -> String {
567-
format!("Gradient ({} stops)", self.0.len())
567+
format!("Gradient ({} stops)", self.len())
568568
}
569569
fn element_widget(&self, _index: usize) -> WidgetInstance {
570570
ColorInput::new(FillChoice::Gradient(self.clone()))

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use graphene_std::renderer::Quad;
1414
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
1515
use graphene_std::table::Table;
1616
use graphene_std::text::{Font, TypesettingConfig};
17-
use graphene_std::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
17+
use graphene_std::vector::style::{Fill, Gradient, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
1818

1919
#[derive(ExtractField)]
2020
pub struct GraphOperationMessageContext<'a> {
@@ -337,7 +337,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
337337
let offset_to_center = DVec2::new(size.width() as f64, size.height() as f64) / -2.;
338338
let transform = transform * DAffine2::from_translation(offset_to_center);
339339

340-
import_usvg_node(&mut modify_inputs, &usvg::Node::Group(Box::new(tree.root().clone())), transform, id, parent, insert_index);
340+
let graphite_gradient_stops = extract_graphite_gradient_stops(&svg);
341+
342+
import_usvg_node(
343+
&mut modify_inputs,
344+
&usvg::Node::Group(Box::new(tree.root().clone())),
345+
transform,
346+
id,
347+
parent,
348+
insert_index,
349+
&graphite_gradient_stops,
350+
);
341351
}
342352
}
343353
}
@@ -362,7 +372,85 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
362372
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])
363373
}
364374

365-
fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, transform: DAffine2, id: NodeId, parent: LayerNodeIdentifier, insert_index: usize) {
375+
const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
376+
377+
/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
378+
/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
379+
/// alongside the real stops. Real stops are tagged with `graphite:midpoint` attributes.
380+
/// Returns a map from gradient element `id` to `GradientStops` containing only the real stops.
381+
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
382+
let mut result = HashMap::new();
383+
384+
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
385+
if !svg.contains("graphite:midpoint") {
386+
return result;
387+
}
388+
389+
let doc = match usvg::roxmltree::Document::parse(svg) {
390+
Ok(doc) => doc,
391+
Err(_) => return result,
392+
};
393+
394+
for node in doc.descendants() {
395+
match node.tag_name().name() {
396+
"linearGradient" | "radialGradient" => {}
397+
_ => continue,
398+
}
399+
400+
let gradient_id = match node.attribute("id") {
401+
Some(id) => id.to_string(),
402+
None => continue,
403+
};
404+
405+
let mut real_stops = Vec::new();
406+
let mut has_any_midpoint = false;
407+
408+
for child in node.children() {
409+
if child.tag_name().name() != "stop" {
410+
continue;
411+
}
412+
413+
let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());
414+
415+
if let Some(midpoint) = midpoint {
416+
has_any_midpoint = true;
417+
418+
let offset = child.attribute("offset").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.);
419+
let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::<f32>().ok()).unwrap_or(1.);
420+
let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK);
421+
422+
real_stops.push(GradientStop { position: offset, midpoint, color });
423+
}
424+
}
425+
426+
if has_any_midpoint && !real_stops.is_empty() {
427+
result.insert(gradient_id, GradientStops::new(real_stops));
428+
}
429+
}
430+
431+
result
432+
}
433+
434+
fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
435+
let hex = hex.strip_prefix('#')?;
436+
if hex.len() != 6 {
437+
return None;
438+
}
439+
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
440+
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
441+
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
442+
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
443+
}
444+
445+
fn import_usvg_node(
446+
modify_inputs: &mut ModifyInputsContext,
447+
node: &usvg::Node,
448+
transform: DAffine2,
449+
id: NodeId,
450+
parent: LayerNodeIdentifier,
451+
insert_index: usize,
452+
graphite_gradient_stops: &HashMap<String, GradientStops>,
453+
) {
366454
let layer = modify_inputs.create_layer(id);
367455
modify_inputs.network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
368456
modify_inputs.layer_node = Some(layer);
@@ -372,7 +460,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
372460
match node {
373461
usvg::Node::Group(group) => {
374462
for child in group.children() {
375-
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0);
463+
import_usvg_node(modify_inputs, child, transform, NodeId::new(), layer, 0, graphite_gradient_stops);
376464
}
377465
modify_inputs.layer_node = Some(layer);
378466
}
@@ -388,7 +476,7 @@ fn import_usvg_node(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
388476

389477
if let Some(fill) = path.fill() {
390478
let bounds_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
391-
apply_usvg_fill(fill, modify_inputs, bounds_transform);
479+
apply_usvg_fill(fill, modify_inputs, bounds_transform, graphite_gradient_stops);
392480
}
393481
if let Some(stroke) = path.stroke() {
394482
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
432520
}
433521
}
434522

435-
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2) {
523+
fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, bounds_transform: DAffine2, graphite_gradient_stops: &HashMap<String, GradientStops>) {
436524
modify_inputs.fill_set(match &fill.paint() {
437525
usvg::Paint::Color(color) => Fill::solid(usvg_color(*color, fill.opacity().get())),
438526
usvg::Paint::LinearGradient(linear) => {
@@ -443,8 +531,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
443531

444532
let gradient_type = GradientType::Linear;
445533

446-
let stops = linear.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
447-
let stops = GradientStops::new(stops);
534+
let stops = match graphite_gradient_stops.get(linear.id()) {
535+
Some(graphite_stops) => graphite_stops.clone(),
536+
None => {
537+
let stops = linear.stops().iter().map(|stop| GradientStop {
538+
position: stop.offset().get() as f64,
539+
midpoint: 0.5,
540+
color: usvg_color(stop.color(), stop.opacity().get()),
541+
});
542+
GradientStops::new(stops)
543+
}
544+
};
448545

449546
Fill::Gradient(Gradient { start, end, gradient_type, stops })
450547
}
@@ -457,8 +554,17 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
457554

458555
let gradient_type = GradientType::Radial;
459556

460-
let stops = radial.stops().iter().map(|stop| (stop.offset().get() as f64, usvg_color(stop.color(), stop.opacity().get()))).collect();
461-
let stops = GradientStops::new(stops);
557+
let stops = match graphite_gradient_stops.get(radial.id()) {
558+
Some(graphite_stops) => graphite_stops.clone(),
559+
None => {
560+
let stops = radial.stops().iter().map(|stop| GradientStop {
561+
position: stop.offset().get() as f64,
562+
midpoint: 0.5,
563+
color: usvg_color(stop.color(), stop.opacity().get()),
564+
});
565+
GradientStops::new(stops)
566+
}
567+
};
462568

463569
Fill::Gradient(Gradient { start, end, gradient_type, stops })
464570
}

0 commit comments

Comments
 (0)