Layer opacity (#312)
Closes #187 * Add layer opacity input * Improve Rust code cleanliness
This commit is contained in:
parent
12fc330952
commit
0cdd1762b8
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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))),
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue