title: Drag image within container, not moving with mouse, slightly quicker
body: <p>I have got a Vue component where i can zoom in on an image and then move it around the container. When zoomed there is also a small viewport to show what part of the image is visible. However when moving the image around it is moving faster than the mouse, i'm guessing this is due to using the <code>scale</code> transform.</p>
  <p>I also feel like when i'm clicking and dragging on the viewport I shouldn't be reversing the values twice however this seems to be the only way to get it moving the square with the mouse.</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>Vue.component('test', {
    template: '#template',
    data: function() {
      return {
        loading: true,
  
        loop: true,
  
        speed: 8,
  
        speedController: 0,
  
        zoomEnabled: true,
        zoomLevels: [1, 1.5, 2, 2.5, 3],
        zoomLevel: 1,
  
        frame: 1,
        images: [],
        imagesPreloaded: 0,
  
        reverse: false,
  
        viewportScale: 0.3,
        viewportEnabled: true,
        viewportOpacity: 0.8,
  
        lastX: 0,
        lastY: 0,
  
        startX: 0,
        startY: 0,
  
        translateX: 0,
        translateY: 0,
  
        isMoving: false,
        isDragging: false,
      };
    },
    mounted() {
      window.addEventListener('mouseup', this.handleEnd);
      window.addEventListener('touchend', this.handleEnd);
    },
    beforeDestroy() {
      window.removeEventListener('mouseup', this.handleEnd);
      window.removeEventListener('touchend', this.handleEnd);
    },
    methods: {
      handleSlider(event) {
        this.frame = Number(event.target.value);
      },
      zoom(direction) {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        if (this.zoomLevels[this.zoomLevels.indexOf(closest) + direction] === undefined) {
          return;
        }
  
        let current = this.zoomLevels.indexOf(closest);
        let index = current += direction;
        if (direction === 0) {
          index = 0;
        }
        this.zoomLevel = this.zoomLevels[index];
  
        window.requestAnimationFrame(() =&gt; {
          this.translate(null, this.$refs.image, true);
        });
      },
      zoomWheel($event) {
        $event.preventDefault();
  
        this.zoomLevel += $event.deltaY * -0.01;
  
        if (this.zoomLevel &lt; 1) {
          this.zoomLevel = 1;
        }
  
        let maxZoom = this.zoomLevels[this.zoomLevels.length - 1];
  
        this.zoomLevel = Math.min(Math.max(.125, this.zoomLevel), maxZoom);
  
        window.requestAnimationFrame(() =&gt; {
          this.translate(null, this.$refs.image, true);
        });
      },
      handleStart($event) {
        $event.preventDefault();
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
        this.isMoving = true;
        this.isDragging = true;
  
        this.startX = $event.pageX || $event.touches[0].pageX;
        this.startY = $event.pageY || $event.touches[0].pageY;
      },
      handleMove($event, viewport) {
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
        if (this.isMoving &amp;&amp; this.isDragging) {
          const positions = {
            x: $event.pageX || $event.touches[0].pageX,
            y: $event.pageY || $event.touches[0].pageY
          }
  
          if (this.zoomLevel !== 1) {
            this.translate(positions, $event.target, null, viewport);
          }
          if (this.zoomLevel === 1) {
            this.changeFrame(positions);
          }
        }
      },
      handleEnd($event) {
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
        this.isMoving = false;
      },
      translate(positions, element, zooming, viewport) {
        if (positions === null) {
          positions = {
            x: this.startX,
            y: this.startY
          };
        }
  
        let move = {
          x: Math.floor(positions.x - this.startX),
          y: Math.floor(positions.y - this.startY)
        };
  
        // Reverse Mouse Movement
        if (viewport) {
          move.x = -move.x;
          move.y = -move.y;
        }
  
        let image = element.getBoundingClientRect();
        let container = element.parentNode.getBoundingClientRect();
  
        let translate = {
          left: Math.floor((container.left - image.left) - (move.x * this.zoomLevel)),
          right: Math.floor((container.right - image.right) - (move.x * this.zoomLevel)),
          top: Math.floor((container.top - image.top) - (move.y * this.zoomLevel)),
          bottom: Math.floor((container.bottom - image.bottom) - (move.y * this.zoomLevel))
        };
  
        // Reverse Translate Movement
        if (viewport) {
          translate.left = -translate.left;
          translate.right = -translate.right;
          translate.top = -translate.top;
          translate.bottom = -translate.bottom;
        }
  
        if (zooming) {
          if (translate.left &lt;= 0) {
            this.translateX += Math.floor(translate.left);
          }
          if (translate.right &gt;= 0) {
            this.translateX += Math.floor(translate.right);
          }
          if (translate.top &lt;= 0) {
            this.translateY += Math.floor(translate.top);
          }
          if (translate.bottom &gt;= 0) {
            this.translateY += Math.floor(translate.bottom);
          }
        }
  
        if (translate.left &gt;= 0 &amp;&amp; translate.right &lt;= 0) {
          this.translateX += Math.floor(move.x);
        }
        if (translate.top &gt;= 0 &amp;&amp; translate.bottom &lt;= 0) {
          this.translateY += Math.floor(move.y);
        }
  
        this.startX = positions.x;
        this.startY = positions.y;
      }
    },
    computed: {
      nextZoomLevel: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        if (this.zoomLevels.indexOf(closest) === this.zoomLevels.length - 1) {
          return this.zoomLevels[0];
        }
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + 1];
      },
      viewportTransform: function() {
        if (this.viewportEnabled) {
          let translateX = -((this.translateX * this.viewportScale) * this.zoomLevel);
          let translateY = -((this.translateY * this.viewportScale) * this.zoomLevel);
  
          return `scale(${1 / this.zoomLevel}) translateX(${translateX}px) translateY(${translateY}px)`;
        }
      },
      transform: function() {
        return `scale(${this.zoomLevel}) translateX(${this.translateX}px) translateY(${this.translateY}px)`;
      },
      canZoomIn: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + 1] === undefined
      },
      canZoomOut: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + -1] === undefined
      }
    }
  });
  
  window.vue = new Vue({el: '#app'});</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>.media-360-viewer {
    position: relative;
    overflow: hidden;
    background: #000;
  }
  
  .media-360-viewer&gt;img {
    width: 100%;
  }
  
  .media-360-viewer&gt;img.canTranslate {
    cursor: grab;
  }
  
  .media-360-viewer&gt;img.isTranslating {
    cursor: grabbing;
  }
  
  .media-360-viewer&gt;img.canRotate {
    cursor: w-resize;
  }
  
  .media-360-viewer__loader {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
  }
  
  .media-360-viewer__loader * {
    user-select: none;
  }
  
  .media-360-viewer__loader&gt;svg {
    width: 100%;
    height: 100%;
    transform: rotate(270deg);
  }
  
  .media-360-viewer__loader--text {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
  }
  
  .media-360-viewer__loader--text p {
    font-size: 100%;
    font-weight: bold;
    color: #fff;
  }
  
  .media-360-viewer__loader--text p.large {
    font-size: 150%;
  }
  
  .media-360-viewer__loader--background {
    stroke-dasharray: 0;
    stroke-dashoffset: 0;
    stroke: rgba(0, 0, 0, 0.7);
    stroke-width: 25px;
  }
  
  .media-360-viewer__loader--cover {
    stroke-dasharray: 200%;
    stroke: #848484;
    stroke-width: 15px;
    stroke-linecap: round;
  }
  
  .media-360-viewer__loader--background,
  .media-360-viewer__loader--cover {
    fill: transparent;
  }
  
  .media-360-viewer__viewport {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    border: 1px solid black;
    overflow: hidden;
  }
  
  .media-360-viewer__viewport--image {
    width: 100%;
    pointer-events: none;
  }
  
  .media-360-viewer__viewport--zoom {
    position: absolute;
    bottom: 5px;
    right: 5px;
    color: #fff;
    z-index: 3;
    font-size: 12px;
    pointer-events: none;
  }
  
  .media-360-viewer__viewport--square {
    border: 1px solid black;
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    box-shadow: rgba(0, 0, 0, 0.5) 0 0 0 10000px;
    cursor: grab;
    transition: background ease-in-out 0.1s;
  }
  
  .media-360-viewer__viewport--square:hover {
    background: rgba(255, 255, 255, 0.2);
  }
  
  .media-360-viewer__header {
    position: absolute;
    top: 10px;
    left: 0;
    width: 100%;
  }
  
  .media-360-viewer__tools {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-bottom: 10px;
  }
  
  .media-360-viewer__tools&gt;a {
    margin: 0 5px;
    color: #000;
    background: #fff;
    border-radius: 50%;
    width: 40px;
    text-align: center;
    line-height: 40px;
  }
  
  .media-360-viewer__tools&gt;a[disabled] {
    opacity: .5;
    cursor: not-allowed;
  }
  
  .media-360-viewer__tools&gt;a[disabled]:hover {
    color: #000;
    background: #fff;
  }
  
  .media-360-viewer__tools&gt;a:hover {
    background: #000;
    color: #fff;
  }
  
  .media-360-viewer__tools--autoplay:before {
    font-family: 'ClickIcons';
    content: '\ea81';
  }
  
  .media-360-viewer__tools--autoplay.active:before {
    content: '\eb48';
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"&gt;&lt;/script&gt;
  
  &lt;div id="app"&gt;
    &lt;test&gt;&lt;/test&gt;
  &lt;/div&gt;
  
  &lt;script type="text/x-template" id="template"&gt;
    &lt;div class="media-360-viewer" ref="container"&gt;
      &lt;transition name="fade"&gt;
        &lt;div class="media-360-viewer__viewport" v-if="zoomLevel &gt; 1 &amp;&amp; viewportEnabled" :style="{ width: (viewportScale * 100) + '%' }"&gt;
          &lt;img tabindex="1" draggable="false" alt="Viewport" class="media-360-viewer__viewport--image" src="https://www.bennetts.co.uk/-/media/bikesocial/2019-september-images/2020-yamaha-yzf-r1-and-r1m-review/2020-yamaha-r1-and-r1m_005.ashx?h=493&amp;w=740&amp;la=en&amp;hash=F97CD240F0DDFA9540E912DCF7F07019017035C6"&gt;
          &lt;span class="media-360-viewer__viewport--zoom"&gt;
                      x{{ Math.round(zoomLevel * 10) / 10 }}
                  &lt;/span&gt;
          &lt;span :style="{ transform: viewportTransform }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove($event, true)" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove($event, true)" class="media-360-viewer__viewport--square"&gt;&lt;/span&gt;
        &lt;/div&gt;
      &lt;/transition&gt;
      &lt;img tabindex="1" ref="image" draggable="false" src="https://www.bennetts.co.uk/-/media/bikesocial/2019-september-images/2020-yamaha-yzf-r1-and-r1m-review/2020-yamaha-r1-and-r1m_005.ashx?h=493&amp;w=740&amp;la=en&amp;hash=F97CD240F0DDFA9540E912DCF7F07019017035C6" :style="{ transform: transform }" :class="{
                   canTranslate: zoomLevel &gt; 1 &amp;&amp; zoomEnabled,
                   canRotate: zoomLevel === 1,
                   isTranslating: zoomLevel &gt; 1 &amp;&amp; zoomEnabled &amp;&amp; isMoving
               }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove" @dblclick="zoom" @wheel="zoomWheel" alt="360 Image" /&gt;
    &lt;/div&gt;
  &lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>
  <p>Does anyone know how I can fix this?</p>
  <h1>Edit</h1>
  <p>Below is the updated code with the changes suggested by the accepted answer, only issue now is the viewport square going out of frame.</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>Vue.component('test', {
    template: '#template',
    data: function() {
      return {
        loading: true,
  
        loop: true,
  
        speed: 8,
  
        speedController: 0,
  
        zoomEnabled: true,
        zoomLevels: [1, 1.5, 2, 2.5, 3],
        zoomLevel: 1,
  
        frame: 1,
        images: [],
        imagesPreloaded: 0,
  
        reverse: false,
  
        viewportScale: 0.3,
        viewportEnabled: true,
        viewportOpacity: 0.8,
  
        lastX: 0,
        lastY: 0,
  
        startX: 0,
        startY: 0,
  
        translateX: 0,
        translateY: 0,
  
        isMoving: false,
        isDragging: false,
      };
    },
    mounted() {
      window.addEventListener('mouseup', this.handleEnd);
      window.addEventListener('touchend', this.handleEnd);
    },
    beforeDestroy() {
      window.removeEventListener('mouseup', this.handleEnd);
      window.removeEventListener('touchend', this.handleEnd);
    },
    methods: {
      zoom(direction) {
  
        // todo: Load high res image based on zoom level
  
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        if (this.zoomLevels[this.zoomLevels.indexOf(closest) + direction] === undefined) {
          return;
        }
  
        let current = this.zoomLevels.indexOf(closest);
        let index = current += direction;
        if (direction === 0) {
          index = 0;
        }
        this.zoomLevel = this.zoomLevels[index];
  
        this.translate(null, this.$refs.image, true);
      },
      zoomWheel($event) {
        $event.preventDefault();
  
        this.zoomLevel += $event.deltaY * -0.01;
  
        if (this.zoomLevel &lt; 1) {
          this.zoomLevel = 1;
        }
  
        let maxZoom = this.zoomLevels[this.zoomLevels.length - 1];
  
        this.zoomLevel = Math.min(Math.max(.125, this.zoomLevel), maxZoom);
  
        this.translate(null, this.$refs.image, true);
      },
      handleStart($event) {
        $event.preventDefault();
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
        this.isMoving = true;
        this.isDragging = true;
  
        this.startX = $event.pageX || $event.touches[0].pageX;
        this.startY = $event.pageY || $event.touches[0].pageY;
      },
      handleMove($event, viewport) {
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
  
        if (this.isMoving &amp;&amp; this.isDragging) {
          const positions = {
            x: $event.pageX || $event.touches[0].pageX,
            y: $event.pageY || $event.touches[0].pageY
          }
  
          if (this.zoomLevel !== 1) {
            this.translate(positions, $event.target, null, viewport);
          }
        }
      },
      handleEnd($event) {
        if ($event.button &amp;&amp; $event.button !== 0) {
          return;
        }
        this.isMoving = false;
      },
      translate(positions, element, zooming, viewport) {
        window.requestAnimationFrame(() =&gt; {
          positions = positions || {
            x: this.startX,
            y: this.startY
          };
  
          if (viewport) {
            this._translateFromViewport(positions, element);
          } else {
            this._translateFromImage(positions, element, zooming);
          }
  
          this.startX = positions.x;
          this.startY = positions.y;
        });
      },
  
      _translateFromViewport: function(positions, element) {
        let move = {
          x: positions.x - this.startX,
          y: positions.y - this.startY
        };
  
        let box = element.getBoundingClientRect();
        let container = element.parentNode.getBoundingClientRect();
  
        let translate = {
          left: (container.left - box.left) - ((move.x * this.viewportScale) * this.zoomLevel),
          right: (container.right - box.right) - ((move.x * this.viewportScale) * this.zoomLevel),
          top: (container.top - box.top) - ((move.y * this.viewportScale) * this.zoomLevel),
          bottom: (container.bottom - box.bottom) - ((move.y * this.viewportScale) * this.zoomLevel)
        };
  
        if (translate.left &lt;= 0 &amp;&amp; translate.right &gt;= 0) {
          this.translateX -= move.x / this.viewportScale;
        }
  
        if (translate.top &lt;= 0 &amp;&amp; translate.bottom &gt;= 0) {
          this.translateY -= move.y / this.viewportScale
        }
      },
      _translateFromImage: function(positions, element, zooming) {
        let move = {
          x: Math.floor(positions.x - this.startX),
          y: Math.floor(positions.y - this.startY)
        };
  
        let image = element.getBoundingClientRect();
        let container = element.parentNode.getBoundingClientRect();
  
        let translate = {
          left: (container.left - image.left) - (move.x * this.zoomLevel),
          right: (container.right - image.right) - (move.x * this.zoomLevel),
          top: (container.top - image.top) - (move.y * this.zoomLevel),
          bottom: (container.bottom - image.bottom) - (move.y * this.zoomLevel)
        };
  
        if (zooming) {
          if (translate.left &lt;= 0) {
            this.translateX += translate.left;
          }
          if (translate.right &gt;= 0) {
            this.translateX += translate.right;
          }
          if (translate.top &lt;= 0) {
            this.translateY += translate.top;
          }
          if (translate.bottom &gt;= 0) {
            this.translateY += translate.bottom;
          }
        }
  
        if (translate.left &gt;= 0 &amp;&amp; translate.right &lt;= 0) {
          this.translateX += move.x / this.zoomLevel;
        }
  
        if (translate.top &gt;= 0 &amp;&amp; translate.bottom &lt;= 0) {
          this.translateY += move.y / this.zoomLevel;
        }
      },
    },
    computed: {
      preloadProgress: function() {
        return Math.floor(this.imagesPreloaded / this.images.length * 100);
      },
      currentPath: function() {
        return this.images[this.frame - 1];
      },
      nextZoomLevel: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        if (this.zoomLevels.indexOf(closest) === this.zoomLevels.length - 1) {
          return this.zoomLevels[0];
        }
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + 1];
      },
      viewportTransform: function() {
        if (this.viewportEnabled) {
          let translateX = -((this.translateX * this.viewportScale) * this.zoomLevel);
          let translateY = -((this.translateY * this.viewportScale) * this.zoomLevel);
  
          return `scale(${1 / this.zoomLevel}) translateX(${translateX}px) translateY(${translateY}px)`;
        }
      },
      transform: function() {
        return `scale(${this.zoomLevel}) translateX(${this.translateX}px) translateY(${this.translateY}px)`;
      },
      canZoomIn: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + 1] === undefined
      },
      canZoomOut: function() {
        const closest = this.zoomLevels.reduce((a, b) =&gt; {
          return Math.abs(b - this.zoomLevel) &lt; Math.abs(a - this.zoomLevel) ? b : a;
        });
  
        return this.zoomLevels[this.zoomLevels.indexOf(closest) + -1] === undefined
      }
    }
  });
  
  window.vue = new Vue({
    el: '#app'
  });</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>.media-360-viewer {
      position: relative;
      overflow: hidden;
      background: #000;
    width: 500px;
  }
  
  .media-360-viewer__image {
      width: 100%;
  }
  
  .media-360-viewer__image.canTranslate {
      cursor: grab;
  }
  
  .media-360-viewer__image.isTranslating {
      cursor: grabbing;
  }
  
  .media-360-viewer__image.canRotate {
      cursor: w-resize;
  }
  
  .media-360-viewer__loader {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
  }
  
  .media-360-viewer__loader * {
      user-select: none;
  }
  
  .media-360-viewer__loader &gt; svg {
      width: 100%;
      height: 100%;
      transform: rotate(270deg);
  }
  
  .media-360-viewer__loader--text {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
  }
  
  .media-360-viewer__loader--text p {
      font-size: 100%;
      font-weight: bold;
      color: #fff;
  }
  
  .media-360-viewer__loader--text p.large {
      font-size: 150%;
  }
  
  .media-360-viewer__loader--background {
      stroke-dasharray: 0;
      stroke-dashoffset: 0;
      stroke: rgba(0, 0, 0, 0.7);
      stroke-width: 25px;
  }
  
  .media-360-viewer__loader--cover {
      stroke-dasharray: 200%;
      stroke: #848484;
      stroke-width: 15px;
      stroke-linecap: round;
  }
  
  .media-360-viewer__loader--background,
  .media-360-viewer__loader--cover {
      fill: transparent;
  }
  
  .media-360-viewer__viewport {
      position: absolute;
      top: 10px;
      left: 10px;
      z-index: 2;
      overflow: hidden;
  }
  
  .media-360-viewer__viewport--image {
      width: 100%;
      pointer-events: none;
  }
  
  .media-360-viewer__viewport--zoom {
      position: absolute;
      bottom: 5px;
      right: 5px;
      color: #fff;
      z-index: 3;
      font-size: 12px;
      pointer-events: none;
  }
  
  .media-360-viewer__viewport--square {
      display: block;
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      box-shadow: rgba(0, 0, 0, 0.8) 0 0 0 10000px;
      cursor: grab;
      transition: background ease-in-out 0.1s;
  }
  
  .media-360-viewer__viewport--square:hover {
      background: rgba(255, 255, 255, 0.2);
  }
  
  .media-360-viewer__tools {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      padding-bottom: 10px;
  }
  
  .media-360-viewer__tools &gt; a {
      margin: 0 5px;
      color: #000;
      background: #fff;
      border-radius: 50%;
      width: 40px;
      text-align: center;
      line-height: 40px;
  }
  
  .media-360-viewer__tools &gt; a[disabled] {
      opacity: .5;
      cursor: not-allowed;
  }
  
  .media-360-viewer__tools &gt; a[disabled]:hover {
      color: #000;
      background: #fff;
  }
  
  .media-360-viewer__tools &gt; a:hover {
      background: #000;
      color: #fff;
  }
  
  .media-360-viewer__tools--autoplay:before {
      font-family: 'ClickIcons';
      content: '\ea81';
  }
  
  .media-360-viewer__tools--autoplay.active:before {
      content: '\eb48';
  }
  
  .fade-enter-active,
  .fade-leave-active {
      transition: opacity .5s;
  }
  
  .fade-enter,
  .fade-leave-to {
      opacity: 0;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"&gt;&lt;/script&gt;
  
  &lt;div id="app"&gt;
    &lt;test&gt;&lt;/test&gt;
  &lt;/div&gt;
  
  &lt;script type="text/x-template" id="template"&gt;
    &lt;div class="media-360-viewer" ref="container"&gt;
      &lt;transition name="fade"&gt;
        &lt;div class="media-360-viewer__viewport" v-if="zoomLevel &gt; 1 &amp;&amp; viewportEnabled" :style="{ width: (viewportScale * 100) + '%' }"&gt;
          &lt;img tabindex="1" draggable="false" alt="Viewport" class="media-360-viewer__viewport--image" src="https://www.bennetts.co.uk/-/media/bikesocial/2019-september-images/2020-yamaha-yzf-r1-and-r1m-review/2020-yamaha-r1-and-r1m_005.ashx?h=493&amp;w=740&amp;la=en&amp;hash=F97CD240F0DDFA9540E912DCF7F07019017035C6"&gt;
          &lt;span class="media-360-viewer__viewport--zoom"&gt;
                      x{{ Math.round(zoomLevel * 10) / 10 }}
                  &lt;/span&gt;
          &lt;span :style="{ transform: viewportTransform }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove($event, true)" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove($event, true)" class="media-360-viewer__viewport--square"&gt;&lt;/span&gt;
        &lt;/div&gt;
      &lt;/transition&gt;
      &lt;img tabindex="1" ref="image" draggable="false" src="https://www.bennetts.co.uk/-/media/bikesocial/2019-september-images/2020-yamaha-yzf-r1-and-r1m-review/2020-yamaha-r1-and-r1m_005.ashx?h=493&amp;w=740&amp;la=en&amp;hash=F97CD240F0DDFA9540E912DCF7F07019017035C6"
        :style="{ transform: transform }" :class="{
                   canTranslate: zoomLevel &gt; 1 &amp;&amp; zoomEnabled,
                   canRotate: zoomLevel === 1,
                   isTranslating: zoomLevel &gt; 1 &amp;&amp; zoomEnabled &amp;&amp; isMoving
               }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove" @dblclick="zoom" @wheel="zoomWheel" alt="360 Image" /&gt;
    &lt;/div&gt;
  &lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>