Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag image within container, not moving with mouse, slightly quicker

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 scale transform.

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.

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>

Does anyone know how I can fix this?

Edit

Below is the updated code with the changes suggested by the accepted answer, only issue now is the viewport square going out of frame.

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>
like image 327
Martyn Ball Avatar asked Aug 21 '20 09:08

Martyn Ball


1 Answers

Math.floor only when necessary

A part of this problem is a result of calling Math.floor all the time. Each time you call Math.floor your next calculations will be less accurate.

If you still want to round numbers, do it only at the end of your calculations chain or even in the place where you are using the variable. For example:

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

  return `scale(${this.zoomLevel}) translateX(${translateX}px) translateY(${translateY}px)`;
}

Scaling factor

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

This can be fixed by dividing number you're adding to the translate value by scale factor, which is determined by zoomLevel like so:

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
}

Scaling factor - viewport

Viewport is still somehow broken too because of scaling, we need to adjust translation value too, but this time not using zoomLevel but scale of the viewport which is stored in this.viewportScale. So, merging two of solutions together we now have code like this:

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
    }
}

Reversing values

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

I don't think that there is a better way to do it unless you want to make two inner translate functions that will handle translation depending on the input source (viewport or image). It will be definetely cleaner approach to coding. For example if in future you would like to write some viewport specific code you will not need to include another and another if. Your function could like like so:

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
}

where _translateFromViewport and _translateFromImage functions hold feature specific code.

like image 64
Magiczne Avatar answered Nov 14 '22 21:11

Magiczne