From 235def7d06e9cf4cd128f6a5cd56522cbb1b4577 Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Sun, 20 Dec 2020 14:06:15 +0100 Subject: [PATCH] Restructured layer masking and UI --- .../components/layers/layers.vue | 53 ++++++++++++---- .../components/layers/messages.json | 2 +- src/components/ui/zcanvas/layer-sprite.js | 15 ++--- src/store/modules/document-module.js | 19 ++++-- .../store/modules/document-module.spec.js | 61 +++++++++++++++---- 5 files changed, 112 insertions(+), 38 deletions(-) diff --git a/src/components/options-panel/components/layers/layers.vue b/src/components/options-panel/components/layers/layers.vue index ea005ac..64d16af 100644 --- a/src/components/options-panel/components/layers/layers.vue +++ b/src/components/options-panel/components/layers/layers.vue @@ -31,8 +31,28 @@ :class="{ 'active': layer.index === activeLayerIndex }" - @click="setActiveLayerIndex( layer.index )" - >{{ layer.name }} + > + {{ layer.name }} + + × +
-
@@ -73,6 +86,7 @@ export default { "activeDocument", "activeLayer", "activeLayerIndex", + "activeLayerMask", "layers", ]), reverseLayers() { @@ -88,6 +102,7 @@ export default { "removeLayer", "updateLayer", "setActiveLayerIndex", + "setActiveLayerMask", "addMaskToActiveLayer", ]), requestLayerAdd() { @@ -101,8 +116,8 @@ export default { } }); }, - requestLayerRemove() { - this.removeLayer( this.activeLayer ); + requestLayerRemove( index ) { + this.removeLayer( index ); }, }, }; @@ -131,16 +146,28 @@ h3 { padding: 0 $spacing-xsmall; @include boxSize(); @include customFont(); + display: flex; &:hover, &.active { background-color: $color-1; - color: #FFF; + color: #000; } &.active { padding: $spacing-xsmall $spacing-xsmall; border: none; } + + .name { + flex: 3; + @include truncate(); + } + .mask { + flex: 1; + } + .highlight { + color: #FFF; + } } .actions { diff --git a/src/components/options-panel/components/layers/messages.json b/src/components/options-panel/components/layers/messages.json index a2d4038..66d3b18 100644 --- a/src/components/options-panel/components/layers/messages.json +++ b/src/components/options-panel/components/layers/messages.json @@ -3,6 +3,6 @@ "layers": "Layers", "addLayer": "Add layer", "addMask": "Add mask", - "removeLayer": "Remove layer" + "mask": "Mask" } } diff --git a/src/components/ui/zcanvas/layer-sprite.js b/src/components/ui/zcanvas/layer-sprite.js index 35b7af8..a3b1d6a 100644 --- a/src/components/ui/zcanvas/layer-sprite.js +++ b/src/components/ui/zcanvas/layer-sprite.js @@ -22,7 +22,7 @@ */ import { sprite } from "zcanvas"; import { createCanvas } from "@/utils/canvas-util"; -import { LAYER_GRAPHIC, LAYER_MASK } from "@/definitions/layer-types"; +import { LAYER_GRAPHIC, LAYER_MASK } from "@/definitions/layer-types"; class LayerSprite extends sprite { constructor( layer ) { @@ -38,7 +38,7 @@ class LayerSprite extends sprite { super({ bitmap, x, y, width, height } ); this.setDraggable( true ); - this._layer = layer; + this.layer = layer; // create brush (always as all layers can be maskable) const brushCanvas = createCanvas(); @@ -50,11 +50,11 @@ class LayerSprite extends sprite { } isDrawable() { - return this._layer.type === LAYER_GRAPHIC || this.isMaskable(); + return this.layer.type === LAYER_GRAPHIC || this.isMaskable(); } isMaskable() { - return !!this._layer.mask; + return !!this.layer.mask; } cacheGradient( color, radius = 30 ) { @@ -70,7 +70,7 @@ class LayerSprite extends sprite { const gradient = this._brushCtx.createRadialGradient( x, y, innerRadius, x, y, outerRadius ); gradient.addColorStop( 0, color ); - gradient.addColorStop( 1, 'rgba(255,255,255,0)' ); + gradient.addColorStop( 1, "rgba(255,255,255,0)" ); this._brushCtx.clearRect( 0, 0, this._brushCvs.width, this._brushCvs.height ); this._brushCtx.arc( x, y, radius, 0, 2 * Math.PI ); @@ -83,10 +83,11 @@ class LayerSprite extends sprite { // overridden from zCanvas.sprite handleMove( x, y ) { if ( !this.isDrawable() ) { + // not drawable, perform default behaviour (drag) return super.handleMove( x, y ); } - // cache this upfront - const ctx = this.isMaskable() ? this._layer.mask.getContext( "2d" ) : this._bitmap.getContext( "2d" ); + // get the drawing context, cache this upfront + const ctx = this.isMaskable() ? this.layer.mask.getContext( "2d" ) : this._bitmap.getContext( "2d" ); // note we draw onto the layer bitmap to make this permanent ctx.drawImage( this._brushCvs, x - this._halfRadius, y - this._halfRadius ); } diff --git a/src/store/modules/document-module.js b/src/store/modules/document-module.js index a2df301..0c9670f 100644 --- a/src/store/modules/document-module.js +++ b/src/store/modules/document-module.js @@ -31,13 +31,15 @@ export default { documents : [], // opened documents activeIndex: 0, // the currently active document activeLayerIndex: 0, // the currently active layer within the currently active document + maskActive: false, }, getters: { documents: state => state.documents, activeDocument: state => state.documents[ state.activeIndex ], layers: ( state, getters ) => getters.activeDocument?.layers, - activeLayer: ( state, getters ) => getters.layers?.[ state.activeLayerIndex ], activeLayerIndex: state => state.activeLayerIndex, + activeLayer: ( state, getters ) => getters.layers?.[ state.activeLayerIndex ], + activeLayerMask: ( state, getters ) => ( state.maskActive && getters.activeLayer.mask ) || null, }, mutations: { setActiveDocument( state, index ) { @@ -76,9 +78,9 @@ export default { layers.push( LayerFactory.create( opts ) ); state.activeLayerIndex = layers.length - 1; }, - removeLayer( state, layer ) { - const index = state.documents[ state.activeIndex ]?.layers.indexOf( layer ); - if ( index < 0 ) { + removeLayer( state, index ) { + const layer = state.documents[ state.activeIndex ]?.layers[ index ]; + if ( !layer ) { return; } flushLayerSprites( layer ); @@ -87,8 +89,13 @@ export default { state.activeLayerIndex = Math.max( 0, index - 1 ); } }, - setActiveLayerIndex( state, index ) { - state.activeLayerIndex = index; + setActiveLayerIndex( state, layerIndex ) { + state.activeLayerIndex = layerIndex; + state.maskActive = false; + }, + setActiveLayerMask( state, layerIndex ) { + state.activeLayerIndex = layerIndex; + state.maskActive = !!state.documents[ state.activeIndex ].layers[ layerIndex ].mask; }, updateLayer( state, { index, opts = {} }) { const layer = state.documents[ state.activeIndex ].layers[ index ]; diff --git a/tests/unit/store/modules/document-module.spec.js b/tests/unit/store/modules/document-module.spec.js index 3a6e22e..cc2f724 100644 --- a/tests/unit/store/modules/document-module.spec.js +++ b/tests/unit/store/modules/document-module.spec.js @@ -37,6 +37,11 @@ describe( "Vuex document module", () => { expect( getters.layers( state, mockedGetters )).toEqual( mockedGetters.activeDocument.layers ); }); + it( "should be able to retrieve the active Layer index", () => { + const state = { activeLayerIndex: 2 }; + expect( getters.activeLayerIndex( state )).toEqual( 2 ); + }); + it( "should be able to retrieve the active Layer for the active Document", () => { const state = { activeLayerIndex: 1 }; const mockedGetters = { @@ -45,9 +50,16 @@ describe( "Vuex document module", () => { expect( getters.activeLayer( state, mockedGetters )).toEqual( mockedGetters.layers[ 1 ]); }); - it( "should be able to retrieve the active Layer index", () => { - const state = { activeLayerIndex: 2 }; - expect( getters.activeLayerIndex( state )).toEqual( 2 ); + it( "should be able to retrieve the active Layer mask, when set", () => { + const state = { maskActive: false }; + const mockedGetters = { activeLayer: { name: "layer1" } }; + // null because mask is not active + expect( getters.activeLayerMask( state, mockedGetters )).toBeNull(); + state.maskActive = true; + // null because layer has no mask drawable + expect( getters.activeLayerMask( state, mockedGetters )).toBeNull(); + mockedGetters.activeLayer.mask = { src: "mask" }; + expect( getters.activeLayerMask( state, mockedGetters )).toEqual( mockedGetters.activeLayer.mask ); }); }); @@ -151,7 +163,7 @@ describe( "Vuex document module", () => { }; mockUpdateFn = jest.fn(); const layer = state.documents[ 0 ].layers[ 1 ]; - mutations.removeLayer( state, layer ); + mutations.removeLayer( state, 1 ); expect( state.documents[ 0 ].layers ).toEqual([ { name: "layer1" }, { name: "layer3" } ]); @@ -160,13 +172,40 @@ describe( "Vuex document module", () => { }); }); - it( "should be able to set the active layer index", () => { - const state = { - documents: [ { name: "foo", layers: [{ name: "layer1" }, { name: "layer2" }] }], - activeLayerIndex: 0 - }; - mutations.setActiveLayerIndex( state, 1 ); - expect( state.activeLayerIndex ).toEqual( 1 ); + describe( "when setting the active layer content", () => { + it( "should be able to set the active layer index", () => { + const state = { + documents: [ { name: "foo", layers: [{ name: "layer1" }, { name: "layer2" }] }], + activeLayerIndex: 0 + }; + mutations.setActiveLayerIndex( state, 1 ); + expect( state.activeLayerIndex ).toEqual( 1 ); + }); + + it( "should unset the active layer mask when setting the active layer index", () => { + const state = { + documents: [ { name: "foo", layers: [{ name: "layer1" }, { name: "layer2" }] }], + activeLayerIndex: 0, + maskActive: true, + }; + mutations.setActiveLayerIndex( state, 1 ); + expect( state.maskActive ).toBe( false ); + }); + + it( "should be able to set the active layer mask", () => { + const state = { + documents: [{ + name: "foo", + layers: [ { name: "layer1" }, { name: "layer2", mask: { src: "mask" } } ] + }], + activeIndex: 0, + activeLayerIndex: 0, + maskActive: false, + }; + mutations.setActiveLayerMask( state, 1 ); + expect( state.activeLayerIndex ).toEqual( 1 ); + expect( state.maskActive ).toBe( true ); + }); }); it( "should be able to update the options of a specific layer within the active Document", () => {