mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Restructured sprite factories, fixed bugs with multiple layer addition
This commit is contained in:
@@ -37,17 +37,16 @@ npm run lint
|
||||
|
||||
# TODO / Roadmap
|
||||
|
||||
* Add layer view to options-panel: allow naming, repositioning, set as mask
|
||||
* Drawable layers must be added to document (and thus be recalled when switching documents)
|
||||
* Layer view in options-panel: allow naming, repositioning, set as mask
|
||||
* Drawable layer bitmap content must be added to layer.bitmap (and thus be recalled when switching documents)
|
||||
* Add brush options > size, transparency
|
||||
* opening image as new layer doesn't do proper sizing
|
||||
* scale logic should move from zoomable-canvas into zCanvas (as handleInteraction needs to transform offsets by zoom ratio, see drawable-layer!)
|
||||
* 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)
|
||||
* Default canvas background should be transparency blocks (requires zCanvas bg pattern update or just a lowest render layer that isn't part of the document)
|
||||
* Zoom in should be center based
|
||||
* 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 canvas-util disposeSprite, keep instance count of usages)
|
||||
* Unload Blobs when images are no longer used in document (see sprite-factory disposeSprite, keep instance count of usages)
|
||||
* Export output to image file
|
||||
* Import / export documents from/to disk|Dropbox
|
||||
* Restore project by selecting folder from file system
|
||||
|
||||
@@ -47,12 +47,13 @@ import ZoomableCanvas from "@/components/ui/zcanvas/zoomable-canvas";
|
||||
import DrawableLayer from "@/components/ui/zcanvas/drawable-layer";
|
||||
import { scaleToRatio } from "@/utils/image-math";
|
||||
import {
|
||||
createSpriteForGraphic, runSpriteFn, flushSpritesInLayer, flushCache,
|
||||
} from "@/utils/canvas-util";
|
||||
createSpriteForLayer, runSpriteFn, flushLayerSprites, flushCache,
|
||||
} from "@/factories/sprite-factory";
|
||||
|
||||
/* internal non-reactive properties */
|
||||
|
||||
let lastDocument, drawableLayer;
|
||||
let lastDocument;
|
||||
const layerPool = new Map();
|
||||
// scale of the on-screen canvas relative to the document
|
||||
let xScale = 1, yScale = 1, zoom = 1, containerSize;
|
||||
|
||||
@@ -68,6 +69,7 @@ export default {
|
||||
]),
|
||||
...mapGetters([
|
||||
"activeDocument",
|
||||
"layers",
|
||||
"activeTool",
|
||||
"zoomOptions",
|
||||
]),
|
||||
@@ -78,7 +80,6 @@ export default {
|
||||
this.scaleCanvas();
|
||||
},
|
||||
activeDocument: {
|
||||
deep: true,
|
||||
handler( document, oldValue = null ) {
|
||||
if ( !document?.layers ) {
|
||||
if ( this.zCanvas ) {
|
||||
@@ -95,22 +96,35 @@ export default {
|
||||
this.scaleCanvas();
|
||||
});
|
||||
}
|
||||
const { name, width, height } = document;
|
||||
if ( name !== lastDocument ) {
|
||||
lastDocument = name;
|
||||
const { id, width, height } = document;
|
||||
if ( id !== lastDocument ) {
|
||||
lastDocument = id;
|
||||
flushCache(); // switching between documents
|
||||
}
|
||||
if ( this.zCanvas.width !== width || this.zCanvas.height !== height ) {
|
||||
this.scaleCanvas();
|
||||
}
|
||||
document.layers.forEach( layer => {
|
||||
},
|
||||
},
|
||||
layers: {
|
||||
deep: true,
|
||||
handler( layers ) {
|
||||
const seen = [];
|
||||
layers?.forEach( layer => {
|
||||
if ( !layer.visible ) {
|
||||
flushSpritesInLayer( layer );
|
||||
flushLayerSprites( layer );
|
||||
return;
|
||||
}
|
||||
layer.graphics.forEach( graphic => {
|
||||
const sprite = createSpriteForGraphic( this.zCanvas, graphic );
|
||||
});
|
||||
if ( !layerPool.has( layer.id )) {
|
||||
const sprite = createSpriteForLayer( this.zCanvas, layer );
|
||||
layerPool.set( layer.id, sprite );
|
||||
}
|
||||
seen.push( layer.id );
|
||||
});
|
||||
[ ...layerPool.keys() ].filter( id => !seen.includes( id )).forEach( id => {
|
||||
console.warn("remove" + id);
|
||||
flushLayerSprites( layerPool.get( id ));
|
||||
layerPool.delete( id );
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -123,10 +137,6 @@ export default {
|
||||
isDraggable = true;
|
||||
break;
|
||||
case "brush":
|
||||
if ( !drawableLayer ) {
|
||||
drawableLayer = new DrawableLayer( this.activeDocument );
|
||||
this.zCanvas.addChild( drawableLayer );
|
||||
}
|
||||
break;
|
||||
}
|
||||
runSpriteFn( sprite => sprite.setDraggable( isDraggable || sprite instanceof DrawableLayer ));
|
||||
|
||||
@@ -243,6 +243,6 @@ export default {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
vertical-align: top;
|
||||
@include textOverflow();
|
||||
@include truncate();
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
v-t="'loginToDropbox'"
|
||||
type="button"
|
||||
class="button dropbox"
|
||||
@click="login"
|
||||
@click="login()"
|
||||
></button>
|
||||
</template>
|
||||
<template v-if="authenticated || awaitingConnection">
|
||||
@@ -37,7 +37,7 @@
|
||||
type="button"
|
||||
class="button dropbox"
|
||||
:disabled="awaitingConnection"
|
||||
@click="openFileBrowser"
|
||||
@click="openFileBrowser()"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ export default {
|
||||
}),
|
||||
computed: {
|
||||
awaitingConnection() {
|
||||
return !this.isauthenticated && !this.authUrl;
|
||||
return !this.authenticated && !this.authUrl;
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
@@ -76,6 +76,7 @@ export default {
|
||||
window.dropboxClientId || localStorage?.getItem( "dropboxClientId" ),
|
||||
window.dropboxRedirect || `${window.location.href}login.html`
|
||||
);
|
||||
this.login();
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
@@ -101,6 +102,7 @@ export default {
|
||||
}
|
||||
},
|
||||
openFileBrowser() {
|
||||
console.warn("alstu?");
|
||||
this.openModal( DROPBOX_FILE_SELECTOR );
|
||||
},
|
||||
showConnectionMessage() {
|
||||
|
||||
@@ -73,7 +73,11 @@ export default {
|
||||
"setActiveLayerIndex",
|
||||
]),
|
||||
requestLayerAdd() {
|
||||
this.addLayer( this.$t( "newLayerNum", { num: this.layers.length + 1 }));
|
||||
this.addLayer({
|
||||
name: this.$t( "newLayerNum", { num: this.layers.length + 1 }),
|
||||
width: this.activeDocument.width,
|
||||
height: this.activeDocument.height
|
||||
});
|
||||
},
|
||||
requestLayerRemove() {
|
||||
this.removeLayer( this.activeLayer );
|
||||
@@ -93,6 +97,7 @@ h3 {
|
||||
.layer-list {
|
||||
padding: $spacing-small 0;
|
||||
@include boxSize();
|
||||
@include truncate();
|
||||
border-top: 1px solid $color-lines;
|
||||
border-bottom: 1px solid $color-lines;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from "vuex";
|
||||
import Slider from "@/components/ui/slider/slider";
|
||||
import tools from "@/definitions/tools";
|
||||
import messages from "./messages.json";
|
||||
import Slider from "@/components/ui/slider/slider";
|
||||
import ToolTypes from "@/definitions/tool-types";
|
||||
import messages from "./messages.json";
|
||||
|
||||
const MAX_ZOOM = 3;
|
||||
|
||||
@@ -59,7 +59,7 @@ export default {
|
||||
},
|
||||
set( value ) {
|
||||
this.setToolOptionValue({
|
||||
tool: tools.ZOOM,
|
||||
tool: ToolTypes.ZOOM,
|
||||
option: "level",
|
||||
value: ( value / this.max ) * MAX_ZOOM
|
||||
});
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
:key="button.type"
|
||||
v-t="button.i18n"
|
||||
class="tool-button"
|
||||
:class="{ 'active': activeTool === button.type }"
|
||||
:class="{
|
||||
'active': activeTool === button.type
|
||||
}"
|
||||
:disabled="button.disabled"
|
||||
@click="setActiveTool( button.type )"
|
||||
></button>
|
||||
</div>
|
||||
@@ -45,6 +48,8 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapMutations } from "vuex";
|
||||
import { LAYER_GRAPHIC } from "@/definitions/image-types";
|
||||
import ToolTypes from "@/definitions/tool-types";
|
||||
import messages from "./messages.json";
|
||||
|
||||
export default {
|
||||
@@ -55,6 +60,8 @@ export default {
|
||||
]),
|
||||
...mapGetters([
|
||||
"activeTool",
|
||||
"activeDocument",
|
||||
"activeLayer",
|
||||
]),
|
||||
collapsed: {
|
||||
get() {
|
||||
@@ -66,11 +73,34 @@ export default {
|
||||
},
|
||||
tools() {
|
||||
return [
|
||||
{ type: "move", i18n: "move" }, { type: "zoom", i18n: "zoom" },
|
||||
{ type: "brush", i18n: "brush" }
|
||||
{ type: ToolTypes.MOVE, i18n: "move", disabled: !this.activeDocument },
|
||||
{ type: ToolTypes.ZOOM, i18n: "zoom", disabled: !this.activeDocument },
|
||||
{ type: ToolTypes.BRUSH, i18n: "brush", disabled: !this.activeLayer || this.activeLayer.type !== LAYER_GRAPHIC }
|
||||
]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDocument( document ) {
|
||||
if ( !document ) {
|
||||
this.setActiveTool( null );
|
||||
}
|
||||
},
|
||||
activeLayer( layer ) {
|
||||
if ( !layer ) {
|
||||
return;
|
||||
}
|
||||
switch ( layer.type ) {
|
||||
default:
|
||||
// brushing only allowed on graphic type layers
|
||||
if ( this.activeTool === ToolTypes.BRUSH ) {
|
||||
this.setActiveTool( ToolTypes.MOVE );
|
||||
}
|
||||
break;
|
||||
case LAYER_GRAPHIC:
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"setActiveTool",
|
||||
|
||||
@@ -24,6 +24,7 @@ import { sprite } from "zcanvas";
|
||||
import { createCanvas } from "@/utils/canvas-util";
|
||||
|
||||
function DrawableLayer({ width, height }) {
|
||||
console.warn(width,height);
|
||||
const { cvs, ctx } = createCanvas( width, height );
|
||||
const opts = {
|
||||
bitmap: cvs, x: 0, y: 0, width, height
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2019-2020 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
const JPEG = "image/jpeg";
|
||||
const PNG = "image/png";
|
||||
const GIF = "image/gif";
|
||||
|
||||
@@ -20,26 +20,6 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
let UID_COUNTER = 0;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Creates a graphic to be displayed within a layer of a Document
|
||||
*
|
||||
* @param {HTMLImageElement} bitmap
|
||||
* @param {Number=} x position of the bitmap within the layer
|
||||
* @param {Number=} y position of the bitmap within the layer
|
||||
* @param {Number=} width of the bitmap, defaults to actual bitmap width
|
||||
* @param {Number=} height of the bitmap, defaults to actual bitmap width
|
||||
*/
|
||||
create( bitmap, x = 0, y = 0, width = -1, height = -1 ) {
|
||||
if ( width === -1 || height === -1 ) {
|
||||
({ width, height } = bitmap);
|
||||
}
|
||||
const transparent = true; // TODO: MIME check
|
||||
return {
|
||||
id: `graphic_${( ++UID_COUNTER )}`,
|
||||
bitmap, x, y, width, height, transparent
|
||||
};
|
||||
},
|
||||
};
|
||||
export const LAYER_GRAPHIC = "graphic";
|
||||
export const LAYER_IMAGE = "image";
|
||||
export const LAYER_MASK = "mask";
|
||||
@@ -20,6 +20,8 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
let UID_COUNTER = 0;
|
||||
|
||||
import LayerFactory from "./layer-factory";
|
||||
|
||||
export default {
|
||||
@@ -27,12 +29,15 @@ export default {
|
||||
* Creates a new Document (project which contains
|
||||
* all layers and image content)
|
||||
*/
|
||||
create( name = "New document", width = 400, height = 300 ) {
|
||||
create({
|
||||
name = "New document", width = 400, height = 300
|
||||
} = {}) {
|
||||
return {
|
||||
id: `doc_${( ++UID_COUNTER )}`,
|
||||
name,
|
||||
width,
|
||||
height,
|
||||
layers: [ LayerFactory.create() ],
|
||||
layers: [ LayerFactory.create({ width, height }) ],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,16 +20,64 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export default {
|
||||
import {
|
||||
LAYER_GRAPHIC, LAYER_IMAGE, LAYER_MASK
|
||||
} from "@/definitions/layer-types";
|
||||
|
||||
let UID_COUNTER = 0;
|
||||
|
||||
const LayerFactory = {
|
||||
/**
|
||||
* Creates a new layer for use within a Document
|
||||
*/
|
||||
create( name = "New Layer", graphics = [] ) {
|
||||
create({
|
||||
name = "New Layer",
|
||||
type = LAYER_GRAPHIC, bitmap = null,
|
||||
x = 0, y = 0, width = 1, height = 1, visible = true
|
||||
} = {}) {
|
||||
return {
|
||||
id: `layer_${( ++UID_COUNTER )}`,
|
||||
name,
|
||||
graphics,
|
||||
masks: [],
|
||||
visible: true
|
||||
type,
|
||||
bitmap,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
visible
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Saving layer properties into a simplified JSON structure
|
||||
* for project storage
|
||||
*/
|
||||
save( layer ) {
|
||||
return {
|
||||
n: layer.name,
|
||||
t: layer.type,
|
||||
b: null, // TODO how to serialize layer Bitmap data?
|
||||
x: layer.x,
|
||||
y: layer.y,
|
||||
w: layer.width,
|
||||
h: layer.height,
|
||||
v: layer.visible,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Creating a new layer instance from a stored layer structure
|
||||
* inside a stored project
|
||||
*/
|
||||
load( layer ) {
|
||||
// TODO how to deserialize layer Bitmap data?
|
||||
return LayerFactory.create({
|
||||
name: layer.n, type: layer.t,
|
||||
bitmap: layer.b,
|
||||
x: layer.x, y: layer.y,
|
||||
width: layer.w, height: layer.h,
|
||||
visible: layer.v
|
||||
});
|
||||
}
|
||||
};
|
||||
export default LayerFactory;
|
||||
|
||||
100
src/factories/sprite-factory.js
Normal file
100
src/factories/sprite-factory.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020 - https://www.igorski.nl
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { sprite } from "zcanvas";
|
||||
import DrawableLayer from "@/components/ui/zcanvas/drawable-layer";
|
||||
import { LAYER_IMAGE, LAYER_GRAPHIC, LAYER_MASK } from "@/definitions/layer-types";
|
||||
|
||||
/**
|
||||
* Sprites are used to represent layer content. These are mapped
|
||||
* to the layer ids (see layer-factory.js)
|
||||
*/
|
||||
const spriteCache = new Map();
|
||||
|
||||
/**
|
||||
* Runs given fn on each Sprite in the cache
|
||||
*/
|
||||
export const runSpriteFn = fn => {
|
||||
spriteCache.forEach( fn );
|
||||
};
|
||||
|
||||
/**
|
||||
* If a layer were to be removed / set to invisible, we
|
||||
* flush all its cached Sprites.
|
||||
*/
|
||||
export const flushLayerSprites = ({ id }) => {
|
||||
console.warn( `flushing sprite for "${id}"` );
|
||||
if ( hasSpriteForLayer({ id })) {
|
||||
disposeSprite( spriteCache.get( id ));
|
||||
spriteCache.delete( id );
|
||||
}
|
||||
};
|
||||
|
||||
export const hasSpriteForLayer = ({ id }) => {
|
||||
return spriteCache.has( id );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the entire cache and disposes all Sprites.
|
||||
*/
|
||||
export const flushCache = () => {
|
||||
console.warn( "flushing sprite cache" );
|
||||
spriteCache.forEach( disposeSprite );
|
||||
spriteCache.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazily retrieve / create a cached sprite to represent given
|
||||
* layer content on given zCanvas instance
|
||||
*/
|
||||
export const createSpriteForLayer = ( zCanvasInstance, { id, type, bitmap, x, y, width, height }) => {
|
||||
let output;
|
||||
if ( hasSpriteForLayer({ id })) {
|
||||
output = spriteCache.get( id );
|
||||
}
|
||||
// lazily create sprite
|
||||
if ( !output ) {
|
||||
switch ( type ) {
|
||||
default:
|
||||
case LAYER_GRAPHIC:
|
||||
output = new DrawableLayer({ width, height });
|
||||
break;
|
||||
case LAYER_IMAGE:
|
||||
output = new sprite({
|
||||
bitmap, x, y, width, height
|
||||
});
|
||||
break;
|
||||
}
|
||||
output.setDraggable( true );
|
||||
zCanvasInstance.addChild( output );
|
||||
spriteCache.set( id, output );
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/* internal methods */
|
||||
|
||||
function disposeSprite( sprite ) {
|
||||
console.warn( "disposing sprite" );
|
||||
sprite?.dispose();
|
||||
// TODO: also free associated bitmap ?
|
||||
}
|
||||
@@ -20,7 +20,8 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import{ mapGetters, mapMutations, mapActions } from "vuex";
|
||||
import { mapGetters, mapMutations, mapActions } from "vuex";
|
||||
import { LAYER_IMAGE } from "@/definitions/layer-types";
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
@@ -35,7 +36,7 @@ export default {
|
||||
"setActiveDocumentSize",
|
||||
"addNewDocument",
|
||||
"addLayer",
|
||||
"addGraphicToLayer",
|
||||
"updateLayer",
|
||||
]),
|
||||
...mapActions([
|
||||
"addImage",
|
||||
@@ -45,27 +46,36 @@ export default {
|
||||
|
||||
image.src = source;
|
||||
|
||||
const currentDocumentIsEmpty = this.layers?.length === 1 && !this.layers[ 0 ].graphics.length;
|
||||
|
||||
const currentDocumentIsEmpty = this.layers?.length <= 1 && this.layers?.[0]?.bitmap === null;
|
||||
const layerOpts = {
|
||||
bitmap: image,
|
||||
type: LAYER_IMAGE,
|
||||
name: file.name,
|
||||
...size,
|
||||
};
|
||||
switch ( this.fileTarget) {
|
||||
default:
|
||||
case "layer":
|
||||
// if this is the first content of an existing document, scale document to image size
|
||||
if ( currentDocumentIsEmpty ) {
|
||||
this.setActiveDocumentSize( size );
|
||||
} else if ( !this.activeDocument ) {
|
||||
if ( !this.activeDocument ) {
|
||||
this.addNewDocument( file.name );
|
||||
return this.updateSizeAndLayer( size, layerOpts );
|
||||
} else if ( currentDocumentIsEmpty ) {
|
||||
this.setActiveDocumentSize( size );
|
||||
}
|
||||
this.addLayer( file.name );
|
||||
this.addLayer( layerOpts );
|
||||
break;
|
||||
case "document":
|
||||
if ( !currentDocumentIsEmpty ) {
|
||||
this.addNewDocument( file.name );
|
||||
}
|
||||
this.setActiveDocumentSize( size );
|
||||
this.updateSizeAndLayer( size, layerOpts );
|
||||
break;
|
||||
}
|
||||
this.addGraphicToLayer({ index: this.layers.length - 1, bitmap: image, size });
|
||||
},
|
||||
}
|
||||
updateSizeAndLayer( size, opts ) {
|
||||
this.setActiveDocumentSize( size );
|
||||
this.updateLayer({ index: this.layers.length - 1, opts });
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { createCanvas } from './canvas-util';
|
||||
import { createCanvas } from "@/utils/canvas-util";
|
||||
|
||||
const { URL } = window;
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ import Vue from "vue";
|
||||
|
||||
import DocumentFactory from "@/factories/document-factory";
|
||||
import LayerFactory from "@/factories/layer-factory";
|
||||
import GraphicFactory from "@/factories/graphic-factory";
|
||||
import { flushSpritesInLayer } from "@/utils/canvas-util";
|
||||
import { flushLayerSprites } from "@/factories/sprite-factory";
|
||||
|
||||
export default {
|
||||
state: {
|
||||
@@ -50,7 +49,7 @@ export default {
|
||||
document.height = height;
|
||||
},
|
||||
addNewDocument( state, name ) {
|
||||
state.documents.push( DocumentFactory.create( name ));
|
||||
state.documents.push( DocumentFactory.create({ name }));
|
||||
state.activeIndex = state.documents.length - 1;
|
||||
},
|
||||
closeActiveDocument( state ) {
|
||||
@@ -59,13 +58,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
// free allocated resources
|
||||
document.layers.forEach( layer => flushSpritesInLayer( layer ));
|
||||
document.layers.forEach( layer => flushLayerSprites( layer ));
|
||||
Vue.delete( state.documents, state.activeIndex );
|
||||
state.activeIndex = Math.min( state.documents.length - 1, state.activeIndex );
|
||||
},
|
||||
addLayer( state, optName ) {
|
||||
addLayer( state, opts ) {
|
||||
const layers = state.documents[ state.activeIndex ].layers;
|
||||
layers.unshift( LayerFactory.create( optName ) );
|
||||
layers.unshift( LayerFactory.create( opts ) );
|
||||
state.activeLayerIndex = 0;
|
||||
},
|
||||
removeLayer( state, layer ) {
|
||||
@@ -73,16 +72,18 @@ export default {
|
||||
if ( index < 0 ) {
|
||||
return;
|
||||
}
|
||||
flushSpritesInLayer( layer );
|
||||
flushLayerSprites( layer );
|
||||
Vue.delete( state.documents[ state.activeIndex ].layers, index );
|
||||
},
|
||||
setActiveLayerIndex( state, index ) {
|
||||
state.activeLayerIndex = index;
|
||||
},
|
||||
addGraphicToLayer( state, { index, bitmap, size = {} }) {
|
||||
state.documents[ state.activeIndex ].layers[ index ]?.graphics.push(
|
||||
GraphicFactory.create( bitmap, 0, 0, size.width, size.height )
|
||||
);
|
||||
updateLayer( state, { index, opts = {} }) {
|
||||
const layer = state.documents[ state.activeIndex ].layers[ index ];
|
||||
Vue.set( state.documents[ state.activeIndex ].layers, index, {
|
||||
...layer,
|
||||
...opts
|
||||
});
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
||||
@@ -20,20 +20,20 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import Vue from "vue";
|
||||
import tools from "@/definitions/tools";
|
||||
import Vue from "vue";
|
||||
import ToolTypes from "@/definitions/tool-types";
|
||||
|
||||
export default {
|
||||
state: {
|
||||
activeTool: tools.MOVE,
|
||||
activeTool: null,
|
||||
options: {
|
||||
[ tools.ZOOM ] : { level: 1 },
|
||||
[ tools.BRUSH ]: { },
|
||||
[ ToolTypes.ZOOM ] : { level: 1 },
|
||||
[ ToolTypes.BRUSH ]: { },
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
activeTool: state => state.activeTool,
|
||||
zoomOptions: state => state.options[ tools.ZOOM ],
|
||||
zoomOptions: state => state.options[ ToolTypes.ZOOM ],
|
||||
},
|
||||
mutations: {
|
||||
setActiveTool( state, tool ) {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@mixin textOverflow {
|
||||
@mixin truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -20,13 +20,6 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { sprite } from "zcanvas";
|
||||
|
||||
/**
|
||||
* Sprites are used to represend graphics. These are mapped
|
||||
* to the graphic ids (see graphic-factory.js)
|
||||
*/
|
||||
const spriteCache = new Map();
|
||||
|
||||
/**
|
||||
* Creates a new HTMLCanvasElement, returning both
|
||||
@@ -42,61 +35,3 @@ export const createCanvas = ( optWidth = 0, optHeight = 0 ) => {
|
||||
}
|
||||
return { cvs, ctx };
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs given fn on each Sprite in the cache
|
||||
*/
|
||||
export const runSpriteFn = fn => {
|
||||
spriteCache.forEach( fn );
|
||||
};
|
||||
|
||||
/**
|
||||
* If a layer were to be removed / set to invisible, we
|
||||
* flush all its cached Sprites.
|
||||
*/
|
||||
export const flushSpritesInLayer = layer => {
|
||||
console.warn("flushing sprites in layer");
|
||||
layer.graphics.forEach(({ id }) => {
|
||||
if ( spriteCache.has( id )) {
|
||||
disposeSprite( spriteCache.get( id ));
|
||||
spriteCache.delete( id );
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the entire cache and disposes all Sprites.
|
||||
*/
|
||||
export const flushCache = () => {
|
||||
console.warn("flushing cache");
|
||||
spriteCache.forEach( disposeSprite );
|
||||
spriteCache.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazily retrieve / create a cached sprite to represent given
|
||||
* graphic on given zCanvas instance
|
||||
*/
|
||||
export const createSpriteForGraphic = ( zCanvasInstance, { id, bitmap, x, y, width, height }) => {
|
||||
let output;
|
||||
if ( spriteCache.has( id )) {
|
||||
output = spriteCache.get( id );
|
||||
}
|
||||
// lazily create sprite
|
||||
if ( !output ) {
|
||||
output = new sprite({
|
||||
bitmap, x, y, width, height
|
||||
});
|
||||
output.setDraggable( true );
|
||||
zCanvasInstance.addChild( output );
|
||||
spriteCache.set( id, output );
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/* internal methods */
|
||||
|
||||
function disposeSprite( sprite ) {
|
||||
sprite?.dispose();
|
||||
// TODO: also free associated bitmap ?
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import { createCanvas } from './canvas-util';
|
||||
import { createCanvas } from "@/utils/canvas-util";
|
||||
|
||||
const { URL } = window;
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import DocumentModule from "@/store/modules/document-module";
|
||||
import GraphicFactory from "@/factories/graphic-factory";
|
||||
import LayerFactory from "@/factories/layer-factory";
|
||||
import DocumentModule from "@/store/modules/document-module";
|
||||
import { LAYER_IMAGE } from "@/definitions/layer-types";
|
||||
|
||||
const { getters, mutations } = DocumentModule;
|
||||
|
||||
let mockUpdateFn;
|
||||
jest.mock("@/utils/canvas-util", () => ({
|
||||
flushSpritesInLayer: (...args) => mockUpdateFn?.( "flushSpritesInLayer", ...args ),
|
||||
jest.mock( "@/factories/sprite-factory", () => ({
|
||||
flushLayerSprites: (...args) => mockUpdateFn?.( "flushLayerSprites", ...args ),
|
||||
}));
|
||||
jest.mock( "@/factories/layer-factory", () => ({
|
||||
create: (...args) => mockUpdateFn?.( "create", ...args ),
|
||||
}));
|
||||
|
||||
describe( "Vuex document module", () => {
|
||||
@@ -81,9 +83,9 @@ describe( "Vuex document module", () => {
|
||||
});
|
||||
|
||||
it( "should be able to close the active Document", () => {
|
||||
const layer1 = LayerFactory.create();
|
||||
const layer2 = LayerFactory.create();
|
||||
const layer3 = LayerFactory.create();
|
||||
const layer1 = { name: "layer1" };
|
||||
const layer2 = { name: "layer2" };
|
||||
const layer3 = { name: "layer3" };
|
||||
const state = {
|
||||
documents: [
|
||||
{ name: "foo", layers: [ layer1 ] },
|
||||
@@ -95,8 +97,8 @@ describe( "Vuex document module", () => {
|
||||
mutations.closeActiveDocument( state );
|
||||
expect( state.documents ).toEqual([ { name: "foo", layers: [ layer1 ] }]);
|
||||
expect( state.activeIndex ).toEqual( 0 );
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushSpritesInLayer", layer2 );
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "flushSpritesInLayer", layer3 );
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushLayerSprites", layer2 );
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 2, "flushLayerSprites", layer3 );
|
||||
});
|
||||
|
||||
describe( "when adding layers", () => {
|
||||
@@ -105,8 +107,13 @@ describe( "Vuex document module", () => {
|
||||
documents: [ { name: "foo", layers: [] } ],
|
||||
activeIndex: 0
|
||||
};
|
||||
mutations.addLayer( state );
|
||||
expect( state.documents[ 0 ].layers ).toEqual([ LayerFactory.create() ]);
|
||||
const mockLayer = { name: "bar" };
|
||||
mockUpdateFn = jest.fn(() => mockLayer );
|
||||
const opts = { name: "baz", width: 50, height: 100 };
|
||||
mutations.addLayer( state, opts );
|
||||
// assert LayerFactory is invoked with provided opts when calling addLayer()
|
||||
expect( mockUpdateFn ).toHaveBeenCalledWith( "create", opts );
|
||||
expect( state.documents[ 0 ].layers ).toEqual([ mockLayer ]);
|
||||
});
|
||||
|
||||
it( "should update the active layer index to the last added layers index", () => {
|
||||
@@ -124,9 +131,9 @@ describe( "Vuex document module", () => {
|
||||
documents: [ { name: "foo", layers: [{ name: "layer1" }] }],
|
||||
activeIndex: 0
|
||||
};
|
||||
mutations.addLayer( state, "layer2" );
|
||||
mutations.addLayer( state, { name: "layer2" });
|
||||
expect( state.documents[ 0 ].layers ).toEqual([
|
||||
LayerFactory.create( "layer2" ), { name: "layer1" }
|
||||
expect.any( Object ), { name: "layer1" }
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -145,7 +152,7 @@ describe( "Vuex document module", () => {
|
||||
expect( state.documents[ 0 ].layers ).toEqual([
|
||||
{ name: "layer1" }, { name: "layer3" }
|
||||
]);
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushSpritesInLayer", layer );
|
||||
expect( mockUpdateFn ).toHaveBeenNthCalledWith( 1, "flushLayerSprites", layer );
|
||||
});
|
||||
|
||||
it( "should be able to set the active layer index", () => {
|
||||
@@ -157,18 +164,32 @@ describe( "Vuex document module", () => {
|
||||
expect( state.activeLayerIndex ).toEqual( 1 );
|
||||
});
|
||||
|
||||
it( "should be able to add a Graphic to a specific layer within the active Document", () => {
|
||||
it( "should be able to update the options of a specific layer within the active Document", () => {
|
||||
const layer1 = { name: "layer1" };
|
||||
const layer2 = { name: "layer2" };
|
||||
const state = {
|
||||
documents: [{
|
||||
name: "foo",
|
||||
layers: [ LayerFactory.create( "layer1" ), LayerFactory.create( "layer2" ) ]
|
||||
layers: [ layer1, layer2 ]
|
||||
}],
|
||||
activeIndex: 0
|
||||
};
|
||||
const index = 1;
|
||||
const bitmap = { name: "bar" };
|
||||
mutations.addGraphicToLayer( state, { index, bitmap });
|
||||
expect( state.documents[ 0 ].layers[ index ].graphics ).toEqual( expect.any( Object ));
|
||||
const index = 1;
|
||||
const opts = {
|
||||
name: "layer2 updated",
|
||||
x: 100,
|
||||
y: 200,
|
||||
bitmap: new Image(),
|
||||
width: 100,
|
||||
height: 150,
|
||||
type: LAYER_IMAGE
|
||||
};
|
||||
mutations.updateLayer( state, { index, opts });
|
||||
expect( state.documents[ 0 ].layers[ index ] ).toEqual({
|
||||
id: layer2.id,
|
||||
visible: layer2.visible,
|
||||
...opts
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user