Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -802,8 +802,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
parent_and_insert_index,
place_at_origin,
} => {
// All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb`

let layer_parent = self.new_layer_parent(true);
let image_size = DVec2::new(image.width as f64, image.height as f64);

Expand Down
4 changes: 3 additions & 1 deletion editor/src/messages/portfolio/document_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2123,7 +2123,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
_ => None,
});

if let Some(image) = image {
if let Some(mut image) = image {
// Legacy embedded pixel data is premultiplied, so restore straight alpha before encoding it
image.data.iter_mut().for_each(|pixel| *pixel = pixel.to_unassociated_alpha());
Comment thread
Keavon marked this conversation as resolved.
let hash = document.resources.embedded.store(Resource::new(image.to_png()));

let resource_id = ResourceId::new();
Expand Down
20 changes: 16 additions & 4 deletions node-graph/libraries/no-std-types/src/blending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode,
blend_mode => apply_blend_mode(foreground, background, blend_mode),
};

background.alpha_blend(target_color.apply_opacity(opacity))
background.alpha_blend(target_color.with_alpha(target_color.a() * opacity))
}

/// Mixes the two colors by the blend mode's own formula, leaving the alpha compositing to the caller.
Expand Down Expand Up @@ -283,17 +283,29 @@ mod tests {
}

#[test]
fn darker_color_compares_unassociated_channels() {
// The premultiplied backdrop reads as 0.1 gray but is really 0.5 gray, so the 0.4 gray foreground is the darker color
fn darker_color_ignores_backdrop_alpha() {
// The backdrop's low alpha doesn't darken its color, so the 0.4 gray foreground is the darker color
let foreground = Color::from_rgbaf32_unchecked(0.4, 0.4, 0.4, 1.);
let background = Color::from_rgbaf32_unchecked(0.1, 0.1, 0.1, 0.2);
let background = Color::from_rgbaf32_unchecked(0.5, 0.5, 0.5, 0.2);

let blended = apply_blend_mode(foreground, background, BlendMode::DarkerColor);

assert!((blended.r() - 0.4).abs() < 1e-5, "red was {}", blended.r());
assert!((blended.a() - 1.).abs() < 1e-5, "alpha was {}", blended.a());
}

#[test]
fn source_over_weights_straight_colors_by_alpha() {
let over = Color::from_rgbaf32_unchecked(1., 0., 0., 0.5);
let under = Color::from_rgbaf32_unchecked(0., 0., 1., 1.);

let blended = under.alpha_blend(over);

assert!((blended.r() - 0.5).abs() < 1e-5, "red was {}", blended.r());
assert!((blended.b() - 0.5).abs() < 1e-5, "blue was {}", blended.b());
assert!((blended.a() - 1.).abs() < 1e-5, "alpha was {}", blended.a());
}

#[test]
fn alpha_only_modes_fade_with_opacity() {
let foreground = Color::from_rgbaf32_unchecked(0.9, 0.9, 0.9, 1.);
Expand Down
8 changes: 0 additions & 8 deletions node-graph/libraries/no-std-types/src/color/color_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,6 @@ pub trait RGBMut: RGB {
fn set_blue(&mut self, blue: Self::ColorChannel);
}

pub trait AssociatedAlpha: RGB + Alpha {
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
}

pub trait UnassociatedAlpha: RGB + Alpha {
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
}

pub trait Alpha {
type AlphaChannel: LinearChannel;
const TRANSPARENT: Self;
Expand Down
85 changes: 32 additions & 53 deletions node-graph/libraries/no-std-types/src/color/color_types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::color_traits::{Alpha, AlphaMut, Luminance, Pixel, RGB, RGBMut, Rec709Primaries, SRGB};
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use bytemuck::{Pod, Zeroable};
use core::fmt::Debug;
Expand Down Expand Up @@ -72,7 +72,7 @@ impl Alpha for RGBA16F {
type AlphaChannel = f32;
#[inline(always)]
fn alpha(&self) -> f32 {
self.alpha.to_f32() / 255.
self.alpha.to_f32()
}

const TRANSPARENT: Self = RGBA16F {
Expand All @@ -83,9 +83,8 @@ impl Alpha for RGBA16F {
};

fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
let alpha = alpha * 255.;
let mut result = *self;
result.alpha = f16::from_f32(alpha * self.alpha());
result.alpha = f16::from_f32(self.alpha() * alpha);
result
}
}
Expand Down Expand Up @@ -254,7 +253,7 @@ impl RGB for Luma {

impl Pixel for Luma {}

/// Linear-light sRGB color with `f32` channels (alpha unassociated for swatch/UI colors, associated/premultiplied for pixel data inside [`Image<Color>`]).
/// Linear-light sRGB color with `f32` channels and unassociated (straight) alpha.
Comment thread
Keavon marked this conversation as resolved.
///
/// Channels range from `0.` to `f32::MAX`, encoding brightness proportional to light intensity (cd/m² nits in HDR, or `0..=1` mapped to white for SDR).
///
Expand Down Expand Up @@ -359,9 +358,7 @@ impl Pixel for Color {
}

fn from_bytes(bytes: &[u8]) -> Self {
// `Image<Color>` pixel convention is linear-light with associated (premultiplied) alpha.
let srgba = SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]);
Color::from(srgba).apply_opacity(bytes[3] as f32 / 255.)
SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()
}
fn byte_size() -> usize {
4
Expand All @@ -378,18 +375,7 @@ impl Alpha for Color {
}
#[inline(always)]
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
Self {
red: self.red * alpha,
green: self.green * alpha,
blue: self.blue * alpha,
alpha: self.alpha * alpha,
}
}
}

impl AssociatedAlpha for Color {
fn to_unassociated<Out: super::UnassociatedAlpha>(&self) -> Out {
todo!()
Self { alpha: self.alpha * alpha, ..*self }
}
}

Expand Down Expand Up @@ -443,12 +429,6 @@ impl Color {
Color { red, green, blue, alpha }
}

/// Construct a `Color` from unassociated (straight) RGBA channels, premultiplying the RGB channels by alpha.
#[inline(always)]
pub fn new_from_unassociated_rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Color {
Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha)
}

/// Create a linear-light `Color` from HSL coordinates (all between 0 and 1).
/// HSL is defined on sRGB display values, so the RGB produced by the HSL math is gamma-encoded and decoded to linear before being wrapped in `Color`.
///
Expand Down Expand Up @@ -707,8 +687,7 @@ impl Color {
/// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB, with `other`'s alpha.
#[inline(always)]
pub fn blend_darker_color(&self, other: Color) -> Color {
let background = self.to_unassociated_alpha();
let darker = if background.average_rgb_channels() <= other.average_rgb_channels() { background } else { other };
let darker = if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other };

darker.with_alpha(other.alpha)
}
Expand Down Expand Up @@ -740,8 +719,7 @@ impl Color {
/// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB, with `other`'s alpha.
#[inline(always)]
pub fn blend_lighter_color(&self, other: Color) -> Color {
let background = self.to_unassociated_alpha();
let lighter = if background.average_rgb_channels() >= other.average_rgb_channels() { background } else { other };
let lighter = if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other };

lighter.with_alpha(other.alpha)
}
Expand Down Expand Up @@ -824,25 +802,23 @@ impl Color {

/// Whole-color "Hue" blend: source hue with this color's saturation and Rec.601 luma, with `c_s`'s alpha.
pub fn blend_hue(&self, c_s: Color) -> Color {
let background = self.to_unassociated_alpha();
let sat_b = background.chroma_range();
let lum_b = background.luminance_rec_601();
let sat_b = self.chroma_range();
let lum_b = self.luminance_rec_601();

c_s.with_saturation(sat_b).with_luminance(lum_b).with_alpha(c_s.alpha)
}

/// Whole-color "Saturation" blend: this color's hue/luma with source saturation, with `c_s`'s alpha.
pub fn blend_saturation(&self, c_s: Color) -> Color {
let background = self.to_unassociated_alpha();
let sat_s = c_s.chroma_range();
let lum_b = background.luminance_rec_601();
let lum_b = self.luminance_rec_601();

background.with_saturation(sat_s).with_luminance(lum_b).with_alpha(c_s.alpha)
self.with_saturation(sat_s).with_luminance(lum_b).with_alpha(c_s.alpha)
}

/// Whole-color "Color" blend: source hue/saturation with this color's luma, with `c_s`'s alpha.
pub fn blend_color(&self, c_s: Color) -> Color {
let lum_b = self.to_unassociated_alpha().luminance_rec_601();
let lum_b = self.luminance_rec_601();

c_s.with_luminance(lum_b).with_alpha(c_s.alpha)
}
Expand All @@ -851,7 +827,7 @@ impl Color {
pub fn blend_luminosity(&self, c_s: Color) -> Color {
let lum_s = c_s.luminance_rec_601();

self.to_unassociated_alpha().with_luminance(lum_s).with_alpha(c_s.alpha)
self.with_luminance(lum_s).with_alpha(c_s.alpha)
}

/// All four channels as `(red, green, blue, alpha)`.
Expand Down Expand Up @@ -990,13 +966,13 @@ impl Color {
Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a())
}

/// Multiply all four channels (including alpha) by `opacity`, applying an additional premultiplication factor to this Color.
/// Multiply RGB by alpha, giving the associated (premultiplied) form for compositing and filtering.
#[inline(always)]
pub fn apply_opacity(&self, opacity: f32) -> Self {
Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity)
pub fn to_associated_alpha(&self) -> Self {
Comment thread
Keavon marked this conversation as resolved.
self.map_rgb(|channel| channel * self.alpha)
}

/// Divide RGB by alpha to recover unassociated (straight-alpha) channels; no-op if alpha is zero.
/// Divide RGB by alpha, undoing [`Self::to_associated_alpha`]; no-op if alpha is zero.
#[inline(always)]
pub fn to_unassociated_alpha(&self) -> Self {
if self.alpha == 0. {
Expand All @@ -1011,27 +987,30 @@ impl Color {
}
}

/// Apply a per-channel blend function to this color (unmultiplied) and `other`, returning a color with `other`'s alpha; channels are clamped to 0..1.
/// Apply a per-channel blend function to this color and `other`, returning a color with `other`'s alpha; channels are clamped to 0..1.
#[inline(always)]
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.),
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.),
alpha: other.alpha,
}
}

/// Porter-Duff "source over" composite of `other` over `self`. Both colors must use associated (premultiplied) alpha.
/// Porter-Duff "source over" composite of `other` over `self`.
#[inline(always)]
pub fn alpha_blend(&self, other: Color) -> Self {
let inv_alpha = 1. - other.alpha;
let under_weight = self.alpha * (1. - other.alpha);
let alpha = other.alpha + under_weight;
if alpha == 0. {
return Self::TRANSPARENT;
}
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,
red: (other.red * other.alpha + self.red * under_weight) / alpha,
green: (other.green * other.alpha + self.green * under_weight) / alpha,
blue: (other.blue * other.alpha + self.blue * under_weight) / alpha,
alpha,
}
}

Expand Down
28 changes: 15 additions & 13 deletions node-graph/libraries/raster-types/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,7 @@ impl<P: Pixel> Image<P> {
impl Image<Color> {
/// 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| {
// `Image<Color>` pixels are stored linear-light with premultiplied alpha
let srgba = SRGBA8::new(v[0], v[1], v[2], v[3]);
Color::from(srgba).apply_opacity(v[3] as f32 / 255.)
})
.collect();
let data = image_data.chunks_exact(4).map(|v| SRGBA8::new(v[0], v[1], v[2], v[3]).into()).collect();
Image {
width,
height,
Expand All @@ -171,7 +164,7 @@ impl Image<Color> {
}

use super::*;
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
impl<P: Alpha + RGB> Image<P>
Comment thread
Keavon marked this conversation as resolved.
where
P::ColorChannel: Linear,
<P as Alpha>::AlphaChannel: Linear,
Expand All @@ -195,10 +188,9 @@ where
// Smaller alpha values than this would map to fully transparent
// anyway, avoid expensive encoding.
if a >= 0.5 / 255. {
let undo_premultiply = 1. / a;
let r = color.r().to_f32() * undo_premultiply;
let g = color.g().to_f32() * undo_premultiply;
let b = color.b().to_f32() * undo_premultiply;
let r = color.r().to_f32();
let g = color.g().to_f32();
let b = color.b().to_f32();

// Compute new sRGB value if necessary.
if r != last_r {
Expand Down Expand Up @@ -287,4 +279,14 @@ mod test {

assert_eq!(image, deserialized);
}

#[test]
fn image_data_round_trips_translucent_pixels() {
use super::*;
let bytes = [255, 0, 0, 128, 0, 255, 0, 1, 255, 255, 255, 41, 10, 20, 30, 255];

let image = Image::from_image_data(&bytes, 4, 1);

assert_eq!(image.to_flat_u8().0, bytes);
}
}
23 changes: 5 additions & 18 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use dyn_any::DynAny;
use glam::{DAffine2, DMat2, DVec2};
use graphene_hash::CacheHashWrapper;
use graphene_resource::Resource;
use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture};
use graphic_types::raster_types::{CPU, GPU, Image, Raster, Texture};
use graphic_types::vector_types::gradient::{Gradient, GradientForm};
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
use graphic_types::vector_types::vector::misc::dvec2_to_point;
Expand Down Expand Up @@ -130,9 +130,7 @@ fn composite_paint_over(over: Color, under: Color, blend_mode: BlendMode) -> Col
return Color::TRANSPARENT;
}

// The blend formulas read their backdrop premultiplied
let premultiplied_under = Color::from_rgbaf32_unchecked(under.r() * under_alpha, under.g() * under_alpha, under.b() * under_alpha, under_alpha);
let mixed = apply_blend_mode(over, premultiplied_under, blend_mode);
let mixed = apply_blend_mode(over, under, blend_mode);

// The mode only mixes where the backdrop has coverage, so its alpha interpolates each source channel from the raw color to the mixed color
let source_channel = |over_channel: f32, mixed_channel: f32| over_channel * (1. - under_alpha) + mixed_channel * under_alpha;
Expand Down Expand Up @@ -428,17 +426,8 @@ fn singular_values(transform: DAffine2) -> (f64, f64) {
pub fn black_or_white_for_best_contrast(background: Option<Color>) -> Color {
let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR };

let alpha = bg.a();

// Un-premultiply, then encode to gamma sRGB to do the composite in display space.
let (gamma_r, gamma_g, gamma_b) = if alpha > f32::EPSILON {
let [r, g, b, _] = Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb_channels();
(r, g, b)
} else {
(0., 0., 0.)
};

// Composite over black in sRGB space (premultiplied by alpha), then decode to linear for the luminance test.
// Composite over black in gamma sRGB space, then decode to linear for the luminance test.
let [gamma_r, gamma_g, gamma_b, alpha] = bg.to_gamma_srgb_channels();
let composited = Color::from_gamma_srgb_channels(gamma_r * alpha, gamma_g * alpha, gamma_b * alpha, 1.);

let threshold = (1.05 * 0.05f32).sqrt() - 0.05;
Expand Down Expand Up @@ -2295,9 +2284,7 @@ fn render_raster_cpu_item_svg(item: ItemRef<'_, Raster<CPU>>, render: &mut SvgRe
}

if render_params.to_canvas() {
let mut image_copy = image.clone();
image_copy.data_mut().map_pixels(|p| p.to_unassociated_alpha());
let id = *render.image_data.entry(CacheHashWrapper(image_copy.into_data())).or_insert_with(generate_uuid);
let id = *render.image_data.entry(CacheHashWrapper(image.clone().into_data())).or_insert_with(generate_uuid);

render.parent_tag(
"foreignObject",
Expand Down
4 changes: 1 addition & 3 deletions node-graph/libraries/wgpu-executor/src/texture_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,7 @@ impl RasterGpuToRasterCpuConverter {
let start = row * row_stride;
let row_slice = &view[start..start + row_bytes];
for px in row_slice.chunks_exact(4) {
// `Image<Color>` pixels are stored linear-light with associated (premultiplied) alpha
let srgba = SRGBA8::new(px[0], px[1], px[2], px[3]);
cpu_data.push(Color::from(srgba).apply_opacity(px[3] as f32 / 255.));
cpu_data.push(SRGBA8::new(px[0], px[1], px[2], px[3]).into());
}
}

Expand Down
Loading
Loading