feat(canvas): enhance composite shader with selection overlay and marching ants

- Updated the canvas composite shader to include a selection mask fill and animated marching ants border.
- Added new uniforms for animation time, selection mask presence, and quick mask mode.
- Modified the fragment shader to handle selection rendering, including color tinting for selected areas and a dash pattern for the marching ants effect.
- Refactored the OverlayProgram to remove the old marching ants rendering logic, now handled by the GPU shader.
- Introduced new data structures in the shader pipeline for managing selection mask textures and samplers.
- Implemented functionality to upload selection mask data to the GPU.
- Updated selection state handling to ensure correct bounds calculations.
- Enhanced sidebar tool button rendering with fallback for missing icons.
This commit is contained in:
2026-07-17 23:15:27 +03:00
parent b09795ccff
commit b2c88e5471
8 changed files with 634 additions and 209 deletions
+55
View File
@@ -751,6 +751,9 @@ impl Engine {
// ── Filter Ops ───────────────────────────────────────────────────────
pub fn apply_filter(&mut self, filter_id: &str, params: serde_json::Value) {
// Clone the selection mask before borrowing the layer mutably, to avoid
// borrow-checker conflict between `active_layer_mut()` and `selection_mask`.
let mask = self.document.selection_mask.clone();
if let Some(layer) = self.document.active_layer_mut() {
let before_pixels = layer.pixels.clone();
let _ = apply_filter(
@@ -760,6 +763,30 @@ impl Engine {
layer.width,
layer.height,
);
// When a selection is active, restore original pixels for unselected
// areas so the filter is confined to the selected region.
if let Some(ref mask) = mask {
let w = layer.width as usize;
let h = layer.height as usize;
if mask.len() == w * h {
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
if mask[idx] == 0 {
let pidx = idx * 4;
if pidx + 3 < layer.pixels.len()
&& pidx + 3 < before_pixels.len()
{
layer.pixels[pidx] = before_pixels[pidx];
layer.pixels[pidx + 1] = before_pixels[pidx + 1];
layer.pixels[pidx + 2] = before_pixels[pidx + 2];
layer.pixels[pidx + 3] = before_pixels[pidx + 3];
}
}
}
}
}
}
layer.dirty = true;
self.document.push_draw_snapshot(
self.document.active_layer,
@@ -773,7 +800,11 @@ impl Engine {
}
pub fn apply_filter_preview(&mut self, filter_id: &str, params: serde_json::Value) {
let mask = self.document.selection_mask.clone();
if let Some(layer) = self.document.active_layer_mut() {
// Save the current pixels before applying the filter, so we can
// restore unselected areas afterwards.
let saved_before = layer.pixels.clone();
let _ = apply_filter(
filter_id,
&params,
@@ -781,6 +812,30 @@ impl Engine {
layer.width,
layer.height,
);
// When a selection is active, restore original pixels for unselected
// areas so the filter preview is confined to the selected region.
if let Some(ref mask) = mask {
let w = layer.width as usize;
let h = layer.height as usize;
if mask.len() == w * h {
for y in 0..h {
for x in 0..w {
let idx = y * w + x;
if mask[idx] == 0 {
let pidx = idx * 4;
if pidx + 3 < layer.pixels.len()
&& pidx + 3 < saved_before.len()
{
layer.pixels[pidx] = saved_before[pidx];
layer.pixels[pidx + 1] = saved_before[pidx + 1];
layer.pixels[pidx + 2] = saved_before[pidx + 2];
layer.pixels[pidx + 3] = saved_before[pidx + 3];
}
}
}
}
}
}
layer.dirty = true;
self.document.composite_dirty = true;
}