Color system based on traits, and conversion to linear color in the graph (#1123)

* Migrate Nodes to use RasterMut + Samplable

* Add Pixel trait to include serialization

* Implement traits for Color and propagate new generics

* Always convert to linear color when loading images
This commit is contained in:
Dennis Kobert
2023-04-16 02:57:05 +02:00
committed by Keavon Chambers
parent e21c2fb67b
commit 37b892a516
16 changed files with 638 additions and 265 deletions

View File

@@ -1,7 +1,7 @@
use std::marker::PhantomData;
use glam::{DAffine2, DVec2};
use graphene_core::raster::{Color, Image, ImageFrame};
use graphene_core::raster::{Color, Image, ImageFrame, RasterMut};
use graphene_core::transform::TransformMut;
use graphene_core::vector::VectorData;
use graphene_core::Node;
@@ -93,7 +93,7 @@ fn erase(input: (Color, Color), flow: f64) -> Color {
}
#[node_fn(BrushTextureNode)]
fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> ImageFrame {
fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> ImageFrame<Color> {
// Diameter
let radius = diameter / 2.;
// TODO: Remove the 4px padding after figuring out why the brush stamp gets randomly offset by 1px up/down/left/right when clicking with the Brush tool
@@ -128,7 +128,7 @@ fn brush_texture(diameter: f64, color: Color, hardness: f64, flow: f64) -> Image
let pixel_fill = summation / MULTISAMPLE_GRID.len() as f64;
let pixel = image.get_mut(x, y).unwrap();
let pixel = image.get_pixel_mut(x, y).unwrap();
*pixel = color.apply_opacity(pixel_fill as f32);
}
}
@@ -187,7 +187,7 @@ mod test {
assert_eq!(image.image.height, size.ceil() as u32 + 4);
assert_eq!(image.transform, DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil() + 4.), 0., -DVec2::splat(size / 2.)));
// center pixel should be BLACK
assert_eq!(image.image.get(11, 11), Some(&Color::BLACK));
assert_eq!(image.image.get_pixel(11, 11), Some(Color::BLACK));
}
#[test]

View File

@@ -1,11 +1,12 @@
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{BVec2, DAffine2, DVec2};
use graphene_core::raster::{Color, Image, ImageFrame};
use graphene_core::raster::{Alpha, Channel, Color, Image, ImageFrame, Luminance, Pixel, RasterMut, Sample};
use graphene_core::transform::Transform;
use graphene_core::value::{ClonedNode, ValueNode};
use graphene_core::Node;
use std::fmt::Debug;
use std::marker::PhantomData;
use std::path::Path;
@@ -93,10 +94,12 @@ pub fn export_image_node<'i, 's: 'i>() -> impl Node<'i, 's, (Image, &'i str), Ou
}
*/
pub struct DownresNode;
pub struct DownresNode<P> {
_p: PhantomData<P>,
}
#[node_macro::node_fn(DownresNode)]
fn downres(image_frame: ImageFrame) -> ImageFrame {
#[node_macro::node_fn(DownresNode<_P>)]
fn downres<_P: Pixel>(image_frame: ImageFrame<_P>) -> ImageFrame<_P> {
let target_width = (image_frame.transform.transform_vector2((1., 0.).into()).length() as usize).min(image_frame.image.width as usize);
let target_height = (image_frame.transform.transform_vector2((0., 1.).into()).length() as usize).min(image_frame.image.height as usize);
@@ -121,43 +124,22 @@ fn downres(image_frame: ImageFrame) -> ImageFrame {
}
#[derive(Debug, Clone, Copy)]
pub struct MapImageNode<MapFn> {
pub struct MapImageNode<P, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
}
#[node_macro::node_fn(MapImageNode)]
fn map_image<MapFn>(image: Image, map_fn: &'any_input MapFn) -> Image
#[node_macro::node_fn(MapImageNode<_P>)]
fn map_image<MapFn, _P, Img: RasterMut<Pixel = _P>>(image: Img, map_fn: &'any_input MapFn) -> Img
where
MapFn: for<'any_input> Node<'any_input, Color, Output = Color> + 'input,
MapFn: for<'any_input> Node<'any_input, _P, Output = _P> + 'input,
{
let mut image = image;
for pixel in &mut image.data {
*pixel = map_fn.eval(*pixel);
}
image.map_pixels(|c| map_fn.eval(c));
image
}
#[derive(Debug, Clone, Copy)]
pub struct MapImageFrameNode<MapFn> {
map_fn: MapFn,
}
impl<MapFn: dyn_any::StaticTypeSized> StaticType for MapImageFrameNode<MapFn> {
type Static = MapImageFrameNode<MapFn::Static>;
}
#[node_macro::node_fn(MapImageFrameNode)]
fn map_image<MapFn>(mut image_frame: ImageFrame, map_fn: &'any_input MapFn) -> ImageFrame
where
MapFn: for<'any_input> Node<'any_input, Color, Output = Color> + 'input,
{
for pixel in &mut image_frame.image.data {
*pixel = map_fn.eval(*pixel);
}
image_frame
}
#[derive(Debug, Clone, DynAny)]
pub struct AxisAlignedBbox {
start: DVec2,
@@ -227,33 +209,48 @@ fn compute_transformed_bounding_box(transform: DAffine2) -> Bbox {
}
#[derive(Debug, Clone, Copy)]
pub struct MaskImageNode<Mask> {
mask: Mask,
pub struct MaskImageNode<P, S, Stencil> {
stencil: Stencil,
_p: PhantomData<P>,
_s: PhantomData<S>,
}
#[node_macro::node_fn(MaskImageNode)]
fn mask_image(mut image: ImageFrame, mask: ImageFrame) -> ImageFrame {
let image_size = DVec2::new(image.image.width as f64, image.image.height as f64);
let mask_size = DVec2::new(mask.image.width as f64, mask.image.height as f64);
#[node_macro::node_fn(MaskImageNode<_P, _S>)]
fn mask_image<
// _P is the color of the input image. It must have an alpha channel because that is going to
// be modified by the mask
_P: Copy + Alpha,
// _S is the color of the stencil. It must have a luminance channel because that is used to
// mask the input image
_S: Luminance,
// Input image
Input: Transform + RasterMut<Pixel = _P>,
// Stencil
Stencil: Sample<Pixel = _S> + Transform,
>(
mut image: Input,
stencil: Stencil,
) -> Input {
let image_size = DVec2::new(image.width() as f64, image.height() as f64);
let mask_size = stencil.transform().decompose_scale();
if mask_size == DVec2::ZERO {
return image;
}
// Transforms a point from the background image to the forground image
let bg_to_fg = DAffine2::from_scale(mask_size) * mask.transform.inverse() * image.transform * DAffine2::from_scale(1. / image_size);
let bg_to_fg = DAffine2::from_scale(mask_size) * stencil.transform().inverse() * image.transform() * DAffine2::from_scale(1. / image_size);
for y in 0..image.image.height {
for x in 0..image.image.width {
for y in 0..image.height() {
for x in 0..image.width() {
let image_point = DVec2::new(x as f64, y as f64);
let mut mask_point = bg_to_fg.transform_point2(image_point);
mask_point = mask_point.clamp(DVec2::ZERO, mask_size);
let image_pixel = image.get_mut(x as usize, y as usize);
let mask_pixel = mask.sample(mask_point);
let alpha = image_pixel.a() * mask_pixel.r();
*image_pixel = Color::from_rgbaf32(image_pixel.r(), image_pixel.g(), image_pixel.b(), alpha).unwrap();
let image_pixel = image.get_pixel_mut(x as u32, y as u32).unwrap();
if let Some(mask_pixel) = stencil.sample(mask_point) {
image_pixel.multiply_alpha(mask_pixel.l().to_channel());
}
}
}
@@ -261,18 +258,19 @@ fn mask_image(mut image: ImageFrame, mask: ImageFrame) -> ImageFrame {
}
#[derive(Debug, Clone, Copy)]
pub struct BlendImageTupleNode<MapFn> {
pub struct BlendImageTupleNode<P, MapFn> {
map_fn: MapFn,
_p: PhantomData<P>,
}
impl<MapFn: StaticTypeSized> StaticType for BlendImageTupleNode<MapFn> {
type Static = BlendImageTupleNode<MapFn::Static>;
impl<MapFn: StaticTypeSized, P: StaticTypeSized> StaticType for BlendImageTupleNode<P, MapFn> {
type Static = BlendImageTupleNode<P::Static, MapFn::Static>;
}
#[node_macro::node_fn(BlendImageTupleNode)]
fn blend_image_tuple<MapFn>(images: (ImageFrame, ImageFrame), map_fn: &'any_input MapFn) -> ImageFrame
#[node_macro::node_fn(BlendImageTupleNode<_P>)]
fn blend_image_tuple<_P: Pixel + Debug, MapFn>(images: (ImageFrame<_P>, ImageFrame<_P>), map_fn: &'any_input MapFn) -> ImageFrame<_P>
where
MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input + Clone,
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input + Clone,
{
let (background, foreground) = images;
@@ -281,30 +279,34 @@ where
}
#[derive(Debug, Clone, Copy)]
pub struct BlendImageNode<Background, MapFn> {
pub struct BlendImageNode<P, Background, MapFn> {
background: Background,
map_fn: MapFn,
_p: PhantomData<P>,
}
impl<Background: StaticTypeSized, MapFn: StaticTypeSized> StaticType for BlendImageNode<Background, MapFn> {
type Static = BlendImageNode<Background::Static, MapFn::Static>;
impl<P: StaticTypeSized, Background: StaticTypeSized, MapFn: StaticTypeSized> StaticType for BlendImageNode<P, Background, MapFn> {
type Static = BlendImageNode<P::Static, Background::Static, MapFn::Static>;
}
// TODO: Implement proper blending
#[node_macro::node_fn(BlendImageNode)]
fn blend_image<MapFn, Frame: AsRef<ImageFrame>>(foreground: Frame, mut background: ImageFrame, map_fn: &'any_input MapFn) -> ImageFrame
#[node_macro::node_fn(BlendImageNode<_P>)]
fn blend_image<_P: Clone, MapFn, Frame: Sample<Pixel = _P> + Transform, Background: RasterMut<Pixel = _P> + Transform>(
foreground: Frame,
mut background: Background,
map_fn: &'any_input MapFn,
) -> Background
where
MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input,
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P> + 'input,
{
let foreground = foreground.as_ref();
let foreground_size = DVec2::new(foreground.image.width as f64, foreground.image.height as f64);
let background_size = DVec2::new(background.image.width as f64, background.image.height as f64);
let foreground_size = foreground.transform().decompose_scale();
let background_size = DVec2::new(background.width() as f64, background.height() as f64);
// Transforms a point from the background image to the forground image
let bg_to_fg = DAffine2::from_scale(foreground_size) * foreground.transform.inverse() * background.transform * DAffine2::from_scale(1. / background_size);
let bg_to_fg = background.transform() * DAffine2::from_scale(1. / background_size);
// Footprint of the foreground image (0,0) (1, 1) in the background image space
let bg_aabb = compute_transformed_bounding_box(background.transform.inverse() * foreground.transform).axis_aligned_bbox();
let bg_aabb = compute_transformed_bounding_box(background.transform().inverse() * foreground.transform()).axis_aligned_bbox();
// Clamp the foreground image to the background image
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
@@ -314,14 +316,12 @@ where
for x in start.x..end.x {
let bg_point = DVec2::new(x as f64, y as f64);
let fg_point = bg_to_fg.transform_point2(bg_point);
if !((fg_point.cmpge(DVec2::ZERO) & fg_point.cmple(foreground_size)) == BVec2::new(true, true)) {
continue;
if let Some(src_pixel) = foreground.sample(fg_point) {
if let Some(dst_pixel) = background.get_pixel_mut(x, y) {
*dst_pixel = map_fn.eval((src_pixel, dst_pixel.clone()));
}
}
let dst_pixel = background.get_mut(x as usize, y as usize);
let src_pixel = foreground.sample(fg_point);
*dst_pixel = map_fn.eval((src_pixel, *dst_pixel));
}
}
@@ -347,12 +347,13 @@ fn merge_bounding_box_node<_Data: Transform>(input: (Option<AxisAlignedBbox>, _D
}
#[derive(Clone, Debug, PartialEq)]
pub struct EmptyImageNode<FillColor> {
pub struct EmptyImageNode<P, FillColor> {
pub color: FillColor,
_p: PhantomData<P>,
}
#[node_macro::node_fn(EmptyImageNode)]
fn empty_image(transform: DAffine2, color: Color) -> ImageFrame {
#[node_macro::node_fn(EmptyImageNode<_P>)]
fn empty_image<_P: Pixel>(transform: DAffine2, color: _P) -> ImageFrame<_P> {
let width = transform.transform_vector2(DVec2::new(1., 0.)).length() as u32;
let height = transform.transform_vector2(DVec2::new(0., 1.)).length() as u32;
@@ -361,12 +362,13 @@ fn empty_image(transform: DAffine2, color: Color) -> ImageFrame {
}
#[derive(Debug, Clone, Copy)]
pub struct ImaginateNode<E> {
pub struct ImaginateNode<P, E> {
cached: E,
_p: PhantomData<P>,
}
#[node_macro::node_fn(ImaginateNode)]
fn imaginate(image_frame: ImageFrame, cached: Option<std::sync::Arc<graphene_core::raster::Image>>) -> ImageFrame {
#[node_macro::node_fn(ImaginateNode<_P>)]
fn imaginate<_P: Pixel>(image_frame: ImageFrame<_P>, cached: Option<std::sync::Arc<graphene_core::raster::Image<_P>>>) -> ImageFrame<_P> {
let cached_image = cached.map(|mut x| std::sync::Arc::make_mut(&mut x).clone()).unwrap_or(image_frame.image);
ImageFrame {
image: cached_image,
@@ -375,11 +377,12 @@ fn imaginate(image_frame: ImageFrame, cached: Option<std::sync::Arc<graphene_cor
}
#[derive(Debug, Clone, Copy)]
pub struct ImageFrameNode<Transform> {
pub struct ImageFrameNode<P, Transform> {
transform: Transform,
_p: PhantomData<P>,
}
#[node_macro::node_fn(ImageFrameNode)]
fn image_frame(image: Image, transform: DAffine2) -> graphene_core::raster::ImageFrame {
#[node_macro::node_fn(ImageFrameNode<_P>)]
fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> graphene_core::raster::ImageFrame<_P> {
graphene_core::raster::ImageFrame { image, transform }
}
#[cfg(test)]