平移缩放图像的问题

时间:2018-07-16 16:48:58

标签: javascript css scaling css-transforms panning

我正在构建一个图像裁剪工具,使您可以在边框内缩放和平移。

在下面的示例(全屏最佳视图)中,您将看到以100%比例缩放的图像开始,并且可以使用鼠标来平移图像。您不应将其拖动到边界框的任何边缘上。

如果使用边界框底部的滑块,则应该能够上下拖动它以将其大小调整为与边界框一样小。调整大小应始终锚定图像,使其缩放到边界框的中心。

我的问题是,当您缩放到100%以下时,将无法平移图像。我认为这是由于将变换源应用于平移叠加层,但我确实很困惑。

我已经尝试解决这个问题一个星期了,因此,如果有人能解决,我会为您买啤酒。也很乐意在需要时添加更多详细信息!

var ImageCropper = (function() {
  var outerContainer = document.querySelector('#cropper-outer-container')
  var frameContainer = outerContainer.querySelector('#frame-container')
  var mediaContainer = outerContainer.querySelector('#cropper-media-container')
  var panOverlay = outerContainer.querySelector('#cropper-pan-overlay')
  var scaleRange = outerContainer.querySelector('#cropper-scale-range')
  var scaleHandle = outerContainer.querySelector('#cropper-scale-handle')

  var boundPanMouseDown, boundPanMouseMove, boundPanMouseUp,
      boundScaleMouseDown, boundScaleMouseMove, boundScaleMouseUp

  var requestAnimationFrameWithLeadingCall = function(func, context) {
    var initialCall = false
    return function() {
      var args = arguments
      if (!initialCall) {
        initialCall = true
        return func.apply(context, args)
      }

      window.requestAnimationFrame(function() {
        func.apply(context, args)
      })
    }
  }

  var Cropper = function(options) {
    this.imageDimensions = options.imageDimensions
    this.dimensionRequirements = options.dimensionRequirements

    boundPanMouseDown = panMouseDown.bind(this)
    boundPanMouseMove = panMouseMove.bind(this)
    boundPanMouseUp = panMouseUp.bind(this)
    boundScaleMouseDown = scaleMouseDown.bind(this)
    boundScaleMouseMove = scaleMouseMove.bind(this)
    boundScaleMouseUp = scaleMouseUp.bind(this)

    panOverlay.style.width = this.imageDimensions.width + 'px'
    panOverlay.style.height = this.imageDimensions.height + 'px'

    // x - the CSS pixel offset from the left
    // y - the CSS pixel offset from the top
    // s - the CSS 0-1 scale for the media
    this.transform = { x: 0, y: 0, s: 1 }

    // lastHandleX - the last known CSS pixel offset of
    //   the scale handle from the left
    // scalePercentage - the 1-100% scale for the media
    this.scaling = { active: false, lastHandleX: 0, scalePercentage: 0 }

    // lastPanX - the last known CSS pixel offset of
    //   the panned media from the left
    // lastPanY - the last known CSS pixel offset of
    //   the panned media from the top
    this.panning = { active: false, lastPanX: 0, lastPanY: 0 }

    panOverlay.addEventListener('mousedown', boundPanMouseDown)
    panOverlay.addEventListener('dragstart', function() { return false })
    scaleHandle.addEventListener('mousedown', boundScaleMouseDown)
    scaleHandle.addEventListener('dragstart', function() { return false })

    setInitialPosition.call(this)
  }

  var updateCoordinates = function(keys, event) {
    keys = keys || []

    switch (true) {
    case keys.indexOf('overlay') > -1:
      var overlayRect = panOverlay.getBoundingClientRect()

      this.panning.overlay = {
        width: overlayRect.width,
        height: overlayRect.height,
        left: overlayRect.left,
        top: overlayRect.top,
        offsetX: (function() {
          if (!event) { return 0 }

          if (event.clientX) {
            return overlayRect.left - event.clientX
          }
        })(),
        offsetY: (function() {
          if (!event) { return 0 }

          if (event.clientY) {
            return overlayRect.top - event.clientY
          }
        })()
      }
    case keys.indexOf('bounds') > -1:
      var boundsRect = frameContainer.getBoundingClientRect()

      this.panning.bounds = {
        width: boundsRect.width,
        height: boundsRect.height,
        top: boundsRect.top,
        left: boundsRect.left,
        right: boundsRect.right,
        bottom: boundsRect.top + boundsRect.height
      }
    case keys.indexOf('scale') > -1:
      var rangeRect = scaleRange.getBoundingClientRect()

      this.scaling.range = {
        left: rangeRect.left,
        right: rangeRect.right,
        width: rangeRect.width
      }
    case keys.indexOf('handle') > -1:
      var handleRect = scaleHandle.getBoundingClientRect()

      this.scaling.handle = {
        left: handleRect.left,
        width: handleRect.width,
        offsetX: (function() {
          if (!event) { return 0 }

          if (event.clientX) {
            return handleRect.left - event.clientX
          }
        })()
      }
    }
  }

  var performTransformationsInternals = function(transformations) {
    transformations = transformations || {}

    var desiredPanX = transformations.x || this.transform.x
    var desiredPanY = transformations.y || this.transform.y
    var desiredScale = transformations.s

    this.transform.x = Math.ceil(desiredPanX)
    this.transform.y = Math.ceil(desiredPanY)
    this.transform.s = desiredScale || this.transform.s

    // Apply cropper and media transformations
    var transforms = [
      'translate(' + this.transform.x + 'px, ' + this.transform.y + 'px)',
      'scale(' + this.transform.s + ')'
    ].join(' ')

    panOverlay.style.transform = transforms
    mediaContainer.style.transform = transforms
  }

  var performTransformations = requestAnimationFrameWithLeadingCall(function(context, transformations) {
    performTransformationsInternals.call(context, transformations)
  })

  var updateTransformOrigin = function() {
    updateCoordinates.call(this, ['overlay', 'bounds'])

    var originX = ((this.panning.bounds.left - this.panning.overlay.left) / (this.panning.overlay.width - this.panning.bounds.width)) * 100
    var originY = ((this.panning.bounds.top - this.panning.overlay.top) / (this.panning.overlay.height - this.panning.bounds.height)) * 100

    var newOrigin = originX + '% ' + originY + '%'

    panOverlay.style.transformOrigin = newOrigin
    mediaContainer.style.transformOrigin = newOrigin
  }

  var setInitialPosition = function() {
    updateCoordinates.call(this, ['overlay', 'bounds', 'scale', 'handle'])

    var scale = 1
    var percentage = 1

    var median, scale

    if (this.imageDimensions.orientation === 'landscape') {
      median = this.dimensionRequirements.minHeight +
        (this.imageDimensions.height - this.dimensionRequirements.minHeight) * percentage
      scale = median / this.imageDimensions.height
    } else if (this.imageDimensions.orientation === 'portrait') {
      median = this.dimensionRequirements.minWidth +
        (this.imageDimensions.width - this.dimensionRequirements.minWidth) * percentage
      scale = median / this.imageDimensions.width
    }

    var handleX = this.scaling.range.width - (this.scaling.handle.width / 2)
    var panX = (this.panning.bounds.width - this.panning.overlay.width) / 2
    var panY = 0 - ((this.imageDimensions.height - (this.imageDimensions.height * scale)) / 2)

    scaleHandle.style.transform = 'translateX(' + handleX + 'px)'

    this.scaling.percentage = percentage
    this.scaling.lastHandleX = handleX
    this.panning.lastPanX = panX
    this.panning.lastPanY = panY

    performTransformations(this, {
      x: panX,
      y: panY,
      s: scale
    })

    updateTransformOrigin.call(this)
  }

  var calculatePanning = function(event) {
    var pageX = event.pageX
    var pageY = event.pageY
    var desiredX, desiredY

    // Don't allow panning below top of cropper bounds
    if (pageX + this.panning.overlay.offsetX >= this.panning.bounds.left) {
      desiredX = (this.imageDimensions.width - this.panning.overlay.width) / 2
    // Don't allow panning before right side of cropper bounds
    } else if (pageX + this.panning.overlay.offsetX + this.panning.overlay.width <= this.panning.bounds.right) {
      desiredX = this.panning.bounds.width - this.panning.overlay.width
      desiredX = desiredX - ((this.imageDimensions.width - this.panning.overlay.width) / 2)
    } else {
     desiredX = (pageX - this.panning.overlay.left + this.panning.overlay.offsetX) + this.panning.lastPanX
    }

    // Don't allow panning past left side of cropper bounds
    if (pageY + this.panning.overlay.offsetY >= this.panning.bounds.top) {
      desiredY = (this.imageDimensions.height - this.panning.overlay.height) / 2
    // Don't allow panning above bottom of cropper bounds
    } else if (pageY + this.panning.overlay.offsetY + this.panning.overlay.height <= this.panning.bounds.bottom) {
      desiredY = this.panning.bounds.height - this.panning.overlay.height
      desiredY = desiredY - ((this.imageDimensions.height - this.panning.overlay.height) / 2)
    } else {
      desiredY = (pageY - this.panning.overlay.top + this.panning.overlay.offsetY) + this.panning.lastPanY
    }

    return { x: desiredX, y: desiredY }
  }

  /**
   * Panning
   */
  var panMouseDown = function(event) {
    event.preventDefault()

    if (this.panning.active) { return }
    this.panning.active = true

    updateCoordinates.call(this, ['overlay', 'bounds'], event)

    window.addEventListener('mousemove', boundPanMouseMove)
    window.addEventListener('mouseup', boundPanMouseUp)
  }

  var panMouseMove = function(event) {
    event.preventDefault()

    if (!this.panning.active) { return }

    var newCoords = calculatePanning.call(this, event)

    performTransformations(this, {
      x: newCoords.x,
      y: newCoords.y
    })
  }

  var panMouseUp = function(event) {
    this.panning.active = false

    var newCoords = calculatePanning.call(this, event)
    this.panning.lastPanX = newCoords.x
    this.panning.lastPanY = newCoords.y

    updateTransformOrigin.call(this)

    window.removeEventListener('mousemove', boundPanMouseMove)
    window.removeEventListener('mouseup', boundPanMouseUp)
  }

  /**
   * Scaling
   */
  var scaleMouseDown = function(event) {
    event.preventDefault()

    if (this.scaling.active) { return }
    this.scaling.active = true

    updateCoordinates.call(this, ['scale', 'handle'], event)

    window.addEventListener('mousemove', boundScaleMouseMove)
    window.addEventListener('mouseup', boundScaleMouseUp)
  }

  var scaleMouseMove = function(event) {
    event.preventDefault()

    if (!this.scaling.active) { return }

    var pageX = event.pageX
    var handleX = (pageX - this.scaling.handle.left + this.scaling.handle.offsetX) + this.scaling.lastHandleX

    // Don't allow a negative handleX value
    if (handleX <= 0) {
      handleX = 0
    // Don't allow a handleX value that would exceed the width of the range
    } else if (handleX >= this.scaling.range.width - this.scaling.handle.width + 11) {
      handleX = this.scaling.range.width - 14
    }

    scaleHandle.style.transform = 'translateX(' + handleX + 'px)'

    // This scale amount is not the CSS transform scale, but the percentage
    // of the total width of the media to scale
    var percentage = handleX / (this.scaling.range.width - this.scaling.handle.width + 11)
    
    // Now we can use the percentage to calculate a CSS transform scale
    var median, scale

    if (this.imageDimensions.orientation === 'landscape') {
      median = this.dimensionRequirements.minHeight +
        (this.imageDimensions.height - this.dimensionRequirements.minHeight) * percentage
      scale = median / this.imageDimensions.height
    } else if (this.imageDimensions.orientation === 'portrait') {
      median = this.dimensionRequirements.minWidth +
        (this.imageDimensions.width - this.dimensionRequirements.minWidth) * percentage
      scale = median / this.imageDimensions.width
    }

    performTransformations(this, {
      s: scale
    })
  }

  var scaleMouseUp = function(event) {
    this.scaling.active = false

    updateCoordinates.call(this, ['scale', 'handle'], event)

    var newHandleX = this.scaling.handle.left - this.scaling.range.left + 5
    this.scaling.lastHandleX = newHandleX

    window.removeEventListener('mousemove', boundScaleMouseMove)
    window.removeEventListener('mouseup', boundScaleMouseUp)
  }

  return Cropper
})()

new ImageCropper({
  imageDimensions: {
    width: 1275,
    height: 1700,
    orientation: 'portrait',
  },
  dimensionRequirements: {
    minWidth: 400,
    minHeight: 300
  }
})
#cropper-outer-container {
  position: relative;
  max-width: 400px;
  margin: 50px auto 0 auto;
}

#frame-container {
  position: relative;
  max-width: 400px;
  box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.1);
  border-radius: 3px;
  text-align: center;
  box-sizing: border-box;
  will-change: border-bottom-width, box-shadow;
  transition: background-color 0.09s ease-in, border-bottom-width 0.09s ease-in, border-color 0.09s ease-in;
}

#sizer-image {
  pointer-events: none;
  user-select: none;
  width: 100%;
  height: auto;
  vertical-align: bottom;
  opacity: 0;
}

#cropper-pan-overlay {
  position: absolute;
  z-index: 1;
  cursor: move;
  cursor: grab;
  cursor: -webkit-grab;
}

#cropper-media-container {
  position: absolute;
}

#cropper-scale-container {
  position: absolute;
  bottom: 19px;
  left: 50%;
  width: 140px;
  height: 8px;
  z-index: 2;
  transform: translateX(-50%);
  transition: all 0.09s ease-in;
}

#cropper-scale-range {
  position: absolute;
  top: 0;
  left: 5px;
  width: 130px;
  height: 8px;
  border-radius: 6px;
  background: green;
  box-shadow: inset 0 0 0 2px white;
}

#cropper-scale-handle {
  position: absolute;
  top: -8px;
  left: 0;
  width: 25px;
  height: 25px;
  background: transparent;
  border: none;
  padding: 0;
  cursor: ew-resize;
}
#cropper-scale-handle:before {
  content: '';
  width: 14px;
  height: 14px;
  background: red;
  position: absolute;
  top: 5px;
  left: 5px;
  border-radius: 50%;
  transition: all 0.07s linear;
}
#cropper-scale-handle:hover, #cropper-scale-handle:focus {
  outline: none;
}
#cropper-scale-handle:hover:before, #cropper-scale-handle:focus:before {
  background-color: red;
  transform: scale(1.2);
}
<div id="cropper-outer-container">
  <div id="cropper-scale-container">
    <div id="cropper-scale-range"></div>
    <button id="cropper-scale-handle" type="button"></button>
  </div>

  <div id="cropper-pan-overlay"></div>
  <div id="cropper-media-container">
    <img src="https://images.unsplash.com/photo-1531514381259-8c9fedc910b8?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d533a87c6828d114a01512418b54e935&auto=format&fit=crop&w=1275&q=80">
  </div>

  <div id="frame-container">
    <img id="sizer-image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsAQMAAADXeXeBAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAACVJREFUaN7twTEBAAAAwiD7p7bETmAAAAAAAAAAAAAAAAAAAEDqO8QAAZ7t+n4AAAAASUVORK5CYII=" />
  </div>
</div>

注意:我知道这是很多代码,可以使您的大脑全神贯注。我尽力只给您证明问题的必要条件。

0 个答案:

没有答案