839 lines
27 KiB
Rust
839 lines
27 KiB
Rust
use hcie_blend::blend_pixels;
|
|
use hcie_fx::apply_layer_effects;
|
|
use hcie_protocol::Layer;
|
|
use hcie_tile::{TiledLayer, TILE_SIZE};
|
|
use rayon::prelude::*;
|
|
|
|
/// Pixel-area threshold below which the compositor uses a sequential row
|
|
/// loop instead of Rayon parallelism. For small dirty regions (e.g. a single
|
|
/// brush stamp) the thread-pool overhead of `par_chunks_exact_mut` dominates
|
|
/// the actual blending work. 256x256 matches the tile size and is a safe
|
|
/// crossover point where sequential iteration is consistently faster.
|
|
const SEQUENTIAL_PIXEL_THRESHOLD: u32 = 256 * 256;
|
|
|
|
/// Composite layers using tile storage.
|
|
/// Each layer's dense buffer is read via `layer.get_pixel()`.
|
|
/// This is the same as the parallel version but optimized to skip
|
|
/// transparent tiles. Uses dirty region to further cull work.
|
|
pub fn composite_tiled(
|
|
layers: &[Layer],
|
|
tile_layers: &[Option<TiledLayer>],
|
|
canvas_width: u32,
|
|
canvas_height: u32,
|
|
) -> Vec<u8> {
|
|
let px_count = (canvas_width * canvas_height) as usize;
|
|
let mut output = vec![0u8; px_count * 4];
|
|
composite_tiled_into(
|
|
layers,
|
|
tile_layers,
|
|
canvas_width,
|
|
canvas_height,
|
|
0,
|
|
0,
|
|
canvas_width,
|
|
canvas_height,
|
|
&mut output,
|
|
);
|
|
output
|
|
}
|
|
|
|
/// Composite tile layers into an existing buffer within a region.
|
|
pub fn composite_tiled_into(
|
|
layers: &[Layer],
|
|
tile_layers: &[Option<TiledLayer>],
|
|
canvas_width: u32,
|
|
canvas_height: u32,
|
|
x0: u32,
|
|
y0: u32,
|
|
x1: u32,
|
|
y1: u32,
|
|
output: &mut [u8],
|
|
) {
|
|
let region_w = x1.saturating_sub(x0);
|
|
let region_h = y1.saturating_sub(y0);
|
|
if region_w == 0 || region_h == 0 {
|
|
return;
|
|
}
|
|
|
|
let use_sequential = region_w.saturating_mul(region_h) <= SEQUENTIAL_PIXEL_THRESHOLD;
|
|
let row_stride = canvas_width as usize * 4;
|
|
|
|
// Pre-compute the base layer index for every clipping mask layer once.
|
|
// This avoids an O(i) reverse scan per pixel during blending.
|
|
let mut base_indices = Vec::with_capacity(layers.len());
|
|
for i in 0..layers.len() {
|
|
if layers[i].clipping_mask {
|
|
let mut base = None;
|
|
for j in (0..i).rev() {
|
|
if !layers[j].clipping_mask {
|
|
base = Some(j);
|
|
break;
|
|
}
|
|
}
|
|
base_indices.push(base);
|
|
} else {
|
|
base_indices.push(None);
|
|
}
|
|
}
|
|
|
|
for (i, layer) in layers.iter().enumerate() {
|
|
if !layer.visible {
|
|
continue;
|
|
}
|
|
if layer.layer_type == hcie_protocol::LayerType::Group {
|
|
continue;
|
|
}
|
|
let blend: hcie_blend::BlendMode = layer.blend_mode.into();
|
|
let opacity = layer.opacity.clamp(0.0, 1.0);
|
|
let is_clipping = layer.clipping_mask;
|
|
let base_idx = base_indices[i];
|
|
|
|
let (offset_x, offset_y) = match &layer.data {
|
|
hcie_protocol::LayerData::Text {
|
|
offset_x, offset_y, ..
|
|
} => (*offset_x as i32, *offset_y as i32),
|
|
_ => (0, 0),
|
|
};
|
|
|
|
let cx0 = (x0 as i32).max(offset_x).max(0) as u32;
|
|
let cx1 = (x1 as i32)
|
|
.min(offset_x + layer.width as i32)
|
|
.min(canvas_width as i32)
|
|
.max(0) as u32;
|
|
let cy0 = (y0 as i32).max(offset_y).max(0) as u32;
|
|
let cy1 = (y1 as i32)
|
|
.min(offset_y + layer.height as i32)
|
|
.min(canvas_height as i32)
|
|
.max(0) as u32;
|
|
|
|
let layer_w = layer.width.min(canvas_width);
|
|
let layer_h = layer.height.min(canvas_height);
|
|
|
|
let is_adj = layer.adjustment.is_some();
|
|
if is_adj {
|
|
let region_y0 = y0;
|
|
let region_y1 = y1.min(canvas_height);
|
|
let region_x0 = x0;
|
|
let region_x1 = x1.min(canvas_width);
|
|
|
|
if use_sequential {
|
|
for y in region_y0..region_y1 {
|
|
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
|
let gy = y;
|
|
for x in region_x0..region_x1 {
|
|
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
|
|
}
|
|
}
|
|
} else {
|
|
output
|
|
.par_chunks_exact_mut(row_stride)
|
|
.enumerate()
|
|
.for_each(|(y, row)| {
|
|
let gy = y as u32;
|
|
if gy < region_y0 || gy >= region_y1 {
|
|
return;
|
|
}
|
|
for x in region_x0..region_x1 {
|
|
blend_adjustment_pixel(row, x, gy, layer, blend, opacity);
|
|
}
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// If layer has effects or non-default fill_opacity, compute effects buffer.
|
|
// Prefer the cached `effects_cache` when available to avoid re-running the
|
|
// expensive effects pipeline on every partial composite.
|
|
let has_effects =
|
|
!layer.effects.is_empty() || !layer.styles.is_empty() || (layer.fill_opacity < 1.0);
|
|
let mut fx_effects: Vec<hcie_fx::LayerEffect> = layer
|
|
.effects
|
|
.iter()
|
|
.map(hcie_fx::protocol_to_hcie_fx_effect)
|
|
.collect();
|
|
fx_effects.extend(
|
|
layer
|
|
.styles
|
|
.iter()
|
|
.filter_map(|s| hcie_fx::layer_style_to_effect(s)),
|
|
);
|
|
let effect_buf = if has_effects {
|
|
if let Ok(Some(cached)) = layer.effects_cache.lock().as_deref() {
|
|
if cached.width == layer.width && cached.height == layer.height {
|
|
Some(cached.rendered.clone())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
.unwrap_or_else(|| {
|
|
apply_layer_effects(
|
|
&layer.pixels,
|
|
layer.width,
|
|
layer.height,
|
|
&fx_effects,
|
|
layer.fill_opacity,
|
|
)
|
|
})
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
// Try tile path first (only if no effects — effects need full dense buffer)
|
|
if !has_effects {
|
|
if let Some(Some(tl)) = tile_layers
|
|
.get(i)
|
|
.filter(|_| layer.layer_type != hcie_protocol::LayerType::Text)
|
|
{
|
|
if tl.tile_count() == 0 && i > 0 {
|
|
continue; // fully transparent layer with no tiles
|
|
}
|
|
let t_start_x = x0 / TILE_SIZE;
|
|
let _t_start_y = y0 / TILE_SIZE;
|
|
let t_end_x = div_ceil(x1, TILE_SIZE);
|
|
let _t_end_y = div_ceil(y1, TILE_SIZE);
|
|
|
|
if use_sequential {
|
|
for y in y0..y1.min(layer_h) {
|
|
let row =
|
|
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
|
let gy = y;
|
|
let ty = gy / TILE_SIZE;
|
|
let ly = (gy % TILE_SIZE) as usize;
|
|
for tx in t_start_x..t_end_x {
|
|
let gx_start = tx * TILE_SIZE;
|
|
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
|
|
if gx_end <= gx_start {
|
|
continue;
|
|
}
|
|
if let Some(tile) = tl.get_tile(tx, ty) {
|
|
let start_x = gx_start.max(x0);
|
|
for gx in start_x..gx_end {
|
|
blend_tile_pixel(
|
|
row,
|
|
gx,
|
|
gy,
|
|
ly,
|
|
tile,
|
|
layer,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
output
|
|
.par_chunks_exact_mut(row_stride)
|
|
.enumerate()
|
|
.for_each(|(y, row)| {
|
|
let gy = y as u32;
|
|
if gy < y0 || gy >= y1 || gy >= layer_h {
|
|
return;
|
|
}
|
|
let ty = gy / TILE_SIZE;
|
|
let ly = (gy % TILE_SIZE) as usize;
|
|
for tx in t_start_x..t_end_x {
|
|
let gx_start = tx * TILE_SIZE;
|
|
let gx_end = (gx_start + TILE_SIZE).min(x1).min(layer_w);
|
|
if gx_end <= gx_start {
|
|
continue;
|
|
}
|
|
if let Some(tile) = tl.get_tile(tx, ty) {
|
|
let start_x = gx_start.max(x0);
|
|
for gx in start_x..gx_end {
|
|
blend_tile_pixel(
|
|
row,
|
|
gx,
|
|
gy,
|
|
ly,
|
|
tile,
|
|
layer,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Fallback to dense pixel path (no effects)
|
|
if cx0 >= cx1 || cy0 >= cy1 {
|
|
continue;
|
|
}
|
|
if use_sequential {
|
|
for y in cy0..cy1 {
|
|
let row =
|
|
&mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
|
let gy = y;
|
|
let ly = (gy as i32 - offset_y) as u32;
|
|
for gx in cx0..cx1 {
|
|
let lx = (gx as i32 - offset_x) as u32;
|
|
blend_dense_pixel_offset(
|
|
row,
|
|
gx,
|
|
gy,
|
|
lx,
|
|
ly,
|
|
layer,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
output
|
|
.par_chunks_exact_mut(row_stride)
|
|
.enumerate()
|
|
.for_each(|(y, row)| {
|
|
let gy = y as u32;
|
|
if gy < cy0 || gy >= cy1 {
|
|
return;
|
|
}
|
|
let ly = (gy as i32 - offset_y) as u32;
|
|
for gx in cx0..cx1 {
|
|
let lx = (gx as i32 - offset_x) as u32;
|
|
blend_dense_pixel_offset(
|
|
row,
|
|
gx,
|
|
gy,
|
|
lx,
|
|
ly,
|
|
layer,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Effects path — use pre-computed effects buffer
|
|
if cx0 >= cx1 || cy0 >= cy1 {
|
|
continue;
|
|
}
|
|
let eb = effect_buf.as_slice();
|
|
if use_sequential {
|
|
for y in cy0..cy1 {
|
|
let row = &mut output[y as usize * row_stride..(y as usize + 1) * row_stride];
|
|
let gy = y;
|
|
let ly = (gy as i32 - offset_y) as u32;
|
|
for gx in cx0..cx1 {
|
|
let lx = (gx as i32 - offset_x) as u32;
|
|
blend_effects_pixel_offset(
|
|
row,
|
|
gx,
|
|
gy,
|
|
lx,
|
|
ly,
|
|
layer,
|
|
eb,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
output
|
|
.par_chunks_exact_mut(row_stride)
|
|
.enumerate()
|
|
.for_each(|(y, row)| {
|
|
let gy = y as u32;
|
|
if gy < cy0 || gy >= cy1 {
|
|
return;
|
|
}
|
|
let ly = (gy as i32 - offset_y) as u32;
|
|
for gx in cx0..cx1 {
|
|
let lx = (gx as i32 - offset_x) as u32;
|
|
blend_effects_pixel_offset(
|
|
row,
|
|
gx,
|
|
gy,
|
|
lx,
|
|
ly,
|
|
layer,
|
|
eb,
|
|
is_clipping,
|
|
base_idx,
|
|
layers,
|
|
blend,
|
|
opacity,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Applies one adjustment-layer pixel to the current composite row.
|
|
///
|
|
/// **Purpose:** Evaluates curves, gradient-map, or hue/saturation data while
|
|
/// respecting masks, opacity, and blend mode. **Logic & Workflow:** Transparent
|
|
/// or masked-out destinations exit early; Normal blend bypasses the generic
|
|
/// blend dispatcher, and fully opaque adjustment pixels are assigned directly.
|
|
/// **Arguments:** `row` is the destination row, `x`/`gy` are canvas coordinates,
|
|
/// `layer` owns adjustment and mask data, and `blend`/`opacity` control mixing.
|
|
/// **Returns:** Nothing. **Side Effects / Dependencies:** Mutates only the RGB
|
|
/// channels of one destination pixel and uses `hcie-blend` for non-Normal modes.
|
|
#[inline]
|
|
fn blend_adjustment_pixel(
|
|
row: &mut [u8],
|
|
x: u32,
|
|
gy: u32,
|
|
layer: &Layer,
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
let out_idx = (x as usize) * 4;
|
|
let dst = [
|
|
row[out_idx],
|
|
row[out_idx + 1],
|
|
row[out_idx + 2],
|
|
row[out_idx + 3],
|
|
];
|
|
if dst[3] == 0 {
|
|
return;
|
|
}
|
|
|
|
let mask_val = layer.get_mask_value(x, gy);
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
let adj_rgb = match layer.adjustment.as_ref().unwrap() {
|
|
hcie_blend::Adjustment::Curves {
|
|
lut_r,
|
|
lut_g,
|
|
lut_b,
|
|
} => crate::parallel::apply_curves(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
|
|
hcie_blend::Adjustment::GradientMap {
|
|
lut_r,
|
|
lut_g,
|
|
lut_b,
|
|
} => crate::parallel::apply_gradient_map(dst[0], dst[1], dst[2], lut_r, lut_g, lut_b),
|
|
hcie_blend::Adjustment::HueSaturation {
|
|
hue,
|
|
saturation,
|
|
lightness,
|
|
} => crate::parallel::apply_hue_saturation(
|
|
dst[0],
|
|
dst[1],
|
|
dst[2],
|
|
*hue,
|
|
*saturation,
|
|
*lightness,
|
|
),
|
|
};
|
|
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
if eff_opacity <= 0.0 {
|
|
return;
|
|
}
|
|
|
|
let blended_rgb = if blend == hcie_blend::BlendMode::Normal {
|
|
adj_rgb
|
|
} else {
|
|
let blended = blend_pixels(
|
|
[dst[0], dst[1], dst[2], 255],
|
|
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
|
|
blend,
|
|
1.0,
|
|
);
|
|
[blended[0], blended[1], blended[2]]
|
|
};
|
|
|
|
if eff_opacity >= 1.0 {
|
|
row[out_idx] = blended_rgb[0];
|
|
row[out_idx + 1] = blended_rgb[1];
|
|
row[out_idx + 2] = blended_rgb[2];
|
|
return;
|
|
}
|
|
|
|
let inverse_opacity = 1.0 - eff_opacity;
|
|
row[out_idx] =
|
|
(inverse_opacity * dst[0] as f32 + eff_opacity * blended_rgb[0] as f32).round() as u8;
|
|
row[out_idx + 1] =
|
|
(inverse_opacity * dst[1] as f32 + eff_opacity * blended_rgb[1] as f32).round() as u8;
|
|
row[out_idx + 2] =
|
|
(inverse_opacity * dst[2] as f32 + eff_opacity * blended_rgb[2] as f32).round() as u8;
|
|
}
|
|
|
|
#[inline]
|
|
fn blend_tile_pixel(
|
|
row: &mut [u8],
|
|
gx: u32,
|
|
gy: u32,
|
|
ly: usize,
|
|
tile: &hcie_tile::Tile,
|
|
layer: &Layer,
|
|
is_clipping: bool,
|
|
base_idx: Option<usize>,
|
|
layers: &[Layer],
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
let lx = (gx % TILE_SIZE) as usize;
|
|
let src_idx = (ly * TILE_SIZE as usize + lx) * 4;
|
|
let mut src = [
|
|
tile.pixels[src_idx],
|
|
tile.pixels[src_idx + 1],
|
|
tile.pixels[src_idx + 2],
|
|
tile.pixels[src_idx + 3],
|
|
];
|
|
|
|
// Inline get_mask_value — hot path
|
|
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
|
let (m_top, m_left, m_bottom, m_right) =
|
|
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
|
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
|
let mask_w = m_right - m_left;
|
|
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
|
mask.get(mask_idx)
|
|
.copied()
|
|
.unwrap_or(layer.mask_default_color)
|
|
} else {
|
|
layer.mask_default_color
|
|
}
|
|
} else {
|
|
255u8
|
|
};
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
if is_clipping {
|
|
if let Some(bi) = base_idx {
|
|
// Inline get_pixel for clipping base layer
|
|
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
|
let base = [
|
|
layers[bi].pixels[base_i],
|
|
layers[bi].pixels[base_i + 1],
|
|
layers[bi].pixels[base_i + 2],
|
|
layers[bi].pixels[base_i + 3],
|
|
];
|
|
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
|
} else {
|
|
src[3] = 0;
|
|
}
|
|
}
|
|
|
|
if src[3] == 0 {
|
|
return;
|
|
}
|
|
let dst_idx = (gx as usize) * 4;
|
|
let dst = [
|
|
row[dst_idx],
|
|
row[dst_idx + 1],
|
|
row[dst_idx + 2],
|
|
row[dst_idx + 3],
|
|
];
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
|
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
|
}
|
|
|
|
#[inline]
|
|
fn blend_dense_pixel(
|
|
row: &mut [u8],
|
|
gx: u32,
|
|
gy: u32,
|
|
layer: &Layer,
|
|
is_clipping: bool,
|
|
base_idx: Option<usize>,
|
|
layers: &[Layer],
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
// Inline get_pixel — hot path
|
|
let src_i = (gy * layer.width + gx) as usize * 4;
|
|
if src_i + 3 >= layer.pixels.len() {
|
|
return;
|
|
}
|
|
let mut src = [
|
|
layer.pixels[src_i],
|
|
layer.pixels[src_i + 1],
|
|
layer.pixels[src_i + 2],
|
|
layer.pixels[src_i + 3],
|
|
];
|
|
|
|
// Inline get_mask_value — hot path
|
|
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
|
let (m_top, m_left, m_bottom, m_right) =
|
|
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
|
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
|
let mask_w = m_right - m_left;
|
|
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
|
mask.get(mask_idx)
|
|
.copied()
|
|
.unwrap_or(layer.mask_default_color)
|
|
} else {
|
|
layer.mask_default_color
|
|
}
|
|
} else {
|
|
255u8
|
|
};
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
if is_clipping {
|
|
if let Some(bi) = base_idx {
|
|
// Inline get_pixel for clipping base layer
|
|
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
|
let base = [
|
|
layers[bi].pixels[base_i],
|
|
layers[bi].pixels[base_i + 1],
|
|
layers[bi].pixels[base_i + 2],
|
|
layers[bi].pixels[base_i + 3],
|
|
];
|
|
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
|
} else {
|
|
src[3] = 0;
|
|
}
|
|
}
|
|
|
|
if src[3] == 0 {
|
|
return;
|
|
}
|
|
let dst_idx = (gx as usize) * 4;
|
|
let dst = [
|
|
row[dst_idx],
|
|
row[dst_idx + 1],
|
|
row[dst_idx + 2],
|
|
row[dst_idx + 3],
|
|
];
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
|
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
|
}
|
|
|
|
#[inline]
|
|
fn blend_effects_pixel(
|
|
row: &mut [u8],
|
|
gx: u32,
|
|
gy: u32,
|
|
layer: &Layer,
|
|
eb: &[u8],
|
|
is_clipping: bool,
|
|
base_idx: Option<usize>,
|
|
layers: &[Layer],
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
let src_idx = (gy as usize * layer.width as usize + gx as usize) * 4;
|
|
let mut src = [
|
|
eb[src_idx],
|
|
eb[src_idx + 1],
|
|
eb[src_idx + 2],
|
|
eb[src_idx + 3],
|
|
];
|
|
|
|
// Inline get_mask_value — hot path
|
|
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
|
let (m_top, m_left, m_bottom, m_right) =
|
|
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
|
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
|
let mask_w = m_right - m_left;
|
|
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
|
mask.get(mask_idx)
|
|
.copied()
|
|
.unwrap_or(layer.mask_default_color)
|
|
} else {
|
|
layer.mask_default_color
|
|
}
|
|
} else {
|
|
255u8
|
|
};
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
if is_clipping {
|
|
if let Some(bi) = base_idx {
|
|
// Inline get_pixel for clipping base layer
|
|
let base_i = (gy * layers[bi].width + gx) as usize * 4;
|
|
let base = [
|
|
layers[bi].pixels[base_i],
|
|
layers[bi].pixels[base_i + 1],
|
|
layers[bi].pixels[base_i + 2],
|
|
layers[bi].pixels[base_i + 3],
|
|
];
|
|
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
|
} else {
|
|
src[3] = 0;
|
|
}
|
|
}
|
|
|
|
if src[3] == 0 {
|
|
return;
|
|
}
|
|
let dst_idx = (gx as usize) * 4;
|
|
let dst = [
|
|
row[dst_idx],
|
|
row[dst_idx + 1],
|
|
row[dst_idx + 2],
|
|
row[dst_idx + 3],
|
|
];
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
|
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
|
}
|
|
|
|
#[inline]
|
|
fn blend_dense_pixel_offset(
|
|
row: &mut [u8],
|
|
gx: u32,
|
|
gy: u32,
|
|
lx: u32,
|
|
ly: u32,
|
|
layer: &Layer,
|
|
is_clipping: bool,
|
|
base_idx: Option<usize>,
|
|
layers: &[Layer],
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
let src_i = (ly * layer.width + lx) as usize * 4;
|
|
if src_i + 3 >= layer.pixels.len() {
|
|
return;
|
|
}
|
|
let mut src = [
|
|
layer.pixels[src_i],
|
|
layer.pixels[src_i + 1],
|
|
layer.pixels[src_i + 2],
|
|
layer.pixels[src_i + 3],
|
|
];
|
|
|
|
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
|
let (m_top, m_left, m_bottom, m_right) =
|
|
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
|
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
|
let mask_w = m_right - m_left;
|
|
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
|
mask.get(mask_idx)
|
|
.copied()
|
|
.unwrap_or(layer.mask_default_color)
|
|
} else {
|
|
layer.mask_default_color
|
|
}
|
|
} else {
|
|
255u8
|
|
};
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
if is_clipping {
|
|
if let Some(bi) = base_idx {
|
|
let base = layers[bi].get_pixel(gx, gy);
|
|
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
|
} else {
|
|
src[3] = 0;
|
|
}
|
|
}
|
|
|
|
if src[3] == 0 {
|
|
return;
|
|
}
|
|
let dst_idx = (gx as usize) * 4;
|
|
let dst = [
|
|
row[dst_idx],
|
|
row[dst_idx + 1],
|
|
row[dst_idx + 2],
|
|
row[dst_idx + 3],
|
|
];
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
|
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
|
}
|
|
|
|
#[inline]
|
|
fn blend_effects_pixel_offset(
|
|
row: &mut [u8],
|
|
gx: u32,
|
|
gy: u32,
|
|
lx: u32,
|
|
ly: u32,
|
|
layer: &Layer,
|
|
eb: &[u8],
|
|
is_clipping: bool,
|
|
base_idx: Option<usize>,
|
|
layers: &[Layer],
|
|
blend: hcie_blend::BlendMode,
|
|
opacity: f32,
|
|
) {
|
|
let src_idx = (ly as usize * layer.width as usize + lx as usize) * 4;
|
|
let mut src = [
|
|
eb[src_idx],
|
|
eb[src_idx + 1],
|
|
eb[src_idx + 2],
|
|
eb[src_idx + 3],
|
|
];
|
|
|
|
let mask_val = if let (Some(mask), Some(mb)) = (&layer.mask_pixels, &layer.mask_bounds) {
|
|
let (m_top, m_left, m_bottom, m_right) =
|
|
(mb[0] as u32, mb[1] as u32, mb[2] as u32, mb[3] as u32);
|
|
if gy >= m_top && gy < m_bottom && gx >= m_left && gx < m_right {
|
|
let mask_w = m_right - m_left;
|
|
let mask_idx = ((gy - m_top) * mask_w + (gx - m_left)) as usize;
|
|
mask.get(mask_idx)
|
|
.copied()
|
|
.unwrap_or(layer.mask_default_color)
|
|
} else {
|
|
layer.mask_default_color
|
|
}
|
|
} else {
|
|
255u8
|
|
};
|
|
if mask_val == 0 {
|
|
return;
|
|
}
|
|
|
|
if is_clipping {
|
|
if let Some(bi) = base_idx {
|
|
let base = layers[bi].get_pixel(gx, gy);
|
|
src[3] = (src[3] as f32 * base[3] as f32 / 255.0 + 0.5) as u8;
|
|
} else {
|
|
src[3] = 0;
|
|
}
|
|
}
|
|
|
|
if src[3] == 0 {
|
|
return;
|
|
}
|
|
let dst_idx = (gx as usize) * 4;
|
|
let dst = [
|
|
row[dst_idx],
|
|
row[dst_idx + 1],
|
|
row[dst_idx + 2],
|
|
row[dst_idx + 3],
|
|
];
|
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
|
let blended = blend_pixels(dst, src, blend, eff_opacity);
|
|
row[dst_idx..dst_idx + 4].copy_from_slice(&blended);
|
|
}
|
|
|
|
#[inline]
|
|
fn div_ceil(a: u32, b: u32) -> u32 {
|
|
(a + b - 1) / b
|
|
}
|