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
/// 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.
/// Uses `Display` instead of `Debug` for attribute types that have a nicer human-readable format.
fn display_value_override(any: &dyn Any) -> Option<String> {
if let Some(value) = any.downcast_ref::<BlendMode>() {
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 {
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);
}
@ -449,26 +449,45 @@ impl<'a> ModifyInputsContext<'a> {
return;
};
// 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(InputConnector::node(opacity_node_id, 2), NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
self.set_input_with_refresh(
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) {
// 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 existing = self.existing_proto_node_id(identifier.clone(), false);
let existed = existing.is_some();
let Some(opacity_node_id) = existing.or_else(|| self.existing_proto_node_id(identifier, true)) else {
return;
};
// Disable the opacity component on a freshly-created node so the slider only affects fill, mirroring the opacity-slider case
// (where the node's default `has_fill = false` already keeps fill out of the picture)
// Freshly-created node defaults to opacity enabled; disable it so the fill slider works independently
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
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), false);
self.set_input_with_refresh(InputConnector::node(opacity_node_id, 4), NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
self.set_input_with_refresh(
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.
@ -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 {
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);
}

View File

@ -34,8 +34,8 @@ use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
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_START, ATTR_TRANSFORM, ATTR_TYPE,
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_OPACITY, ATTR_OPACITY_FILL, ATTR_SPREAD_METHOD, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
};
#[cfg(feature = "wasm")]
pub use tsify;

View File

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

View File

@ -1,5 +1,5 @@
use crate::graphic::Graphic;
use core_types::blending::AlphaBlending;
use core_types::blending::BlendMode;
use core_types::table::{Table, TableRow};
use core_types::uuid::NodeId;
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> {
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
/// 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).
@ -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"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[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::graphene_hash::CacheHash;
use core_types::ops::TableConvert;
use core_types::render_complexity::RenderComplexity;
use core_types::table::{Table, TableRow};
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 glam::DAffine2;
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`)
/// 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 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>>) {
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 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() {
// 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) => {
for index in 0..sub_table.len() {
let child_transform: DAffine2 = sub_table.attribute_cloned_or_default(ATTR_TRANSFORM, index);
let child_alpha_blending: AlphaBlending = sub_table.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index);
// Identity default means a missing column still composes correctly
for v in sub_table.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
*v = current_transform * *v;
}
sub_table.set_attribute(ATTR_TRANSFORM, index, current_transform * child_transform);
sub_table.set_attribute(ATTR_ALPHA_BLENDING, index, compose_alpha_blending(current_alpha_blending, child_alpha_blending));
// f64 defaults to 0, but opacity/fill default to 1, so missing columns must be set rather than multiplied
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);
}
// 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 => {
if let Some(typed_table) = extract_variant(other) {
for row in typed_table.into_iter() {
let row_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
let row_alpha_blending: AlphaBlending = row.attribute_cloned_or_default(ATTR_ALPHA_BLENDING);
let (element, mut attributes) = row.into_parts();
for mut item in typed_table.into_iter() {
let row_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM);
let row_opacity: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.);
let row_fill: f64 = item.attribute_cloned_or(ATTR_OPACITY_FILL, 1.);
attributes.insert(ATTR_TRANSFORM, current_transform * row_transform);
attributes.insert(ATTR_ALPHA_BLENDING, compose_alpha_blending(current_alpha_blending, row_alpha_blending));
attributes.insert(ATTR_EDITOR_LAYER_PATH, layer_path.clone());
item.set_attribute(ATTR_TRANSFORM, current_transform * row_transform);
item.set_attribute(ATTR_OPACITY, current_opacity * row_opacity);
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 {
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 {
Graphic::Vector(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 {
match self {
Graphic::Vector(vector) => vector
.iter_element_values()
.zip(vector.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING))
.all(|(element, alpha_blending)| {
(alpha_blending.opacity > 1. - f32::EPSILON) && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
Graphic::Vector(vector) => (0..vector.len()).all(|index| {
let Some(element) = vector.element(index) else { return false };
let opacity: f64 = vector.attribute_cloned_or(ATTR_OPACITY, index, 1.);
opacity > 1. - f64::EPSILON && element.style.fill().is_opaque() && element.style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
}),
_ => 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> {
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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldGraphicGroup {
elements: Vec<(Graphic, Option<NodeId>)>,
transform: DAffine2,
alpha_blending: AlphaBlending,
alpha_blending: LegacyAlphaBlending,
}
#[derive(Clone, Debug, PartialEq, DynAny, Default)]
#[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"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[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 mod migrations {
use core_types::{
AlphaBlending,
table::{Table, TableRow},
};
use core_types::blending::BlendMode;
use core_types::table::{Table, TableRow};
use dyn_any::DynAny;
use glam::DAffine2;
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> {
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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
pub alpha_blending: LegacyAlphaBlending,
pub style: PathStyle,
@ -47,7 +56,7 @@ pub mod migrations {
#[cfg_attr(feature = "serde", serde(alias = "instances", alias = "instance"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[derive(Clone, Debug)]

View File

@ -1,63 +1,6 @@
use core::fmt::Display;
use node_macro::BufferStruct;
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)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]

View File

@ -1,8 +1,9 @@
use crate::raster_types::{CPU, Raster};
use crate::{Bitmap, BitmapMut};
use core_types::blending::BlendMode;
use core_types::color::float_to_srgb_u8;
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 core_types::color::*;
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> {
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)]
enum RasterFrame {
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> {
image: Image<P>,
transform: DAffine2,
alpha_blending: AlphaBlending,
alpha_blending: LegacyAlphaBlending,
}
#[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"))]
element: Vec<T>,
transform: Vec<DAffine2>,
alpha_blending: Vec<AlphaBlending>,
alpha_blending: Vec<LegacyAlphaBlending>,
}
#[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 }) => {
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_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
}
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> {
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)]
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
@ -416,7 +442,7 @@ pub fn migrate_image_frame_row<'de, D: serde::Deserializer<'de>>(deserializer: D
pub struct OldImageFrame<P: Pixel> {
image: Image<P>,
transform: DAffine2,
alpha_blending: AlphaBlending,
alpha_blending: LegacyAlphaBlending,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@ -1,6 +1,5 @@
use crate::render_ext::RenderExt;
use crate::to_peniko::BlendModeExt;
use core_types::AlphaBlending;
use core_types::CacheHash;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
@ -11,7 +10,8 @@ use core_types::table::{Table, TableRow};
use core_types::transform::Footprint;
use core_types::uuid::{NodeId, generate_uuid};
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 glam::{DAffine2, DVec2};
@ -686,7 +686,9 @@ impl Render for Table<Graphic> {
for index in 0..self.len() {
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();
render.parent_tag(
@ -697,13 +699,13 @@ impl Render for Table<Graphic> {
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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode != BlendMode::default() {
attributes.push("style", blend_mode.render());
}
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() {
let row_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM, index);
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 mut layer = false;
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(),
_ => blend_mode_attr.to_peniko(),
};
let mut bounds = RenderBoundingBox::None;
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
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 && blend_mode_attr != BlendMode::default()) {
bounds = element.bounding_box(transform, true);
if let RenderBoundingBox::Rectangle(bounds) = bounds {
@ -882,7 +886,10 @@ impl Render for Table<Vector> {
for index in 0..self.len() {
let Some(vector) = self.element(index) else { continue };
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
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(
TableRow::new_from_element(cloned_vector)
.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)
@ -1034,13 +1044,13 @@ impl Render for Table<Vector> {
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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode_attr != BlendMode::default() {
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 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 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.);
@ -1105,12 +1118,12 @@ impl Render for Table<Vector> {
// If we're using opacity or a blend mode, we need to push a layer
let blend_mode = match render_params.render_mode {
RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(),
_ => blend_mode_attr.to_peniko(),
};
let mut layer = false;
let opacity = alpha_blending.opacity(render_params.for_mask);
if opacity < 1. || alpha_blending.blend_mode != BlendMode::default() {
let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32;
if opacity < 1. || blend_mode_attr != BlendMode::default() {
layer = true;
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));
@ -1264,7 +1277,10 @@ impl Render for Table<Vector> {
let vector_table = Table::new_from_row(
TableRow::new_from_element(cloned_element)
.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);
@ -1442,7 +1458,9 @@ impl Render for Table<Raster<CPU>> {
let Some(image) = self.element(index) else { continue };
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() {
continue;
@ -1469,13 +1487,13 @@ impl Render for Table<Raster<CPU>> {
attributes.push("width", size.x.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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
},
|render| {
@ -1509,12 +1527,12 @@ impl Render for Table<Raster<CPU>> {
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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode_attr != BlendMode::default() {
attributes.push("style", blend_mode_attr.render());
}
});
}
@ -1528,13 +1546,15 @@ impl Render for Table<Raster<CPU>> {
continue;
}
let alpha_blending: AlphaBlending = self.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, index);
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_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;
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 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) {
for index in 0..self.len() {
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 {
RenderMode::Outline => peniko::Mix::Normal,
_ => alpha_blending.blend_mode.to_peniko(),
_ => blend_mode_attr.to_peniko(),
};
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 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);
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;
}
@ -1703,7 +1728,10 @@ impl Render for Table<Raster<GPU>> {
// later replace with the current viewport transform before each render.
impl Render for Table<Color> {
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| {
// 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.
@ -1716,13 +1744,13 @@ impl Render for Table<Color> {
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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode != BlendMode::default() {
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) {
use vello::peniko;
for (color, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) {
let blend_mode = alpha_blending.blend_mode.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask);
for (index, color) in self.iter_element_values().enumerate() {
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 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 rect = kurbo::Rect::from_origin_size(kurbo::Point::ZERO, kurbo::Size::new(1., 1.));
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);
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
layer = true;
@ -1770,7 +1801,9 @@ impl Render for Table<GradientStops> {
for index in 0..self.len() {
let Some(gradient) = self.element(index) else { continue };
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 gradient_type: GradientType = self.attribute_cloned_or_default(ATTR_GRADIENT_TYPE, index);
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}')"));
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. {
attributes.push("opacity", opacity.to_string());
}
if alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", alpha_blending.blend_mode.render());
if blend_mode != BlendMode::default() {
attributes.push("style", blend_mode.render());
}
});
}
@ -1853,17 +1886,20 @@ impl Render for Table<GradientStops> {
return;
}
for ((((gradient, transform), alpha_blending), spread_method), gradient_type) in self
for (((index, gradient), spread_method), gradient_type) in self
.iter_element_values()
.zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM))
.zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING))
.enumerate()
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD))
.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 blend_mode = alpha_blending.blend_mode.to_peniko();
let opacity = alpha_blending.opacity(render_params.for_mask);
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 mut stops: peniko::ColorStops = peniko::ColorStops::new();
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 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);
// See implementation in `Table<Color>` for more detail
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.
pub use crate::gradient::*;
use core_types::ATTR_ALPHA_BLENDING;
use core_types::AlphaBlending;
use core_types::ATTR_OPACITY;
use core_types::Color;
use core_types::color::Alpha;
use core_types::table::Table;
@ -136,9 +135,9 @@ impl From<Option<Color>> for Fill {
impl From<Table<Color>> for 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();
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::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::Vector;
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.))
}
}
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> {
fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.opacity *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY, factor);
}
}
impl MultiplyAlpha for Table<Graphic> {
fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.opacity *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY, factor);
}
}
impl MultiplyAlpha for Table<Raster<CPU>> {
fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.opacity *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY, factor);
}
}
impl MultiplyAlpha for Table<Color> {
fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.opacity *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY, factor);
}
}
impl MultiplyAlpha for Table<GradientStops> {
fn multiply_alpha(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.opacity *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY, factor);
}
}
@ -62,37 +64,27 @@ impl MultiplyFill for Color {
}
impl MultiplyFill for Table<Vector> {
fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.fill *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
}
}
impl MultiplyFill for Table<Graphic> {
fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.fill *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
}
}
impl MultiplyFill for Table<Raster<CPU>> {
fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.fill *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
}
}
impl MultiplyFill for Table<Color> {
fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.fill *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
}
}
impl MultiplyFill for Table<GradientStops> {
fn multiply_fill(&mut self, factor: f64) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.fill *= factor as f32;
}
multiply_table_attribute(self, ATTR_OPACITY_FILL, factor);
}
}
@ -100,39 +92,35 @@ trait SetBlendMode {
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> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.blend_mode = blend_mode;
}
set_table_blend_mode(self, blend_mode);
}
}
impl SetBlendMode for Table<Graphic> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.blend_mode = blend_mode;
}
set_table_blend_mode(self, blend_mode);
}
}
impl SetBlendMode for Table<Raster<CPU>> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.blend_mode = blend_mode;
}
set_table_blend_mode(self, blend_mode);
}
}
impl SetBlendMode for Table<Color> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.blend_mode = blend_mode;
}
set_table_blend_mode(self, blend_mode);
}
}
impl SetBlendMode for Table<GradientStops> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.blend_mode = blend_mode;
}
set_table_blend_mode(self, blend_mode);
}
}
@ -140,39 +128,35 @@ trait SetClip {
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> {
fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.clip = clip;
}
set_table_clip(self, clip);
}
}
impl SetClip for Table<Graphic> {
fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.clip = clip;
}
set_table_clip(self, clip);
}
}
impl SetClip for Table<Raster<CPU>> {
fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.clip = clip;
}
set_table_clip(self, clip);
}
}
impl SetClip for Table<Color> {
fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.clip = clip;
}
set_table_clip(self, clip);
}
}
impl SetClip for Table<GradientStops> {
fn set_clip(&mut self, clip: bool) {
for a in self.iter_attribute_values_mut_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING) {
a.clip = clip;
}
set_table_clip(self, clip);
}
}

View File

@ -10,8 +10,8 @@ use core_types::table::{Table, TableRow};
use core_types::transform::Transform;
use core_types::uuid::NodeId;
use core_types::value::ClonedNode;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM};
use core_types::{AlphaBlending, Ctx, Node};
use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_EDITOR_LAYER_PATH, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM};
use core_types::{Ctx, Node};
use glam::{DAffine2, DVec2};
use raster_nodes::blending_nodes::blend_colors;
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 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);
*image.element_mut(0).unwrap() = actual_image.into_element();
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
@ -410,7 +416,7 @@ mod test {
let image = brush(
(),
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 }],
style: BrushStyle {
color: Color::BLACK,
@ -420,7 +426,7 @@ mod test {
spacing: 20.,
blend_mode: BlendMode::Normal,
},
}],
}),
BrushCache::default(),
)
.await;

View File

@ -1,6 +1,6 @@
use core_types::table::{Table, TableRow};
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 graphic_types::vector_types::subpath::{ManipulatorGroup, Subpath};
use graphic_types::vector_types::vector::PointId;
@ -179,7 +179,7 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
}
Graphic::RasterCPU(image) => {
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);
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));
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)
};
@ -198,14 +201,17 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
.map(|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 alpha_blending: AlphaBlending = image.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, i);
make_row(parent_transform * row_transform, layer, alpha_blending)
let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
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<_>>()
}
Graphic::RasterGPU(image) => {
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);
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));
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)
};
@ -224,8 +233,11 @@ fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
.map(|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 alpha_blending: AlphaBlending = image.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, i);
make_row(parent_transform * row_transform, layer, alpha_blending)
let blend_mode: BlendMode = image.attribute_cloned_or_default(ATTR_BLEND_MODE, i);
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<_>>()
}

View File

@ -1,12 +1,11 @@
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::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
use core_types::context::{Ctx, ExtractFootprint};
use core_types::math::bbox::Bbox;
use core_types::table::{Table, TableRow};
use core_types::transform::Transform;
use core_types::{ATTR_ALPHA_BLENDING, ATTR_TRANSFORM};
use dyn_any::DynAny;
use fastnoise_lite;
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));
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`
result_table

View File

@ -1,13 +1,16 @@
use core::cmp::Ordering;
use core::f64::consts::{PI, TAU};
use core::hash::{Hash, Hasher};
use core_types::AlphaBlending;
use core_types::blending::BlendMode;
use core_types::bounds::{BoundingBox, RenderBoundingBox};
use core_types::registry::types::{Angle, Length, Multiplier, Percentage, PixelLength, Progression, SeedValue};
use core_types::table::{Table, TableRow};
use core_types::transform::{Footprint, Transform};
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 graphic_types::Vector;
use graphic_types::raster_types::{CPU, GPU, Raster};
@ -2310,10 +2313,20 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
return content;
};
// Lerp styles
let source_alpha_blending: AlphaBlending = content.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, source_index);
let target_alpha_blending: AlphaBlending = content.attribute_cloned_or_default(ATTR_ALPHA_BLENDING, target_index);
let vector_alpha_blending = source_alpha_blending.lerp(&target_alpha_blending, time as f32);
// Lerp blending attributes: opacity/fill interpolate, blend_mode/clip step at the midpoint
let source_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, source_index);
let target_blend_mode: BlendMode = content.attribute_cloned_or_default(ATTR_BLEND_MODE, target_index);
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.
// 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(
TableRow::new_from_element(vector)
.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_MERGED_LAYERS, graphic_table_content),
)