mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Migrate demo artwork and fix all failing CI tests (#1459)
* Initial work on fixing tests * Fix formatting * Remove dead code to satisfy rustc warnings * Insert into an artboard * Load updated artwork in editor * Remove popup when importing image * Fix up demo art * Change transform app[lication method * Reduce number of enums called BlendMode * Finalize the demo artwork upgrade * Code review pass --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
719c96ecd8
commit
8a1cf3ad5d
@@ -13,9 +13,21 @@ use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
pub mod renderer;
|
||||
|
||||
/// A list of [`GraphicElement`]s
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny, Default)]
|
||||
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct GraphicGroup(Vec<GraphicElement>);
|
||||
pub struct GraphicGroup {
|
||||
elements: Vec<GraphicElement>,
|
||||
pub opacity: f32,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for GraphicGroup {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.elements.hash(state);
|
||||
self.opacity.to_bits().hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|element| element.to_bits().hash(state))
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal data for a [`GraphicElement`]. Can be [`VectorData`], [`ImageFrame`], text, or a nested [`GraphicGroup`]
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
@@ -166,12 +178,12 @@ impl From<Artboard> for GraphicElementData {
|
||||
impl Deref for GraphicGroup {
|
||||
type Target = Vec<GraphicElement>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
&self.elements
|
||||
}
|
||||
}
|
||||
impl DerefMut for GraphicGroup {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
&mut self.elements
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,12 +205,20 @@ where
|
||||
graphic_element_data: value.into(),
|
||||
..Default::default()
|
||||
};
|
||||
Self(vec![element])
|
||||
Self {
|
||||
elements: (vec![element]),
|
||||
opacity: 1.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphicGroup {
|
||||
pub const EMPTY: Self = Self(Vec::new());
|
||||
pub const EMPTY: Self = Self {
|
||||
elements: Vec::new(),
|
||||
opacity: 1.,
|
||||
transform: DAffine2::IDENTITY,
|
||||
};
|
||||
|
||||
pub fn to_usvg_tree(&self, resolution: UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
|
||||
let root_node = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
|
||||
@@ -211,7 +231,7 @@ impl GraphicGroup {
|
||||
root: root_node.clone(),
|
||||
};
|
||||
|
||||
for element in self.0.iter() {
|
||||
for element in self.iter() {
|
||||
root_node.append(element.to_usvg_node());
|
||||
}
|
||||
tree
|
||||
@@ -293,7 +313,7 @@ impl GraphicElement {
|
||||
GraphicElementData::GraphicGroup(group) => {
|
||||
let group_element = usvg::Node::new(usvg::NodeKind::Group(usvg::Group::default()));
|
||||
|
||||
for element in group.0.iter() {
|
||||
for element in group.iter() {
|
||||
group_element.append(element.to_usvg_node());
|
||||
}
|
||||
group_element
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::raster::{Image, ImageFrame};
|
||||
use crate::raster::{BlendMode, Image, ImageFrame};
|
||||
use crate::uuid::{generate_uuid, ManipulatorGroupId};
|
||||
use crate::{vector::VectorData, Artboard, Color, GraphicElementData, GraphicGroup};
|
||||
use base64::Engine;
|
||||
@@ -58,6 +58,8 @@ pub struct SvgRender {
|
||||
pub svg: SvgSegmentList,
|
||||
pub svg_defs: String,
|
||||
pub transform: DAffine2,
|
||||
pub opacity: f32,
|
||||
pub blend_mode: BlendMode,
|
||||
pub image_data: Vec<(u64, Image<Color>)>,
|
||||
indent: usize,
|
||||
}
|
||||
@@ -68,6 +70,8 @@ impl SvgRender {
|
||||
svg: SvgSegmentList::default(),
|
||||
svg_defs: String::new(),
|
||||
transform: DAffine2::IDENTITY,
|
||||
opacity: 1.,
|
||||
blend_mode: BlendMode::Normal,
|
||||
image_data: Vec::new(),
|
||||
indent: 0,
|
||||
}
|
||||
@@ -187,29 +191,52 @@ pub trait GraphicElementRendered {
|
||||
|
||||
impl GraphicElementRendered for GraphicGroup {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
self.iter().for_each(|element| element.graphic_element_data.render_svg(render, render_params))
|
||||
let old_opacity = render.opacity;
|
||||
render.opacity *= self.opacity;
|
||||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| attributes.push("transform", format_transform_matrix(self.transform)),
|
||||
|render| {
|
||||
for element in self.iter() {
|
||||
render.blend_mode = element.blend_mode;
|
||||
element.graphic_element_data.render_svg(render, render_params);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
render.opacity = old_opacity;
|
||||
}
|
||||
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.iter().filter_map(|element| element.graphic_element_data.bounding_box(transform)).reduce(Quad::combine_bounds)
|
||||
self.iter()
|
||||
.filter_map(|element| element.graphic_element_data.bounding_box(transform * self.transform))
|
||||
.reduce(Quad::combine_bounds)
|
||||
}
|
||||
fn add_click_targets(&self, _click_targets: &mut Vec<ClickTarget>) {}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for VectorData {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let multiplied_transform = render.transform * self.transform;
|
||||
let layer_bounds = self.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(render.transform).unwrap_or_default();
|
||||
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
|
||||
|
||||
let mut path = String::new();
|
||||
for subpath in &self.subpaths {
|
||||
let _ = subpath.subpath_to_svg(&mut path, self.transform * render.transform);
|
||||
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
|
||||
}
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("class", "vector-data");
|
||||
attributes.push("d", path);
|
||||
let render = &mut attributes.0;
|
||||
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, render.transform, layer_bounds, transformed_bounds);
|
||||
let style = self.style.render(render_params.view_mode, &mut render.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
|
||||
attributes.push_val(style);
|
||||
if attributes.0.blend_mode != BlendMode::default() {
|
||||
attributes.push_complex("style", |v| {
|
||||
v.svg.push("mix-blend-mode: ");
|
||||
v.svg.push(v.blend_mode.to_svg_style_name());
|
||||
v.svg.push(";");
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use super::curve::{Curve, CurveManipulatorGroup, ValueMapperNode};
|
||||
use super::{Channel, Color, Node, RGBMut};
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use super::ImageFrame;
|
||||
use super::{Channel, Color, Node, RGBMut};
|
||||
use crate::vector::VectorData;
|
||||
use crate::GraphicGroup;
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
|
||||
@@ -171,6 +172,58 @@ impl core::fmt::Display for BlendMode {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl BlendMode {
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> &'static str {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => "normal",
|
||||
// Darken group
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
// Lighten group
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
// Contrast group
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
// Inversion group
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
// Component group
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
_ => {
|
||||
warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of all the blend modes in their conventional ordering and grouping.
|
||||
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
|
||||
[
|
||||
// Normal group
|
||||
&[BlendMode::Normal],
|
||||
// Darken group
|
||||
&[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn],
|
||||
// Lighten group
|
||||
&[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge],
|
||||
// Contrast group
|
||||
&[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight],
|
||||
// Inversion group
|
||||
&[BlendMode::Difference, BlendMode::Exclusion],
|
||||
// Component group
|
||||
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct LuminanceNode<LuminanceCalculation> {
|
||||
@@ -850,6 +903,20 @@ fn image_opacity(color: Color, opacity_multiplier: f32) -> Color {
|
||||
Color::from_rgbaf32_unchecked(color.r(), color.g(), color.b(), color.a() * opacity_multiplier)
|
||||
}
|
||||
|
||||
#[node_macro::node_impl(OpacityNode)]
|
||||
fn image_opacity(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
|
||||
let opacity_multiplier = opacity_multiplier / 100.;
|
||||
vector_data.style.opacity *= opacity_multiplier;
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[node_macro::node_impl(OpacityNode)]
|
||||
fn image_opacity(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
|
||||
let opacity_multiplier = opacity_multiplier / 100.;
|
||||
graphic_group.opacity *= opacity_multiplier;
|
||||
graphic_group
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PosterizeNode<P> {
|
||||
posterize_value: P,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::raster::ImageFrame;
|
||||
use crate::raster::Pixel;
|
||||
use crate::vector::VectorData;
|
||||
use crate::GraphicElementData;
|
||||
use crate::GraphicGroup;
|
||||
use crate::Node;
|
||||
|
||||
pub trait Transform {
|
||||
@@ -47,6 +48,21 @@ impl<P: Pixel> TransformMut for ImageFrame<P> {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
impl Transform for GraphicGroup {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
self.transform
|
||||
}
|
||||
}
|
||||
impl Transform for &GraphicGroup {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
self.transform
|
||||
}
|
||||
}
|
||||
impl TransformMut for GraphicGroup {
|
||||
fn transform_mut(&mut self) -> &mut DAffine2 {
|
||||
&mut self.transform
|
||||
}
|
||||
}
|
||||
impl Transform for GraphicElementData {
|
||||
fn transform(&self) -> DAffine2 {
|
||||
match self {
|
||||
|
||||
@@ -65,14 +65,14 @@ impl Gradient {
|
||||
}
|
||||
|
||||
/// Adds the gradient def, returning the gradient id
|
||||
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
fn render_defs(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> u64 {
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
let updated_transform = multiplied_transform * bound_transform;
|
||||
|
||||
let mut positions = String::new();
|
||||
for (position, color) in self.positions.iter().filter_map(|(pos, color)| color.map(|color| (pos, color))) {
|
||||
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.rgba_hex());
|
||||
let _ = write!(positions, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a() * opacity).rgba_hex());
|
||||
}
|
||||
|
||||
let mod_gradient = transformed_bound_transform.inverse();
|
||||
@@ -179,12 +179,12 @@ impl Fill {
|
||||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs.
|
||||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], opacity: f32) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a())),
|
||||
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill", color.a() * opacity)),
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds, opacity);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
@@ -326,12 +326,12 @@ impl Stroke {
|
||||
}
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self) -> String {
|
||||
pub fn render(&self, opacity: f32) -> String {
|
||||
if let Some(color) = self.color {
|
||||
format!(
|
||||
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
|
||||
color.rgb_hex(),
|
||||
format_opacity("stroke", color.a()),
|
||||
format_opacity("stroke", opacity * color.a()),
|
||||
self.weight,
|
||||
self.dash_lengths(),
|
||||
self.dash_offset,
|
||||
@@ -405,15 +405,24 @@ impl Default for Stroke {
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, Hash, specta::Type)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, DynAny, specta::Type)]
|
||||
pub struct PathStyle {
|
||||
stroke: Option<Stroke>,
|
||||
fill: Fill,
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for PathStyle {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.stroke.hash(state);
|
||||
self.fill.hash(state);
|
||||
self.opacity.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
|
||||
Self { stroke, fill }
|
||||
Self { stroke, fill, opacity: 1. }
|
||||
}
|
||||
|
||||
/// Get the current path's [Fill].
|
||||
@@ -522,12 +531,12 @@ impl PathStyle {
|
||||
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
let fill_attribute = match (view_mode, &self.fill) {
|
||||
(ViewMode::Outline, _) => Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds),
|
||||
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds),
|
||||
(ViewMode::Outline, _) => Fill::None.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity),
|
||||
(_, fill) => fill.render(svg_defs, multiplied_transform, bounds, transformed_bounds, self.opacity),
|
||||
};
|
||||
let stroke_attribute = match (view_mode, &self.stroke) {
|
||||
(ViewMode::Outline, _) => Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(),
|
||||
(_, Some(stroke)) => stroke.render(),
|
||||
(ViewMode::Outline, _) => Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT).render(self.opacity),
|
||||
(_, Some(stroke)) => stroke.render(self.opacity),
|
||||
(_, None) => String::new(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user