Replace the AlphaBlending struct with separate attributes (#4086)

* Replace the AlphaBlending struct with separate attributes

* Fix bug

* Fix bug
This commit is contained in:
Keavon Chambers 2026-05-01 03:27:42 -07:00 committed by GitHub
parent 86134c26b4
commit 4474de4662
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 374 additions and 295 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -904,9 +904,7 @@ macro_rules! known_table_row_types {
}; };
} }
/// Override hook for [`Table::attribute_display_value`] that prefers `Display` over `Debug` for select /// Uses `Display` instead of `Debug` for attribute types that have a nicer human-readable format.
/// attribute types. The underlying storage is generic and can only see a `Debug` bound, so types whose
/// nicer `Display` rendering matters in the data panel are listed here explicitly.
fn display_value_override(any: &dyn Any) -> Option<String> { fn display_value_override(any: &dyn Any) -> Option<String> {
if let Some(value) = any.downcast_ref::<BlendMode>() { if let Some(value) = any.downcast_ref::<BlendMode>() {
return Some(value.to_string()); return Some(value.to_string());

View File

@ -440,7 +440,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else { let Some(blend_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::blend_mode::IDENTIFIER, true) else {
return; return;
}; };
let input_connector = InputConnector::node(blend_node_id, 1); let input_connector = InputConnector::node(blend_node_id, graphene_std::blending_nodes::blend_mode::BlendModeInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
} }
@ -449,26 +449,45 @@ impl<'a> ModifyInputsContext<'a> {
return; return;
}; };
// Enable the `has_opacity` checkbox so the value is applied // Enable the `has_opacity` checkbox so the value is applied
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(true), false), false); self.set_input_with_refresh(
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 2), NodeInput::value(TaggedValue::F64(opacity * 100.), false), false); InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
NodeInput::value(TaggedValue::Bool(true), false),
false,
);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::OpacityInput::INDEX),
NodeInput::value(TaggedValue::F64(opacity * 100.), false),
false,
);
} }
pub fn opacity_fill_set(&mut self, fill: f64) { pub fn opacity_fill_set(&mut self, fill: f64) {
// Reuse the Opacity node if already present (saving a chain walk on slider drags), otherwise let the next call create it // Reuse an existing Opacity node to avoid a redundant chain walk on slider drags
let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER; let identifier = graphene_std::blending_nodes::opacity::IDENTIFIER;
let existing = self.existing_proto_node_id(identifier.clone(), false); let existing = self.existing_proto_node_id(identifier.clone(), false);
let existed = existing.is_some(); let existed = existing.is_some();
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else { let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
return; return;
}; };
// Disable the opacity component on a freshly-created node so the slider only affects fill, mirroring the opacity-slider case // Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
// (where the node's default `has_fill = false` already keeps fill out of the picture)
if !existed { if !existed {
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 1), NodeInput::value(TaggedValue::Bool(false), false), false); self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasOpacityInput::INDEX),
NodeInput::value(TaggedValue::Bool(false), false),
false,
);
} }
// Enable the `has_fill` checkbox so the value is applied // Enable the `has_fill` checkbox so the value is applied
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), false); self.set_input_with_refresh(
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 4), NodeInput::value(TaggedValue::F64(fill * 100.), false), false); InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::HasFillInput::INDEX),
NodeInput::value(TaggedValue::Bool(true), false),
false,
);
self.set_input_with_refresh(
InputConnector::node(opacity_node_id, graphene_std::blending_nodes::opacity::FillInput::INDEX),
NodeInput::value(TaggedValue::F64(fill * 100.), false),
false,
);
} }
/// Set the stops table on the 'Gradient Value' node, creating it if necessary. /// Set the stops table on the 'Gradient Value' node, creating it if necessary.
@ -570,7 +589,7 @@ impl<'a> ModifyInputsContext<'a> {
let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else { let Some(clip_node_id) = self.existing_proto_node_id(graphene_std::blending_nodes::clipping_mask::IDENTIFIER, true) else {
return; return;
}; };
let input_connector = InputConnector::node(clip_node_id, 1); let input_connector = InputConnector::node(clip_node_id, graphene_std::blending_nodes::clipping_mask::ClipInput::INDEX);
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
} }

View File

@ -34,8 +34,8 @@ use std::any::TypeId;
use std::future::Future; use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
pub use table::{ pub use table::{
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME, ATTR_SPREAD_METHOD, ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_END, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_NAME,
ATTR_START, ATTR_TRANSFORM, ATTR_TYPE, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
}; };
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
pub use tsify; pub use tsify;

View File

@ -10,59 +10,60 @@ use std::fmt::Debug;
// Standard attribute keys used across the data flow // Standard attribute keys used across the data flow
// ===================================================================== // =====================================================================
/// Attribute key for a row's `DAffine2` transformation, applied when rendering and when accumulating /// Row's `DAffine2` transformation, composed multiplicatively through nested groups.
/// transforms through nested compositions.
pub const ATTR_TRANSFORM: &str = "transform"; pub const ATTR_TRANSFORM: &str = "transform";
/// Attribute key for a row's `AlphaBlending` (blend mode + opacity + fill + clip), composed /// Row's `BlendMode`, controlling how it composites with content beneath it.
/// multiplicatively through nested compositions. pub const ATTR_BLEND_MODE: &str = "blend_mode";
pub const ATTR_ALPHA_BLENDING: &str = "alpha_blending";
/// Attribute key under which each row of an editor-aware layer stores a `Table<NodeId>` describing the /// Row's opacity multiplier (`f64`, implicit default `1.`).
/// path (from the root document network) to the layer node that owns the row. Editor tools use this to /// Composed multiplicatively through nested groups. Affects content clipped to the row.
/// route clicks/selection back to the originating layer at any nesting depth. pub const ATTR_OPACITY: &str = "opacity";
/// Row's fill opacity multiplier (`f64`, implicit default `1.`).
/// Like opacity but does not affect content clipped to the row.
pub const ATTR_OPACITY_FILL: &str = "opacity_fill";
/// Whether a row inherits the alpha of the content beneath it (clipping mask).
pub const ATTR_CLIPPING_MASK: &str = "clipping_mask";
/// `Table<NodeId>` path from the root network to the layer node owning this row.
/// Used by editor tools to route clicks/selection back to the originating layer.
pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path"; pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path";
/// Attribute key under which a row stores a `Table<Graphic>` snapshot of the upstream content that fed /// `Table<Graphic>` snapshot of the upstream content that fed into a destructive merge
/// into a destructive merge (Boolean Operation, Flatten Path, Morph, Rasterize, etc.). The renderer /// (Boolean Operation, Rasterize, etc.), so the editor can still surface click targets for
/// recurses into this snapshot during metadata collection so the editor can still surface click targets /// the original child layers after their content has been collapsed.
/// for the original child layers after their content has been collapsed into a single output.
pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers"; pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers";
/// Attribute key for the byte offset where a regex match begins in the input string, set by the /// Byte offset where a regex match begins ('Regex Find All', 'Regex Capture' text nodes).
/// `regex_find_all` and `regex_capture` text nodes.
pub const ATTR_START: &str = "start"; pub const ATTR_START: &str = "start";
/// Attribute key for the byte offset where a regex match ends in the input string, set by the /// Byte offset where a regex match ends ('Regex Find All', 'Regex Capture' text nodes).
/// `regex_find_all` and `regex_capture` text nodes.
pub const ATTR_END: &str = "end"; pub const ATTR_END: &str = "end";
/// Attribute key for a regex named-capture-group's name (empty for unnamed groups), set by the /// Regex named-capture-group's name, or empty for unnamed groups ('Regex Capture' text node).
/// `regex_capture` text node.
pub const ATTR_NAME: &str = "name"; pub const ATTR_NAME: &str = "name";
/// Attribute key for a JSON value's type (`"string"`, `"number"`, `"object"`, etc.), set by the /// JSON value's type string (`"string"`, `"number"`, `"object"`, etc.) from 'JSON Query All'.
/// `json_query_all` text node alongside each extracted value.
pub const ATTR_TYPE: &str = "type"; pub const ATTR_TYPE: &str = "type";
/// Attribute key for an artboard row's `DVec2` top-left corner location in document coordinates. /// Artboard's `DVec2` top-left corner in document coordinates.
pub const ATTR_LOCATION: &str = "location"; pub const ATTR_LOCATION: &str = "location";
/// Attribute key for an artboard row's `DVec2` width and height. /// Artboard's `DVec2` width and height.
pub const ATTR_DIMENSIONS: &str = "dimensions"; pub const ATTR_DIMENSIONS: &str = "dimensions";
/// Attribute key for an artboard row's `Color` background fill. /// Artboard's `Color` background fill.
pub const ATTR_BACKGROUND: &str = "background"; pub const ATTR_BACKGROUND: &str = "background";
/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard bounds. /// Whether an artboard clips content to its bounds.
pub const ATTR_CLIP: &str = "clip"; pub const ATTR_CLIP: &str = "clip";
/// Attribute key for a `Table<GradientStops>` row's `GradientSpreadMethod`, controlling the gradient's behavior /// Gradient's `GradientSpreadMethod` (`Pad`, `Reflect`, or `Repeat`).
/// outside the start/end stops (`Pad` clamps to the boundary colors, `Reflect` mirrors, `Repeat` tiles).
pub const ATTR_SPREAD_METHOD: &str = "spread_method"; pub const ATTR_SPREAD_METHOD: &str = "spread_method";
/// Attribute key for a `Table<GradientStops>` row's `GradientType`, choosing between a linear gradient (color /// Gradient's `GradientType` (`Linear` or `Radial`).
/// transitions along the gradient line) or a radial gradient (color transitions outward from the line's start).
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type"; pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
// ===================== // =====================

View File

@ -1,5 +1,5 @@
use crate::graphic::Graphic; use crate::graphic::Graphic;
use core_types::blending::AlphaBlending; use core_types::blending::BlendMode;
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color}; use core_types::{ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_LOCATION, Color};
@ -22,6 +22,17 @@ use glam::{DAffine2, IVec2};
pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Table<Graphic>>, D::Error> { pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Table<Graphic>>, D::Error> {
use serde::Deserialize; use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
/// Pre-migration shape of the artboard's stored data: the struct that used to live as the element /// Pre-migration shape of the artboard's stored data: the struct that used to live as the element
/// of `Table<Artboard>`. Kept as a private type so we can deserialize legacy documents into the new /// of `Table<Artboard>`. Kept as a private type so we can deserialize legacy documents into the new
/// `Table<Table<Graphic>>` (element = `content`, other fields → row attributes). /// `Table<Table<Graphic>>` (element = `content`, other fields → row attributes).
@ -48,7 +59,7 @@ pub fn migrate_artboard<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Re
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>, element: Vec<T>,
transform: Vec<DAffine2>, transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>, alpha_blending: Vec<LegacyAlphaBlending>,
} }
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@ -1,11 +1,11 @@
use core_types::blending::AlphaBlending; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::graphene_hash::CacheHash; use core_types::graphene_hash::CacheHash;
use core_types::ops::TableConvert; use core_types::ops::TableConvert;
use core_types::render_complexity::RenderComplexity; use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, Color}; use core_types::{ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::DAffine2; use glam::DAffine2;
use raster_types::{CPU, GPU, Raster}; use raster_types::{CPU, GPU, Raster};
@ -130,47 +130,57 @@ impl From<Table<GradientStops>> for Graphic {
/// Deeply flattens a `Table<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`) /// Deeply flattens a `Table<Graphic>`, collecting only elements matching a specific variant (extracted by `extract_variant`)
/// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`Table`s composes transforms and opacity. /// and discarding all other non-matching content. Recursion through `Graphic::Graphic` sub-`Table`s composes transforms and opacity.
fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) -> Table<T> { fn flatten_graphic_table<T>(content: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) -> Table<T> {
fn compose_alpha_blending(parent: AlphaBlending, child: AlphaBlending) -> AlphaBlending {
AlphaBlending {
blend_mode: child.blend_mode,
opacity: parent.opacity * child.opacity,
fill: child.fill,
clip: child.clip,
}
}
fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) { fn flatten_recursive<T>(output: &mut Table<T>, current_graphic_table: Table<Graphic>, extract_variant: fn(Graphic) -> Option<Table<T>>) {
for current_graphic_row in current_graphic_table.into_iter() { for current_graphic_row in current_graphic_table.into_iter() {
let layer_path: Table<NodeId> = current_graphic_row.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); let layer_path: Table<NodeId> = current_graphic_row.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default(ATTR_TRANSFORM); let current_transform: DAffine2 = current_graphic_row.attribute_cloned_or_default(ATTR_TRANSFORM);
let current_alpha_blending: AlphaBlending = current_graphic_row.attribute_cloned_or_default(ATTR_ALPHA_BLENDING); let current_opacity: f64 = current_graphic_row.attribute_cloned_or(ATTR_OPACITY, 1.);
let current_fill: f64 = current_graphic_row.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
match current_graphic_row.into_element() { match current_graphic_row.into_element() {
// Recurse into nested `Table<Graphic>` items, composing the parent's transform onto each child // Compose the parent's transform, opacity, and fill onto each child row
Graphic::Graphic(mut sub_table) => { Graphic::Graphic(mut sub_table) => {
for index in 0..sub_table.len() { // Identity default means a missing column still composes correctly
let child_transform: DAffine2 = sub_table.attribute_cloned_or_default(ATTR_TRANSFORM, index); for v in sub_table.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
let child_alpha_blending: AlphaBlending = sub_table.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); *v = current_transform * *v;
}
sub_table.set_attribute(ATTR_TRANSFORM, index, current_transform * child_transform); // f64 defaults to 0, but opacity/fill default to 1, so missing columns must be set rather than multiplied
sub_table.set_attribute(ATTR_ALPHA_BLENDING, index, compose_alpha_blending(current_alpha_blending, child_alpha_blending)); if let Some(values) = sub_table.iter_attribute_values_mut::<f64>(ATTR_OPACITY) {
for v in values {
*v *= current_opacity;
}
} else {
for v in sub_table.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY) {
*v = current_opacity;
}
}
if let Some(values) = sub_table.iter_attribute_values_mut::<f64>(ATTR_OPACITY_FILL) {
for v in values {
*v *= current_fill;
}
} else {
for v in sub_table.iter_attribute_values_mut_or_default::<f64>(ATTR_OPACITY_FILL) {
*v = current_fill;
}
} }
flatten_recursive(output, sub_table, extract_variant); flatten_recursive(output, sub_table, extract_variant);
} }
// Try to extract the target variant; if it matches, push its items with composed transform and opacity // Extract the target variant and push its items with composed transform, opacity, and fill
other => { other => {
if let Some(typed_table) = extract_variant(other) { if let Some(typed_table) = extract_variant(other) {
for row in typed_table.into_iter() { for mut item in typed_table.into_iter() {
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); let row_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default(ATTR_ALPHA_BLENDING); let row_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.);
let (element, mut attributes) = row.into_parts(); let row_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
attributes.insert(ATTR_TRANSFORM, current_transform * row_transform); item.set_attribute(ATTR_TRANSFORM, current_transform * row_transform);
attributes.insert(ATTR_ALPHA_BLENDING, compose_alpha_blending(current_alpha_blending, row_alpha_blending)); item.set_attribute(ATTR_OPACITY, current_opacity * row_opacity);
attributes.insert(ATTR_EDITOR_LAYER_PATH, layer_path.clone()); item.set_attribute(ATTR_OPACITY_FILL, current_fill * row_fill);
item.set_attribute(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
output.push(TableRow::from_parts(element, attributes)); output.push(item);
} }
} }
} }
@ -321,8 +331,9 @@ impl Graphic {
pub fn had_clip_enabled(&self) -> bool { pub fn had_clip_enabled(&self) -> bool {
fn all_clipped<T>(table: &Table<T>) -> bool { fn all_clipped<T>(table: &Table<T>) -> bool {
table.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING).all(|a| a.clip) table.iter_attribute_values_or_default::<bool>(ATTR_CLIPPING_MASK).all(|clip| clip)
} }
match self { match self {
Graphic::Vector(table) => all_clipped(table), Graphic::Vector(table) => all_clipped(table),
Graphic::Graphic(table) => all_clipped(table), Graphic::Graphic(table) => all_clipped(table),
@ -335,12 +346,11 @@ impl Graphic {
pub fn can_reduce_to_clip_path(&self) -> bool { pub fn can_reduce_to_clip_path(&self) -> bool {
match self { match self {
Graphic::Vector(vector) => vector Graphic::Vector(vector) => (0..vector.len()).all(|index| {
.iter_element_values() let Some(element) = vector.element(index) else { return false };
.zip(vector.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
.all(|(element, alpha_blending)| { opacity > 1. - f64::EPSILON && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
(alpha_blending.opacity > 1. - f32::EPSILON) && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke()) }),
}),
_ => false, _ => false,
} }
} }
@ -474,12 +484,23 @@ impl<T: Clone> OmitIndex for Table<T> {
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> { pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
use serde::Deserialize; use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)] #[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldGraphicGroup { pub struct OldGraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>, elements: Vec<(Graphic, Option<NodeId>)>,
transform: DAffine2, transform: DAffine2,
alpha_blending: AlphaBlending, alpha_blending: LegacyAlphaBlending,
} }
#[derive(Clone, Debug, PartialEq, DynAny, Default)] #[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -502,7 +523,7 @@ pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Res
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>, element: Vec<T>,
transform: Vec<DAffine2>, transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>, alpha_blending: Vec<LegacyAlphaBlending>,
} }
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@ -10,10 +10,8 @@ pub use vector_types;
pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector}; pub use graphic::{Graphic, IntoGraphicTable, TryFromGraphic, Vector};
pub mod migrations { pub mod migrations {
use core_types::{ use core_types::blending::BlendMode;
AlphaBlending, use core_types::table::{Table, TableRow};
table::{Table, TableRow},
};
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::DAffine2; use glam::DAffine2;
use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId}; use vector_types::vector::{PathStyle, PointDomain, RegionDomain, SegmentDomain, misc::HandleId};
@ -24,11 +22,22 @@ pub mod migrations {
pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Vector>, D::Error> { pub fn migrate_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Vector>, D::Error> {
use serde::Deserialize; use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, PartialEq, DynAny)] #[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldVectorData { pub struct OldVectorData {
pub transform: DAffine2, pub transform: DAffine2,
pub alpha_blending: AlphaBlending, pub alpha_blending: LegacyAlphaBlending,
pub style: PathStyle, pub style: PathStyle,
@ -47,7 +56,7 @@ pub mod migrations {
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>, element: Vec<T>,
transform: Vec<DAffine2>, transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>, alpha_blending: Vec<LegacyAlphaBlending>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@ -1,63 +1,6 @@
use core::fmt::Display; use core::fmt::Display;
use node_macro::BufferStruct; use node_macro::BufferStruct;
use num_enum::{FromPrimitive, IntoPrimitive}; use num_enum::{FromPrimitive, IntoPrimitive};
#[cfg(not(feature = "std"))]
use num_traits::float::Float;
#[derive(Debug, Clone, Copy, PartialEq, BufferStruct)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, serde::Serialize, serde::Deserialize, graphene_hash::CacheHash))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "std", serde(default))]
pub struct AlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
impl Default for AlphaBlending {
fn default() -> Self {
Self::new()
}
}
impl Display for AlphaBlending {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let round = |x: f32| (x * 1e3).round() / 1e3;
write!(
f,
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
self.blend_mode,
round(self.opacity * 100.),
round(self.fill * 100.),
if self.clip { "Yes" } else { "No" }
)
}
}
impl AlphaBlending {
pub const fn new() -> Self {
Self {
opacity: 1.,
fill: 1.,
blend_mode: BlendMode::Normal,
clip: false,
}
}
pub fn lerp(&self, other: &Self, t: f32) -> Self {
let lerp = |a: f32, b: f32, t: f32| a + (b - a) * t;
AlphaBlending {
opacity: lerp(self.opacity, other.opacity, t),
fill: lerp(self.fill, other.fill, t),
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
clip: if t < 0.5 { self.clip } else { other.clip },
}
}
pub fn opacity(&self, mask: bool) -> f32 {
self.opacity * if mask { 1. } else { self.fill }
}
}
#[repr(i32)] #[repr(i32)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]

View File

@ -1,8 +1,9 @@
use crate::raster_types::{CPU, Raster}; use crate::raster_types::{CPU, Raster};
use crate::{Bitmap, BitmapMut}; use crate::{Bitmap, BitmapMut};
use core_types::blending::BlendMode;
use core_types::color::float_to_srgb_u8; use core_types::color::float_to_srgb_u8;
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::{ATTR_ALPHA_BLENDING, ATTR_TRANSFORM, AlphaBlending, Color}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Color};
// use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently // use crate::vector::Vector; // TODO: Check if Vector is actually used, if so handle differently
use core_types::color::*; use core_types::color::*;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
@ -222,6 +223,17 @@ impl<P: Pixel> IntoIterator for Image<P> {
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> { pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> {
use serde::Deserialize; use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)] #[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)]
enum RasterFrame { enum RasterFrame {
ImageFrame(Table<Image<Color>>), ImageFrame(Table<Image<Color>>),
@ -280,7 +292,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
pub struct OldImageFrame<P: Pixel> { pub struct OldImageFrame<P: Pixel> {
image: Image<P>, image: Image<P>,
transform: DAffine2, transform: DAffine2,
alpha_blending: AlphaBlending, alpha_blending: LegacyAlphaBlending,
} }
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -303,7 +315,7 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))] #[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>, element: Vec<T>,
transform: Vec<DAffine2>, transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>, alpha_blending: Vec<LegacyAlphaBlending>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -339,7 +351,10 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => { FormatVersions::OldImageFrame(OldImageFrame { image, transform, alpha_blending }) => {
let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image)); let mut image_frame_table = Table::new_from_element(Raster::new_cpu(image));
image_frame_table.set_attribute(ATTR_TRANSFORM, 0, transform); image_frame_table.set_attribute(ATTR_TRANSFORM, 0, transform);
image_frame_table.set_attribute(ATTR_ALPHA_BLENDING, 0, alpha_blending); image_frame_table.set_attribute(ATTR_BLEND_MODE, 0, alpha_blending.blend_mode);
image_frame_table.set_attribute(ATTR_OPACITY, 0, alpha_blending.opacity as f64);
image_frame_table.set_attribute(ATTR_OPACITY_FILL, 0, alpha_blending.fill as f64);
image_frame_table.set_attribute(ATTR_CLIPPING_MASK, 0, alpha_blending.clip);
image_frame_table image_frame_table
} }
FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)), FormatVersions::OlderImageFrameTable(old_table) => from_image_frame_table(older_table_to_new_table(old_table)),
@ -356,6 +371,17 @@ pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) ->
pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<TableRow<Raster<CPU>>, D::Error> { pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<TableRow<Raster<CPU>>, D::Error> {
use serde::Deserialize; use serde::Deserialize;
/// Mirrors the removed `AlphaBlending` struct for legacy document deserialization.
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct LegacyAlphaBlending {
pub blend_mode: BlendMode,
pub opacity: f32,
pub fill: f32,
pub clip: bool,
}
#[derive(Clone, Debug, PartialEq, DynAny)] #[derive(Clone, Debug, PartialEq, DynAny)]
enum RasterFrame { enum RasterFrame {
/// A CPU-based 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 /// A CPU-based 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
@ -416,7 +442,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
pub struct OldImageFrame<P: Pixel> { pub struct OldImageFrame<P: Pixel> {
image: Image<P>, image: Image<P>,
transform: DAffine2, transform: DAffine2,
alpha_blending: AlphaBlending, alpha_blending: LegacyAlphaBlending,
} }
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@ -1,6 +1,5 @@
use crate::render_ext::RenderExt; use crate::render_ext::RenderExt;
use crate::to_peniko::BlendModeExt; use crate::to_peniko::BlendModeExt;
use core_types::AlphaBlending;
use core_types::CacheHash; use core_types::CacheHash;
use core_types::blending::BlendMode; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
@ -11,7 +10,8 @@ use core_types::table::{Table, TableRow};
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid}; use core_types::uuid::{NodeId, generate_uuid};
use core_types::{ use core_types::{
ATTR_ALPHA_BLENDING, ATTR_BACKGROUND, ATTR_CLIP, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_SPREAD_METHOD, ATTR_TRANSFORM, ATTR_BACKGROUND, ATTR_BLEND_MODE, ATTR_CLIP, ATTR_CLIPPING_MASK, ATTR_DIMENSIONS, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_GRADIENT_TYPE, ATTR_LOCATION, ATTR_OPACITY,
ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_TRANSFORM,
}; };
use dyn_any::DynAny; use dyn_any::DynAny;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@ -686,7 +686,9 @@ impl Render for Table<Graphic> {
for index in 0..self.len() { for index in 0..self.len() {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
render.parent_tag( render.parent_tag(
@ -697,13 +699,13 @@ impl Render for Table<Graphic> {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push(ATTR_TRANSFORM, matrix);
} }
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode.render());
} }
let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled(); let next_clips = index + 1 < self.len() && self.element(index + 1).unwrap().had_clip_enabled();
@ -741,19 +743,21 @@ impl Render for Table<Graphic> {
for index in 0..self.len() { for index in 0..self.len() {
let row_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let row_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let transform = transform * row_transform; let transform = transform * row_transform;
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let element = self.element(index).unwrap(); let element = self.element(index).unwrap();
let mut layer = false; let mut layer = false;
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(), _ => blend_mode_attr.to_peniko(),
}; };
let mut bounds = RenderBoundingBox::None; let mut bounds = RenderBoundingBox::None;
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) { if opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default()) {
bounds = element.bounding_box(transform, true); bounds = element.bounding_box(transform, true);
if let RenderBoundingBox::Rectangle(bounds) = bounds { if let RenderBoundingBox::Rectangle(bounds) = bounds {
@ -882,7 +886,10 @@ impl Render for Table<Vector> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue }; let Some(vector) = self.element(index) else { continue };
let multiplied_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let multiplied_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let clipping_mask_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index);
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform // Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = vector.style.stroke().filter(|stroke| stroke.weight() > 0.);
@ -948,7 +955,10 @@ impl Render for Table<Vector> {
let vector_row = Table::new_from_row( let vector_row = Table::new_from_row(
TableRow::new_from_element(cloned_vector) TableRow::new_from_element(cloned_vector)
.with_attribute(ATTR_TRANSFORM, multiplied_transform) .with_attribute(ATTR_TRANSFORM, multiplied_transform)
.with_attribute(ATTR_ALPHA_BLENDING, alpha_blending), .with_attribute(ATTR_BLEND_MODE, blend_mode_attr)
.with_attribute(ATTR_OPACITY, opacity_attr)
.with_attribute(ATTR_OPACITY_FILL, opacity_fill_attr)
.with_attribute(ATTR_CLIPPING_MASK, clipping_mask_attr),
); );
(id, mask_type, vector_row) (id, mask_type, vector_row)
@ -1034,13 +1044,13 @@ impl Render for Table<Vector> {
attributes.push("fill-rule", "evenodd"); attributes.push("fill-rule", "evenodd");
} }
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode_attr != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode_attr.render());
} }
}); });
@ -1076,7 +1086,10 @@ impl Render for Table<Vector> {
let Some(element) = self.element(index) else { continue }; let Some(element) = self.element(index) else { continue };
let row_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let row_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let clip_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index);
let multiplied_transform = parent_transform * row_transform; let multiplied_transform = parent_transform * row_transform;
let has_real_stroke = element.style.stroke().filter(|stroke| stroke.weight() > 0.); let has_real_stroke = element.style.stroke().filter(|stroke| stroke.weight() > 0.);
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.); let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
@ -1105,12 +1118,12 @@ impl Render for Table<Vector> {
// If we're using opacity or a blend mode, we need to push a layer // If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(), _ => blend_mode_attr.to_peniko(),
}; };
let mut layer = false; let mut layer = false;
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { if opacity < 1. || blend_mode_attr != BlendMode::default() {
layer = true; layer = true;
let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width); let weight = element.style.stroke().as_ref().map_or(0., Stroke::effective_width);
let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform)); let quad = Quad::from_box(layer_bounds).inflate(weight * max_scale(applied_stroke_transform));
@ -1264,7 +1277,10 @@ impl Render for Table<Vector> {
let vector_table = Table::new_from_row( let vector_table = Table::new_from_row(
TableRow::new_from_element(cloned_element) TableRow::new_from_element(cloned_element)
.with_attribute(ATTR_TRANSFORM, row_transform) .with_attribute(ATTR_TRANSFORM, row_transform)
.with_attribute(ATTR_ALPHA_BLENDING, alpha_blending), .with_attribute(ATTR_BLEND_MODE, blend_mode_attr)
.with_attribute(ATTR_OPACITY, opacity_attr)
.with_attribute(ATTR_OPACITY_FILL, opacity_fill_attr)
.with_attribute(ATTR_CLIPPING_MASK, clip_attr),
); );
let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds);
@ -1442,7 +1458,9 @@ impl Render for Table<Raster<CPU>> {
let Some(image) = self.element(index) else { continue }; let Some(image) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
if image.data.is_empty() { if image.data.is_empty() {
continue; continue;
@ -1469,13 +1487,13 @@ impl Render for Table<Raster<CPU>> {
attributes.push("width", size.x.to_string()); attributes.push("width", size.x.to_string());
attributes.push("height", size.y.to_string()); attributes.push("height", size.y.to_string());
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode_attr != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode_attr.render());
} }
}, },
|render| { |render| {
@ -1509,12 +1527,12 @@ impl Render for Table<Raster<CPU>> {
attributes.push(ATTR_TRANSFORM, matrix); attributes.push(ATTR_TRANSFORM, matrix);
} }
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode_attr != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode_attr.render());
} }
}); });
} }
@ -1528,13 +1546,15 @@ impl Render for Table<Raster<CPU>> {
continue; continue;
} }
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let blend_mode = alpha_blending.blend_mode.to_peniko(); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let mut layer = false; let mut layer = false;
if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default())) if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && blend_mode_attr != BlendMode::default()))
&& let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, false) && let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, false)
{ {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
@ -1615,20 +1635,25 @@ impl Render for Table<Raster<GPU>> {
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
for index in 0..self.len() { for index in 0..self.len() {
let Some(raster) = self.element(index) else { continue }; let Some(raster) = self.element(index) else { continue };
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let clip_attr: bool = self.attribute_cloned_or_default(ATTR_CLIPPING_MASK, index);
let blend_mode = match render_params.render_mode { let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal, RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(), _ => blend_mode_attr.to_peniko(),
}; };
let mut layer = false; let mut layer = false;
if (render_params.render_mode != RenderMode::Outline && alpha_blending != Default::default()) let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let any_nondefault = blend_mode_attr != BlendMode::default() || opacity < 1. || clip_attr;
if (render_params.render_mode != RenderMode::Outline && any_nondefault)
&& let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, true) && let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, true)
{ {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
scene.push_layer(peniko::Fill::NonZero, blending, alpha_blending.opacity, kurbo::Affine::IDENTITY, &rect); scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::IDENTITY, &rect);
layer = true; layer = true;
} }
@ -1703,7 +1728,10 @@ impl Render for Table<Raster<GPU>> {
// later replace with the current viewport transform before each render. // later replace with the current viewport transform before each render.
impl Render for Table<Color> { impl Render for Table<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) { for (index, color) in self.iter_element_values().enumerate() {
let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
render.leaf_tag("polyline", |attributes| { render.leaf_tag("polyline", |attributes| {
// Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses // Stand-in for an infinite background. Chrome's SVG renderer keeps internal coordinates in f32 and loses
// precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning. // precision past ~2^24 (~16.7 million), causing tile-boundary artifacts that pop in and out during panning.
@ -1716,13 +1744,13 @@ impl Render for Table<Color> {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
} }
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode.render());
} }
}); });
} }
@ -1731,16 +1759,19 @@ impl Render for Table<Color> {
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
use vello::peniko; use vello::peniko;
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) { for (index, color) in self.iter_element_values().enumerate() {
let blend_mode = alpha_blending.blend_mode.to_peniko(); let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let blend_mode = blend_mode_attr.to_peniko();
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]); let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)); let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
let mut layer = false; let mut layer = false;
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { if opacity < 1. || blend_mode_attr != BlendMode::default() {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect); scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
layer = true; layer = true;
@ -1770,7 +1801,9 @@ impl Render for Table<GradientStops> {
for index in 0..self.len() { for index in 0..self.len() {
let Some(gradient) = self.element(index) else { continue }; let Some(gradient) = self.element(index) else { continue };
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index); let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index); let blend_mode: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index); let spread_method: GradientSpreadMethod = self.attribute_cloned_or_default(ATTR_SPREAD_METHOD, index);
let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index); let gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" }; let tag = if thumbnail_rect.is_some() { "rect" } else { "polyline" };
@ -1834,13 +1867,13 @@ impl Render for Table<GradientStops> {
attributes.push("fill", format!("url('#{gradient_id}')")); attributes.push("fill", format!("url('#{gradient_id}')"));
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. { if opacity < 1. {
attributes.push("opacity", opacity.to_string()); attributes.push("opacity", opacity.to_string());
} }
if alpha_blending.blend_mode != BlendMode::default() { if blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render()); attributes.push("style", blend_mode.render());
} }
}); });
} }
@ -1853,17 +1886,20 @@ impl Render for Table<GradientStops> {
return; return;
} }
for ((((gradient, transform), alpha_blending), spread_method), gradient_type) in self for (((index, gradient), spread_method), gradient_type) in self
.iter_element_values() .iter_element_values()
.zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) .enumerate()
.zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING))
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD)) .zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD))
.zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE)) .zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE))
{ {
let transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let blend_mode_attr: BlendMode = self.attribute_cloned_or_default(ATTR_BLEND_MODE, index);
let opacity_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY, index, 1.);
let opacity_fill_attr: f64 = self.attribute_cloned_or(ATTR_OPACITY_FILL, index, 1.);
let gradient_transform = parent_transform * transform; let gradient_transform = parent_transform * transform;
let blend_mode = alpha_blending.blend_mode.to_peniko(); let blend_mode = blend_mode_attr.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
let mut stops: peniko::ColorStops = peniko::ColorStops::new(); let mut stops: peniko::ColorStops = peniko::ColorStops::new();
for (position, color, _) in gradient.interpolated_samples() { for (position, color, _) in gradient.interpolated_samples() {
@ -1907,7 +1943,7 @@ impl Render for Table<GradientStops> {
let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.)); let rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
let mut layer = false; let mut layer = false;
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() { if opacity < 1. || blend_mode_attr != BlendMode::default() {
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
// See implementation in `Table<Color>` for more detail // See implementation in `Table<Color>` for more detail
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect); scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);

View File

@ -1,8 +1,7 @@
//! Contains stylistic options for SVG elements. //! Contains stylistic options for SVG elements.
pub use crate::gradient::*; pub use crate::gradient::*;
use core_types::ATTR_ALPHA_BLENDING; use core_types::ATTR_OPACITY;
use core_types::AlphaBlending;
use core_types::Color; use core_types::Color;
use core_types::color::Alpha; use core_types::color::Alpha;
use core_types::table::Table; use core_types::table::Table;
@ -136,9 +135,9 @@ impl From<Option<Color>> for Fill {
impl From<Table<Color>> for Fill { impl From<Table<Color>> for Fill {
fn from(color: Table<Color>) -> Fill { fn from(color: Table<Color>) -> Fill {
let alpha = color.attribute_cloned_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING, 0).opacity; let alpha: f64 = color.attribute_cloned_or(ATTR_OPACITY, 0, 1.);
let color = color.element(0).copied(); let color = color.element(0).copied();
Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha))) Fill::solid_or_none(color.map(|c| c.with_alpha(c.alpha() * alpha as f32)))
} }
} }

View File

@ -1,7 +1,6 @@
use core_types::AlphaBlending;
use core_types::registry::types::Percentage; use core_types::registry::types::Percentage;
use core_types::table::Table; use core_types::table::Table;
use core_types::{ATTR_ALPHA_BLENDING, BlendMode, Color, Ctx}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, BlendMode, Color, Ctx};
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::raster_types::{CPU, Raster}; use graphic_types::raster_types::{CPU, Raster};
@ -16,39 +15,42 @@ impl MultiplyAlpha for Color {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
} }
} }
fn multiply_table_attribute<T>(table: &mut Table<T>, key: &str, factor: f64) {
if let Some(values) = table.iter_attribute_values_mut::<f64>(key) {
for v in values {
*v *= factor;
}
} else {
for v in table.iter_attribute_values_mut_or_default::<f64>(key) {
*v = factor;
}
}
}
impl MultiplyAlpha for Table<Vector> { impl MultiplyAlpha for Table<Vector> {
fn multiply_alpha(&mut self, factor: f64) { fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY, factor);
a.opacity *= factor as f32;
}
} }
} }
impl MultiplyAlpha for Table<Graphic> { impl MultiplyAlpha for Table<Graphic> {
fn multiply_alpha(&mut self, factor: f64) { fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY, factor);
a.opacity *= factor as f32;
}
} }
} }
impl MultiplyAlpha for Table<Raster<CPU>> { impl MultiplyAlpha for Table<Raster<CPU>> {
fn multiply_alpha(&mut self, factor: f64) { fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY, factor);
a.opacity *= factor as f32;
}
} }
} }
impl MultiplyAlpha for Table<Color> { impl MultiplyAlpha for Table<Color> {
fn multiply_alpha(&mut self, factor: f64) { fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY, factor);
a.opacity *= factor as f32;
}
} }
} }
impl MultiplyAlpha for Table<GradientStops> { impl MultiplyAlpha for Table<GradientStops> {
fn multiply_alpha(&mut self, factor: f64) { fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY, factor);
a.opacity *= factor as f32;
}
} }
} }
@ -62,37 +64,27 @@ impl MultiplyFill for Color {
} }
impl MultiplyFill for Table<Vector> { impl MultiplyFill for Table<Vector> {
fn multiply_fill(&mut self, factor: f64) { fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
a.fill *= factor as f32;
}
} }
} }
impl MultiplyFill for Table<Graphic> { impl MultiplyFill for Table<Graphic> {
fn multiply_fill(&mut self, factor: f64) { fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
a.fill *= factor as f32;
}
} }
} }
impl MultiplyFill for Table<Raster<CPU>> { impl MultiplyFill for Table<Raster<CPU>> {
fn multiply_fill(&mut self, factor: f64) { fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
a.fill *= factor as f32;
}
} }
} }
impl MultiplyFill for Table<Color> { impl MultiplyFill for Table<Color> {
fn multiply_fill(&mut self, factor: f64) { fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
a.fill *= factor as f32;
}
} }
} }
impl MultiplyFill for Table<GradientStops> { impl MultiplyFill for Table<GradientStops> {
fn multiply_fill(&mut self, factor: f64) { fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
a.fill *= factor as f32;
}
} }
} }
@ -100,39 +92,35 @@ trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode); fn set_blend_mode(&mut self, blend_mode: BlendMode);
} }
fn set_table_blend_mode<T>(table: &mut Table<T>, blend_mode: BlendMode) {
for v in table.iter_attribute_values_mut_or_default::<BlendMode>(ATTR_BLEND_MODE) {
*v = blend_mode;
}
}
impl SetBlendMode for Table<Vector> { impl SetBlendMode for Table<Vector> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) { fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_blend_mode(self, blend_mode);
a.blend_mode = blend_mode;
}
} }
} }
impl SetBlendMode for Table<Graphic> { impl SetBlendMode for Table<Graphic> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) { fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_blend_mode(self, blend_mode);
a.blend_mode = blend_mode;
}
} }
} }
impl SetBlendMode for Table<Raster<CPU>> { impl SetBlendMode for Table<Raster<CPU>> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) { fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_blend_mode(self, blend_mode);
a.blend_mode = blend_mode;
}
} }
} }
impl SetBlendMode for Table<Color> { impl SetBlendMode for Table<Color> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) { fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_blend_mode(self, blend_mode);
a.blend_mode = blend_mode;
}
} }
} }
impl SetBlendMode for Table<GradientStops> { impl SetBlendMode for Table<GradientStops> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) { fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_blend_mode(self, blend_mode);
a.blend_mode = blend_mode;
}
} }
} }
@ -140,39 +128,35 @@ trait SetClip {
fn set_clip(&mut self, clip: bool); fn set_clip(&mut self, clip: bool);
} }
fn set_table_clip<T>(table: &mut Table<T>, clip: bool) {
for v in table.iter_attribute_values_mut_or_default::<bool>(ATTR_CLIPPING_MASK) {
*v = clip;
}
}
impl SetClip for Table<Vector> { impl SetClip for Table<Vector> {
fn set_clip(&mut self, clip: bool) { fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_clip(self, clip);
a.clip = clip;
}
} }
} }
impl SetClip for Table<Graphic> { impl SetClip for Table<Graphic> {
fn set_clip(&mut self, clip: bool) { fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_clip(self, clip);
a.clip = clip;
}
} }
} }
impl SetClip for Table<Raster<CPU>> { impl SetClip for Table<Raster<CPU>> {
fn set_clip(&mut self, clip: bool) { fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_clip(self, clip);
a.clip = clip;
}
} }
} }
impl SetClip for Table<Color> { impl SetClip for Table<Color> {
fn set_clip(&mut self, clip: bool) { fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_clip(self, clip);
a.clip = clip;
}
} }
} }
impl SetClip for Table<GradientStops> { impl SetClip for Table<GradientStops> {
fn set_clip(&mut self, clip: bool) { fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) { set_table_clip(self, clip);
a.clip = clip;
}
} }
} }

View File

@ -10,8 +10,8 @@ use core_types::table::{Table, TableRow};
use core_types::transform::Transform; use core_types::transform::Transform;
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::value::ClonedNode; use core_types::value::ClonedNode;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
use core_types::{AlphaBlending, Ctx, Node}; use core_types::{Ctx, Node};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors; use raster_nodes::blending_nodes::blend_colors;
use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds}; use raster_nodes::std_nodes::{empty_image, extend_image_to_bounds};
@ -314,12 +314,18 @@ async fn brush(
} }
let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM);
let alpha_blending: AlphaBlending = actual_image.attribute_cloned_or_default(ATTR_ALPHA_BLENDING); let blend_mode: BlendMode = actual_image.attribute_cloned_or_default(ATTR_BLEND_MODE);
let opacity: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY, 1.);
let fill: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
let clip: bool = actual_image.attribute_cloned_or_default(ATTR_CLIPPING_MASK);
let layer: Table<NodeId> = actual_image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH); let layer: Table<NodeId> = actual_image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH);
*image.element_mut(0).unwrap() = actual_image.into_element(); *image.element_mut(0).unwrap() = actual_image.into_element();
image.set_attribute(ATTR_TRANSFORM, 0, transform); image.set_attribute(ATTR_TRANSFORM, 0, transform);
image.set_attribute(ATTR_ALPHA_BLENDING, 0, alpha_blending); image.set_attribute(ATTR_BLEND_MODE, 0, blend_mode);
image.set_attribute(ATTR_OPACITY, 0, opacity);
image.set_attribute(ATTR_OPACITY_FILL, 0, fill);
image.set_attribute(ATTR_CLIPPING_MASK, 0, clip);
image.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer); image.set_attribute(ATTR_EDITOR_LAYER_PATH, 0, layer);
image image
@ -410,7 +416,7 @@ mod test {
let image = brush( let image = brush(
(), (),
Table::new_from_element(Raster::new_cpu(Image::<Color>::default())), Table::new_from_element(Raster::new_cpu(Image::<Color>::default())),
vec![BrushStroke { Table::new_from_element(BrushStroke {
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }], trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
style: BrushStyle { style: BrushStyle {
color: Color::BLACK, color: Color::BLACK,
@ -420,7 +426,7 @@ mod test {
spacing: 20., spacing: 20.,
blend_mode: BlendMode::Normal, blend_mode: BlendMode::Normal,
}, },
}], }),
BrushCache::default(), BrushCache::default(),
) )
.await; .await;

View File

@ -1,6 +1,6 @@
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, AlphaBlending, Color, Ctx}; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, BlendMode, Color, Ctx};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath}; use graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId; use graphic_types::vector_types::vector::PointId;
@ -179,7 +179,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
} }
Graphic::RasterCPU(image) => { Graphic::RasterCPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default(ATTR_TRANSFORM, index); let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let make_row = |transform, layer, alpha_blending| { let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@ -187,7 +187,10 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
element.style.set_fill(Fill::Solid(Color::BLACK)); element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow::new_from_element(element) TableRow::new_from_element(element)
.with_attribute(ATTR_ALPHA_BLENDING, alpha_blending) .with_attribute(ATTR_BLEND_MODE, blend_mode)
.with_attribute(ATTR_OPACITY, opacity)
.with_attribute(ATTR_OPACITY_FILL, fill)
.with_attribute(ATTR_CLIPPING_MASK, clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer)
}; };
@ -198,14 +201,17 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
let layer: Table<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); let layer: Table<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, i); let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
make_row(parent_transform * row_transform, layer, alpha_blending) let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.);
let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i);
make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
Graphic::RasterGPU(image) => { Graphic::RasterGPU(image) => {
let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default(ATTR_TRANSFORM, index); let parent_transform: DAffine2 = graphic_table.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let make_row = |transform, layer, alpha_blending| { let make_item = |transform, layer, blend_mode: BlendMode, opacity: f64, fill: f64, clip: bool| {
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE); let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform); subpath.apply_transform(transform);
@ -213,7 +219,10 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
element.style.set_fill(Fill::Solid(Color::BLACK)); element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow::new_from_element(element) TableRow::new_from_element(element)
.with_attribute(ATTR_ALPHA_BLENDING, alpha_blending) .with_attribute(ATTR_BLEND_MODE, blend_mode)
.with_attribute(ATTR_OPACITY, opacity)
.with_attribute(ATTR_OPACITY_FILL, fill)
.with_attribute(ATTR_CLIPPING_MASK, clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer)
}; };
@ -224,8 +233,11 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
.map(|i| { .map(|i| {
let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i); let row_transform: DAffine2 = image.attribute_cloned_or_default(ATTR_TRANSFORM, i);
let layer: Table<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i); let layer: Table<NodeId> = image.attribute_cloned_or_default(ATTR_EDITOR_LAYER_PATH, i);
let alpha_blending: AlphaBlending = image.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, i); let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
make_row(parent_transform * row_transform, layer, alpha_blending) let opacity: f64 = image.attribute_cloned_or(ATTR_OPACITY, i, 1.);
let fill: f64 = image.attribute_cloned_or(ATTR_OPACITY_FILL, i, 1.);
let clip: bool = image.attribute_cloned_or_default(ATTR_CLIPPING_MASK, i);
make_item(parent_transform * row_transform, layer, blend_mode, opacity, fill, clip)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View File

@ -1,12 +1,11 @@
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType}; use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
use core_types::blending::AlphaBlending; use core_types::ATTR_TRANSFORM;
use core_types::color::Color; use core_types::color::Color;
use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut}; use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use core_types::context::{Ctx, ExtractFootprint}; use core_types::context::{Ctx, ExtractFootprint};
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::transform::Transform; use core_types::transform::Transform;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_TRANSFORM};
use dyn_any::DynAny; use dyn_any::DynAny;
use fastnoise_lite; use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2}; use glam::{DAffine2, DVec2, Vec2};
@ -284,7 +283,6 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: Table<Color>) -> Tab
let mut result_table = Table::new_from_element(Raster::new_cpu(image)); let mut result_table = Table::new_from_element(Raster::new_cpu(image));
result_table.set_attribute(ATTR_TRANSFORM, 0, transform); result_table.set_attribute(ATTR_TRANSFORM, 0, transform);
result_table.set_attribute(ATTR_ALPHA_BLENDING, 0, AlphaBlending::default());
// Callers of empty_image can safely unwrap on returned `Table` // Callers of empty_image can safely unwrap on returned `Table`
result_table result_table

View File

@ -1,13 +1,16 @@
use core::cmp::Ordering; use core::cmp::Ordering;
use core::f64::consts::{PI, TAU}; use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher}; use core::hash::{Hash, Hasher};
use core_types::AlphaBlending; use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue}; use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::table::{Table, TableRow}; use core_types::table::{Table, TableRow};
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::NodeId; use core_types::uuid::NodeId;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll, OwnedContextImpl}; use core_types::{
ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, CloneVarArgs, Color, Context, Ctx, ExtractAll,
OwnedContextImpl,
};
use glam::{DAffine2, DMat2, DVec2}; use glam::{DAffine2, DMat2, DVec2};
use graphic_types::Vector; use graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster}; use graphic_types::raster_types::{CPU, GPU, Raster};
@ -2310,10 +2313,20 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
return content; return content;
}; };
// Lerp styles // Lerp blending attributes: opacity/fill interpolate, blend_mode/clip step at the midpoint
let source_alpha_blending: AlphaBlending = content.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, source_index); let source_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, source_index);
let target_alpha_blending: AlphaBlending = content.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, target_index); let target_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, target_index);
let vector_alpha_blending = source_alpha_blending.lerp(&target_alpha_blending, time as f32); let source_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, source_index, 1.);
let target_opacity: f64 = content.attribute_cloned_or(ATTR_OPACITY, target_index, 1.);
let source_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, source_index, 1.);
let target_fill: f64 = content.attribute_cloned_or(ATTR_OPACITY_FILL, target_index, 1.);
let source_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, source_index);
let target_clip: bool = content.attribute_cloned_or_default(ATTR_CLIPPING_MASK, target_index);
let lerped_blend_mode = if time < 0.5 { source_blend_mode } else { target_blend_mode };
let lerped_opacity = source_opacity + (target_opacity - source_opacity) * time;
let lerped_fill = source_fill + (target_fill - source_fill) * time;
let lerped_clip = if time < 0.5 { source_clip } else { target_clip };
// Evaluate the spatial position on the control path for the translation component. // Evaluate the spatial position on the control path for the translation component.
// When the segment has zero arc length (e.g., two objects at the same position), inv_arclen // When the segment has zero arc length (e.g., two objects at the same position), inv_arclen
@ -2534,7 +2547,10 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
Table::new_from_row( Table::new_from_row(
TableRow::new_from_element(vector) TableRow::new_from_element(vector)
.with_attribute(ATTR_TRANSFORM, lerped_transform) .with_attribute(ATTR_TRANSFORM, lerped_transform)
.with_attribute(ATTR_ALPHA_BLENDING, vector_alpha_blending) .with_attribute(ATTR_BLEND_MODE, lerped_blend_mode)
.with_attribute(ATTR_OPACITY, lerped_opacity)
.with_attribute(ATTR_OPACITY_FILL, lerped_fill)
.with_attribute(ATTR_CLIPPING_MASK, lerped_clip)
.with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path) .with_attribute(ATTR_EDITOR_LAYER_PATH, layer_path)
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_table_content), .with_attribute(ATTR_EDITOR_MERGED_LAYERS, graphic_table_content),
) )