已给版本
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 546 B |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 777 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 281 KiB |
@ -25,6 +25,17 @@
|
|||||||
<div class="mark mark-wh" :class="isMarkList[3] ? '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 class="mark mark-gz" :class="isMarkList[4] ? 'is-mark' : ''"></div>
|
||||||
</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-1"></div>
|
||||||
<div class="top-bird-2"></div>
|
<div class="top-bird-2"></div>
|
||||||
<div class="wh-cloud"></div>
|
<div class="wh-cloud"></div>
|
||||||
@ -35,9 +46,7 @@
|
|||||||
<div class="boat-3"></div>
|
<div class="boat-3"></div>
|
||||||
<div class="bottom-bird-1"></div>
|
<div class="bottom-bird-1"></div>
|
||||||
<div class="bottom-cloud"></div>
|
<div class="bottom-cloud"></div>
|
||||||
<div class="xfl-icon" :class="'xfl-location-' + locationId">
|
|
||||||
<div id="frameBox"></div>
|
|
||||||
</div>
|
|
||||||
<!-- 指引提示 -->
|
<!-- 指引提示 -->
|
||||||
<div class="click-tips" v-if="locationId == 0"></div>
|
<div class="click-tips" v-if="locationId == 0"></div>
|
||||||
<div class="tips-text" v-if="locationId == 0"></div>
|
<div class="tips-text" v-if="locationId == 0"></div>
|
||||||
@ -52,26 +61,42 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cls-btn" @click="hideVideo"></div>
|
<div class="cls-btn" @click="hideVideo"></div>
|
||||||
</div>
|
</div>
|
||||||
<Line />
|
<!-- <Line /> -->
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import gsap from 'gsap'
|
|
||||||
import Plyr from 'plyr';
|
import Plyr from 'plyr';
|
||||||
import 'plyr/dist/plyr.css'; // 导入 Plyr 样式
|
import 'plyr/dist/plyr.css'; // 导入 Plyr 样式
|
||||||
import ImageFramePlayer from 'image-frame-player';
|
import ImageFramePlayer from 'image-frame-player';
|
||||||
import {showSuccessToast} from "vant";
|
import { showSuccessToast } from "vant";
|
||||||
import Line from './Line.vue';
|
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, 38, 48, 67, 100,]
|
||||||
|
|
||||||
const playerRef = ref(null)
|
const playerRef = ref(null)
|
||||||
const frame = ref(null)
|
|
||||||
const imageFramePlayer = ref(null)
|
const imageFramePlayer = ref(null)
|
||||||
const locationId = ref(0)
|
const locationId = ref(0) //当前所在节点
|
||||||
const isMarkList = ref([false, false, false, false, false])
|
const isMarkList = ref([false, false, false, false, false])
|
||||||
const isDone = ref(false)
|
const isDone = ref(false)
|
||||||
const isShowDone = ref(false)
|
const isShowDone = ref(false)
|
||||||
|
const xflIconRef = ref(null)
|
||||||
|
const currentProgress = ref(0) // 当前进度百分比
|
||||||
|
const debounce = ref(false)
|
||||||
|
let animation = null
|
||||||
|
|
||||||
// 视频库
|
// 视频库
|
||||||
const videoList = [
|
const videoList = [
|
||||||
@ -88,6 +113,7 @@ const frameList = Array.from({ length: 35 }, (_, index) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
initSvgAnimation()
|
||||||
animationFn();
|
animationFn();
|
||||||
palyFrame()
|
palyFrame()
|
||||||
playerRef.value = new Plyr('#player', {
|
playerRef.value = new Plyr('#player', {
|
||||||
@ -124,8 +150,18 @@ const palyFrame = () => {
|
|||||||
// 播放事件
|
// 播放事件
|
||||||
const playFn = (index) => {
|
const playFn = (index) => {
|
||||||
|
|
||||||
if (locationId.value === 0 || locationId.value === index) {
|
if (debounce.value) return
|
||||||
locationId.value = index
|
debounce.value = true
|
||||||
|
if (locationId.value == 0) {
|
||||||
|
gsap.set('.xfl-icon', { autoAlpha: 1 })
|
||||||
|
}
|
||||||
|
if (locationId.value !== index) {
|
||||||
|
// 计算相差多少步
|
||||||
|
// 取正数
|
||||||
|
const setpNum = Math.abs(index - locationId.value)
|
||||||
|
console.log('setpNum', setpNum);
|
||||||
|
|
||||||
|
locationId.value = index;
|
||||||
playerRef.value.source = {
|
playerRef.value.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
sources: [
|
sources: [
|
||||||
@ -135,22 +171,8 @@ const playFn = (index) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
gsap.to(".xfl-icon", {
|
handleMove(progressArr[index - 1], setpNum)
|
||||||
autoAlpha: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
onComplete: () => {
|
|
||||||
gsap.to(".video-popup", {
|
|
||||||
autoAlpha: 1,
|
|
||||||
duration: 0.5
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
gsap.to(".xfl-icon", {
|
|
||||||
autoAlpha: 0,
|
|
||||||
duration: 0.5,
|
|
||||||
onComplete: () => {
|
|
||||||
locationId.value = index
|
locationId.value = index
|
||||||
playerRef.value.source = {
|
playerRef.value.source = {
|
||||||
type: 'video',
|
type: 'video',
|
||||||
@ -161,27 +183,20 @@ const playFn = (index) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
gsap.to(".xfl-icon", {
|
|
||||||
autoAlpha: 1,
|
|
||||||
duration: 0.5,
|
|
||||||
onComplete: () => {
|
|
||||||
gsap.to(".video-popup", {
|
gsap.to(".video-popup", {
|
||||||
autoAlpha: 1,
|
autoAlpha: 1,
|
||||||
duration: 0.5
|
duration: 0.3,
|
||||||
});
|
onComplete: () => {
|
||||||
|
debounce.value = false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
// 标记已经去过的地点
|
|
||||||
isMarkList.value[locationId.value - 1] = true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 关闭弹窗
|
// 关闭弹窗
|
||||||
const hideVideo = () => {
|
const hideVideo = () => {
|
||||||
// playerRef.value.fullscreen.enter();
|
// playerRef.value.fullscreen.enter();
|
||||||
@ -191,13 +206,88 @@ const hideVideo = () => {
|
|||||||
duration: 0.5
|
duration: 0.5
|
||||||
});
|
});
|
||||||
|
|
||||||
if(isDone.value && !isShowDone.value){
|
if (isDone.value && !isShowDone.value) {
|
||||||
showSuccessToast('您已完成所有任务!')
|
showSuccessToast('您已完成所有任务!')
|
||||||
isShowDone.value = true
|
isShowDone.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 新增:坐标转换方法
|
||||||
|
const convertCoordinates = (point) => {
|
||||||
|
const svg = document.querySelector('.path-svg')
|
||||||
|
const pt = svg.createSVGPoint()
|
||||||
|
|
||||||
|
pt.x = point.x
|
||||||
|
pt.y = point.y
|
||||||
|
return pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSvgAnimation = () => {
|
||||||
|
// 获取SVG元素
|
||||||
|
const svg = document.querySelector('.path-svg')
|
||||||
|
const path = document.querySelector('#motionPath')
|
||||||
|
|
||||||
|
// 关键:设置SVG的preserveAspectRatio
|
||||||
|
svg.setAttribute('preserveAspectRatio', 'none')
|
||||||
|
|
||||||
|
animation = gsap.to(xflIconRef.value, {
|
||||||
|
motionPath: {
|
||||||
|
path: "#motionPath",
|
||||||
|
align: "#motionPath",
|
||||||
|
autoRotate: false,
|
||||||
|
// 新增坐标转换
|
||||||
|
from: convertCoordinates({ x: 0, y: 0 }),
|
||||||
|
to: convertCoordinates({ x: 1, y: 1 }),
|
||||||
|
usePathData: true
|
||||||
|
},
|
||||||
|
paused: true,
|
||||||
|
duration: 1,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始定位优化
|
||||||
|
gsap.set(xflIconRef.value, {
|
||||||
|
x: convertCoordinates(path.getPointAtLength(0)).x,
|
||||||
|
y: convertCoordinates(path.getPointAtLength(0)).y
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一运动控制方法
|
||||||
|
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,
|
||||||
|
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 animationFn = () => {
|
||||||
|
|
||||||
@ -344,7 +434,7 @@ const animationFn = () => {
|
|||||||
.IndexPage {
|
.IndexPage {
|
||||||
@include bgSrc("index/bg.jpg");
|
@include bgSrc("index/bg.jpg");
|
||||||
@include box(750px, 1929px);
|
@include box(750px, 1929px);
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.index-title {
|
.index-title {
|
||||||
@ -512,51 +602,6 @@ const animationFn = () => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xfl-icon {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
width: 163px;
|
|
||||||
height: 153px;
|
|
||||||
// 以中心放大1.5倍
|
|
||||||
transform: scale(1.5);
|
|
||||||
|
|
||||||
#frameBox {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-0 {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-1 {
|
|
||||||
left: 421px;
|
|
||||||
top: 512px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-2 {
|
|
||||||
left: 245px;
|
|
||||||
top: 679px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-3 {
|
|
||||||
left: 347px;
|
|
||||||
top: 929px;
|
|
||||||
transform: scaleX(-1) scale(1.5);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-4 {
|
|
||||||
left: 104px;
|
|
||||||
top: 1305px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xfl-location-5 {
|
|
||||||
left: 104px;
|
|
||||||
top: 1513px;
|
|
||||||
transform: scaleX(-1) scale(1.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-tips {
|
.click-tips {
|
||||||
@include pos(58px, 71px, 556px, 500px);
|
@include pos(58px, 71px, 556px, 500px);
|
||||||
@ -596,4 +641,49 @@ const animationFn = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.line-box {
|
||||||
|
@include pos(348px, 1048px, 185px, 583px);
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
/* 确保元素可超出容器 */
|
||||||
|
|
||||||
|
.path-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateZ(0);
|
||||||
|
opacity: 0;
|
||||||
|
/* 修复渲染层级 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.xfl-icon {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 163px;
|
||||||
|
height: 153px;
|
||||||
|
// 以中心放大1.5倍
|
||||||
|
|
||||||
|
will-change: transform;
|
||||||
|
transform: translate(-50%, -50%) scale(1.5);
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
#frameBox {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .box {
|
||||||
|
// width: 30px;
|
||||||
|
// height: 30px;
|
||||||
|
// background-color: #42b983;
|
||||||
|
// position: absolute;
|
||||||
|
// left: 0;
|
||||||
|
// top: 0;
|
||||||
|
// transform: translate(-50%, -50%);
|
||||||
|
// will-change: transform;
|
||||||
|
// /* 优化动画性能 */
|
||||||
|
// }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
159
src/components/Line copy.vue
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<!-- viewBox="0 0 600 1800" -->
|
||||||
|
<div class="svg-box">
|
||||||
|
<svg ref="svg" width="100%" height="100%" viewBox="0 0 348 1048" @click="handlePathClick">
|
||||||
|
<!-- 可交互的L形路径 -->
|
||||||
|
|
||||||
|
<!-- 动态节点 -->
|
||||||
|
<circle v-for="(point, i) in dynamicPoints" :key="i" :cx="point.x" :cy="point.y" r="12" fill="#757575"
|
||||||
|
class="node" @click.stop="moveCharacter(point.progress)" />
|
||||||
|
|
||||||
|
<path ref="path"
|
||||||
|
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="3.5" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 动态吉祥物 -->
|
||||||
|
<transition name="line">
|
||||||
|
<div v-show="showMascot" class="mascot" :style="mascotStyle">
|
||||||
|
<!-- <div class="antenna"></div> -->
|
||||||
|
<div class="body"></div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
// 路径控制点(根据L形结构设置)
|
||||||
|
const dynamicPoints = ref([
|
||||||
|
{ progress: 0, x: 318, y: 10 }, // 顶部起点
|
||||||
|
{ progress: 33, x: 140, y: 181 }, // 垂直段中点
|
||||||
|
{ progress: 66, x: 244, y: 425 }, // 转折点
|
||||||
|
{ progress: 83, x: 0, y: 815 }, // 水平段中点
|
||||||
|
{ progress: 100, x: 0, y: 1000 } // 右侧终点
|
||||||
|
])
|
||||||
|
|
||||||
|
// 吉祥物移动逻辑
|
||||||
|
const path = ref(null)
|
||||||
|
const mascotStyle = ref({ transform: 'translate(0,0)' })
|
||||||
|
const showMascot = ref(true) // true表示默认显示
|
||||||
|
const calculatePosition = (progress) => {
|
||||||
|
const pathEl = path.value
|
||||||
|
const totalLength = pathEl.getTotalLength()
|
||||||
|
|
||||||
|
const point = pathEl.getPointAtLength((totalLength * progress) / 100)
|
||||||
|
|
||||||
|
// 计算方向向量
|
||||||
|
const nextPoint = pathEl.getPointAtLength((totalLength * (progress + 1)) / 100)
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: point.x - 40, // 40为吉祥物尺寸的一半
|
||||||
|
y: point.y - 40,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveCharacter = (progress) => {
|
||||||
|
const { x, y, } = calculatePosition(progress)
|
||||||
|
mascotStyle.value = {
|
||||||
|
transform: `translate(${x}px, ${y}px)`,
|
||||||
|
transition: 'all 0.6s cubic-bezier(0.68, -0.55, 0.27, 1.55)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击路径任意位置添加节点
|
||||||
|
const handlePathClick = (e) => {
|
||||||
|
const svg = e.currentTarget
|
||||||
|
const pt = svg.createSVGPoint()
|
||||||
|
pt.x = e.clientX
|
||||||
|
pt.y = e.clientY
|
||||||
|
const { x, y } = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||||
|
|
||||||
|
dynamicPoints.value.push({
|
||||||
|
progress: calculateProgress(x, y),
|
||||||
|
x: x,
|
||||||
|
y: y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
const calculateProgress = (x, y) => {
|
||||||
|
// 根据实际路径结构计算进度百分比
|
||||||
|
if (y < 500) return (y - 200) / 3 // 垂直段
|
||||||
|
return 66 + (x - 300) / 2 // 水平段
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
padding: 2rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba($color: #000000, $alpha: .7)
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-box {
|
||||||
|
@include pos(348px, 1048px, 180px, 578px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive-path {
|
||||||
|
cursor: crosshair;
|
||||||
|
transition: stroke 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
stroke: #FF9100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mascot {
|
||||||
|
position: absolute;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background-color: aqua;
|
||||||
|
|
||||||
|
.body {
|
||||||
|
background: #2196F3;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.antenna {
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
left: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 20px;
|
||||||
|
background: #FFF;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce-enter-active {
|
||||||
|
animation: bounce-in 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-in {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,55 +1,143 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="line-container">
|
||||||
|
<button @click="handleMove(25)">前进25%</button>
|
||||||
|
<button @click="handleMove(-25)">后退25%</button>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- SVG路径作为轨迹 -->
|
<svg class="path-svg" viewBox="0 0 348 1048">
|
||||||
<svg style="display: none;">
|
<path id="motionPath"
|
||||||
<path id="path" d="M10 80 L40 50 C60 30 80 80 100 50 S140 20 180 80" />
|
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>
|
</svg>
|
||||||
|
<div class="box" ref="box"></div>
|
||||||
<!-- 动画元素 -->
|
</div>
|
||||||
<div class="box"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { ref, onMounted } from 'vue'
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap'
|
||||||
|
import { MotionPathPlugin } from 'gsap/MotionPathPlugin'
|
||||||
|
|
||||||
// 手动注册 PathPlugin(GSAP 3.9.0+ 版本内置)
|
gsap.registerPlugin(MotionPathPlugin)
|
||||||
gsap.registerPlugin(gsap.PathPlugin);
|
|
||||||
|
const box = ref(null)
|
||||||
|
const currentProgress = ref(0) // 当前进度百分比
|
||||||
|
let animation = null
|
||||||
|
|
||||||
|
// 新增:坐标转换方法
|
||||||
|
const convertCoordinates = (point) => {
|
||||||
|
const svg = document.querySelector('.path-svg')
|
||||||
|
const pt = svg.createSVGPoint()
|
||||||
|
|
||||||
|
pt.x = point.x
|
||||||
|
pt.y = point.y
|
||||||
|
return pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||||
|
}
|
||||||
|
|
||||||
console.log('gsap',gsap);
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const box = document.querySelector('.box');
|
// 获取SVG元素
|
||||||
const path = document.getElementById('path');
|
const svg = document.querySelector('.path-svg')
|
||||||
|
const path = document.querySelector('#motionPath')
|
||||||
|
|
||||||
// 创建动画
|
// 关键:设置SVG的preserveAspectRatio
|
||||||
gsap.to(box, {
|
svg.setAttribute('preserveAspectRatio', 'none')
|
||||||
duration: 5,
|
|
||||||
ease: 'power2.inOut',
|
animation = gsap.to(box.value, {
|
||||||
motionPath: {
|
motionPath: {
|
||||||
path: path, // 绑定 SVG 路径元素
|
path: "#motionPath",
|
||||||
align: 'start', // 对齐路径起点(默认值)
|
align: "#motionPath",
|
||||||
autoRotate: true // 自动旋转方向(GSAP 3.9.0+ 支持)
|
autoRotate: false,
|
||||||
|
// 新增坐标转换
|
||||||
|
from: convertCoordinates({ x: 0, y: 0 }),
|
||||||
|
to: convertCoordinates({ x: 1, y: 1 }),
|
||||||
|
usePathData: true
|
||||||
|
},
|
||||||
|
paused: true,
|
||||||
|
duration: 1,
|
||||||
|
ease: "power2.inOut"
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始定位优化
|
||||||
|
gsap.set(box.value, {
|
||||||
|
x: convertCoordinates(path.getPointAtLength(0)).x,
|
||||||
|
y: convertCoordinates(path.getPointAtLength(0)).y
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 统一运动控制方法
|
||||||
|
const handleMove = (percent) => {
|
||||||
|
const target = currentProgress.value + percent
|
||||||
|
|
||||||
|
// 边界限制
|
||||||
|
const clampedTarget = Math.min(Math.max(target, 0), 100)
|
||||||
|
|
||||||
|
// 创建补间动画
|
||||||
|
gsap.to(currentProgress, {
|
||||||
|
value: clampedTarget,
|
||||||
|
duration: 1,
|
||||||
|
onUpdate: () => {
|
||||||
|
animation.progress(currentProgress.value / 100)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
console.log(`当前进度: ${currentProgress.value}%`)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
|
.line-container {
|
||||||
|
@include fixed();
|
||||||
|
// @include flexCen();
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 348px;
|
||||||
|
height: 1048px;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow: visible;
|
||||||
|
/* 确保元素可超出容器 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 600px;
|
height: 100%;
|
||||||
|
transform: translateZ(0);
|
||||||
|
/* 修复渲染层级 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
width: 50px;
|
width: 30px;
|
||||||
height: 50px;
|
height: 30px;
|
||||||
background-color: red;
|
background-color: #42b983;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 80px; /* 初始位置与SVG路径起点对齐 */
|
left: 0;
|
||||||
left: 10px;
|
top: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
will-change: transform;
|
||||||
|
/* 优化动画性能 */
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 10px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #42b983;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #3aa876;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||