Prep gcore splitup: move various symbols into their own modules (#2746)

* move `trait AsU32` from `gcore::vector::misc` to `gcore`

* move blending and gradient to their own modules

* fix unused warnings

* move `Quad`, `Rect` and `BBox` to `gcore::math`

* extract `ReferencePoint` and transform nodes from `transform`

* move color-related code to `mod color`

* fix unused warning in test code

* move blending-related nodes and code to `mod blending_nodes`

* move ClickTarget code to `mod vector::click_target`
This commit is contained in:
Firestar99 2025-06-27 11:54:34 +02:00 committed by GitHub
parent c797877763
commit 2ddae98bcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1407 additions and 1341 deletions

View file

@ -11,8 +11,8 @@ use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::raster::BlendMode;
use graphene_std::raster::Image;
use graphene_std::renderer::ClickTarget;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::ClickTarget;
use graphene_std::vector::style::ViewMode;
#[impl_message(Message, PortfolioMessage, Document)]

View file

@ -29,9 +29,10 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::math::quad::Quad;
use graphene_std::raster::BlendMode;
use graphene_std::raster_types::{Raster, RasterDataTable};
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::style::ViewMode;
use graphene_std::vector::{PointId, path_bool_lib};
use std::time::Duration;

View file

@ -166,13 +166,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
@ -1650,7 +1650,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
NodeInput::network(concrete!(DVec2), 5),
],
manual_composition: Some(concrete!(Context)),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::TransformNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::TransformNode")),
..Default::default()
},
]
@ -1746,13 +1746,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
@ -1859,13 +1859,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
@ -2014,13 +2014,13 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::FreezeRealTimeNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::FreezeRealTimeNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform::BoundlessFootprintNode")),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::BoundlessFootprintNode")),
manual_composition: Some(generic!(T)),
..Default::default()
},

View file

@ -21,6 +21,7 @@ use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors;
use graphene_std::math::math_ext::QuadExt;
use graphene_std::*;
use renderer::Quad;
use std::cmp::Ordering;

View file

@ -9,8 +9,8 @@ use core::borrow::Borrow;
use core::f64::consts::{FRAC_PI_2, TAU};
use glam::{DAffine2, DVec2};
use graphene_std::Color;
use graphene_std::renderer::ClickTargetType;
use graphene_std::renderer::Quad;
use graphene_std::math::quad::Quad;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::{PointId, SegmentId, VectorData};
use std::collections::HashMap;
use wasm_bindgen::{JsCast, JsValue};

View file

@ -3,8 +3,9 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeId;
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData};
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;

View file

@ -12,8 +12,9 @@ use glam::{DAffine2, DVec2, IVec2};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete};
use graphene_std::renderer::{ClickTarget, ClickTargetType, Quad};
use graphene_std::math::quad::Quad;
use graphene_std::transform::Footprint;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
use graphene_std::vector::{PointId, VectorData, VectorModificationType};
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypes;
use interpreted_executor::node_registry::NODE_REGISTRY;

View file

@ -14,12 +14,12 @@ use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAl
use graphene_std::vector::{VectorData, VectorDataTable};
use std::collections::HashMap;
const TEXT_REPLACEMENTS: [(&str, &str); 2] = [
const TEXT_REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::vector::vector_nodes::SamplePointsNode", "graphene_core::vector::SamplePolylineNode"),
("graphene_core::vector::vector_nodes::SubpathSegmentLengthsNode", "graphene_core::vector::SubpathSegmentLengthsNode"),
];
const REPLACEMENTS: [(&str, &str); 40] = [
const REPLACEMENTS: &[(&str, &str)] = &[
("graphene_core::AddArtboardNode", "graphene_core::graphic_element::AppendArtboardNode"),
("graphene_core::ConstructArtboardNode", "graphene_core::graphic_element::ToArtboardNode"),
("graphene_core::ToGraphicElementNode", "graphene_core::graphic_element::ToElementNode"),
@ -31,6 +31,9 @@ const REPLACEMENTS: [(&str, &str); 40] = [
("graphene_core::ops::Vector2ValueNode", "graphene_core::ops::CoordinateValueNode"),
("graphene_core::raster::BlackAndWhiteNode", "graphene_core::raster::adjustments::BlackAndWhiteNode"),
("graphene_core::raster::BlendNode", "graphene_core::raster::adjustments::BlendNode"),
("graphene_core::raster::BlendModeNode", "graphene_core::blending_nodes::BlendModeNode"),
("graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"),
("graphene_core::raster::BlendingNode", "graphene_core::blending_nodes::BlendingNode"),
("graphene_core::raster::ChannelMixerNode", "graphene_core::raster::adjustments::ChannelMixerNode"),
("graphene_core::raster::adjustments::ColorOverlayNode", "graphene_core::raster::adjustments::ColorOverlayNode"),
("graphene_core::raster::ExposureNode", "graphene_core::raster::adjustments::ExposureNode"),
@ -48,7 +51,11 @@ const REPLACEMENTS: [(&str, &str); 40] = [
("graphene_core::raster::ThresholdNode", "graphene_core::raster::adjustments::ThresholdNode"),
("graphene_core::raster::VibranceNode", "graphene_core::raster::adjustments::VibranceNode"),
("graphene_core::text::TextGeneratorNode", "graphene_core::text::TextNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform::ReplaceTransformNode"),
("graphene_core::transform::SetTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::ReplaceTransformNode", "graphene_core::transform_nodes::ReplaceTransformNode"),
("graphene_core::transform::TransformNode", "graphene_core::transform_nodes::TransformNode"),
("graphene_core::transform::BoundlessFootprintNode", "graphene_core::transform_nodes::BoundlessFootprintNode"),
("graphene_core::transform::FreezeRealTimeNode", "graphene_core::transform_nodes::FreezeRealTimeNode"),
("graphene_core::vector::SplinesFromPointsNode", "graphene_core::vector::SplineNode"),
("graphene_core::vector::generator_nodes::EllipseGenerator", "graphene_core::vector::generator_nodes::EllipseNode"),
("graphene_core::vector::generator_nodes::LineGenerator", "graphene_core::vector::generator_nodes::LineNode"),

View file

@ -13,7 +13,7 @@ use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::renderer::ClickTargetType;
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::dvec2_to_point;
use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque;

View file

@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::misc::*;
use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_std::math::math_ext::QuadExt;
use graphene_std::renderer::Quad;
use graphene_std::vector::PointId;

View file

@ -157,7 +157,7 @@ raster_node!(graphene_core::raster::OpacityNode<_>, params: [f64]),
There is also the more general `register_node!` for nodes that do not need to run per pixel.
```rs
register_node!(graphene_core::transform::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
register_node!(graphene_core::transform_nodes::SetTransformNode<_>, input: VectorData, params: [DAffine2]),
```
## Debugging

View file

@ -0,0 +1,271 @@
use dyn_any::DynAny;
use std::hash::Hash;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[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 Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 },
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type)]
#[repr(i32)]
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
#[cfg(feature = "vello")]
impl From<BlendMode> for vello::peniko::Mix {
fn from(val: BlendMode) -> Self {
match val {
// Normal group
BlendMode::Normal => vello::peniko::Mix::Normal,
// Darken group
BlendMode::Darken => vello::peniko::Mix::Darken,
BlendMode::Multiply => vello::peniko::Mix::Multiply,
BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn,
// Lighten group
BlendMode::Lighten => vello::peniko::Mix::Lighten,
BlendMode::Screen => vello::peniko::Mix::Screen,
BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge,
// Contrast group
BlendMode::Overlay => vello::peniko::Mix::Overlay,
BlendMode::SoftLight => vello::peniko::Mix::SoftLight,
BlendMode::HardLight => vello::peniko::Mix::HardLight,
// Inversion group
BlendMode::Difference => vello::peniko::Mix::Difference,
BlendMode::Exclusion => vello::peniko::Mix::Exclusion,
// Component group
BlendMode::Hue => vello::peniko::Mix::Hue,
BlendMode::Saturation => vello::peniko::Mix::Saturation,
BlendMode::Color => vello::peniko::Mix::Color,
BlendMode::Luminosity => vello::peniko::Mix::Luminosity,
_ => todo!(),
}
}
}

View file

@ -0,0 +1,175 @@
use crate::raster::Image;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use crate::{BlendMode, Color, Ctx, GraphicElement, GraphicGroupTable};
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}

View file

@ -1,5 +1,5 @@
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use super::{Alpha, AlphaMut, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use bytemuck::{Pod, Zeroable};
use dyn_any::DynAny;
use half::f16;
@ -345,7 +345,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.3, 0.14, 0.15, 0.92).unwrap();
/// assert!(color.components() == (0.3, 0.14, 0.15, 0.92));
///
@ -383,7 +383,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgb8_srgb(0x72, 0x67, 0x62);
/// let color2 = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0xFF);
/// assert_eq!(color, color2)
@ -398,7 +398,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x72, 0x67, 0x62, 0x61);
/// ```
#[inline(always)]
@ -416,7 +416,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.);
/// ```
pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color {
@ -458,7 +458,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.r() == 0.114);
/// ```
@ -471,7 +471,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.g() == 0.103);
/// ```
@ -484,7 +484,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.b() == 0.98);
/// ```
@ -497,7 +497,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert!(color.a() == 0.97);
/// ```
@ -773,7 +773,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// assert_eq!(color.components(), (0.114, 0.103, 0.98, 0.97));
/// ```
@ -786,7 +786,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a261", color.to_rgba_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@ -803,7 +803,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in linear space.
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@ -813,7 +813,7 @@ impl Color {
/// Return a 6-character RGB hex string (without a # prefix). Use this if the [`Color`] is in gamma space.
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61); // Premultiplied alpha
/// assert_eq!("3240a2", color.to_rgb_hex_srgb()); // Equivalent hex incorporating premultiplied alpha
/// ```
@ -825,7 +825,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgbaf32(0.114, 0.103, 0.98, 0.97).unwrap();
/// // TODO: Add test
/// ```
@ -840,7 +840,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_hsla(0.5, 0.2, 0.3, 1.).to_hsla();
/// ```
pub fn to_hsla(&self) -> [f32; 4] {
@ -876,7 +876,7 @@ impl Color {
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgba_str("7C67FA61").unwrap();
/// ```
pub fn from_rgba_str(color_str: &str) -> Option<Color> {
@ -894,7 +894,7 @@ impl Color {
/// Creates a color from a 6-character RGB hex string (without a # prefix).
///
/// ```
/// use graphene_core::raster::color::Color;
/// use graphene_core::color::Color;
/// let color = Color::from_rgb_str("7C67FA").unwrap();
/// ```
pub fn from_rgb_str(color_str: &str) -> Option<Color> {

View file

@ -0,0 +1,205 @@
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
pub use crate::blending::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}

View file

@ -0,0 +1,7 @@
mod color;
mod color_traits;
mod discrete_srgb;
pub use color::*;
pub use color_traits::*;
pub use discrete_srgb::*;

View file

@ -0,0 +1,248 @@
use crate::Color;
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(pub Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
self.0.iter().for_each(|(position, color)| {
position.to_bits().hash(state);
color.hash(state);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops {
pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() {
return Color::BLACK;
}
if t <= self.0[0].0 {
return self.0[0].1;
}
if t >= self.0[self.0.len() - 1].0 {
return self.0[self.0.len() - 1].1;
}
for i in 0..self.0.len() - 1 {
let (t1, c1) = self.0[i];
let (t2, c2) = self.0[i + 1];
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
return c1.lerp(&c2, normalized_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl std::hash::Hash for Gradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.0
.iter()
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let stops = self
.stops
.0
.iter()
.zip(other.stops.0.iter())
.map(|((a_pos, a_color), (b_pos, b_color))| {
let position = a_pos + (b_pos - a_pos) * time;
let color = a_color.lerp(b_color, time as f32);
(position, color)
})
.collect::<Vec<_>>();
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
// Lerp between the nearest colors if applicable
(a, Some(b)) => a.lerp(
&b,
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
),
// Use the start or the end color if applicable
(v, _) => v,
};
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
}

View file

@ -1,5 +1,5 @@
use crate::blending::AlphaBlending;
use crate::instances::{Instance, Instances};
use crate::raster::BlendMode;
use crate::raster::image::Image;
use crate::raster_types::{CPU, GPU, Raster, RasterDataTable};
use crate::transform::TransformMut;
@ -12,63 +12,6 @@ use std::hash::Hash;
pub mod renderer;
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type, serde::Serialize, serde::Deserialize)]
#[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 Hash for AlphaBlending {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.opacity.to_bits().hash(state);
self.fill.to_bits().hash(state);
self.blend_mode.hash(state);
self.clip.hash(state);
}
}
impl std::fmt::Display for AlphaBlending {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 },
}
}
}
// TODO: Eventually remove this migration document upgrade code
pub fn migrate_graphic_group<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<GraphicGroupTable, D::Error> {
use serde::Deserialize;

View file

@ -1,55 +1,23 @@
mod quad;
mod rect;
use crate::instances::Instance;
pub use crate::math::quad::Quad;
pub use crate::math::rect::Rect;
use crate::raster::{BlendMode, Image};
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::transform::{Footprint, Transform};
use crate::uuid::{NodeId, generate_uuid};
use crate::vector::VectorDataTable;
use crate::vector::click_target::{ClickTarget, FreePoint};
use crate::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
use crate::vector::{PointId, VectorDataTable};
use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable};
use bezier_rs::Subpath;
use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use glam::{DAffine2, DVec2};
use num_traits::Zero;
pub use quad::Quad;
pub use rect::Rect;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
#[cfg(feature = "vello")]
use vello::*;
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FreePoint {
pub id: PointId,
pub position: DVec2,
}
impl FreePoint {
pub fn new(id: PointId, position: DVec2) -> Self {
Self { id, position }
}
pub fn apply_transform(&mut self, transform: DAffine2) {
self.position = transform.transform_point2(self.position);
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ClickTargetType {
Subpath(Subpath<PointId>),
FreePoint(FreePoint),
}
/// Represents a clickable target for the layer
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ClickTarget {
target_type: ClickTargetType,
stroke_width: f64,
bounding_box: Option<[DVec2; 2]>,
}
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
enum MaskType {
Clip,
@ -73,133 +41,6 @@ impl MaskType {
}
}
impl ClickTarget {
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
let bounding_box = subpath.loose_bounding_box();
Self {
target_type: ClickTargetType::Subpath(subpath),
stroke_width,
bounding_box,
}
}
pub fn new_with_free_point(point: FreePoint) -> Self {
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
let stroke_width = 10.;
let bounding_box = Some([
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
]);
Self {
target_type: ClickTargetType::FreePoint(point),
stroke_width,
bounding_box,
}
}
pub fn target_type(&self) -> &ClickTargetType {
&self.target_type
}
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.bounding_box
}
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
}
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
match self.target_type {
ClickTargetType::Subpath(ref mut subpath) => {
subpath.apply_transform(affine_transform);
}
ClickTargetType::FreePoint(ref mut point) => {
point.apply_transform(affine_transform);
}
}
self.update_bbox();
}
fn update_bbox(&mut self) {
match self.target_type {
ClickTargetType::Subpath(ref subpath) => {
self.bounding_box = subpath.bounding_box();
}
ClickTargetType::FreePoint(ref point) => {
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
}
}
}
/// Does the click target intersect the path
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
// Check if the matrix is not invertible
let mut layer_transform = layer_transform;
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
}
let inverse = layer_transform.inverse();
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
match self.target_type() {
ClickTargetType::Subpath(subpath) => {
// Check if outlines intersect
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
if subpath.iter().any(outline_intersects) {
return true;
}
// Check if selection is entirely within the shape
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
return true;
}
// Check if shape is entirely within selection
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
}
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
}
}
/// Does the click target intersect the point (accounting for stroke size)
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)];
let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y;
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
if !self
.bounding_box
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
{
return false;
}
// Allows for selecting lines
// TODO: actual intersection of stroke
let inflated_quad = Quad::from_box(target_bounds);
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
}
/// Does the click target intersect the point (not accounting for stroke size)
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
// Check if the point is within the bounding box
if self
.bounding_box
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
{
// Check if the point is within the shape
match self.target_type() {
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
ClickTargetType::FreePoint(free_point) => free_point.position == point,
}
} else {
false
}
}
}
/// Mutable state used whilst rendering to an SVG
pub struct SvgRender {
pub svg: Vec<SvgSegment>,

View file

@ -1,38 +1,40 @@
#[macro_use]
extern crate log;
pub use crate as graphene_core;
pub use ctor;
pub use num_traits;
pub mod animation;
pub mod blending;
pub mod blending_nodes;
pub mod color;
pub mod consts;
pub mod context;
pub mod generic;
pub mod gradient;
mod graphic_element;
pub mod instances;
pub mod logic;
pub mod math;
pub mod memo;
pub mod misc;
pub mod ops;
pub mod raster;
pub mod raster_types;
pub mod registry;
pub mod structural;
pub mod text;
pub mod transform;
pub mod transform_nodes;
pub mod uuid;
pub mod value;
pub mod memo;
pub mod raster;
pub mod transform;
mod graphic_element;
pub use graphic_element::*;
pub mod vector;
pub mod registry;
pub use crate as graphene_core;
pub use blending::*;
pub use context::*;
pub use ctor;
pub use dyn_any::{StaticTypeSized, WasmNotSend, WasmNotSync};
pub use graphic_element::*;
pub use memo::MemoHash;
pub use num_traits;
pub use raster::Color;
use std::any::TypeId;
use std::future::Future;
@ -159,3 +161,12 @@ pub trait NodeInputDecleration {
fn identifier() -> &'static str;
type Result;
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}

View file

@ -0,0 +1,25 @@
use crate::math::quad::Quad;
use crate::math::rect::Rect;
use bezier_rs::Bezier;
pub trait QuadExt {
/// Get all the edges in the rect as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl QuadExt for Quad {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}
pub trait RectExt {
/// Get all the edges in the quad as linear bezier curves
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_;
}
impl RectExt for Rect {
fn bezier_lines(&self) -> impl Iterator<Item = Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| Bezier::from_linear_dvec2(start, end))
}
}

View file

@ -0,0 +1,4 @@
pub mod bbox;
pub mod math_ext;
pub mod quad;
pub mod rect;

View file

@ -58,11 +58,6 @@ impl Quad {
self.edges().into_iter().all(|[a, b]| (a - b).length_squared() >= width.powi(2))
}
/// Get all the edges in the quad as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.all_edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Generates the axis aligned bounding box of the quad
pub fn bounding_box(&self) -> [DVec2; 2] {
[

View file

@ -1,4 +1,4 @@
use super::Quad;
use crate::math::quad::Quad;
use glam::{DAffine2, DVec2};
#[derive(Debug, Clone, Default, Copy, PartialEq)]
@ -43,11 +43,6 @@ impl Rect {
[[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]]
}
/// Get all the edges in the rect as linear bezier curves
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
}
/// Gets the center of a rect
#[must_use]
pub fn center(&self) -> DVec2 {

View file

@ -1,222 +1,25 @@
pub use self::color::{Color, Luma, SRGBA8};
use crate::Ctx;
use crate::GraphicGroupTable;
pub use crate::color::*;
use crate::raster_types::{CPU, RasterDataTable};
use crate::registry::types::Percentage;
use crate::vector::VectorDataTable;
use bytemuck::{Pod, Zeroable};
use glam::DVec2;
use std::fmt::Debug;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
/// as to not yet rename all references
pub mod color {
pub use super::*;
}
pub mod adjustments;
pub mod bbox;
pub mod brush_cache;
pub mod color;
pub mod curve;
pub mod discrete_srgb;
pub mod image;
pub use self::image::Image;
pub use adjustments::*;
pub trait Linear {
fn from_f32(x: f32) -> Self;
fn to_f32(self) -> f32;
fn from_f64(x: f64) -> Self;
fn to_f64(self) -> f64;
fn lerp(self, other: Self, value: Self) -> Self
where
Self: Sized + Copy,
Self: std::ops::Sub<Self, Output = Self>,
Self: std::ops::Mul<Self, Output = Self>,
Self: std::ops::Add<Self, Output = Self>,
{
self + (other - self) * value
}
}
#[rustfmt::skip]
impl Linear for f32 {
#[inline(always)] fn from_f32(x: f32) -> Self { x }
#[inline(always)] fn to_f32(self) -> f32 { self }
#[inline(always)] fn from_f64(x: f64) -> Self { x as f32 }
#[inline(always)] fn to_f64(self) -> f64 { self as f64 }
}
#[rustfmt::skip]
impl Linear for f64 {
#[inline(always)] fn from_f32(x: f32) -> Self { x as f64 }
#[inline(always)] fn to_f32(self) -> f32 { self as f32 }
#[inline(always)] fn from_f64(x: f64) -> Self { x }
#[inline(always)] fn to_f64(self) -> f64 { self }
}
pub trait Channel: Copy + Debug {
fn to_linear<Out: Linear>(self) -> Out;
fn from_linear<In: Linear>(linear: In) -> Self;
}
pub trait LinearChannel: Channel {
fn cast_linear_channel<Out: LinearChannel>(self) -> Out {
Out::from_linear(self.to_linear::<f64>())
}
}
impl<T: Linear + Debug + Copy> Channel for T {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
Out::from_f64(self.to_f64())
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
Self::from_f64(linear.to_f64())
}
}
impl<T: Linear + Debug + Copy> LinearChannel for T {}
use num_derive::*;
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Num, NumCast, NumOps, One, Zero, ToPrimitive, FromPrimitive)]
pub struct SRGBGammaFloat(f32);
impl Channel for SRGBGammaFloat {
#[inline(always)]
fn to_linear<Out: Linear>(self) -> Out {
let x = self.0;
Out::from_f32(if x <= 0.04045 { x / 12.92 } else { ((x + 0.055) / 1.055).powf(2.4) })
}
#[inline(always)]
fn from_linear<In: Linear>(linear: In) -> Self {
let x = linear.to_f32();
if x <= 0.0031308 { Self(x * 12.92) } else { Self(1.055 * x.powf(1. / 2.4) - 0.055) }
}
}
pub trait RGBPrimaries {
const RED: DVec2;
const GREEN: DVec2;
const BLUE: DVec2;
const WHITE: DVec2;
}
pub trait Rec709Primaries {}
impl<T: Rec709Primaries> RGBPrimaries for T {
const RED: DVec2 = DVec2::new(0.64, 0.33);
const GREEN: DVec2 = DVec2::new(0.3, 0.6);
const BLUE: DVec2 = DVec2::new(0.15, 0.06);
const WHITE: DVec2 = DVec2::new(0.3127, 0.329);
}
pub trait SRGB: Rec709Primaries {}
pub trait Serde: serde::Serialize + for<'a> serde::Deserialize<'a> {}
#[cfg(not(feature = "serde"))]
pub trait Serde {}
impl<T: serde::Serialize + for<'a> serde::Deserialize<'a>> Serde for T {}
#[cfg(not(feature = "serde"))]
impl<T> Serde for T {}
// TODO: Come up with a better name for this trait
pub trait Pixel: Clone + Pod + Zeroable + Default {
#[cfg(not(target_arch = "spirv"))]
fn to_bytes(&self) -> Vec<u8> {
bytemuck::bytes_of(self).to_vec()
}
// TODO: use u8 for Color
fn from_bytes(bytes: &[u8]) -> Self {
*bytemuck::try_from_bytes(bytes).expect("Failed to convert bytes to pixel")
}
fn byte_size() -> usize {
size_of::<Self>()
}
}
pub trait RGB: Pixel {
type ColorChannel: Channel;
fn red(&self) -> Self::ColorChannel;
fn r(&self) -> Self::ColorChannel {
self.red()
}
fn green(&self) -> Self::ColorChannel;
fn g(&self) -> Self::ColorChannel {
self.green()
}
fn blue(&self) -> Self::ColorChannel;
fn b(&self) -> Self::ColorChannel {
self.blue()
}
}
pub trait RGBMut: RGB {
fn set_red(&mut self, red: Self::ColorChannel);
fn set_green(&mut self, green: Self::ColorChannel);
fn set_blue(&mut self, blue: Self::ColorChannel);
}
pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}
pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}
pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
fn alpha(&self) -> Self::AlphaChannel;
fn a(&self) -> Self::AlphaChannel {
self.alpha()
}
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self;
}
pub trait AlphaMut: Alpha {
fn set_alpha(&mut self, value: Self::AlphaChannel);
}
pub trait Depth {
type DepthChannel: Channel;
fn depth(&self) -> Self::DepthChannel;
fn d(&self) -> Self::DepthChannel {
self.depth()
}
}
pub trait ExtraChannels<const NUM: usize> {
type ChannelType: Channel;
fn extra_channels(&self) -> [Self::ChannelType; NUM];
}
pub trait Luminance {
type LuminanceChannel: LinearChannel;
fn luminance(&self) -> Self::LuminanceChannel;
fn l(&self) -> Self::LuminanceChannel {
self.luminance()
}
}
pub trait LuminanceMut: Luminance {
fn set_luminance(&mut self, luminance: Self::LuminanceChannel);
}
// TODO: We might rename this to Raster at some point
pub trait Sample {
type Pixel: Pixel;
// TODO: Add an area parameter
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel>;
}
impl<T: Sample> Sample for &T {
type Pixel = T::Pixel;
#[inline(always)]
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
(**self).sample(pos, area)
}
}
pub trait Bitmap {
type Pixel: Pixel;
fn width(&self) -> u32;
@ -282,112 +85,3 @@ impl<T: BitmapMut + Bitmap> BitmapMut for &mut T {
(*self).get_pixel_mut(x, y)
}
}
pub use self::image::Image;
pub mod image;
trait SetBlendMode {
fn set_blend_mode(&mut self, blend_mode: BlendMode);
}
impl SetBlendMode for VectorDataTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for GraphicGroupTable {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
impl SetBlendMode for RasterDataTable<CPU> {
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.blend_mode = blend_mode;
}
}
}
trait SetClip {
fn set_clip(&mut self, clip: bool);
}
impl SetClip for VectorDataTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for GraphicGroupTable {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
impl SetClip for RasterDataTable<CPU> {
fn set_clip(&mut self, clip: bool) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.clip = clip;
}
}
}
#[node_macro::node(category("Style"))]
fn blend_mode<T: SetBlendMode>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value
}
#[node_macro::node(category("Style"))]
fn opacity<T: MultiplyAlpha>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
#[default(100.)] opacity: Percentage,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.multiply_alpha(opacity / 100.);
value
}
#[node_macro::node(category("Style"))]
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
_: impl Ctx,
#[implementations(
GraphicGroupTable,
VectorDataTable,
RasterDataTable<CPU>,
)]
mut value: T,
blend_mode: BlendMode,
#[default(100.)] opacity: Percentage,
#[default(100.)] fill: Percentage,
#[default(false)] clip: bool,
) -> T {
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
value.set_blend_mode(blend_mode);
value.multiply_alpha(opacity / 100.);
value.multiply_fill(fill / 100.);
value.set_clip(clip);
value
}

View file

@ -1,15 +1,15 @@
#![allow(clippy::too_many_arguments)]
use crate::GraphicElement;
use crate::blending::BlendMode;
use crate::raster::curve::{CubicSplines, CurveManipulatorGroup};
use crate::raster::curve::{Curve, ValueMapperNode};
use crate::raster::image::Image;
use crate::raster::{Channel, Color, Pixel};
use crate::raster_types::{CPU, Raster, RasterDataTable};
use crate::registry::types::{Angle, Percentage, SignedPercentage};
use crate::vector::VectorDataTable;
use crate::vector::style::GradientStops;
use crate::{Ctx, Node};
use crate::{GraphicElement, GraphicGroupTable};
use dyn_any::DynAny;
use std::cmp::Ordering;
use std::fmt::Debug;
@ -41,217 +41,6 @@ pub enum LuminanceCalculation {
MaximumChannels,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash, specta::Type, serde::Serialize, serde::Deserialize)]
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
pub enum BlendMode {
// Basic group
#[default]
Normal,
// Darken group
Darken,
Multiply,
ColorBurn,
LinearBurn,
DarkerColor,
// Lighten group
Lighten,
Screen,
ColorDodge,
LinearDodge,
LighterColor,
// Contrast group
Overlay,
SoftLight,
HardLight,
VividLight,
LinearLight,
PinLight,
HardMix,
// Inversion group
Difference,
Exclusion,
Subtract,
Divide,
// Component group
Hue,
Saturation,
Color,
Luminosity,
// Other stuff
Erase,
Restore,
MultiplyAlpha,
}
impl BlendMode {
/// All standard blend modes ordered by group.
pub fn list() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
// Lighten group
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
// Contrast group
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
// Inversion group
&[Difference, Exclusion, Subtract, Divide],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
/// The subset of [`BlendMode::list()`] that is supported by SVG.
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
use BlendMode::*;
[
// Normal group
&[Normal],
// Darken group
&[Darken, Multiply, ColorBurn],
// Lighten group
&[Lighten, Screen, ColorDodge],
// Contrast group
&[Overlay, SoftLight, HardLight],
// Inversion group
&[Difference, Exclusion],
// Component group
&[Hue, Saturation, Color, Luminosity],
]
}
pub fn index_in_list(&self) -> Option<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
/// Convert the enum to the CSS string for the blend mode.
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
pub fn to_svg_style_name(&self) -> Option<&'static str> {
match self {
// Normal group
BlendMode::Normal => Some("normal"),
// Darken group
BlendMode::Darken => Some("darken"),
BlendMode::Multiply => Some("multiply"),
BlendMode::ColorBurn => Some("color-burn"),
// Lighten group
BlendMode::Lighten => Some("lighten"),
BlendMode::Screen => Some("screen"),
BlendMode::ColorDodge => Some("color-dodge"),
// Contrast group
BlendMode::Overlay => Some("overlay"),
BlendMode::SoftLight => Some("soft-light"),
BlendMode::HardLight => Some("hard-light"),
// Inversion group
BlendMode::Difference => Some("difference"),
BlendMode::Exclusion => Some("exclusion"),
// Component group
BlendMode::Hue => Some("hue"),
BlendMode::Saturation => Some("saturation"),
BlendMode::Color => Some("color"),
BlendMode::Luminosity => Some("luminosity"),
_ => None,
}
}
/// Renders the blend mode CSS style declaration.
pub fn render(&self) -> String {
format!(
r#" mix-blend-mode: {};"#,
self.to_svg_style_name().unwrap_or_else(|| {
warn!("Unsupported blend mode {self:?}");
"normal"
})
)
}
}
impl std::fmt::Display for BlendMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Normal group
BlendMode::Normal => write!(f, "Normal"),
// Darken group
BlendMode::Darken => write!(f, "Darken"),
BlendMode::Multiply => write!(f, "Multiply"),
BlendMode::ColorBurn => write!(f, "Color Burn"),
BlendMode::LinearBurn => write!(f, "Linear Burn"),
BlendMode::DarkerColor => write!(f, "Darker Color"),
// Lighten group
BlendMode::Lighten => write!(f, "Lighten"),
BlendMode::Screen => write!(f, "Screen"),
BlendMode::ColorDodge => write!(f, "Color Dodge"),
BlendMode::LinearDodge => write!(f, "Linear Dodge"),
BlendMode::LighterColor => write!(f, "Lighter Color"),
// Contrast group
BlendMode::Overlay => write!(f, "Overlay"),
BlendMode::SoftLight => write!(f, "Soft Light"),
BlendMode::HardLight => write!(f, "Hard Light"),
BlendMode::VividLight => write!(f, "Vivid Light"),
BlendMode::LinearLight => write!(f, "Linear Light"),
BlendMode::PinLight => write!(f, "Pin Light"),
BlendMode::HardMix => write!(f, "Hard Mix"),
// Inversion group
BlendMode::Difference => write!(f, "Difference"),
BlendMode::Exclusion => write!(f, "Exclusion"),
BlendMode::Subtract => write!(f, "Subtract"),
BlendMode::Divide => write!(f, "Divide"),
// Component group
BlendMode::Hue => write!(f, "Hue"),
BlendMode::Saturation => write!(f, "Saturation"),
BlendMode::Color => write!(f, "Color"),
BlendMode::Luminosity => write!(f, "Luminosity"),
// Other utility blend modes (hidden from the normal list)
BlendMode::Erase => write!(f, "Erase"),
BlendMode::Restore => write!(f, "Restore"),
BlendMode::MultiplyAlpha => write!(f, "Multiply Alpha"),
}
}
}
#[cfg(feature = "vello")]
impl From<BlendMode> for vello::peniko::Mix {
fn from(val: BlendMode) -> Self {
match val {
// Normal group
BlendMode::Normal => vello::peniko::Mix::Normal,
// Darken group
BlendMode::Darken => vello::peniko::Mix::Darken,
BlendMode::Multiply => vello::peniko::Mix::Multiply,
BlendMode::ColorBurn => vello::peniko::Mix::ColorBurn,
// Lighten group
BlendMode::Lighten => vello::peniko::Mix::Lighten,
BlendMode::Screen => vello::peniko::Mix::Screen,
BlendMode::ColorDodge => vello::peniko::Mix::ColorDodge,
// Contrast group
BlendMode::Overlay => vello::peniko::Mix::Overlay,
BlendMode::SoftLight => vello::peniko::Mix::SoftLight,
BlendMode::HardLight => vello::peniko::Mix::HardLight,
// Inversion group
BlendMode::Difference => vello::peniko::Mix::Difference,
BlendMode::Exclusion => vello::peniko::Mix::Exclusion,
// Component group
BlendMode::Hue => vello::peniko::Mix::Hue,
BlendMode::Saturation => vello::peniko::Mix::Saturation,
BlendMode::Color => vello::peniko::Mix::Color,
BlendMode::Luminosity => vello::peniko::Mix::Luminosity,
_ => todo!(),
}
}
}
#[node_macro::node(category("Raster: Adjustment"))]
fn luminance<T: Adjust<Color>>(
_: impl Ctx,
@ -1272,70 +1061,6 @@ async fn selective_color<T: Adjust<Color>>(
image
}
pub(super) trait MultiplyAlpha {
fn multiply_alpha(&mut self, factor: f64);
}
impl MultiplyAlpha for Color {
fn multiply_alpha(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyAlpha for VectorDataTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for GraphicGroupTable {
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
impl MultiplyAlpha for RasterDataTable<CPU>
where
GraphicElement: From<Image<Color>>,
{
fn multiply_alpha(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.opacity *= factor as f32;
}
}
}
pub(super) trait MultiplyFill {
fn multiply_fill(&mut self, factor: f64);
}
impl MultiplyFill for Color {
fn multiply_fill(&mut self, factor: f64) {
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
}
}
impl MultiplyFill for VectorDataTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for GraphicGroupTable {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
impl MultiplyFill for RasterDataTable<CPU> {
fn multiply_fill(&mut self, factor: f64) {
for instance in self.instance_mut_iter() {
instance.alpha_blending.fill *= factor as f32;
}
}
}
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
//
@ -1499,22 +1224,10 @@ fn color_overlay<T: Adjust<Color>>(
#[cfg(test)]
mod test {
use crate::raster::adjustments::BlendMode;
use crate::Color;
use crate::blending::BlendMode;
use crate::raster::image::Image;
use crate::raster_types::{Raster, RasterDataTable};
use crate::{Color, Node};
use std::pin::Pin;
#[derive(Clone)]
pub struct FutureWrapperNode<T: Clone>(T);
impl<'i, T: 'i + Clone + Send> Node<'i, ()> for FutureWrapperNode<T> {
type Output = Pin<Box<dyn Future<Output = T> + 'i + Send>>;
fn eval(&'i self, _input: ()) -> Self::Output {
let value = self.0.clone();
Box::pin(async move { value })
}
}
#[tokio::test]
async fn color_overlay_multiply() {

View file

@ -1,6 +1,6 @@
use super::Color;
use super::discrete_srgb::float_to_srgb_u8;
use crate::AlphaBlending;
use crate::color::float_to_srgb_u8;
use crate::instances::{Instance, Instances};
use crate::raster_types::Raster;
use core::hash::{Hash, Hasher};

View file

@ -59,7 +59,7 @@ pub struct FieldMetadata {
pub unit: Option<&'static str>,
}
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
pub trait ChoiceTypeStatic: Sized + Copy + crate::AsU32 + Send + Sync {
const WIDGET_HINT: ChoiceWidgetHint;
const DESCRIPTION: Option<&'static str>;
fn list() -> &'static [&'static [(Self, VariantMetadata)]];

View file

@ -1,8 +1,6 @@
use crate::instances::Instances;
use crate::raster::bbox::AxisAlignedBbox;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::vector::VectorDataTable;
use crate::{Artboard, CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use crate::Artboard;
use crate::math::bbox::AxisAlignedBbox;
pub use crate::vector::ReferencePoint;
use core::f64;
use glam::{DAffine2, DMat2, DVec2};
@ -152,186 +150,3 @@ impl<T: TransformMut> ApplyTransform for T {
impl ApplyTransform for () {
fn apply_transform(&mut self, &_modification: &DAffine2) {}
}
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target
}
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
*data_transform.transform = transform.transform();
}
data
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {
#[default]
None,
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl ReferencePoint {
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
let size = bounding_box.size();
let offset = match self {
ReferencePoint::None => return None,
ReferencePoint::TopLeft => DVec2::ZERO,
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
};
Some(bounding_box.start + offset)
}
}
impl From<&str> for ReferencePoint {
fn from(input: &str) -> Self {
match input {
"None" => ReferencePoint::None,
"TopLeft" => ReferencePoint::TopLeft,
"TopCenter" => ReferencePoint::TopCenter,
"TopRight" => ReferencePoint::TopRight,
"CenterLeft" => ReferencePoint::CenterLeft,
"Center" => ReferencePoint::Center,
"CenterRight" => ReferencePoint::CenterRight,
"BottomLeft" => ReferencePoint::BottomLeft,
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
}
}
}
impl From<ReferencePoint> for Option<DVec2> {
fn from(input: ReferencePoint) -> Self {
match input {
ReferencePoint::None => None,
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
}
}
}
impl From<DVec2> for ReferencePoint {
fn from(input: DVec2) -> Self {
const TOLERANCE: f64 = 1e-5_f64;
if input.y.abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::TopLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::TopCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::TopRight;
}
} else if (input.y - 0.5).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::CenterLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::Center;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::CenterRight;
}
} else if (input.y - 1.).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::BottomLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::BottomCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::BottomRight;
}
}
ReferencePoint::None
}
}

View file

@ -0,0 +1,89 @@
use crate::instances::Instances;
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::transform::{ApplyTransform, Footprint, Transform};
use crate::vector::VectorDataTable;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, GraphicGroupTable, OwnedContextImpl};
use core::f64;
use glam::{DAffine2, DVec2};
#[node_macro::node(category(""))]
async fn transform<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
)]
transform_target: impl Node<Context<'static>, Output = Instances<T>>,
translate: DVec2,
rotate: f64,
scale: DVec2,
shear: DVec2,
_pivot: DVec2,
) -> Instances<T> {
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let footprint = ctx.try_footprint().copied();
let mut ctx = OwnedContextImpl::from(ctx);
if let Some(mut footprint) = footprint {
footprint.apply_transform(&matrix);
ctx = ctx.with_footprint(footprint);
}
let mut transform_target = transform_target.eval(ctx.into_context()).await;
for data_transform in transform_target.instance_mut_iter() {
*data_transform.transform = matrix * *data_transform.transform;
}
transform_target
}
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
#[implementations(VectorDataTable, RasterDataTable<CPU>, GraphicGroupTable)] mut data: Instances<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Instances<Data> {
for data_transform in data.instance_mut_iter() {
*data_transform.transform = transform.transform();
}
data
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> VectorDataTable,
Context -> GraphicGroupTable,
Context -> RasterDataTable<CPU>,
Context -> RasterDataTable<GPU>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}

View file

@ -1,6 +1,6 @@
use crate::Color;
use crate::math::bbox::AxisAlignedBbox;
use crate::raster::BlendMode;
use crate::raster::bbox::AxisAlignedBbox;
use dyn_any::DynAny;
use glam::DVec2;
use std::hash::{Hash, Hasher};

View file

@ -0,0 +1,162 @@
use crate::math::math_ext::QuadExt;
use crate::renderer::Quad;
use crate::vector::PointId;
use bezier_rs::Subpath;
use glam::{DAffine2, DMat2, DVec2};
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FreePoint {
pub id: PointId,
pub position: DVec2,
}
impl FreePoint {
pub fn new(id: PointId, position: DVec2) -> Self {
Self { id, position }
}
pub fn apply_transform(&mut self, transform: DAffine2) {
self.position = transform.transform_point2(self.position);
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ClickTargetType {
Subpath(Subpath<PointId>),
FreePoint(FreePoint),
}
/// Represents a clickable target for the layer
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ClickTarget {
target_type: ClickTargetType,
stroke_width: f64,
bounding_box: Option<[DVec2; 2]>,
}
impl ClickTarget {
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
let bounding_box = subpath.loose_bounding_box();
Self {
target_type: ClickTargetType::Subpath(subpath),
stroke_width,
bounding_box,
}
}
pub fn new_with_free_point(point: FreePoint) -> Self {
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
let stroke_width = 10.;
let bounding_box = Some([
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
]);
Self {
target_type: ClickTargetType::FreePoint(point),
stroke_width,
bounding_box,
}
}
pub fn target_type(&self) -> &ClickTargetType {
&self.target_type
}
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
self.bounding_box
}
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
}
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
match self.target_type {
ClickTargetType::Subpath(ref mut subpath) => {
subpath.apply_transform(affine_transform);
}
ClickTargetType::FreePoint(ref mut point) => {
point.apply_transform(affine_transform);
}
}
self.update_bbox();
}
fn update_bbox(&mut self) {
match self.target_type {
ClickTargetType::Subpath(ref subpath) => {
self.bounding_box = subpath.bounding_box();
}
ClickTargetType::FreePoint(ref point) => {
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
}
}
}
/// Does the click target intersect the path
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool {
// Check if the matrix is not invertible
let mut layer_transform = layer_transform;
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
layer_transform.matrix2 += DMat2::IDENTITY * 1e-4; // TODO: Is this the cleanest way to handle this?
}
let inverse = layer_transform.inverse();
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
match self.target_type() {
ClickTargetType::Subpath(subpath) => {
// Check if outlines intersect
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
if subpath.iter().any(outline_intersects) {
return true;
}
// Check if selection is entirely within the shape
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
return true;
}
// Check if shape is entirely within selection
let any_point_from_subpath = subpath.manipulator_groups().first().map(|group| group.anchor);
any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::<i32>() != 0)
}
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
}
}
/// Does the click target intersect the point (accounting for stroke size)
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)];
let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y;
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
if !self
.bounding_box
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
{
return false;
}
// Allows for selecting lines
// TODO: actual intersection of stroke
let inflated_quad = Quad::from_box(target_bounds);
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
}
/// Does the click target intersect the point (not accounting for stroke size)
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
// Check if the point is within the bounding box
if self
.bounding_box
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
{
// Check if the point is within the shape
match self.target_type() {
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
ClickTargetType::FreePoint(free_point) => free_point.position == point,
}
} else {
false
}
}
}

View file

@ -29,15 +29,6 @@ pub enum BooleanOperation {
Difference,
}
pub trait AsU32 {
fn as_u32(&self) -> u32;
}
impl AsU32 for u32 {
fn as_u32(&self) -> u32 {
*self
}
}
pub trait AsU64 {
fn as_u64(&self) -> u64;
}

View file

@ -1,12 +1,15 @@
mod algorithms;
pub mod brush_stroke;
pub mod click_target;
pub mod generator_nodes;
pub mod misc;
mod reference_point;
pub mod style;
mod vector_data;
mod vector_nodes;
pub use bezier_rs;
pub use reference_point::*;
pub use style::PathStyle;
pub use vector_data::*;
pub use vector_nodes::*;

View file

@ -0,0 +1,103 @@
use crate::math::bbox::AxisAlignedBbox;
use glam::DVec2;
#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ReferencePoint {
#[default]
None,
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl ReferencePoint {
pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option<DVec2> {
let size = bounding_box.size();
let offset = match self {
ReferencePoint::None => return None,
ReferencePoint::TopLeft => DVec2::ZERO,
ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.),
ReferencePoint::TopRight => DVec2::new(size.x, 0.),
ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.),
ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.),
ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.),
ReferencePoint::BottomLeft => DVec2::new(0., size.y),
ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y),
ReferencePoint::BottomRight => DVec2::new(size.x, size.y),
};
Some(bounding_box.start + offset)
}
}
impl From<&str> for ReferencePoint {
fn from(input: &str) -> Self {
match input {
"None" => ReferencePoint::None,
"TopLeft" => ReferencePoint::TopLeft,
"TopCenter" => ReferencePoint::TopCenter,
"TopRight" => ReferencePoint::TopRight,
"CenterLeft" => ReferencePoint::CenterLeft,
"Center" => ReferencePoint::Center,
"CenterRight" => ReferencePoint::CenterRight,
"BottomLeft" => ReferencePoint::BottomLeft,
"BottomCenter" => ReferencePoint::BottomCenter,
"BottomRight" => ReferencePoint::BottomRight,
_ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"),
}
}
}
impl From<ReferencePoint> for Option<DVec2> {
fn from(input: ReferencePoint) -> Self {
match input {
ReferencePoint::None => None,
ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)),
ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)),
ReferencePoint::TopRight => Some(DVec2::new(1., 0.)),
ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)),
ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)),
ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)),
ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)),
ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)),
ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)),
}
}
}
impl From<DVec2> for ReferencePoint {
fn from(input: DVec2) -> Self {
const TOLERANCE: f64 = 1e-5_f64;
if input.y.abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::TopLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::TopCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::TopRight;
}
} else if (input.y - 0.5).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::CenterLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::Center;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::CenterRight;
}
} else if (input.y - 1.).abs() < TOLERANCE {
if input.x.abs() < TOLERANCE {
return ReferencePoint::BottomLeft;
} else if (input.x - 0.5).abs() < TOLERANCE {
return ReferencePoint::BottomCenter;
} else if (input.x - 1.).abs() < TOLERANCE {
return ReferencePoint::BottomRight;
}
}
ReferencePoint::None
}
}

View file

@ -2,217 +2,13 @@
use crate::Color;
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
pub use crate::gradient::*;
use crate::renderer::{RenderParams, format_transform_matrix};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use std::fmt::Write;
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum GradientType {
#[default]
Linear,
Radial,
}
// TODO: Someday we could switch this to a Box[T] to avoid over-allocation
// TODO: Use linear not gamma colors
/// A list of colors associated with positions (in the range 0 to 1) along a gradient.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct GradientStops(Vec<(f64, Color)>);
impl std::hash::Hash for GradientStops {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.len().hash(state);
self.0.iter().for_each(|(position, color)| {
position.to_bits().hash(state);
color.hash(state);
});
}
}
impl Default for GradientStops {
fn default() -> Self {
Self(vec![(0., Color::BLACK), (1., Color::WHITE)])
}
}
impl IntoIterator for GradientStops {
type Item = (f64, Color);
type IntoIter = std::vec::IntoIter<(f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a GradientStops {
type Item = &'a (f64, Color);
type IntoIter = std::slice::Iter<'a, (f64, Color)>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl std::ops::Index<usize> for GradientStops {
type Output = (f64, Color);
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl std::ops::Deref for GradientStops {
type Target = Vec<(f64, Color)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for GradientStops {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl GradientStops {
pub fn new(stops: Vec<(f64, Color)>) -> Self {
let mut stops = Self(stops);
stops.sort();
stops
}
pub fn evaluate(&self, t: f64) -> Color {
if self.0.is_empty() {
return Color::BLACK;
}
if t <= self.0[0].0 {
return self.0[0].1;
}
if t >= self.0[self.0.len() - 1].0 {
return self.0[self.0.len() - 1].1;
}
for i in 0..self.0.len() - 1 {
let (t1, c1) = self.0[i];
let (t2, c2) = self.0[i + 1];
if t >= t1 && t <= t2 {
let normalized_t = (t - t1) / (t2 - t1);
return c1.lerp(&c2, normalized_t as f32);
}
}
Color::BLACK
}
pub fn sort(&mut self) {
self.0.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
}
pub fn reversed(&self) -> Self {
Self(self.0.iter().rev().map(|(position, color)| (1. - position, *color)).collect())
}
pub fn map_colors<F: Fn(&Color) -> Color>(&self, f: F) -> Self {
Self(self.0.iter().map(|(position, color)| (*position, f(color))).collect())
}
}
/// A gradient fill.
///
/// Contains the start and end points, along with the colors at varying points along the length.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub struct Gradient {
pub stops: GradientStops,
pub gradient_type: GradientType,
pub start: DVec2,
pub end: DVec2,
pub transform: DAffine2,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: GradientStops::default(),
gradient_type: GradientType::Linear,
start: DVec2::new(0., 0.5),
end: DVec2::new(1., 0.5),
transform: DAffine2::IDENTITY,
}
}
}
impl std::hash::Hash for Gradient {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.stops.0.len().hash(state);
[].iter()
.chain(self.start.to_array().iter())
.chain(self.end.to_array().iter())
.chain(self.transform.to_cols_array().iter())
.chain(self.stops.0.iter().map(|(position, _)| position))
.for_each(|x| x.to_bits().hash(state));
self.stops.0.iter().for_each(|(_, color)| color.hash(state));
self.gradient_type.hash(state);
}
}
impl std::fmt::Display for Gradient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let round = |x: f64| (x * 1e3).round() / 1e3;
let stops = self
.stops
.0
.iter()
.map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb()))
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} Gradient: {stops}", self.gradient_type)
}
}
impl Gradient {
/// Constructs a new gradient with the colors at 0 and 1 specified.
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
Gradient {
start,
end,
stops: GradientStops::new(vec![(0., start_color.to_gamma_srgb()), (1., end_color.to_gamma_srgb())]),
transform,
gradient_type,
}
}
pub fn lerp(&self, other: &Self, time: f64) -> Self {
let start = self.start + (other.start - self.start) * time;
let end = self.end + (other.end - self.end) * time;
let transform = self.transform;
let stops = self
.stops
.0
.iter()
.zip(other.stops.0.iter())
.map(|((a_pos, a_color), (b_pos, b_color))| {
let position = a_pos + (b_pos - a_pos) * time;
let color = a_color.lerp(b_color, time as f32);
(position, color)
})
.collect::<Vec<_>>();
let stops = GradientStops::new(stops);
let gradient_type = if time < 0.5 { self.gradient_type } else { other.gradient_type };
Self {
start,
end,
transform,
stops,
gradient_type,
}
}
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 {
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
@ -268,44 +64,6 @@ impl Gradient {
gradient_id
}
/// Insert a stop into the gradient, the index if successful
pub fn insert_stop(&mut self, mouse: DVec2, transform: DAffine2) -> Option<usize> {
// Transform the start and end positions to the same coordinate space as the mouse.
let (start, end) = (transform.transform_point2(self.start), transform.transform_point2(self.end));
// Calculate the new position by finding the closest point on the line
let new_position = ((end - start).angle_to(mouse - start)).cos() * start.distance(mouse) / start.distance(end);
// Don't insert point past end of line
if !(0. ..=1.).contains(&new_position) {
return None;
}
// Compute the color of the inserted stop
let get_color = |index: usize, time: f64| match (self.stops.0[index].1, self.stops.0.get(index + 1).map(|(_, c)| *c)) {
// Lerp between the nearest colors if applicable
(a, Some(b)) => a.lerp(
&b,
((time - self.stops.0[index].0) / self.stops.0.get(index + 1).map(|end| end.0 - self.stops.0[index].0).unwrap_or_default()) as f32,
),
// Use the start or the end color if applicable
(v, _) => v,
};
// Compute the correct index to keep the positions in order
let mut index = 0;
while self.stops.0.len() > index && self.stops.0[index].0 <= new_position {
index += 1;
}
let new_color = get_color(index - 1, new_position);
// Insert the new stop
self.stops.0.insert(index, (new_position, new_color));
Some(index)
}
}
/// Describes the fill of a layer.

View file

@ -5,7 +5,7 @@ mod modification;
use super::misc::{dvec2_to_point, point_to_dvec2};
use super::style::{PathStyle, Stroke};
use crate::instances::Instances;
use crate::renderer::{ClickTargetType, FreePoint};
use crate::vector::click_target::{ClickTargetType, FreePoint};
use crate::{AlphaBlending, Color, GraphicGroupTable};
pub use attributes::*;
use bezier_rs::ManipulatorGroup;

View file

@ -3,8 +3,8 @@ use glam::{DAffine2, DVec2};
use graph_craft::generic::FnNode;
use graph_craft::proto::FutureWrapperNode;
use graphene_core::instances::Instance;
use graphene_core::math::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::adjustments::blend_colors;
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
use graphene_core::raster::brush_cache::BrushCache;
use graphene_core::raster::image::Image;
use graphene_core::raster::{Alpha, BitmapMut, BlendMode, Color, Pixel, Sample};

View file

@ -2,11 +2,11 @@ use dyn_any::DynAny;
use fastnoise_lite;
use glam::{DAffine2, DVec2, Vec2};
use graphene_core::instances::Instance;
use graphene_core::raster::bbox::Bbox;
use graphene_core::math::bbox::Bbox;
pub use graphene_core::raster::*;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::transform::Transform;
use graphene_core::{AlphaBlending, Ctx, ExtractFootprint};
use graphene_core::{Ctx, ExtractFootprint};
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
use std::fmt::Debug;

View file

@ -5,7 +5,7 @@ use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
#[cfg(target_arch = "wasm32")]
use graphene_core::instances::Instances;
#[cfg(target_arch = "wasm32")]
use graphene_core::raster::bbox::Bbox;
use graphene_core::math::bbox::Bbox;
use graphene_core::raster::image::Image;
use graphene_core::raster_types::{CPU, Raster, RasterDataTable};
use graphene_core::renderer::RenderMetadata;

View file

@ -12,14 +12,20 @@ use graphene_core::{NodeIO, NodeIOTypes};
use graphene_core::{fn_type_fut, future};
use graphene_std::Context;
use graphene_std::GraphicElement;
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, IntoTypeErasedNode};
#[cfg(feature = "gpu")]
use graphene_std::any::DowncastBothNode;
use graphene_std::any::{ComposeTypeErased, DynAnyNode, IntoTypeErasedNode};
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
use graphene_std::wasm_application_io::*;
#[cfg(feature = "gpu")]
use graphene_std::wasm_application_io::{WasmEditorApi, WasmSurfaceHandle};
use node_registry_macros::{async_node, convert_node, into_node};
use once_cell::sync::Lazy;
use std::collections::HashMap;
#[cfg(feature = "gpu")]
use std::sync::Arc;
use wgpu_executor::{WgpuExecutor, WgpuSurface, WindowHandle};
#[cfg(feature = "gpu")]
use wgpu_executor::WgpuExecutor;
use wgpu_executor::{WgpuSurface, WindowHandle};
// TODO: turn into hashmap
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {

View file

@ -168,7 +168,7 @@ fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum)
WidgetHint::Dropdown => quote! { Dropdown },
};
Ok(quote! {
impl #crate_name::vector::misc::AsU32 for #name {
impl #crate_name::AsU32 for #name {
fn as_u32(&self) -> u32 {
*self as u32
}