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:
0HyperCube
2023-11-19 01:06:19 +00:00
committed by Keavon Chambers
parent 719c96ecd8
commit 8a1cf3ad5d
40 changed files with 1238 additions and 477 deletions

View File

@@ -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

View File

@@ -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]> {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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(),
};

View File

@@ -329,6 +329,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
register_node!(graphene_std::raster::EmptyImageNode<_, _>, input: DAffine2, params: [Color]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: ImageFrame<Color>, fn_params: [Footprint => ImageFrame<Color>]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicGroup, fn_params: [Footprint => graphene_core::GraphicGroup]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Footprint, output: graphene_core::GraphicElementData, fn_params: [Footprint => graphene_core::GraphicElementData]),
async_node!(graphene_std::wasm_application_io::LoadResourceNode<_>, input: WasmEditorApi, output: Arc<[u8]>, params: [String]),
register_node!(graphene_std::wasm_application_io::DecodeImageNode, input: Arc<[u8]>, params: []),
@@ -542,6 +543,8 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
),
],
raster_node!(graphene_core::raster::OpacityNode<_>, params: [f32]),
register_node!(graphene_core::raster::OpacityNode<_>, input: VectorData, params: [f32]),
register_node!(graphene_core::raster::OpacityNode<_>, input: GraphicGroup, params: [f32]),
raster_node!(graphene_core::raster::PosterizeNode<_>, params: [f32]),
raster_node!(graphene_core::raster::ExposureNode<_, _, _>, params: [f32, f32, f32]),
register_node!(graphene_core::memo::LetNode<_>, input: Option<ImageFrame<Color>>, params: []),
@@ -741,6 +744,32 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
NodeIOTypes::new(concrete!(Footprint), concrete!(ImageFrame<Color>), params)
},
),
(
NodeIdentifier::new("graphene_core::transform::TransformNode<_, _, _, _, _, _>"),
|mut args| {
Box::pin(async move {
const EXPECT_MESSAGE: &str = "Not enough arguments provided to construct node";
args.reverse();
let node = <graphene_core::transform::TransformNode<_, _, _, _, _, _>>::new(
DowncastBothNode::<Footprint, GraphicGroup>::new(args.pop().expect(EXPECT_MESSAGE)),
graphene_std::any::input_node::<DVec2>(args.pop().expect(EXPECT_MESSAGE)),
graphene_std::any::input_node::<f32>(args.pop().expect(EXPECT_MESSAGE)),
graphene_std::any::input_node::<DVec2>(args.pop().expect(EXPECT_MESSAGE)),
graphene_std::any::input_node::<DVec2>(args.pop().expect(EXPECT_MESSAGE)),
graphene_std::any::input_node::<DVec2>(args.pop().expect(EXPECT_MESSAGE)),
);
let any: DynAnyNode<Footprint, _, _> = graphene_std::any::DynAnyNode::new(node);
Box::new(any) as TypeErasedBox
})
},
{
let params = vec![fn_type!(Footprint, GraphicGroup), fn_type!(DVec2), fn_type!(f32), fn_type!(DVec2), fn_type!(DVec2), fn_type!(DVec2)];
NodeIOTypes::new(concrete!(Footprint), concrete!(GraphicGroup), params)
},
),
],
register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [VectorData]),
register_node!(graphene_core::transform::SetTransformNode<_>, input: ImageFrame<Color>, params: [ImageFrame<Color>]),