Loaded image files now appear on screen

This commit is contained in:
Igor Zinken
2020-12-10 23:20:37 +01:00
parent 02df19de7b
commit dcecf2fdc5
14 changed files with 199 additions and 65 deletions

View File

@@ -25,5 +25,16 @@ npm run test
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
# TODO / Roadmap
* Load Blobs when about to use image in document
* Unload Blobs when images are no longer used in document
* Change layer order
* Save output to file
* Use magnification (document size is relative to available screen size)
* Add zoom
* Restore project by selecting folder from file system
* Transformation matrix (old Cropbox)
* Paint brush
* Clone brush
* Full screen mode

View File

@@ -2,17 +2,20 @@
<div id="app">
<h1 v-t="'nicephore'"></h1>
<file-selector />
<canvas />
<document-canvas />
</div>
</template>
<script>
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import FileSelector from '@/components/file-selector/file-selector';
import Canvas from '@/components/canvas/canvas';
import messages from './messages.json';
import Vue from "vue";
import Vuex from "vuex";
import VueI18n from "vue-i18n";
import FileSelector from "@/components/file-selector/file-selector";
import DocumentCanvas from "@/components/document-canvas/document-canvas";
import store from "./store";
import messages from "./messages.json";
Vue.use( Vuex );
Vue.use( VueI18n );
// Create VueI18n instance with options
@@ -22,9 +25,10 @@ const i18n = new VueI18n({
export default {
i18n,
store: new Vuex.Store( store ),
components: {
FileSelector,
Canvas,
DocumentCanvas,
},
};
</script>

View File

@@ -1,26 +0,0 @@
<template>
<div>
<canvas ref="canvasContainer" />
</div>
</template>
<script>
import { canvas } from 'zcanvas';
const width = 400, height = 300;
let zCanvas;
export default {
mounted() {
zCanvas = new canvas({
width,
height,
animate: true,
smoothing: true,
stretchToFit: false,
fps: 60
});
zCanvas.insertInPage( this.$refs.canvasContainer );
},
};
</script>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<div ref="canvasContainer" />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { canvas, sprite } from 'zcanvas';
const width = 400, height = 300;
let zCanvas;
export default {
computed: {
...mapGetters([
'document',
]),
},
watch: {
document: {
deep: true,
handler({ layers }) {
if ( !layers ) {
return;
}
layers.forEach( layer => {
if ( !layer.visible ) {
return;
}
// TODO: change detection
layer.graphics.forEach(({ bitmap, x, y, width, height }) => {
const graphic = new sprite({
bitmap, x, y, width, height
});
graphic.setDraggable( true )
zCanvas.addChild( graphic );
});
});
},
},
},
mounted() {
zCanvas = new canvas({
width,
height,
animate: true,
smoothing: true,
stretchToFit: false,
fps: 60
});
zCanvas.insertInPage( this.$refs.canvasContainer );
},
};
</script>

View File

@@ -9,8 +9,9 @@
</template>
<script>
import { loader } from 'zcanvas';
import messages from './messages.json';
import { mapGetters, mapMutations, mapActions } from "vuex";
import { loader } from "zcanvas";
import messages from "./messages.json";
const ACCEPTED_IMAGE_TYPES = [ "image/png", "image/gif", "image/jpeg" ];
@@ -19,7 +20,19 @@ export default {
data: () => ({
acceptedImageTypes: ACCEPTED_IMAGE_TYPES,
}),
computed: {
...mapGetters([
'layers',
]),
},
methods: {
...mapMutations([
"addLayer",
"addGraphicToLayer",
]),
...mapActions([
"addImage",
]),
async handleFileSelect({ target }) {
const files = target?.files;
if ( !files || files.length === 0 ) {
@@ -33,9 +46,13 @@ export default {
// load the image contents using the zCanvas.loader
// which will also provide the image dimensions
try {
const imageElement = reader.result;
const { image, size } = await loader.loadImage( imageElement);
this.addImage({ file, imageElement, size });
const imageSource = reader.result;
const { image, size } = await loader.loadImage( imageSource );
const { source } = await this.addImage({ file, image, size });
// TODO: the below is test code
image.src = source;
this.addLayer();
this.addGraphicToLayer({ index: this.layers.length - 1, bitmap: image });
} catch {
// TODO: show warning
}

View File

@@ -0,0 +1,13 @@
import LayerFactory from './layer-factory';
/**
* Creates a new Document (project which contains
* all layers and image content)
*/
export default {
create() {
return {
layers: [ LayerFactory.create() ],
};
},
};

View File

@@ -0,0 +1,21 @@
/**
* Creates a graphic to be displayed within the
* layer of a Document
*/
export default {
/**
* @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);
}
return {
bitmap, x, y, width, height
};
},
};

View File

@@ -0,0 +1,12 @@
/**
* Creates a new layer within a Document
*/
export default {
create( name = 'New Layer', graphics = [] ) {
return {
name,
graphics,
visible: true
}
},
};

View File

@@ -1,6 +1,5 @@
import Vue from 'vue';
import App from './App.vue';
import store from './store';
// igorski.nl maintains its own service worker registration, if your
// custom app requires PWA support, uncomment the following line
//import './registerServiceWorker'
@@ -8,6 +7,5 @@ import store from './store';
Vue.config.productionTip = false;
new Vue({
store,
render: h => h( App )
}).$mount( '#app' );

View File

@@ -1,12 +1,10 @@
import Vue from 'vue';
import Vuex from 'vuex';
import image from './modules/image-module';
import documentModule from './modules/document-module';
import imageModule from './modules/image-module';
Vue.use( Vuex );
export default new Vuex.Store({
export default {
modules: {
image,
documentModule,
imageModule,
},
state: {
@@ -17,7 +15,4 @@ export default new Vuex.Store({
actions: {
},
modules: {
}
});
};

View File

@@ -0,0 +1,25 @@
import Vue from 'vue';
import DocumentFactory from '@/factories/document-factory';
import LayerFactory from '@/factories/layer-factory';
import GraphicFactory from '@/factories/graphic-factory';
export default {
state: {
document: DocumentFactory.create(),
},
getters: {
document: state => state.document,
layers: state => state.document.layers,
},
mutations: {
addLayer( state ) {
state.document.layers.push( LayerFactory.create() );
},
addGraphicToLayer( state, { index, bitmap }) {
state.document.layers[ index ]?.graphics.push( GraphicFactory.create( bitmap ));
},
},
actions: {
}
}

View File

@@ -1,6 +1,11 @@
import Vue from 'vue';
import { imageToSource, disposeSource } from '@/utils/memory-util';
/**
* Image module maintains a list of local image resources (selected from file system)
* that can be used within the application. It is separate from the document-module
* as the images are not necessarily used within the document (yet)
*/
export default {
state: {
images: [],
@@ -26,9 +31,11 @@ export default {
/**
* Registers an image for use in the application.
*/
async addImage( state, { file, imageElement, size }) {
const source = await imageToSource( imageElement );
state.images.push({ file, size, source });
async addImage({ state }, { file, image, size }) {
const source = await imageToSource( image, file.type );
const imageData = { file, size, source };
state.images.push( imageData );
return imageData;
},
}
};

View File

@@ -10,14 +10,14 @@ const { URL } = window;
*
* @param {HTMLImageElement} imageElement fully loaded image
* @param {String=} type optional mime type, defaults to JPEG for photographic content
* @param {Number=} optQuality optional JPEG compression to use (when mime is JPEG)
* @param {Number=} optQuality optional JPEG compression to use (when mime is JPEG) between 0 - 1
* @return {String} Blob URL
*/
export const imageToSource = async ( imageElement, type = "image/jpeg", optQuality = 90 ) => {
export const imageToSource = ( imageElement, type = "image/jpeg", optQuality = .9 ) => {
const { cvs, ctx } = createCanvas();
cvs.width = imageElement.clientWidth;
cvs.height = imageElement.clientHeight;
cvs.width = imageElement.naturalWidth || imageElement.width;
cvs.height = imageElement.naturalHeight || imageElement.height;
ctx.drawImage( imageElement, 0, 0 );

View File

@@ -35,18 +35,20 @@ describe('Vuex image module', () => {
describe('actions', () => {
it('should be able to store an image object in the images list', async () => {
const state = { images: [] };
const image = {
const input = {
file: new Blob(),
imageElement: new Image(),
image: new Image(),
size: { width: 100, height: 100 }
};
mockUpdateFn = jest.fn(() => blobURL );
const blobURL = 'blob://foo';
await actions.addImage( state, image );
const image = await actions.addImage({ state }, input );
// assert image has been added to list
expect( state.images ).toEqual([ { file: image.file, size: image.size, source: blobURL } ]);
expect( state.images ).toEqual([ { file: input.file, size: input.size, source: blobURL } ]);
// assert image data has been allocated as Blob
expect( mockUpdateFn ).toHaveBeenCalledWith( 'imageToSource', image.imageElement );
expect( mockUpdateFn ).toHaveBeenCalledWith( 'imageToSource', input.image );
// assert return data contains allocated Blob resource
expect( image ).toEqual( state.images[ 0 ]);
});
});
});