mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Toolbox and options panels are now collapsible. Some mobile view optimizations
This commit is contained in:
@@ -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
18511
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
BIN
src/assets/images/document_transparent_bg.png
Normal file
BIN
src/assets/images/document_transparent_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 332 B |
@@ -365,5 +365,10 @@ h1 {
|
||||
position: absolute;
|
||||
top: $spacing-small;
|
||||
right: $spacing-medium;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $color-1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
type="button"
|
||||
class="close-button"
|
||||
@click="requestDocumentClose()"
|
||||
>x</button>
|
||||
>×</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;
|
||||
|
||||
@@ -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 ? '←' : '→' }}</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>
|
||||
|
||||
|
||||
@@ -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 ? '→' : '←' }}</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",
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
Reference in New Issue
Block a user