Introduce ZoomThumbnail, improve zoom behavior.
authorAlexandre Segura <mex.zktk@gmail.com>
Wed, 22 Feb 2017 11:04:16 +0100
changeset 335 86dbf2cdeeeb
parent 334 a0caf91c8374
child 336 157819077ee5
Introduce ZoomThumbnail, improve zoom behavior.
src/iconolab/templates/iconolab/detail_image.html
src_js/iconolab-bundle/src/components/editor/Canvas.vue
src_js/iconolab-bundle/src/components/editor/ShapeRect.vue
src_js/iconolab-bundle/src/components/editor/ZoomThumbnail.vue
--- a/src/iconolab/templates/iconolab/detail_image.html	Tue Feb 21 16:04:20 2017 +0100
+++ b/src/iconolab/templates/iconolab/detail_image.html	Wed Feb 22 11:04:16 2017 +0100
@@ -16,7 +16,7 @@
             data-annotation-id="{{ annotation.annotation_guid }}"
             data-revision-id="{{ annotation.current_revision.revision_guid }}">
             <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-            <h4 class="list-group-item-heading">{{ annotation.current_revision.title }}</h4>
+            <h5 class="list-group-item-heading">{{ annotation.current_revision.title }}</h5>
             <p class="list-group-item-text">{{ annotation.current_revision.description }}</p>
             {% for tagging_info in annotation.current_revision.tagginginfo_set.all %}
               <span class="label label-warning">{{ tagging_info.tag.label }}</span>
@@ -30,11 +30,12 @@
     </div>
   </div>
   <div class="annotation-navigator-canvas">
-    {% with image.media as img %}
+
     <image-annotator v-bind:annotation="annotation"
       ref="annotator"
-      image="{{ img.url }}"></image-annotator>
-    {% endwith %}
+      image="{% with image.media as img %}{{ img.url }}{% endwith %}"
+      thumbnail="{% thumbnail image.media '120x120' crop=False as thumb %}{{ thumb.url }}{% endthumbnail %}"></image-annotator>
+
     <form id="form-annotation" action="{% url 'annotation_create' collection_name image.image_guid %}" method="POST">
       {% csrf_token %}
       <input type="hidden" name="{{ form.title.name }}">
@@ -75,7 +76,9 @@
   var vm = new Vue({
     el: '.annotation-navigator',
     data: function() {
-      return { annotation: null };
+      return {
+        annotation: null
+      };
     }
   });
 
--- a/src_js/iconolab-bundle/src/components/editor/Canvas.vue	Tue Feb 21 16:04:20 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue	Wed Feb 22 11:04:16 2017 +0100
@@ -17,6 +17,18 @@
                 <button @click="zoomOut" type="button" class="btn btn-default"><i class="fa fa-minus" aria-hidden="true"></i></button>
             </div>
         </div>
+        <div class="zoom-thumbnail">
+            <zoom-thumbnail
+                ref="thumbnail"
+                @change="changeViewBox($event)"
+                @dragstart="hideTooltip"
+                @dragend="showTooltip"
+                v-bind:image="thumbnail"
+                v-bind:viewport="viewport"
+                v-bind:viewBox="viewBox"
+                v-bind:imageWidth="imageWidth"
+                v-bind:imageHeight="imageHeight"></zoom-thumbnail>
+        </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'}">
@@ -35,10 +47,12 @@
     import Snap from 'snapsvg'
     import ShapeRect from './ShapeRect.vue'
     import ShapeFree from './ShapeFree.vue'
+    import ZoomThumbnail from './ZoomThumbnail.vue'
 
     export default {
         props: {
             image: String,
+            thumbnail: String,
             annotation: {
                 type: Object,
                 default: function () {
@@ -54,7 +68,8 @@
         },
         components: {
             shapeRect: ShapeRect,
-            shapeFree: ShapeFree
+            shapeFree: ShapeFree,
+            zoomThumbnail: ZoomThumbnail,
         },
         data() {
             return {
@@ -105,32 +120,24 @@
                     factor = scale - 1;
                 }
 
-                var center = this.getCenter();
-
-                var viewBoxW = this.imgMinSize - (this.imgMinSize * factor);
-                var viewBoxH = viewBoxW;
+                if (scale === 1) {
+                    this.resetViewBox();
+                } else {
+                    var center = this.getCenter();
 
-                const viewBoxPrev = this.viewBox.slice();
-                const viewBoxNew = [
-                    center.x - viewBoxW / 2,
-                    center.y - viewBoxH / 2,
-                    viewBoxW,
-                    viewBoxH
-                ];
+                    var viewBoxW = this.imgMinSize - (this.imgMinSize * factor);
+                    var viewBoxH = viewBoxW;
 
-                this.$refs.rect.hideTooltip();
-                Snap.animate(
-                    viewBoxPrev, viewBoxNew,
-                    (viewBox) => this.paper.attr({ "viewBox": viewBox }),
-                    350, mina.easeinout,
-                    () => {
-                        this.viewBox = viewBoxNew;
-                        this.$refs.rect.showTooltip();
-                        if (scale === 1) {
-                            this.resetZoom();
-                        }
-                    }
-                );
+                    var viewBox = [
+                        center.x - viewBoxW / 2,
+                        center.y - viewBoxH / 2,
+                        viewBoxW,
+                        viewBoxH
+                    ];
+
+                    this.hideTooltip();
+                    this.animateViewBox(viewBox, () => this.showTooltip());
+                }
             }
         },
         mounted() {
@@ -178,13 +185,30 @@
 
         },
         methods: {
-
+            hideTooltip: function() {
+                if (this.mode === 'free') {
+                    this.$refs.free.hideTooltip();
+                }
+                if (this.mode === 'rect') {
+                    this.$refs.rect.hideTooltip();
+                }
+            },
+            showTooltip: function() {
+                if (this.mode === 'free') {
+                    this.$refs.free.showTooltip();
+                }
+                if (this.mode === 'rect') {
+                    this.$refs.rect.showTooltip();
+                }
+            },
             reset: function() {
                 // Clear shapes
                 this.$refs.rect.clear();
                 this.$refs.free.clear();
 
                 this.removeEventHandlers();
+                this.resetZoom();
+                this.resetViewBox();
 
                 if (this.mode === 'free') {
                     this.handleDrawFree();
@@ -228,10 +252,36 @@
                 }
             },
 
+            changeViewBox: function(e) {
+                const viewBox = this.viewBox.slice();
+
+                viewBox[0] = e.x;
+                viewBox[1] = e.y;
+
+                this.paper.attr({ "viewBox": viewBox });
+            },
+
             resetZoom: function() {
                 this.scale = 1;
-                this.viewBox = [0, 0, this.imageWidth, this.imageHeight];
-                this.paper.attr({ "viewBox": this.viewBox });
+                this.$refs.thumbnail.reset();
+            },
+
+            animateViewBox: function(viewBox, cb) {
+                const viewBoxPrev = this.viewBox.slice();
+
+                Snap.animate(
+                    viewBoxPrev, viewBox,
+                    (viewBox) => this.paper.attr({ "viewBox": viewBox }),
+                    350, mina.easeinout,
+                    () => {
+                        this.viewBox = viewBox;
+                        if (cb) { cb(); }
+                    }
+                );
+            },
+
+            resetViewBox: function() {
+                this.animateViewBox([0, 0, this.imageWidth, this.imageHeight]);
             },
 
             zoomIn: function() {
@@ -430,8 +480,8 @@
 }
 .zoomer {
     position: absolute;
-    bottom: 30px;
-    right: 30px;
+    bottom: 15px;
+    right: 15px;
 }
 .cut-canvas {
     width: 100%;
@@ -448,6 +498,11 @@
     top: 15px;
     left: 15px;
 }
+.zoom-thumbnail {
+    position: absolute;
+    left: 15px;
+    bottom: 15px;
+}
 .mode-controls .btn > svg {
     margin-top: 4px;
 }
--- a/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue	Tue Feb 21 16:04:20 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue	Wed Feb 22 11:04:16 2017 +0100
@@ -155,11 +155,6 @@
                 bottomRightHandler.drag(handlerEvents.onMove, handlerEvents.onStart, handlerEvents.onEnd);
             },
 
-            getCenter: function() {
-                var shape = new Snap(this.$refs.shape);
-                console.log(shape.getBBox());
-            },
-
             fromSVGPath: function(pathString, tooltip) {
                 var bBox = Snap.path.getBBox(pathString);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/ZoomThumbnail.vue	Wed Feb 22 11:04:16 2017 +0100
@@ -0,0 +1,134 @@
+<template>
+    <div class="wrapper">
+        <svg ref="svg" v-bind:width="thumbnailWidth" v-bind:height="thumbnailHeight">
+            <image xmlns:xlink="http://www.w3.org/1999/xlink"
+                x="0" y="0"
+                v-bind:width="thumbnailWidth" v-bind:height="thumbnailHeight"
+                v-bind:xlink:href="image"></image>
+            <rect ref="handler"
+                class="move-handler"
+                v-bind:x="x" v-bind:y="y"
+                v-bind:width="width" v-bind:height="height"
+                style="fill: black; opacity: 0.4"></rect>
+        </svg>
+    </div>
+</template>
+
+<script>
+
+    import Snap from 'snapsvg'
+
+    export default {
+        props: [
+            'image',
+            'viewport',
+            'viewBox',
+            'imageWidth',
+            'imageHeight',
+        ],
+        data() {
+            return {
+                thumbnailWidth: 0,
+                thumbnailHeight: 0,
+                loaded: false
+            }
+        },
+        computed: {
+            x: function() {
+                return this.viewBox[0] * this.getRatioX();
+            },
+            y: function() {
+                return this.viewBox[1] * this.getRatioY();
+            },
+            width: function() {
+                return this.viewBox[2] * this.getRatioX();
+            },
+            height: function() {
+                return this.viewBox[3] * this.getRatioY();
+            }
+        },
+        methods: {
+            reset: function() {
+                var handler = new Snap(this.$refs.handler);
+                handler.node.removeAttribute('transform');
+            },
+            getRatioX: function() {
+                if (this.imageWidth === 0) { return 0; }
+
+                return this.thumbnailWidth / this.imageWidth;
+            },
+            getRatioY: function() {
+                if (this.imageHeight === 0) { return 0; }
+
+                return this.thumbnailHeight / this.imageHeight;
+            }
+        },
+        mounted() {
+
+            var svg = new Snap(this.$refs.svg);
+            var handler = new Snap(this.$refs.handler);
+
+            var img = new Image();
+            img.onload = (e) => {
+
+                var self = this;
+
+                svg.attr({
+                    viewBox: [0, 0, img.width, img.height]
+                });
+
+                Object.assign(this, {
+                    thumbnailWidth: img.width,
+                    thumbnailHeight: img.height,
+                    loaded: true
+                });
+
+                var events = {
+                    onMove: function (dx, dy, x, y) {
+
+                        var snapInvMatrix = this.transform().diffMatrix.invert();
+                        snapInvMatrix.e = snapInvMatrix.f = 0;
+                        var tdx = snapInvMatrix.x(dx, dy);
+                        var tdy = snapInvMatrix.y(dx, dy);
+
+                        var transformValue = this.data('origTransform') + (this.data('origTransform') ? "T" : "t") + [tdx, tdy];
+                        this.transform(transformValue);
+
+                        self.$emit('change', {
+                            x: this.getBBox().x * (self.imageWidth / self.thumbnailWidth),
+                            y: this.getBBox().y * (self.imageHeight / self.thumbnailHeight),
+                        });
+
+                    },
+                    onStart: function () {
+                        this.data('origTransform', this.transform().local);
+                        self.$emit('dragstart');
+                    },
+                    onEnd: function () {
+                        self.$emit('dragend');
+                    }
+                }
+
+                handler.drag(events.onMove, events.onStart, events.onEnd);
+            }
+
+            img.src = this.image;
+        }
+    }
+
+</script>
+
+<style scoped>
+.wrapper {
+    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+.wrapper svg {
+    border: 2px solid black;
+    border-radius: 4px;
+}
+.move-handler {
+    cursor: -moz-grab;
+    cursor: -webkit-grab;
+    cursor: grab;
+}
+</style>