Fix the blend mode and opacity widgets of the Layers panel (#1506)
* Fix blend mode and opacity * Cleanup and bug fixes --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
6bce72dccd
commit
29222700f4
|
|
@ -54,6 +54,10 @@ impl DocumentMetadata {
|
|||
self.all_layers().filter(|layer| self.selected_nodes.contains(&layer.to_node()))
|
||||
}
|
||||
|
||||
pub fn selected_layers_except_artboards(&self) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
self.selected_layers().filter(move |layer| !self.artboards.contains(layer))
|
||||
}
|
||||
|
||||
pub fn selected_layers_contains(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
self.selected_layers().any(|selected| selected == layer)
|
||||
}
|
||||
|
|
@ -259,7 +263,7 @@ impl DocumentMetadata {
|
|||
}
|
||||
|
||||
pub fn is_artboard(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
||||
network.upstream_flow_back_from_nodes(vec![layer.to_node()], true).any(|(node, _)| node.name == "Artboard")
|
||||
network.upstream_flow_back_from_nodes(vec![layer.to_node()], true).any(|(node, _)| node.is_artboard())
|
||||
}
|
||||
|
||||
pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
||||
|
|
@ -267,7 +271,7 @@ pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool {
|
|||
|| network
|
||||
.upstream_flow_back_from_nodes(vec![layer.to_node()], true)
|
||||
.skip(1)
|
||||
.any(|(node, _)| node.name == "Artboard" || node.is_layer())
|
||||
.any(|(node, _)| node.is_artboard() || node.is_layer())
|
||||
}
|
||||
|
||||
// click targets
|
||||
|
|
|
|||
|
|
@ -342,13 +342,7 @@ impl Layer {
|
|||
self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
|
||||
let _ = self.cache.write_str(&(f.to_string() + if i == 5 { "" } else { "," }));
|
||||
});
|
||||
let _ = write!(
|
||||
self.cache,
|
||||
r#")" style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
|
||||
self.blend_mode.to_svg_style_name(),
|
||||
self.opacity,
|
||||
self.thumbnail_cache.as_str()
|
||||
);
|
||||
let _ = write!(self.cache, r#")" style="opacity: {};{}">{}</g>"#, self.opacity, self.blend_mode.render(), self.thumbnail_cache.as_str());
|
||||
|
||||
self.cache_dirty = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog};
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_artboard;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
|
||||
/// Stores the dialogs which require state. These are the ones that have their own message handlers, and are not the ones defined in `simple_dialogs`.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
@ -78,7 +78,7 @@ impl MessageHandler<DialogMessage, DialogData<'_>> for DialogMessageHandler {
|
|||
.document_legacy
|
||||
.metadata
|
||||
.all_layers()
|
||||
.filter(|&layer| is_artboard(layer, &document.document_legacy))
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Artboard"))
|
||||
.map(|layer| {
|
||||
(
|
||||
layer,
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ pub enum FrontendMessage {
|
|||
#[serde(rename = "hintData")]
|
||||
hint_data: HintData,
|
||||
},
|
||||
UpdateLayerTreeOptionsLayout {
|
||||
UpdateLayersPanelOptionsLayout {
|
||||
#[serde(rename = "layoutTarget")]
|
||||
layout_target: LayoutTarget,
|
||||
diff: Vec<WidgetDiff>,
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ impl LayoutMessageHandler {
|
|||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff },
|
||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff },
|
||||
LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff },
|
||||
LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout { layout_target, diff },
|
||||
LayoutTarget::LayersPanelOptions => FrontendMessage::UpdateLayersPanelOptionsLayout { layout_target, diff },
|
||||
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
||||
LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { layout_target, diff },
|
||||
LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { layout_target, diff },
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ pub enum LayoutTarget {
|
|||
/// The button below the tool shelf and directly above the working colors which lets the user toggle the node graph overlaid on the canvas.
|
||||
GraphViewOverlayButton,
|
||||
/// Options for opacity seen at the top of the Layers panel.
|
||||
LayerTreeOptions,
|
||||
LayersPanelOptions,
|
||||
/// The dropdown menu at the very top of the application: File, Edit, etc.
|
||||
MenuBar,
|
||||
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
|
|||
use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_blend_mode, get_opacity};
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
responses.add(FolderChanged { affected_folder_path: vec![] });
|
||||
responses.add(BroadcastEvent::SelectionChanged);
|
||||
|
||||
self.update_layer_tree_options_bar_widgets(responses, &render_data);
|
||||
self.update_layers_panel_options_bar_widgets(responses);
|
||||
}
|
||||
AlignSelectedLayers { axis, aggregate } => {
|
||||
self.backup(responses);
|
||||
|
|
@ -268,7 +269,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
// Clear the options bar
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(Default::default()),
|
||||
layout_target: LayoutTarget::LayerTreeOptions,
|
||||
layout_target: LayoutTarget::LayersPanelOptions,
|
||||
});
|
||||
}
|
||||
CommitTransaction => (),
|
||||
|
|
@ -449,7 +450,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry });
|
||||
}
|
||||
responses.add(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path });
|
||||
self.update_layer_tree_options_bar_widgets(responses, &render_data);
|
||||
self.update_layers_panel_options_bar_widgets(responses);
|
||||
}
|
||||
MoveSelectedLayersTo { parent, insert_index } => {
|
||||
let selected_layers = self.metadata().selected_layers().collect::<Vec<_>>();
|
||||
|
|
@ -697,8 +698,8 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
}
|
||||
SetBlendModeForSelectedLayers { blend_mode } => {
|
||||
self.backup(responses);
|
||||
for path in self.selected_layers() {
|
||||
responses.add(DocumentOperation::SetLayerBlendMode { path: path.to_vec(), blend_mode });
|
||||
for layer in self.metadata().selected_layers_except_artboards() {
|
||||
responses.add(GraphOperationMessage::BlendModeSet { layer: layer.to_path(), blend_mode });
|
||||
}
|
||||
}
|
||||
SetImageBlobUrl {
|
||||
|
|
@ -737,10 +738,10 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
}
|
||||
SetOpacityForSelectedLayers { opacity } => {
|
||||
self.backup(responses);
|
||||
let opacity = opacity.clamp(0., 1.);
|
||||
let opacity = opacity.clamp(0., 1.) as f32;
|
||||
|
||||
for path in self.selected_layers().map(|path| path.to_vec()) {
|
||||
responses.add(DocumentOperation::SetLayerOpacity { path, opacity });
|
||||
for layer in self.metadata().selected_layers_except_artboards() {
|
||||
responses.add(GraphOperationMessage::OpacitySet { layer: layer.to_path(), opacity });
|
||||
}
|
||||
}
|
||||
SetOverlaysVisibility { visible } => {
|
||||
|
|
@ -1519,72 +1520,71 @@ impl DocumentMessageHandler {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn update_layer_tree_options_bar_widgets(&self, responses: &mut VecDeque<Message>, render_data: &RenderData) {
|
||||
let mut opacity = None;
|
||||
let mut opacity_is_mixed = false;
|
||||
pub fn update_layers_panel_options_bar_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||
// Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode).
|
||||
let selected_layers_except_artboards = self.metadata().selected_layers_except_artboards();
|
||||
|
||||
let mut blend_mode = None;
|
||||
let mut blend_mode_is_mixed = false;
|
||||
|
||||
self.layer_metadata
|
||||
.keys()
|
||||
.filter_map(|path| self.layer_panel_entry_from_path(path, render_data))
|
||||
.filter(|layer_panel_entry| layer_panel_entry.layer_metadata.selected)
|
||||
.flat_map(|layer_panel_entry| self.document_legacy.layer(layer_panel_entry.path.as_slice()))
|
||||
.for_each(|layer| {
|
||||
match opacity {
|
||||
None => opacity = Some(layer.opacity),
|
||||
Some(opacity) => {
|
||||
if (opacity - layer.opacity).abs() > (1. / 1_000_000.) {
|
||||
opacity_is_mixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match blend_mode {
|
||||
None => blend_mode = Some(layer.blend_mode),
|
||||
Some(blend_mode) => {
|
||||
if blend_mode != layer.blend_mode {
|
||||
blend_mode_is_mixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
|
||||
let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| {
|
||||
(
|
||||
get_opacity(layer, &self.document_legacy).unwrap_or(100.),
|
||||
get_blend_mode(layer, &self.document_legacy).unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
let first_opacity_and_blend_mode = opacity_and_blend_mode.next();
|
||||
let result_opacity_and_blend_mode = opacity_and_blend_mode;
|
||||
|
||||
if opacity_is_mixed {
|
||||
opacity = None;
|
||||
// If there are no selected layers, disable the opacity and blend mode widgets.
|
||||
let disabled = first_opacity_and_blend_mode.is_none();
|
||||
|
||||
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
|
||||
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
|
||||
// If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity.
|
||||
let (opacity, blend_mode) = first_opacity_and_blend_mode
|
||||
.map(|(first_opacity, first_blend_mode)| {
|
||||
let mut opacity_identical = true;
|
||||
let mut blend_mode_identical = true;
|
||||
|
||||
for (opacity, blend_mode) in result_opacity_and_blend_mode {
|
||||
if (opacity - first_opacity).abs() > (f32::EPSILON * 100.) {
|
||||
opacity_identical = false;
|
||||
}
|
||||
if blend_mode != first_blend_mode {
|
||||
blend_mode_identical = false;
|
||||
}
|
||||
if blend_mode_is_mixed {
|
||||
blend_mode = None;
|
||||
}
|
||||
|
||||
let blend_mode_menu_entries = BlendMode::list_modes_in_groups()
|
||||
(opacity_identical.then(|| first_opacity), blend_mode_identical.then(|| first_blend_mode))
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
|
||||
let blend_mode_menu_entries = BlendMode::list_svg_subset()
|
||||
.iter()
|
||||
.map(|modes| {
|
||||
modes
|
||||
.iter()
|
||||
.map(|mode| {
|
||||
MenuListEntry::new(mode.to_string())
|
||||
.value(mode.to_string())
|
||||
.on_update(|_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode: *mode }.into())
|
||||
.map(|&blend_mode| {
|
||||
MenuListEntry::new(blend_mode.to_string())
|
||||
.value(blend_mode.to_string())
|
||||
.on_update(move |_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode }.into())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let layer_tree_options = WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
let layers_panel_options_bar = WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
DropdownInput::new(blend_mode_menu_entries)
|
||||
.selected_index(blend_mode.map(|blend_mode| blend_mode as u32))
|
||||
.disabled(blend_mode.is_none() && !blend_mode_is_mixed)
|
||||
.selected_index(blend_mode.map(|blend_mode| blend_mode.index_in_list_svg_subset()).flatten().map(|index| index as u32))
|
||||
.disabled(disabled)
|
||||
.draw_icon(false)
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(opacity.map(|opacity| opacity * 100.))
|
||||
NumberInput::new(opacity.map(|opacity| opacity as f64))
|
||||
.label("Opacity")
|
||||
.unit("%")
|
||||
.display_decimal_places(2)
|
||||
.disabled(opacity.is_none() && !opacity_is_mixed)
|
||||
.disabled(disabled)
|
||||
.min(0.)
|
||||
.max(100.)
|
||||
.range_min(Some(0.))
|
||||
|
|
@ -1613,8 +1613,8 @@ impl DocumentMessageHandler {
|
|||
}]);
|
||||
|
||||
responses.add(LayoutMessage::SendLayout {
|
||||
layout: Layout::WidgetLayout(layer_tree_options),
|
||||
layout_target: LayoutTarget::LayerTreeOptions,
|
||||
layout: Layout::WidgetLayout(layers_panel_options_bar),
|
||||
layout_target: LayoutTarget::LayersPanelOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use bezier_rs::Subpath;
|
|||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use graph_craft::document::DocumentNode;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
|
|
@ -23,6 +24,14 @@ pub enum GraphOperationMessage {
|
|||
layer: LayerIdentifier,
|
||||
fill: Fill,
|
||||
},
|
||||
OpacitySet {
|
||||
layer: LayerIdentifier,
|
||||
opacity: f32,
|
||||
},
|
||||
BlendModeSet {
|
||||
layer: LayerIdentifier,
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
UpdateBounds {
|
||||
layer: LayerIdentifier,
|
||||
old_bounds: [DVec2; 2],
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use document_legacy::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
|||
use document_legacy::{LayerId, Operation};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
|
|
@ -284,7 +284,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let new_input = output_node.inputs.first().cloned().filter(|input| input.as_node().is_some());
|
||||
let node_id = generate_uuid();
|
||||
|
||||
output_node.metadata.position.x += 8;
|
||||
output_node.inputs[0] = NodeInput::node(node_id, 0);
|
||||
|
||||
let Some(node_type) = resolve_document_node_type(name) else {
|
||||
|
|
@ -294,6 +293,12 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata);
|
||||
update_input(&mut new_document_node.inputs, node_id, self.document_metadata);
|
||||
self.network.nodes.insert(node_id, new_document_node);
|
||||
|
||||
let upstream_nodes = self.network.upstream_flow_back_from_nodes(vec![node_id], true).map(|(_, id)| id).collect::<Vec<_>>();
|
||||
for node_id in upstream_nodes {
|
||||
let Some(node) = self.network.nodes.get_mut(&node_id) else { continue };
|
||||
node.metadata.position.x -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the inputs of a specific node
|
||||
|
|
@ -367,6 +372,18 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
});
|
||||
}
|
||||
|
||||
fn opacity_set(&mut self, opacity: f32) {
|
||||
self.modify_inputs("Opacity", false, |inputs, _node_id, _metadata| {
|
||||
inputs[1] = NodeInput::value(TaggedValue::F32(opacity * 100.), false);
|
||||
});
|
||||
}
|
||||
|
||||
fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
self.modify_inputs("Blend Mode", false, |inputs, _node_id, _metadata| {
|
||||
inputs[1] = NodeInput::value(TaggedValue::BlendMode(blend_mode), false);
|
||||
});
|
||||
}
|
||||
|
||||
fn stroke_set(&mut self, stroke: Stroke) {
|
||||
self.modify_inputs("Stroke", false, |inputs, _node_id, _metadata| {
|
||||
inputs[1] = NodeInput::value(TaggedValue::OptionalColor(stroke.color), false);
|
||||
|
|
@ -564,6 +581,16 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
|||
responses.add(Operation::SetLayerFill { path: layer, fill });
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) {
|
||||
modify_inputs.opacity_set(opacity);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::BlendModeSet { layer, blend_mode } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) {
|
||||
modify_inputs.blend_mode_set(blend_mode);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) {
|
||||
modify_inputs.update_bounds(old_bounds, new_bounds);
|
||||
|
|
@ -715,7 +742,7 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
|||
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
|
||||
let layer_nodes = modify_inputs.network.nodes.iter().filter(|(_, node)| node.is_layer()).map(|(id, _)| *id).collect::<Vec<_>>();
|
||||
for layer in layer_nodes {
|
||||
if modify_inputs.network.upstream_flow_back_from_nodes(vec![layer], true).any(|(node, _id)| node.name == "Artboard") {
|
||||
if modify_inputs.network.upstream_flow_back_from_nodes(vec![layer], true).any(|(node, _id)| node.is_artboard()) {
|
||||
modify_inputs.delete_layer(layer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,27 +368,29 @@ fn noise_type(document_node: &DocumentNode, node_id: u64, index: usize, name: &s
|
|||
LayoutGroup::Row { widgets }.with_tooltip("Type of Noise")
|
||||
}
|
||||
|
||||
//TODO Use generalized Version of this as soon as it's available
|
||||
// TODO: Use generalized version of this as soon as it's available
|
||||
fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
if let &NodeInput::Value {
|
||||
tagged_value: TaggedValue::BlendMode(mode),
|
||||
tagged_value: TaggedValue::BlendMode(blend_mode),
|
||||
exposed: false,
|
||||
} = &document_node.inputs[index]
|
||||
{
|
||||
let entries = BlendMode::list()
|
||||
let entries = BlendMode::list_svg_subset()
|
||||
.iter()
|
||||
.map(|category| {
|
||||
category
|
||||
.iter()
|
||||
.map(|mode| MenuListEntry::new(mode.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(*mode), node_id, index)))
|
||||
.map(|blend_mode| MenuListEntry::new(blend_mode.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(*blend_mode), node_id, index)))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(),
|
||||
DropdownInput::new(entries)
|
||||
.selected_index(blend_mode.index_in_list_svg_subset().map(|index| index as u32))
|
||||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending")
|
||||
|
|
|
|||
|
|
@ -680,7 +680,7 @@ impl PortfolioMessageHandler {
|
|||
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
new_document.update_layer_tree_options_bar_widgets(responses, &render_data);
|
||||
new_document.update_layers_panel_options_bar_widgets(responses);
|
||||
|
||||
self.documents.insert(document_id, new_document);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::messages::prelude::*;
|
|||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier, LayerId, Operation};
|
||||
use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork};
|
||||
use graphene_core::raster::ImageFrame;
|
||||
use graphene_core::raster::{BlendMode, ImageFrame};
|
||||
use graphene_core::text::Font;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::style::{FillType, Gradient};
|
||||
|
|
@ -98,7 +98,7 @@ pub fn get_mirror_handles(layer: LayerNodeIdentifier, document: &Document) -> Op
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the current gradient of a layer from the closest fill node
|
||||
/// Get the current gradient of a layer from the closest Fill node
|
||||
pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option<Gradient> {
|
||||
let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?;
|
||||
let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else {
|
||||
|
|
@ -128,7 +128,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option<G
|
|||
})
|
||||
}
|
||||
|
||||
/// Get the current fill of a layer from the closest fill node
|
||||
/// Get the current fill of a layer from the closest Fill node
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, document: &Document) -> Option<Color> {
|
||||
let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?;
|
||||
let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else {
|
||||
|
|
@ -137,14 +137,39 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, document: &Document) -> Option
|
|||
Some(*color)
|
||||
}
|
||||
|
||||
pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, document)?.node_id("Text")
|
||||
/// Get the current blend mode of a layer from the closest Blend Mode node
|
||||
pub fn get_blend_mode(layer: LayerNodeIdentifier, document: &Document) -> Option<BlendMode> {
|
||||
let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Blend Mode")?;
|
||||
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*blend_mode)
|
||||
}
|
||||
|
||||
/// Get the current opacity of a layer from the closest Opacity node.
|
||||
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
|
||||
/// - Multiplied with additional opacity nodes earlier in the chain
|
||||
/// - Set by an Opacity node with an exposed parameter value driven by another node
|
||||
/// - Already factored into the pixel alpha channel of an image
|
||||
/// - The default value of 100% if no Opacity node is present, but this function returns None in that case
|
||||
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
|
||||
pub fn get_opacity(layer: LayerNodeIdentifier, document: &Document) -> Option<f32> {
|
||||
let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Opacity")?;
|
||||
let TaggedValue::F32(opacity) = inputs.get(1)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*opacity)
|
||||
}
|
||||
|
||||
pub fn get_fill_id(layer: LayerNodeIdentifier, document: &Document) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, document)?.node_id("Fill")
|
||||
}
|
||||
|
||||
/// Gets properties from the text node
|
||||
pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, document)?.node_id("Text")
|
||||
}
|
||||
|
||||
/// Gets properties from the Text node
|
||||
pub fn get_text(layer: LayerNodeIdentifier, document: &Document) -> Option<(&String, &Font, f64)> {
|
||||
let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Text")?;
|
||||
let NodeInput::Value {
|
||||
|
|
@ -174,19 +199,9 @@ pub fn get_text(layer: LayerNodeIdentifier, document: &Document) -> Option<(&Str
|
|||
Some((text, font, font_size))
|
||||
}
|
||||
|
||||
/// Is a specified layer an artboard?
|
||||
pub fn is_artboard(layer: LayerNodeIdentifier, document: &Document) -> bool {
|
||||
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Artboard"))
|
||||
}
|
||||
|
||||
/// Is a specified layer a shape?
|
||||
pub fn is_shape_layer(layer: LayerNodeIdentifier, document: &Document) -> bool {
|
||||
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Shape"))
|
||||
}
|
||||
|
||||
/// Is a specified layer text?
|
||||
pub fn is_text_layer(layer: LayerNodeIdentifier, document: &Document) -> bool {
|
||||
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Text"))
|
||||
/// Checks if a specified layer uses an upstream node matching the given name.
|
||||
pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document: &Document, node_name: &str) -> bool {
|
||||
NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.find_node_inputs(node_name).is_some())
|
||||
}
|
||||
|
||||
/// Convert subpaths to an iterator of manipulator groups
|
||||
|
|
@ -243,19 +258,18 @@ impl<'a> NodeGraphLayer<'a> {
|
|||
self.node_graph.upstream_flow_back_from_nodes(vec![self.layer_node], true)
|
||||
}
|
||||
|
||||
/// Does a node exist in the layer's primary flow
|
||||
pub fn uses_node(&self, node_name: &str) -> bool {
|
||||
self.primary_layer_flow().any(|(node, _id)| node.name == node_name)
|
||||
}
|
||||
|
||||
/// Node id of a node if it exists in the layer's primary flow
|
||||
pub fn node_id(&self, node_name: &str) -> Option<NodeId> {
|
||||
self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(_node, id)| id)
|
||||
}
|
||||
|
||||
/// Find all of the inputs of a specific node within the layer's primary flow
|
||||
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
|
||||
pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec<NodeInput>> {
|
||||
self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(node, _id)| &node.inputs)
|
||||
self.primary_layer_flow()
|
||||
.skip(1)
|
||||
.take_while(|(node, _)| !node.is_layer())
|
||||
.find(|(node, _)| node.name == node_name)
|
||||
.map(|(node, _id)| &node.inputs)
|
||||
}
|
||||
|
||||
/// Find a specific input of a node within the layer's primary flow
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_artboard;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
||||
|
|
@ -150,7 +150,10 @@ impl ArtboardToolData {
|
|||
fn select_artboard(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) -> bool {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let mut intersections = document.document_legacy.click_xray(input.mouse.position).filter(|&layer| is_artboard(layer, &document.document_legacy));
|
||||
let mut intersections = document
|
||||
.document_legacy
|
||||
.click_xray(input.mouse.position)
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Artboard"));
|
||||
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
if let Some(intersection) = intersections.next() {
|
||||
|
|
|
|||
|
|
@ -11,39 +11,8 @@ use graphene_core::uuid::generate_uuid;
|
|||
use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
||||
use graphene_core::Color;
|
||||
|
||||
const EXPOSED_BLEND_MODES: &[&[BlendMode]] = {
|
||||
use BlendMode::*;
|
||||
&[
|
||||
// Basic group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion, Subtract, Divide],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
};
|
||||
|
||||
const BRUSH_MAX_SIZE: f64 = 5000.;
|
||||
|
||||
fn blend_mode_dropdown_idx(target_blend_mode: BlendMode) -> Option<u32> {
|
||||
let mut i = 0;
|
||||
for group in EXPOSED_BLEND_MODES {
|
||||
for &blend_mode in group.iter() {
|
||||
if blend_mode == target_blend_mode {
|
||||
return Some(i);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub enum DrawMode {
|
||||
Draw = 0,
|
||||
|
|
@ -192,7 +161,7 @@ impl LayoutHolder for BrushTool {
|
|||
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
|
||||
let blend_mode_entries: Vec<Vec<_>> = EXPOSED_BLEND_MODES
|
||||
let blend_mode_entries: Vec<Vec<_>> = BlendMode::list()
|
||||
.iter()
|
||||
.map(|group| {
|
||||
group
|
||||
|
|
@ -207,7 +176,7 @@ impl LayoutHolder for BrushTool {
|
|||
.collect();
|
||||
widgets.push(
|
||||
DropdownInput::new(blend_mode_entries)
|
||||
.selected_index(blend_mode_dropdown_idx(self.options.blend_mode))
|
||||
.selected_index(self.options.blend_mode.index_in_list().map(|index| index as u32))
|
||||
.tooltip("The blend mode used with the background when performing a brush stroke. Only used in draw mode.")
|
||||
.disabled(self.options.draw_mode != DrawMode::Draw)
|
||||
.widget_holder(),
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
|||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_shape_layer;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_text_layer;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::path_outline::*;
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
|
||||
|
|
@ -804,7 +803,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
if let Some(layer) = selected_layers.next() {
|
||||
// Check that only one layer is selected
|
||||
if selected_layers.next().is_none() && is_text_layer(layer, &document.document_legacy) {
|
||||
if selected_layers.next().is_none() && is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text") {
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
||||
responses.add(TextToolMessage::EditSelected);
|
||||
}
|
||||
|
|
@ -952,10 +951,10 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer:
|
|||
}
|
||||
|
||||
fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document: &Document, responses: &mut VecDeque<Message>) {
|
||||
if is_text_layer(layer, document) {
|
||||
if is_layer_fed_by_node_of_name(layer, document, "Text") {
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
||||
responses.add(TextToolMessage::EditSelected);
|
||||
} else if is_shape_layer(layer, document) {
|
||||
} else if is_layer_fed_by_node_of_name(layer, document, "Shape") {
|
||||
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use super::tool_prelude::*;
|
|||
use crate::application::generate_uuid;
|
||||
use crate::consts::COLOR_ACCENT;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_text_layer};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
|
||||
use document_legacy::document_metadata::LayerNodeIdentifier;
|
||||
use document_legacy::intersection::Quad;
|
||||
|
|
@ -277,7 +277,7 @@ impl TextToolData {
|
|||
if let Some(clicked_text_layer_path) = document
|
||||
.document_legacy
|
||||
.click(mouse, document.network())
|
||||
.filter(|&layer| is_text_layer(layer, &document.document_legacy))
|
||||
.filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text"))
|
||||
{
|
||||
self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses);
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ fn can_edit_selected(document: &DocumentMessageHandler) -> Option<LayerNodeIdent
|
|||
return None;
|
||||
}
|
||||
|
||||
if !is_text_layer(layer, &document.document_legacy) {
|
||||
if !is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text") {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { beginDraggingElement } from "@graphite/io-managers/drag";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayerTreeOptionsLayout } from "@graphite/wasm-communication/messages";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
|
||||
import type { LayerType, LayerPanelEntry } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
|
@ -46,12 +46,12 @@
|
|||
let dragInPanel = false;
|
||||
|
||||
// Layouts
|
||||
let layerTreeOptionsLayout = defaultWidgetLayout();
|
||||
let layersPanelOptionsLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
|
||||
patchWidgetLayout(layerTreeOptionsLayout, updateLayerTreeOptionsLayout);
|
||||
layerTreeOptionsLayout = layerTreeOptionsLayout;
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelOptionsLayout, (updateLayersPanelOptionsLayout) => {
|
||||
patchWidgetLayout(layersPanelOptionsLayout, updateLayersPanelOptionsLayout);
|
||||
layersPanelOptionsLayout = layersPanelOptionsLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
|
||||
|
|
@ -301,7 +301,7 @@
|
|||
|
||||
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
|
||||
<LayoutRow class="options-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layerTreeOptionsLayout} />
|
||||
<WidgetLayout layout={layersPanelOptionsLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
<LayoutCol class="list" bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>
|
||||
|
|
|
|||
|
|
@ -1345,7 +1345,7 @@ export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
|
|||
|
||||
export class UpdateGraphViewOverlayButtonLayout extends WidgetDiffUpdate {}
|
||||
|
||||
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {}
|
||||
export class UpdateLayersPanelOptionsLayout extends WidgetDiffUpdate {}
|
||||
|
||||
// Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed
|
||||
export class UpdateMenuBarLayout extends JsMessage {
|
||||
|
|
@ -1441,7 +1441,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateGraphViewOverlayButtonLayout,
|
||||
UpdateImageData,
|
||||
UpdateInputHints,
|
||||
UpdateLayerTreeOptionsLayout,
|
||||
UpdateLayersPanelOptionsLayout,
|
||||
UpdateMenuBarLayout,
|
||||
UpdateMouseCursor,
|
||||
UpdateNodeGraph,
|
||||
|
|
|
|||
|
|
@ -52,42 +52,14 @@ impl core::fmt::Display for LuminanceCalculation {
|
|||
}
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
pub fn list() -> [&'static [BlendMode]; 6] {
|
||||
[
|
||||
// Normal group
|
||||
&[BlendMode::Normal],
|
||||
// Darken group
|
||||
&[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor],
|
||||
// Lighten group
|
||||
&[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge, BlendMode::LinearDodge, BlendMode::LighterColor],
|
||||
// Contrast group
|
||||
&[
|
||||
BlendMode::Overlay,
|
||||
BlendMode::SoftLight,
|
||||
BlendMode::HardLight,
|
||||
BlendMode::VividLight,
|
||||
BlendMode::LinearLight,
|
||||
BlendMode::PinLight,
|
||||
BlendMode::HardMix,
|
||||
],
|
||||
// Inversion group
|
||||
&[BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract, BlendMode::Divide],
|
||||
// Component group
|
||||
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)]
|
||||
#[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this?
|
||||
pub enum BlendMode {
|
||||
#[default]
|
||||
// Basic group
|
||||
#[default]
|
||||
Normal,
|
||||
// Not supported by SVG, but we should someday support: Dissolve
|
||||
|
||||
// Darken group
|
||||
Darken,
|
||||
|
|
@ -130,6 +102,95 @@ pub enum BlendMode {
|
|||
MultiplyAlpha,
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// All standard blend modes ordered by group.
|
||||
pub fn list() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge, LinearDodge, LighterColor],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion, Subtract, Divide],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
/// The subset of [`BlendMode::list()`] that is supported by SVG.
|
||||
pub fn list_svg_subset() -> [&'static [BlendMode]; 6] {
|
||||
use BlendMode::*;
|
||||
[
|
||||
// Normal group
|
||||
&[Normal],
|
||||
// Darken group
|
||||
&[Darken, Multiply, ColorBurn],
|
||||
// Lighten group
|
||||
&[Lighten, Screen, ColorDodge],
|
||||
// Contrast group
|
||||
&[Overlay, SoftLight, HardLight],
|
||||
// Inversion group
|
||||
&[Difference, Exclusion],
|
||||
// Component group
|
||||
&[Hue, Saturation, Color, Luminosity],
|
||||
]
|
||||
}
|
||||
|
||||
pub fn index_in_list(&self) -> Option<usize> {
|
||||
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
|
||||
Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
|
||||
}
|
||||
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => Some("normal"),
|
||||
// Darken group
|
||||
BlendMode::Darken => Some("darken"),
|
||||
BlendMode::Multiply => Some("multiply"),
|
||||
BlendMode::ColorBurn => Some("color-burn"),
|
||||
// Lighten group
|
||||
BlendMode::Lighten => Some("lighten"),
|
||||
BlendMode::Screen => Some("screen"),
|
||||
BlendMode::ColorDodge => Some("color-dodge"),
|
||||
// Contrast group
|
||||
BlendMode::Overlay => Some("overlay"),
|
||||
BlendMode::SoftLight => Some("soft-light"),
|
||||
BlendMode::HardLight => Some("hard-light"),
|
||||
// Inversion group
|
||||
BlendMode::Difference => Some("difference"),
|
||||
BlendMode::Exclusion => Some("exclusion"),
|
||||
// Component group
|
||||
BlendMode::Hue => Some("hue"),
|
||||
BlendMode::Saturation => Some("saturation"),
|
||||
BlendMode::Color => Some("color"),
|
||||
BlendMode::Luminosity => Some("luminosity"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the blend mode CSS style declaration.
|
||||
pub fn render(&self) -> String {
|
||||
format!(
|
||||
r#" mix-blend-mode: {};"#,
|
||||
self.to_svg_style_name().unwrap_or_else(|| {
|
||||
warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BlendMode {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
|
|
@ -173,64 +234,6 @@ impl core::fmt::Display for BlendMode {
|
|||
}
|
||||
}
|
||||
|
||||
impl BlendMode {
|
||||
/// Convert the enum to the CSS string for the blend mode.
|
||||
/// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values)
|
||||
pub fn to_svg_style_name(&self) -> &'static str {
|
||||
match self {
|
||||
// Normal group
|
||||
BlendMode::Normal => "normal",
|
||||
// Darken group
|
||||
BlendMode::Darken => "darken",
|
||||
BlendMode::Multiply => "multiply",
|
||||
BlendMode::ColorBurn => "color-burn",
|
||||
// Lighten group
|
||||
BlendMode::Lighten => "lighten",
|
||||
BlendMode::Screen => "screen",
|
||||
BlendMode::ColorDodge => "color-dodge",
|
||||
// Contrast group
|
||||
BlendMode::Overlay => "overlay",
|
||||
BlendMode::SoftLight => "soft-light",
|
||||
BlendMode::HardLight => "hard-light",
|
||||
// Inversion group
|
||||
BlendMode::Difference => "difference",
|
||||
BlendMode::Exclusion => "exclusion",
|
||||
// Component group
|
||||
BlendMode::Hue => "hue",
|
||||
BlendMode::Saturation => "saturation",
|
||||
BlendMode::Color => "color",
|
||||
BlendMode::Luminosity => "luminosity",
|
||||
_ => {
|
||||
warn!("Unsupported blend mode {self:?}");
|
||||
"normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the blend mode CSS style declaration.
|
||||
pub fn render(&self) -> String {
|
||||
format!(r#" mix-blend-mode: {};"#, self.to_svg_style_name())
|
||||
}
|
||||
|
||||
/// List of all the blend modes in their conventional ordering and grouping.
|
||||
pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] {
|
||||
[
|
||||
// Normal group
|
||||
&[BlendMode::Normal],
|
||||
// Darken group
|
||||
&[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn],
|
||||
// Lighten group
|
||||
&[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge],
|
||||
// Contrast group
|
||||
&[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight],
|
||||
// Inversion group
|
||||
&[BlendMode::Difference, BlendMode::Exclusion],
|
||||
// Component group
|
||||
&[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct LuminanceNode<LuminanceCalculation> {
|
||||
luminance_calc: LuminanceCalculation,
|
||||
|
|
|
|||
|
|
@ -276,6 +276,12 @@ impl DocumentNode {
|
|||
// TODO: Or, more fundamentally separate the concept of a layer from a node.
|
||||
self.name == "Layer"
|
||||
}
|
||||
|
||||
pub fn is_artboard(&self) -> bool {
|
||||
// TODO: Use something more robust than checking against a string.
|
||||
// TODO: Or, more fundamentally separate the concept of a layer from a node.
|
||||
self.name == "Artboard"
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the possible inputs to a node.
|
||||
|
|
|
|||
Loading…
Reference in New Issue