已给版本

This commit is contained in:
梁泽军 2025-03-04 16:13:41 +08:00
parent ebfab921df
commit e3f8651643
32 changed files with 468 additions and 131 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 281 KiB

View File

@ -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')
// SVGpreserveAspectRatio
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>

View 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>

View File

@ -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'
// PathPluginGSAP 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')
// // SVGpreserveAspectRatio
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>