Skip to content

Commit 51d5af0

Browse files
committed
Website redesign
1 parent d94affb commit 51d5af0

28 files changed

+1804
-914
lines changed

.vitepress/components/Animations.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</div>
1414
</div>
1515
<div class="carousel-dots">
16-
<span v-for="(animation, index) in animations" :key="index" class="dot"
16+
<span v-for="(animation, index) in animations" :key="index" class="carousel-dot"
1717
:class="{ active: currentIndex === index }" @click="goToSlide(index)"></span>
1818
</div>
1919
</div>
@@ -290,7 +290,7 @@ export default {
290290
margin-top: 1rem;
291291
}
292292
293-
.dot {
293+
.carousel-dot {
294294
display: inline-block;
295295
width: 10px;
296296
height: 10px;
@@ -301,11 +301,11 @@ export default {
301301
transition: background-color 0.3s;
302302
}
303303
304-
.dot:hover {
304+
.carousel-dot:hover {
305305
background-color: var(--vp-c-brand-light);
306306
}
307307
308-
.dot.active {
308+
.carousel-dot.active {
309309
background-color: var(--vp-c-brand);
310310
}
311311
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<template>
2+
<div ref="containerRef" class="animation-container xs:aspect-[2/1]">
3+
<div class="w-full h-full">
4+
<div class="animation-box">
5+
<component :is="component" class="auto-animate-child" v-bind="componentProps"
6+
:onComplete="handleChildComplete" ref="childRef" />
7+
</div>
8+
</div>
9+
</div>
10+
</template>
11+
12+
<script setup>
13+
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
14+
15+
const props = defineProps({
16+
pauseThreshold: {
17+
type: Number,
18+
default: 0.4
19+
},
20+
rootMargin: {
21+
type: String,
22+
default: '0px'
23+
},
24+
// The component to render (pass the imported component object)
25+
component: {
26+
type: [Object, Function, String],
27+
required: false,
28+
default: null
29+
},
30+
// Optional props to pass to the rendered component
31+
componentProps: {
32+
type: Object,
33+
required: false,
34+
default: () => ({})
35+
},
36+
});
37+
38+
const containerRef = ref(null);
39+
const isPlaying = ref(false);
40+
let intersectionObserver = null;
41+
42+
const childRef = ref(null);
43+
44+
const startAnimation = async () => {
45+
if (!isPlaying.value) {
46+
isPlaying.value = true;
47+
// Try to start the child component directly if it exposes startAnimation
48+
if (childRef.value && childRef.value.startAnimation) {
49+
try {
50+
await childRef.value.startAnimation();
51+
} catch (e) {
52+
console.error('Error starting child animation:', e);
53+
}
54+
}
55+
}
56+
};
57+
58+
const stopAnimation = () => {
59+
if (isPlaying.value) {
60+
isPlaying.value = false;
61+
// Try to stop the child component directly
62+
if (childRef.value && childRef.value.stopAnimation) {
63+
try {
64+
childRef.value.stopAnimation();
65+
} catch (e) {
66+
console.error('Error stopping child animation:', e);
67+
}
68+
}
69+
}
70+
};
71+
72+
/**
73+
* When the child reports completion via its onComplete prop, restart it.
74+
* We call the child's startAnimation again to auto-replay.
75+
*/
76+
const handleChildComplete = async (payload) => {
77+
childRef.value.startAnimation();
78+
};
79+
80+
const setupIntersectionObserver = () => {
81+
if ('IntersectionObserver' in window && containerRef.value) {
82+
intersectionObserver = new IntersectionObserver((entries) => {
83+
entries.forEach(entry => {
84+
const visibilityRatio = entry.intersectionRatio;
85+
86+
// Get the element's position relative to the viewport
87+
const rect = entry.boundingClientRect;
88+
const windowHeight = window.innerHeight;
89+
90+
// Determine if the bottom of the element is visible in the viewport
91+
const bottomInViewport = rect.bottom >= 0 && rect.bottom <= windowHeight;
92+
93+
if ((visibilityRatio < props.pauseThreshold) && isPlaying.value) {
94+
stopAnimation();
95+
} else if (bottomInViewport && !isPlaying.value) {
96+
startAnimation();
97+
}
98+
});
99+
}, {
100+
threshold: [0, 0.1, 0.25, 0.5, 0.75, 1.0], // Watch for different visibility levels
101+
rootMargin: props.rootMargin
102+
});
103+
104+
intersectionObserver.observe(containerRef.value);
105+
}
106+
};
107+
108+
onMounted(() => {
109+
setupIntersectionObserver();
110+
});
111+
112+
onBeforeUnmount(() => {
113+
if (intersectionObserver && containerRef.value) {
114+
intersectionObserver.unobserve(containerRef.value);
115+
intersectionObserver.disconnect();
116+
}
117+
118+
// Clear the parent-provided animationRef when we unmount so callers
119+
// don't hold a stale reference.
120+
if (props.animationRef) {
121+
try {
122+
props.animationRef.value = null;
123+
} catch (e) {
124+
// Ignore
125+
}
126+
}
127+
});
128+
129+
defineExpose({
130+
startAnimation,
131+
stopAnimation
132+
});
133+
</script>
134+
135+
<style scoped>
136+
137+
.animation-box {
138+
overflow: hidden;
139+
transform-origin: top left;
140+
transform: scale(var(--scale));
141+
}
142+
143+
.animation-box :deep(p) {
144+
all: reset;
145+
}
146+
.animation-box {
147+
--scale: calc(min(100cqw / 400px, 1));
148+
width: 400px;
149+
}
150+
.animation-container {
151+
width: 100%;
152+
max-width: 400px;
153+
container-type: inline-size;
154+
background-color: #FAFAFA;
155+
}
156+
157+
@media (min-width: 480px) {
158+
.animation-box {
159+
width: 800px;
160+
aspect-ratio: 2 / 1;
161+
--scale: calc(min(100cqw / 800px, 1));
162+
}
163+
.animation-container {
164+
width: 100%;
165+
max-width: 800px;
166+
}
167+
}
168+
</style>

0 commit comments

Comments
 (0)