我正在构建一个图像裁剪工具,使您可以在边框内缩放和平移。
在下面的示例(全屏最佳视图)中,您将看到以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>
注意:我知道这是很多代码,可以使您的大脑全神贯注。我尽力只给您证明问题的必要条件。