xyyh-hhj/src/components/Index.vue
2025-03-05 14:12:51 +08:00

716 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="IndexPage">
<div class="index-title">
<div class="title-flower"></div>
</div>
<div class="location">
<div class="building index-location-bj" @click="playFn(1)"></div>
<div class="building index-location-xa" @click="playFn(2)"></div>
<div class="building index-location-sh" @click="playFn(3)"></div>
<div class="building index-location-wh" @click="playFn(4)"></div>
<div class="building index-location-gz" @click="playFn(5)"></div>
</div>
<div class="location-name">
<div class="place-name bj" @click="playFn(1)"></div>
<div class="place-name xa" @click="playFn(2)"></div>
<div class="place-name sh" @click="playFn(3)"></div>
<div class="place-name wh" @click="playFn(4)"></div>
<div class="place-name gz" @click="playFn(5)"></div>
</div>
<div class="index-line"></div>
<div class="location-mark">
<div class="mark mark-bj" :class="isMarkList[0] ? 'is-mark' : ''"></div>
<div class="mark mark-xa" :class="isMarkList[1] ? 'is-mark' : ''"></div>
<div class="mark mark-sh" :class="isMarkList[2] ? 'is-mark' : ''"></div>
<div class="mark mark-wh" :class="isMarkList[3] ? 'is-mark' : ''"></div>
<div class="mark mark-gz" :class="isMarkList[4] ? 'is-mark' : ''"></div>
</div>
<div class="line-box">
<svg class="path-svg" viewBox="0 0 348 1048">
<path id="motionPath"
d="M318.125,1.816 L318.125,112.223 L142.430,112.223 L142.430,297.624 L245.454,297.624 L245.454,804.524 L4.236,804.524 L4.236,1014.515"
fill="none" stroke="rgb(230, 91, 36)" stroke-width="2" stroke-linecap="round" />
</svg>
<!-- <div class="box" ref="xflIconRef"></div> -->
<div class="xfl-icon" ref="xflIconRef">
<div id="frameBox"></div>
</div>
</div>
<div class="top-bird-1"></div>
<div class="top-bird-2"></div>
<div class="wh-cloud"></div>
<div class="crane-1"></div>
<div class="crane-2"></div>
<div class="boat-1"></div>
<div class="boat-2"></div>
<div class="boat-3"></div>
<div class="bottom-bird-1"></div>
<div class="bottom-cloud"></div>
<!-- 指引提示 -->
<div class="click-tips" v-if="locationId == 0"></div>
<div class="tips-text" v-if="locationId == 0"></div>
</div>
<div class="video-popup" @touchmove.prevent>
<div class="video-box">
<video class="plyr" id="player" controls>
<source src="https://cdn.plyr.io/static/our-video.mp4" type="video/mp4">
<!-- 你可以根据需要添加多个source标签支持不同的视频格式 -->
</video>
</div>
<div class="cls-btn" @click="hideVideo"></div>
</div>
<!-- <Line /> -->
</template>
<script setup>
import Plyr from 'plyr';
import 'plyr/dist/plyr.css'; // 导入 Plyr 样式
import ImageFramePlayer from 'image-frame-player';
import { showSuccessToast } from "vant";
import Line from './Line.vue';
import { gsap } from 'gsap'
import { MotionPathPlugin } from 'gsap/MotionPathPlugin'
gsap.registerPlugin(MotionPathPlugin)
// 新增设备能力检测
const isLegacyiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) &&
/OS [1-9]_/.test(navigator.userAgent)
console.log('isLegacyiOS', isLegacyiOS);
// 路线节点
const progressArr = [0, 25, 45, 85, 100,]
const playerRef = ref(null)
const imageFramePlayer = ref(null)
const locationId = ref(0) //当前所在节点
const isMarkList = ref([false, false, false, false, false])
const isDone = ref(false)
const isShowDone = ref(false)
const xflIconRef = ref(null)
const currentProgress = ref(0) // 当前进度百分比
const debounce = ref(true)
let animation = null
// 视频库
const videoList = [
new URL(`../assets/video/video.mp4`, import.meta.url).href,
new URL(`../assets/video/video.mp4`, import.meta.url).href,
new URL(`../assets/video/video.mp4`, import.meta.url).href,
new URL(`../assets/video/video.mp4`, import.meta.url).href,
new URL(`../assets/video/gz.mp4`, import.meta.url).href,
]
// 帧图
const frameList = Array.from({ length: 35 }, (_, index) => {
return new URL(`../assets/images/xlz/xfl_${index + 1}.png`, import.meta.url).href
});
onMounted(() => {
initSvgAnimation()
animationFn();
palyFrame()
playerRef.value = new Plyr('#player', {
controls: ['play', 'progress', 'current-time', 'fullscreen'],
fullscreen: {
enabled: true, // 启用全屏功能
fallback: true, // 启用全屏回退
iosNative: true, // 在 iOS 上启用原生全屏
},
autoplay: true, // 自动播放
mute: false, // 初始不静音
loop: { // 循环播放
active: false
},
})
})
// 帧图初始化
const palyFrame = () => {
imageFramePlayer.value = new ImageFramePlayer({
dom: document.getElementById("frameBox"),
imgArr: frameList,
fps: 25,
useCanvas: true,
loop: -1,
yoyo: true
});
imageFramePlayer.value.play();
};
// 播放事件
const playFn = (index) => {
if (debounce.value) return
debounce.value = true
if (locationId.value == 0) {
gsap.set('.xfl-icon', { autoAlpha: 1 })
}
if (locationId.value !== index) {
// 计算相差多少步
const setpNum = Math.abs(index - locationId.value)
const direction = index - locationId.value > 0 ? 1 : -1
console.log('setpNum', setpNum);
locationId.value = index;
playerRef.value.source = {
type: 'video',
sources: [
{
src: videoList[locationId.value - 1],
type: 'video/mp4'
}
]
};
handleMove(progressArr[index - 1], setpNum, direction)
} else {
locationId.value = index
playerRef.value.source = {
type: 'video',
sources: [
{
src: videoList[locationId.value - 1],
type: 'video/mp4'
}
]
};
gsap.to(".video-popup", {
autoAlpha: 1,
duration: 0.3,
onComplete: () => {
debounce.value = false
}
});
}
}
// 关闭弹窗
const hideVideo = () => {
// playerRef.value.fullscreen.enter();
playerRef.value.stop();
gsap.to(".video-popup", {
autoAlpha: 0,
duration: 0.5
});
if (isDone.value && !isShowDone.value) {
showSuccessToast('您已完成所有任务!')
isShowDone.value = true
}
}
// 新增:坐标转换方法
const convertCoordinates = (point) => {
const svg = document.querySelector('.path-svg')
const pt = svg.createSVGPoint()
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
// 添加1px偏移修复iOS精度问题
pt.x = point.x + 1
pt.y = point.y + 1
// 考虑设备像素比
const dpr = window.devicePixelRatio || 1
const screenPoint = pt.matrixTransform(svg.getScreenCTM().inverse())
return {
x: Math.round(screenPoint.x * dpr),
y: Math.round(screenPoint.y * dpr)
}
}
const initSvgAnimation = async () => {
// 获取SVG元素
const svg = document.querySelector('.path-svg')
const path = document.querySelector('#motionPath')
// 关键设置SVG的preserveAspectRatio
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet')
animation = gsap.to(xflIconRef.value, {
motionPath: {
path: "#motionPath",
align: "#motionPath",
autoRotate: false,
alignOrigin: [0.5, 0.5], // 中心点对齐
type: "uniform", // 均匀分布路径点
curviness: 0,
// 新增坐标转换
from: convertCoordinates({ x: 0, y: 0 }),
to: convertCoordinates({ x: 1, y: 1 }),
usePathData: true
},
snap: {
x: 1, // 强制对齐整像素
y: 1
},
paused: true,
duration: 1,
ease: "none",
force3D: true // 启用GPU加速
})
// 等待2帧确保渲染完成
// await new Promise(resolve => requestAnimationFrame(resolve))
// await new Promise(resolve => requestAnimationFrame(resolve))
// 强制重排
// void path.offsetHeight
// 初始定位优化
// gsap.set(xflIconRef.value, {
// x: convertCoordinates(path.getPointAtLength(0)).x,
// y: convertCoordinates(path.getPointAtLength(0)).y,
// scale: 1.5,
// force3D: true
// })
}
// 统一运动控制方法
const handleMove = (percent, setpNum,) => {
const target = percent
// 边界限制
const clampedTarget = Math.min(Math.max(target, 0), 100)
console.log('clampedTarget', clampedTarget);
// 创建补间动画
gsap.to(currentProgress, {
value: clampedTarget,
duration: 1 * setpNum,
ease: "power2.inOut", // 统一缓动
onUpdate: () => {
animation.progress(currentProgress.value / 100)
},
onComplete: () => {
console.log(`当前进度: ${currentProgress.value}%`)
// 标记已经去过的地点
isMarkList.value[locationId.value - 1] = true
gsap.to(".video-popup", {
autoAlpha: 1,
duration: 0.3,
onComplete: () => {
debounce.value = false
}
});
}
})
}
// 入场动画
const animationFn = () => {
const tl = gsap.timeline({
onComplete: () => {
gsap.to(".title-flower", {
rotation: 360,
duration: 4,
repeat: -1,
ease: "linear",
});
gsap.to(".title-flower", {
scale: 1.5,
duration: 4,
repeat: -1,
ease: "linear",
yoyo: true,
});
gsap.to(".index-title", {
y: "-=20",
duration: 2,
repeat: -1,
ease: "linear",
yoyo: true,
});
gsap.to(".place-name", {
scale: "1.05",
y: "-=5",
duration: 2,
repeat: -1,
yoyo: true,
// ease: "back.inOut(1.7)",
})
gsap.to(".boat-1,.boat-2", {
scale: "1.05",
x: "-=30",
duration: 4,
repeat: -1,
yoyo: true,
ease: "back.inOut(1.1)",
})
gsap.to(".boat-3", {
scale: "1.05",
x: "+=30",
duration: 3,
repeat: -1,
yoyo: true,
ease: "back.inOut(2.9)",
})
gsap.to(".crane-1", {
scale: "1.1",
y: "-=10",
duration: 3,
repeat: -1,
yoyo: true,
})
gsap.to(".crane-2", {
scale: "1.1",
y: "-=10",
x: "-=20",
duration: 3,
repeat: -1,
yoyo: true,
})
gsap.to(".click-tips", {
scale: ".7",
x: "-=10",
duration: 2,
repeat: -1,
yoyo: true,
ease: "bounce.out()",
})
}
});
tl.from(".IndexPage", {
autoAlpha: 0,
scale: 1.2,
duration: 0.75,
ease: "power2.out",
onComplete: () => {
}
})
.from(".index-title", {
autoAlpha: 0,
y: 20,
duration: 1,
ease: "power1.out",
})
.from(".bj,.index-location-bj", {
autoAlpha: 0,
y: -20,
duration: 1,
ease: "back.inOut(.3)",
}, 0.3)
.from(".xa,.index-location-xa", {
autoAlpha: 0,
y: -20,
duration: 1,
ease: "back.inOut(3)",
}, 0.5)
.from(".sh,.index-location-sh", {
autoAlpha: 0,
y: -20,
duration: 1,
ease: "back.inOut(3)",
}, 0.7)
.from(".wh,.index-location-wh", {
autoAlpha: 0,
y: -20,
duration: 1,
ease: "back.inOut(3)",
}, 1)
.from(".gz,.index-location-gz", {
autoAlpha: 0,
y: -20,
duration: 1,
ease: "back.inOut(3)",
}, 1.2)
.from(".index-line,.location-mark", {
autoAlpha: 0,
duration: 1,
ease: "back.inOut(3)",
}, 1.5)
.from(".click-tips,.tips-text", {
autoAlpha: 0,
x: 20,
duration: 1,
ease: "back.inOut(3)",
onComplete: () => {
debounce.value = false
}
})
}
</script>
<style lang="scss" scoped>
.IndexPage {
@include bgSrc("index/bg.jpg");
@include box(750px, 1929px);
overflow: hidden;
position: relative;
.index-title {
@include bgSrc("index/title.png");
@include pos(502px, 299px, 114px, 44px);
position: relative;
pointer-events: none;
overflow: hidden;
.title-flower {
@include bgSrc("index/title-flower.png");
@include pos(58px, 56px, 246px, 79px);
}
}
.index-line {
@include bgSrc("index/line.png");
@include pos(348px, 1048px, 170px, 568px);
pointer-events: none;
}
.wh-cloud {
@include bgSrc("index/wh-cloud.png");
@include pos(324px, 298px, 9px, 1061px);
pointer-events: none;
}
.location {
.index-location-bj {
@include bgSrc("index/location-bj.png");
@include pos(507px, 233px, 142px, 341px);
}
.index-location-xa {
@include bgSrc("index/location-xa.png");
@include pos(303px, 319px, 36px, 647px);
}
.index-location-sh {
@include bgSrc("index/location-sh.png");
@include pos(378px, 413px, 372px, 760px);
}
.index-location-wh {
@include bgSrc("index/location-wh.png");
@include pos(192px, 229px, 94px, 1067px);
}
.index-location-gz {
@include bgSrc("index/location-gz.png");
@include pos(750px, 507px, 0px, 1402px);
}
}
.location-name {
.bj {
@include bgSrc("index/name-bj.png");
@include pos(98px, 65px, 161px, 404px);
}
.xa {
@include bgSrc("index/name-xa.png");
@include pos(98px, 65px, 190px, 658px);
}
.sh {
@include bgSrc("index/name-sh.png");
@include pos(98px, 65px, 610px, 797px);
}
.wh {
@include bgSrc("index/name-wh.png");
@include pos(98px, 66px, 266px, 1061px);
}
.gz {
@include bgSrc("index/name-gz.png");
@include pos(98px, 66px, 499px, 1389px);
}
}
.location-mark {
.mark {
@include bgSrc("index/sign-icon.png");
// 灰色滤镜
filter: grayscale(100%);
}
.mark-bj {
@include pos(73px, 75px, 466px, 540px);
}
.mark-xa {
@include pos(73px, 75px, 291px, 731px);
}
.mark-sh {
@include pos(73px, 75px, 393px, 976px);
}
.mark-wh {
@include pos(73px, 75px, 152px, 1348px);
}
.mark-gz {
@include pos(73px, 75px, 150px, 1558px);
}
.is-mark {
filter: grayscale(0%);
}
}
.top-bird-1 {
@include bgSrc("index/top-bird-1.png");
@include pos(112px, 46px, 559px, 117px);
pointer-events: none;
}
.top-bird-2 {
@include bgSrc("index/top-bird-2.png");
@include pos(102px, 42px, 553px, 408px);
pointer-events: none;
}
.crane-1 {
@include bgSrc("index/crane-1.png");
@include pos(80px, 116px, 44px, 1147px);
pointer-events: none;
}
.crane-2 {
@include bgSrc("index/crane-2.png");
@include pos(117px, 112px, 197px, 1210px);
pointer-events: none;
}
.boat-1 {
@include bgSrc("index/boat.png");
@include pos(181px, 57px, 567px, 1156px);
pointer-events: none;
}
.boat-2 {
@include bgSrc("index/boat-2.png");
@include pos(186px, 62px, 125px, 1749px);
pointer-events: none;
}
.boat-3 {
@include bgSrc("index/boat.png");
@include pos(197px, 49px, 438px, 1834px);
pointer-events: none;
}
.bottom-bird-1 {
@include bgSrc("index/bottom-bird-1.png");
@include pos(89px, 42px, 324px, 1797px);
pointer-events: none;
}
.bottom-cloud {
@include bgSrc("index/cloud-bottom.png");
@include pos(749px, 371px, 0px, 1558px);
pointer-events: none;
}
.click-tips {
@include pos(58px, 71px, 556px, 500px);
@include bgSrc("index/arrow-icon.png");
pointer-events: none;
}
.tips-text {
@include pos(163px, 92px, 586px, 560px);
@include bgSrc("index/tips.png");
pointer-events: none;
}
}
.video-popup {
@include fixed();
@include flexCen();
background-color: rgba($color: #000000, $alpha: .6);
visibility: hidden;
.cls-btn {
@include box(50px, 50px);
@include bgSrc("index/cls-btn.png");
margin-top: 40px;
}
.video-box {
width: 700px;
border-radius: 12px;
border: 1px solid #fff;
overflow: hidden;
.plyr {
@include box(100%, 100%);
}
}
}
.line-box {
@include pos(348px, 1048px, 185px, 583px);
// overflow: visible;
pointer-events: none;
/* 确保元素可超出容器 */
.path-svg {
width: 100%;
height: 100%;
// transform: translateZ(0);
opacity: 0;
/* 修复渲染层级 */
backface-visibility: hidden;
/* 修复iOS偏移 */
}
.xfl-icon {
pointer-events: none;
position: absolute;
width: 163px;
height: 153px;
transform: scale(1.5);
visibility: hidden;
/* 旧iOS需要绝对像素值 */
@media screen and (-webkit-min-device-pixel-ratio:0) {
transform: scale(1.5);
}
#frameBox {
width: 100%;
height: 100%;
}
}
}
</style>