已给版本
|
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-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>
|
||||
@ -35,9 +46,7 @@
|
||||
<div class="boat-3"></div>
|
||||
<div class="bottom-bird-1"></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="tips-text" v-if="locationId == 0"></div>
|
||||
@ -52,26 +61,42 @@
|
||||
</div>
|
||||
<div class="cls-btn" @click="hideVideo"></div>
|
||||
</div>
|
||||
<Line />
|
||||
|
||||
<!-- <Line /> -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import gsap from 'gsap'
|
||||
|
||||
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, 38, 48, 67, 100,]
|
||||
|
||||
const playerRef = ref(null)
|
||||
const frame = ref(null)
|
||||
const imageFramePlayer = ref(null)
|
||||
const locationId = ref(0)
|
||||
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(false)
|
||||
let animation = null
|
||||
|
||||
// 视频库
|
||||
const videoList = [
|
||||
@ -88,6 +113,7 @@ const frameList = Array.from({ length: 35 }, (_, index) => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initSvgAnimation()
|
||||
animationFn();
|
||||
palyFrame()
|
||||
playerRef.value = new Plyr('#player', {
|
||||
@ -124,8 +150,18 @@ const palyFrame = () => {
|
||||
// 播放事件
|
||||
const playFn = (index) => {
|
||||
|
||||
if (locationId.value === 0 || locationId.value === index) {
|
||||
locationId.value = 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)
|
||||
console.log('setpNum', setpNum);
|
||||
|
||||
locationId.value = index;
|
||||
playerRef.value.source = {
|
||||
type: 'video',
|
||||
sources: [
|
||||
@ -135,22 +171,8 @@ const playFn = (index) => {
|
||||
}
|
||||
]
|
||||
};
|
||||
gsap.to(".xfl-icon", {
|
||||
autoAlpha: 1,
|
||||
duration: 0.5,
|
||||
onComplete: () => {
|
||||
gsap.to(".video-popup", {
|
||||
autoAlpha: 1,
|
||||
duration: 0.5
|
||||
});
|
||||
}
|
||||
});
|
||||
handleMove(progressArr[index - 1], setpNum)
|
||||
} else {
|
||||
|
||||
gsap.to(".xfl-icon", {
|
||||
autoAlpha: 0,
|
||||
duration: 0.5,
|
||||
onComplete: () => {
|
||||
locationId.value = index
|
||||
playerRef.value.source = {
|
||||
type: 'video',
|
||||
@ -161,26 +183,19 @@ const playFn = (index) => {
|
||||
}
|
||||
]
|
||||
};
|
||||
gsap.to(".xfl-icon", {
|
||||
autoAlpha: 1,
|
||||
duration: 0.5,
|
||||
onComplete: () => {
|
||||
gsap.to(".video-popup", {
|
||||
autoAlpha: 1,
|
||||
duration: 0.5
|
||||
});
|
||||
duration: 0.3,
|
||||
onComplete: () => {
|
||||
debounce.value = false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 标记已经去过的地点
|
||||
isMarkList.value[locationId.value - 1] = true
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 关闭弹窗
|
||||
const hideVideo = () => {
|
||||
@ -198,6 +213,81 @@ const hideVideo = () => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 新增:坐标转换方法
|
||||
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 = () => {
|
||||
|
||||
@ -344,7 +434,7 @@ const animationFn = () => {
|
||||
.IndexPage {
|
||||
@include bgSrc("index/bg.jpg");
|
||||
@include box(750px, 1929px);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.index-title {
|
||||
@ -512,51 +602,6 @@ const animationFn = () => {
|
||||
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 {
|
||||
@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>
|
||||
|
||||
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>
|
||||
<div class="line-container">
|
||||
<button @click="handleMove(25)">前进25%</button>
|
||||
<button @click="handleMove(-25)">后退25%</button>
|
||||
<div class="container">
|
||||
<!-- SVG路径作为轨迹 -->
|
||||
<svg style="display: none;">
|
||||
<path id="path" d="M10 80 L40 50 C60 30 80 80 100 50 S140 20 180 80" />
|
||||
<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"></div>
|
||||
<div class="box" ref="box"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { MotionPathPlugin } from 'gsap/MotionPathPlugin'
|
||||
|
||||
// 手动注册 PathPlugin(GSAP 3.9.0+ 版本内置)
|
||||
gsap.registerPlugin(gsap.PathPlugin);
|
||||
gsap.registerPlugin(MotionPathPlugin)
|
||||
|
||||
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(() => {
|
||||
const box = document.querySelector('.box');
|
||||
const path = document.getElementById('path');
|
||||
// 获取SVG元素
|
||||
const svg = document.querySelector('.path-svg')
|
||||
const path = document.querySelector('#motionPath')
|
||||
|
||||
// 创建动画
|
||||
gsap.to(box, {
|
||||
duration: 5,
|
||||
ease: 'power2.inOut',
|
||||
// 关键:设置SVG的preserveAspectRatio
|
||||
svg.setAttribute('preserveAspectRatio', 'none')
|
||||
|
||||
animation = gsap.to(box.value, {
|
||||
motionPath: {
|
||||
path: path, // 绑定 SVG 路径元素
|
||||
align: 'start', // 对齐路径起点(默认值)
|
||||
autoRotate: true // 自动旋转方向(GSAP 3.9.0+ 支持)
|
||||
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(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>
|
||||
|
||||
<style>
|
||||
<style lang="scss" scoped>
|
||||
.line-container {
|
||||
@include fixed();
|
||||
// @include flexCen();
|
||||
background-color: rgba($color: #000000, $alpha: 0.7);
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
width: 348px;
|
||||
height: 1048px;
|
||||
margin-top: 20px;
|
||||
overflow: visible;
|
||||
/* 确保元素可超出容器 */
|
||||
}
|
||||
|
||||
.path-svg {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
height: 100%;
|
||||
transform: translateZ(0);
|
||||
/* 修复渲染层级 */
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: red;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: #42b983;
|
||||
position: absolute;
|
||||
top: 80px; /* 初始位置与SVG路径起点对齐 */
|
||||
left: 10px;
|
||||
left: 0;
|
||||
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>
|
||||