Last active
August 17, 2021 22:17
-
-
Save b4n92uid/b3386eb0c48b0c7ad0a68456f611fd86 to your computer and use it in GitHub Desktop.
[Vuetify] TheaterBox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<v-dialog | |
:value="value" | |
@input="v => $emit('input', v)" | |
overlay | |
fullscreen | |
transition="fade-transition" | |
content-class="theater-box" | |
> | |
<div> | |
<div class="__wrap"> | |
<div | |
ref="image" | |
@mousedown.prevent | |
@mousewheel="zoomWheel" | |
:style="style" | |
class="__media" | |
:class="{ | |
'--smooth': !transform.isPanning && !transform.isScaling | |
}" | |
> | |
<swiper | |
ref="swiper" | |
:items="items" | |
:slidesPerView="1" | |
:disabled="transform.scale > 1" | |
:currentSlide="index" | |
:options="{ autoHeight: true }" | |
@slideChange="i => $emit('update:index', i)" | |
#default="{ item }" | |
> | |
<div :key="index"> | |
<slot :item="item"></slot> | |
</div> | |
</swiper> | |
</div> | |
</div> | |
<div class="__actions"> | |
<div> | |
<template v-if="$isDesktop"> | |
<v-btn text icon color="white" @click="zoomIn(4)"> | |
<v-icon>mdi-magnify-plus</v-icon> | |
</v-btn> | |
<v-btn text icon color="white" @click="zoomOff(4)"> | |
<v-icon>mdi-magnify-minus</v-icon> | |
</v-btn> | |
<v-btn text icon color="white" @click="restore"> | |
<v-icon>mdi-magnify-close</v-icon> | |
</v-btn> | |
<v-spacer></v-spacer> | |
</template> | |
<v-btn text icon color="white" @click="transform.rotate -= 90"> | |
<v-icon>mdi-rotate-left</v-icon> | |
</v-btn> | |
<v-btn text icon color="white" @click="transform.rotate += 90"> | |
<v-icon>mdi-rotate-right</v-icon> | |
</v-btn> | |
<v-spacer></v-spacer> | |
<v-btn text icon color="white" @click="prev"> | |
<v-icon>mdi-chevron-left</v-icon> | |
</v-btn> | |
<span>{{ index + 1 }} / {{ items.length }}</span> | |
<v-btn text icon color="white" @click="next"> | |
<v-icon>mdi-chevron-right</v-icon> | |
</v-btn> | |
<v-spacer></v-spacer> | |
<v-btn text color="white" @click="close">Fermer</v-btn> | |
</div> | |
</div> | |
</div> | |
</v-dialog> | |
</template> | |
<script> | |
import Swiper from "./Swiper.vue"; | |
import Hammer from "hammerjs"; | |
function clamp(value, min, max) { | |
return Math.max(Math.min(value, max), min); | |
} | |
export default { | |
components: { Swiper }, | |
props: { | |
items: { | |
type: Array, | |
default: () => [] | |
}, | |
index: { | |
type: Number, | |
default: 0 | |
}, | |
value: { | |
type: Boolean, | |
default: false | |
}, | |
zoomMin: { | |
type: Number, | |
default: 1.0 | |
}, | |
zoomMax: { | |
type: Number, | |
default: 4.0 | |
} | |
}, | |
mounted() {}, | |
data() { | |
return { | |
transform: { | |
isScaling: false, | |
scale: 1.0, | |
lastScale: 1.0, | |
rotate: 0, | |
isPanning: false, | |
pos: { | |
x: 0, | |
y: 0 | |
}, | |
lastPos: { | |
x: 0, | |
y: 0 | |
} | |
} | |
}; | |
}, | |
computed: { | |
style() { | |
const cssTransform = [ | |
`scale(${this.transform.scale})`, | |
`translate(${this.transform.pos.x}px, ${this.transform.pos.y}px)`, | |
`rotate(${this.transform.rotate}deg)` | |
]; | |
return { | |
transform: cssTransform.join(" ") | |
}; | |
}, | |
center() { | |
return { | |
x: this.$refs.image.clientWidth / 2, | |
y: this.$refs.image.clientHeight / 2 | |
}; | |
} | |
}, | |
watch: { | |
index: "restore", | |
value(v) { | |
if (v) { | |
this.$nextTick(() => { | |
this.$refs.swiper.update(); | |
this.initTouchEvents(); | |
}); | |
this.restore(); | |
} | |
} | |
}, | |
methods: { | |
initTouchEvents() { | |
const mc = new Hammer.Manager(this.$refs.image, { | |
recognizers: [ | |
[Hammer.Tap], | |
[Hammer.Pan, { direction: Hammer.DIRECTION_ALL }], | |
[Hammer.Pinch, { enable: false }] | |
] | |
}); | |
/** | |
* Tap Event | |
*/ | |
mc.on("tap", e => { | |
if (e.tapCount >= 2) { | |
if (this.transform.scale > 1) this.zoomRestore(); | |
else this.zoomIn(4); | |
} | |
}); | |
/** | |
* Pinch Events | |
*/ | |
mc.on("pinchstart", () => { | |
this.transform.isScaling = true; | |
this.transform.lastScale = this.transform.scale; | |
}); | |
mc.on("pinchmove", e => { | |
if (!this.transform.isScaling) return; | |
this.transform.scale = clamp( | |
this.transform.lastScale * e.scale, | |
this.zoomMin, | |
this.zoomMax | |
).toFixed(2); | |
}); | |
mc.on("pinchend", () => { | |
if (!this.transform.isScaling) return; | |
this.transform.isScaling = false; | |
}); | |
/** | |
* Pan Events | |
*/ | |
mc.on("panstart", () => { | |
if (this.transform.scale <= 1) return; | |
this.transform.isPanning = true; | |
this.transform.lastPos.x = this.transform.pos.x; | |
this.transform.lastPos.y = this.transform.pos.y; | |
}); | |
mc.on("panmove", e => { | |
if (!this.transform.isPanning) return; | |
this.panmove(e); | |
}); | |
mc.on("panend", () => { | |
if (!this.transform.isPanning) return; | |
this.transform.isPanning = false; | |
}); | |
}, | |
panmove(e) { | |
/** | |
* The touch movement scaled with the zoom factor to get | |
* a smooth movement as we zoom the image | |
* | |
* Substract the last position to get a relative position | |
* | |
* Inverse the last position the get the correct behavior | |
*/ | |
this.transform.pos.x = | |
e.deltaX * (1 / this.transform.scale) - this.transform.lastPos.x * -1; | |
this.transform.pos.y = | |
e.deltaY * (1 / this.transform.scale) - this.transform.lastPos.y * -1; | |
this.transform.pos.x = clamp( | |
this.transform.pos.x, | |
-this.center.x, | |
this.center.x | |
); | |
this.transform.pos.y = clamp( | |
this.transform.pos.y, | |
-this.center.y, | |
this.center.y | |
); | |
}, | |
zoomWheel(e, factor = 1) { | |
if (e.deltaY < 0 && this.transform.scale < 1.0001) { | |
/** | |
* e.clientX - this.$refs.image.offsetLeft | |
* To get mouse pos relative to the top left of image element | |
* | |
* this.center.x | |
* To get relative mouse pos from center of image element | |
* | |
* Inverse the result to get correct behavior for the mouvement | |
*/ | |
this.transform.pos.x = -( | |
e.clientX - | |
this.$refs.image.offsetLeft - | |
this.center.x | |
); | |
this.transform.pos.y = -( | |
e.clientY - | |
this.$refs.image.offsetTop - | |
this.center.y | |
); | |
this.transform.pos.x = clamp( | |
this.transform.pos.x, | |
-this.center.x / 2, | |
this.center.x / 2 | |
); | |
this.transform.pos.y = clamp( | |
this.transform.pos.y, | |
-this.center.y / 2, | |
this.center.y / 2 | |
); | |
} | |
/** | |
* Clamp scale between defined MIN and MAX | |
*/ | |
this.transform.scale = clamp( | |
this.transform.scale * factor - e.deltaY * 0.01, | |
this.zoomMin, | |
this.zoomMax | |
); | |
if (this.transform.scale - 1 < Number.EPSILON) { | |
this.transform.pos.x = 0; | |
this.transform.pos.y = 0; | |
} | |
}, | |
zoomRestore() { | |
this.transform.scale = 1; | |
this.transform.pos.x = 0; | |
this.transform.pos.y = 0; | |
}, | |
zoomIn(factor = 1) { | |
this.transform.scale = Math.min( | |
this.transform.scale + 0.2 * factor, | |
this.zoomMax | |
); | |
}, | |
zoomOff(factor = 1) { | |
this.transform.scale = Math.max( | |
this.transform.scale - 0.2 * factor, | |
this.zoomMin | |
); | |
}, | |
restore() { | |
this.transform.isPanning = false; | |
this.transform.scale = 1; | |
this.transform.rotate = 0; | |
this.transform.pos.x = 0; | |
this.transform.pos.y = 0; | |
this.transform.lastPos.x = 0; | |
this.transform.lastPos.y = 0; | |
}, | |
prev() { | |
this.$emit("update:index", Math.max(this.index - 1, 0)); | |
}, | |
next() { | |
this.$emit( | |
"update:index", | |
Math.min(this.index + 1, this.items.length - 1) | |
); | |
}, | |
close() { | |
this.$emit("input", false); | |
} | |
} | |
}; | |
</script> | |
<style lang="scss"> | |
@import "~@/scss/responsive.scss"; | |
.theater-box { | |
.__wrap { | |
z-index: -1; | |
position: absolute; | |
height: 100%; | |
width: 100%; | |
overflow: hidden; | |
text-align: center; | |
background: black; | |
.__media { | |
position: relative; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin: auto; | |
cursor: grab; | |
@include isDesktop { | |
width: 50%; | |
} | |
& > div { | |
width: 100%; | |
max-height: 100%; | |
} | |
img, | |
video { | |
max-width: 100%; | |
} | |
&.--smooth { | |
transition: transform 200ms ease-in-out; | |
} | |
} | |
} | |
.__actions { | |
z-index: 2; | |
display: flex; | |
justify-content: center; | |
width: 100%; | |
position: absolute; | |
bottom: 0; | |
padding: 4px; | |
background: linear-gradient(to top, black, transparent); | |
color: white; | |
> div { | |
flex-grow: 1; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
@include isDesktop { | |
max-width: 50vw; | |
} | |
} | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment