Wrap opacity/blend_mode in alpha_blending struct for graphic elements

This commit is contained in:
Keavon Chambers 2023-12-08 20:15:37 -08:00
parent 10f2fa92e5
commit e459e599b4
15 changed files with 111 additions and 96 deletions

View file

@ -13,38 +13,72 @@ use glam::{DAffine2, DVec2, IVec2, UVec2};
pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlphaBlending {
pub opacity: f32,
pub blend_mode: BlendMode,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl core::hash::Hash for AlphaBlending {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.blend_mode.hash(state);
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
blend_mode: BlendMode::Normal,
}
}
}
/// A list of [`GraphicElement`]s
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GraphicGroup {
elements: Vec<GraphicElement>,
pub opacity: f32,
pub blend_mode: BlendMode,
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
}
impl core::hash::Hash for GraphicGroup {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.transform.to_cols_array().iter().for_each(|element| element.to_bits().hash(state));
self.elements.hash(state);
self.opacity.to_bits().hash(state);
self.transform.to_cols_array().iter().for_each(|element| element.to_bits().hash(state))
self.alpha_blending.hash(state);
}
}
/// Internal data for a [`GraphicElement`]. Can be [`VectorData`], [`ImageFrame`], text, or a nested [`GraphicGroup`]
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
/// Can be another recursively nested [`GraphicGroup`], [`VectorData`], an [`ImageFrame`], text (not yet implemented), or an [`Artboard`].
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GraphicElement {
VectorShape(Box<VectorData>),
ImageFrame(ImageFrame<Color>),
Text(String),
/// Equivalent to the SVG <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(GraphicGroup),
/// A vector shape, equivalent to the SVG <path> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path
VectorData(Box<VectorData>),
/// A bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
ImageFrame(ImageFrame<Color>),
// TODO: Switch from `String` to a proper formatted typography type
/// Text, equivalent to the SVG <text> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
/// (Not yet implemented.)
Text(String),
/// The bounds for displaying a page of contained content
Artboard(Artboard),
}
// TODO: Can this be removed? It doesn't necessarily make that much sense to have a default when, instead, the entire GraphicElement just shouldn't exist if there's no specific content to assign it.
impl Default for GraphicElement {
fn default() -> Self {
Self::VectorShape(Box::new(VectorData::empty()))
Self::VectorData(Box::new(VectorData::empty()))
}
}
@ -131,7 +165,7 @@ impl From<ImageFrame<Color>> for GraphicElement {
}
impl From<VectorData> for GraphicElement {
fn from(vector_data: VectorData) -> Self {
GraphicElement::VectorShape(Box::new(vector_data))
GraphicElement::VectorData(Box::new(vector_data))
}
}
impl From<GraphicGroup> for GraphicElement {
@ -173,9 +207,8 @@ where
fn from(value: T) -> Self {
Self {
elements: (vec![value.into()]),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::default(),
}
}
}
@ -183,9 +216,8 @@ where
impl GraphicGroup {
pub const EMPTY: Self = Self {
elements: Vec::new(),
opacity: 1.,
blend_mode: BlendMode::Normal,
transform: DAffine2::IDENTITY,
alpha_blending: AlphaBlending::new(),
};
pub fn to_usvg_tree(&self, resolution: UVec2, viewbox: [DVec2; 2]) -> usvg::Tree {
@ -214,7 +246,7 @@ impl GraphicElement {
}
match self {
GraphicElement::VectorShape(vector_data) => {
GraphicElement::VectorData(vector_data) => {
use usvg::tiny_skia_path::PathBuilder;
let mut builder = PathBuilder::new();

View file

@ -223,12 +223,12 @@ impl GraphicElementRendered for GraphicGroup {
|attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
if self.opacity < 1. {
attributes.push("opacity", self.opacity.to_string());
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
}
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
},
|render| {
@ -275,12 +275,12 @@ impl GraphicElementRendered for VectorData {
.render(render_params.view_mode, &mut attributes.0.svg_defs, multiplied_transform, layer_bounds, transformed_bounds);
attributes.push_val(fill_and_stroke);
if self.style.opacity < 1. {
attributes.push("opacity", self.style.opacity.to_string());
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
}
if self.style.blend_mode != BlendMode::default() {
attributes.push("style", self.style.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
}
@ -426,8 +426,8 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", SvgSegment::BlobUrl(uuid));
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
render.image_data.push((uuid, self.image.clone()))
@ -449,8 +449,8 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", base64_string);
if self.blend_mode != BlendMode::default() {
attributes.push("style", self.blend_mode.render());
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
});
}
@ -493,7 +493,7 @@ impl GraphicElementRendered for ImageFrame<Color> {
impl GraphicElementRendered for GraphicElement {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.render_svg(render, render_params),
GraphicElement::VectorData(vector_data) => vector_data.render_svg(render, render_params),
GraphicElement::ImageFrame(image_frame) => image_frame.render_svg(render, render_params),
GraphicElement::Text(_) => todo!("Render a text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params),
@ -503,7 +503,7 @@ impl GraphicElementRendered for GraphicElement {
fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
match self {
GraphicElement::VectorShape(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
GraphicElement::VectorData(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
GraphicElement::ImageFrame(image_frame) => image_frame.bounding_box(transform),
GraphicElement::Text(_) => todo!("Bounds of a text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform),
@ -513,7 +513,7 @@ impl GraphicElementRendered for GraphicElement {
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElement::VectorData(vector_data) => vector_data.add_click_targets(click_targets),
GraphicElement::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets),
GraphicElement::Text(_) => todo!("click target for text GraphicElement"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets),
@ -523,7 +523,7 @@ impl GraphicElementRendered for GraphicElement {
fn to_usvg_node(&self) -> usvg::Node {
match self {
GraphicElement::VectorShape(vector_data) => vector_data.to_usvg_node(),
GraphicElement::VectorData(vector_data) => vector_data.to_usvg_node(),
GraphicElement::ImageFrame(image_frame) => image_frame.to_usvg_node(),
GraphicElement::Text(text) => text.to_usvg_node(),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.to_usvg_node(),

View file

@ -912,14 +912,14 @@ fn opacity_node(color: Color, opacity_multiplier: f32) -> Color {
#[node_macro::node_impl(OpacityNode)]
fn opacity_node(mut vector_data: VectorData, opacity_multiplier: f32) -> VectorData {
let opacity_multiplier = opacity_multiplier / 100.;
vector_data.style.opacity *= opacity_multiplier;
vector_data.alpha_blending.opacity *= opacity_multiplier;
vector_data
}
#[node_macro::node_impl(OpacityNode)]
fn opacity_node(mut graphic_group: GraphicGroup, opacity_multiplier: f32) -> GraphicGroup {
let opacity_multiplier = opacity_multiplier / 100.;
graphic_group.opacity *= opacity_multiplier;
graphic_group.alpha_blending.opacity *= opacity_multiplier;
graphic_group
}
@ -930,19 +930,19 @@ pub struct BlendModeNode<BM> {
#[node_macro::node_fn(BlendModeNode)]
fn blend_mode_node(mut vector_data: VectorData, blend_mode: BlendMode) -> VectorData {
vector_data.style.blend_mode = blend_mode;
vector_data.alpha_blending.blend_mode = blend_mode;
vector_data
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut graphic_group: GraphicGroup, blend_mode: BlendMode) -> GraphicGroup {
graphic_group.blend_mode = blend_mode;
graphic_group.alpha_blending.blend_mode = blend_mode;
graphic_group
}
#[node_macro::node_impl(BlendModeNode)]
fn blend_mode_node(mut image_frame: ImageFrame<Color>, blend_mode: BlendMode) -> ImageFrame<Color> {
image_frame.blend_mode = blend_mode;
image_frame.alpha_blending.blend_mode = blend_mode;
image_frame
}

View file

@ -1,6 +1,6 @@
use super::discrete_srgb::float_to_srgb_u8;
use super::{Color, ImageSlice};
use crate::Node;
use crate::{AlphaBlending, Node};
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use dyn_any::StaticType;
@ -260,7 +260,7 @@ pub struct ImageFrame<P: Pixel> {
// positive going right and y axis positive going down, with the origin
// being an unspecified quantity.
pub transform: DAffine2,
pub blend_mode: BlendMode,
pub alpha_blending: AlphaBlending,
}
impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
@ -312,7 +312,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::ZERO,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::new(),
}
}
@ -320,7 +320,7 @@ impl<P: Copy + Pixel> ImageFrame<P> {
Self {
image: Image::empty(),
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::new(),
}
}
@ -381,7 +381,7 @@ impl From<ImageFrame<Color>> for ImageFrame<SRGBA8> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
alpha_blending: image.alpha_blending,
}
}
}
@ -396,7 +396,7 @@ impl From<ImageFrame<SRGBA8>> for ImageFrame<Color> {
height: image.image.height,
},
transform: image.transform,
blend_mode: BlendMode::Normal,
alpha_blending: image.alpha_blending,
}
}
}

View file

@ -73,7 +73,7 @@ impl TransformMut for GraphicGroup {
impl Transform for GraphicElement {
fn transform(&self) -> DAffine2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.transform(),
GraphicElement::VectorData(vector_shape) => vector_shape.transform(),
GraphicElement::ImageFrame(image_frame) => image_frame.transform(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform(),
@ -82,7 +82,7 @@ impl Transform for GraphicElement {
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElement::VectorData(vector_shape) => vector_shape.local_pivot(pivot),
GraphicElement::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot),
@ -91,7 +91,7 @@ impl Transform for GraphicElement {
}
fn decompose_scale(&self) -> DVec2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.decompose_scale(),
GraphicElement::VectorData(vector_shape) => vector_shape.decompose_scale(),
GraphicElement::ImageFrame(image_frame) => image_frame.decompose_scale(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.decompose_scale(),
@ -102,7 +102,7 @@ impl Transform for GraphicElement {
impl TransformMut for GraphicElement {
fn transform_mut(&mut self) -> &mut DAffine2 {
match self {
GraphicElement::VectorShape(vector_shape) => vector_shape.transform_mut(),
GraphicElement::VectorData(vector_shape) => vector_shape.transform_mut(),
GraphicElement::ImageFrame(image_frame) => image_frame.transform_mut(),
GraphicElement::Text(_) => todo!("Transform of text"),
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform_mut(),

View file

@ -1,7 +1,6 @@
//! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::raster::BlendMode;
use crate::Color;
use dyn_any::{DynAny, StaticType};
@ -410,27 +409,18 @@ impl Default for Stroke {
pub struct PathStyle {
stroke: Option<Stroke>,
fill: Fill,
pub opacity: f32,
pub blend_mode: BlendMode,
}
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);
self.blend_mode.hash(state);
}
}
impl PathStyle {
pub const fn new(stroke: Option<Stroke>, fill: Fill) -> Self {
Self {
stroke,
fill,
opacity: 1.,
blend_mode: BlendMode::Normal,
}
Self { stroke, fill }
}
/// Get the current path's [Fill].

View file

@ -1,6 +1,6 @@
use super::style::{PathStyle, Stroke};
use crate::uuid::ManipulatorGroupId;
use crate::Color;
use crate::{uuid::ManipulatorGroupId, AlphaBlending};
use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType};
@ -8,13 +8,14 @@ use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
/// [VectorData] is passed between nodes.
/// It contains a list of subpaths (that may be open or closed), a transform and some style information.
/// It contains a list of subpaths (that may be open or closed), a transform, and some style information.
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
pub alpha_blending: AlphaBlending,
// TODO: Keavon asks: what is this for? Is it dead code? It seems to only be set, never read.
pub mirror_angle: Vec<ManipulatorGroupId>,
}
@ -24,6 +25,7 @@ impl core::hash::Hash for VectorData {
self.subpaths.hash(state);
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
self.style.hash(state);
self.alpha_blending.hash(state);
self.mirror_angle.hash(state);
}
}
@ -35,6 +37,7 @@ impl VectorData {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(),
mirror_angle: Vec::new(),
}
}

View file

@ -356,7 +356,7 @@ async fn brush(image: ImageFrame<Color>, bounds: ImageFrame<Color>, strokes: Vec
let opaque_image = ImageFrame {
image: Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE),
transform: background_bounds,
blend_mode: BlendMode::Normal,
..Default::default()
};
let mut erase_restore_mask = opaque_image;
@ -410,11 +410,7 @@ mod test {
#[test]
fn test_translate_node() {
let image = Image::new(10, 10, Color::TRANSPARENT);
let mut image = ImageFrame {
image,
transform: DAffine2::IDENTITY,
blend_mode: BlendMode::Normal,
};
let mut image = ImageFrame { image, ..Default::default() };
image.translate(DVec2::new(1., 2.));
let translate_node = TranslateNode::new(ClonedNode::new(image));
let image = translate_node.eval(DVec2::new(1., 2.));

View file

@ -90,7 +90,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height,
},
transform: image.transform,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
};
// TODO: The cache should be based on the network topology not the node name
@ -142,7 +142,7 @@ async fn map_gpu<'a: 'input>(image: ImageFrame<Color>, node: DocumentNode, edito
height: image.image.height,
},
transform: image.transform,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
}
}
@ -588,6 +588,6 @@ async fn blend_gpu_image(foreground: ImageFrame<Color>, background: ImageFrame<C
height: background.image.height,
},
transform: background.transform,
blend_mode: background.blend_mode,
alpha_blending: background.alpha_blending,
}
}

View file

@ -8,7 +8,7 @@ use graphene_core::transform::{Footprint, Transform};
use crate::wasm_application_io::WasmEditorApi;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::value::CopiedNode;
use graphene_core::{Color, Node};
use graphene_core::{AlphaBlending, Color, Node};
use std::collections::HashMap;
use std::fmt::Debug;
@ -115,7 +115,7 @@ fn sample(footprint: Footprint, image_frame: ImageFrame<Color>) -> ImageFrame<Co
ImageFrame {
image,
transform: new_transform,
blend_mode: image_frame.blend_mode,
alpha_blending: image_frame.alpha_blending,
}
}
@ -309,7 +309,7 @@ where
let mut new_background = ImageFrame {
image: new_background,
transform: transfrom,
blend_mode: background.blend_mode,
alpha_blending: background.alpha_blending,
};
new_background = blend_image(background, new_background, map_fn);
@ -422,7 +422,7 @@ fn extend_image_to_bounds_node(image: ImageFrame<Color>, bounds: DAffine2) -> Im
ImageFrame {
image: new_img,
transform: new_texture_to_layer_space,
blend_mode: image.blend_mode,
alpha_blending: image.alpha_blending,
}
}
@ -457,8 +457,11 @@ fn empty_image<_P: Pixel>(transform: DAffine2, color: _P) -> ImageFrame<_P> {
let image = Image::new(width, height, color);
let blend_mode = BlendMode::Normal;
ImageFrame { image, transform, blend_mode }
ImageFrame {
image,
transform,
alpha_blending: AlphaBlending::default(),
}
}
macro_rules! generate_imaginate_node {
@ -495,7 +498,7 @@ macro_rules! generate_imaginate_node {
use std::hash::Hasher;
let mut hasher = rustc_hash::FxHasher::default();
frame.image.hash(&mut hasher);
let hash =hasher.finish();
let hash = hasher.finish();
Box::pin(async move {
let controller: std::pin::Pin<Box<dyn std::future::Future<Output = ImaginateController>>> = controller;
@ -505,16 +508,12 @@ macro_rules! generate_imaginate_node {
let image = super::imaginate::imaginate(frame.image, editor_api, controller, $($val,)*).await;
self.cache.lock().unwrap().insert(hash, image.clone());
return ImageFrame {
image,
..frame
}
return ImageFrame { image, ..frame }
}
let image = self.cache.lock().unwrap().get(&hash).cloned().unwrap_or_default();
ImageFrame {
image,
..frame
}
ImageFrame { image, ..frame }
})
}
}
@ -549,7 +548,7 @@ fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_cor
graphene_core::raster::ImageFrame {
image,
transform,
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::default(),
}
}
@ -576,7 +575,7 @@ fn pixel_noise(width: u32, height: u32, seed: u32, noise_type: NoiseType) -> gra
ImageFrame::<Color> {
image,
transform: DAffine2::from_scale(DVec2::new(width as f64, height as f64)),
blend_mode: BlendMode::Normal,
alpha_blending: AlphaBlending::default(),
}
}
@ -621,7 +620,7 @@ fn mandelbrot_node(footprint: Footprint) -> ImageFrame<Color> {
ImageFrame {
image: Image { width, height, data },
transform: DAffine2::from_translation(offset) * DAffine2::from_scale(size),
blend_mode: BlendMode::Normal,
..Default::default()
}
}

View file

@ -279,8 +279,7 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
width: image.width(),
height: image.height(),
},
transform: glam::DAffine2::IDENTITY,
blend_mode: graphene_core::raster::BlendMode::Normal,
..Default::default()
};
image
}

View file

@ -312,7 +312,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
let empty_image = ImageFrame {
image: Image::new(bounds.x, bounds.y, Color::BLACK),
transform,
blend_mode: BlendMode::Normal,
..Default::default()
};
let final_image = ClonedNode::new(empty_image).then(complete_node);
let final_image = FutureWrapperNode::new(final_image);