mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
c797877763
commit
2ddae98bcf
44 changed files with 1407 additions and 1341 deletions
|
@ -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
|
||||
|
|
271
node-graph/gcore/src/blending.rs
Normal file
271
node-graph/gcore/src/blending.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
}
|
175
node-graph/gcore/src/blending_nodes.rs
Normal file
175
node-graph/gcore/src/blending_nodes.rs
Normal 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
|
||||
}
|
|
@ -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> {
|
205
node-graph/gcore/src/color/color_traits.rs
Normal file
205
node-graph/gcore/src/color/color_traits.rs
Normal 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)
|
||||
}
|
||||
}
|
7
node-graph/gcore/src/color/mod.rs
Normal file
7
node-graph/gcore/src/color/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod color;
|
||||
mod color_traits;
|
||||
mod discrete_srgb;
|
||||
|
||||
pub use color::*;
|
||||
pub use color_traits::*;
|
||||
pub use discrete_srgb::*;
|
248
node-graph/gcore/src/gradient.rs
Normal file
248
node-graph/gcore/src/gradient.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
25
node-graph/gcore/src/math/math_ext.rs
Normal file
25
node-graph/gcore/src/math/math_ext.rs
Normal 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))
|
||||
}
|
||||
}
|
4
node-graph/gcore/src/math/mod.rs
Normal file
4
node-graph/gcore/src/math/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod bbox;
|
||||
pub mod math_ext;
|
||||
pub mod quad;
|
||||
pub mod rect;
|
|
@ -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] {
|
||||
[
|
|
@ -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 {
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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)]];
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
89
node-graph/gcore/src/transform_nodes.rs
Normal file
89
node-graph/gcore/src/transform_nodes.rs
Normal 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
|
||||
}
|
|
@ -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};
|
||||
|
|
162
node-graph/gcore/src/vector/click_target.rs
Normal file
162
node-graph/gcore/src/vector/click_target.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
103
node-graph/gcore/src/vector/reference_point.rs
Normal file
103
node-graph/gcore/src/vector/reference_point.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue