203 lines
4.5 KiB
Vue
203 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from "vue";
|
|
|
|
const props = defineProps<{
|
|
images: string[];
|
|
index: number;
|
|
mode: "slide" | "fade" | "card";
|
|
zoom: boolean;
|
|
flipMs: number;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
next: [];
|
|
prev: [];
|
|
}>();
|
|
|
|
const dir = ref<"next" | "prev">("next");
|
|
const current = computed(() => props.images[props.index] ?? "");
|
|
const transitionName = computed(() => `fx-${props.mode}-${dir.value}`);
|
|
|
|
watch(
|
|
() => props.index,
|
|
(now, was) => {
|
|
if (was === undefined) return;
|
|
dir.value = now >= was ? "next" : "prev";
|
|
},
|
|
);
|
|
|
|
let startX = 0;
|
|
let tracking = false;
|
|
|
|
function onPointerDown(ev: PointerEvent) {
|
|
tracking = true;
|
|
startX = ev.clientX;
|
|
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
|
|
}
|
|
|
|
function onPointerUp(ev: PointerEvent) {
|
|
if (!tracking) return;
|
|
tracking = false;
|
|
const dx = ev.clientX - startX;
|
|
if (Math.abs(dx) < 48) {
|
|
const el = ev.currentTarget as HTMLElement;
|
|
const x = ev.clientX - el.getBoundingClientRect().left;
|
|
if (x < el.clientWidth / 2) emit("next");
|
|
else emit("prev");
|
|
return;
|
|
}
|
|
if (dx < 0) emit("next");
|
|
else emit("prev");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="fx-stage"
|
|
:class="[mode, { 'zoom-in': zoom }]"
|
|
:style="{ '--flip-ms': `${flipMs}ms` }"
|
|
@pointerdown.prevent="onPointerDown"
|
|
@pointerup.prevent="onPointerUp"
|
|
@pointercancel="tracking = false"
|
|
>
|
|
<Transition :name="transitionName">
|
|
<div :key="`${mode}-${index}`" class="page-wrap">
|
|
<img class="page-img" :src="current" alt="" draggable="false" />
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.fx-stage {
|
|
position: relative;
|
|
width: min(920px, 100%);
|
|
margin: 0 auto;
|
|
height: min(74vh, 780px);
|
|
display: grid;
|
|
place-items: center;
|
|
perspective: 2200px;
|
|
touch-action: pan-y;
|
|
overflow: visible;
|
|
}
|
|
|
|
.fx-stage.card {
|
|
perspective: 2600px;
|
|
}
|
|
|
|
.page-wrap {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
transform-style: preserve-3d;
|
|
}
|
|
.page-img {
|
|
max-width: min(100%, 820px);
|
|
max-height: 72vh;
|
|
width: auto;
|
|
height: auto;
|
|
object-fit: contain;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
box-shadow:
|
|
0 8px 10px rgba(0, 0, 0, 0.18),
|
|
0 28px 60px rgba(0, 0, 0, 0.45),
|
|
0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
user-select: none;
|
|
backface-visibility: hidden;
|
|
}
|
|
|
|
.zoom-in .page-img {
|
|
max-height: 90vh;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.fx-slide-next-enter-active,
|
|
.fx-slide-next-leave-active,
|
|
.fx-slide-prev-enter-active,
|
|
.fx-slide-prev-leave-active,
|
|
.fx-fade-next-enter-active,
|
|
.fx-fade-next-leave-active,
|
|
.fx-fade-prev-enter-active,
|
|
.fx-fade-prev-leave-active,
|
|
.fx-card-next-enter-active,
|
|
.fx-card-next-leave-active,
|
|
.fx-card-prev-enter-active,
|
|
.fx-card-prev-leave-active {
|
|
position: absolute;
|
|
transition:
|
|
transform var(--flip-ms, 1s) cubic-bezier(0.22, 1, 0.36, 1),
|
|
opacity var(--flip-ms, 1s) cubic-bezier(0.22, 1, 0.36, 1),
|
|
filter var(--flip-ms, 1s) ease;
|
|
}
|
|
|
|
.fx-slide-next-enter-from {
|
|
transform: translateX(-58%) scale(0.86) rotateY(-22deg);
|
|
opacity: 0;
|
|
filter: blur(10px);
|
|
}
|
|
.fx-slide-next-leave-to {
|
|
transform: translateX(52%) scale(0.88) rotateY(16deg);
|
|
opacity: 0;
|
|
filter: blur(8px);
|
|
}
|
|
.fx-slide-prev-enter-from {
|
|
transform: translateX(58%) scale(0.86) rotateY(22deg);
|
|
opacity: 0;
|
|
filter: blur(10px);
|
|
}
|
|
.fx-slide-prev-leave-to {
|
|
transform: translateX(-52%) scale(0.88) rotateY(-16deg);
|
|
opacity: 0;
|
|
filter: blur(8px);
|
|
}
|
|
|
|
.fx-fade-next-enter-from,
|
|
.fx-fade-prev-enter-from {
|
|
opacity: 0;
|
|
transform: scale(1.12);
|
|
filter: blur(16px) brightness(1.15);
|
|
}
|
|
.fx-fade-next-leave-to,
|
|
.fx-fade-prev-leave-to {
|
|
opacity: 0;
|
|
transform: scale(0.9);
|
|
filter: blur(10px) brightness(0.7);
|
|
}
|
|
|
|
.fx-card-next-enter-from {
|
|
transform: rotateY(-95deg) translateZ(80px) scale(0.92);
|
|
opacity: 0.15;
|
|
}
|
|
.fx-card-next-leave-to {
|
|
transform: rotateY(95deg) translateZ(80px) scale(0.92);
|
|
opacity: 0.15;
|
|
}
|
|
.fx-card-prev-enter-from {
|
|
transform: rotateY(95deg) translateZ(80px) scale(0.92);
|
|
opacity: 0.15;
|
|
}
|
|
.fx-card-prev-leave-to {
|
|
transform: rotateY(-95deg) translateZ(80px) scale(0.92);
|
|
opacity: 0.15;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.fx-slide-next-enter-active,
|
|
.fx-slide-next-leave-active,
|
|
.fx-slide-prev-enter-active,
|
|
.fx-slide-prev-leave-active,
|
|
.fx-fade-next-enter-active,
|
|
.fx-fade-next-leave-active,
|
|
.fx-fade-prev-enter-active,
|
|
.fx-fade-prev-leave-active,
|
|
.fx-card-next-enter-active,
|
|
.fx-card-next-leave-active,
|
|
.fx-card-prev-enter-active,
|
|
.fx-card-prev-leave-active {
|
|
transition-duration: 0.01ms;
|
|
}
|
|
}
|
|
</style>
|