Add transparent bg and make raster and vector tools use same colors

This commit is contained in:
Skyler Lehmkuhl 2026-03-01 23:38:20 -05:00
parent 1c3f794958
commit 6162adfa9f
6 changed files with 91 additions and 69 deletions

View File

@ -438,10 +438,38 @@ pub fn render_document_with_transform(
/// Draw the document background
fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) {
let background_rect = Rect::new(0.0, 0.0, document.width, document.height);
let bg = &document.background_color;
// Convert our ShapeColor to vello's peniko Color
let background_color = document.background_color.to_peniko();
// Draw checkerboard behind transparent backgrounds
if bg.a < 255 {
use vello::peniko::{Blob, Color, Extend, ImageAlphaType, ImageData, ImageQuality};
// 2x2 pixel checkerboard pattern: light/dark alternating
let light: [u8; 4] = [204, 204, 204, 255];
let dark: [u8; 4] = [170, 170, 170, 255];
let pixels: Vec<u8> = [light, dark, dark, light].concat();
let image_data = ImageData {
data: Blob::from(pixels),
format: ImageFormat::Rgba8,
width: 2,
height: 2,
alpha_type: ImageAlphaType::AlphaPremultiplied,
};
let brush = ImageBrush::new(image_data)
.with_extend(Extend::Repeat)
.with_quality(ImageQuality::Low);
// Scale each pixel to 16x16 document units
let brush_transform = Affine::scale(16.0);
scene.fill(
Fill::NonZero,
base_transform,
&brush,
Some(brush_transform),
&background_rect,
);
}
// Draw the background color on top (alpha-blended)
let background_color = bg.to_peniko();
scene.fill(
Fill::NonZero,
base_transform,

View File

@ -761,6 +761,7 @@ struct EditorApp {
brush_opacity: f32, // brush opacity 0.01.0
brush_hardness: f32, // brush hardness 0.01.0
brush_spacing: f32, // dabs_per_radius (fraction of radius per dab)
brush_use_fg: bool, // true = paint with FG (stroke) color, false = BG (fill) color
// Audio engine integration
#[allow(dead_code)] // Must be kept alive to maintain audio output
audio_stream: Option<cpal::Stream>,
@ -1039,6 +1040,7 @@ impl EditorApp {
brush_opacity: 1.0,
brush_hardness: 0.5,
brush_spacing: 0.1,
brush_use_fg: true,
audio_stream,
audio_controller,
audio_event_rx,
@ -5158,6 +5160,7 @@ impl eframe::App for EditorApp {
brush_opacity: &mut self.brush_opacity,
brush_hardness: &mut self.brush_hardness,
brush_spacing: &mut self.brush_spacing,
brush_use_fg: &mut self.brush_use_fg,
audio_controller: self.audio_controller.as_ref(),
video_manager: &self.video_manager,
playback_time: &mut self.playback_time,

View File

@ -302,6 +302,14 @@ impl InfopanelPane {
// Raster paint tools
Tool::Draw | Tool::Erase | Tool::Smudge if is_raster_paint_tool => {
// Color source toggle (Draw tool only)
if matches!(tool, Tool::Draw) {
ui.horizontal(|ui| {
ui.label("Color:");
ui.selectable_value(shared.brush_use_fg, true, "FG");
ui.selectable_value(shared.brush_use_fg, false, "BG");
});
}
ui.horizontal(|ui| {
ui.label("Size:");
ui.add(
@ -374,20 +382,9 @@ impl InfopanelPane {
ui.label("Fill:");
match info.fill_color {
Some(Some(color)) => {
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
color.r, color.g, color.b, color.a,
);
if egui::color_picker::color_edit_button_srgba(
ui,
&mut egui_color,
egui::color_picker::Alpha::OnlyBlend,
).changed() {
let new_color = ShapeColor {
r: egui_color.r(),
g: egui_color.g(),
b: egui_color.b(),
a: egui_color.a(),
};
let mut rgba = [color.r, color.g, color.b, color.a];
if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() {
let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]);
let action = SetShapePropertiesAction::set_fill_color(
layer_id, time, face_ids.clone(), Some(new_color),
);
@ -408,20 +405,9 @@ impl InfopanelPane {
ui.label("Stroke:");
match info.stroke_color {
Some(Some(color)) => {
let mut egui_color = egui::Color32::from_rgba_unmultiplied(
color.r, color.g, color.b, color.a,
);
if egui::color_picker::color_edit_button_srgba(
ui,
&mut egui_color,
egui::color_picker::Alpha::OnlyBlend,
).changed() {
let new_color = ShapeColor {
r: egui_color.r(),
g: egui_color.g(),
b: egui_color.b(),
a: egui_color.a(),
};
let mut rgba = [color.r, color.g, color.b, color.a];
if ui.color_edit_button_srgba_unmultiplied(&mut rgba).changed() {
let new_color = ShapeColor::rgba(rgba[0], rgba[1], rgba[2], rgba[3]);
let action = SetShapePropertiesAction::set_stroke_color(
layer_id, time, edge_ids.clone(), Some(new_color),
);
@ -538,14 +524,14 @@ impl InfopanelPane {
}
});
// Background color
// Background color (with alpha)
ui.horizontal(|ui| {
ui.label("Background:");
let bg = document.background_color;
let mut color = [bg.r, bg.g, bg.b];
if ui.color_edit_button_srgb(&mut color).changed() {
let mut color = [bg.r, bg.g, bg.b, bg.a];
if ui.color_edit_button_srgba_unmultiplied(&mut color).changed() {
let action = SetDocumentPropertiesAction::set_background_color(
ShapeColor::rgb(color[0], color[1], color[2]),
ShapeColor::rgba(color[0], color[1], color[2], color[3]),
);
shared.pending_actions.push(Box::new(action));
}

View File

@ -192,6 +192,8 @@ pub struct SharedPaneState<'a> {
pub brush_opacity: &'a mut f32,
pub brush_hardness: &'a mut f32,
pub brush_spacing: &'a mut f32,
/// Whether the brush paints with the foreground (fill) color (true) or background (stroke) color (false)
pub brush_use_fg: &'a mut bool,
/// Audio engine controller for playback control (wrapped in Arc<Mutex<>> for thread safety)
pub audio_controller: Option<&'a std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
/// Video manager for video decoding and frame caching

View File

@ -4420,7 +4420,7 @@ impl StagePane {
let color = if matches!(blend_mode, lightningbeam_core::raster_layer::RasterBlendMode::Erase) {
[1.0f32, 1.0, 1.0, 1.0]
} else {
let c = *shared.stroke_color;
let c = if *shared.brush_use_fg { *shared.stroke_color } else { *shared.fill_color };
[c.r() as f32 / 255.0, c.g() as f32 / 255.0, c.b() as f32 / 255.0, c.a() as f32 / 255.0]
};

View File

@ -205,19 +205,49 @@ impl PaneRenderer for ToolbarPane {
let color_row_width = fill_label_width + color_button_size + button_spacing;
let color_x = rect.left() + (rect.width() - color_row_width) / 2.0;
// For raster layers show a single "Color" swatch (brush paint color = stroke_color).
// For vector layers show Fill + Stroke.
if !is_raster {
// Fill color label
// Two color swatches:
// Stroke/FG always on top, Fill/BG always on bottom.
// Raster layers label them "FG" / "BG"; vector layers label them "Stroke" / "Fill".
{
let stroke_label = if is_raster { "FG" } else { "Stroke" };
ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER,
"Fill",
stroke_label,
egui::FontId::proportional(14.0),
egui::Color32::from_gray(200),
);
let stroke_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size),
);
let stroke_button_id = ui.id().with(("stroke_color_button", path));
let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click());
draw_color_button(ui, stroke_button_rect, *shared.stroke_color);
egui::containers::Popup::from_toggle_button_response(&stroke_response)
.show(|ui| {
ui.spacing_mut().slider_width = 275.0;
let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend);
if changed {
*shared.active_color_mode = super::ColorMode::Stroke;
}
});
y += color_button_size + button_spacing;
}
// Fill/BG color swatch
{
let fill_label = if is_raster { "BG" } else { "Fill" };
ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER,
fill_label,
egui::FontId::proportional(14.0),
egui::Color32::from_gray(200),
);
// Fill color button
let fill_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size),
@ -227,40 +257,13 @@ impl PaneRenderer for ToolbarPane {
draw_color_button(ui, fill_button_rect, *shared.fill_color);
egui::containers::Popup::from_toggle_button_response(&fill_response)
.show(|ui| {
ui.spacing_mut().slider_width = 275.0;
let changed = egui::color_picker::color_picker_color32(ui, shared.fill_color, egui::color_picker::Alpha::OnlyBlend);
if changed {
*shared.active_color_mode = super::ColorMode::Fill;
}
});
y += color_button_size + button_spacing;
}
// Stroke/brush color label
let stroke_label = if is_raster { "Color" } else { "Stroke" };
ui.painter().text(
egui::pos2(color_x + fill_label_width / 2.0, y + color_button_size / 2.0),
egui::Align2::CENTER_CENTER,
stroke_label,
egui::FontId::proportional(14.0),
egui::Color32::from_gray(200),
);
// Stroke color button
let stroke_button_rect = egui::Rect::from_min_size(
egui::pos2(color_x + fill_label_width + button_spacing, y),
egui::vec2(color_button_size, color_button_size),
);
let stroke_button_id = ui.id().with(("stroke_color_button", path));
let stroke_response = ui.interact(stroke_button_rect, stroke_button_id, egui::Sense::click());
draw_color_button(ui, stroke_button_rect, *shared.stroke_color);
egui::containers::Popup::from_toggle_button_response(&stroke_response)
.show(|ui| {
let changed = egui::color_picker::color_picker_color32(ui, shared.stroke_color, egui::color_picker::Alpha::OnlyBlend);
if changed {
*shared.active_color_mode = super::ColorMode::Stroke;
}
});
} // end color pickers
}