mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 23:28:11 +08:00
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:
co-authored by
Keavon Chambers
Dennis Kobert
parent
63e3b5c604
commit
8f9168bfe5
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,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 },
|
||||
|
||||
Reference in New Issue
Block a user