Implement the Brush tool (#1099)

* Implement Brush Node

* Add color Input

* Add VectorPointsNode

* Add Erase Node

* Adapt compilation infrastructure to allow non Image Frame inputs

* Remove debug output from TransformNode

* Fix transform calculation

* Fix Blending by making the brush texture use associated alpha

* Code improvements and UX polish

* Rename Opacity to Flow

* Add erase option to brush node + fix freehand tool

* Fix crash

* Revert erase implementation

* Fix flattening id calculation

* Fix some transformation issues

* Fix changing the pivot location

* Fix vector data modify bounds

* Minor fn name cleanup

* Fix some tests

* Fix tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Dennis Kobert
2023-04-11 10:35:21 +02:00
committed by Keavon Chambers
parent 758f757775
commit 589ff9a2d3
36 changed files with 1527 additions and 406 deletions

View File

@@ -60,7 +60,7 @@ where
core::any::type_name::<Self::Output>()
}
#[cfg(feature = "alloc")]
fn to_node_io(&self, parameters: Vec<(Type, Type)>) -> NodeIOTypes {
fn to_node_io(&self, parameters: Vec<Type>) -> NodeIOTypes {
NodeIOTypes {
input: concrete!(<Input as StaticType>::Static),
output: concrete!(<Self::Output as StaticType>::Static),

View File

@@ -23,7 +23,7 @@ pub struct AddParameterNode<Second> {
}
#[node_macro::node_fn(AddParameterNode)]
fn flat_map<U, T>(first: U, second: T) -> <U as Add<T>>::Output
fn add_parameter<U, T>(first: U, second: T) -> <U as Add<T>>::Output
where
U: Add<T>,
{

View File

@@ -239,12 +239,11 @@ fn brighten_color_node(color: Color, brightness: f32) -> Color {
}
#[derive(Debug)]
pub struct ForEachNode<Iter, MapNode> {
pub struct ForEachNode<MapNode> {
map_node: MapNode,
_iter: PhantomData<Iter>,
}
#[node_macro::node_fn(ForEachNode<_Iter>)]
#[node_macro::node_fn(ForEachNode)]
fn map_node<_Iter: Iterator, MapNode>(input: _Iter, map_node: &'any_input MapNode) -> ()
where
MapNode: for<'any_input> Node<'any_input, _Iter::Item, Output = ()> + 'input,
@@ -359,6 +358,15 @@ mod image {
data: Vec::new(),
}
}
pub fn new(width: u32, height: u32, color: Color) -> Self {
Self {
width,
height,
data: vec![color; (width * height) as usize],
}
}
pub fn as_slice(&self) -> ImageSlice {
ImageSlice {
width: self.width,
@@ -366,6 +374,15 @@ mod image {
data: self.data.as_slice(),
}
}
pub fn get_mut(&mut self, x: u32, y: u32) -> Option<&mut Color> {
self.data.get_mut((y * self.width + x) as usize)
}
pub fn get(&self, x: u32, y: u32) -> Option<&Color> {
self.data.get((y * self.width + x) as usize)
}
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
let data = image_data.chunks_exact(4).map(|v| Color::from_rgba8(v[0], v[1], v[2], v[3])).collect();
@@ -451,6 +468,12 @@ mod image {
}
}
impl AsRef<ImageFrame> for ImageFrame {
fn as_ref(&self) -> &ImageFrame {
self
}
}
impl Hash for ImageFrame {
fn hash<H: Hasher>(&self, state: &mut H) {
self.image.hash(state);

View File

@@ -300,7 +300,7 @@ pub struct InvertRGBNode;
#[node_macro::node_fn(InvertRGBNode)]
fn invert_image(color: Color) -> Color {
color.map_rgb(|c| 1. - c)
color.map_rgb(|c| color.a() - c)
}
#[derive(Debug, Clone, Copy)]
@@ -339,43 +339,55 @@ pub struct BlendNode<BlendMode, Opacity> {
opacity: Opacity,
}
impl<Opacity: dyn_any::StaticTypeSized, Blend: dyn_any::StaticTypeSized> StaticType for BlendNode<Blend, Opacity> {
type Static = BlendNode<Blend::Static, Opacity::Static>;
}
#[node_macro::node_fn(BlendNode)]
fn blend_node(input: (Color, Color), blend_mode: BlendMode, opacity: f64) -> Color {
let (source_color, backdrop) = input;
let actual_opacity = 1. - (opacity / 100.) as f32;
return match blend_mode {
BlendMode::Normal => backdrop.blend_rgb(source_color, Color::blend_normal),
BlendMode::Multiply => backdrop.blend_rgb(source_color, Color::blend_multiply),
BlendMode::Darken => backdrop.blend_rgb(source_color, Color::blend_darken),
BlendMode::ColorBurn => backdrop.blend_rgb(source_color, Color::blend_color_burn),
BlendMode::LinearBurn => backdrop.blend_rgb(source_color, Color::blend_linear_burn),
BlendMode::DarkerColor => backdrop.blend_darker_color(source_color),
let opacity = opacity / 100.;
BlendMode::Screen => backdrop.blend_rgb(source_color, Color::blend_screen),
BlendMode::Lighten => backdrop.blend_rgb(source_color, Color::blend_lighten),
BlendMode::ColorDodge => backdrop.blend_rgb(source_color, Color::blend_color_dodge),
BlendMode::LinearDodge => backdrop.blend_rgb(source_color, Color::blend_linear_dodge),
BlendMode::LighterColor => backdrop.blend_lighter_color(source_color),
let (foreground, background) = input;
let foreground = foreground.to_linear_srgb();
let background = background.to_linear_srgb();
BlendMode::Overlay => source_color.blend_rgb(backdrop, Color::blend_hardlight),
BlendMode::SoftLight => backdrop.blend_rgb(source_color, Color::blend_softlight),
BlendMode::HardLight => backdrop.blend_rgb(source_color, Color::blend_hardlight),
BlendMode::VividLight => backdrop.blend_rgb(source_color, Color::blend_vivid_light),
BlendMode::LinearLight => backdrop.blend_rgb(source_color, Color::blend_linear_light),
BlendMode::PinLight => backdrop.blend_rgb(source_color, Color::blend_pin_light),
BlendMode::HardMix => backdrop.blend_rgb(source_color, Color::blend_hard_mix),
let target_color = match blend_mode {
BlendMode::Normal => background.blend_rgb(foreground, Color::blend_normal),
BlendMode::Multiply => background.blend_rgb(foreground, Color::blend_multiply),
BlendMode::Darken => background.blend_rgb(foreground, Color::blend_darken),
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::Difference => backdrop.blend_rgb(source_color, Color::blend_exclusion),
BlendMode::Exclusion => backdrop.blend_rgb(source_color, Color::blend_exclusion),
BlendMode::Subtract => backdrop.blend_rgb(source_color, Color::blend_subtract),
BlendMode::Divide => backdrop.blend_rgb(source_color, Color::blend_divide),
BlendMode::Screen => background.blend_rgb(foreground, Color::blend_screen),
BlendMode::Lighten => background.blend_rgb(foreground, Color::blend_lighten),
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),
BlendMode::Hue => backdrop.blend_hue(source_color),
BlendMode::Saturation => backdrop.blend_saturation(source_color),
BlendMode::Color => backdrop.blend_color(source_color),
BlendMode::Luminosity => backdrop.blend_luminosity(source_color),
}
.lerp(backdrop, actual_opacity);
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),
BlendMode::VividLight => background.blend_rgb(foreground, Color::blend_vivid_light),
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),
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),
BlendMode::Hue => background.blend_hue(foreground),
BlendMode::Saturation => background.blend_saturation(foreground),
BlendMode::Color => background.blend_color(foreground),
BlendMode::Luminosity => background.blend_luminosity(foreground),
};
let multiplied_target_color = target_color.to_associated_alpha(opacity as f32);
let blended = background.alpha_blend(multiplied_target_color);
blended.to_gamma_srgb()
}
#[derive(Debug, Clone, Copy)]

View File

@@ -83,6 +83,11 @@ impl Color {
Color { red, green, blue, alpha }
}
/// Return an opaque `Color` from given `f32` RGB channels.
pub fn from_unassociated_alpha(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
}
/// Return an opaque SDR `Color` given RGB channels from `0` to `255`.
///
/// # Examples
@@ -602,14 +607,49 @@ impl Color {
pub fn map_rgb<F: Fn(f32) -> f32>(&self, f: F) -> Self {
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a())
}
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
Color {
red: f(self.red, other.red).clamp(0., 1.),
green: f(self.green, other.green).clamp(0., 1.),
blue: f(self.blue, other.blue).clamp(0., 1.),
pub fn apply_opacity(&self, opacity: f32) -> Self {
Self::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), self.a() * opacity)
}
pub fn to_associated_alpha(&self, alpha: f32) -> Self {
Self {
red: self.red * alpha,
green: self.green * alpha,
blue: self.blue * alpha,
alpha: self.alpha * alpha,
}
}
pub fn to_unassociated_alpha(&self) -> Self {
let unmultiply = 1. / self.alpha;
Self {
red: self.red * unmultiply,
green: self.green * unmultiply,
blue: self.blue * unmultiply,
alpha: self.alpha,
}
}
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
let background = self.to_unassociated_alpha();
Color {
red: f(background.red, other.red).clamp(0., 1.),
green: f(background.green, other.green).clamp(0., 1.),
blue: f(background.blue, other.blue).clamp(0., 1.),
alpha: other.alpha,
}
}
pub fn alpha_blend(&self, other: Color) -> Self {
let inv_alpha = 1. - other.alpha;
Self {
red: self.red * inv_alpha + other.red,
green: self.green * inv_alpha + other.green,
blue: self.blue * inv_alpha + other.blue,
alpha: self.alpha * inv_alpha + other.alpha,
}
}
}
#[test]

View File

@@ -6,6 +6,61 @@ use crate::raster::ImageFrame;
use crate::vector::VectorData;
use crate::Node;
pub trait Transform {
fn transform(&self) -> DAffine2;
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
pivot
}
}
pub trait TransformMut: Transform {
fn transform_mut(&mut self) -> &mut DAffine2;
fn translate(&mut self, offset: DVec2) {
*self.transform_mut() = DAffine2::from_translation(offset) * self.transform();
}
}
impl Transform for ImageFrame {
fn transform(&self) -> DAffine2 {
self.transform
}
}
impl Transform for &ImageFrame {
fn transform(&self) -> DAffine2 {
self.transform
}
}
impl TransformMut for ImageFrame {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
impl Transform for VectorData {
fn transform(&self) -> DAffine2 {
self.transform
}
fn local_pivot(&self, pivot: DVec2) -> DVec2 {
self.local_pivot(pivot)
}
}
impl TransformMut for VectorData {
fn transform_mut(&mut self) -> &mut DAffine2 {
&mut self.transform
}
}
impl Transform for DAffine2 {
fn transform(&self) -> DAffine2 {
*self
}
}
impl TransformMut for DAffine2 {
fn transform_mut(&mut self) -> &mut DAffine2 {
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct TransformNode<Translation, Rotation, Scale, Shear, Pivot> {
pub(crate) translate: Translation,
@@ -16,45 +71,12 @@ pub struct TransformNode<Translation, Rotation, Scale, Shear, Pivot> {
}
#[node_macro::node_fn(TransformNode)]
pub(crate) fn transform_vector_data(mut vector_data: VectorData, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2, pivot: DVec2) -> VectorData {
let pivot = DAffine2::from_translation(vector_data.local_pivot(pivot));
pub(crate) fn transform_vector_data<Data: TransformMut>(mut data: Data, translate: DVec2, rotate: f64, scale: DVec2, shear: DVec2, pivot: DVec2) -> Data {
let pivot = DAffine2::from_translation(data.local_pivot(pivot));
let modification = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse();
vector_data.transform = modification * vector_data.transform;
let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse();
let data_transform = data.transform_mut();
*data_transform = modification * (*data_transform);
vector_data
}
impl<'input, Translation: 'input, Rotation: 'input, Scale: 'input, Shear: 'input, Pivot: 'input> Node<'input, ImageFrame> for TransformNode<Translation, Rotation, Scale, Shear, Pivot>
where
Translation: for<'any_input> Node<'any_input, (), Output = DVec2>,
Rotation: for<'any_input> Node<'any_input, (), Output = f64>,
Scale: for<'any_input> Node<'any_input, (), Output = DVec2>,
Shear: for<'any_input> Node<'any_input, (), Output = DVec2>,
Pivot: for<'any_input> Node<'any_input, (), Output = DVec2>,
{
type Output = ImageFrame;
#[inline]
fn eval<'node: 'input>(&'node self, mut image_frame: ImageFrame) -> Self::Output {
let translate = self.translate.eval(());
let rotate = self.rotate.eval(());
let scale = self.scale.eval(());
let shear = self.shear.eval(());
let pivot = self.pivot.eval(());
let pivot = DAffine2::from_translation(pivot);
let modification = pivot * DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]) * pivot.inverse();
image_frame.transform = modification * image_frame.transform;
image_frame
}
}
// Generates a transform matrix that rotates around the center of the image
fn generate_transform(shear: DVec2, transform: &DAffine2, scale: DVec2, rotate: f64, translate: DVec2) -> DAffine2 {
let shear_matrix = DAffine2::from_cols_array(&[1., shear.y, shear.x, 1., 0., 0.]);
let pivot = transform.transform_point2(DVec2::splat(0.5));
let translate_to_center = DAffine2::from_translation(-pivot);
translate_to_center.inverse() * DAffine2::from_scale_angle_translation(scale, rotate, translate) * shear_matrix * translate_to_center
data
}

View File

@@ -9,13 +9,17 @@ pub use std::borrow::Cow;
pub struct NodeIOTypes {
pub input: Type,
pub output: Type,
pub parameters: Vec<(Type, Type)>,
pub parameters: Vec<Type>,
}
impl NodeIOTypes {
pub fn new(input: Type, output: Type, parameters: Vec<(Type, Type)>) -> Self {
pub fn new(input: Type, output: Type, parameters: Vec<Type>) -> Self {
Self { input, output, parameters }
}
pub fn ty(&self) -> Type {
Type::Fn(Box::new(self.input.clone()), Box::new(self.output.clone()))
}
}
#[macro_export]
@@ -34,6 +38,20 @@ macro_rules! generic {
}};
}
#[macro_export]
macro_rules! fn_type {
($input:ty, $output:ty) => {
Type::Fn(Box::new(concrete!($input)), Box::new(concrete!($output)))
};
}
#[macro_export]
macro_rules! value_fn {
($output:ty) => {
Type::Fn(Box::new(concrete!(())), Box::new(concrete!($output)))
};
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, specta::Type)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeIdentifier {
@@ -72,16 +90,62 @@ impl PartialEq for TypeDescriptor {
pub enum Type {
Generic(Cow<'static, str>),
Concrete(TypeDescriptor),
Fn(Box<Type>, Box<Type>),
}
impl Type {
pub fn is_generic(&self) -> bool {
matches!(self, Type::Generic(_))
}
pub fn is_concrete(&self) -> bool {
matches!(self, Type::Concrete(_))
}
pub fn is_fn(&self) -> bool {
matches!(self, Type::Fn(_, _))
}
pub fn is_value(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
}
pub fn is_unit(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Concrete(_))
}
pub fn is_generic_or_fn(&self) -> bool {
matches!(self, Type::Fn(_, _) | Type::Generic(_))
}
pub fn fn_input(&self) -> Option<&Type> {
match self {
Type::Fn(first, _) => Some(first),
_ => None,
}
}
pub fn fn_output(&self) -> Option<&Type> {
match self {
Type::Fn(_, second) => Some(second),
_ => None,
}
}
pub fn function(input: &Type, output: &Type) -> Type {
Type::Fn(Box::new(input.clone()), Box::new(output.clone()))
}
}
impl core::fmt::Debug for Type {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Generic(arg0) => f.write_fmt(format_args!("Generic({})", arg0)),
Self::Generic(arg0) => write!(f, "Generic({})", arg0),
#[cfg(feature = "type_id_logging")]
Self::Concrete(arg0) => f.write_fmt(format_args!("Concrete({}, {:?}))", arg0.name, arg0.id)),
Self::Concrete(arg0) => write!(f, "Concrete({}, {:?})", arg0.name, arg0.id),
#[cfg(not(feature = "type_id_logging"))]
Self::Concrete(arg0) => f.write_fmt(format_args!("Concrete({})", arg0.name)),
Self::Concrete(arg0) => write!(f, "Concrete({})", arg0.name),
Self::Fn(arg0, arg1) => write!(f, "({:?} -> {:?})", arg0, arg1),
}
}
}
@@ -91,6 +155,7 @@ impl std::fmt::Display for Type {
match self {
Type::Generic(name) => write!(f, "{}", name),
Type::Concrete(ty) => write!(f, "{}", ty.name),
Type::Fn(input, output) => write!(f, "({} -> {})", input, output),
}
}
}

View File

@@ -1,4 +1,5 @@
use core::marker::PhantomData;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use crate::Node;
@@ -15,6 +16,10 @@ impl<'i, const N: u32> Node<'i, ()> for IntNode<N> {
#[derive(Default, Debug)]
pub struct ValueNode<T>(pub T);
impl<T: StaticTypeSized> StaticType for ValueNode<T> {
type Static = ValueNode<T::Static>;
}
impl<'i, T: 'i> Node<'i, ()> for ValueNode<T> {
type Output = &'i T;
fn eval<'s: 'i>(&'s self, _input: ()) -> Self::Output {
@@ -43,6 +48,13 @@ impl<T: Clone + Copy> Copy for ValueNode<T> {}
#[derive(Clone)]
pub struct ClonedNode<T: Clone>(pub T);
impl<T: Clone + StaticTypeSized> StaticType for ClonedNode<T>
where
T::Static: Clone,
{
type Static = ClonedNode<T::Static>;
}
impl<'i, T: Clone + 'i> Node<'i, ()> for ClonedNode<T> {
type Output = T;
fn eval<'s: 'i>(&'s self, _input: ()) -> Self::Output {