mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Loaded image files now appear on screen
This commit is contained in:
15
README.md
15
README.md
@@ -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
|
||||
|
||||
18
src/App.vue
18
src/App.vue
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
55
src/components/document-canvas/document-canvas.vue
Normal file
55
src/components/document-canvas/document-canvas.vue
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
13
src/factories/document-factory.js
Normal file
13
src/factories/document-factory.js
Normal 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() ],
|
||||
};
|
||||
},
|
||||
};
|
||||
21
src/factories/graphic-factory.js
Normal file
21
src/factories/graphic-factory.js
Normal 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
|
||||
};
|
||||
},
|
||||
};
|
||||
12
src/factories/layer-factory.js
Normal file
12
src/factories/layer-factory.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Creates a new layer within a Document
|
||||
*/
|
||||
export default {
|
||||
create( name = 'New Layer', graphics = [] ) {
|
||||
return {
|
||||
name,
|
||||
graphics,
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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' );
|
||||
|
||||
@@ -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: {
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
25
src/store/modules/document-module.js
Normal file
25
src/store/modules/document-module.js
Normal 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: {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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 ]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user