更加优雅的首页加载动画
This commit is contained in:
parent
f6c410a231
commit
f5683dd52a
@ -111,11 +111,12 @@ function makeHomeAnimation() {
|
||||
|
||||
|
||||
const elements = [
|
||||
{ selector: ".bilibili-player-container", start: "top 65%", end: "top 65%" },
|
||||
{ selector: "#openmcp-为谁准备", trigger: '#openmcp-为谁准备', start: "top 65%", end: "top 65%" },
|
||||
{ selector: ".k-tabs", trigger: '#openmcp-为谁准备', start: "top 65%", end: "top 65%" },
|
||||
{ selector: "#问题解答-faq", trigger: '#问题解答-faq', start: "top 65%", end: "top 65%" },
|
||||
{ selector: ".el-collapse", trigger: '#问题解答-faq', start: "top 65%", end: "top 65%" },
|
||||
{ selector: "#home-0", trigger: '#home-0', start: "top 65%", end: "top 65%" },
|
||||
{ selector: ".bilibili-player-container", trigger: '#home-0', start: "top 65%", end: "top 65%" },
|
||||
{ selector: "#home-1", trigger: '#home-1', start: "top 65%", end: "top 65%" },
|
||||
{ selector: ".k-tabs", trigger: '#home-1', start: "top 65%", end: "top 65%" },
|
||||
{ selector: "#home-2", trigger: '#home-2', start: "top 65%", end: "top 65%" },
|
||||
{ selector: ".el-collapse", trigger: '#home-2', start: "top 65%", end: "top 65%" },
|
||||
];
|
||||
|
||||
elements.forEach(element => {
|
||||
|
@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="k-tabs" :style="panelStyle">
|
||||
<div class="k-tabs">
|
||||
<div class="k-tabs-tags">
|
||||
<div class="k-tabs-tag-item" v-for="pane of tabsContainer.paneInfos" :key="pane.id"
|
||||
:ref="el => tabsContainer.getPanes(el, pane.id)" @click="tabsContainer.switchLabel(pane.id)"
|
||||
:class="{ 'active-tab': tabsContainer.lastPaneId === pane.id }">
|
||||
:ref="el => tabsContainer.getPanes(el, pane.id)" @click="tabsContainer.switchLabel(pane.id)" :class="{
|
||||
'active-tab': tabsContainer.lastPaneId === pane.id,
|
||||
'prev-tab': pane.id === prevActiveIndex
|
||||
}">
|
||||
<span :class="pane.labelClass"></span>
|
||||
<span>{{ pane.label }}</span>
|
||||
</div>
|
||||
@ -15,7 +17,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, useSlots, provide, nextTick, computed, onMounted } from 'vue';
|
||||
import { reactive, useSlots, nextTick, onMounted, ref } from 'vue';
|
||||
import gsap from 'gsap';
|
||||
|
||||
type PaneInfo = {
|
||||
id: number;
|
||||
@ -23,116 +26,96 @@ type PaneInfo = {
|
||||
labelClass: string;
|
||||
};
|
||||
|
||||
interface TabsContainer {
|
||||
paneInfos: PaneInfo[];
|
||||
panes: HTMLElement[];
|
||||
lastPaneId?: number;
|
||||
activeLabel: string;
|
||||
panelContainer?: any;
|
||||
height: string;
|
||||
getPanes: (el: any, id: string | number) => void;
|
||||
switchLabel: (id: number) => void;
|
||||
updateLabels: () => void;
|
||||
}
|
||||
|
||||
const slots = useSlots();
|
||||
let maxChildHeight = 0;
|
||||
const prevActiveIndex = ref<number | null>(null);
|
||||
|
||||
function resizeTab(id: number) {
|
||||
const container = tabsContainer.panelContainer;
|
||||
if (container) {
|
||||
const panels = Array.from(container.children) as HTMLElement[];
|
||||
|
||||
const currentPanel = panels[id];
|
||||
if (currentPanel) {
|
||||
maxChildHeight = Math.max(maxChildHeight, currentPanel.clientHeight);
|
||||
tabsContainer.height = maxChildHeight + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const panelStyle = computed(() => ({
|
||||
height: tabsContainer.height
|
||||
}));
|
||||
|
||||
const tabsContainer: TabsContainer = reactive({
|
||||
paneInfos: [],
|
||||
panes: [],
|
||||
const tabsContainer = reactive({
|
||||
paneInfos: [] as PaneInfo[],
|
||||
panes: [] as HTMLElement[],
|
||||
lastPaneId: 0,
|
||||
activeLabel: '',
|
||||
hoverBar: null,
|
||||
panelContainer: undefined,
|
||||
height: '0',
|
||||
getPanes(el: HTMLElement | null, id: string | number) {
|
||||
if (el) {
|
||||
this.panes[id] = el;
|
||||
}
|
||||
panelContainer: undefined as HTMLElement | undefined,
|
||||
|
||||
getPanes(el: HTMLElement | null, id: number) {
|
||||
if (el) this.panes[id] = el;
|
||||
},
|
||||
switchLabel(id: number) {
|
||||
if (this.lastPaneId === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
async switchLabel(id: number) {
|
||||
if (this.lastPaneId === id) return;
|
||||
|
||||
const oldId = this.lastPaneId;
|
||||
prevActiveIndex.value = oldId;
|
||||
this.lastPaneId = id;
|
||||
this.activeLabel = this.paneInfos[id]?.label || '';
|
||||
|
||||
const container = tabsContainer.panelContainer;
|
||||
const container = this.panelContainer;
|
||||
if (!container) return;
|
||||
|
||||
const panels = Array.from(container.children) as HTMLElement[];
|
||||
const oldPanel = panels[oldId];
|
||||
const newPanel = panels[id];
|
||||
|
||||
// 进行动画切换
|
||||
// 1. First shrink the old panel to 0.3 scale
|
||||
if (oldPanel) {
|
||||
await gsap.to(oldPanel, {
|
||||
scale: 0.3,
|
||||
opacity: 0,
|
||||
duration: 0.3,
|
||||
ease: "power2.in"
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Hide all other panels
|
||||
panels.forEach((panel, index) => {
|
||||
console.log('index', index);
|
||||
console.log('id', id);
|
||||
|
||||
panel.style.transition = 'opacity 0.3s ease';
|
||||
if (index === id) {
|
||||
panel.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
panel.style.opacity = '1';
|
||||
}, 150);
|
||||
} else {
|
||||
panel.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
panel.style.display = 'none';
|
||||
}, 300);
|
||||
if (index !== id) {
|
||||
gsap.set(panel, {
|
||||
display: 'none',
|
||||
opacity: 0,
|
||||
scale: 1
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Show and animate in the new panel
|
||||
gsap.set(newPanel, {
|
||||
display: 'block',
|
||||
opacity: 0,
|
||||
scale: 1.2
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
resizeTab(id);
|
||||
gsap.to(newPanel, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: "back.out(1.2)"
|
||||
});
|
||||
},
|
||||
updateLabels() {
|
||||
const defaultChildren = slots.default?.() || [];
|
||||
|
||||
this.paneInfos = [];
|
||||
for (const index in defaultChildren) {
|
||||
const vnode = defaultChildren[index];
|
||||
this.paneInfos.push({
|
||||
id: Number(index),
|
||||
updateLabels() {
|
||||
this.paneInfos = (slots.default?.() || []).map((vnode, index) => ({
|
||||
id: index,
|
||||
label: vnode.props?.label || '',
|
||||
labelClass: vnode.props?.labelClass || '',
|
||||
});
|
||||
}
|
||||
if (this.paneInfos.length > 0) {
|
||||
this.activeLabel = this.paneInfos[0]?.label || '';
|
||||
nextTick(() => {
|
||||
resizeTab(0);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
tabsContainer.updateLabels();
|
||||
|
||||
onMounted(() => {
|
||||
if (tabsContainer.panelContainer) {
|
||||
const panels = Array.from(tabsContainer.panelContainer.children) as HTMLElement[];
|
||||
panels.forEach((panel, index) => {
|
||||
panel.style.position = 'absolute';
|
||||
panel.style.width = '100%';
|
||||
|
||||
if (index != tabsContainer.lastPaneId) {
|
||||
if (index !== 0) {
|
||||
panel.style.display = 'none';
|
||||
panel.style.opacity = '0';
|
||||
} else {
|
||||
gsap.from(panel, {
|
||||
scale: 1.2,
|
||||
opacity: 0,
|
||||
duration: 0.5
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -142,58 +125,43 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.k-tabs {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.k-tabs-tags {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.k-tabs-tag-item {
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
border-radius: .5em;
|
||||
margin-right: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.8rem;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.k-tabs-tag-item {
|
||||
padding: 6px 16px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.k-tabs-tag-item:hover {
|
||||
background-color: var(--vp-c-brand-2);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transform-origin: center;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.k-tabs-tag-item.active-tab {
|
||||
background-color: var(--vp-c-brand-3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 414px) {
|
||||
.k-tabs-tags {
|
||||
}
|
||||
}
|
||||
|
||||
.hover-bar {
|
||||
background-color: var(--vp-c-brand-3);
|
||||
border-radius: .9em .9em 0 0;
|
||||
transition: .35s ease-in-out;
|
||||
position: absolute;
|
||||
.k-tabs-content {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.k-tabs-content>* {
|
||||
transition: opacity 0.3s ease;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: center center;
|
||||
}
|
||||
</style>
|
@ -1,22 +1,24 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="bilibili-player-container">
|
||||
<div class="bilibili-player-container" @mouseenter="onHover" @mouseleave="onHoverEnd">
|
||||
<iframe v-if="isPlaying" :src="playerUrl" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
<div v-else class="cover-container" @click="playVideo">
|
||||
<img :src="props.cover" class="cover-image" />
|
||||
<button class="play-button">
|
||||
<img :src="props.cover" class="cover-image" ref="coverImage" />
|
||||
<button class="play-button" ref="playButton">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||
<path fill="currentColor" d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hover-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import gsap from 'gsap'
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
@ -31,10 +33,56 @@ const props = defineProps({
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const playerUrl = ref(props.url);
|
||||
const coverImage = ref(null);
|
||||
const playButton = ref(null);
|
||||
|
||||
function playVideo() {
|
||||
isPlaying.value = true
|
||||
}
|
||||
|
||||
function onHover() {
|
||||
if (!isPlaying.value) {
|
||||
gsap.to(coverImage.value, {
|
||||
filter: 'brightness(0.7) saturate(1.2)',
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
});
|
||||
gsap.to(playButton.value, {
|
||||
scale: 1.15,
|
||||
duration: 0.3,
|
||||
ease: 'elastic.out(1, 0.5)'
|
||||
});
|
||||
gsap.to('.hover-overlay', {
|
||||
opacity: 1,
|
||||
duration: 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onHoverEnd() {
|
||||
if (!isPlaying.value) {
|
||||
gsap.to(coverImage.value, {
|
||||
filter: 'brightness(1) saturate(1)',
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
});
|
||||
gsap.to(playButton.value, {
|
||||
scale: 1,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
});
|
||||
gsap.to('.hover-overlay', {
|
||||
opacity: 0,
|
||||
duration: 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始状态设置
|
||||
gsap.set(coverImage.value, { filter: 'brightness(1) saturate(1)' });
|
||||
gsap.set('.hover-overlay', { opacity: 0 });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -54,7 +102,13 @@ function playVideo() {
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: .5em;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--vp-c-brand-2);
|
||||
transition: box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bilibili-player-container:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.cover-container {
|
||||
@ -68,6 +122,19 @@ function playVideo() {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: filter 0.3s ease;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.hover-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(var(--vp-c-brand-2-rgb), 0.2) 0%, rgba(var(--vp-c-brand-1-rgb), 0.1) 100%);
|
||||
pointer-events: none;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
@ -85,16 +152,24 @@ function playVideo() {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
transition: transform 0.3s ease, background-color 0.3s ease;
|
||||
will-change: transform;
|
||||
z-index: 2;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
background-color: var(--vp-c-brand-2);
|
||||
background-color: var(--vp-c-brand-1);
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
.play-button svg {
|
||||
margin-left: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.play-button:hover svg {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
iframe {
|
||||
@ -102,4 +177,24 @@ iframe {
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.bilibili-player-container {
|
||||
min-width: 90vw;
|
||||
min-height: calc(90vw * 9 / 16);
|
||||
width: 90vw;
|
||||
height: calc(90vw * 9 / 16);
|
||||
}
|
||||
|
||||
.play-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.play-button svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div style="height: fit-content; width: fit-content; position: relative;" ref="container">
|
||||
<svg class="VPImage image-src" viewBox="0 0 612 612" fill="none" xmlns="http://www.w3.org/2000/svg" ref="svgElement">
|
||||
<svg class="VPImage image-src" viewBox="0 0 612 612" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
ref="svgElement">
|
||||
<defs>
|
||||
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
||||
<stop offset="0" stop-color="#A1A7F6" />
|
||||
@ -58,6 +59,11 @@
|
||||
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||
</filter>
|
||||
<!-- 新增描边渐变色定义 -->
|
||||
<linearGradient id="strokeGradient" gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#A48BF7" />
|
||||
<stop offset="80%" stop-color="#F7F6FD" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(6 2)">
|
||||
<g>
|
||||
@ -87,67 +93,45 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
|
||||
const container = ref(null)
|
||||
const svgElement = ref(null)
|
||||
const container = ref < any > (null);
|
||||
const svgElement = ref < any > (null);
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const paths = svgElement.value.querySelectorAll('path')
|
||||
const paths = svgElement.value.querySelectorAll('path');
|
||||
// 标题文字
|
||||
const nameClip = document.querySelector('.VPHero .heading .name.clip');
|
||||
const headingText = document.querySelector('.VPHero .heading .text');
|
||||
const tagline = document.querySelector('.VPHero .tagline');
|
||||
const actions = document.querySelector('.VPHero .actions');
|
||||
|
||||
// 设置首页各个元素的初始状态
|
||||
gsap.set(nameClip, {
|
||||
transform: 'translate(50px, 80px)',
|
||||
scale: 1.8,
|
||||
});
|
||||
|
||||
gsap.set([headingText, tagline, actions], {
|
||||
opacity: 0,
|
||||
y: 20
|
||||
});
|
||||
|
||||
// 设置初始状态
|
||||
gsap.set(paths, {
|
||||
strokeDasharray: (i, target) => {
|
||||
strokeDasharray: (_, target) => {
|
||||
const length = target.getTotalLength()
|
||||
target.style.strokeDasharray = length
|
||||
return length
|
||||
},
|
||||
strokeDashoffset: (i, target) => target.getTotalLength(),
|
||||
stroke: 'var(--vp-c-brand-2)',
|
||||
strokeWidth: 3,
|
||||
strokeDashoffset: (_, target) => target.getTotalLength(),
|
||||
stroke: 'url(#strokeGradient)', // 使用渐变色描边
|
||||
strokeWidth: 3.5,
|
||||
fillOpacity: 0
|
||||
})
|
||||
|
||||
// 创建主时间线
|
||||
const master = gsap.timeline()
|
||||
|
||||
// 描边动画时间线
|
||||
const drawTimeline = gsap.timeline({
|
||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||
})
|
||||
|
||||
// 为所有路径添加描边动画
|
||||
paths.forEach(path => {
|
||||
drawTimeline.to(path, {
|
||||
strokeDashoffset: 0,
|
||||
}, "<+=0.3")
|
||||
})
|
||||
|
||||
// 反向描边消失动画时间线
|
||||
const reverseTimeline = gsap.timeline({
|
||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||
})
|
||||
|
||||
// 为所有路径添加反向描边动画
|
||||
paths.forEach(path => {
|
||||
reverseTimeline.to(path, {
|
||||
fillOpacity: 1,
|
||||
strokeDashoffset: (i, target) => target.getTotalLength()
|
||||
}, "<")
|
||||
})
|
||||
|
||||
// 微扰动画时间线
|
||||
const wiggleTimeline = gsap.timeline({
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
defaults: {
|
||||
duration: 2,
|
||||
ease: "sine.inOut"
|
||||
}
|
||||
})
|
||||
|
||||
// 为每个圆形元素创建不同的微扰动画
|
||||
const wiggleElements = [
|
||||
{
|
||||
@ -166,7 +150,55 @@ onMounted(() => {
|
||||
el: svgElement.value.querySelector('[transform="translate(388 129)"]'),
|
||||
x: "+=9", y: "+=4", rotation: "-=2"
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
// 创建主时间线
|
||||
const master = gsap.timeline();
|
||||
|
||||
// 描边动画时间线
|
||||
const drawTimeline = gsap.timeline({
|
||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||
});
|
||||
|
||||
// OpenMCP 标题入场动画
|
||||
if (!nameClip || !headingText || !tagline || !actions) {
|
||||
console.error("One or more required elements are missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
drawTimeline.to(nameClip, {
|
||||
transform: 'translate(50px, 80px)',
|
||||
}, "<+=0.3");
|
||||
|
||||
// 右侧 OpenMCP 图标描边动画
|
||||
paths.forEach((path: gsap.TweenTarget) => {
|
||||
drawTimeline.to(path, {
|
||||
strokeDashoffset: 0,
|
||||
}, "<+=0.3");
|
||||
});
|
||||
|
||||
// 反向描边消失动画时间线
|
||||
const reverseTimeline = gsap.timeline({
|
||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||
});
|
||||
|
||||
// 为所有路径添加反向描边动画
|
||||
paths.forEach(path => {
|
||||
reverseTimeline.to(path, {
|
||||
fillOpacity: 1,
|
||||
strokeDashoffset: (i, target) => target.getTotalLength()
|
||||
}, "<")
|
||||
});
|
||||
|
||||
// 微扰动画时间线
|
||||
const wiggleTimeline = gsap.timeline({
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
defaults: {
|
||||
duration: 2,
|
||||
ease: "sine.inOut"
|
||||
}
|
||||
});
|
||||
|
||||
// 为每个元素添加独特的微扰动画
|
||||
wiggleElements.forEach(item => {
|
||||
@ -174,15 +206,90 @@ onMounted(() => {
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
rotation: item.rotation,
|
||||
scale: item.scale,
|
||||
transformOrigin: "center center"
|
||||
}, "<")
|
||||
})
|
||||
});
|
||||
|
||||
// 创建主时间线
|
||||
const heroTimeline = gsap.timeline({
|
||||
defaults: {
|
||||
duration: 0.8,
|
||||
ease: "power3.out"
|
||||
}
|
||||
});
|
||||
|
||||
// 添加动画序列
|
||||
heroTimeline
|
||||
.to(nameClip, {
|
||||
transform: 'translate(0, 0)',
|
||||
scale: 1,
|
||||
duration: 1.2,
|
||||
ease: "back.out(1.7)"
|
||||
})
|
||||
.to([headingText, tagline, actions], {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
stagger: 0.15,
|
||||
duration: 0.6
|
||||
}, "-=0.4"); // 与上一个动画重叠0.4秒
|
||||
|
||||
|
||||
// 下方三个 feature 的动画
|
||||
const boxTimeline = gsap.timeline({
|
||||
defaults: {
|
||||
duration: 0.8,
|
||||
ease: "power3.out"
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有box元素
|
||||
const boxes = document.querySelectorAll('.VPFeatures .item.grid-3');
|
||||
|
||||
// 设置初始状态
|
||||
gsap.set(boxes, {
|
||||
opacity: 0,
|
||||
y: 30,
|
||||
scale: 0.9
|
||||
});
|
||||
|
||||
// 添加动画到时间线
|
||||
boxTimeline.to(boxes, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
stagger: 0.15,
|
||||
onComplete: () => {
|
||||
gsap.set(boxes, { clearProps: "all" });
|
||||
}
|
||||
});
|
||||
|
||||
// 优化heroTimeline动画效果
|
||||
heroTimeline
|
||||
.to(nameClip, {
|
||||
transform: 'translate(0, 0)',
|
||||
scale: 1,
|
||||
duration: 1.0, // 缩短持续时间
|
||||
ease: "elastic.out(1, 0.5)" // 改用弹性效果
|
||||
})
|
||||
.to([headingText, tagline], {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
stagger: 0.2, // 增加错开时间
|
||||
duration: 0.8,
|
||||
ease: "back.out(1.5)" // 添加弹跳效果
|
||||
}, "-=0.3")
|
||||
.to(actions, {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
ease: "power2.out" // 更平滑的缓动
|
||||
}, "-=0.2");
|
||||
|
||||
// 控制动画顺序
|
||||
master.add(drawTimeline)
|
||||
.add(reverseTimeline, "+=0.5")
|
||||
.add(wiggleTimeline) // 所有动画完成后开始微扰动画
|
||||
.add(reverseTimeline, "+=0.3") // 缩短间隔时间
|
||||
.add(heroTimeline, "-=0.3")
|
||||
.add(boxTimeline, "<+=1.3")
|
||||
.add(wiggleTimeline, "<+=0.2");
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -59,6 +59,7 @@
|
||||
background-color: var(--vp-c-brand-3);
|
||||
}
|
||||
|
||||
.VPFeatures .item:hover .box .title,
|
||||
.VPFeatures .item:hover .box .details {
|
||||
color: var(--vp-c-white);
|
||||
}
|
||||
|
@ -266,7 +266,52 @@ iframe {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.VPHero .name.clip {
|
||||
color: var(--vp-c-brand-2) !important;
|
||||
background-color: var(--vp-c-brand-2) !important;
|
||||
.VPHero .heading .name.clip {
|
||||
background: var(--vp-home-hero-name-background);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: var(--vp-home-hero-name-color);
|
||||
}
|
||||
|
||||
.VPHero .heading .text {
|
||||
}
|
||||
|
||||
.VPHero .tagline {
|
||||
}
|
||||
|
||||
.VPHero .actions {
|
||||
}
|
||||
|
||||
|
||||
#home-0 {
|
||||
font-size: 40px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#home-0 > span {
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
line-height: 0.8;
|
||||
}
|
||||
|
||||
#home-1 {
|
||||
font-size: 40px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#home-1 > span {
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
line-height: 0.8;
|
||||
}
|
||||
|
||||
#home-2 {
|
||||
font-size: 40px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
#home-2 > span {
|
||||
font-size: 20px;
|
||||
opacity: 0.5;
|
||||
line-height: 0.8;
|
||||
}
|
23
index.md
23
index.md
@ -40,6 +40,15 @@ features:
|
||||
|
||||
<br><br>
|
||||
|
||||
|
||||
|
||||
<h2 id="home-0">
|
||||
为您的 MCP Agent 开发排忧解难
|
||||
<br>
|
||||
<span>Provide Funs and Convenien for your mcp agent dev</span>
|
||||
</h2>
|
||||
|
||||
|
||||
<BiliPlayer
|
||||
url="//player.bilibili.com/player.html?isOutside=true&aid=114445745397200&bvid=BV1zYGozgEHcautoplay=false"
|
||||
cover="https://picx.zhimg.com/80/v2-8c1f5d99066ed272554146ed8caf7cc3_1440w.png"
|
||||
@ -47,7 +56,12 @@ features:
|
||||
|
||||
<br>
|
||||
|
||||
## OpenMCP 为谁准备?
|
||||
|
||||
<h2 id="home-1">
|
||||
OpenMCP 为谁准备?
|
||||
<br>
|
||||
<span>The Development of OpenMCP is for ...</span>
|
||||
</h2>
|
||||
|
||||
<br>
|
||||
|
||||
@ -88,8 +102,11 @@ features:
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## 问题解答 FAQ
|
||||
<h2 id="home-2">
|
||||
问题解答
|
||||
<br>
|
||||
<span>Waiting for Your Questions</span>
|
||||
</h2>
|
||||
|
||||
<el-collapse>
|
||||
<el-collapse-item title="OpenMCP 适合做什么?" name="1">
|
||||
|
Loading…
x
Reference in New Issue
Block a user