Files
digitalcatalog/client/src/components/effects/CssPageEffect.vue
T

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>