Layer opacity (#312)

Closes #187

* Add layer opacity input

* Improve Rust code cleanliness
This commit is contained in:
Keavon Chambers 2021-07-27 23:15:23 -07:00
parent 12fc330952
commit 0cdd1762b8
9 changed files with 86 additions and 18 deletions

View File

@ -1,11 +1,11 @@
<template> <template>
<LayoutCol :class="'layer-tree-panel'"> <LayoutCol :class="'layer-tree-panel'">
<LayoutRow :class="'options-bar'"> <LayoutRow :class="'options-bar'">
<DropdownInput :menuEntries="blendModeEntries" v-model:selectedIndex="blendModeSelectedIndex" @update:selectedIndex="blendModeChanged" :disabled="blendModeDropdownDisabled" /> <DropdownInput v-model:selectedIndex="blendModeSelectedIndex" @update:selectedIndex="setLayerBlendMode" :menuEntries="blendModeEntries" :disabled="blendModeDropdownDisabled" />
<Separator :type="SeparatorType.Related" /> <Separator :type="SeparatorType.Related" />
<NumberInput v-model:value="opacity" :min="0" :max="100" :unit="`%`" :displayDecimalPlaces="2" /> <NumberInput v-model:value="opacity" @update:value="setLayerOpacity" :min="0" :max="100" :unit="`%`" :displayDecimalPlaces="2" :disabled="opacityNumberInputDisabled" />
<Separator :type="SeparatorType.Related" /> <Separator :type="SeparatorType.Related" />
@ -179,9 +179,16 @@ export default defineComponent({
const { toggle_layer_visibility } = await wasm; const { toggle_layer_visibility } = await wasm;
toggle_layer_visibility(path); toggle_layer_visibility(path);
}, },
async setLayerBlendMode(blendMode: BlendMode) { async setLayerBlendMode() {
const { set_blend_mode_for_selected_layers } = await wasm; const blendMode = this.blendModeEntries.flat()[this.blendModeSelectedIndex].value as BlendMode;
set_blend_mode_for_selected_layers(blendMode); if (blendMode) {
const { set_blend_mode_for_selected_layers } = await wasm;
set_blend_mode_for_selected_layers(blendMode);
}
},
async setLayerOpacity() {
const { set_opacity_for_selected_layers } = await wasm;
set_opacity_for_selected_layers(this.opacity);
}, },
async handleControlClick(clickedLayer: LayerPanelEntry) { async handleControlClick(clickedLayer: LayerPanelEntry) {
const index = this.layers.indexOf(clickedLayer); const index = this.layers.indexOf(clickedLayer);
@ -199,7 +206,7 @@ export default defineComponent({
}, },
async handleShiftClick(clickedLayer: LayerPanelEntry) { async handleShiftClick(clickedLayer: LayerPanelEntry) {
// The two paths of the range are stored in selectionRangeStartLayer and selectionRangeEndLayer // The two paths of the range are stored in selectionRangeStartLayer and selectionRangeEndLayer
// So for a new Shift+Click, select all layers between selectionRangeStartLayer and selectionRangeEndLayer (stored in prev Shift+Click) // So for a new Shift+Click, select all layers between selectionRangeStartLayer and selectionRangeEndLayer (stored in previous Shift+Click)
this.clearSelection(); this.clearSelection();
this.selectionRangeEndLayer = clickedLayer; this.selectionRangeEndLayer = clickedLayer;
@ -276,12 +283,28 @@ export default defineComponent({
this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode); this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode);
} else { } else {
// Display a dash when they are not all the same value // Display a dash when they are not all the same value
this.blendModeSelectedIndex = -1; this.blendModeSelectedIndex = NaN;
} }
}, },
blendModeChanged() { setOpacityForSelectedLayers() {
const blendMode = this.blendModeEntries.flat()[this.blendModeSelectedIndex].value as BlendMode; const selected = this.layers.filter((layer) => layer.layer_data.selected);
if (blendMode) this.setLayerBlendMode(blendMode);
if (selected.length < 1) {
this.opacity = 100;
this.opacityNumberInputDisabled = true;
return;
}
this.opacityNumberInputDisabled = false;
const firstEncounteredOpacity = selected[0].opacity;
const allOpacitiesAlike = !selected.find((layer) => layer.opacity !== firstEncounteredOpacity);
if (allOpacitiesAlike) {
this.opacity = firstEncounteredOpacity;
} else {
// Display a dash when they are not all the same value
this.opacity = NaN;
}
}, },
}, },
mounted() { mounted() {
@ -295,6 +318,7 @@ export default defineComponent({
this.layers = responseLayers; this.layers = responseLayers;
this.setBlendModeForSelectedLayers(); this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers();
} }
}); });
registerResponseHandler(ResponseType.CollapseFolder, (responseData) => { registerResponseHandler(ResponseType.CollapseFolder, (responseData) => {
@ -306,6 +330,7 @@ export default defineComponent({
blendModeEntries, blendModeEntries,
blendModeSelectedIndex: 0, blendModeSelectedIndex: 0,
blendModeDropdownDisabled: true, blendModeDropdownDisabled: true,
opacityNumberInputDisabled: true,
layers: [] as Array<LayerPanelEntry>, layers: [] as Array<LayerPanelEntry>,
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry, selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry, selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,

View File

@ -244,10 +244,15 @@ function newBlendMode(input: string): BlendMode {
return blendMode; return blendMode;
} }
function newOpacity(input: number): number {
return input * 100;
}
export interface LayerPanelEntry { export interface LayerPanelEntry {
name: string; name: string;
visible: boolean; visible: boolean;
blend_mode: BlendMode; blend_mode: BlendMode;
opacity: number;
layer_type: LayerType; layer_type: LayerType;
path: BigUint64Array; path: BigUint64Array;
layer_data: LayerData; layer_data: LayerData;
@ -258,6 +263,7 @@ function newLayerPanelEntry(input: any): LayerPanelEntry {
name: input.name, name: input.name,
visible: input.visible, visible: input.visible,
blend_mode: newBlendMode(input.blend_mode), blend_mode: newBlendMode(input.blend_mode),
opacity: newOpacity(input.opacity),
layer_type: newLayerType(input.layer_type), layer_type: newLayerType(input.layer_type),
layer_data: newLayerData(input.layer_data), layer_data: newLayerData(input.layer_data),
path: new BigUint64Array(input.path.map((n: number) => BigInt(n))), path: new BigUint64Array(input.path.map((n: number) => BigInt(n))),

View File

@ -213,7 +213,7 @@ pub fn reorder_selected_layers(delta: i32) -> Result<(), JsValue> {
.map_err(convert_error) .map_err(convert_error)
} }
/// Set the blend mode of the selected layers /// Set the blend mode for the selected layers
#[wasm_bindgen] #[wasm_bindgen]
pub fn set_blend_mode_for_selected_layers(blend_mode_svg_style_name: String) -> Result<(), JsValue> { pub fn set_blend_mode_for_selected_layers(blend_mode_svg_style_name: String) -> Result<(), JsValue> {
let blend_mode = match blend_mode_svg_style_name.as_str() { let blend_mode = match blend_mode_svg_style_name.as_str() {
@ -239,6 +239,17 @@ pub fn set_blend_mode_for_selected_layers(blend_mode_svg_style_name: String) ->
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SetBlendModeForSelectedLayers(blend_mode)).map_err(convert_error)) EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SetBlendModeForSelectedLayers(blend_mode)).map_err(convert_error))
} }
/// Set the opacity for the selected layers
#[wasm_bindgen]
pub fn set_opacity_for_selected_layers(opacity_percent: f64) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| {
editor
.borrow_mut()
.handle_message(DocumentMessage::SetOpacityForSelectedLayers(opacity_percent / 100.))
.map_err(convert_error)
})
}
/// Export the document /// Export the document
#[wasm_bindgen] #[wasm_bindgen]
pub fn export_document() -> Result<(), JsValue> { pub fn export_document() -> Result<(), JsValue> {

View File

@ -412,15 +412,22 @@ impl Document {
} }
Operation::SetLayerBlendMode { path, blend_mode } => { Operation::SetLayerBlendMode { path, blend_mode } => {
self.mark_as_dirty(path)?; self.mark_as_dirty(path)?;
self.layer_mut(&path).unwrap().blend_mode = *blend_mode; self.layer_mut(path)?.blend_mode = *blend_mode;
let path = path.as_slice()[..path.len() - 1].to_vec();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
}
Operation::SetLayerOpacity { path, opacity } => {
self.mark_as_dirty(path)?;
self.layer_mut(path)?.opacity = *opacity;
let path = path.as_slice()[..path.len() - 1].to_vec(); let path = path.as_slice()[..path.len() - 1].to_vec();
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }]) Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
} }
Operation::FillLayer { path, color } => { Operation::FillLayer { path, color } => {
let layer = self.layer_mut(path).unwrap(); self.layer_mut(path)?.style.set_fill(layers::style::Fill::new(*color));
layer.style.set_fill(layers::style::Fill::new(*color));
self.mark_as_dirty(path)?; self.mark_as_dirty(path)?;
Some(vec![DocumentResponse::DocumentChanged]) Some(vec![DocumentResponse::DocumentChanged])
} }

View File

@ -175,6 +175,7 @@ pub struct Layer {
pub thumbnail_cache: String, pub thumbnail_cache: String,
pub cache_dirty: bool, pub cache_dirty: bool,
pub blend_mode: BlendMode, pub blend_mode: BlendMode,
pub opacity: f64,
} }
impl Layer { impl Layer {
@ -189,6 +190,7 @@ impl Layer {
thumbnail_cache: String::new(), thumbnail_cache: String::new(),
cache_dirty: true, cache_dirty: true,
blend_mode: BlendMode::Normal, blend_mode: BlendMode::Normal,
opacity: 1.,
} }
} }
@ -203,8 +205,9 @@ impl Layer {
self.cache.clear(); self.cache.clear();
let _ = write!( let _ = write!(
self.cache, self.cache,
r#"<g style="mix-blend-mode: {}">{}</g>"#, r#"<g style="mix-blend-mode: {}; opacity: {}">{}</g>"#,
self.blend_mode.to_svg_style_name(), self.blend_mode.to_svg_style_name(),
self.opacity,
self.thumbnail_cache.as_str() self.thumbnail_cache.as_str()
); );

View File

@ -77,6 +77,10 @@ pub enum Operation {
path: Vec<LayerId>, path: Vec<LayerId>,
blend_mode: BlendMode, blend_mode: BlendMode,
}, },
SetLayerOpacity {
path: Vec<LayerId>,
opacity: f64,
},
FillLayer { FillLayer {
path: Vec<LayerId>, path: Vec<LayerId>,
color: Color, color: Color,

View File

@ -40,6 +40,7 @@ fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[
pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &mut 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; let blend_mode = layer.blend_mode;
let opacity = layer.opacity;
let layer_type: LayerType = (&layer.data).into(); let layer_type: LayerType = (&layer.data).into();
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type)); 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 = layer.current_bounding_box().unwrap_or([DVec2::ZERO, DVec2::ZERO]);
@ -66,6 +67,7 @@ pub fn layer_panel_entry(layer_data: &mut LayerData, layer: &mut Layer, path: Ve
name, name,
visible: layer.visible, visible: layer.visible,
blend_mode, blend_mode,
opacity,
layer_type, layer_type,
layer_data: *layer_data, layer_data: *layer_data,
path, path,

View File

@ -27,6 +27,7 @@ pub enum DocumentMessage {
DuplicateSelectedLayers, DuplicateSelectedLayers,
CopySelectedLayers, CopySelectedLayers,
SetBlendModeForSelectedLayers(BlendMode), SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
PasteLayers { path: Vec<LayerId>, insert_index: isize }, PasteLayers { path: Vec<LayerId>, insert_index: isize },
AddFolder(Vec<LayerId>), AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String), RenameLayer(Vec<LayerId>, String),
@ -61,7 +62,7 @@ pub enum DocumentMessage {
AlignSelectedLayers(AlignAxis, AlignAggregate), AlignSelectedLayers(AlignAxis, AlignAggregate),
DragLayer(Vec<LayerId>, DVec2), DragLayer(Vec<LayerId>, DVec2),
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize }, MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
ReorderSelectedLayers(i32), // relatve_position, ReorderSelectedLayers(i32), // relative_position,
SetLayerTranslation(Vec<LayerId>, Option<f64>, Option<f64>), SetLayerTranslation(Vec<LayerId>, Option<f64>, Option<f64>),
} }
@ -361,8 +362,16 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
SetBlendModeForSelectedLayers(blend_mode) => { SetBlendModeForSelectedLayers(blend_mode) => {
let active_document = self.active_document(); let active_document = self.active_document();
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path)) { for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path: path.clone(), blend_mode }.into()); responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
let opacity = opacity.clamp(0., 1.);
let active_document = self.active_document();
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
} }
} }
ToggleLayerVisibility(path) => { ToggleLayerVisibility(path) => {

View File

@ -11,6 +11,7 @@ pub struct LayerPanelEntry {
pub name: String, pub name: String,
pub visible: bool, pub visible: bool,
pub blend_mode: BlendMode, pub blend_mode: BlendMode,
pub opacity: f64,
pub layer_type: LayerType, pub layer_type: LayerType,
pub layer_data: LayerData, pub layer_data: LayerData,
pub path: Vec<LayerId>, pub path: Vec<LayerId>,