更加优雅的首页加载动画
This commit is contained in:
parent
f6c410a231
commit
f5683dd52a
@ -111,11 +111,12 @@ function makeHomeAnimation() {
|
|||||||
|
|
||||||
|
|
||||||
const elements = [
|
const elements = [
|
||||||
{ selector: ".bilibili-player-container", start: "top 65%", end: "top 65%" },
|
{ selector: "#home-0", trigger: '#home-0', start: "top 65%", end: "top 65%" },
|
||||||
{ selector: "#openmcp-为谁准备", trigger: '#openmcp-为谁准备', start: "top 65%", end: "top 65%" },
|
{ selector: ".bilibili-player-container", trigger: '#home-0', start: "top 65%", end: "top 65%" },
|
||||||
{ selector: ".k-tabs", trigger: '#openmcp-为谁准备', start: "top 65%", end: "top 65%" },
|
{ selector: "#home-1", trigger: '#home-1', start: "top 65%", end: "top 65%" },
|
||||||
{ selector: "#问题解答-faq", trigger: '#问题解答-faq', start: "top 65%", end: "top 65%" },
|
{ selector: ".k-tabs", trigger: '#home-1', start: "top 65%", end: "top 65%" },
|
||||||
{ selector: ".el-collapse", trigger: '#问题解答-faq', 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 => {
|
elements.forEach(element => {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="k-tabs" :style="panelStyle">
|
<div class="k-tabs">
|
||||||
<div class="k-tabs-tags">
|
<div class="k-tabs-tags">
|
||||||
<div class="k-tabs-tag-item" v-for="pane of tabsContainer.paneInfos" :key="pane.id"
|
<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)"
|
:ref="el => tabsContainer.getPanes(el, pane.id)" @click="tabsContainer.switchLabel(pane.id)" :class="{
|
||||||
:class="{ 'active-tab': tabsContainer.lastPaneId === pane.id }">
|
'active-tab': tabsContainer.lastPaneId === pane.id,
|
||||||
|
'prev-tab': pane.id === prevActiveIndex
|
||||||
|
}">
|
||||||
<span :class="pane.labelClass"></span>
|
<span :class="pane.labelClass"></span>
|
||||||
<span>{{ pane.label }}</span>
|
<span>{{ pane.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -15,7 +17,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 = {
|
type PaneInfo = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -23,116 +26,96 @@ type PaneInfo = {
|
|||||||
labelClass: string;
|
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();
|
const slots = useSlots();
|
||||||
let maxChildHeight = 0;
|
const prevActiveIndex = ref<number | null>(null);
|
||||||
|
|
||||||
function resizeTab(id: number) {
|
const tabsContainer = reactive({
|
||||||
const container = tabsContainer.panelContainer;
|
paneInfos: [] as PaneInfo[],
|
||||||
if (container) {
|
panes: [] as HTMLElement[],
|
||||||
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: [],
|
|
||||||
lastPaneId: 0,
|
lastPaneId: 0,
|
||||||
activeLabel: '',
|
panelContainer: undefined as HTMLElement | undefined,
|
||||||
hoverBar: null,
|
|
||||||
panelContainer: undefined,
|
getPanes(el: HTMLElement | null, id: number) {
|
||||||
height: '0',
|
if (el) this.panes[id] = el;
|
||||||
getPanes(el: HTMLElement | null, id: string | number) {
|
|
||||||
if (el) {
|
|
||||||
this.panes[id] = el;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
switchLabel(id: number) {
|
|
||||||
if (this.lastPaneId === id) {
|
async switchLabel(id: number) {
|
||||||
return;
|
if (this.lastPaneId === id) return;
|
||||||
}
|
|
||||||
|
const oldId = this.lastPaneId;
|
||||||
|
prevActiveIndex.value = oldId;
|
||||||
this.lastPaneId = id;
|
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 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) => {
|
panels.forEach((panel, index) => {
|
||||||
console.log('index', index);
|
if (index !== id) {
|
||||||
console.log('id', id);
|
gsap.set(panel, {
|
||||||
|
display: 'none',
|
||||||
panel.style.transition = 'opacity 0.3s ease';
|
opacity: 0,
|
||||||
if (index === id) {
|
scale: 1
|
||||||
panel.style.display = 'block';
|
});
|
||||||
setTimeout(() => {
|
|
||||||
panel.style.opacity = '1';
|
|
||||||
}, 150);
|
|
||||||
} else {
|
|
||||||
panel.style.opacity = '0';
|
|
||||||
setTimeout(() => {
|
|
||||||
panel.style.display = 'none';
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. Show and animate in the new panel
|
||||||
|
gsap.set(newPanel, {
|
||||||
|
display: 'block',
|
||||||
|
opacity: 0,
|
||||||
|
scale: 1.2
|
||||||
|
});
|
||||||
|
|
||||||
nextTick(() => {
|
gsap.to(newPanel, {
|
||||||
resizeTab(id);
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
duration: 0.4,
|
||||||
|
ease: "back.out(1.2)"
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateLabels() {
|
|
||||||
const defaultChildren = slots.default?.() || [];
|
|
||||||
|
|
||||||
this.paneInfos = [];
|
updateLabels() {
|
||||||
for (const index in defaultChildren) {
|
this.paneInfos = (slots.default?.() || []).map((vnode, index) => ({
|
||||||
const vnode = defaultChildren[index];
|
id: index,
|
||||||
this.paneInfos.push({
|
label: vnode.props?.label || '',
|
||||||
id: Number(index),
|
labelClass: vnode.props?.labelClass || '',
|
||||||
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();
|
tabsContainer.updateLabels();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (tabsContainer.panelContainer) {
|
if (tabsContainer.panelContainer) {
|
||||||
const panels = Array.from(tabsContainer.panelContainer.children) as HTMLElement[];
|
const panels = Array.from(tabsContainer.panelContainer.children) as HTMLElement[];
|
||||||
panels.forEach((panel, index) => {
|
panels.forEach((panel, index) => {
|
||||||
panel.style.position = 'absolute';
|
panel.style.position = 'absolute';
|
||||||
|
panel.style.width = '100%';
|
||||||
if (index != tabsContainer.lastPaneId) {
|
|
||||||
|
if (index !== 0) {
|
||||||
panel.style.display = 'none';
|
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>
|
<style scoped>
|
||||||
.k-tabs {
|
.k-tabs {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.k-tabs-tags {
|
.k-tabs-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.k-tabs-tag-item {
|
.k-tabs-tag-item {
|
||||||
background-color: var(--vp-button-alt-bg);
|
background-color: var(--vp-button-alt-bg);
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
margin-right: 10px;
|
padding: 6px 16px;
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
display: inline-block;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
transition: all 0.3s ease;
|
||||||
user-select: none;
|
transform-origin: center;
|
||||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
.k-tabs-tag-item.active-tab {
|
||||||
.k-tabs-tag-item {
|
|
||||||
padding: 6px 16px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.k-tabs-tag-item:hover {
|
|
||||||
background-color: var(--vp-c-brand-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.k-tabs-tag-item.active-tab {
|
|
||||||
background-color: var(--vp-c-brand-3);
|
background-color: var(--vp-c-brand-3);
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 414px) {
|
.k-tabs-content {
|
||||||
.k-tabs-tags {
|
position: relative;
|
||||||
}
|
min-height: 200px;
|
||||||
}
|
|
||||||
|
|
||||||
.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>* {
|
.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>
|
</style>
|
@ -1,22 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<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>
|
<iframe v-if="isPlaying" :src="playerUrl" frameborder="0" allowfullscreen></iframe>
|
||||||
|
|
||||||
<div v-else class="cover-container" @click="playVideo">
|
<div v-else class="cover-container" @click="playVideo">
|
||||||
<img :src="props.cover" class="cover-image" />
|
<img :src="props.cover" class="cover-image" ref="coverImage" />
|
||||||
<button class="play-button">
|
<button class="play-button" ref="playButton">
|
||||||
<svg viewBox="0 0 24 24" width="48" height="48">
|
<svg viewBox="0 0 24 24" width="48" height="48">
|
||||||
<path fill="currentColor" d="M8 5v14l11-7z" />
|
<path fill="currentColor" d="M8 5v14l11-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="hover-overlay"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import gsap from 'gsap'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
url: {
|
url: {
|
||||||
@ -31,10 +33,56 @@ const props = defineProps({
|
|||||||
|
|
||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const playerUrl = ref(props.url);
|
const playerUrl = ref(props.url);
|
||||||
|
const coverImage = ref(null);
|
||||||
|
const playButton = ref(null);
|
||||||
|
|
||||||
function playVideo() {
|
function playVideo() {
|
||||||
isPlaying.value = true
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -54,7 +102,13 @@ function playVideo() {
|
|||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
border-radius: .5em;
|
border-radius: .5em;
|
||||||
overflow: hidden;
|
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 {
|
.cover-container {
|
||||||
@ -68,6 +122,19 @@ function playVideo() {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
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 {
|
.play-button {
|
||||||
@ -85,16 +152,24 @@ function playVideo() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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 {
|
.play-button:hover {
|
||||||
transform: translate(-50%, -50%) scale(1.1);
|
background-color: var(--vp-c-brand-1);
|
||||||
background-color: var(--vp-c-brand-2);
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-button svg {
|
.play-button svg {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button:hover svg {
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@ -102,4 +177,24 @@ iframe {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
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>
|
</style>
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="height: fit-content; width: fit-content; position: relative;" ref="container">
|
<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>
|
<defs>
|
||||||
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
<linearGradient id="gradient_1" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
|
||||||
<stop offset="0" stop-color="#A1A7F6" />
|
<stop offset="0" stop-color="#A1A7F6" />
|
||||||
@ -58,6 +59,11 @@
|
|||||||
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
<feBlend mode="normal" in2="BackgroundImageFix_1" result="Shadow_2" />
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
<feBlend mode="normal" in="SourceGraphic" in2="Shadow_2" result="Shape_3" />
|
||||||
</filter>
|
</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>
|
</defs>
|
||||||
<g transform="translate(6 2)">
|
<g transform="translate(6 2)">
|
||||||
<g>
|
<g>
|
||||||
@ -87,102 +93,203 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { gsap } from 'gsap'
|
import { gsap } from 'gsap'
|
||||||
|
|
||||||
const container = ref(null)
|
const container = ref < any > (null);
|
||||||
const svgElement = ref(null)
|
const svgElement = ref < any > (null);
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
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, {
|
gsap.set(paths, {
|
||||||
strokeDasharray: (i, target) => {
|
strokeDasharray: (_, target) => {
|
||||||
const length = target.getTotalLength()
|
const length = target.getTotalLength()
|
||||||
target.style.strokeDasharray = length
|
target.style.strokeDasharray = length
|
||||||
return length
|
return length
|
||||||
},
|
},
|
||||||
strokeDashoffset: (i, target) => target.getTotalLength(),
|
strokeDashoffset: (_, target) => target.getTotalLength(),
|
||||||
stroke: 'var(--vp-c-brand-2)',
|
stroke: 'url(#strokeGradient)', // 使用渐变色描边
|
||||||
strokeWidth: 3,
|
strokeWidth: 3.5,
|
||||||
fillOpacity: 0
|
fillOpacity: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 为每个圆形元素创建不同的微扰动画
|
||||||
|
const wiggleElements = [
|
||||||
|
{
|
||||||
|
el: svgElement.value.querySelector('[transform="translate(293 324)"]'),
|
||||||
|
x: "+=12", y: "+=5", rotation: "+=2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: svgElement.value.querySelector('[transform="translate(48 269)"]'),
|
||||||
|
x: "-=5", y: "-=10", rotation: "-=1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: svgElement.value.querySelector('[transform="translate(188 56)"]'),
|
||||||
|
x: "-=4", y: "+=5", rotation: "+=3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
el: svgElement.value.querySelector('[transform="translate(388 129)"]'),
|
||||||
|
x: "+=9", y: "+=4", rotation: "-=2"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// 创建主时间线
|
// 创建主时间线
|
||||||
const master = gsap.timeline()
|
const master = gsap.timeline();
|
||||||
|
|
||||||
// 描边动画时间线
|
// 描边动画时间线
|
||||||
const drawTimeline = gsap.timeline({
|
const drawTimeline = gsap.timeline({
|
||||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||||
})
|
});
|
||||||
|
|
||||||
// 为所有路径添加描边动画
|
// OpenMCP 标题入场动画
|
||||||
paths.forEach(path => {
|
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, {
|
drawTimeline.to(path, {
|
||||||
strokeDashoffset: 0,
|
strokeDashoffset: 0,
|
||||||
}, "<+=0.3")
|
}, "<+=0.3");
|
||||||
})
|
});
|
||||||
|
|
||||||
// 反向描边消失动画时间线
|
// 反向描边消失动画时间线
|
||||||
const reverseTimeline = gsap.timeline({
|
const reverseTimeline = gsap.timeline({
|
||||||
defaults: { duration: 1.5, ease: "power2.inOut" }
|
defaults: { duration: 1.5, ease: "power2.inOut" }
|
||||||
})
|
});
|
||||||
|
|
||||||
// 为所有路径添加反向描边动画
|
// 为所有路径添加反向描边动画
|
||||||
paths.forEach(path => {
|
paths.forEach(path => {
|
||||||
reverseTimeline.to(path, {
|
reverseTimeline.to(path, {
|
||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
strokeDashoffset: (i, target) => target.getTotalLength()
|
strokeDashoffset: (i, target) => target.getTotalLength()
|
||||||
}, "<")
|
}, "<")
|
||||||
})
|
});
|
||||||
|
|
||||||
// 微扰动画时间线
|
// 微扰动画时间线
|
||||||
const wiggleTimeline = gsap.timeline({
|
const wiggleTimeline = gsap.timeline({
|
||||||
repeat: -1,
|
repeat: -1,
|
||||||
yoyo: true,
|
yoyo: true,
|
||||||
defaults: {
|
defaults: {
|
||||||
duration: 2,
|
duration: 2,
|
||||||
ease: "sine.inOut"
|
ease: "sine.inOut"
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 为每个圆形元素创建不同的微扰动画
|
|
||||||
const wiggleElements = [
|
|
||||||
{
|
|
||||||
el: svgElement.value.querySelector('[transform="translate(293 324)"]'),
|
|
||||||
x: "+=12", y: "+=5", rotation: "+=2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: svgElement.value.querySelector('[transform="translate(48 269)"]'),
|
|
||||||
x: "-=5", y: "-=10", rotation: "-=1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: svgElement.value.querySelector('[transform="translate(188 56)"]'),
|
|
||||||
x: "-=4", y: "+=5", rotation: "+=3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
el: svgElement.value.querySelector('[transform="translate(388 129)"]'),
|
|
||||||
x: "+=9", y: "+=4", rotation: "-=2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 为每个元素添加独特的微扰动画
|
// 为每个元素添加独特的微扰动画
|
||||||
wiggleElements.forEach(item => {
|
wiggleElements.forEach(item => {
|
||||||
wiggleTimeline.to(item.el, {
|
wiggleTimeline.to(item.el, {
|
||||||
x: item.x,
|
x: item.x,
|
||||||
y: item.y,
|
y: item.y,
|
||||||
rotation: item.rotation,
|
rotation: item.rotation,
|
||||||
scale: item.scale,
|
|
||||||
transformOrigin: "center center"
|
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)
|
master.add(drawTimeline)
|
||||||
.add(reverseTimeline, "+=0.5")
|
.add(reverseTimeline, "+=0.3") // 缩短间隔时间
|
||||||
.add(wiggleTimeline) // 所有动画完成后开始微扰动画
|
.add(heroTimeline, "-=0.3")
|
||||||
|
.add(boxTimeline, "<+=1.3")
|
||||||
|
.add(wiggleTimeline, "<+=0.2");
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
background-color: var(--vp-c-brand-3);
|
background-color: var(--vp-c-brand-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.VPFeatures .item:hover .box .title,
|
||||||
.VPFeatures .item:hover .box .details {
|
.VPFeatures .item:hover .box .details {
|
||||||
color: var(--vp-c-white);
|
color: var(--vp-c-white);
|
||||||
}
|
}
|
||||||
|
@ -266,7 +266,52 @@ iframe {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.VPHero .name.clip {
|
.VPHero .heading .name.clip {
|
||||||
color: var(--vp-c-brand-2) !important;
|
background: var(--vp-home-hero-name-background);
|
||||||
background-color: var(--vp-c-brand-2) !important;
|
-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>
|
<br><br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h2 id="home-0">
|
||||||
|
为您的 MCP Agent 开发排忧解难
|
||||||
|
<br>
|
||||||
|
<span>Provide Funs and Convenien for your mcp agent dev</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
<BiliPlayer
|
<BiliPlayer
|
||||||
url="//player.bilibili.com/player.html?isOutside=true&aid=114445745397200&bvid=BV1zYGozgEHcautoplay=false"
|
url="//player.bilibili.com/player.html?isOutside=true&aid=114445745397200&bvid=BV1zYGozgEHcautoplay=false"
|
||||||
cover="https://picx.zhimg.com/80/v2-8c1f5d99066ed272554146ed8caf7cc3_1440w.png"
|
cover="https://picx.zhimg.com/80/v2-8c1f5d99066ed272554146ed8caf7cc3_1440w.png"
|
||||||
@ -47,7 +56,12 @@ features:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
## OpenMCP 为谁准备?
|
|
||||||
|
<h2 id="home-1">
|
||||||
|
OpenMCP 为谁准备?
|
||||||
|
<br>
|
||||||
|
<span>The Development of OpenMCP is for ...</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
@ -88,8 +102,11 @@ features:
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
<h2 id="home-2">
|
||||||
## 问题解答 FAQ
|
问题解答
|
||||||
|
<br>
|
||||||
|
<span>Waiting for Your Questions</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
<el-collapse>
|
<el-collapse>
|
||||||
<el-collapse-item title="OpenMCP 适合做什么?" name="1">
|
<el-collapse-item title="OpenMCP 适合做什么?" name="1">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user