Проблема с пинч-зумом в компоненте Vue

#javascript #vue.js

#javascript #vue.js

Вопрос:

у меня есть компонент Vue, который позволяет мне увеличивать изображение, с помощью колесика мыши работает нормально, однако увеличенный масштаб ведет себя странно.

В принципе, если вы поместите 2 пальца на экран далеко друг от друга, это сильно увеличит масштаб, поместите их ближе друг к другу, и это немного увеличит масштаб. Что должно произойти, так это то, что он не увеличивает или уменьшает масштаб, пока вы на самом деле не переместите пальцы внутрь или наружу.

Есть идеи?

 Vue.component('test', {
  data() {
    return {
      loading: false,

      loop: true,

      speed: 8,

      speedController: 0,

      zoomEnabled: true,
      zoomLevels: [1, 1.5, 2, 2.5, 3],
      zoomLevel: 1,

      frame: 1,
      images: [],
      imagesPreloaded: 0,

      spinEnabled: true,
      spinAuto: false,

      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,

      lastPinch: 0,

      animationRequestID: 0,

      spinStart: null,
      spinThen: Date.now(),
      fps: 1000 / 8,

      axiosRequest: null,

      $clickEvent: null,
      $moveEvent: null,

      output: ''
    };
  },
  mounted() {
    window.addEventListener('mouseup', this.handleEnd);
    window.addEventListener('touchend', this.handleEnd);
    window.addEventListener('resize', this.fetch);
  },
  beforeDestroy() {
    window.removeEventListener('mouseup', this.handleEnd);
    window.removeEventListener('touchend', this.handleEnd);
  },
  methods: {
    handleSlider(event) {
      this.frame = Number(event.target.value);
    },

    zoom(direction) {
      if (this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   direction] === undefined) {
        return;
      }

      let current = this.zoomLevels.indexOf(this.closestZoom);
      let index = current  = direction;
      if (direction === 0) {
        index = 0;
      }
      this.zoomLevel = this.zoomLevels[index];

      this.translate(null, true);
    },
    zoomWheel($event) {
      this.zoomLevel  = $event.deltaY * -0.01;

      if (this.zoomLevel < 1) {
        this.zoomLevel = 1;
      }

      $event.preventDefault();

      let maxZoom = this.zoomLevels[this.zoomLevels.length - 1];

      this.zoomLevel = Math.min(Math.max(.125, this.zoomLevel), maxZoom);

      this.translate(null, true);
    },
    zoomPinch($event) {

      let curDiff = Math.abs($event.touches[0].clientX - $event.touches[1].clientX);

      $event.deltaY = this.lastPinch - curDiff;

      this.zoomWheel($event);

      this.lastPinch = curDiff;
    },
    handleStart($event) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      this.$clickEvent = $event;

      if (this.animationRequestID !== 0) {
        this.spinStop();
      }
      this.isMoving = true;
      this.isDragging = true;

      // this.startTouchX = [ $event.touches[0].clientX, $event.touches[1].clientX ];
      // this.startTouchY = [ [ $event.touches[0].clientY, $event.touches[1].clientY ] ];

      this.startX = this.$clickEvent.pageX || this.$clickEvent.touches[0].pageX;
      this.startY = this.$clickEvent.pageY || this.$clickEvent.touches[0].pageY;
    },
    handleMove($event, viewport) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      if ($event.touches amp;amp; $event.touches.length > 1) {
        this.zoomPinch($event);
        return;
      }

      this.$moveEvent = $event;

      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, null, viewport);
        }
        if (this.zoomLevel === 1) {
          this.changeFrame(positions);
        }

        this.lastX = positions.x;
        this.lastY = positions.y;
      }
    },
    handleEnd($event) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      this.isMoving = false;
    },

    spin(index) {
      let i = index;
      if (i >= this.images.length) {
        i = 1;
      }
      this.animationRequestID = window.requestAnimationFrame(() => this.spin(i));

      let now = Date.now();
      let elapsed = now - this.spinThen;

      if (elapsed > this.fps) {
        this.spinThen = now - (elapsed % this.fps);
        this.frame = i;
        i  = 1;
      }
    },
    spinToggle() {
      if (this.animationRequestID === 0 amp;amp; this.zoomLevel === 1) {
        this.spin(this.frame);
        return;
      }
      this.spinStop();
    },
    spinStop() {
      if (this.animationRequestID) {
        window.cancelAnimationFrame(this.animationRequestID);
        this.animationRequestID = 0;
      }
    },

    translate(positions, zooming, viewport) {
      if (this.$moveEvent) {
        this.$moveEvent.preventDefault();
      }
      window.requestAnimationFrame(() => {
        positions = positions || {
          x: this.startX,
          y: this.startY
        };

        if (viewport) {
          this._translateFromViewport(positions);
        } else {
          this._translateFromImage(positions, zooming);
        }

        this.startX = positions.x;
        this.startY = positions.y;
      });
    },

    /**
     * @param positions
     * @private
     */
    _translateFromViewport: function(positions) {
      let move = {
        x: Math.floor(positions.x - this.startX),
        y: Math.floor(positions.y - this.startY)
      };

      let box = this.$refs.viewportBox.getBoundingClientRect();
      let container = this.$refs.viewportContainer.getBoundingClientRect();

      // Amount of pixels moved within animation frame, adjust based on viewport scale.
      // Zoom level doesn't matter as image scale doesn't move, so box is moving same amount of pixels.
      let moveAmountX = (move.x / this.viewportScale);
      let moveAmountY = (move.y / this.viewportScale);

      // Find the current offset of the container bounds, calculate new offset based on movement amount
      let calculatedOffset = {
        left: (container.left - box.left) - moveAmountX,
        right: (container.right - box.right) - moveAmountX,
        top: (container.top - box.top) - moveAmountY,
        bottom: (container.bottom - box.bottom) - moveAmountY
      };

      this.output = JSON.stringify(calculatedOffset);

      // Only move if the calculated new offset is not out of container bounds
      // Reverse the movement as moving box in same direction as cursor rather than the image.
      if (calculatedOffset.left <= 0 amp;amp; calculatedOffset.right >= 0) {
        this.translateX  = -moveAmountX;
      }
      if (calculatedOffset.top <= 0 amp;amp; calculatedOffset.bottom >= 0) {
        this.translateY  = -moveAmountY;
      }

    },
    _translateFromImage: function(positions, zooming) {
      let move = {
        x: Math.floor(positions.x - this.startX),
        y: Math.floor(positions.y - this.startY)
      };

      let image = this.$refs.image.getBoundingClientRect();
      let container = this.$refs.container.getBoundingClientRect();

      let moveAmountX = move.x * this.zoomLevel;
      let moveAmountY = move.y * this.zoomLevel;

      let calculatedOffset = {
        left: (container.left - image.left) - moveAmountX,
        right: (container.right - image.right) - moveAmountX,
        top: (container.top - image.top) - moveAmountY,
        bottom: (container.bottom - image.bottom) - moveAmountY
      };

      if (zooming) {
        if (calculatedOffset.left <= 0) {
          this.translateX  = calculatedOffset.left;
        }
        if (calculatedOffset.right >= 0) {
          this.translateX  = calculatedOffset.right;
        }
        if (calculatedOffset.top <= 0) {
          this.translateY  = calculatedOffset.top;
        }
        if (calculatedOffset.bottom >= 0) {
          this.translateY  = calculatedOffset.bottom;
        }
      }

      if (calculatedOffset.left >= 0 amp;amp; calculatedOffset.right <= 0) {
        this.translateX  = move.x / this.zoomLevel;
      }

      if (calculatedOffset.top >= 0 amp;amp; calculatedOffset.bottom <= 0) {
        this.translateY  = move.y / this.zoomLevel;
      }
    },

    changeFrame(positions) {
      this.speedController  = 1;
      if (this.speedController < this.speed) {
        return;
      }
      if (this.speedController > this.speed) {
        this.speedController = 0;
      }

      if (positions.x > this.lastX) {
        if (this.frame >= 0 amp;amp; this.frame < this.images.length) {
          this.frame  = 1;
        } else if (this.loop) {
          this.frame = 1;
        }
      } else if (positions.x < this.lastX) {
        if (this.frame >= 0 amp;amp; this.frame - 1 > 0) {
          this.frame -= 1;
        } else if (this.loop) {
          this.frame = this.images.length;
        }
      }
    }
  },
  watch: {
    zoomLevel: function() {
      if (this.zoomLevel !== 1 amp;amp; this.animationRequestID !== 0) {
        this.spinStop();
      }
    }
  },
  computed: {
    closestZoom: function() {
      return this.zoomLevels.reduce((a, b) => {
        return Math.abs(b - this.zoomLevel) < Math.abs(a - this.zoomLevel) ? b : a;
      });
    },
    imageSet: function() {
      return this.images.map(image => {
        return image[this.closestZoom].url;
      });
    },
    preloadProgress: function() {
      return Math.floor(this.imagesPreloaded / this.images.length * 100);
    },
    currentPath: function() {
      return this.images[this.frame - 1][this.closestZoom].url;
    },
    nextZoomLevel: function() {
      if (this.zoomLevels.indexOf(this.closestZoom) === this.zoomLevels.length - 1) {
        return this.zoomLevels[0];
      }
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   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() {
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   1] === undefined
    },
    canZoomOut: function() {
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   -1] === undefined
    }
  },
  template: '#template'
});

window.vue = new Vue({}).$mount('#app');  
 .media-360-viewer {
  position: relative;
  overflow: hidden;
  display: inline-block;
  background: #000;
  width: 100%;
  transition: filter .2s ease-in-out;
  amp;__image {
    width: 100%;
    cursor: grab;
    amp;.isTranslating {
      cursor: grabbing;
    }
    amp;.loading {
      filter: blur(4px);
    }
  }
  amp;__loader {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, .5);
    * {
      user-select: none;
    }
    amp;>svg {
      width: 100%;
      height: 100%;
      transform: rotate(270deg);
    }
    amp;--text {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      p {
        font-size: 100%;
        font-weight: bold;
        color: #fff;
        amp;.large {
          font-size: 150%;
        }
      }
    }
    amp;--background {
      stroke-dasharray: 0;
      stroke-dashoffset: 0;
      stroke: rgba(0, 0, 0, .7);
      stroke-width: 25px;
    }
    amp;--cover {
      stroke-dasharray: 200%;
      stroke: #848484;
      stroke-width: 15px;
      stroke-linecap: round;
    }
    amp;--background,
    amp;--cover {
      fill: transparent;
    }
  }
  amp;__viewport {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    overflow: hidden;
    amp;--image {
      width: 100%;
      pointer-events: none;
    }
    amp;--zoom {
      position: absolute;
      bottom: 5px;
      right: 5px;
      color: #fff;
      z-index: 3;
      font-size: 12px;
      pointer-events: none;
    }
    amp;--square {
      display: block;
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      box-shadow: rgba(0, 0, 0, .6) 0 0 0 10000px;
      cursor: grab;
      transition: background ease-in-out .1s;
      amp;:hover {
        background: rgba(255, 255, 255, .2);
      }
    }
  }
  amp;__tools {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-bottom: 10px;
    amp;>a {
      margin: 0 5px;
      color: #000;
      background: #fff;
      border-radius: 50%;
      width: 40px;
      text-align: center;
      line-height: 40px;
      amp;[disabled] {
        opacity: .5;
        cursor: not-allowed;
        amp;:hover {
          color: #000;
          background: #fff;
        }
      }
      amp;:hover {
        background: #000;
        color: #fff;
      }
    }
    amp;--autoplay {
      amp;:before {
        font-family: 'ClickIcons';
        content: 'ea81';
      }
      amp;.active:before {
        content: 'eb48';
      }
    }
  }
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity .5s;
}

.fade-enter,
.fade-leave-to {
  opacity: 0;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <test class="test"></test>
</div>

<script type="text/x-template" id="template">
  <div>
    <div class="media-360-viewer" ref="container">
      <img tabindex="1" ref="image" draggable="false" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/styles/amp_1200/s3/2020_YAM_YZF1000R1SPL_EU_BWM2_STA_001-70560.jpg?itok=5bisLKmj" :style="{ transform: transform }" class="media-360-viewer__image"
        @touchend="handleEnd" @touchmove="handleMove" @touchstart="handleStart" @wheel="zoomWheel" alt="360 Image" />
    </div>
  </div>
</script>  

Ответ №1:

Я не уверен, что правильно понимаю вашу проблему, но, как я пытаюсь, я думаю, что проблема вашего кода в этой строке:

 $event.deltaY = this.lastPinch - curDiff;
  

Кажется, что this.lastPinch удерживается дельта предыдущего touchmove события, поэтому при первом событии вы должны игнорировать его и уточнить, когда touchend .

 ...
  zoomPinch ($event) {
    ...
    if (this.lastPinch) {
      $event.deltaY = this.lastPinch - curDiff;
      this.zoomWheel($event);
    }
    ...
  }
...
  handleEnd ($event) {
    ...
    this.lastPinch = 0
  }
...
  

 Vue.component('test', {
  data() {
    return {
      loading: false,

      loop: true,

      speed: 8,

      speedController: 0,

      zoomEnabled: true,
      zoomLevels: [1, 1.5, 2, 2.5, 3],
      zoomLevel: 1,

      frame: 1,
      images: [],
      imagesPreloaded: 0,

      spinEnabled: true,
      spinAuto: false,

      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,

      lastPinch: 0,

      animationRequestID: 0,

      spinStart: null,
      spinThen: Date.now(),
      fps: 1000 / 8,

      axiosRequest: null,

      $clickEvent: null,
      $moveEvent: null,

      output: ''
    };
  },
  mounted() {
    window.addEventListener('mouseup', this.handleEnd);
    window.addEventListener('touchend', this.handleEnd);
    window.addEventListener('resize', this.fetch);
  },
  beforeDestroy() {
    window.removeEventListener('mouseup', this.handleEnd);
    window.removeEventListener('touchend', this.handleEnd);
  },
  methods: {
    handleSlider(event) {
      this.frame = Number(event.target.value);
    },

    zoom(direction) {
      if (this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   direction] === undefined) {
        return;
      }

      let current = this.zoomLevels.indexOf(this.closestZoom);
      let index = current  = direction;
      if (direction === 0) {
        index = 0;
      }
      this.zoomLevel = this.zoomLevels[index];

      this.translate(null, true);
    },
    zoomWheel($event) {
      this.zoomLevel  = $event.deltaY * -0.01;

      if (this.zoomLevel < 1) {
        this.zoomLevel = 1;
      }

      $event.preventDefault();

      let maxZoom = this.zoomLevels[this.zoomLevels.length - 1];

      this.zoomLevel = Math.min(Math.max(.125, this.zoomLevel), maxZoom);

      this.translate(null, true);
    },
    zoomPinch($event) {

      let curDiff = Math.abs($event.touches[0].clientX - $event.touches[1].clientX);

      if (this.lastPinch) {
        $event.deltaY = this.lastPinch - curDiff;
        this.zoomWheel($event);
      }

      this.lastPinch = curDiff;
    },
    handleStart($event) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      this.$clickEvent = $event;

      if (this.animationRequestID !== 0) {
        this.spinStop();
      }
      this.isMoving = true;
      this.isDragging = true;

      // this.startTouchX = [ $event.touches[0].clientX, $event.touches[1].clientX ];
      // this.startTouchY = [ [ $event.touches[0].clientY, $event.touches[1].clientY ] ];

      this.startX = this.$clickEvent.pageX || this.$clickEvent.touches[0].pageX;
      this.startY = this.$clickEvent.pageY || this.$clickEvent.touches[0].pageY;
    },
    handleMove($event, viewport) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      if ($event.touches amp;amp; $event.touches.length > 1) {
        this.zoomPinch($event);
        return;
      }

      this.$moveEvent = $event;

      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, null, viewport);
        }
        if (this.zoomLevel === 1) {
          this.changeFrame(positions);
        }

        this.lastX = positions.x;
        this.lastY = positions.y;
      }
    },
    handleEnd($event) {
      if ($event.button amp;amp; $event.button !== 0) {
        return;
      }
      this.isMoving = false;
      this.lastPinch = 0;
    },

    spin(index) {
      let i = index;
      if (i >= this.images.length) {
        i = 1;
      }
      this.animationRequestID = window.requestAnimationFrame(() => this.spin(i));

      let now = Date.now();
      let elapsed = now - this.spinThen;

      if (elapsed > this.fps) {
        this.spinThen = now - (elapsed % this.fps);
        this.frame = i;
        i  = 1;
      }
    },
    spinToggle() {
      if (this.animationRequestID === 0 amp;amp; this.zoomLevel === 1) {
        this.spin(this.frame);
        return;
      }
      this.spinStop();
    },
    spinStop() {
      if (this.animationRequestID) {
        window.cancelAnimationFrame(this.animationRequestID);
        this.animationRequestID = 0;
      }
    },

    translate(positions, zooming, viewport) {
      if (this.$moveEvent) {
        this.$moveEvent.preventDefault();
      }
      window.requestAnimationFrame(() => {
        positions = positions || {
          x: this.startX,
          y: this.startY
        };

        if (viewport) {
          this._translateFromViewport(positions);
        } else {
          this._translateFromImage(positions, zooming);
        }

        this.startX = positions.x;
        this.startY = positions.y;
      });
    },

    /**
     * @param positions
     * @private
     */
    _translateFromViewport: function(positions) {
      let move = {
        x: Math.floor(positions.x - this.startX),
        y: Math.floor(positions.y - this.startY)
      };

      let box = this.$refs.viewportBox.getBoundingClientRect();
      let container = this.$refs.viewportContainer.getBoundingClientRect();

      // Amount of pixels moved within animation frame, adjust based on viewport scale.
      // Zoom level doesn't matter as image scale doesn't move, so box is moving same amount of pixels.
      let moveAmountX = (move.x / this.viewportScale);
      let moveAmountY = (move.y / this.viewportScale);

      // Find the current offset of the container bounds, calculate new offset based on movement amount
      let calculatedOffset = {
        left: (container.left - box.left) - moveAmountX,
        right: (container.right - box.right) - moveAmountX,
        top: (container.top - box.top) - moveAmountY,
        bottom: (container.bottom - box.bottom) - moveAmountY
      };

      this.output = JSON.stringify(calculatedOffset);

      // Only move if the calculated new offset is not out of container bounds
      // Reverse the movement as moving box in same direction as cursor rather than the image.
      if (calculatedOffset.left <= 0 amp;amp; calculatedOffset.right >= 0) {
        this.translateX  = -moveAmountX;
      }
      if (calculatedOffset.top <= 0 amp;amp; calculatedOffset.bottom >= 0) {
        this.translateY  = -moveAmountY;
      }

    },
    _translateFromImage: function(positions, zooming) {
      let move = {
        x: Math.floor(positions.x - this.startX),
        y: Math.floor(positions.y - this.startY)
      };

      let image = this.$refs.image.getBoundingClientRect();
      let container = this.$refs.container.getBoundingClientRect();

      let moveAmountX = move.x * this.zoomLevel;
      let moveAmountY = move.y * this.zoomLevel;

      let calculatedOffset = {
        left: (container.left - image.left) - moveAmountX,
        right: (container.right - image.right) - moveAmountX,
        top: (container.top - image.top) - moveAmountY,
        bottom: (container.bottom - image.bottom) - moveAmountY
      };

      if (zooming) {
        if (calculatedOffset.left <= 0) {
          this.translateX  = calculatedOffset.left;
        }
        if (calculatedOffset.right >= 0) {
          this.translateX  = calculatedOffset.right;
        }
        if (calculatedOffset.top <= 0) {
          this.translateY  = calculatedOffset.top;
        }
        if (calculatedOffset.bottom >= 0) {
          this.translateY  = calculatedOffset.bottom;
        }
      }

      if (calculatedOffset.left >= 0 amp;amp; calculatedOffset.right <= 0) {
        this.translateX  = move.x / this.zoomLevel;
      }

      if (calculatedOffset.top >= 0 amp;amp; calculatedOffset.bottom <= 0) {
        this.translateY  = move.y / this.zoomLevel;
      }
    },

    changeFrame(positions) {
      this.speedController  = 1;
      if (this.speedController < this.speed) {
        return;
      }
      if (this.speedController > this.speed) {
        this.speedController = 0;
      }

      if (positions.x > this.lastX) {
        if (this.frame >= 0 amp;amp; this.frame < this.images.length) {
          this.frame  = 1;
        } else if (this.loop) {
          this.frame = 1;
        }
      } else if (positions.x < this.lastX) {
        if (this.frame >= 0 amp;amp; this.frame - 1 > 0) {
          this.frame -= 1;
        } else if (this.loop) {
          this.frame = this.images.length;
        }
      }
    }
  },
  watch: {
    zoomLevel: function() {
      if (this.zoomLevel !== 1 amp;amp; this.animationRequestID !== 0) {
        this.spinStop();
      }
    }
  },
  computed: {
    closestZoom: function() {
      return this.zoomLevels.reduce((a, b) => {
        return Math.abs(b - this.zoomLevel) < Math.abs(a - this.zoomLevel) ? b : a;
      });
    },
    imageSet: function() {
      return this.images.map(image => {
        return image[this.closestZoom].url;
      });
    },
    preloadProgress: function() {
      return Math.floor(this.imagesPreloaded / this.images.length * 100);
    },
    currentPath: function() {
      return this.images[this.frame - 1][this.closestZoom].url;
    },
    nextZoomLevel: function() {
      if (this.zoomLevels.indexOf(this.closestZoom) === this.zoomLevels.length - 1) {
        return this.zoomLevels[0];
      }
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   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() {
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   1] === undefined
    },
    canZoomOut: function() {
      return this.zoomLevels[this.zoomLevels.indexOf(this.closestZoom)   -1] === undefined
    }
  },
  template: '#template'
});

window.vue = new Vue({}).$mount('#app');  
 .media-360-viewer {
  position: relative;
  overflow: hidden;
  display: inline-block;
  background: #000;
  width: 100%;
  transition: filter .2s ease-in-out;
  amp;__image {
    width: 100%;
    cursor: grab;
    amp;.isTranslating {
      cursor: grabbing;
    }
    amp;.loading {
      filter: blur(4px);
    }
  }
  amp;__loader {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, .5);
    * {
      user-select: none;
    }
    amp;>svg {
      width: 100%;
      height: 100%;
      transform: rotate(270deg);
    }
    amp;--text {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      p {
        font-size: 100%;
        font-weight: bold;
        color: #fff;
        amp;.large {
          font-size: 150%;
        }
      }
    }
    amp;--background {
      stroke-dasharray: 0;
      stroke-dashoffset: 0;
      stroke: rgba(0, 0, 0, .7);
      stroke-width: 25px;
    }
    amp;--cover {
      stroke-dasharray: 200%;
      stroke: #848484;
      stroke-width: 15px;
      stroke-linecap: round;
    }
    amp;--background,
    amp;--cover {
      fill: transparent;
    }
  }
  amp;__viewport {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 2;
    overflow: hidden;
    amp;--image {
      width: 100%;
      pointer-events: none;
    }
    amp;--zoom {
      position: absolute;
      bottom: 5px;
      right: 5px;
      color: #fff;
      z-index: 3;
      font-size: 12px;
      pointer-events: none;
    }
    amp;--square {
      display: block;
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      box-shadow: rgba(0, 0, 0, .6) 0 0 0 10000px;
      cursor: grab;
      transition: background ease-in-out .1s;
      amp;:hover {
        background: rgba(255, 255, 255, .2);
      }
    }
  }
  amp;__tools {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    padding-bottom: 10px;
    amp;>a {
      margin: 0 5px;
      color: #000;
      background: #fff;
      border-radius: 50%;
      width: 40px;
      text-align: center;
      line-height: 40px;
      amp;[disabled] {
        opacity: .5;
        cursor: not-allowed;
        amp;:hover {
          color: #000;
          background: #fff;
        }
      }
      amp;:hover {
        background: #000;
        color: #fff;
      }
    }
    amp;--autoplay {
      amp;:before {
        font-family: 'ClickIcons';
        content: 'ea81';
      }
      amp;.active:before {
        content: 'eb48';
      }
    }
  }
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity .5s;
}

.fade-enter,
.fade-leave-to {
  opacity: 0;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <test class="test"></test>
</div>

<script type="text/x-template" id="template">
  <div>
    <div class="media-360-viewer" ref="container">
      <img tabindex="1" ref="image" draggable="false" src="https://s3-eu-west-1.amazonaws.com/crash.net/visordown.com/styles/amp_1200/s3/2020_YAM_YZF1000R1SPL_EU_BWM2_STA_001-70560.jpg?itok=5bisLKmj" :style="{ transform: transform }" class="media-360-viewer__image"
        @touchend="handleEnd" @touchmove="handleMove" @touchstart="handleStart" @wheel="zoomWheel" alt="360 Image" />
    </div>
  </div>
</script>  

Комментарии:

1. Спасибо, это устранило мою проблему. Я тестировал раньше, и это не сработало, но я забыл скомпилировать код! Но теперь это работает, я скомпилировал!