From 5c6b203f9cb3c763f068b972790d52a47faa35bf Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Sat, 19 Dec 2020 12:47:33 +0100 Subject: [PATCH] Made zooming consistent with ideal document scale for window size. Added keyboard shortcut for zoom --- README.md | 3 +- .../document-canvas/document-canvas.vue | 89 +++++++++++++------ .../resize-document/resize-document.vue | 2 +- .../file-menu/export-image/export-image.vue | 6 +- .../tool-options-zoom/messages.json | 4 +- .../tool-options-zoom/tool-options-zoom.vue | 44 +++++++-- .../ui/color-picker/color-picker.vue | 4 +- src/definitions/tool-types.js | 2 + src/photomound.vue | 6 +- src/services/keyboard-service.js | 18 +++- src/store/index.js | 6 +- src/store/modules/canvas-module.js | 48 ++++++++++ src/store/modules/document-module.js | 10 ++- src/utils/image-math.js | 9 ++ .../unit/store/modules/canvas-module.spec.js | 32 +++++++ .../store/modules/document-module.spec.js | 1 + tests/unit/store/store.spec.js | 7 -- tests/unit/utils/image-math.spec.js | 27 ++++++ 18 files changed, 257 insertions(+), 61 deletions(-) create mode 100644 src/store/modules/canvas-module.js create mode 100644 tests/unit/store/modules/canvas-module.spec.js create mode 100644 tests/unit/utils/image-math.spec.js diff --git a/README.md b/README.md index 1473a45..e3b6850 100644 --- a/README.md +++ b/README.md @@ -50,15 +50,16 @@ npm run lint # TODO / Roadmap * Unit tests for factories +* Zoom set original size isn't that accurate (check also on mobile views) * DrawableLayer should only draw when brush tool is active * Canvas clearRect() is not doing full width and height ? (might be related to drawable layer click color problem) +* Document resizing doesn't rescale sprites properly? * Layer masking should be linked to a target layer * Layer view in options-panel: allow naming, repositioning, toggle visibility, change type (for masking), opacity * Canvas util : store transparency of images * Restored base64 images should be treated as binary once more (see layer-factory) * scale logic should move from zoomable-canvas into zCanvas (as handleInteraction needs to transform offsets by zoom ratio, see DrawableLayer!) * adjust scaling (on widescreen images scale in the width, rather than go for full height and zoomed out mode) -* Zoom level 1 should be equal to the last ratio based rescale * Image position must be made persistent (now isn't on document switch) * Implement selections * Unload Blobs when images are no longer used in document (see sprite-factory disposeSprite, keep instance count of usages) diff --git a/src/components/document-canvas/document-canvas.vue b/src/components/document-canvas/document-canvas.vue index 1d61a07..70fecff 100644 --- a/src/components/document-canvas/document-canvas.vue +++ b/src/components/document-canvas/document-canvas.vue @@ -45,20 +45,30 @@ import { mapState, mapGetters, mapMutations, mapActions } from "vuex"; import ZoomableCanvas from "@/components/ui/zcanvas/zoomable-canvas"; import DrawableLayer from "@/components/ui/zcanvas/drawable-layer"; -import { scaleToRatio } from "@/utils/image-math"; +import { MAX_ZOOM } from "@/definitions/tool-types"; +import { scaleToRatio, scaleValue, isPortrait } from "@/utils/image-math"; import { createSpriteForLayer, runSpriteFn, flushLayerSprites, flushCache, } from "@/factories/sprite-factory"; /* internal non-reactive properties */ -let lastDocument; +let lastDocument, containerSize; // maintain a pool of sprites representing the layers within the active document // the sprites themselves are cached within the sprite-factory, this is merely // used for change detection in the current editing session (see watchers) const layerPool = new Map(); // scale of the on-screen canvas relative to the document -let xScale = 1, yScale = 1, zoom = 1, containerSize; +let xScale = 1, yScale = 1, zoom = 1, maxScale = 1; + +const MAX_IMAGE_SIZE = 8000; // in pixels, this determines the max zoom in factor +const calculateMaxScale = ( width, height ) => { + if ( isPortrait( width, height )) { + maxScale = MAX_IMAGE_SIZE / height * 100 / MAX_ZOOM; + } else { + maxScale = MAX_IMAGE_SIZE / width * 100 / MAX_ZOOM; + } +}; export default { data: () => ({ @@ -67,21 +77,21 @@ export default { }), computed: { ...mapState([ - "zCanvas", - "windowSize" + "windowSize", ]), ...mapGetters([ + "zCanvas", "activeDocument", "layers", "activeLayer", "activeTool", "zoomOptions", + "zCanvasBaseDimensions", ]), }, watch: { windowSize() { - this.cacheContainerSize(); - this.scaleCanvas(); + this.calcIdealDimensions(); }, activeDocument: { handler( document, oldValue = null ) { @@ -93,14 +103,6 @@ export default { } return; } - if ( !this.zCanvas ) { - this.createCanvas(); - this.$nextTick(() => { - this.zCanvas.insertInPage( this.$refs.canvasContainer ); - this.cacheContainerSize(); - this.scaleCanvas(); - }); - } const { id, width, height } = document; // switching between documents if ( id !== lastDocument ) { @@ -108,7 +110,16 @@ export default { flushCache(); layerPool.clear(); } - if ( this.zCanvas.width !== width || this.zCanvas.height !== height ) { + if ( !this.zCanvas ) { + this.createCanvas(); + this.$nextTick(() => { + this.zCanvas.insertInPage( this.$refs.canvasContainer ); + this.cacheContainerSize(); + this.scaleCanvas(); + }); + calculateMaxScale( width, height ); + } else if ( this.zCanvas.width !== width || this.zCanvas.height !== height ) { + calculateMaxScale( width, height ); this.scaleCanvas(); } }, @@ -140,7 +151,9 @@ export default { return; } const { id } = layer; - [ ...layerPool.entries() ].forEach(([ key, sprite ]) => sprite.setInteractive( key === id )); + [ ...layerPool.entries() ].forEach(([ key, sprite ]) => { + sprite.setInteractive( key === id ) + }); }, }, activeTool( tool ) { @@ -159,7 +172,12 @@ export default { zoomOptions: { deep: true, handler({ level }) { - zoom = level; + // are we zooming in or out (relative from the base, not necessarily the previous value) + if ( level > 0 ) { + zoom = scaleValue( level, MAX_ZOOM, maxScale - 1 ) + 1; + } else { + zoom = 1 - scaleValue( Math.abs( level ), MAX_ZOOM, 1 - ( 1 / maxScale )); + } // cache the current scroll offset so we can zoom from the current offset // note that by default we zoom from the center (when document was unscrolled) @@ -167,7 +185,7 @@ export default { const ratioX = Math.round( scrollLeft / scrollWidth ) || .5; const ratioY = Math.round( scrollTop / scrollHeight ) || .5; - // rescale canvas, note we can omit the ratio check as the ratio will remain the same + // rescale canvas, note we omit the best fit calculation as we zoom from the calculated base this.scaleCanvas( false ); // maintain relative scroll offset after rescale @@ -184,6 +202,7 @@ export default { methods: { ...mapMutations([ "setZCanvas", + "setZCanvasBaseDimensions", ]), ...mapActions([ "requestDocumentClose", @@ -202,25 +221,37 @@ export default { containerSize = this.$el.parentNode?.getBoundingClientRect(); }, /** - * Ensures the canvas fills out the available space while also maintaining - * the ratio of the document is is representing. + * Scales the canvas to dimensions corresponding to document size and zoom. + * When calculateBestFit is true, this ensures the canvas fills out the available + * space while also maintaining the ratio of the document is is representing. + * This resulting value is used as the baseline for the unzoomed level. This + * should be recalculated on window resize. */ - scaleCanvas( performRatioScale = true ) { + scaleCanvas( calculateBestFit = true ) { if ( !this.activeDocument ) { return; } - let { width, height } = this.activeDocument; - if ( performRatioScale ) { - ({ width, height } = scaleToRatio( width, height, containerSize.width, containerSize.height )); + if ( calculateBestFit ) { + const { width, height } = this.activeDocument; + const scaledSize = scaleToRatio( width, height, containerSize.width, containerSize.height ); + this.setZCanvasBaseDimensions( scaledSize ); + xScale = scaledSize.width / this.activeDocument.width; + yScale = scaledSize.height / this.activeDocument.height; } this.wrapperHeight = `${window.innerHeight - containerSize.top - 20}px`; - this.zCanvas.setDimensions( width * zoom, height * zoom, true, true ); // replace to not multiply by zoom - xScale = width / this.activeDocument.width; - yScale = height / this.activeDocument.height; + // replace below by updated zCanvas lib to not multiply by zoom + this.zCanvas.setDimensions( + this.zCanvasBaseDimensions.width * zoom, + this.zCanvasBaseDimensions.height * zoom, + true, true + ); this.zCanvas.setZoomFactor( xScale * zoom, yScale * zoom ); // replace with zCanvas.setZoom() - this.centerCanvas = this.zCanvas.getWidth() < containerSize.width || this.zCanvas.getHeight() < containerSize.height ; }, + calcIdealDimensions() { + this.cacheContainerSize(); + this.scaleCanvas(); + }, }, }; diff --git a/src/components/edit-menu/resize-document/resize-document.vue b/src/components/edit-menu/resize-document/resize-document.vue index 00e6cd4..fe1762e 100644 --- a/src/components/edit-menu/resize-document/resize-document.vue +++ b/src/components/edit-menu/resize-document/resize-document.vue @@ -26,7 +26,7 @@