<div id="app"
@mousemove="drag"
@touchmove="drag"
@mouseup="stopDrag"
@touchend="stopDrag"
class="flex items-center justify-center bg-black w-screen h-screen">
<div class="mobile-container relative overflow-hidden">
<!-- image below -->
<img class="absolute h-full z-0"
:style="prevImageStyle"
:src="previousImage">
<!-- interactive image -->
<img class="absolute h-full z-10"
@mousedown.prevent="startDrag"
@touchstart="startDrag"
:style="currentImageStyle"
:src="currentImage">
<!-- image above -->
<img class="absolute h-full z-20"
:style="nextImageStyle"
:src="nextImage">
<pre class="fixed bottom-0 left-0 p-3 text-white z-50 bg-gray-800 opacity-75 pointer-events-none">
dragging: {{ dragging }}
animating: {{ animating }}
imagesIndexes: [{{previousImageIndex}}] [{{currentImageIndex}}] [{{nextImageIndex}}]
cursorStartX: {{ cursorStartX }}
cursorCurrentX: {{ cursorCurrentX }}
diffX: {{ diffX }}
currentImagePosition: {{currentImagePosition}}
nextImagePosition: {{nextImagePosition}}</pre>
</div>
</div>
const images = [
'https://res.cloudinary.com/ederchrono/image/upload/v1556684546/wotw-013/nature-1.jpg',
'https://res.cloudinary.com/ederchrono/image/upload/v1556684546/wotw-013/nature-2.jpg',
'https://res.cloudinary.com/ederchrono/image/upload/v1556684526/wotw-013/nature-3.jpg',
'https://res.cloudinary.com/ederchrono/image/upload/v1556684544/wotw-013/nature-4.jpg',
'https://res.cloudinary.com/ederchrono/image/upload/v1556684520/wotw-013/nature-5.jpg',
'https://res.cloudinary.com/ederchrono/image/upload/v1556684527/wotw-013/nature-6.jpg'
]
const DEVICE_WIDTH = 320
const HALF_WIDTH = DEVICE_WIDTH / 2
const DRAGGING_SPEED = 1.2
const MAX_BLUR = 8
const getCursorX = (event) => {
if(event.touches && event.touches.length) {
// touch
return event.touches[0].pageX
}
if(event.pageX && event.pageY) {
// mouse
return event.pageX
}
return 0
}
const clampPosition = (position) => {
// constrain image to be between 0 and device width
return Math.max(Math.min(position, DEVICE_WIDTH), 0)
}
const calculateBlur = (position) => {
return MAX_BLUR * (1 - (position / DEVICE_WIDTH));
}
new Vue({
el: '#app',
data: {
dragging: false,
animating: false,
cursorStartX: 0,
cursorCurrentX: 0,
currentImageIndex: 0,
currentImageAnimatedX: 0,
nextImageAnimatedX: DEVICE_WIDTH
},
methods: {
startDrag(e) {
if(this.animating) {
// avoid dragging when animation is running
return
}
this.dragging = true
this.cursorStartX = getCursorX(e)
this.cursorCurrentX = this.cursorStartX
},
drag(e) {
if(!this.dragging) {
// avoid updating if not dragging
return
}
this.cursorCurrentX = getCursorX(e)
},
stopDrag(e) {
let animationProps = this.createReleaseAnimation()
this.dragging = false
this.animating = true
TweenLite.to(this, 0.2, {
...animationProps,
onComplete: () => {this.animating = false}
})
},
createReleaseAnimation() {
if(this.swipingLeft) {
if(this.nextImagePosition > HALF_WIDTH) {
// next image should be animated back to be offscreen
this.nextImageAnimatedX = this.nextImagePosition
return {nextImageAnimatedX: DEVICE_WIDTH}
}
// current image "copies" the nextImage position
this.currentImageAnimatedX = this.nextImagePosition
// the nextImage is sent offscreen
this.nextImageAnimatedX = DEVICE_WIDTH
// Change the image index to become the next image in the array
// images src attribute will update accordingly
this.currentImageIndex = this.nextImageIndex
return {currentImageAnimatedX: 0}
}
// swipe right
if(this.currentImagePosition < HALF_WIDTH) {
// current image should be animated back to center position
this.currentImageAnimatedX = this.currentImagePosition
return {currentImageAnimatedX: 0}
}
// the nextImage "copies" the currentImage position
this.nextImageAnimatedX = this.currentImagePosition
// the currentImage gets centered to become the prevImage
this.currentImageAnimatedX = 0
// Change the image index to become the previous image in the array
this.currentImageIndex = this.previousImageIndex
return {nextImageAnimatedX: DEVICE_WIDTH}
}
},
computed: {
diffX () {
return this.cursorStartX - this.cursorCurrentX
},
currentImage () {
return images[this.currentImageIndex]
},
previousImageIndex () {
return (this.currentImageIndex - 1 + images.length) % images.length
},
previousImage () {
return images[this.previousImageIndex]
},
nextImageIndex () {
return (this.currentImageIndex+1) % images.length
},
nextImage () {
return images[this.nextImageIndex]
},
swipingLeft () {
return this.diffX >= 0
},
currentImagePosition () {
if(this.animating) {
return this.currentImageAnimatedX
}
if(!this.dragging || this.swipingLeft) {
return 0
}
const position = this.diffX * -DRAGGING_SPEED
return clampPosition(position)
},
prevImageStyle () {
const blur = calculateBlur(this.currentImagePosition)
return {
'filter': `blur(${blur}px)`
}
},
currentImageStyle () {
const blur = calculateBlur(this.nextImagePosition)
return {
'left': `${this.currentImagePosition}px`,
'filter': `blur(${blur}px)`
}
},
nextImagePosition () {
if(this.animating) {
return this.nextImageAnimatedX
}
const swipingRight = !this.swipingLeft
if(!this.dragging || swipingRight) {
return DEVICE_WIDTH
}
const position = DEVICE_WIDTH - (this.diffX * DRAGGING_SPEED)
return clampPosition(position)
},
nextImageStyle () {
return {
'left': `${this.nextImagePosition}px`
}
}
}
})