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