mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Brush blend modes and erase/restore (#1261)
* Made blit node numerically stable. * Added blend mode parameter to brush strokes. * Fixed difference blend mode. * Added erase/restore blend modes. * Added blend mode and draw mode widgets. * Added comment explaining the ImageFrame.transform. * Initial blit/blend version. * Working version of erase/restore. * Improved inlining for blend functions. * Dsiable the blend mode selector in erase/draw mode. * Fixed incorrect bounds calculation. * Use factor instead of percentage for opacity * Rearrange options bar widgets * Tidy up blend modes * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
4e1bfddcd8
commit
5558deba5e
@@ -199,6 +199,7 @@ pub trait Sample {
|
||||
impl<'i, T: Sample> Sample for &'i T {
|
||||
type Pixel = T::Pixel;
|
||||
|
||||
#[inline(always)]
|
||||
fn sample(&self, pos: DVec2, area: DVec2) -> Option<Self::Pixel> {
|
||||
(**self).sample(pos, area)
|
||||
}
|
||||
|
||||
@@ -46,37 +46,28 @@ impl core::fmt::Display for LuminanceCalculation {
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
pub fn list() -> [BlendMode; 29] {
|
||||
pub fn list() -> [&'static [BlendMode]; 6] {
|
||||
[
|
||||
BlendMode::Normal,
|
||||
BlendMode::Multiply,
|
||||
BlendMode::Darken,
|
||||
BlendMode::ColorBurn,
|
||||
BlendMode::LinearBurn,
|
||||
BlendMode::DarkerColor,
|
||||
BlendMode::Screen,
|
||||
BlendMode::Lighten,
|
||||
BlendMode::ColorDodge,
|
||||
BlendMode::LinearDodge,
|
||||
BlendMode::LighterColor,
|
||||
BlendMode::Overlay,
|
||||
BlendMode::SoftLight,
|
||||
BlendMode::HardLight,
|
||||
BlendMode::VividLight,
|
||||
BlendMode::LinearLight,
|
||||
BlendMode::PinLight,
|
||||
BlendMode::HardMix,
|
||||
BlendMode::Difference,
|
||||
BlendMode::Exclusion,
|
||||
BlendMode::Subtract,
|
||||
BlendMode::Divide,
|
||||
BlendMode::Hue,
|
||||
BlendMode::Saturation,
|
||||
BlendMode::Color,
|
||||
BlendMode::Luminosity,
|
||||
BlendMode::InsertRed,
|
||||
BlendMode::InsertGreen,
|
||||
BlendMode::InsertBlue,
|
||||
// Normal group
|
||||
&[BlendMode::Normal],
|
||||
// Darken group
|
||||
&[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor],
|
||||
// Lighten group
|
||||
&[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge, BlendMode::LinearDodge, BlendMode::LighterColor],
|
||||
// Contrast group
|
||||
&[
|
||||
BlendMode::Overlay,
|
||||
BlendMode::SoftLight,
|
||||
BlendMode::HardLight,
|
||||
BlendMode::VividLight,
|
||||
BlendMode::LinearLight,
|
||||
BlendMode::PinLight,
|
||||
BlendMode::HardMix,
|
||||
],
|
||||
// Inversion group
|
||||
&[BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract, BlendMode::Divide],
|
||||
// Component group
|
||||
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -84,7 +75,7 @@ impl BlendMode {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)]
|
||||
#[repr(i32)] // TODO: Enable Int8 capability for SPRIV so that we don't need this?
|
||||
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
|
||||
pub enum BlendMode {
|
||||
#[default]
|
||||
// Basic group
|
||||
@@ -126,29 +117,30 @@ pub enum BlendMode {
|
||||
Color,
|
||||
Luminosity,
|
||||
|
||||
// Other Stuff
|
||||
InsertRed,
|
||||
InsertGreen,
|
||||
InsertBlue,
|
||||
// Other stuff
|
||||
Erase,
|
||||
Restore,
|
||||
MultiplyAlpha,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BlendMode {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => write!(f, "Normal"),
|
||||
|
||||
BlendMode::Multiply => write!(f, "Multiply"),
|
||||
// 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"),
|
||||
|
||||
BlendMode::Screen => write!(f, "Screen"),
|
||||
// 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"),
|
||||
@@ -156,64 +148,24 @@ impl core::fmt::Display for BlendMode {
|
||||
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"),
|
||||
|
||||
BlendMode::InsertRed => write!(f, "Insert Red"),
|
||||
BlendMode::InsertGreen => write!(f, "Insert Green"),
|
||||
BlendMode::InsertBlue => write!(f, "Insert Blue"),
|
||||
// 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_primtive_string(blend_mode: &BlendMode) -> &'static str {
|
||||
match blend_mode {
|
||||
BlendMode::Normal => "Normal",
|
||||
|
||||
BlendMode::Multiply => "Multiply",
|
||||
BlendMode::Darken => "Darken",
|
||||
BlendMode::ColorBurn => "ColorBurn",
|
||||
BlendMode::LinearBurn => "LinearBurn",
|
||||
BlendMode::DarkerColor => "DarkerColor",
|
||||
|
||||
BlendMode::Screen => "Screen",
|
||||
BlendMode::Lighten => "Lighten",
|
||||
BlendMode::ColorDodge => "ColorDodge",
|
||||
BlendMode::LinearDodge => "LinearDodge",
|
||||
BlendMode::LighterColor => "LighterColor",
|
||||
|
||||
BlendMode::Overlay => "Overlay",
|
||||
BlendMode::SoftLight => "SoftLight",
|
||||
BlendMode::HardLight => "HardLight",
|
||||
BlendMode::VividLight => "VividLight",
|
||||
BlendMode::LinearLight => "LinearLight",
|
||||
BlendMode::PinLight => "PinLight",
|
||||
BlendMode::HardMix => "HardMix",
|
||||
|
||||
BlendMode::Difference => "Difference",
|
||||
BlendMode::Exclusion => "Exclusion",
|
||||
BlendMode::Subtract => "Subtract",
|
||||
BlendMode::Divide => "Divide",
|
||||
|
||||
BlendMode::Hue => "Hue",
|
||||
BlendMode::Saturation => "Saturation",
|
||||
BlendMode::Color => "Color",
|
||||
BlendMode::Luminosity => "Luminosity",
|
||||
|
||||
BlendMode::InsertRed => "InsertRed",
|
||||
BlendMode::InsertGreen => "InsertGreen",
|
||||
BlendMode::InsertBlue => "InsertBlue",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct LuminanceNode<LuminanceCalculation> {
|
||||
luminance_calc: LuminanceCalculation,
|
||||
@@ -452,24 +404,27 @@ pub struct BlendNode<BlendMode, Opacity> {
|
||||
|
||||
#[node_macro::node_fn(BlendNode)]
|
||||
fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color {
|
||||
let opacity = opacity as f32 / 100.;
|
||||
|
||||
let (foreground, background) = input;
|
||||
blend_colors(input.0, input.1, blend_mode, opacity as f32 / 100.)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, opacity: f32) -> Color {
|
||||
let target_color = match blend_mode {
|
||||
// Normal group
|
||||
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal),
|
||||
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
|
||||
// Darken group
|
||||
BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken),
|
||||
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
|
||||
BlendMode::ColorBurn => background.blend_rgb(foreground, Color::blend_color_burn),
|
||||
BlendMode::LinearBurn => background.blend_rgb(foreground, Color::blend_linear_burn),
|
||||
BlendMode::DarkerColor => background.blend_darker_color(foreground),
|
||||
|
||||
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten),
|
||||
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
|
||||
BlendMode::ColorDodge => background.blend_rgb(foreground, Color::blend_color_dodge),
|
||||
BlendMode::LinearDodge => background.blend_rgb(foreground, Color::blend_linear_dodge),
|
||||
BlendMode::LighterColor => background.blend_lighter_color(foreground),
|
||||
|
||||
// Contrast group
|
||||
BlendMode::Overlay => foreground.blend_rgb(background, Color::blend_hardlight),
|
||||
BlendMode::SoftLight => background.blend_rgb(foreground, Color::blend_softlight),
|
||||
BlendMode::HardLight => background.blend_rgb(foreground, Color::blend_hardlight),
|
||||
@@ -477,20 +432,20 @@ fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Col
|
||||
BlendMode::LinearLight => background.blend_rgb(foreground, Color::blend_linear_light),
|
||||
BlendMode::PinLight => background.blend_rgb(foreground, Color::blend_pin_light),
|
||||
BlendMode::HardMix => background.blend_rgb(foreground, Color::blend_hard_mix),
|
||||
|
||||
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_exclusion),
|
||||
// Inversion group
|
||||
BlendMode::Difference => background.blend_rgb(foreground, Color::blend_difference),
|
||||
BlendMode::Exclusion => background.blend_rgb(foreground, Color::blend_exclusion),
|
||||
BlendMode::Subtract => background.blend_rgb(foreground, Color::blend_subtract),
|
||||
BlendMode::Divide => background.blend_rgb(foreground, Color::blend_divide),
|
||||
|
||||
// Component group
|
||||
BlendMode::Hue => background.blend_hue(foreground),
|
||||
BlendMode::Saturation => background.blend_saturation(foreground),
|
||||
BlendMode::Color => background.blend_color(foreground),
|
||||
BlendMode::Luminosity => background.blend_luminosity(foreground),
|
||||
|
||||
BlendMode::InsertRed => foreground.with_red(background.r()),
|
||||
BlendMode::InsertGreen => foreground.with_green(background.g()),
|
||||
BlendMode::InsertBlue => foreground.with_blue(background.b()),
|
||||
// Other utility blend modes (hidden from the normal list)
|
||||
BlendMode::Erase => return background.alpha_subtract(foreground),
|
||||
BlendMode::Restore => return background.alpha_add(foreground),
|
||||
BlendMode::MultiplyAlpha => return background.alpha_multiply(foreground),
|
||||
};
|
||||
|
||||
background.alpha_blend(target_color.to_associated_alpha(opacity))
|
||||
|
||||
@@ -65,6 +65,15 @@ impl Bbox {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_transform(transform: DAffine2) -> Self {
|
||||
Self {
|
||||
top_left: transform.transform_point2(DVec2::new(0., 1.)),
|
||||
top_right: transform.transform_point2(DVec2::new(1., 1.)),
|
||||
bottom_left: transform.transform_point2(DVec2::new(0., 0.)),
|
||||
bottom_right: transform.transform_point2(DVec2::new(1., 0.)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn affine_transform(self, transform: DAffine2) -> Self {
|
||||
Self {
|
||||
top_left: transform.transform_point2(self.top_left),
|
||||
|
||||
@@ -938,6 +938,30 @@ impl Color {
|
||||
alpha: self.alpha * inv_alpha + other.alpha,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn alpha_add(&self, other: Color) -> Self {
|
||||
Self {
|
||||
alpha: (self.alpha + other.alpha).clamp(0., 1.),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn alpha_subtract(&self, other: Color) -> Self {
|
||||
Self {
|
||||
alpha: (self.alpha - other.alpha).clamp(0., 1.),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn alpha_multiply(&self, other: Color) -> Self {
|
||||
Self {
|
||||
alpha: (self.alpha * other.alpha).clamp(0., 1.),
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -67,12 +67,15 @@ where
|
||||
|
||||
impl<P: Copy + Pixel> Raster for Image<P> {
|
||||
type Pixel = P;
|
||||
#[inline(always)]
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
|
||||
self.data.get((x + y * self.width) as usize).copied()
|
||||
}
|
||||
#[inline(always)]
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
#[inline(always)]
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
@@ -237,6 +240,16 @@ fn map_node<P: Pixel>(input: (u32, u32), data: Vec<P>) -> Image<P> {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ImageFrame<P: Pixel> {
|
||||
pub image: Image<P>,
|
||||
|
||||
// The transform that maps image space to layer space.
|
||||
//
|
||||
// Image space is unitless [0, 1] for both axes, with x axis positive
|
||||
// going right and y axis positive going down, with the origin lying at
|
||||
// the topleft of the image and (1, 1) lying at the bottom right of the image.
|
||||
//
|
||||
// Layer space has pixels as its units for both axes, with the x axis
|
||||
// positive going right and y axis positive going down, with the origin
|
||||
// being an unspecified quantity.
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
@@ -244,6 +257,7 @@ impl<P: Debug + Copy + Pixel> Sample for ImageFrame<P> {
|
||||
type Pixel = P;
|
||||
|
||||
// TODO: Improve sampling logic
|
||||
#[inline(always)]
|
||||
fn sample(&self, pos: DVec2, _area: DVec2) -> Option<Self::Pixel> {
|
||||
let image_size = DVec2::new(self.image.width() as f64, self.image.height() as f64);
|
||||
let pos = (DAffine2::from_scale(image_size) * self.transform.inverse()).transform_point2(pos);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::raster::bbox::AxisAlignedBbox;
|
||||
use crate::raster::BlendMode;
|
||||
use crate::Color;
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
@@ -14,6 +15,7 @@ pub struct BrushStyle {
|
||||
pub hardness: f64,
|
||||
pub flow: f64,
|
||||
pub spacing: f64, // Spacing as a fraction of the diameter.
|
||||
pub blend_mode: BlendMode,
|
||||
}
|
||||
|
||||
impl Default for BrushStyle {
|
||||
@@ -24,6 +26,7 @@ impl Default for BrushStyle {
|
||||
hardness: 50.,
|
||||
flow: 100.,
|
||||
spacing: 50., // Percentage of diameter.
|
||||
blend_mode: BlendMode::Normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +44,8 @@ impl Hash for BrushStyle {
|
||||
#[derive(Clone, Debug, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct BrushInputSample {
|
||||
// The position of the sample in layer space, in pixels.
|
||||
// The origin of layer space is not specified.
|
||||
pub position: DVec2,
|
||||
// Future work: pressure, stylus angle, etc.
|
||||
}
|
||||
@@ -63,11 +68,11 @@ pub struct BrushStroke {
|
||||
impl BrushStroke {
|
||||
pub fn bounding_box(&self) -> AxisAlignedBbox {
|
||||
let radius = self.style.diameter / 2.;
|
||||
self.trace
|
||||
self.compute_blit_points()
|
||||
.iter()
|
||||
.map(|sample| AxisAlignedBbox {
|
||||
start: sample.position + DVec2::new(-radius, -radius),
|
||||
end: sample.position + DVec2::new(radius, radius),
|
||||
.map(|pos| AxisAlignedBbox {
|
||||
start: *pos + DVec2::new(-radius, -radius),
|
||||
end: *pos + DVec2::new(radius, radius),
|
||||
})
|
||||
.reduce(|a, b| a.union(&b))
|
||||
.unwrap_or(AxisAlignedBbox::ZERO)
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::executor::Any;
|
||||
pub use crate::imaginate_input::{ImaginateMaskStartingFill, ImaginateSamplingMethod, ImaginateStatus};
|
||||
use crate::proto::{Any as DAny, FutureAny};
|
||||
|
||||
use graphene_core::raster::{to_primtive_string, BlendMode, LuminanceCalculation};
|
||||
use graphene_core::raster::{BlendMode, LuminanceCalculation};
|
||||
use graphene_core::{Color, Node, Type};
|
||||
|
||||
use dyn_any::DynAny;
|
||||
@@ -188,7 +188,7 @@ impl<'a> TaggedValue {
|
||||
TaggedValue::F32(x) => x.to_string() + "_f32",
|
||||
TaggedValue::F64(x) => x.to_string() + "_f64",
|
||||
TaggedValue::Bool(x) => x.to_string(),
|
||||
TaggedValue::BlendMode(blend_mode) => "BlendMode::".to_string() + to_primtive_string(blend_mode),
|
||||
TaggedValue::BlendMode(blend_mode) => "BlendMode::".to_string() + &blend_mode.to_string(),
|
||||
_ => panic!("Cannot convert to primitive string"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
use std::marker::PhantomData;
|
||||
use crate::raster::{blend_image_closure, BlendImageTupleNode, EmptyImageNode};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::raster::{Alpha, Color, ImageFrame, Pixel, Sample};
|
||||
use graphene_core::raster::adjustments::blend_colors;
|
||||
use graphene_core::raster::{Alpha, Color, Image, ImageFrame, Pixel, Sample};
|
||||
use graphene_core::raster::{BlendMode, BlendNode};
|
||||
use graphene_core::transform::{Transform, TransformMut};
|
||||
use graphene_core::value::{CopiedNode, ValueNode};
|
||||
use graphene_core::vector::brush_stroke::BrushStyle;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::Node;
|
||||
use node_macro::node_fn;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ReduceNode<Initial, Lambda> {
|
||||
pub initial: Initial,
|
||||
@@ -159,15 +165,20 @@ pub struct BlitNode<P, Texture, Positions, BlendFn> {
|
||||
}
|
||||
|
||||
#[node_fn(BlitNode<_P>)]
|
||||
fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<_P>
|
||||
fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: Image<_P>, positions: Vec<DVec2>, blend_mode: BlendFn) -> ImageFrame<_P>
|
||||
where
|
||||
BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
|
||||
{
|
||||
if positions.len() == 0 {
|
||||
return target;
|
||||
}
|
||||
|
||||
let target_size = DVec2::new(target.image.width as f64, target.image.height as f64);
|
||||
let texture_size = DVec2::new(texture.width as f64, texture.height as f64);
|
||||
let document_to_target = DAffine2::from_translation(-texture_size / 2.) * DAffine2::from_scale(target_size) * target.transform.inverse();
|
||||
|
||||
for position in positions {
|
||||
let target_size = DVec2::new(target.image.width as f64, target.image.height as f64);
|
||||
let texture_size = DVec2::new(texture.image.width as f64, texture.image.height as f64);
|
||||
let document_to_target = target.transform.inverse();
|
||||
let start = document_to_target.transform_point2(position) * target_size - texture_size / 2.;
|
||||
let start = document_to_target.transform_point2(position).round();
|
||||
let stop = start + texture_size;
|
||||
|
||||
// Half-open integer ranges [start, stop).
|
||||
@@ -178,17 +189,17 @@ where
|
||||
let blit_area_dimensions = (clamp_stop - clamp_start).min(texture_size.as_uvec2() - blit_area_offset);
|
||||
|
||||
// Tight blitting loop. Eagerly assert bounds to hopefully eliminate bounds check inside loop.
|
||||
let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.image.width as usize) + (x as usize) };
|
||||
let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.width as usize) + (x as usize) };
|
||||
let target_index = |x: u32, y: u32| -> usize { (y as usize * target.image.width as usize) + (x as usize) };
|
||||
|
||||
let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1);
|
||||
let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1);
|
||||
assert!(texture_index(max_x, max_y) < texture.image.data.len());
|
||||
assert!(texture_index(max_x, max_y) < texture.data.len());
|
||||
assert!(target_index(max_x, max_y) < target.image.data.len());
|
||||
|
||||
for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y {
|
||||
for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x {
|
||||
let src_pixel = texture.image.data[texture_index(x, y)];
|
||||
let src_pixel = texture.data[texture_index(x, y)];
|
||||
let dst_pixel = &mut target.image.data[target_index(x + clamp_start.x, y + clamp_start.y)];
|
||||
*dst_pixel = blend_mode.eval((src_pixel, *dst_pixel));
|
||||
}
|
||||
@@ -198,6 +209,76 @@ where
|
||||
target
|
||||
}
|
||||
|
||||
pub fn create_brush_texture(brush_style: BrushStyle) -> Image<Color> {
|
||||
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(brush_style.color), CopiedNode::new(brush_style.hardness), CopiedNode::new(brush_style.flow));
|
||||
let stamp = stamp.eval(brush_style.diameter);
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.));
|
||||
let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform);
|
||||
let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
|
||||
let blend_executor = BlendImageTupleNode::new(ValueNode::new(normal_blend));
|
||||
blend_executor.eval((blank_texture, stamp)).image
|
||||
}
|
||||
|
||||
macro_rules! inline_blend_funcs {
|
||||
($bg:ident, $fg:ident, $blend_mode:ident, $opacity:ident, [$($mode:path,)*]) => {
|
||||
match std::hint::black_box($blend_mode) {
|
||||
$(
|
||||
$mode => {
|
||||
blend_image_closure($fg, $bg, |a, b| blend_colors(a, b, $mode, $opacity))
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn blend_with_mode(background: ImageFrame<Color>, foreground: ImageFrame<Color>, blend_mode: BlendMode, opacity: f32) -> ImageFrame<Color> {
|
||||
let opacity = opacity / 100.;
|
||||
inline_blend_funcs!(
|
||||
background,
|
||||
foreground,
|
||||
blend_mode,
|
||||
opacity,
|
||||
[
|
||||
// Normal group
|
||||
BlendMode::Normal,
|
||||
// Darken group
|
||||
BlendMode::Darken,
|
||||
BlendMode::Multiply,
|
||||
BlendMode::ColorBurn,
|
||||
BlendMode::LinearBurn,
|
||||
BlendMode::DarkerColor,
|
||||
// Lighten group
|
||||
BlendMode::Lighten,
|
||||
BlendMode::Screen,
|
||||
BlendMode::ColorDodge,
|
||||
BlendMode::LinearDodge,
|
||||
BlendMode::LighterColor,
|
||||
// Contrast group
|
||||
BlendMode::Overlay,
|
||||
BlendMode::SoftLight,
|
||||
BlendMode::HardLight,
|
||||
BlendMode::VividLight,
|
||||
BlendMode::LinearLight,
|
||||
BlendMode::PinLight,
|
||||
BlendMode::HardMix,
|
||||
// Inversion group
|
||||
BlendMode::Difference,
|
||||
BlendMode::Exclusion,
|
||||
BlendMode::Subtract,
|
||||
BlendMode::Divide,
|
||||
// Component group
|
||||
BlendMode::Hue,
|
||||
BlendMode::Saturation,
|
||||
BlendMode::Color,
|
||||
BlendMode::Luminosity,
|
||||
// Other utility blend modes (hidden from the normal list)
|
||||
BlendMode::Erase,
|
||||
BlendMode::Restore,
|
||||
BlendMode::MultiplyAlpha,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
@@ -279,6 +279,17 @@ fn blend_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> + Tra
|
||||
) -> Background
|
||||
where
|
||||
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
|
||||
{
|
||||
blend_image_closure(foreground, background, |a, b| map_fn.eval((a, b)))
|
||||
}
|
||||
|
||||
pub fn blend_image_closure<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform + Sample<Pixel = _P>>(
|
||||
foreground: Frame,
|
||||
mut background: Background,
|
||||
map_fn: MapFn,
|
||||
) -> Background
|
||||
where
|
||||
MapFn: Fn(_P, _P) -> _P,
|
||||
{
|
||||
let background_size = DVec2::new(background.width() as f64, background.height() as f64);
|
||||
// Transforms a point from the background image to the forground image
|
||||
@@ -299,7 +310,7 @@ where
|
||||
|
||||
if let Some(src_pixel) = foreground.sample(fg_point, area) {
|
||||
if let Some(dst_pixel) = background.get_pixel_mut(x, y) {
|
||||
*dst_pixel = map_fn.eval((src_pixel, *dst_pixel));
|
||||
*dst_pixel = map_fn(src_pixel, *dst_pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use graph_craft::proto::{NodeConstructor, TypeErasedPinned};
|
||||
use graphene_core::ops::IdNode;
|
||||
use graphene_core::quantization::QuantizationChannels;
|
||||
use graphene_core::raster::bbox::AxisAlignedBbox;
|
||||
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
|
||||
use graphene_core::raster::color::Color;
|
||||
use graphene_core::structural::Then;
|
||||
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
|
||||
@@ -14,6 +14,7 @@ use graphene_core::{fn_type, raster::*};
|
||||
use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor};
|
||||
use graphene_core::{Node, NodeIO, NodeIOTypes};
|
||||
use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode};
|
||||
use graphene_std::brush;
|
||||
use graphene_std::raster::BlendImageTupleNode;
|
||||
use graphene_std::raster::*;
|
||||
|
||||
@@ -311,8 +312,11 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||
let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]);
|
||||
let strokes: DowncastBothNode<(), Vec<BrushStroke>> = DowncastBothNode::new(args[2]);
|
||||
|
||||
let strokes = strokes.eval(()).await;
|
||||
let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
|
||||
let image_val = image.eval(()).await;
|
||||
let strokes_val = strokes.eval(()).await;
|
||||
let stroke_bbox = strokes_val.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
|
||||
let image_bbox = Bbox::from_transform(image_val.transform).to_axis_aligned_bbox();
|
||||
let bbox = stroke_bbox.union(&image_bbox);
|
||||
|
||||
let mut background_bounds = CopiedNode::new(bbox.to_transform());
|
||||
let bounds_transform = bounds.eval(()).await.transform;
|
||||
@@ -320,30 +324,66 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||
background_bounds = CopiedNode::new(bounds_transform);
|
||||
}
|
||||
|
||||
let has_erase_strokes = strokes_val.iter().any(|s| s.style.blend_mode == BlendMode::Erase);
|
||||
let blank_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
|
||||
let background = image.and_then(ExtendImageNode::new(blank_image));
|
||||
let opaque_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::WHITE)));
|
||||
let mut erase_restore_mask = has_erase_strokes.then(|| opaque_image.eval(()));
|
||||
let mut actual_image = ExtendImageNode::new(blank_image).eval(image_val);
|
||||
for stroke in strokes_val {
|
||||
let normal_blend = BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
|
||||
|
||||
let mut blits = Vec::new();
|
||||
for stroke in strokes {
|
||||
let stamp = BrushStampGeneratorNode::new(CopiedNode::new(stroke.style.color), CopiedNode::new(stroke.style.hardness), CopiedNode::new(stroke.style.flow));
|
||||
let stamp = stamp.eval(stroke.style.diameter);
|
||||
// Create brush texture.
|
||||
// TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes.
|
||||
let brush_texture = brush::create_brush_texture(stroke.style.clone());
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(stroke.style.diameter), 0., -DVec2::splat(stroke.style.diameter / 2.0));
|
||||
let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform);
|
||||
// Compute transformation from stroke texture space into layer space, and create the stroke texture.
|
||||
let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect();
|
||||
let mut bbox = stroke.bounding_box();
|
||||
bbox.start = bbox.start.floor();
|
||||
bbox.end = bbox.end.floor();
|
||||
let stroke_size = bbox.size() + DVec2::splat(stroke.style.diameter);
|
||||
// For numerical stability we want to place the first blit point at a stable, integer offset
|
||||
// in layer space.
|
||||
let snap_offset = positions[0].floor() - positions[0];
|
||||
let stroke_origin_in_layer = bbox.start - snap_offset - DVec2::splat(stroke.style.diameter / 2.);
|
||||
let stroke_to_layer = DAffine2::from_translation(stroke_origin_in_layer) * DAffine2::from_scale(stroke_size);
|
||||
|
||||
let blend_params = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
|
||||
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
|
||||
let texture = blend_executor.eval((blank_texture, stamp));
|
||||
match stroke.style.blend_mode {
|
||||
BlendMode::Erase => {
|
||||
if let Some(mask) = erase_restore_mask {
|
||||
let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Erase), CopiedNode::new(100.));
|
||||
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
|
||||
erase_restore_mask = Some(blit_node.eval(mask));
|
||||
}
|
||||
}
|
||||
|
||||
let translations: Vec<_> = stroke.compute_blit_points().into_iter().collect();
|
||||
let blit_node = BlitNode::new(ClonedNode::new(texture), ClonedNode::new(translations), ClonedNode::new(blend_params));
|
||||
blits.push(blit_node);
|
||||
// Yes, this is essentially the same as the above, but we duplicate to inline the blend mode.
|
||||
BlendMode::Restore => {
|
||||
if let Some(mask) = erase_restore_mask {
|
||||
let blend_params = BlendNode::new(CopiedNode::new(BlendMode::Restore), CopiedNode::new(100.));
|
||||
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(blend_params));
|
||||
erase_restore_mask = Some(blit_node.eval(mask));
|
||||
}
|
||||
}
|
||||
|
||||
blend_mode => {
|
||||
let blit_node = BlitNode::new(ClonedNode::new(brush_texture), ClonedNode::new(positions), ClonedNode::new(normal_blend));
|
||||
let empty_stroke_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(stroke_to_layer);
|
||||
let stroke_texture = blit_node.eval(empty_stroke_texture);
|
||||
// TODO: Is this the correct way to do opacity in blending?
|
||||
actual_image = brush::blend_with_mode(actual_image, stroke_texture, blend_mode, stroke.style.color.a() * 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let all_blits = ChainApplyNode::new(background);
|
||||
let node = ClonedNode::new(blits.into_iter()).then(all_blits);
|
||||
if let Some(mask) = erase_restore_mask {
|
||||
let blend_params = BlendNode::new(CopiedNode::new(BlendMode::MultiplyAlpha), CopiedNode::new(100.));
|
||||
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
|
||||
actual_image = blend_executor.eval((actual_image, mask));
|
||||
}
|
||||
|
||||
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node));
|
||||
// TODO: there *has* to be a better way to do this.
|
||||
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(FutureWrapperNode::new(ClonedNode::new(actual_image))));
|
||||
Box::pin(any) as TypeErasedPinned
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user