Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ vello = "0.9"
vello_encoding = "0.9"
resvg = "0.47"
usvg = "0.47"
svgtypes = "0.16"
parley = { version = "0.9", default-features = false, features = ["std"] }
skrifa = "0.42"
polycool = "0.4"
Expand Down
1 change: 1 addition & 0 deletions editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ tsify = { workspace = true }
dyn-any = { workspace = true }
num_enum = { workspace = true }
usvg = { workspace = true }
svgtypes = { workspace = true }
once_cell = { workspace = true }
web-sys = { workspace = true }
vello = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::style::{GradientSpreadMethod, GradientStop, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{Artboard, Color};
use std::str::FromStr;

#[derive(ExtractField)]
pub struct GraphOperationMessageContext<'a> {
Expand Down Expand Up @@ -47,7 +48,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
transform,
} => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform, None, None);
}
}
GraphOperationMessage::BlendingFillSet { layer, fill } => {
Expand Down Expand Up @@ -512,7 +513,6 @@ const GRAPHITE_NAMESPACE: &str = "https://graphite.art";
fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops> {
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;
}
Expand All @@ -522,6 +522,8 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
Err(_) => return result,
};

let mut raw_stops: HashMap<String, Vec<GradientStop>> = HashMap::new();

for node in doc.descendants() {
match node.tag_name().name() {
"linearGradient" | "radialGradient" => {}
Expand All @@ -533,7 +535,7 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
None => continue,
};

let mut real_stops = Vec::new();
let mut stops = Vec::new();
let mut has_any_midpoint = false;

for child in node.children() {
Expand All @@ -542,35 +544,68 @@ fn extract_graphite_gradient_stops(svg: &str) -> HashMap<String, GradientStops>
}

let midpoint = child.attribute((GRAPHITE_NAMESPACE, "midpoint")).and_then(|v| v.parse::<f64>().ok());

if let Some(midpoint) = midpoint {
has_any_midpoint = true;

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

real_stops.push(GradientStop { position: offset, midpoint, color });
stops.push(GradientStop { position: offset, midpoint, color });
}
}

if has_any_midpoint && !real_stops.is_empty() {
result.insert(gradient_id, GradientStops::new(real_stops));
if has_any_midpoint && !stops.is_empty() {
raw_stops.insert(gradient_id, stops);
}
}

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,
};

if raw_stops.contains_key(&gradient_id) {
continue;
}

let href = node.attribute("href").or_else(|| node.attribute(("http://www.w3.org/1999/xlink", "href")));
if let Some(referenced_id) = href.and_then(|h| h.strip_prefix('#')) {
if let Some(inherited) = raw_stops.get(referenced_id) {
raw_stops.insert(gradient_id, inherited.clone());
}
}
}

for (id, stops) in raw_stops {
result.insert(id, GradientStops::new(stops));
}

result
}

fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
let hex = hex.strip_prefix('#')?;
if hex.len() != 6 {
return None;
fn parse_stop_offset(s: &str) -> f64 {
if let Some(pct) = s.strip_suffix('%') {
pct.trim().parse::<f64>().unwrap_or(0.) / 100.
} else {
s.trim().parse::<f64>().unwrap_or(0.)
}
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 parse_stop_color(value: &str, opacity: f32) -> Option<Color> {
let value = value.trim();
if value.to_ascii_lowercase() == "transparent" {
return Some(Color::from_rgbaf32_unchecked(0., 0., 0., 0.));
}
svgtypes::Color::from_str(value)
.ok()
.map(|c| Color::from_rgbaf32_unchecked(c.red as f32 / 255., c.green as f32 / 255., c.blue as f32 / 255., (c.alpha as f32 / 255.) * opacity))
}

/// Import a usvg node as the root of an SVG import operation.
Expand Down Expand Up @@ -831,18 +866,24 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
}
};
let spread_method = convert_spread_method(linear.spread_method());
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform, Some(DVec2::ZERO), Some(0.));
}
usvg::Paint::RadialGradient(radial) => {
let gradient_transform = usvg_transform(radial.transform());

let center = DVec2::new(radial.cx() as f64, radial.cy() as f64);
let edge = center + DVec2::X * radial.r().get() as f64;
let radius = radial.r().get() as f64;
let focal = DVec2::new(radial.fx() as f64, radial.fy() as f64);
let focal_radius_raw = radial.fr().get() as f64;

let focal_center = (focal - center) / radius;
let focal_radius = focal_radius_raw / radius;

let edge = center + DVec2::X * radius;
let (start, end) = (gradient_transform.transform_point2(center), gradient_transform.transform_point2(edge));
let direction = end - start;
let transform = DAffine2::from_cols(direction, direction.perp(), start);

let gradient_type = GradientType::Radial;

let gradient = match graphite_gradient_stops.get(radial.id()) {
Some(graphite_stops) => graphite_stops.clone(),
None => {
Expand All @@ -856,7 +897,7 @@ fn apply_usvg_fill(fill: &usvg::Fill, modify_inputs: &mut ModifyInputsContext, g
};
let spread_method = convert_spread_method(radial.spread_method());

modify_inputs.fill_gradient_set(gradient, gradient_type, spread_method, transform);
modify_inputs.fill_gradient_set(gradient, GradientType::Radial, spread_method, transform, Some(focal_center), Some(focal_radius));
}
usvg::Paint::Pattern(_) => warn!("SVG patterns are not currently supported"),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,15 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Color(color), false), false);
}

pub fn fill_gradient_set(&mut self, gradient: GradientStops, gradient_type: GradientType, spread_method: GradientSpreadMethod, transform: DAffine2) {
pub fn fill_gradient_set(
&mut self,
gradient: GradientStops,
gradient_type: GradientType,
spread_method: GradientSpreadMethod,
transform: DAffine2,
focal_center: Option<DVec2>,
focal_radius: Option<f64>,
) {
let Some(fill_node_id) = self.existing_proto_node_id(graphene_std::vector_nodes::fill::IDENTIFIER, true) else {
return;
};
Expand Down Expand Up @@ -501,6 +509,22 @@ impl<'a> ModifyInputsContext<'a> {
true,
);

if let Some(focal_center) = focal_center {
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalCenterInput::INDEX),
NodeInput::value(TaggedValue::DVec2(focal_center), false),
true,
);
}

if let Some(focal_radius) = focal_radius {
self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::FocalRadiusInput::INDEX),
NodeInput::value(TaggedValue::F64(focal_radius), false),
true,
);
}

self.set_input_with_refresh(
InputConnector::node(fill_node_id, graphene_std::vector::fill::SpreadMethodInput::INDEX),
NodeInput::value(TaggedValue::GradientSpreadMethod(spread_method), false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,8 @@ pub struct FillNodeGradient {
pub transform: DAffine2,
/// Whether the transform input holds a plain value (so it may be written to) rather than a wire.
pub transform_is_value: bool,
pub focal_center: DVec2,
pub focal_radius: f64,
}

/// Decode a Fill node's gradient metadata inputs, resolving an unset transform to the default over `bounding_box`. Returns `None` when the fill input isn't a gradient value.
Expand All @@ -654,13 +656,23 @@ pub fn read_fill_node_gradient(fill_node: &DocumentNode, bounding_box: impl FnOn
Some(&TaggedValue::OptionalDAffine2(value)) => value.unwrap_or_else(|| initial_gradient_transform_for_bounding_box(bounding_box())),
_ => DAffine2::IDENTITY,
};
let focal_center = match fill_node.inputs.get(fill::FocalCenterInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::DVec2(value)) => value,
_ => DVec2::ZERO,
};
let focal_radius = match fill_node.inputs.get(fill::FocalRadiusInput::INDEX).and_then(|input| input.as_value()) {
Some(&TaggedValue::F64(value)) => value,
_ => 0.,
};

Some(FillNodeGradient {
stops: stops.clone(),
gradient_type,
spread_method,
transform,
transform_is_value: transform_input.is_some(),
focal_center,
focal_radius,
})
}
/// Returns the stroke color from a layer's upstream Stroke node.
Expand Down
6 changes: 6 additions & 0 deletions editor/src/messages/tool/tool_messages/gradient_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@
gradient_type: gradient.gradient_type,
spread_method: gradient.spread_method,
transform: gradient.transform,
focal_center: gradient.focal_center,
focal_radius: gradient.focal_radius,
},
GradientSource::Direct,
));
Expand All @@ -420,6 +422,8 @@
transform: DAffine2,
gradient_type: GradientType,
spread_method: GradientSpreadMethod,
focal_center: DVec2,

Check failure on line 425 in editor/src/messages/tool/tool_messages/gradient_tool.rs

View workflow job for this annotation

GitHub Actions / test

fields `focal_center` and `focal_radius` are never read

Check warning on line 425 in editor/src/messages/tool/tool_messages/gradient_tool.rs

View workflow job for this annotation

GitHub Actions / build / web

fields `focal_center` and `focal_radius` are never read
focal_radius: f64,
}

/// Resolve the gradient transform, type, and spread method by walking the chain feeding the layer. Transform composes all
Expand Down Expand Up @@ -468,6 +472,7 @@
transform: composed_transform,
gradient_type: gradient_type.unwrap_or_default(),
spread_method: spread_method.unwrap_or_default(),
..Default::default()
}
}

Expand Down Expand Up @@ -1512,6 +1517,7 @@
transform: DAffine2::IDENTITY,
gradient_type: tool_options.gradient_type,
spread_method: tool_options.spread_method,
..Default::default()
},
GradientSource::Direct,
),
Expand Down
4 changes: 2 additions & 2 deletions node-graph/libraries/core-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ pub use graphene_hash;
pub use graphene_hash::CacheHash;
pub use list::{
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_END,
ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL,
ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,
ATTR_NAME, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TEXT_ALIGN, ATTR_TRANSFORM, ATTR_TYPE,
};
pub use memo::MemoHash;
pub use no_std_types::AsU32;
Expand Down
4 changes: 4 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ pub const ATTR_CLIP: &str = "clip";
pub const ATTR_SPREAD_METHOD: &str = "spread_method";
/// Gradient's `GradientType` (`Linear` or `Radial`).
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
/// Radial gradient's `DVec2` focal center relative to the start/center.
pub const ATTR_FOCAL_CENTER: &str = "focal_center";
/// Radial gradient's `f64` focal radius relative to the radius.
pub const ATTR_FOCAL_RADIUS: &str = "focal_radius";
/// Vector graphics object's filled area paint, of type List<T> where T is any graphic type.
pub const ATTR_FILL: &str = "fill";
/// Vector graphics object's stroke paint, of type List<T> where T is any graphic type.
Expand Down
15 changes: 13 additions & 2 deletions node-graph/libraries/rendering/src/render_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{Render, RenderSvgSegmentList, SvgRender};
use core_types::color::SRGBA8;
use core_types::list::List;
use core_types::uuid::generate_uuid;
use core_types::{ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use core_types::{ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, ATTR_GRADIENT_TYPE, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, Color};
use glam::{DAffine2, DVec2};
use graphic_types::Graphic;
use graphic_types::vector_types::gradient::GradientType;
Expand Down Expand Up @@ -146,9 +146,20 @@ impl RenderExt for List<GradientStops> {
);
}
GradientType::Radial => {
let focal_center: DVec2 = self.attribute_cloned_or_default(ATTR_FOCAL_CENTER, 0);
let focal_radius: f64 = self.attribute_cloned_or_default(ATTR_FOCAL_RADIUS, 0);

let mut focal_attrs = String::new();
if focal_center.x.abs() > 1e-9 || focal_center.y.abs() > 1e-9 {
let _ = write!(focal_attrs, r#" fx="{}" fy="{}""#, focal_center.x, focal_center.y);
}
if focal_radius > 1e-9 {
let _ = write!(focal_attrs, r#" fr="{}""#, focal_radius);
}

let _ = write!(
svg_defs,
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method}{gradient_transform}>{}</radialGradient>"#,
r#"<radialGradient id="{}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{focal_attrs}{spread_method}{gradient_transform}>{}</radialGradient>"#,
gradient_id, stop
);
}
Expand Down
22 changes: 13 additions & 9 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use core_types::render_complexity::RenderComplexity;
use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid};
use core_types::{
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME, ATTR_FONT,
ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD,
ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_EDITOR_TEXT_FRAME,
ATTR_FOCAL_CENTER, ATTR_FOCAL_RADIUS, ATTR_FONT, ATTR_FONT_SIZE, ATTR_GRADIENT_TYPE, ATTR_LETTER_SPACING, ATTR_LETTER_TILT, ATTR_LINE_HEIGHT, ATTR_LOCATION, ATTR_MAX_HEIGHT, ATTR_MAX_WIDTH,
ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TEXT_ALIGN, ATTR_TRANSFORM,
};
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
Expand Down Expand Up @@ -419,13 +419,17 @@ fn create_peniko_gradient_brush(gradient_list: &List<GradientStops>, multiplied_
end: to_point(end),
}
.into(),
GradientType::Radial => peniko::RadialGradientPosition {
start_center: to_point(start),
start_radius: 0.,
end_center: to_point(start),
end_radius: start.distance(end) as f32,
GradientType::Radial => {
let focal_center: DVec2 = gradient_list.attribute_cloned_or_default(ATTR_FOCAL_CENTER, 0);
let focal_radius: f64 = gradient_list.attribute_cloned_or_default(ATTR_FOCAL_RADIUS, 0);
peniko::RadialGradientPosition {
start_center: to_point(focal_center),
start_radius: focal_radius as f32,
end_center: to_point(start),
end_radius: start.distance(end) as f32,
}
.into()
}
.into(),
},
extend: match spread_method {
GradientSpreadMethod::Pad => peniko::Extend::Pad,
Expand Down
Loading
Loading