javascript - 在容器内拖动图像,不使用鼠标移动,稍微快一点

标签 javascript css vue.js

我有一个 Vue 组件,我可以在其中放大图像,然后在容器周围移动它。缩放时还有一个小视口(viewport)来显示图像的哪一部分可见。但是,当在它周围移动图像时,它的移动速度比鼠标快,我猜这是由于使用了 scale转换。
我也觉得当我在视口(viewport)上单击并拖动时,我不应该两次反转值,但这似乎是让它用鼠标移动方 block 的唯一方法。

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) => {
        return Math.abs(b - this.zoomLevel) < 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(() => {
        this.translate(null, this.$refs.image, true);
      });
    },
    zoomWheel($event) {
      $event.preventDefault();

      this.zoomLevel += $event.deltaY * -0.01;

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

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

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

      window.requestAnimationFrame(() => {
        this.translate(null, this.$refs.image, true);
      });
    },
    handleStart($event) {
      $event.preventDefault();
      if ($event.button && $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 && $event.button !== 0) {
        return;
      }
      if (this.isMoving && 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 && $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 <= 0) {
          this.translateX += Math.floor(translate.left);
        }
        if (translate.right >= 0) {
          this.translateX += Math.floor(translate.right);
        }
        if (translate.top <= 0) {
          this.translateY += Math.floor(translate.top);
        }
        if (translate.bottom >= 0) {
          this.translateY += Math.floor(translate.bottom);
        }
      }

      if (translate.left >= 0 && translate.right <= 0) {
        this.translateX += Math.floor(move.x);
      }
      if (translate.top >= 0 && translate.bottom <= 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) => {
        return Math.abs(b - this.zoomLevel) < 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) => {
        return Math.abs(b - this.zoomLevel) < 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) => {
        return Math.abs(b - this.zoomLevel) < Math.abs(a - this.zoomLevel) ? b : a;
      });

      return this.zoomLevels[this.zoomLevels.indexOf(closest) + -1] === undefined
    }
  }
});

window.vue = new Vue({el: '#app'});
.media-360-viewer {
  position: relative;
  overflow: hidden;
  background: #000;
}

.media-360-viewer>img {
  width: 100%;
}

.media-360-viewer>img.canTranslate {
  cursor: grab;
}

.media-360-viewer>img.isTranslating {
  cursor: grabbing;
}

.media-360-viewer>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>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>a {
  margin: 0 5px;
  color: #000;
  background: #fff;
  border-radius: 50%;
  width: 40px;
  text-align: center;
  line-height: 40px;
}

.media-360-viewer__tools>a[disabled] {
  opacity: .5;
  cursor: not-allowed;
}

.media-360-viewer__tools>a[disabled]:hover {
  color: #000;
  background: #fff;
}

.media-360-viewer__tools>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';
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <test></test>
</div>

<script type="text/x-template" id="template">
  <div class="media-360-viewer" ref="container">
    <transition name="fade">
      <div class="media-360-viewer__viewport" v-if="zoomLevel > 1 && viewportEnabled" :style="{ width: (viewportScale * 100) + '%' }">
        <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&w=740&la=en&hash=F97CD240F0DDFA9540E912DCF7F07019017035C6">
        <span class="media-360-viewer__viewport--zoom">
                    x{{ Math.round(zoomLevel * 10) / 10 }}
                </span>
        <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"></span>
      </div>
    </transition>
    <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&w=740&la=en&hash=F97CD240F0DDFA9540E912DCF7F07019017035C6" :style="{ transform: transform }" :class="{
                 canTranslate: zoomLevel > 1 && zoomEnabled,
                 canRotate: zoomLevel === 1,
                 isTranslating: zoomLevel > 1 && zoomEnabled && isMoving
             }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove" @dblclick="zoom" @wheel="zoomWheel" alt="360 Image" />
  </div>
</script>

有谁知道我该如何解决这个问题?
编辑
以下是更新后的代码,其中包含已接受答案所建议的更改,现在唯一的问题是视口(viewport)方 block 超出框架。

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) => {
        return Math.abs(b - this.zoomLevel) < 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 < 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 && $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 && $event.button !== 0) {
        return;
      }

      if (this.isMoving && 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 && $event.button !== 0) {
        return;
      }
      this.isMoving = false;
    },
    translate(positions, element, zooming, viewport) {
      window.requestAnimationFrame(() => {
        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 <= 0 && translate.right >= 0) {
        this.translateX -= move.x / this.viewportScale;
      }

      if (translate.top <= 0 && translate.bottom >= 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 <= 0) {
          this.translateX += translate.left;
        }
        if (translate.right >= 0) {
          this.translateX += translate.right;
        }
        if (translate.top <= 0) {
          this.translateY += translate.top;
        }
        if (translate.bottom >= 0) {
          this.translateY += translate.bottom;
        }
      }

      if (translate.left >= 0 && translate.right <= 0) {
        this.translateX += move.x / this.zoomLevel;
      }

      if (translate.top >= 0 && translate.bottom <= 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) => {
        return Math.abs(b - this.zoomLevel) < 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) => {
        return Math.abs(b - this.zoomLevel) < 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) => {
        return Math.abs(b - this.zoomLevel) < Math.abs(a - this.zoomLevel) ? b : a;
      });

      return this.zoomLevels[this.zoomLevels.indexOf(closest) + -1] === undefined
    }
  }
});

window.vue = new Vue({
  el: '#app'
});
.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 > 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 > a {
    margin: 0 5px;
    color: #000;
    background: #fff;
    border-radius: 50%;
    width: 40px;
    text-align: center;
    line-height: 40px;
}

.media-360-viewer__tools > a[disabled] {
    opacity: .5;
    cursor: not-allowed;
}

.media-360-viewer__tools > a[disabled]:hover {
    color: #000;
    background: #fff;
}

.media-360-viewer__tools > 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;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <test></test>
</div>

<script type="text/x-template" id="template">
  <div class="media-360-viewer" ref="container">
    <transition name="fade">
      <div class="media-360-viewer__viewport" v-if="zoomLevel > 1 && viewportEnabled" :style="{ width: (viewportScale * 100) + '%' }">
        <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&w=740&la=en&hash=F97CD240F0DDFA9540E912DCF7F07019017035C6">
        <span class="media-360-viewer__viewport--zoom">
                    x{{ Math.round(zoomLevel * 10) / 10 }}
                </span>
        <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"></span>
      </div>
    </transition>
    <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&w=740&la=en&hash=F97CD240F0DDFA9540E912DCF7F07019017035C6"
      :style="{ transform: transform }" :class="{
                 canTranslate: zoomLevel > 1 && zoomEnabled,
                 canRotate: zoomLevel === 1,
                 isTranslating: zoomLevel > 1 && zoomEnabled && isMoving
             }" @mouseup="handleEnd" @mousedown="handleStart" @mousemove="handleMove" @touchstart="handleStart" @touchend="handleEnd" @touchmove="handleMove" @dblclick="zoom" @wheel="zoomWheel" alt="360 Image" />
  </div>
</script>

最佳答案

Math.floor 仅在必要时
这个问题的一部分是调用 Math.floor 的结果。每时每刻。每次调用Math.floor您的下一个计算将不太准确。
如果您仍想对数字进行四舍五入,请仅在计算链的末尾或什至在您使用变量的地方进行。例如:

transform: function() {
  const translateX = Math.floor(this.translateX)
  const translateY = Math.floor(this.translateY)

  return `scale(${this.zoomLevel}) translateX(${translateX}px) translateY(${translateY}px)`;
}
比例因子

However when moving the image around it is moving faster than the mouse


这可以通过将要添加到转换值的数字除以比例因子来解决,比例因子由 zoomLevel 确定像这样:
if (translate.left >= 0 && translate.right <= 0) {
    this.translateX += move.x / this.zoomLevel
}

if (translate.top >= 0 && translate.bottom <= 0) {
    this.translateY += move.y / this.zoomLevel
}
缩放因子 - 视口(viewport)
由于缩放,视口(viewport)仍然以某种方式损坏,我们也需要调整平移值,但这次不使用 zoomLevel但是存储在 this.viewportScale 中的视口(viewport)的比例.因此,将两个解决方案合并在一起,我们现在有如下代码:
if (translate.left >= 0 && translate.right <= 0) {
    if (viewport) {
        this.translateX += move.x / this.viewportScale
    } else {
        this.translateX += move.x / this.zoomLevel
    }
}

if (translate.top >= 0 && translate.bottom <= 0) {
    if (viewport) {
        this.translateY += move.y / this.viewportScale
    } else {
        this.translateY += move.y / this.zoomLevel
    }
}
反转值

I also feel like when i'm clicking and dragging on the viewport I shouldn't be reversing the values twice


我认为没有更好的方法可以做到这一点,除非您想创建两个内部翻译函数来根据输入源(视口(viewport)或图像)处理翻译。这将是一种绝对干净的编码方法。例如,如果将来您想编写一些特定于视口(viewport)的代码,则不需要包含另一个和另一个 if .你的函数可能像这样:
translate (positions, element, zooming, viewport) {
    positions = positions || {
        x: this.startX,
        y: this.startY
    }

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

    this.startX = positions.x
    this.startY = positions.y
}
在哪里 _translateFromViewport_translateFromImage函数保存功能特定的代码。

关于javascript - 在容器内拖动图像,不使用鼠标移动,稍微快一点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63520188/

相关文章:

css - 在 HTML 中隐藏字符?

php - 如何在php中使用html div标签

javascript - VueJS 日期选择器 : how to dynamically disable dates?

mysql - 将原始 SQL 查询与 Sequelize ORM 和文字结合使用

javascript - 如何使用 javascript 更改外部样式表中的背景图像?

javascript setTimeout 无法识别函数参数

javascript - 将新对象添加到本地存储

css - 具有全窗口滚动条高度的固定宽度框

vue.js - NuxtServerInit 无法在 Vuex 模块模式下工作 - Nuxt.js

Javascript - 单击动态创建的元素时提醒内容