716 lines
16 KiB
Vue
716 lines
16 KiB
Vue
<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>
|