Blend modes (#252)

* Add backend for selecting layer blend mode

* Change dropdown input to support callback on change

* Add debug messages

* Fix canvas update for blend-modes

* Finish up and polish blend modes implementations

* Add changes from code review

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
George Atkinson
2021-07-23 10:21:07 -07:00
committed by GitHub
co-authored by Keavon Chambers Dennis Kobert
parent 63e3b5c604
commit 8f9168bfe5
18 changed files with 348 additions and 69 deletions
+13 -3
View File
@@ -221,7 +221,9 @@ impl Document {
/// Deletes the layer specified by `path`.
pub fn delete(&mut self, path: &[LayerId]) -> Result<(), DocumentError> {
let (path, id) = split_path(path)?;
let _ = self.layer_mut(path).map(|x| x.cache_dirty = true);
if let Ok(layer) = self.layer_mut(path) {
layer.cache_dirty = true;
}
self.document_folder_mut(path)?.as_folder_mut()?.remove_layer(id)?;
Ok(())
}
@@ -393,13 +395,21 @@ impl Document {
Some(responses)
}
Operation::ToggleVisibility { path } => {
let _ = self.layer_mut(&path).map(|layer| {
if let Ok(layer) = self.layer_mut(&path) {
layer.visible = !layer.visible;
layer.cache_dirty = true;
});
}
let path = path.as_slice()[..path.len() - 1].to_vec();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
}
Operation::SetLayerBlendMode { path, blend_mode } => {
self.mark_as_dirty(path)?;
self.layer_mut(&path).unwrap().blend_mode = *blend_mode;
let path = path.as_slice()[..path.len() - 1].to_vec();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
}
Operation::FillLayer { path, color } => {
let layer = self.layer_mut(path).unwrap();
layer.style.set_fill(layers::style::Fill::new(*color));
+61 -2
View File
@@ -24,12 +24,15 @@ use crate::LayerId;
pub use folder::Folder;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
pub trait LayerData {
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle);
fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle);
}
// TODO: Rename this `LayerDataType` to not be plural in a separate commit (together with `enum ToolOptions`)
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum LayerDataTypes {
Folder(Folder),
@@ -40,6 +43,48 @@ pub enum LayerDataTypes {
Shape(Shape),
}
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
pub enum BlendMode {
Normal,
Multiply,
Darken,
ColorBurn,
Screen,
Lighten,
ColorDodge,
Overlay,
SoftLight,
HardLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
}
impl BlendMode {
fn to_svg_style_name(&self) -> &str {
match self {
BlendMode::Normal => "normal",
BlendMode::Multiply => "multiply",
BlendMode::Darken => "darken",
BlendMode::ColorBurn => "color-burn",
BlendMode::Screen => "screen",
BlendMode::Lighten => "lighten",
BlendMode::ColorDodge => "color-dodge",
BlendMode::Overlay => "overlay",
BlendMode::SoftLight => "soft-light",
BlendMode::HardLight => "hard-light",
BlendMode::Difference => "difference",
BlendMode::Exclusion => "exclusion",
BlendMode::Hue => "hue",
BlendMode::Saturation => "saturation",
BlendMode::Color => "color",
BlendMode::Luminosity => "luminosity",
}
}
}
macro_rules! call_render {
($self:ident.render($svg:ident, $transform:ident, $style:ident) { $($variant:ident),* }) => {
match $self {
@@ -56,7 +101,7 @@ macro_rules! call_kurbo_path {
}
macro_rules! call_intersects_quad {
($self:ident.intersects_quad($quad:ident, $path:ident, $intersections:ident, $style:ident) { $($variant:ident),* }) => {
($self:ident.intersects_quad($quad:ident, $path:ident, $intersections:ident, $style:ident) { $($variant:ident),* }) => {
match $self {
$(Self::$variant(x) => x.intersects_quad($quad, $path, $intersections, $style)),*
}
@@ -76,6 +121,7 @@ impl LayerDataTypes {
}
}
}
pub fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath {
call_kurbo_path! {
self.to_kurbo_path(transform, style) {
@@ -125,7 +171,9 @@ pub struct Layer {
pub transform: glam::DAffine2,
pub style: style::PathStyle,
pub cache: String,
pub thumbnail_cache: String,
pub cache_dirty: bool,
pub blend_mode: BlendMode,
}
impl Layer {
@@ -137,7 +185,9 @@ impl Layer {
transform: glam::DAffine2::from_cols_array(&transform),
style,
cache: String::new(),
thumbnail_cache: String::new(),
cache_dirty: true,
blend_mode: BlendMode::Normal,
}
}
@@ -146,8 +196,17 @@ impl Layer {
return "";
}
if self.cache_dirty {
self.thumbnail_cache.clear();
self.data.render(&mut self.thumbnail_cache, self.transform, self.style);
self.cache.clear();
self.data.render(&mut self.cache, self.transform, self.style);
let _ = write!(
self.cache,
r#"<g style="mix-blend-mode: {}">{}</g>"#,
self.blend_mode.to_svg_style_name(),
self.thumbnail_cache.as_str()
);
self.cache_dirty = false;
}
self.cache.as_str()
+3 -3
View File
@@ -1,10 +1,10 @@
use crate::color::Color;
use serde::{Deserialize, Serialize};
const OPACITY_PERCISION: usize = 3;
const OPACITY_PRECISION: usize = 3;
fn format_opacity(name: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PERCISION as i32)) {
format!(r#" {}-opacity="{:.percision$}""#, name, opacity, percision = OPACITY_PERCISION)
if (opacity - 1.).abs() > 10f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {}-opacity="{:.precision$}""#, name, opacity, precision = OPACITY_PRECISION)
} else {
String::new()
}
+5 -1
View File
@@ -1,6 +1,6 @@
use crate::{
color::Color,
layers::{style, Layer},
layers::{style, BlendMode, Layer},
LayerId,
};
@@ -72,6 +72,10 @@ pub enum Operation {
ToggleVisibility {
path: Vec<LayerId>,
},
SetLayerBlendMode {
path: Vec<LayerId>,
blend_mode: BlendMode,
},
FillLayer {
path: Vec<LayerId>,
color: Color,
+17 -8
View File
@@ -38,11 +38,17 @@ fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[
layer_data.get_mut(path).unwrap()
}
pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &mut Layer, path: Vec<LayerId>) -> LayerPanelEntry {
let blend_mode = layer.blend_mode.clone();
let layer_type: LayerType = (&layer.data).into();
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
let arr = layer.current_bounding_box().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
if layer.cache_dirty {
layer.render();
}
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}</svg>"#,
@@ -50,14 +56,16 @@ pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &Layer, path: Vec<La
y_min,
x_max - x_min,
y_max - y_min,
layer.cache.clone()
layer.thumbnail_cache.clone()
)
} else {
String::new()
};
LayerPanelEntry {
name,
visible: layer.visible,
blend_mode,
layer_type,
layer_data: *layer_data,
path,
@@ -73,16 +81,17 @@ impl Document {
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
/// any actual data, but rather metadata such as visibility and names of the layers.
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
let folder = self.document.document_folder(path)?;
let folder = self.document.document_folder_mut(path)?;
let ids = folder.as_folder()?.layer_ids.clone();
let self_layer_data = &mut self.layer_data;
let entries = folder
.as_folder()?
.layers()
.iter()
.zip(folder.as_folder()?.layer_ids.iter())
.as_folder_mut()?
.layers_mut()
.iter_mut()
.zip(ids)
.rev()
.map(|(layer, id)| {
let path = [path, &[*id]].concat();
let path = [path, &[id]].concat();
layer_panel_entry(layer_data(self_layer_data, &path), layer, path)
})
.collect();
@@ -3,6 +3,7 @@ use crate::{
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
input::{mouse::ViewportPosition, InputPreprocessor},
};
use document_core::layers::BlendMode;
use document_core::layers::Layer;
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
use glam::{DAffine2, DVec2};
@@ -24,6 +25,7 @@ pub enum DocumentMessage {
DeleteSelectedLayers,
DuplicateSelectedLayers,
CopySelectedLayers,
SetBlendModeForSelectedLayers(BlendMode),
PasteLayers,
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
@@ -342,6 +344,13 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
}
.into(),
),
SetBlendModeForSelectedLayers(blend_mode) => {
let active_document = self.active_document();
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path)) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path: path.clone(), blend_mode }.into());
}
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
}
+5 -1
View File
@@ -1,5 +1,8 @@
use crate::document::LayerData;
use document_core::{layers::LayerDataTypes, LayerId};
use document_core::{
layers::{BlendMode, LayerDataTypes},
LayerId,
};
use serde::{Deserialize, Serialize};
use std::fmt;
@@ -7,6 +10,7 @@ use std::fmt;
pub struct LayerPanelEntry {
pub name: String,
pub visible: bool,
pub blend_mode: BlendMode,
pub layer_type: LayerType,
pub layer_data: LayerData,
pub path: Vec<LayerId>,
+1
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
// TODO: Rename this `ToolOption` to not be plural in a separate commit (together with `enum LayerDataTypes`)
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum ToolOptions {
Select { append_mode: SelectAppendMode },