mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Convert the raster std and adjustments families to element kernels and keep the unit primary's slot
This commit is contained in:
@@ -7,3 +7,12 @@ impl Ctx for () {}
|
||||
pub trait ArcCtx: Send + Sync {}
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: ArcCtx> Ctx for std::sync::Arc<T> {}
|
||||
|
||||
// The cache-hash bound record kernels place on their element generics; the
|
||||
// shader build compiles the same signatures without the hashing machinery.
|
||||
#[cfg(feature = "std")]
|
||||
pub use graphene_hash::CacheHash;
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub trait CacheHash {}
|
||||
#[cfg(not(feature = "std"))]
|
||||
impl<T> CacheHash for T {}
|
||||
|
||||
@@ -74,10 +74,10 @@ pub(crate) fn generate_node_code(crate_ident: &CrateIdent, parsed: &ParsedNodeFn
|
||||
(Some(crate::codegen::ir::NodeKind::Routing), crate::codegen::ir::Element::Generic(ident)) => Some(ident.clone()),
|
||||
_ => None,
|
||||
};
|
||||
// A `_: ()` primary stays visible in the metadata but claims no struct field.
|
||||
let record_unit_carrier = record_io && crate::codegen::classify::record_shape(parsed).is_some_and(|shape| shape.skips_carrier());
|
||||
// A `_: ()` primary keeps its slot: dropping it would shift every
|
||||
// per-index classification against the IR and the document's arity.
|
||||
let record_skips_carrier = record_io && !carrier_present;
|
||||
let struct_regular_fields: Vec<_> = regular_fields.iter().skip(record_unit_carrier as usize).copied().collect();
|
||||
let struct_regular_fields: Vec<_> = regular_fields.to_vec();
|
||||
let struct_regular_field_names: Vec<_> = struct_regular_fields.iter().map(|f| &f.pat_ident.ident).collect();
|
||||
|
||||
// Extract function generics used by data fields
|
||||
|
||||
@@ -1938,7 +1938,7 @@ mod tests {
|
||||
let layout = source_opacity_layout();
|
||||
reserve_for(&[&layout]);
|
||||
|
||||
let node = install(SourceOpacityNode::new(ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]);
|
||||
let node = install(SourceOpacityNode::new(ValueNode(()), ValueNode(3.), ValueNode(0.25)), source_opacity_layout_meta(), &[]);
|
||||
assert_eq!(Node::<ContextImpl>::layout(&node), &layout);
|
||||
let GPoll::Final(value) = node.eval(&ctx) else {
|
||||
panic!("expected a final record");
|
||||
|
||||
@@ -19,9 +19,14 @@ mod adjust_std {
|
||||
impl Adjust<Color> for List<Raster<CPU>> {
|
||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||
for element in self.iter_element_values_mut() {
|
||||
for color in element.data_mut().data.iter_mut() {
|
||||
*color = map_fn(color);
|
||||
}
|
||||
element.adjust(&map_fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Adjust<Color> for Raster<CPU> {
|
||||
fn adjust(&mut self, map_fn: impl Fn(&Color) -> Color) {
|
||||
for color in self.data_mut().data.iter_mut() {
|
||||
*color = map_fn(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
use crate::adjust::Adjust;
|
||||
use crate::cubic_spline::CubicSplines;
|
||||
use core::fmt::Debug;
|
||||
#[cfg(feature = "std")]
|
||||
use core_types::list::List;
|
||||
use glam::Vec3;
|
||||
use no_std_types::color::{Color, linear_to_srgb, srgb_to_linear};
|
||||
use no_std_types::context::Ctx;
|
||||
@@ -50,12 +48,12 @@ pub enum LuminanceCalculation {
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn luminance<T: Adjust<Color>>(
|
||||
fn luminance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -75,12 +73,12 @@ fn luminance<T: Adjust<Color>>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn gamma_correction<T: Adjust<Color>>(
|
||||
fn gamma_correction<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -97,12 +95,12 @@ fn gamma_correction<T: Adjust<Color>>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
|
||||
fn extract_channel<T: Adjust<Color>>(
|
||||
fn extract_channel<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -121,12 +119,12 @@ fn extract_channel<T: Adjust<Color>>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Channels"), shader_node(PerPixelAdjust))]
|
||||
fn make_opaque<T: Adjust<Color>>(
|
||||
fn make_opaque<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -143,12 +141,12 @@ fn make_opaque<T: Adjust<Color>>(
|
||||
// TODO: Remove this once GPU shader nodes are able to support the non-classic algorithm
|
||||
// TODO: Maybe re-add the "Raster: Adjustment" category to make this user-facing if we care to make this not just for testing
|
||||
#[node_macro::node(name("Brightness/Contrast Classic"), category(""), properties("brightness_contrast_properties"), shader_node(PerPixelAdjust))]
|
||||
fn brightness_contrast_classic<T: Adjust<Color>>(
|
||||
fn brightness_contrast_classic<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -174,12 +172,12 @@ fn brightness_contrast_classic<T: Adjust<Color>>(
|
||||
// Some further analysis available at:
|
||||
// https://geraldbakker.nl/psnumbers/brightness-contrast.html
|
||||
#[node_macro::node(name("Brightness/Contrast"), category("Raster: Adjustment"), properties("brightness_contrast_properties"), cfg(feature = "std"))]
|
||||
fn brightness_contrast<T: Adjust<Color>>(
|
||||
fn brightness_contrast<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_ctx: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -255,12 +253,12 @@ fn brightness_contrast<T: Adjust<Color>>(
|
||||
// Some further analysis available at:
|
||||
// https://geraldbakker.nl/psnumbers/levels.html
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("levels_properties"), shader_node(PerPixelAdjust))]
|
||||
fn levels<T: Adjust<Color>>(
|
||||
fn levels<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -334,12 +332,12 @@ fn levels<T: Adjust<Color>>(
|
||||
// Works the same for gamma and linear color
|
||||
// TODO: Currently the un-List-wrapped `tint` Color is causing a type error. Put this back in the "Raster: Adjustment" category once that's fixed.
|
||||
#[node_macro::node(name("Black & White"), category(""), properties("black_and_white_properties"), shader_node(PerPixelAdjust))]
|
||||
fn black_and_white<T: Adjust<Color>>(
|
||||
fn black_and_white<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -417,12 +415,12 @@ fn black_and_white<T: Adjust<Color>>(
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27hue%20%27%20%3D%20Old,saturation%2C%20Photoshop%205.0
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=0%20%3D%20Use%20other.-,Hue/Saturation,-Hue/Saturation%20settings
|
||||
#[node_macro::node(name("Hue/Saturation"), category("Raster: Adjustment"), properties("hue_saturation_properties"), shader_node(PerPixelAdjust))]
|
||||
fn hue_saturation<T: Adjust<Color>>(
|
||||
fn hue_saturation<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -449,12 +447,12 @@ fn hue_saturation<T: Adjust<Color>>(
|
||||
// Aims for interoperable compatibility with:
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27%20%3D%20Color%20Lookup-,%27nvrt%27%20%3D%20Invert,-%27post%27%20%3D%20Posterize
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn invert<T: Adjust<Color>>(
|
||||
fn invert<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -470,12 +468,12 @@ fn invert<T: Adjust<Color>>(
|
||||
// Aims for interoperable compatibility with:
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=post%27%20%3D%20Posterize-,%27thrs%27%20%3D%20Threshold,-%27grdm%27%20%3D%20Gradient
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("threshold_properties"), shader_node(PerPixelAdjust))]
|
||||
fn threshold<T: Adjust<Color>>(
|
||||
fn threshold<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -516,12 +514,12 @@ fn threshold<T: Adjust<Color>>(
|
||||
// When both parameters are set, it is equivalent to running this adjustment twice, with only vibrance set and then only saturation set.
|
||||
// (Except for some noise probably due to rounding error.)
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("vibrance_properties"), shader_node(PerPixelAdjust))]
|
||||
fn vibrance<T: Adjust<Color>>(
|
||||
fn vibrance<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -718,12 +716,12 @@ pub enum DomainWarpType {
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"), shader_node(PerPixelAdjust))]
|
||||
fn channel_mixer<T: Adjust<Color>>(
|
||||
fn channel_mixer<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -850,12 +848,12 @@ pub enum SelectiveColorChoice {
|
||||
// Algorithm based on:
|
||||
// https://blog.pkh.me/p/22-understanding-selective-coloring-in-adobe-photoshop.html
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("selective_color_properties"), shader_node(PerPixelAdjust))]
|
||||
fn selective_color<T: Adjust<Color>>(
|
||||
fn selective_color<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut image: T,
|
||||
@@ -994,12 +992,12 @@ fn selective_color<T: Adjust<Color>>(
|
||||
// https://www.axiomx.com/posterize.htm
|
||||
// This algorithm produces fully accurate output in relation to the industry standard.
|
||||
#[node_macro::node(category("Raster: Adjustment"), shader_node(PerPixelAdjust))]
|
||||
fn posterize<T: Adjust<Color>>(
|
||||
fn posterize<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
@@ -1023,12 +1021,12 @@ fn posterize<T: Adjust<Color>>(
|
||||
// Algorithm based on:
|
||||
// https://geraldbakker.nl/psnumbers/exposure.html
|
||||
#[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"), shader_node(PerPixelAdjust))]
|
||||
fn exposure<T: Adjust<Color>>(
|
||||
fn exposure<T: Adjust<Color> + Clone + Send + Sync + no_std_types::context::CacheHash + 'static>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
#[gpu_image]
|
||||
mut input: T,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Not immediately shader compatible due to needing [`GradientStops`] as a param, which needs [`Vec`]
|
||||
|
||||
use crate::adjust::Adjust;
|
||||
use core_types::list::List;
|
||||
use core_types::{Color, Ctx};
|
||||
use core_types::{Color, Ctx, ExtractIndex, InjectIndex};
|
||||
use raster_types::{CPU, Raster};
|
||||
use vector_types::GradientStops;
|
||||
|
||||
@@ -10,18 +9,21 @@ use vector_types::GradientStops;
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27grdm%27%20%3D%20Gradient%20Map
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Gradient%20settings%20(Photoshop%206.0)
|
||||
#[node_macro::node(category("Raster: Adjustment"))]
|
||||
fn gradient_map<T: Adjust<Color>>(
|
||||
_: impl Ctx,
|
||||
fn gradient_map<T: Adjust<Color> + Clone + Send + Sync + core_types::CacheHash + 'static>(
|
||||
_: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
#[implementations(
|
||||
List<Raster<CPU>>,
|
||||
List<Color>,
|
||||
List<GradientStops>,
|
||||
Raster<CPU>,
|
||||
Color,
|
||||
GradientStops,
|
||||
)]
|
||||
mut image: T,
|
||||
gradient: List<GradientStops>,
|
||||
gradient: IList<GradientStops>,
|
||||
reverse: bool,
|
||||
) -> T {
|
||||
let Some(gradient) = gradient.element(0) else { return image };
|
||||
if gradient.is_empty() {
|
||||
return image;
|
||||
}
|
||||
let gradient = gradient.element_ref(0);
|
||||
|
||||
image.adjust(|color| {
|
||||
let intensity = color.luminance_rec_709();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::adjustments::{CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, NoiseType};
|
||||
use core_types::ATTR_TRANSFORM;
|
||||
use core_types::attribute::{Attr, Transform as TransformAttr};
|
||||
use core_types::attribute::{Attr, Attribute, BlendMode as BlendModeAttr, ClippingMask, EditorLayerPath, Opacity, OpacityFill, Transform as TransformAttr};
|
||||
use core_types::color::Color;
|
||||
use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBMut};
|
||||
use core_types::context::{Ctx, ExtractFootprint, ExtractIndex, InjectIndex};
|
||||
use core_types::list::{Item, List};
|
||||
use core_types::extent::{LevelIn, ListIn, ValueIn};
|
||||
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
|
||||
use core_types::math::bbox::Bbox;
|
||||
use core_types::transform::Transform;
|
||||
use dyn_any::DynAny;
|
||||
@@ -31,199 +31,249 @@ impl From<std::io::Error> for Error {
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Debug"))]
|
||||
pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: List<Raster<CPU>>) -> List<Raster<CPU>> {
|
||||
image_frame
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let image_frame_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
let (image, mut attributes) = row.into_parts();
|
||||
pub fn sample_image(ctx: impl Ctx + ExtractFootprint, (image, lane_transform): (Raster<CPU>, Attr<TransformAttr>)) -> (Raster<CPU>, Attr<TransformAttr>) {
|
||||
let image_frame_transform: DAffine2 = *lane_transform;
|
||||
|
||||
// Resize the image using the image crate
|
||||
let data = bytemuck::cast_vec(image.data.clone());
|
||||
// Resize the image using the image crate
|
||||
let data = bytemuck::cast_vec(image.data.clone());
|
||||
|
||||
let footprint = ctx.footprint();
|
||||
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
||||
let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
|
||||
let intersection = viewport_bounds.intersect(&image_bounds);
|
||||
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
|
||||
let size = intersection.size();
|
||||
let size_px = image_size.transform_vector2(size).as_uvec2();
|
||||
let footprint = ctx.footprint();
|
||||
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
||||
let image_bounds = Bbox::from_transform(image_frame_transform).to_axis_aligned_bbox();
|
||||
let intersection = viewport_bounds.intersect(&image_bounds);
|
||||
let image_size = DAffine2::from_scale(DVec2::new(image.width as f64, image.height as f64));
|
||||
let size = intersection.size();
|
||||
let size_px = image_size.transform_vector2(size).as_uvec2();
|
||||
|
||||
// If the image would not be visible, add nothing.
|
||||
if size.x <= 0. || size.y <= 0. {
|
||||
return None;
|
||||
}
|
||||
// A culled lane serves a zero-size raster, which renders as nothing.
|
||||
if size.x <= 0. || size.y <= 0. {
|
||||
return (Raster::new_cpu(Image::default()), Attr(image_frame_transform));
|
||||
}
|
||||
|
||||
let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
|
||||
let image_buffer = ::image::Rgba32FImage::from_raw(image.width, image.height, data).expect("Failed to convert internal image format into image-rs data type.");
|
||||
|
||||
let dynamic_image: ::image::DynamicImage = image_buffer.into();
|
||||
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
|
||||
let offset_px = image_size.transform_vector2(offset).as_uvec2();
|
||||
let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y);
|
||||
let dynamic_image: ::image::DynamicImage = image_buffer.into();
|
||||
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
|
||||
let offset_px = image_size.transform_vector2(offset).as_uvec2();
|
||||
let cropped = dynamic_image.crop_imm(offset_px.x, offset_px.y, size_px.x, size_px.y);
|
||||
|
||||
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
|
||||
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
|
||||
let mut new_width = size_px.x;
|
||||
let mut new_height = size_px.y;
|
||||
let viewport_resolution_x = footprint.transform.transform_vector2(DVec2::X * size.x).length();
|
||||
let viewport_resolution_y = footprint.transform.transform_vector2(DVec2::Y * size.y).length();
|
||||
let mut new_width = size_px.x;
|
||||
let mut new_height = size_px.y;
|
||||
|
||||
// Only downscale the image for now
|
||||
let resized = if new_width < image.width || new_height < image.height {
|
||||
new_width = viewport_resolution_x as u32;
|
||||
new_height = viewport_resolution_y as u32;
|
||||
// TODO: choose filter based on quality requirements
|
||||
cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle)
|
||||
} else {
|
||||
cropped
|
||||
};
|
||||
let buffer = resized.to_rgba32f();
|
||||
let buffer = buffer.into_raw();
|
||||
let vec = bytemuck::cast_vec(buffer);
|
||||
let image = Image {
|
||||
width: new_width,
|
||||
height: new_height,
|
||||
data: vec,
|
||||
base64_string: None,
|
||||
};
|
||||
// we need to adjust the offset if we truncate the offset calculation
|
||||
// Only downscale the image for now
|
||||
let resized = if new_width < image.width || new_height < image.height {
|
||||
new_width = viewport_resolution_x as u32;
|
||||
new_height = viewport_resolution_y as u32;
|
||||
// TODO: choose filter based on quality requirements
|
||||
cropped.resize_exact(new_width, new_height, ::image::imageops::Triangle)
|
||||
} else {
|
||||
cropped
|
||||
};
|
||||
let buffer = resized.to_rgba32f();
|
||||
let buffer = buffer.into_raw();
|
||||
let vec = bytemuck::cast_vec(buffer);
|
||||
let image = Image {
|
||||
width: new_width,
|
||||
height: new_height,
|
||||
data: vec,
|
||||
base64_string: None,
|
||||
};
|
||||
// we need to adjust the offset if we truncate the offset calculation
|
||||
|
||||
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||
attributes.insert(ATTR_TRANSFORM, new_transform);
|
||||
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||
|
||||
Some(Item::from_parts(Raster::new_cpu(image), attributes))
|
||||
})
|
||||
.collect()
|
||||
(Raster::new_cpu(image), Attr(new_transform))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Channels"))]
|
||||
pub fn combine_channels(
|
||||
_: impl Ctx,
|
||||
#[node_macro::node(category("Raster: Channels"), extent(combine_channels_extent))]
|
||||
pub fn combine_channels<'e>(
|
||||
ctx: impl Ctx + ExtractArena<'e> + ExtractIndex + InjectIndex + Copy,
|
||||
_primary: (),
|
||||
#[expose] red: List<Raster<CPU>>,
|
||||
#[expose] green: List<Raster<CPU>>,
|
||||
#[expose] blue: List<Raster<CPU>>,
|
||||
#[expose] alpha: List<Raster<CPU>>,
|
||||
) -> List<Raster<CPU>> {
|
||||
#[expose] red: IList<Raster<CPU>>,
|
||||
#[expose] green: IList<Raster<CPU>>,
|
||||
#[expose] blue: IList<Raster<CPU>>,
|
||||
#[expose] alpha: IList<Raster<CPU>>,
|
||||
) -> Result<
|
||||
IList<(
|
||||
Raster<CPU>,
|
||||
Attr<'e, TransformAttr>,
|
||||
Attr<'e, BlendModeAttr>,
|
||||
Attr<'e, Opacity>,
|
||||
Attr<'e, OpacityFill>,
|
||||
Attr<'e, ClippingMask>,
|
||||
Attr<'e, EditorLayerPath>,
|
||||
)>,
|
||||
Interrupt,
|
||||
> {
|
||||
let lane = ctx.innermost_index() as usize;
|
||||
let max_len = red.len().max(green.len()).max(blue.len()).max(alpha.len());
|
||||
let red = red.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||
let green = green.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||
let blue = blue.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||
let alpha = alpha.into_iter().map(Some).chain(std::iter::repeat(None)).take(max_len);
|
||||
if lane >= max_len {
|
||||
return Err(GraphError::past_end().into());
|
||||
}
|
||||
|
||||
red.zip(green)
|
||||
.zip(blue)
|
||||
.zip(alpha)
|
||||
.filter_map(|(((red, green), blue), alpha)| {
|
||||
// Turn any default zero-sized image items into None
|
||||
let red = red.filter(|i| i.element().width > 0 && i.element().height > 0);
|
||||
let green = green.filter(|i| i.element().width > 0 && i.element().height > 0);
|
||||
let blue = blue.filter(|i| i.element().width > 0 && i.element().height > 0);
|
||||
let alpha = alpha.filter(|i| i.element().width > 0 && i.element().height > 0);
|
||||
// Zero-size lanes and lanes past a shorter channel's end contribute nothing
|
||||
fn pick<'l>(list: &'l core_types::node::List<'_, Raster<CPU>>, lane: usize) -> Option<&'l Raster<CPU>> {
|
||||
(lane < list.len()).then(|| list.element_ref(lane)).filter(|i| i.width > 0 && i.height > 0)
|
||||
}
|
||||
let (red_el, green_el, blue_el, alpha_el) = (pick(&red, lane), pick(&green, lane), pick(&blue, lane), pick(&alpha, lane));
|
||||
|
||||
// Get this item's transform and alpha blending mode from the first non-empty channel
|
||||
let attributes = [&red, &green, &blue, &alpha].iter().find_map(|i| i.as_ref()).map(|i| i.attributes().clone())?;
|
||||
// This lane's transform and blending come from the first non-empty channel
|
||||
let attr_source = [(red_el.is_some(), &red), (green_el.is_some(), &green), (blue_el.is_some(), &blue), (alpha_el.is_some(), &alpha)]
|
||||
.into_iter()
|
||||
.find_map(|(present, list)| present.then_some(list.lane(lane)));
|
||||
|
||||
// Get the common width and height of the channels, which must have equal dimensions
|
||||
let channel_dimensions = [
|
||||
red.as_ref().map(|r| (r.element().width, r.element().height)),
|
||||
green.as_ref().map(|g| (g.element().width, g.element().height)),
|
||||
blue.as_ref().map(|b| (b.element().width, b.element().height)),
|
||||
alpha.as_ref().map(|a| (a.element().width, a.element().height)),
|
||||
];
|
||||
if channel_dimensions.iter().all(Option::is_none)
|
||||
|| channel_dimensions
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y))
|
||||
{
|
||||
return None;
|
||||
// The channels must have equal dimensions; an unusable lane serves a
|
||||
// zero-size raster, which renders as nothing (the legacy form dropped it)
|
||||
let channel_dimensions = [
|
||||
red_el.map(|r| (r.width, r.height)),
|
||||
green_el.map(|g| (g.width, g.height)),
|
||||
blue_el.map(|b| (b.width, b.height)),
|
||||
alpha_el.map(|a| (a.width, a.height)),
|
||||
];
|
||||
let mismatched = channel_dimensions
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|&(x, y)| channel_dimensions.iter().flatten().any(|&(other_x, other_y)| x != other_x || y != other_y));
|
||||
let (Some(source), Some(&(width, height)), false) = (attr_source, channel_dimensions.iter().flatten().next(), mismatched) else {
|
||||
return Ok((
|
||||
Raster::new_cpu(Image::default()),
|
||||
Attr(DAffine2::IDENTITY),
|
||||
Attr(<BlendModeAttr as Attribute>::default()),
|
||||
Attr(1.),
|
||||
Attr(1.),
|
||||
Attr(false),
|
||||
Attr(<EditorLayerPath as Attribute>::default()),
|
||||
));
|
||||
};
|
||||
|
||||
// Create a new image for the output element
|
||||
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
||||
|
||||
// Iterate over all pixels in the image and set the color channels
|
||||
for y in 0..image.height() {
|
||||
for x in 0..image.width() {
|
||||
let image_pixel = image.get_pixel_mut(x, y).unwrap();
|
||||
|
||||
if let Some(r) = red_el.and_then(|r| r.get_pixel(x, y)) {
|
||||
image_pixel.set_red(r.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_red(Channel::from_linear(0.));
|
||||
}
|
||||
let &(width, height) = channel_dimensions.iter().flatten().next()?;
|
||||
|
||||
// Create a new image for the output element
|
||||
let mut image = Image::new(width, height, Color::TRANSPARENT);
|
||||
|
||||
// Iterate over all pixels in the image and set the color channels
|
||||
for y in 0..image.height() {
|
||||
for x in 0..image.width() {
|
||||
let image_pixel = image.get_pixel_mut(x, y).unwrap();
|
||||
|
||||
if let Some(r) = red.as_ref().and_then(|r| r.element().get_pixel(x, y)) {
|
||||
image_pixel.set_red(r.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_red(Channel::from_linear(0.));
|
||||
}
|
||||
if let Some(g) = green.as_ref().and_then(|g| g.element().get_pixel(x, y)) {
|
||||
image_pixel.set_green(g.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_green(Channel::from_linear(0.));
|
||||
}
|
||||
if let Some(b) = blue.as_ref().and_then(|b| b.element().get_pixel(x, y)) {
|
||||
image_pixel.set_blue(b.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_blue(Channel::from_linear(0.));
|
||||
}
|
||||
if let Some(a) = alpha.as_ref().and_then(|a| a.element().get_pixel(x, y)) {
|
||||
image_pixel.set_alpha(a.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_alpha(Channel::from_linear(1.));
|
||||
}
|
||||
}
|
||||
if let Some(g) = green_el.and_then(|g| g.get_pixel(x, y)) {
|
||||
image_pixel.set_green(g.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_green(Channel::from_linear(0.));
|
||||
}
|
||||
if let Some(b) = blue_el.and_then(|b| b.get_pixel(x, y)) {
|
||||
image_pixel.set_blue(b.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_blue(Channel::from_linear(0.));
|
||||
}
|
||||
if let Some(a) = alpha_el.and_then(|a| a.get_pixel(x, y)) {
|
||||
image_pixel.set_alpha(a.l().cast_linear_channel());
|
||||
} else {
|
||||
image_pixel.set_alpha(Channel::from_linear(1.));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Item::from_parts(Raster::new_cpu(image), attributes))
|
||||
})
|
||||
.collect()
|
||||
// The layer path re-parks into the arena so the borrow outlives the batch
|
||||
let layer_path: Vec<core_types::uuid::NodeId> = source.attr::<EditorLayerPath>().to_vec();
|
||||
let (layer_path, _) = ctx.arena().alloc(layer_path).ok_or(GraphError {
|
||||
kind: core_types::gpoll::ErrorKind::ArenaExhausted,
|
||||
trace: Vec::new(),
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
Raster::new_cpu(image),
|
||||
Attr(source.attr::<TransformAttr>()),
|
||||
Attr(source.attr::<BlendModeAttr>()),
|
||||
Attr(source.attr::<Opacity>()),
|
||||
Attr(source.attr::<OpacityFill>()),
|
||||
Attr(source.attr::<ClippingMask>()),
|
||||
Attr(layer_path.as_slice()),
|
||||
))
|
||||
}
|
||||
|
||||
/// The combined level's count is the longest channel's; a lower-bound channel
|
||||
/// keeps the result a lower bound too, and consumers drain to past-end.
|
||||
fn combine_channels_extent(
|
||||
_primary: ValueIn<'_, ()>,
|
||||
red: ListIn<'_, Raster<CPU>>,
|
||||
green: ListIn<'_, Raster<CPU>>,
|
||||
blue: ListIn<'_, Raster<CPU>>,
|
||||
alpha: ListIn<'_, Raster<CPU>>,
|
||||
level: LevelIn,
|
||||
) -> GPoll<Extent> {
|
||||
match level.top() {
|
||||
true => red.total().zip(green.total()).zip(blue.total()).zip(alpha.total()).map(|(((red, green), blue), alpha)| {
|
||||
let totals = [red, green, blue, alpha];
|
||||
let bound = totals
|
||||
.iter()
|
||||
.map(|extent| match extent {
|
||||
Extent::Exactly(count) | Extent::AtLeast(count) => *count,
|
||||
Extent::Free => 0,
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
match totals.iter().all(|extent| matches!(extent, Extent::Exactly(_))) {
|
||||
true => Extent::Exactly(bound),
|
||||
false => Extent::AtLeast(bound),
|
||||
}
|
||||
}),
|
||||
false => GPoll::Final(Extent::Exactly(1)),
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster"))]
|
||||
pub fn mask(
|
||||
_: impl Ctx,
|
||||
_: impl Ctx + ExtractIndex + InjectIndex + Copy,
|
||||
/// The image to be masked.
|
||||
image: List<Raster<CPU>>,
|
||||
(mut image, lane_transform): (Raster<CPU>, Attr<TransformAttr>),
|
||||
/// The stencil to be used for masking.
|
||||
#[expose]
|
||||
stencil: List<Raster<CPU>>,
|
||||
) -> List<Raster<CPU>> {
|
||||
stencil: IList<Raster<CPU>>,
|
||||
) -> (Raster<CPU>, Attr<TransformAttr>) {
|
||||
// TODO: Figure out what it means to support multiple stencil items?
|
||||
let Some(stencil) = stencil.into_iter().next() else {
|
||||
if stencil.is_empty() {
|
||||
// No stencil provided so we return the original image
|
||||
return image;
|
||||
};
|
||||
let stencil_size = DVec2::new(stencil.element().width as f64, stencil.element().height as f64);
|
||||
return (image, Attr(*lane_transform));
|
||||
}
|
||||
let stencil_element = stencil.element_ref(0);
|
||||
let stencil_transform: DAffine2 = stencil.lane(0).attr::<TransformAttr>();
|
||||
let stencil_size = DVec2::new(stencil_element.width as f64, stencil_element.height as f64);
|
||||
|
||||
image
|
||||
.into_iter()
|
||||
.filter_map(|mut row| {
|
||||
let image_size = DVec2::new(row.element().width as f64, row.element().height as f64);
|
||||
let stencil_transform: DAffine2 = stencil.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
let mask_size = stencil_transform.scale_magnitudes();
|
||||
let image_size = DVec2::new(image.width as f64, image.height as f64);
|
||||
let mask_size = stencil_transform.scale_magnitudes();
|
||||
|
||||
if mask_size == DVec2::ZERO {
|
||||
return None;
|
||||
}
|
||||
// A degenerate stencil serves a zero-size raster, which renders as
|
||||
// nothing (the legacy form dropped the lane)
|
||||
if mask_size == DVec2::ZERO {
|
||||
return (Raster::new_cpu(Image::default()), Attr(*lane_transform));
|
||||
}
|
||||
|
||||
// Transforms a point from the background image to the foreground image
|
||||
let transform_attribute: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM);
|
||||
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
|
||||
let stencil_transform_inverse = stencil_transform.inverse();
|
||||
// Transforms a point from the background image to the foreground image
|
||||
let transform_attribute: DAffine2 = *lane_transform;
|
||||
let bg_to_fg = transform_attribute * DAffine2::from_scale(1. / image_size);
|
||||
let stencil_transform_inverse = stencil_transform.inverse();
|
||||
|
||||
for y in 0..row.element().height {
|
||||
for x in 0..row.element().width {
|
||||
let image_point = DVec2::new(x as f64, y as f64);
|
||||
let mask_point = bg_to_fg.transform_point2(image_point);
|
||||
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
|
||||
let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
|
||||
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point);
|
||||
for y in 0..image.height {
|
||||
for x in 0..image.width {
|
||||
let image_point = DVec2::new(x as f64, y as f64);
|
||||
let mask_point = bg_to_fg.transform_point2(image_point);
|
||||
let local_mask_point = stencil_transform_inverse.transform_point2(mask_point);
|
||||
let mask_point = stencil_transform.transform_point2(local_mask_point.clamp(DVec2::ZERO, DVec2::ONE));
|
||||
let mask_point = (DAffine2::from_scale(stencil_size) * stencil_transform.inverse()).transform_point2(mask_point);
|
||||
|
||||
let image_pixel = row.element_mut().data_mut().get_pixel_mut(x, y).unwrap();
|
||||
let mask_pixel = stencil.element().sample(mask_point);
|
||||
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
|
||||
}
|
||||
}
|
||||
let image_pixel = image.data_mut().get_pixel_mut(x, y).unwrap();
|
||||
let mask_pixel = stencil_element.sample(mask_point);
|
||||
*image_pixel = image_pixel.multiplied_alpha(mask_pixel.l().cast_linear_channel());
|
||||
}
|
||||
}
|
||||
|
||||
Some(row)
|
||||
})
|
||||
.collect()
|
||||
(image, Attr(transform_attribute))
|
||||
}
|
||||
|
||||
/// The per-lane extend, shared with the brush's plain callers.
|
||||
@@ -348,7 +398,7 @@ pub fn noise_pattern(
|
||||
#[widget(ParsedWidgetOverride::Custom = "noise_properties_cellular_jitter")]
|
||||
#[default(1.)]
|
||||
cellular_jitter: f64,
|
||||
) -> List<Raster<CPU>> {
|
||||
) -> (Raster<CPU>, Attr<TransformAttr>) {
|
||||
let footprint = ctx.footprint();
|
||||
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
||||
|
||||
@@ -364,9 +414,9 @@ pub fn noise_pattern(
|
||||
size = intersection.size();
|
||||
}
|
||||
|
||||
// If the image would not be visible, return an empty image
|
||||
// A culled pattern serves a zero-size raster, which renders as nothing
|
||||
if size.x <= 0. || size.y <= 0. {
|
||||
return List::new();
|
||||
return (Raster::new_cpu(Image::default()), Attr(DAffine2::IDENTITY));
|
||||
}
|
||||
|
||||
let transform = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
|
||||
@@ -412,7 +462,7 @@ pub fn noise_pattern(
|
||||
}
|
||||
}
|
||||
|
||||
return List::new_from_item(Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform));
|
||||
return (Raster::new_cpu(image), Attr(transform));
|
||||
}
|
||||
};
|
||||
noise.set_noise_type(Some(noise_type));
|
||||
@@ -470,11 +520,11 @@ pub fn noise_pattern(
|
||||
}
|
||||
}
|
||||
|
||||
List::new_from_item(Item::new_from_element(Raster::new_cpu(image)).with_attribute(ATTR_TRANSFORM, transform))
|
||||
(Raster::new_cpu(image), Attr(transform))
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Raster: Pattern"))]
|
||||
pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> {
|
||||
pub fn mandelbrot(ctx: impl Ctx + ExtractFootprint, _primary: ()) -> (Raster<CPU>, Attr<TransformAttr>) {
|
||||
let footprint = ctx.footprint();
|
||||
let viewport_bounds = footprint.viewport_bounds_in_local_space();
|
||||
|
||||
@@ -484,9 +534,9 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> {
|
||||
|
||||
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO);
|
||||
|
||||
// If the image would not be visible, return an empty image
|
||||
// A culled pattern serves a zero-size raster, which renders as nothing
|
||||
if size.x <= 0. || size.y <= 0. {
|
||||
return List::new();
|
||||
return (Raster::new_cpu(Image::default()), Attr(DAffine2::IDENTITY));
|
||||
}
|
||||
|
||||
let scale = footprint.scale();
|
||||
@@ -508,14 +558,14 @@ pub fn mandelbrot(ctx: impl ExtractFootprint + Send) -> List<Raster<CPU>> {
|
||||
}
|
||||
}
|
||||
|
||||
List::new_from_item(
|
||||
Item::new_from_element(Raster::new_cpu(Image {
|
||||
(
|
||||
Raster::new_cpu(Image {
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
..Default::default()
|
||||
}))
|
||||
.with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(offset) * DAffine2::from_scale(size)),
|
||||
}),
|
||||
Attr(DAffine2::from_translation(offset) * DAffine2::from_scale(size)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user