src_js/iconolab-bundle/src/components/editor/Canvas.vue
changeset 320 81945eedc63f
child 323 55c024fc7c60
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue	Wed Feb 15 16:42:06 2017 +0100
@@ -0,0 +1,452 @@
+<template>
+    <div class="zoom">
+        <div>
+            <svg ref="svg"
+                v-bind:class="{ 'cut-canvas': true, 'canvas--rect': mode === 'rect', 'canvas--free': mode === 'free' }"
+                xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+                <image ref="image" v-if="loaded" xmlns:xlink="http://www.w3.org/1999/xlink" v-bind:xlink:href="image" x="0" y="0" />
+                <!-- This slot may contain annotation data -->
+                <slot></slot>
+                <shape-rect ref="rect" v-show="loaded &amp;&amp; mode == 'rect'"
+                    v-bind:paper="paper" v-bind:original-annotation="annotation"></shape-rect>
+                <shape-free ref="free" v-show="loaded &amp;&amp; mode == 'free'"
+                    v-bind:paper="paper" v-bind:original-annotation="annotation"></shape-free>
+            </svg>
+        </div>
+        <div class="zoomer">
+            <div class="btn-group-vertical" role="group" aria-label="...">
+                <button @click="zoomIn" type="button" class="btn btn-default"><i class="fa fa-plus" aria-hidden="true"></i></button>
+                <button @click="zoomOut" type="button" class="btn btn-default"><i class="fa fa-minus" aria-hidden="true"></i></button>
+            </div>
+        </div>
+        <div class="mode-controls">
+            <div class="btn-group" role="group" aria-label="...">
+                <button @click="mode = 'rect'" type="button" v-bind:class="{ btn: true, 'btn-default': true, 'btn-primary': mode === 'rect'}">
+                    <svg width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g><rect x="352" y="432" width="64" height="48"/><polygon points="416,352 416,96 176,96 176,160 352,160 352,352 160,352 160,32 96,32 96,96 32,96 32,160 96,160 96,416 480,416 480,352"/></g><text x="0" y="527" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Bluetip Design</text><text x="0" y="532" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
+                </button>
+                <button @click="mode = 'free'" type="button" v-bind:class="{ btn: true, 'btn-default': true, 'btn-primary': mode === 'free'}">
+                    <svg width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 30 30" xml:space="preserve"><g transform="translate(-450 -380)"><g xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M453,395c0,2.209,1.79,4,4,4c1.307,0,2.455-0.635,3.186-1.604l7.121,4.069C467.11,401.938,467,402.456,467,403    c0,2.209,1.79,4,4,4c2.209,0,4-1.791,4-4s-1.791-4-4-4c-1.307,0-2.455,0.635-3.186,1.604l-7.121-4.069    c0.196-0.473,0.307-0.99,0.307-1.534s-0.11-1.062-0.307-1.534l7.121-4.069c0.73,0.969,1.879,1.604,3.186,1.604    c2.209,0,4-1.791,4-4s-1.791-4-4-4c-2.21,0-4,1.791-4,4c0,0.544,0.11,1.062,0.307,1.534l-7.121,4.069    c-0.73-0.969-1.879-1.604-3.186-1.604C454.79,391,453,392.791,453,395z M471,400c1.654,0,3,1.346,3,3s-1.346,3-3,3s-3-1.346-3-3    S469.346,400,471,400z M471,384c1.654,0,3,1.346,3,3s-1.346,3-3,3s-3-1.346-3-3S469.346,384,471,384z M460,395    c0,1.654-1.346,3-3,3s-3-1.346-3-3s1.346-3,3-3S460,393.346,460,395z"/></g></g><text x="0" y="45" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Hea Poh Lin</text><text x="0" y="50" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
+                </button>
+            </path>
+        </div>
+    </div>
+</template>
+
+<script>
+
+    import Snap from 'snapsvg'
+    import ShapeRect from './ShapeRect.vue'
+    import ShapeFree from './ShapeFree.vue'
+
+    export default {
+        props: [
+            'image',
+        ],
+        components: {
+            shapeRect: ShapeRect,
+            shapeFree: ShapeFree
+        },
+        data() {
+            return {
+                paper: null,
+                loaded: false,
+                mode: 'rect',
+                viewport: { width: 0, height: 0 },
+                viewBox: [0 , 0, 0, 0],
+                zoomFactor: 0.1,
+                scale: 1,
+                imgMinSize: 0,
+                imageWidth: 0,
+                imageHeight: 0,
+                annotation: null
+            }
+        },
+        watch: {
+            mode: function(mode) {
+                this.reset();
+                if (mode === 'free') {
+                    this.handleDrawFree();
+                }
+                if (mode === 'rect') {
+                    this.handleDrawRect();
+                }
+            },
+            loaded: function(loaded) {
+                if (!loaded) { return; }
+
+                if (this.annotation) {
+
+                    var pieces = this.annotation.fragment.split(';');
+                    var path = pieces[0];
+                    var mode = pieces[1].toLowerCase();
+
+                    this.mode = mode;
+
+                    this.$nextTick(() => {
+                        path = this.denormalizePath(path);
+                        if (mode === 'free') {
+                            this.$refs.free.fromSVGPath(path);
+                        }
+                        if (mode === 'rect') {
+                            this.$refs.rect.fromSVGPath(path);
+                        }
+                    });
+
+                } else {
+
+                    if (this.mode === 'free') {
+                        this.handleDrawFree();
+                    }
+                    if (this.mode === 'rect') {
+                        this.handleDrawRect();
+                    }
+                }
+            }
+        },
+        mounted() {
+
+            var img = new Image();
+            img.onload = (e) => {
+
+                this.paper = new Snap(this.$refs.svg);
+
+                this.imgMinSize = Math.min(img.width, img.height);
+
+                // FIXME
+                // Image is actually NOT loaded at this step
+                setTimeout(() => {
+
+                    console.log('Viewport: %s x %s',
+                        this.paper.node.clientWidth, this.paper.node.clientHeight);
+
+                    var viewBox = [0 , 0, img.width, img.height];
+                    var viewport = {
+                        width: this.paper.node.clientWidth,
+                        height: this.paper.node.clientHeight
+                    };
+
+                    Object.assign(this, {
+                        imageWidth: img.width,
+                        imageHeight: img.height,
+                        viewBox: viewBox,
+                        viewport: viewport,
+                    });
+
+                    var handlerSize = 15 * Math.min(viewBox[2], viewBox[3]) / viewport.width;
+
+                    this.$refs.rect.handlerSize = handlerSize;
+                    this.$refs.free.handlerRadius = handlerSize / 2;
+
+                    this.paper.attr({"viewBox": this.viewBox});
+
+                    this.loaded = true;
+
+                }, 100);
+
+            }
+            img.src = this.image;
+
+        },
+        methods: {
+
+            reset: function() {
+                // Clear shapes
+                this.$refs.rect.clear();
+                this.$refs.free.clear();
+
+                // Remove event handlers
+                this.paper.unmousedown();
+                this.paper.unmousemove();
+                this.paper.unmouseup();
+                this.paper.unclick();
+            },
+
+            setAnnotation: function(annotation) {
+                this.annotation = annotation;
+            },
+
+            getCenter: function() {
+                return {
+                    x: this.viewBox[0] + (this.viewBox[2] / 2),
+                    y: this.viewBox[1] + (this.viewBox[3] / 2)
+                }
+            },
+
+            resetZoom: function() {
+                this.scale = 1;
+                this.viewBox = [0, 0, this.imageWidth, this.imageHeight];
+                this.paper.attr({ "viewBox": this.viewBox });
+            },
+
+            zoomIn: function() {
+
+                if ( this.scale === 9) { this.scale--; return; }
+
+                var center = this.getCenter();
+                var scaleFactor = this.zoomFactor * this.scale;
+
+                var viewBoxW = this.imgMinSize - (this.imgMinSize * scaleFactor);
+                var viewBoxH = viewBoxW;
+
+                const viewBoxPrev = this.viewBox.slice(0);
+
+                this.viewBox[0] = center.x - viewBoxW / 2;
+                this.viewBox[1] = center.y - viewBoxH / 2;
+                this.viewBox[2] = viewBoxW;
+                this.viewBox[3] = viewBoxH;
+
+                this.scale++;
+
+                // this.paper.attr({ "viewBox": this.viewBox });
+
+                this.$refs.rect.hideTooltip();
+                Snap.animate(
+                    viewBoxPrev, this.viewBox,
+                    (viewBox) => this.paper.attr({ "viewBox": viewBox }),
+                    300, mina.easeinout,
+                    () => this.$refs.rect.showTooltip()
+                );
+            },
+
+            zoomOut: function() {
+
+                if (this.scale === 1) { this.resetZoom(); return; }
+
+                var center = this.getCenter();
+                var scaleFactor = this.zoomFactor * (this.scale - 1);
+
+                var viewBoxW = this.imgMinSize - (this.imgMinSize * scaleFactor);
+                var viewBoxH = viewBoxW;
+
+                var topX = center.x - viewBoxW / 2;
+                var topY = center.y - viewBoxH / 2;
+
+                const viewBoxPrev = this.viewBox.slice(0);
+
+                this.viewBox[0] = topX; //deal with X and Y
+                this.viewBox[1] = topY;
+                this.viewBox[2] = viewBoxW;
+                this.viewBox[3] = viewBoxH;
+
+                this.scale--;
+
+                // this.paper.attr({ "viewBox": this.viewBox });
+
+                this.$refs.rect.hideTooltip();
+                Snap.animate(
+                    viewBoxPrev, this.viewBox,
+                    (viewBox) => this.paper.attr({ "viewBox": viewBox }),
+                    300, mina.easeinout,
+                    () => this.$refs.rect.showTooltip()
+                );
+            },
+
+            zoomOffset: function() {
+                return {
+                    x: this.viewport.width / this.viewBox[2],
+                    y: this.viewport.height / this.viewBox[3]
+                };
+            },
+
+            computeOffset: function(e) {
+                var rect = this.$refs.image.getBoundingClientRect();
+                var zoomOffset = this.zoomOffset();
+                var offsetX = (e.clientX - rect.left) / Math.min(zoomOffset.x, zoomOffset.y);
+                var offsetY  = (e.clientY - rect.top) / Math.min(zoomOffset.x, zoomOffset.y);
+
+                return { x: offsetX, y: offsetY };
+            },
+
+            computeHandlerSize: function(e) {
+                return 60 * Math.min(this.viewBox[2], this.viewBox[3]) / this.imageWidth;
+            },
+
+            normalizePath: function(path) {
+
+                var xRatio = 100 / this.imageWidth;
+                var yRatio = 100 / this.imageHeight;
+
+                if (isNaN(xRatio) || isNaN(yRatio)) {
+                    throw new Error('Ratio should be a number.');
+                }
+
+                var normalizeMatrix = Snap.matrix(xRatio, 0, 0, yRatio, 0, 0);
+
+                path = Snap.path.map(path, normalizeMatrix).toString();
+
+                if (path.search(/[z|Z]/gi) === -1) {
+                    path += " Z";
+                }
+
+                return path;
+            },
+
+            denormalizePath: function(path) {
+
+                var xRatio = this.imageWidth / 100;
+                var yRatio = this.imageHeight / 100;
+
+                if (isNaN(xRatio) || isNaN(yRatio)) {
+                    throw new Error('Ratio should be a number.');
+                }
+
+                var transformMatrix = Snap.matrix(xRatio, 0, 0, yRatio, 0, 0);
+
+                path = Snap.path.map(path, transformMatrix).toString();
+
+                if (path.search(/[z|Z]/gi) === -1) {
+                    path += " Z";
+                }
+
+                return path;
+            },
+
+            handleDrawFree: function() {
+
+                var clickTimeout;
+
+                var clickHandler = function (offsetX, offsetY) {
+                    clickTimeout = null;
+                    this.$refs.free.addPoint(offsetX, offsetY);
+                }
+
+                this.paper.click((e) => {
+                    if (clickTimeout) { return; }
+                    if (!$(e.target).is('image')) { return; }
+                    if (this.$refs.free.closed) { return; }
+
+                    var offset = this.computeOffset(e);
+                    var offsetX = offset.x;
+                    var offsetY = offset.y;
+                    clickTimeout = setTimeout(clickHandler.bind(this, offsetX, offsetY), 190);
+                });
+
+            },
+
+            handleDrawRect: function() {
+
+                var startPosition = { x: 0, y: 0 };
+                var currentPosition = { x: 0, y: 0 };
+                var canDraw = false;
+
+                var computeOffset = (e) => {
+                    var rect = this.$refs.image.getBoundingClientRect();
+                    var zoomOffset = this.zoomOffset();
+                    var offsetX = (e.clientX - rect.left) / Math.min(zoomOffset.x, zoomOffset.y);
+                    var offsetY  = (e.clientY - rect.top) / Math.min(zoomOffset.x, zoomOffset.y);
+
+                    return { x: offsetX, y: offsetY };
+                }
+
+                this.paper.mousedown((e) => {
+
+                    if (this.$refs.rect.width > 0 && this.$refs.rect.height > 0) { return; }
+
+                    startPosition = computeOffset(e);
+                    canDraw = true;
+                });
+
+                this.paper.mousemove((e) => {
+
+                    if (!canDraw) { return; }
+
+                    var x, y;
+                    currentPosition = computeOffset(e);
+
+                    /* bas -> droite */
+                    var width = Math.abs(currentPosition.x - startPosition.x);
+                    var height = Math.abs(startPosition.y - currentPosition.y);
+
+                    if (currentPosition.y > startPosition.y && currentPosition.x > startPosition.x) {
+                        x = startPosition.x;
+                        y = startPosition.y;
+                    }
+
+                    /* haut -> droite */
+                    if (currentPosition.y < startPosition.y && currentPosition.x > startPosition.x) {
+                        x = currentPosition.x - width;
+                        y = currentPosition.y;
+                    }
+
+                    /* haut -> gauche */
+                    if (currentPosition.y < startPosition.y && currentPosition.x < startPosition.x) {
+                        x = currentPosition.x;
+                        y = currentPosition.y;
+                    }
+
+                    /* bas -> gauche */
+                    if (currentPosition.y > startPosition.y && currentPosition.x < startPosition.x) {
+                        x = currentPosition.x
+                        y = currentPosition.y - height;
+                    }
+
+                    if(!x || !y) { return; }
+
+                    Object.assign(this.$refs.rect, {
+                        x: x,
+                        y: y,
+                        width: width,
+                        height: height,
+                    });
+                });
+
+                this.paper.mouseup((e) => {
+
+                    if (!canDraw) { return; }
+
+                    canDraw = false;
+
+                    if (this.$refs.rect.width === 0 && this.$refs.rect.height === 0) {
+                        var currentPosition = computeOffset(e);
+                        Object.assign(this.$refs.rect, {
+                            x: currentPosition.x,
+                            y: currentPosition.y,
+                            width: this.imgMinSize / 4,
+                            height: this.imgMinSize / 4,
+                        });
+                    }
+
+                    this.$nextTick(() => {
+                        this.$refs.rect.addResizeHandlers();
+                        this.$refs.rect.addTooltip();
+                    });
+
+                });
+            }
+        }
+    }
+
+</script>
+
+<style scoped>
+.zoom {
+    position: relative;
+}
+.zoomer {
+    position: absolute;
+    bottom: 30px;
+    right: 30px;
+}
+.cut-canvas {
+    width: 100%;
+    height: 800px;
+}
+.canvas--rect:hover {
+    cursor: crosshair;
+}
+.canvas--free:hover {
+    cursor: pointer;
+}
+.mode-controls {
+    position: absolute;
+    top: 15px;
+    left: 15px;
+}
+.mode-controls .btn > svg {
+    margin-top: 4px;
+}
+.mode-controls .btn-primary > svg {
+    fill: #fff;
+}
+
+</style>