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
18 changed files with 374 additions and 295 deletions

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