mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add Table<GradientStops> gradient rendering (#3989)
* Add Table<GradientStops> gradient rendering * Add SVG and Vello renderers for Table<GradientStops> * Add thumbnail rendering for Table<GradientStops> * Use row transform to map (0,0), (1,0) unit line to document space * Set 100px width for the initially created gradient * Add support of table gradients for the gradient tool * Fix after review * Thumbnail rendering of artboard with infinite gradient layer * Hide radial gradient's reverse direction button for gradient table * Remove unused imports * Format * Fix conflict with spread method * Code review * Fix thumbnails * Connect up gradient_type and spread_method to attributes --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -3,15 +3,16 @@ use crate::application_io::PlatformEditorApi;
|
||||
use crate::proto::{Any as DAny, FutureAny};
|
||||
use brush_nodes::brush_cache::BrushCache;
|
||||
use brush_nodes::brush_stroke::BrushStroke;
|
||||
use core_types::table::Table;
|
||||
use core_types::table::{Table, TableRow};
|
||||
use core_types::transform::Footprint;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
|
||||
use core_types::{ATTR_TRANSFORM, CacheHash, Color, ContextFeatures, MemoHash, Node, Type};
|
||||
use dyn_any::DynAny;
|
||||
pub use dyn_any::StaticType;
|
||||
use glam::{Affine2, Vec2};
|
||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphic_types::raster_types::{CPU, Image, Raster};
|
||||
use graphic_types::vector_types::gradient::GRADIENT_TABLE_DEFAULT_SCALE;
|
||||
use graphic_types::vector_types::vector::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use graphic_types::vector_types::vector::{self, ReferencePoint};
|
||||
use graphic_types::{Graphic, Vector};
|
||||
@@ -118,7 +119,9 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<()>() => TaggedValue::None,
|
||||
// Table-wrapped types need a single-item default with the element's default, not an empty table
|
||||
x if x == TypeId::of::<Table<Color>>() => TaggedValue::Color(Table::new_from_element(Color::default())),
|
||||
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_element(GradientStops::default())),
|
||||
x if x == TypeId::of::<Table<GradientStops>>() => TaggedValue::GradientTable(Table::new_from_row(
|
||||
TableRow::new_from_element(GradientStops::default()).with_attribute(ATTR_TRANSFORM, DAffine2::from_scale(DVec2::splat(GRADIENT_TABLE_DEFAULT_SCALE))),
|
||||
)),
|
||||
$( x if x == TypeId::of::<$ty>() => TaggedValue::$identifier(Default::default()), )*
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -11,6 +11,15 @@ pub enum RenderBoundingBox {
|
||||
|
||||
pub trait BoundingBox {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
||||
|
||||
/// Returns the bounding box to use when sizing this value's thumbnail in the Layers panel.
|
||||
///
|
||||
/// Diverges from `bounding_box` for types where the rendering bounds wouldn't make a useful thumbnail frame.
|
||||
/// For instance, `GradientStops` is `Infinite` for rendering but returns the line's AABB here, so a `Table<Graphic>`
|
||||
/// group of a gradient and a vector frames around the vector's geometry rather than infinity.
|
||||
/// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a
|
||||
/// small fallback rectangle at the end if no finite bounds remain after combining.
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox;
|
||||
}
|
||||
|
||||
macro_rules! none_impl {
|
||||
@@ -19,6 +28,10 @@ macro_rules! none_impl {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
RenderBoundingBox::None
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
RenderBoundingBox::None
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -32,4 +45,9 @@ impl BoundingBox for Color {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
RenderBoundingBox::Infinite
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox {
|
||||
// A solid color has no intrinsic extent, so its container's other content frames the thumbnail
|
||||
RenderBoundingBox::Infinite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +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_LOCATION, ATTR_NAME, ATTR_START, ATTR_TRANSFORM, ATTR_TYPE,
|
||||
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,
|
||||
};
|
||||
#[cfg(feature = "wasm")]
|
||||
pub use tsify;
|
||||
|
||||
@@ -57,6 +57,14 @@ pub const ATTR_BACKGROUND: &str = "background";
|
||||
/// Attribute key for an artboard row's `bool` flag indicating whether content is clipped to the artboard 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).
|
||||
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).
|
||||
pub const ATTR_GRADIENT_TYPE: &str = "gradient_type";
|
||||
|
||||
// =====================
|
||||
// TRAIT: AttributeValue
|
||||
// =====================
|
||||
@@ -824,12 +832,12 @@ impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Table<T> {
|
||||
}
|
||||
|
||||
impl<T: BoundingBox> BoundingBox for Table<T> {
|
||||
/// Computes the combined bounding box of all rows, composing each row's transform attribute with the given transform.
|
||||
/// Computes the combined bounding box of all items, composing each item's transform attribute with the given transform.
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
let mut combined_bounds = None;
|
||||
|
||||
for (element, row_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
||||
match element.bounding_box(transform * row_transform, include_stroke) {
|
||||
for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
||||
match element.bounding_box(transform * item_transform, include_stroke) {
|
||||
RenderBoundingBox::None => continue,
|
||||
RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite,
|
||||
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
||||
@@ -844,6 +852,29 @@ impl<T: BoundingBox> BoundingBox for Table<T> {
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
// `Infinite` items are skipped here (rather than propagating outward as in `bounding_box`) so a finite sibling in a mixed group dictates the framing
|
||||
let mut combined_bounds = None;
|
||||
let mut any_infinite = false;
|
||||
|
||||
for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<DAffine2>(ATTR_TRANSFORM)) {
|
||||
match element.thumbnail_bounding_box(transform * item_transform, include_stroke) {
|
||||
RenderBoundingBox::None => continue,
|
||||
RenderBoundingBox::Infinite => any_infinite = true,
|
||||
RenderBoundingBox::Rectangle(bounds) => match combined_bounds {
|
||||
Some(existing) => combined_bounds = Some(Quad::combine_bounds(existing, bounds)),
|
||||
None => combined_bounds = Some(bounds),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
match (combined_bounds, any_infinite) {
|
||||
(Some(bounds), _) => RenderBoundingBox::Rectangle(bounds),
|
||||
(None, true) => RenderBoundingBox::Infinite,
|
||||
(None, false) => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for Table<T> {
|
||||
@@ -897,14 +928,14 @@ impl<T: PartialEq> PartialEq for Table<T> {
|
||||
}
|
||||
|
||||
impl<T> ApplyTransform for Table<T> {
|
||||
/// Right-multiplies the modification into each row's transform attribute.
|
||||
/// Right-multiplies the modification into each item's transform attribute.
|
||||
fn apply_transform(&mut self, modification: &DAffine2) {
|
||||
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*transform *= *modification;
|
||||
}
|
||||
}
|
||||
|
||||
/// Left-multiplies the modification into each row's transform attribute.
|
||||
/// Left-multiplies the modification into each item's transform attribute.
|
||||
fn left_apply_transform(&mut self, modification: &DAffine2) {
|
||||
for transform in self.iter_attribute_values_mut_or_default::<DAffine2>(ATTR_TRANSFORM) {
|
||||
*transform = *modification * *transform;
|
||||
|
||||
@@ -357,6 +357,17 @@ impl BoundingBox for Graphic {
|
||||
Graphic::Gradient(table) => table.bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
match self {
|
||||
Graphic::Vector(vector) => vector.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::RasterCPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::RasterGPU(raster) => raster.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Graphic(graphic) => graphic.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Color(color) => color.thumbnail_bounding_box(transform, include_stroke),
|
||||
Graphic::Gradient(gradient) => gradient.thumbnail_bounding_box(transform, include_stroke),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TableConvert<Graphic> for Vector {
|
||||
|
||||
@@ -227,6 +227,10 @@ where
|
||||
let unit_rectangle = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
RenderBoundingBox::Rectangle((transform * unit_rectangle).bounding_box())
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
self.bounding_box(transform, include_stroke)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderComplexity trait implementations
|
||||
|
||||
@@ -26,6 +26,7 @@ kurbo = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
graphic-types = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
vello_encoding = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
@@ -10,7 +10,9 @@ use core_types::render_complexity::RenderComplexity;
|
||||
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_LOCATION, ATTR_TRANSFORM};
|
||||
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,
|
||||
};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_hash::CacheHashWrapper;
|
||||
@@ -287,6 +289,10 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
||||
usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32)
|
||||
}
|
||||
|
||||
fn to_point(p: DVec2) -> kurbo::Point {
|
||||
kurbo::Point::new(p.x, p.y)
|
||||
}
|
||||
|
||||
fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) {
|
||||
use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT;
|
||||
|
||||
@@ -1088,7 +1094,6 @@ impl Render for Table<Vector> {
|
||||
}
|
||||
let layer_bounds = element.bounding_box().unwrap_or_default();
|
||||
|
||||
let to_point = |p: DVec2| kurbo::Point::new(p.x, p.y);
|
||||
let mut path = kurbo::BezPath::new();
|
||||
for mut bezpath in element.stroke_bezpath_iter() {
|
||||
bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array()));
|
||||
@@ -1749,16 +1754,35 @@ impl Render for Table<Color> {
|
||||
}
|
||||
|
||||
impl Render for Table<GradientStops> {
|
||||
// TODO: Fix infinite gradient rendering
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
// For thumbnails the gradient fills a finite rect at the footprint's document space bounds, with a 1-unit margin to cover the `as u32` truncation of `Footprint::resolution`.
|
||||
// The viewBox crops the overshoot. Canvas rendering keeps the polyline path since Chrome rejects rects larger than ~20 million.
|
||||
let thumbnail_rect = if render_params.thumbnail {
|
||||
let truncated_size = render_params.footprint.resolution.as_dvec2();
|
||||
let margin = DVec2::ONE;
|
||||
Some((render_params.footprint.transform.translation - margin / 2., truncated_size + margin))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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);
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
||||
let max = u64::MAX;
|
||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
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" };
|
||||
render.leaf_tag(tag, |attributes| {
|
||||
if let Some((min, size)) = thumbnail_rect {
|
||||
attributes.push("x", min.x.to_string());
|
||||
attributes.push("y", min.y.to_string());
|
||||
attributes.push("width", size.x.to_string());
|
||||
attributes.push("height", size.y.to_string());
|
||||
} else {
|
||||
// Chrome doesn't like drawing centered rectangles bigger than ~20 million so we draw a polyline quad instead
|
||||
let max = u64::MAX;
|
||||
attributes.push("points", format!("{max},{max} -{max},{max} -{max},-{max} {max},-{max}"));
|
||||
}
|
||||
|
||||
let mut stop_string = String::new();
|
||||
for (position, color, original_midpoint) in gradient.interpolated_samples() {
|
||||
@@ -1772,7 +1796,8 @@ impl Render for Table<GradientStops> {
|
||||
stop_string.push_str(" />");
|
||||
}
|
||||
|
||||
let gradient_transform = render_params.footprint.transform * transform;
|
||||
// render_thumbnail already added the footprint transform
|
||||
let gradient_transform = if render_params.thumbnail { transform } else { render_params.footprint.transform * transform };
|
||||
let gradient_transform_matrix = format_transform_matrix(gradient_transform);
|
||||
let gradient_transform_attribute = if gradient_transform_matrix.is_empty() {
|
||||
String::new()
|
||||
@@ -1781,24 +1806,24 @@ impl Render for Table<GradientStops> {
|
||||
};
|
||||
|
||||
let gradient_id = generate_uuid();
|
||||
let start = DVec2::ZERO;
|
||||
let end = DVec2::X;
|
||||
let spread_method_attribute = if spread_method == GradientSpreadMethod::Pad {
|
||||
String::new()
|
||||
} else {
|
||||
format!(r#" spreadMethod="{}""#, spread_method.svg_name())
|
||||
};
|
||||
|
||||
match GradientType::Radial {
|
||||
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied
|
||||
match gradient_type {
|
||||
GradientType::Linear => {
|
||||
let (x1, y1) = (start.x, start.y);
|
||||
let (x2, y2) = (end.x, end.y);
|
||||
let _ = write!(
|
||||
&mut attributes.0.svg_defs,
|
||||
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"{gradient_transform_attribute}>{stop_string}</linearGradient>"#
|
||||
r#"<linearGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="1" y2="0"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</linearGradient>"#
|
||||
);
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let (cx, cy) = (start.x, start.y);
|
||||
let r = start.distance(end);
|
||||
let _ = write!(
|
||||
&mut attributes.0.svg_defs,
|
||||
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="{cx}" cy="{cy}" r="{r}"{gradient_transform_attribute}>{stop_string}</radialGradient>"#
|
||||
r#"<radialGradient id="{gradient_id}" gradientUnits="userSpaceOnUse" cx="0" cy="0" r="1"{spread_method_attribute}{gradient_transform_attribute}>{stop_string}</radialGradient>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1817,28 +1842,82 @@ impl Render for Table<GradientStops> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix infinite gradient rendering
|
||||
fn render_to_vello(&self, scene: &mut Scene, _parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||
use vello::peniko;
|
||||
|
||||
for (gradient, alpha_blending) in self.iter_element_values().zip(self.iter_attribute_values_or_default::<AlphaBlending>(ATTR_ALPHA_BLENDING)) {
|
||||
if let RenderMode::Outline = render_params.render_mode {
|
||||
return;
|
||||
}
|
||||
|
||||
for ((((gradient, transform), alpha_blending), 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))
|
||||
.zip(self.iter_attribute_values_or_default::<GradientSpreadMethod>(ATTR_SPREAD_METHOD))
|
||||
.zip(self.iter_attribute_values_or_default::<GradientType>(ATTR_GRADIENT_TYPE))
|
||||
{
|
||||
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 color = gradient.color.first().copied().unwrap_or(Color::MAGENTA);
|
||||
let vello_color = peniko::Color::new([color.r(), color.g(), color.b(), color.a()]);
|
||||
let mut stops: peniko::ColorStops = peniko::ColorStops::new();
|
||||
for (position, color, _) in gradient.interpolated_samples() {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: position as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
})
|
||||
}
|
||||
|
||||
let extend = match spread_method {
|
||||
GradientSpreadMethod::Pad => peniko::Extend::Pad,
|
||||
GradientSpreadMethod::Reflect => peniko::Extend::Reflect,
|
||||
GradientSpreadMethod::Repeat => peniko::Extend::Repeat,
|
||||
};
|
||||
|
||||
// The unit gradient line is the +X unit vector in local space, before the item's transform is applied.
|
||||
// For radial, the unit-radius circle at the origin scales out to the line's length once the brush transform applies.
|
||||
let kind = match gradient_type {
|
||||
GradientType::Linear => peniko::LinearGradientPosition {
|
||||
start: to_point(DVec2::ZERO),
|
||||
end: to_point(DVec2::X),
|
||||
}
|
||||
.into(),
|
||||
GradientType::Radial => peniko::RadialGradientPosition {
|
||||
start_center: to_point(DVec2::ZERO),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(DVec2::ZERO),
|
||||
end_radius: 1.,
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
|
||||
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind,
|
||||
stops,
|
||||
extend,
|
||||
interpolation_alpha_space: peniko::InterpolationAlphaSpace::Premultiplied,
|
||||
..Default::default()
|
||||
});
|
||||
let brush_transform = kurbo::Affine::new((gradient_transform).to_cols_array());
|
||||
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() {
|
||||
let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver);
|
||||
// See implemenation in `Table<Color>` for more detail
|
||||
// See implementation in `Table<Color>` for more detail
|
||||
scene.push_layer(peniko::Fill::NonZero, blending, opacity, kurbo::Affine::scale(f64::INFINITY), &rect);
|
||||
layer = true;
|
||||
}
|
||||
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::scale(f64::INFINITY), vello_color, None, &rect);
|
||||
// Encode shape and brush manually instead of Scene.fill(), which would multiply brush_transform by the path transform
|
||||
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&kurbo::Affine::scale(f64::INFINITY)));
|
||||
scene.encoding_mut().encode_fill_style(peniko::Fill::NonZero);
|
||||
scene.encoding_mut().encode_shape(&rect, true);
|
||||
|
||||
scene.encoding_mut().encode_transform(vello_encoding::Transform::from_kurbo(&brush_transform));
|
||||
scene.encoding_mut().swap_last_path_tags();
|
||||
scene.encoding_mut().encode_brush(&fill, 1.);
|
||||
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
||||
@@ -2,6 +2,10 @@ use core_types::{Color, render_complexity::RenderComplexity};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Default scale applied to a freshly-created `Table<GradientStops>` item's transform.
|
||||
/// Places the unit gradient line (the +X unit vector in local space) inside a 100×100 document-space box.
|
||||
pub const GRADIENT_TABLE_DEFAULT_SCALE: f64 = 100.;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, graphene_hash::CacheHash, DynAny, node_macro::ChoiceType)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -488,4 +492,12 @@ impl core_types::bounds::BoundingBox for GradientStops {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||
core_types::bounds::RenderBoundingBox::Infinite
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> core_types::bounds::RenderBoundingBox {
|
||||
// AABB of the gradient line itself, leaving aspect padding and sub-pixel fallbacks to the runtime so this stays
|
||||
// a clean per-item geometric bound that combines naturally with siblings
|
||||
let start = transform.transform_point2(DVec2::ZERO);
|
||||
let end = transform.transform_point2(DVec2::X);
|
||||
core_types::bounds::RenderBoundingBox::Rectangle([start.min(end), start.max(end)])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,6 +468,10 @@ impl BoundingBox for Vector {
|
||||
None => RenderBoundingBox::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox {
|
||||
BoundingBox::bounding_box(self, transform, include_stroke)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderComplexity for Vector {
|
||||
|
||||
Reference in New Issue
Block a user