{"id":815,"date":"2020-10-08T13:41:42","date_gmt":"2020-10-08T13:41:42","guid":{"rendered":"https:\/\/potatodie.nl\/diffuse-write-ups\/?p=815"},"modified":"2021-10-03T08:17:19","modified_gmt":"2021-10-03T08:17:19","slug":"magnify-an-image-with-a-draggable-loupe","status":"publish","type":"post","link":"https:\/\/potatodie.nl\/diffuse-write-ups\/magnify-an-image-with-a-draggable-loupe\/","title":{"rendered":"Magnify images with a draggable loupe"},"content":{"rendered":"\n<p>In a <a href=\"https:\/\/potatodie.nl\/diffuse-write-ups\/creating-a-magnifying-glass-with-html5-canvas\/\" data-type=\"post\" data-id=\"789\">previous post<\/a> we&#8217;ve seen how to create a magnifying glass effect with <code>canvas<\/code>. But the position of the lens was static, and of course in a useful application a user must be able to move the lens to a point of interest.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"738\" height=\"343\" src=\"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/MagnifiedHazel.png\" alt=\"\" class=\"wp-image-819\" srcset=\"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/MagnifiedHazel.png 738w, https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/MagnifiedHazel-300x139.png 300w\" sizes=\"auto, (max-width: 738px) 100vw, 738px\" \/><figcaption>Static image of magnifying glass. Hang on for interactive version.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Draggable Vue component<\/h2>\n\n\n\n<p>To that end we&#8217;ll create a Vue component that works as a draggable loupe. The loupe hovers over an image that is likely to be downscaled (otherwise  a magnifying glass is not of much use). Furthermore, we allow the image to be responsive. This means the position of the loupe in CSS pixels will not correspond to the image pixels. Since SVG elements may have their own coordinate system, let&#8217;s use SVG for the draggable, and make its coordinates match the underlying image.<\/p>\n\n\n<div class=\"wp-block-advanced-gutenberg-blocks-code\">\n  <header class=\"wp-block-advanced-gutenberg-blocks-code__header\">\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__lang is-lang-html\">\n      HTML    <\/div>\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__file\">\n      CrossHairs.vue    <\/div>\n  <\/header>\n  <textarea \n    class=\"wp-block-advanced-gutenberg-blocks-code__source\" \n    name=\"codemirror-934996150\" \n    id=\"codemirror-934996150\"\n  >&lt;template&gt;\n    &lt;svg class=&quot;crosshairs&quot; :viewBox=&quot;viewBox&quot;&gt;\n        &lt;circle\n            ref=&quot;crosshairs&quot;\n            cx=&quot;0&quot;\n            cy=&quot;0&quot;\n            :r=&quot;radius * 1.4&quot;\n            :transform=&quot;transform&quot;\n        \/&gt;\n    &lt;\/svg&gt;\n&lt;\/template&gt;<\/textarea>\n  <script>\n    CodeMirror.fromTextArea( document.getElementById('codemirror-934996150'), {\n      mode: 'xml',\n      readOnly: true,\n      theme: 'hopscotch', \n      lineNumbers: true,\n      firstLineNumber: 1,\n      matchBrackets: true,\n      indentUnit: 4,\n      tabSize: 4,\n      lineWrapping: true,\n    } ); \n  <\/script>\n<\/div>\n\n\n\n<p>The coordinate system can be set by the parent via the <code>viewBox<\/code> property. In this way we can make the position of the loupe match with the image coordinates.<\/p>\n\n\n\n<p>We&#8217;ll implement the draggable with <a href=\"https:\/\/greensock.com\/draggable\">GreenSock&#8217;s Draggable<\/a>:<\/p>\n\n\n<div class=\"wp-block-advanced-gutenberg-blocks-code\">\n  <header class=\"wp-block-advanced-gutenberg-blocks-code__header\">\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__lang is-lang-js\">\n      JS    <\/div>\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__file\">\n      CrossHairs.vue    <\/div>\n  <\/header>\n  <textarea \n    class=\"wp-block-advanced-gutenberg-blocks-code__source\" \n    name=\"codemirror-618364457\" \n    id=\"codemirror-618364457\"\n  >&lt;script&gt;\nimport gsap from &quot;gsap&quot;;\nimport { Draggable } from &quot;gsap\/Draggable.js&quot;;\ngsap.registerPlugin(Draggable);\n\nexport default {\n    props: [&quot;position&quot;, &quot;radius&quot;, &quot;container&quot;],\n    computed: {\n        viewBox() {\n            return `0 0 ${this.container.width} ${this.container.height}`;\n        },\n        transform() {\n            \/\/  This transform overwrites the transform that gsap sets\n            return `translate(${this.position.x}, ${this.position.y})`;\n        },\n    },\n    mounted() {\n        this.createDraggable();\n    },\n    methods: {\n        createDraggable: function () {\n            const element = this.$refs.crosshairs;\n            const self = this;\n            Draggable.create(element, {\n                type: &quot;x, y&quot;,\n                onDrag() {\n                    self.$emit(&quot;update&quot;, {\n                        x: this.deltaX,\n                        y: this.deltaY,\n                    });\n                },\n            });\n        },\n    },\n};<\/textarea>\n  <script>\n    CodeMirror.fromTextArea( document.getElementById('codemirror-618364457'), {\n      mode: 'javascript',\n      readOnly: true,\n      theme: 'hopscotch', \n      lineNumbers: true,\n      firstLineNumber: 1,\n      matchBrackets: true,\n      indentUnit: 4,\n      tabSize: 4,\n      lineWrapping: false,\n    } ); \n  <\/script>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Single source of truth<\/h2>\n\n\n\n<p>The draggable passes the change in position (<code>deltaX <\/code>and <code>deltaY<\/code>) to the parent while the user drags. The parent component then adds these deltas to the current position after which the <code>transform <\/code>property of the CrossHairs component reacts to the updated position. Usually gsap&#8217;s Draggable manages the <code>transform <\/code>property (when using <code>type: \"x,y\"<\/code>), but here that property is overwritten when the parent component changes <code>position<\/code>. This guarantees the <em>Single source of truth<\/em> remains by the parent component.<\/p>\n\n\n\n<p>Now let&#8217;s turn to the parent component.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Canvas lasagne<\/h2>\n\n\n\n<p>The CanvasScrutinizer component consists of several layers. First we have a canvas to which we copy our source image. This canvas will also determine the dimensions of the container <code>div<\/code>, since the other layers are positioned absolutely. The second canvas is for drawing the magnified part of the image. Top it off with the CrossHairs component to allow the user to move the loupe. <\/p>\n\n\n<div class=\"wp-block-advanced-gutenberg-blocks-code\">\n  <header class=\"wp-block-advanced-gutenberg-blocks-code__header\">\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__lang is-lang-html\">\n      HTML    <\/div>\n    <div class=\"wp-block-advanced-gutenberg-blocks-code__file\">\n      CanvasScrutinizer.vue    <\/div>\n  <\/header>\n  <textarea \n    class=\"wp-block-advanced-gutenberg-blocks-code__source\" \n    name=\"codemirror-1774278327\" \n    id=\"codemirror-1774278327\"\n  >&lt;template&gt;\n    &lt;div class=&quot;container&quot;&gt;\n        &lt;canvas id=&quot;canvas_original&quot;&gt;&lt;\/canvas&gt;\n        &lt;canvas id=&quot;canvas_magnification&quot;&gt;&lt;\/canvas&gt;\n        &lt;CrossHairs\n            :position=&quot;position&quot;\n            :radius=&quot;radius&quot;\n            :container=&quot;container&quot;\n            @update=&quot;updateCrossHairs&quot;\n        \/&gt;\n    &lt;\/div&gt;\n&lt;\/template&gt;<\/textarea>\n  <script>\n    CodeMirror.fromTextArea( document.getElementById('codemirror-1774278327'), {\n      mode: 'xml',\n      readOnly: true,\n      theme: 'hopscotch', \n      lineNumbers: true,\n      firstLineNumber: 1,\n      matchBrackets: true,\n      indentUnit: 4,\n      tabSize: 4,\n      lineWrapping: true,\n    } ); \n  <\/script>\n<\/div>\n\n\n\n<p>The script part of CanvasScrutinizer.vue is loosely based on the technique explained in the <a href=\"https:\/\/potatodie.nl\/diffuse-write-ups\/creating-a-magnifying-glass-with-html5-canvas\/\" data-type=\"post\" data-id=\"789\">previous post<\/a>. You can check it out on <a href=\"https:\/\/codesandbox.io\/s\/canvasscrutinizer-fcv97\">CodeSandbox<\/a>, along with the app. <\/p>\n\n\n\n<iframe src=\"https:\/\/codesandbox.io\/embed\/canvasscrutinizer-fcv97?fontsize=14&amp;hidenavigation=1&amp;theme=dark&amp;view=preview\" style=\"width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;\" title=\"CanvasScrutinizer\" allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\" sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"><\/iframe>\n\n\n\n<h2 class=\"wp-block-heading\">The loupe at work<\/h2>\n\n\n\n<p>See the application with UI (set magnification factor, set loupe size, image file input) at work in the <a href=\"https:\/\/potatodie.nl\/lab\/loupe\">lab<\/a>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"564\" src=\"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan-1024x564.png\" alt=\"\" class=\"wp-image-846\" srcset=\"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan-1024x564.png 1024w, https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan-300x165.png 300w, https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan-768x423.png 768w, https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan-1200x661.png 1200w, https:\/\/potatodie.nl\/diffuse-write-ups\/wp-content\/uploads\/2020\/10\/loupe-japan.png 1420w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption>Preview of the finished application<\/figcaption><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>In a previous post we&#8217;ve seen how to create a magnifying glass effect with canvas. But the position of the lens was static, and of course in a useful application a user must be able to move the lens to a point of interest. Let&#8217;s make aVue component for that!<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[50,20,7],"tags":[39,43,38,15,35],"class_list":["post-815","post","type-post","status-publish","format-standard","hentry","category-graphics","category-javascript","category-web-development","tag-canvas","tag-draggable","tag-greensock","tag-svg","tag-vue-js"],"acf":[],"_links":{"self":[{"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/posts\/815","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/comments?post=815"}],"version-history":[{"count":12,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/posts\/815\/revisions"}],"predecessor-version":[{"id":847,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/posts\/815\/revisions\/847"}],"wp:attachment":[{"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/media?parent=815"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/categories?post=815"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/potatodie.nl\/diffuse-write-ups\/wp-json\/wp\/v2\/tags?post=815"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}