Skip to content

Commit b9dbbd8

Browse files
committed
SVG2 Graident
1 parent 644e9cf commit b9dbbd8

10 files changed

Lines changed: 224 additions & 34 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ vello = "0.8"
158158
vello_encoding = "0.8"
159159
resvg = "0.47"
160160
usvg = "0.47"
161+
svgtypes = "0.16"
161162
parley = "0.6"
162163
skrifa = "0.40"
163164
polycool = "0.4"

editor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ tsify = { workspace = true }
4040
dyn-any = { workspace = true }
4141
num_enum = { workspace = true }
4242
usvg = { workspace = true }
43+
svgtypes = { workspace = true }
4344
once_cell = { workspace = true }
4445
web-sys = { workspace = true }
4546
vello = { workspace = true }

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

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::str::FromStr;
12
use super::transform_utils;
23
use super::utility_types::ModifyInputsContext;
34
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
@@ -15,7 +16,7 @@ use graphene_std::renderer::Quad;
1516
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
1617
use graphene_std::table::Table;
1718
use graphene_std::text::{Font, TypesettingConfig};
18-
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
19+
use graphene_std::vector::style::{Fill, Gradient, GradientSpreadMethod, GradientStop, GradientStops, GradientType, GradientUnits, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
1920

2021
#[derive(ExtractField)]
2122
pub struct GraphOperationMessageContext<'a> {
@@ -502,7 +503,6 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
502503
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
503504
let mut result = HashMap::new();
504505

505-
// Quick check: if the SVG doesn't reference `graphite:midpoint` at all, skip parsing
506506
if !svg.contains("graphite:midpoint") {
507507
return result;
508508
}
@@ -512,6 +512,8 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
512512
Err(_) => return result,
513513
};
514514

515+
let mut raw_stops: HashMap<String, Vec<GradientStop>> = HashMap::new();
516+
515517
for node in doc.descendants() {
516518
match node.tag_name().name() {
517519
"linearGradient" | "radialGradient" => {}
@@ -523,7 +525,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
523525
None => continue,
524526
};
525527

526-
let mut real_stops = Vec::new();
528+
let mut stops = Vec::new();
527529
let mut has_any_midpoint = false;
528530

529531
for child in node.children() {
@@ -532,35 +534,95 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
532534
}
533535

534536
let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());
535-
536537
if let Some(midpoint) = midpoint {
537538
has_any_midpoint = true;
538539

539-
let offset = child.attribute("offset").and_then(|v| v.parse::<f64>().ok()).unwrap_or(0.);
540+
let offset = parse_stop_offset(child.attribute("offset").unwrap_or("0"));
540541
let opacity = child.attribute("stop-opacity").and_then(|v| v.parse::<f32>().ok()).unwrap_or(1.);
541-
let color = child.attribute("stop-color").and_then(|hex| parse_hex_stop_color(hex, opacity)).unwrap_or(Color::BLACK);
542+
let color = child.attribute("stop-color").and_then(|c| parse_stop_color(c, opacity)).unwrap_or(Color::BLACK);
542543

543-
real_stops.push(GradientStop { position: offset, midpoint, color });
544+
stops.push(GradientStop {
545+
position: offset,
546+
midpoint,
547+
color,
548+
});
544549
}
545550
}
546551

547-
if has_any_midpoint && !real_stops.is_empty() {
548-
result.insert(gradient_id, GradientStops::new(real_stops));
552+
if has_any_midpoint && !stops.is_empty() {
553+
raw_stops.insert(gradient_id, stops);
549554
}
550555
}
551556

557+
for node in doc.descendants() {
558+
match node.tag_name().name() {
559+
"linearGradient" | "radialGradient" => {}
560+
_ => continue,
561+
}
562+
563+
let gradient_id = match node.attribute("id") {
564+
Some(id) => id.to_string(),
565+
None => continue,
566+
};
567+
568+
if raw_stops.contains_key(&gradient_id) {
569+
continue;
570+
}
571+
572+
let href = node.attribute("href").or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")));
573+
if let Some(referenced_id) = href.and_then(|h| h.strip_prefix('#')) {
574+
if let Some(inherited) = raw_stops.get(referenced_id) {
575+
raw_stops.insert(gradient_id, inherited.clone());
576+
}
577+
}
578+
}
579+
580+
for (id, stops) in raw_stops {
581+
result.insert(id, GradientStops::new(stops));
582+
}
583+
552584
result
553585
}
554586

555-
fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
556-
let hex = hex.strip_prefix('#')?;
557-
if hex.len() != 6 {
558-
return None;
587+
fn parse_stop_offset(s: &str) -> f64 {
588+
if let Some(pct) = s.strip_suffix('%') {
589+
pct.trim().parse::<f64>().unwrap_or(0.) / 100.
590+
} else {
591+
s.trim().parse::<f64>().unwrap_or(0.)
559592
}
560-
let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f32 / 255.;
561-
let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f32 / 255.;
562-
let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f32 / 255.;
563-
Some(Color::from_rgbaf32_unchecked(r, g, b, opacity))
593+
}
594+
595+
fn parse_stop_color(value: &str, opacity: f32) -> Option<Color> {
596+
let value = value.trim();
597+
if let Some(hex) = value.strip_prefix('#') {
598+
return parse_hex_color(hex, opacity);
599+
}
600+
named_css_color(value).map(|(r, g, b, alpha)| Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., alpha.unwrap_or(opacity)))
601+
}
602+
603+
fn parse_hex_color(hex: &str, opacity: f32) -> Option<Color> {
604+
let (r, g, b) = match hex.len() {
605+
6 => (
606+
u8::from_str_radix(&hex[0..2], 16).ok()?,
607+
u8::from_str_radix(&hex[2..4], 16).ok()?,
608+
u8::from_str_radix(&hex[4..6], 16).ok()?,
609+
),
610+
3 => {
611+
let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
612+
let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
613+
let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
614+
(r * 17, g * 17, b * 17)
615+
}
616+
_ => return None,
617+
};
618+
Some(Color::from_rgbaf32_unchecked(r as f32 / 255., g as f32 / 255., b as f32 / 255., opacity))
619+
}
620+
621+
fn named_css_color(name: &str) -> Option<(u8, u8, u8, Option<f32>)> {
622+
if name.to_ascii_lowercase() == "transparent" {
623+
return Some((0, 0, 0, Some(0.)));
624+
}
625+
svgtypes::Color::from_str(name).ok().map(|c| (c.red, c.green, c.blue, None))
564626
}
565627

566628
/// Import a usvg node as the root of an SVG import operation.
@@ -826,17 +888,27 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
826888
end,
827889
gradient_type,
828890
stops,
891+
focal_center: start,
892+
focal_radius: 0.,
893+
gradient_units: GradientUnits::UserSpaceOnUse,
829894
spread_method,
830895
})
831896
}
832897
usvg::Paint::RadialGradient(radial) => {
833898
let gradient_transform = usvg_transform(radial.transform());
899+
let inv_bounds = bounds_transform.inverse();
900+
834901
let center = DVec2::new(radial.cx() as f64, radial.cy() as f64);
835902
let edge = center + DVec2::X * radial.r().get() as f64;
836-
let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge));
837-
let (start, end) = (bounds_transform.inverse().transform_point2(start), bounds_transform.inverse().transform_point2(end));
903+
let start = inv_bounds.transform_point2(gradient_transform.transform_point2(center));
904+
let end = inv_bounds.transform_point2(gradient_transform.transform_point2(edge));
838905

839-
let gradient_type = GradientType::Radial;
906+
let focal = DVec2::new(radial.fx() as f64, radial.fy() as f64);
907+
let focal_center = inv_bounds.transform_point2(gradient_transform.transform_point2(focal));
908+
909+
let focal_edge = focal + DVec2::X * radial.fr().get() as f64;
910+
let focal_edge_transformed = inv_bounds.transform_point2(gradient_transform.transform_point2(focal_edge));
911+
let focal_radius = focal_center.distance(focal_edge_transformed);
840912

841913
let stops = match graphite_gradient_stops.get(radial.id()) {
842914
Some(graphite_stops) => graphite_stops.clone(),
@@ -854,8 +926,11 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, b
854926
Fill::Gradient(Gradient {
855927
start,
856928
end,
857-
gradient_type,
929+
gradient_type: GradientType::Radial,
858930
stops,
931+
focal_center,
932+
focal_radius,
933+
gradient_units: GradientUnits::UserSpaceOnUse,
859934
spread_method,
860935
})
861936
}

editor/src/messages/portfolio/document/node_graph/node_properties.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use graphene_std::text_nodes::StringCapitalization;
3131
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
3232
use graphene_std::vector::misc::BooleanOperation;
3333
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
34-
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
34+
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientSpreadMethod, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin, GradientUnits};
3535
use graphene_std::vector::{QRCodeErrorCorrectionLevel, VectorModification};
3636

3737
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
@@ -2663,6 +2663,55 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte
26632663
]);
26642664

26652665
widgets.push(LayoutGroup::row(spread_methods_row));
2666+
2667+
// Gradient Units
2668+
let mut gradient_units_row = vec![TextLabel::new("").widget_instance()];
2669+
add_blank_assist(&mut gradient_units_row);
2670+
2671+
let gradient_units_entries = [GradientUnits::UserSpaceOnUse, GradientUnits::ObjectBoundingBox]
2672+
.iter()
2673+
.map(|&gradient_units| {
2674+
let gradient_for_input = gradient_for_closure.clone();
2675+
let gradient_for_backup = gradient_for_closure.clone();
2676+
2677+
let set_input_value = update_value(
2678+
move |_: &()| {
2679+
let mut new_gradient = gradient_for_input.clone();
2680+
new_gradient.gradient_units = gradient_units;
2681+
TaggedValue::Fill(Fill::Gradient(new_gradient))
2682+
},
2683+
node_id,
2684+
FillInput::<Color>::INDEX,
2685+
);
2686+
2687+
let set_backup_value = update_value(
2688+
move |_: &()| {
2689+
let mut new_gradient = gradient_for_backup.clone();
2690+
new_gradient.gradient_units = gradient_units;
2691+
TaggedValue::Gradient(new_gradient)
2692+
},
2693+
node_id,
2694+
BackupGradientInput::INDEX,
2695+
);
2696+
2697+
RadioEntryData::new(format!("{:?}", gradient_units))
2698+
.label(format!("{:?}", gradient_units))
2699+
.on_update(move |_| Message::Batched {
2700+
messages: Box::new([
2701+
set_input_value(&()),
2702+
set_backup_value(&()),
2703+
]),
2704+
})
2705+
.on_commit(commit_value)
2706+
})
2707+
.collect();
2708+
2709+
gradient_units_row.extend_from_slice(&[
2710+
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
2711+
RadioInput::new(gradient_units_entries).selected_index(Some(gradient.gradient_units as u32)).widget_instance(),
2712+
]);
2713+
2714+
widgets.push(LayoutGroup::row(gradient_units_row));
26662715
}
26672716

26682717
widgets

editor/src/messages/tool/tool_messages/gradient_tool.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
329329
spread_method,
330330
start: transform.transform_point2(DVec2::ZERO),
331331
end: transform.transform_point2(DVec2::X),
332+
focal_center: transform.transform_point2(DVec2::ZERO),
333+
focal_radius: 0.,
334+
gradient_units: Default::default(),
332335
});
333336
}
334337
graph_modification_utils::get_gradient(layer, network_interface)

node-graph/libraries/rendering/src/render_ext.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use crate::renderer::{RenderParams, format_transform_matrix};
22
use core_types::uuid::generate_uuid;
3-
use glam::DAffine2;
3+
use glam::{DAffine2, DVec2};
44
use graphic_types::vector_types::gradient::{Gradient, GradientType};
55
use graphic_types::vector_types::vector::style::{Fill, PaintOrder, PathStyle, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
66
use std::fmt::Write;
7-
use vector_types::gradient::GradientSpreadMethod;
7+
use vector_types::gradient::{GradientSpreadMethod, GradientUnits};
88

99
pub trait RenderExt {
1010
type Output;
@@ -54,21 +54,39 @@ impl RenderExt for Gradient {
5454
format!(r#" spreadMethod="{}""#, self.spread_method.svg_name())
5555
};
5656

57+
let gradient_units = if self.gradient_units == GradientUnits::UserSpaceOnUse {
58+
String::new()
59+
} else {
60+
format!(r#" gradientUnits="{}""#, self.gradient_units.svg_name())
61+
};
62+
5763
let gradient_id = generate_uuid();
5864

5965
match self.gradient_type {
6066
GradientType::Linear => {
6167
let _ = write!(
6268
svg_defs,
63-
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{spread_method}{gradient_transform}>{}</linearGradient>"#,
69+
r#"<linearGradient id="{}" x1="{}" y1="{}" x2="{}" y2="{}"{gradient_units}{spread_method}{gradient_transform}>{}</linearGradient>"#,
6470
gradient_id, start.x, start.y, end.x, end.y, stop
6571
);
6672
}
6773
GradientType::Radial => {
68-
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
74+
let radius = start.distance(end);
75+
let focal = transform_points.transform_point2(self.focal_center);
76+
let focal_edge = transform_points.transform_point2(self.focal_center + DVec2::X * self.focal_radius);
77+
let focal_radius = focal.distance(focal_edge);
78+
79+
let mut focal_attrs = String::new();
80+
if (focal.x - start.x).abs() > 1e-9 || (focal.y - start.y).abs() > 1e-9 {
81+
let _ = write!(focal_attrs, r#" fx="{}" fy="{}""#, focal.x, focal.y);
82+
}
83+
if focal_radius > 1e-9 {
84+
let _ = write!(focal_attrs, r#" fr="{}""#, focal_radius);
85+
}
86+
6987
let _ = write!(
7088
svg_defs,
71-
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{spread_method}{gradient_transform}>{}</radialGradient>"#,
89+
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{focal_attrs}{gradient_units}{spread_method}{gradient_transform}>{}</radialGradient>"#,
7290
gradient_id, start.x, start.y, radius, stop
7391
);
7492
}

node-graph/libraries/rendering/src/renderer.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,9 +1202,10 @@ impl Render for Table<Vector> {
12021202
.into(),
12031203
GradientType::Radial => {
12041204
let radius = start.distance(end);
1205+
let focal = mod_points.transform_point2(gradient.focal_center);
12051206
peniko::RadialGradientPosition {
1206-
start_center: to_point(start),
1207-
start_radius: 0.,
1207+
start_center: to_point(focal),
1208+
start_radius: gradient.focal_radius as f32,
12081209
end_center: to_point(start),
12091210
end_radius: radius as f32,
12101211
}

0 commit comments

Comments
 (0)