Toolbox and options panels are now collapsible. Some mobile view optimizations

This commit is contained in:
Igor Zinken
2020-12-15 14:37:48 +01:00
parent fb3cfdff5e
commit afd4532add
15 changed files with 18663 additions and 87 deletions

View File

@@ -33,13 +33,13 @@ npm run lint
# TODO / Roadmap
* Default canvas background is transparency blocks (requires zCanvas bg pattern update)
* Add zoom control to control zoom factor. Tool options should go to options-panel (duh)
* Drawable layers must be added to document (and thus be recalled when switching documents)
* Add brush options > size, transparency
* Image position must be made persistent (now isn't on document switch)
* Add layer view to options-panel: allow naming, repositioning, set as mask
* Implement selections
* Toolbox and options panel should be collapsible (keyboard shortcuts)
* Unload Blobs when images are no longer used in document (see canvas-util disposeSprite)
* Export output to image file
* Import / export documents from/to disk

18511
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"canvas": "^2.6.1",
"core-js": "^3.6.5",
"register-service-worker": "^1.7.1",
"semantic-ui-css": "^2.4.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -365,5 +365,10 @@ h1 {
position: absolute;
top: $spacing-small;
right: $spacing-medium;
cursor: pointer;
&:hover {
color: $color-1;
}
}
</style>

View File

@@ -31,7 +31,7 @@
type="button"
class="close-button"
@click="requestDocumentClose()"
>x</button>
>&#215;</button>
<div class="content" ref="canvasContainer"></div>
</template>
</div>
@@ -160,15 +160,8 @@ export default {
.canvas-wrapper {
display: inline-block;
width: 100%;
position: relative;
@include component();
.close-button {
position: absolute;
top: $spacing-small;
right: $spacing-small;
}
.content {
padding: 0;
overflow: auto;

View File

@@ -21,17 +21,24 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
<template>
<div>
<div class="options-panel-wrapper">
<h2 v-t="'optionsPanel'"></h2>
<div class="content">
<file-selector />
</div>
<div class="options-panel-wrapper">
<h2 v-if="!collapsed" v-t="'optionsPanel'"></h2>
<button
type="button"
class="close-button"
@click="collapsed = !collapsed"
>{{ collapsed ? '&larr;' : '&rarr;' }}</button>
<div
v-if="!collapsed"
class="content"
>
<file-selector />
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
import FileSelector from "./components/file-selector/file-selector";
import messages from "./messages.json";
@@ -40,6 +47,24 @@ export default {
components: {
FileSelector,
},
computed: {
...mapState([
"optionsPanelOpened",
]),
collapsed: {
get() {
return !this.optionsPanelOpened;
},
set( value ) {
this.setOptionsPanelOpened( !value );
}
},
},
methods: {
...mapMutations([
"setOptionsPanelOpened",
]),
}
};
</script>

View File

@@ -22,8 +22,16 @@
*/
<template>
<div class="toolbox-wrapper">
<h2 v-t="'toolbox'"></h2>
<div class="content">
<h2 v-if="!collapsed" v-t="'toolbox'"></h2>
<button
type="button"
class="close-button"
@click="collapsed = !collapsed"
>{{ collapsed ? '&rarr;' : '&larr;' }}</button>
<div
v-if="!collapsed"
class="content"
>
<button v-for="(button, index) in tools"
:key="button.type"
v-t="button.i18n"
@@ -36,15 +44,26 @@
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import { mapState, mapGetters, mapMutations } from "vuex";
import messages from "./messages.json";
export default {
i18n: { messages },
computed: {
...mapState([
"toolboxOpened",
]),
...mapGetters([
"activeTool",
]),
collapsed: {
get() {
return !this.toolboxOpened;
},
set( value ) {
this.setToolboxOpened( !value );
}
},
tools() {
return [
{ type: "move", i18n: "move" }, { type: "brush", i18n: "brush" }
@@ -54,6 +73,7 @@ export default {
methods: {
...mapMutations([
"setActiveTool",
"setToolboxOpened",
]),
},
};

View File

@@ -27,14 +27,14 @@ function ZoomableCanvas( opts ) {
this.setZoomFactor = function( xScale, yScale ) {
this.zoomFactor = xScale;
// we debounce this as setDimensions() is only updating size
// on render. This zoom factor logic should move into the zCanvas
// library where updateCanvasSize() takes this additional factor into account
window.requestAnimationFrame(() => {
this._canvasContext.scale( xScale, yScale );
this.invalidate(); // TODO: zCanvas.scale must invalidate() !
});
this.invalidate();
};
this.setZoomFactor( 1 );
}

View File

@@ -24,11 +24,22 @@
<div id="app">
<application-menu />
<section class="main">
<toolbox class="toolbox"/>
<div class="document-container">
<document-canvas />
<toolbox
ref="toolbox"
class="toolbox"
:class="{ 'collapsed': !toolboxOpened }"
/>
<div
class="document-container"
:style="{ 'width': docWidth }"
>
<document-canvas ref="documentCanvas" />
</div>
<options-panel class="options-panel" />
<options-panel
ref="optionsPanel"
class="options-panel"
:class="{ 'collapsed': !optionsPanelOpened }"
/>
</section>
<!-- dialog window used for information messages, alerts and confirmations -->
<dialog-window v-if="dialog"
@@ -49,17 +60,18 @@
</template>
<script>
import Vue from "vue";
import Vue from "vue";
import Vuex, { mapState, mapMutations, mapActions } from "vuex";
import VueI18n from "vue-i18n";
import ApplicationMenu from "@/components/application-menu/application-menu";
import DocumentCanvas from "@/components/document-canvas/document-canvas";
import OptionsPanel from "@/components/options-panel/options-panel";
import Toolbox from "@/components/toolbox/toolbox";
import DialogWindow from "@/components/dialog-window/dialog-window";
import VueI18n from "vue-i18n";
import ApplicationMenu from "@/components/application-menu/application-menu";
import DocumentCanvas from "@/components/document-canvas/document-canvas";
import OptionsPanel from "@/components/options-panel/options-panel";
import Toolbox from "@/components/toolbox/toolbox";
import DialogWindow from "@/components/dialog-window/dialog-window";
import { RESIZE_DOCUMENT } from "@/definitions/modal-windows";
import store from "./store";
import messages from "./messages.json";
import { isMobile } from "@/utils/environment-util";
import store from "./store";
import messages from "./messages.json";
Vue.use( Vuex );
Vue.use( VueI18n );
@@ -79,11 +91,17 @@ export default {
DialogWindow,
OptionsPanel,
},
data: () => ({
docWidth: "100%",
}),
computed: {
...mapState([
"blindActive",
"toolboxOpened",
"optionsPanelOpened",
"dialog",
"modal"
"modal",
"windowSize",
]),
activeModal() {
switch ( this.modal ) {
@@ -94,15 +112,28 @@ export default {
}
},
},
watch: {
toolboxOpened() {
this.$nextTick( this.scaleContainer );
},
optionsPanelOpened() {
this.$nextTick( this.scaleContainer );
},
},
async created() {
await this.setupServices( i18n );
// no need to remove the below as we will require it throughout the application lifteimte
window.addEventListener( "resize", this.handleResize.bind( this ));
// 640 declared in _variables.scss to be mobile threshold
if ( !isMobile() ) {
this.setToolboxOpened( true );
}
},
methods: {
...mapMutations([
"setWindowSize",
"closeModal",
"setToolboxOpened",
]),
...mapActions([
"setupServices",
@@ -110,6 +141,20 @@ export default {
handleResize() {
this.setWindowSize({ width: window.innerWidth, height: window.innerHeight });
},
/**
* Ensure the document container has optimal size. Ideally we'd like a pure
* CSS solution, but the toolbox and options panel widths are dynamic (and
* in both cases fixed), making flexbox cumbersome.
*/
scaleContainer() {
if ( isMobile() ) {
return;
}
const toolboxWidth = this.$refs.toolbox?.$el.clientWidth;
const optionsPanelWidth = this.$refs.optionsPanel?.$el.clientWidth;
this.docWidth = `calc(100% - ${toolboxWidth + optionsPanelWidth + 32}px)`;
this.$nextTick(() => this.$refs.documentCanvas?.scaleCanvas());
},
}
};
</script>
@@ -138,10 +183,13 @@ html, body {
height: 100%;
.main {
display: flex;
height: calc(100% - #{$menu-height});
padding: $spacing-medium;
@include boxSize();
height: calc(100% - #{$menu-height});
position: relative;
@include large() {
padding: $spacing-medium;
}
}
.blind {
@@ -154,17 +202,37 @@ html, body {
z-index: 400; // below overlays (see _variables.scss)
}
$toolbox-width: 150px;
$options-width: 300px;
.toolbox,
.document-container,
.options-panel {
display: inline-block;
vertical-align: top;
}
.toolbox,
.options-panel {
@include mobile() {
position: absolute;
top: $menu-height;
z-index: 2;
}
&.collapsed {
width: 45px;
min-height: 40px;
}
}
.toolbox {
width: $toolbox-width;
width: 150px;
}
.document-container {
width: calc(100% - (#{$toolbox-width + $options-width + ($spacing-medium * 2)}));
width: 100%;
margin: 0 $spacing-medium;
}
.options-panel {
width: $options-width;
width: 300px;
@include mobile() {
right: 0;
}
}
}
</style>

View File

@@ -195,6 +195,13 @@ function handleKeyDown( event ) {
}
break;
case 79: // O
if ( hasOption ) {
store.commit( "setOptionsPanelOpened", !store.state.optionsPanelOpened );
preventDefault( event );
}
break;
case 83: // S
if ( hasOption ) {
// ...
@@ -202,6 +209,13 @@ function handleKeyDown( event ) {
}
break;
case 84: // T
if ( hasOption ) {
store.commit( "setToolboxOpened", !store.state.toolboxOpened );
preventDefault( event );
}
break;
case 86: // V
// paste current selection
if ( hasOption ) {

View File

@@ -17,6 +17,8 @@ export default {
},
state: {
menuOpened: false,
toolboxOpened: false,
optionsPanelOpened: true,
blindActive: false,
dialog: null,
modal: null,
@@ -30,6 +32,12 @@ export default {
setMenuOpened( state, value ) {
state.menuOpened = !!value;
},
setToolboxOpened( state, value ) {
state.toolboxOpened = !!value;
},
setOptionsPanelOpened( state, value ) {
state.optionsPanelOpened = !!value;
},
setBlindActive( state, active ) {
state.blindActive = !!active;
},

View File

@@ -1,6 +1,7 @@
@import "_mixins";
@mixin component {
position: relative;
box-shadow: 0 0 5px rgba(0,0,0,.5);
background-color: #666;
@@ -19,4 +20,10 @@
@include boxSize();
height: calc(100% - 40px);
}
.close-button {
position: absolute;
top: $spacing-small;
right: $spacing-small;
}
}

View File

@@ -26,6 +26,8 @@ const d = window.document;
let fsToggle;
let maximizeText, minimizeText;
export const isMobile = () => window.screen.availWidth <= 640; // see _variables.scss
export const supportsFullscreen = () => !Bowser.ios;
export const setToggleButton = ( element, maximizeCopy, minimizeCopy ) => {

View File

@@ -10,6 +10,18 @@ describe( "Vuex store", () => {
expect( state.menuOpened ).toBe( true );
});
it("should be able to toggle the opened state of the toolbox", () => {
const state = { toolboxOpened: false };
mutations.setToolboxOpened( state, true );
expect( state.toolboxOpened ).toBe( true );
});
it("should be able to toggle the opened state of the options panel", () => {
const state = { optionsPanelOpened: false };
mutations.setOptionsPanelOpened( state, true );
expect( state.optionsPanelOpened ).toBe( true );
});
it("should be able to toggle the active state of the blinding layer", () => {
const state = { blindActive: false };
mutations.setBlindActive( state, true );