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 (100 cqw / 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 (100 cqw / 800px , 1 ));
162+ }
163+ .animation-container {
164+ width : 100% ;
165+ max-width : 800px ;
166+ }
167+ }
168+ </style >
0 commit comments