use crate::brush_cache::BrushCache; use crate::brush_stroke::{BrushStroke, BrushStyle}; use core_types::attribute::{Attr, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform as TransformAttr}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; use core_types::color::{Alpha, Color, Pixel, Sample}; use core_types::extent::{LevelIn, ListIn}; use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt}; use core_types::list::{Item, List}; use core_types::math::bbox::{AxisAlignedBbox, Bbox}; use core_types::transform::Transform; use core_types::uuid::NodeId; use core_types::{ATTR_BLEND_MODE, ATTR_CLIPPING_MASK, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM}; use core_types::{Ctx, ExtractIndex, InjectIndex}; use glam::{DAffine2, DVec2}; use raster_nodes::blending_nodes::blend_colors; use raster_nodes::std_nodes::{empty_image_core, extend_image_to_bounds_core}; use raster_types::BitmapMut; use raster_types::Image; use raster_types::{CPU, Raster}; #[derive(Clone, Copy, Debug, PartialEq)] pub struct BrushStampGenerator { color: P, feather_exponent: f32, transform: DAffine2, } // SAFETY: `Static` is `Self` with `P` at its own static projection. unsafe impl dyn_any::StaticType for BrushStampGenerator

where P::Static: Pixel + Alpha, { type Static = BrushStampGenerator; } impl Transform for BrushStampGenerator

{ fn transform(&self) -> DAffine2 { self.transform } } impl Sample for BrushStampGenerator

{ type Pixel = P; #[inline] fn sample(&self, position: DVec2, area: DVec2) -> Option

{ let position = self.transform.inverse().transform_point2(position); let area = self.transform.inverse().transform_vector2(area); let aa_blur_radius = area.length() as f32 * 2.; let center = DVec2::splat(0.5); let distance = (position + area / 2. - center).length() as f32 * 2.; let edge_opacity = 1. - (1. - aa_blur_radius).powf(self.feather_exponent); let result = if distance < 1. - aa_blur_radius { 1. - distance.powf(self.feather_exponent) } else if distance < 1. { // TODO: Replace this with a proper analytical AA implementation edge_opacity * ((1. - distance) / aa_blur_radius) } else { return None; }; use core_types::color::Channel; Some(self.color.multiplied_alpha(P::AlphaChannel::from_linear(result))) } } /// Controls the brush shape with diameter and hardness, plus color and opacity (via flow). /// The feather exponent is calculated from hardness to determine edge softness. /// Used internally to create the brush texture before stamping it repeatedly along a stroke path. #[node_macro::node(category(""), skip_impl)] fn brush_stamp_generator(_: impl Ctx, #[unit(" px")] diameter: f64, color: Color, hardness: f64, flow: f64) -> BrushStampGenerator { // Diameter let radius = diameter / 2.; // Hardness let hardness = hardness / 100.; let feather_exponent = 1. / (1. - hardness) as f32; // Flow let flow = flow / 100.; // Color let color = color.apply_opacity(flow as f32); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(diameter), 0., -DVec2::splat(radius)); BrushStampGenerator { color, feather_exponent, transform } } /// Used to efficiently paint brush strokes. Applies the same texture repeatedly at different positions with proper blending and boundary handling. #[node_macro::node(category(""), skip_impl)] fn blit(_: impl Ctx, mut target: List>, texture: Raster, positions: Vec, blend_mode: BlendFn) -> List> where BlendFn: Fn(Color, Color) -> Color, { if positions.is_empty() { return target; } let (elements, transforms) = target.element_and_attribute_slices_mut::(ATTR_TRANSFORM); for (element, transform_attribute) in elements.iter_mut().zip(transforms.iter()) { let target_width = element.width; let target_height = element.height; let target_size = DVec2::new(target_width as f64, target_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) * transform_attribute.inverse(); for position in &positions { let start = document_to_target.transform_point2(*position).round(); let stop = start + texture_size; // Half-open integer ranges [start, stop). let clamp_start = start.clamp(DVec2::ZERO, target_size).as_uvec2(); let clamp_stop = stop.clamp(DVec2::ZERO, target_size).as_uvec2(); let blit_area_offset = (clamp_start.as_dvec2() - start).as_uvec2().min(texture_size.as_uvec2()); 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.width as usize) + (x as usize) }; let target_index = |x: u32, y: u32| -> usize { (y as usize * target_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.data.len()); assert!(target_index(max_x, max_y) < element.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.data[texture_index(x, y)]; let dst_pixel = &mut element.data_mut().data[target_index(x + clamp_start.x, y + clamp_start.y)]; *dst_pixel = blend_mode(src_pixel, *dst_pixel); } } } } target } pub fn create_brush_texture(brush_style: &BrushStyle) -> Raster { let stamp = brush_stamp_generator(&(), brush_style.diameter, brush_style.color, brush_style.hardness, brush_style.flow); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(brush_style.diameter), 0., -DVec2::splat(brush_style.diameter / 2.)); let blank_texture = { let mut item = Item::new_from_element(empty_image_core(transform, Color::TRANSPARENT)); item.set_attribute(ATTR_TRANSFORM, transform); item }; let image = blend_stamp_closure(stamp, blank_texture, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); image.into_element() } pub fn blend_with_mode(background: Item>, foreground: Item>, blend_mode: BlendMode, opacity: f64) -> Item> { let opacity = opacity as f32 / 100.; match std::hint::black_box(blend_mode) { // Normal group BlendMode::Normal => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Normal, opacity)), // Darken group BlendMode::Darken => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Darken, opacity)), BlendMode::Multiply => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Multiply, opacity)), BlendMode::ColorBurn => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::ColorBurn, opacity)), BlendMode::LinearBurn => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearBurn, opacity)), BlendMode::DarkerColor => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::DarkerColor, opacity)), // Lighten group BlendMode::Lighten => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Lighten, opacity)), BlendMode::Screen => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Screen, opacity)), BlendMode::ColorDodge => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::ColorDodge, opacity)), BlendMode::LinearDodge => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearDodge, opacity)), BlendMode::LighterColor => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LighterColor, opacity)), // Contrast group BlendMode::Overlay => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Overlay, opacity)), BlendMode::SoftLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::SoftLight, opacity)), BlendMode::HardLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::HardLight, opacity)), BlendMode::VividLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::VividLight, opacity)), BlendMode::LinearLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::LinearLight, opacity)), BlendMode::PinLight => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::PinLight, opacity)), BlendMode::HardMix => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::HardMix, opacity)), // Inversion group BlendMode::Difference => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Difference, opacity)), BlendMode::Exclusion => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Exclusion, opacity)), BlendMode::Subtract => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Subtract, opacity)), BlendMode::Divide => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Divide, opacity)), // Component group BlendMode::Hue => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Hue, opacity)), BlendMode::Saturation => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Saturation, opacity)), BlendMode::Color => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Color, opacity)), BlendMode::Luminosity => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Luminosity, opacity)), // Other utility blend modes (hidden from the normal list) BlendMode::Erase => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Erase, opacity)), BlendMode::Restore => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::Restore, opacity)), BlendMode::MultiplyAlpha => blend_image_closure(foreground, background, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, opacity)), } } /// Lane 0 of the materialized background as the legacy item the brush core works /// on; an empty level starts from a blank item, as the pre-flip node did. fn legacy_background(background: core_types::node::List<'_, Raster>) -> Item> { if background.is_empty() { return Item::default(); } let lane = background.lane(0); let mut item = Item::new_from_element(background.element_ref(0).clone()); item.set_attribute(ATTR_TRANSFORM, lane.attr::()); item.set_attribute(ATTR_BLEND_MODE, lane.attr::()); item.set_attribute(ATTR_OPACITY, lane.attr::()); item.set_attribute(ATTR_OPACITY_FILL, lane.attr::()); item.set_attribute(ATTR_CLIPPING_MASK, lane.attr::()); item } /// The brushed image replaces the whole background level with one lane. fn brush_extent(_background: ListIn<'_, Raster>, _trace: ListIn<'_, BrushStroke>, _level: LevelIn) -> GPoll { GPoll::Final(Extent::Exactly(1)) } /// Generates the brush strokes painted with the Brush tool as a raster image. /// If an input image is supplied, strokes are drawn on top of it, expanding bounds as needed. #[node_macro::node(category("Raster"), extent(brush_extent))] fn brush<'e>( ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, /// Optional raster content that may be drawn onto. background: IList>, /// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles. trace: IList, /// Internal cache data used to accelerate rendering of the brush content. #[data] cache: BrushCache, ) -> Result< IList<( Raster, Attr<'e, TransformAttr>, Attr<'e, BlendModeAttr>, Attr<'e, Opacity>, Attr<'e, OpacityFill>, Attr<'e, ClippingMask>, Attr<'e, EditorLayerPath>, )>, Interrupt, > { if ctx.innermost_index() > 0 { return Err(GraphError::past_end().into()); } // The layer path only rides through, so it is read off the source lane // rather than round-tripped as a legacy list attribute. let layer_path: Vec = match background.is_empty() { true => Vec::new(), false => background.lane(0).attr::().to_vec(), }; let strokes: Vec = (0..trace.len()).map(|row| trace.element_ref(row).clone()).collect(); let actual_image = brush_core(legacy_background(background), strokes, cache); let transform: DAffine2 = actual_image.attribute_cloned_or_default(ATTR_TRANSFORM); let blend_mode: BlendMode = actual_image.attribute_cloned_or_default(ATTR_BLEND_MODE); let opacity: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY, 1.); let fill: f64 = actual_image.attribute_cloned_or(ATTR_OPACITY_FILL, 1.); let clip: bool = actual_image.attribute_cloned_or_default(ATTR_CLIPPING_MASK); let layer_path = ctx .arena() .alloc(layer_path) .ok_or_else(|| { Interrupt::from(GraphError { kind: core_types::gpoll::ErrorKind::ArenaExhausted, trace: Vec::new(), }) })? .0; Ok(( actual_image.into_element(), Attr(transform), Attr(blend_mode), Attr(opacity), Attr(fill), Attr(clip), Attr(layer_path.as_slice()), )) } /// The pre-flip brush body, on legacy items: one background item plus every /// stroke in order, returning the painted image. fn brush_core(list_item: Item>, strokes: Vec, cache: &BrushCache) -> Item> { let bounds = List::new_from_item(list_item.clone()).bounding_box(DAffine2::IDENTITY, false); let [start, end] = if let RenderBoundingBox::Rectangle(rect) = bounds { rect } else { [DVec2::ZERO, DVec2::ZERO] }; let background_bbox = AxisAlignedBbox { start, end }; let stroke_bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); let bbox = if background_bbox.size().length() < 0.1 { stroke_bbox } else { stroke_bbox.union(&background_bbox) }; let background_bounds = bbox.to_transform(); let mut draw_strokes: Vec<_> = strokes.iter().filter(|s| !matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)).cloned().collect(); let mut brush_plan = cache.compute_brush_plan(list_item, &draw_strokes); // TODO: Find a way to handle more than one item let mut actual_image = { let background = brush_plan.background; let transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM); let (element, attributes) = background.into_parts(); let (element, transform) = extend_image_to_bounds_core(element, transform, background_bounds); let mut item = Item::from_parts(element, attributes); item.set_attribute(ATTR_TRANSFORM, transform); item }; let final_stroke_idx = brush_plan.strokes.len().saturating_sub(1); for (idx, stroke) in brush_plan.strokes.into_iter().enumerate() { // Create brush texture. // TODO: apply rotation from layer to stamp for non-rotationally-symmetric brushes. let mut brush_texture = cache.get_cached_brush(&stroke.style); if brush_texture.is_none() { let tex = create_brush_texture(&stroke.style); cache.store_brush(stroke.style.clone(), tex.clone()); brush_texture = Some(tex); } let brush_texture = brush_texture.unwrap(); // Compute transformation from stroke texture space into layer space, and create the stroke texture. let skip = if idx == 0 { brush_plan.first_stroke_point_skip } else { 0 }; let positions: Vec<_> = stroke.compute_blit_points().into_iter().skip(skip).collect(); let stroke_texture = if idx == 0 && positions.is_empty() { core::mem::take(&mut brush_plan.first_stroke_texture) } else { 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 blit_target = if idx == 0 { let target = core::mem::take(&mut brush_plan.first_stroke_texture); let transform: DAffine2 = target.attribute_cloned_or_default(ATTR_TRANSFORM); let (element, attributes) = target.into_parts(); let (element, transform) = extend_image_to_bounds_core(element, transform, stroke_to_layer); let mut item = Item::from_parts(element, attributes); item.set_attribute(ATTR_TRANSFORM, transform); List::new_from_item(item) } else { let mut item = Item::new_from_element(empty_image_core(stroke_to_layer, Color::TRANSPARENT)); item.set_attribute(ATTR_TRANSFORM, stroke_to_layer); List::new_from_item(item) }; let list = blit(&(), blit_target, brush_texture, positions, |a, b| blend_colors(a, b, BlendMode::Normal, 1.)); assert_eq!(list.len(), 1); list.into_iter().next().unwrap_or_default() }; // Cache image before doing final blend, and store final stroke texture. if idx == final_stroke_idx { cache.cache_results(core::mem::take(&mut draw_strokes), actual_image.clone(), stroke_texture.clone()); } // TODO: Is this the correct way to do opacity in blending? actual_image = blend_with_mode(actual_image, stroke_texture, stroke.style.blend_mode, (stroke.style.color.a() * 100.) as f64); } let has_erase_or_restore_strokes = strokes.iter().any(|s| matches!(s.style.blend_mode, BlendMode::Erase | BlendMode::Restore)); if has_erase_or_restore_strokes { let opaque_image = Image::new(bbox.size().x as u32, bbox.size().y as u32, Color::WHITE); let mut erase_restore_mask = Item::new_from_element(Raster::new_cpu(opaque_image)).with_attribute(ATTR_TRANSFORM, background_bounds); for stroke in strokes { let mut brush_texture = cache.get_cached_brush(&stroke.style); if brush_texture.is_none() { let tex = create_brush_texture(&stroke.style); cache.store_brush(stroke.style.clone(), tex.clone()); brush_texture = Some(tex); } let brush_texture = brush_texture.unwrap(); let positions: Vec<_> = stroke.compute_blit_points().into_iter().collect(); // For mask composition: Erase subtracts alpha, Restore adds alpha, and Draw acts like Restore to allow repainting erased areas. let mask_blend_mode = match stroke.style.blend_mode { BlendMode::Erase => BlendMode::Erase, BlendMode::Restore => BlendMode::Restore, _ => BlendMode::Restore, }; erase_restore_mask = blit(&(), List::new_from_item(erase_restore_mask), brush_texture, positions, move |a, b| { blend_colors(a, b, mask_blend_mode, 1.) }) .into_iter() .next() .unwrap_or_default(); } actual_image = blend_image_closure(erase_restore_mask, actual_image, |a, b| blend_colors(a, b, BlendMode::MultiplyAlpha, 1.)); } actual_image } pub fn blend_image_closure(foreground: Item>, mut background: Item>, map_fn: impl Fn(Color, Color) -> Color) -> Item> { let foreground_size = DVec2::new(foreground.element().width as f64, foreground.element().height as f64); let background_size = DVec2::new(background.element().width as f64, background.element().height as f64); // Transforms a point from the background image to the foreground image let foreground_transform: DAffine2 = foreground.attribute_cloned_or_default(ATTR_TRANSFORM); let background_transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM); let background_to_foreground = DAffine2::from_scale(foreground_size) * foreground_transform.inverse() * background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground_transform).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); let end = (background_aabb.end * background_size).min(background_size).as_uvec2(); for y in start.y..end.y { for x in start.x..end.x { let background_point = DVec2::new(x as f64, y as f64); let foreground_point = background_to_foreground.transform_point2(background_point); let source_pixel = foreground.element().sample(foreground_point); let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else { continue; }; *destination_pixel = map_fn(source_pixel, *destination_pixel); } } background } pub fn blend_stamp_closure(foreground: BrushStampGenerator, mut background: Item>, map_fn: impl Fn(Color, Color) -> Color) -> Item> { let background_size = DVec2::new(background.element().width as f64, background.element().height as f64); // Transforms a point from the background image to the foreground image let background_transform: DAffine2 = background.attribute_cloned_or_default(ATTR_TRANSFORM); let background_to_foreground = background_transform * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0, 0)..(1, 1) in the background image space let background_aabb = Bbox::unit().affine_transform(background_transform.inverse() * foreground.transform()).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (background_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); let end = (background_aabb.end * background_size).min(background_size).as_uvec2(); let area = background_to_foreground.transform_point2(DVec2::new(1., 1.)) - background_to_foreground.transform_point2(DVec2::ZERO); for y in start.y..end.y { for x in start.x..end.x { let background_point = DVec2::new(x as f64, y as f64); let foreground_point = background_to_foreground.transform_point2(background_point); let Some(source_pixel) = foreground.sample(foreground_point, area) else { continue }; let Some(destination_pixel) = background.element_mut().data_mut().get_pixel_mut(x, y) else { continue; }; *destination_pixel = map_fn(source_pixel, *destination_pixel); } } background } #[cfg(test)] mod test { use super::*; use core_types::transform::Transform; use glam::DAffine2; #[test] fn test_brush_texture() { let size = 20.; let image = brush_stamp_generator(&(), size, Color::BLACK, 100., 100.); assert_eq!(image.transform(), DAffine2::from_scale_angle_translation(DVec2::splat(size.ceil()), 0., -DVec2::splat(size / 2.))); // center pixel should be BLACK assert_eq!(image.sample(DVec2::splat(0.), DVec2::ONE), Some(Color::BLACK)); } #[test] fn test_brush_output_size() { let image = brush_core( Item::new_from_element(Raster::new_cpu(Image::::default())), vec![BrushStroke { trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }], style: BrushStyle { color: Color::BLACK, diameter: 20., hardness: 20., flow: 20., spacing: 20., blend_mode: BlendMode::Normal, }, }], &BrushCache::default(), ); assert_eq!(image.element().width, 20); } }