Skip to content

Instantly share code, notes, and snippets.

@b4n92uid
Last active August 17, 2021 22:17
Show Gist options
  • Save b4n92uid/b3386eb0c48b0c7ad0a68456f611fd86 to your computer and use it in GitHub Desktop.
Save b4n92uid/b3386eb0c48b0c7ad0a68456f611fd86 to your computer and use it in GitHub Desktop.
[Vuetify] TheaterBox
<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