SponsorButton.vue (4822B)
1 <script setup lang="ts"> 2 import { ref, onMounted } from 'vue' 3 4 const particles = ref<SVGElement[]>([]) 5 const isBeating = ref(false) 6 const isTouchDevice = ref(false) 7 8 onMounted(() => { 9 isTouchDevice.value = window.matchMedia('(hover: none) and (pointer: coarse)').matches 10 }) 11 12 // Varied colors like anime.js — different pinks, reds, corals 13 const particleColors = [ 14 '#ff4772', '#ff6b9d', '#ff3355', '#ff8fab', 15 '#e91e63', '#f06292', '#ff1744', '#ff80ab' 16 ] 17 18 const triggerBurst = () => { 19 if (isTouchDevice.value || !particles.value?.length) return 20 21 // Cancel any running animations first so re-hover always works 22 particles.value.forEach((particle, i) => { 23 particle.getAnimations().forEach(a => a.cancel()) 24 25 const angle = (i / particles.value.length) * Math.PI * 2 + (Math.random() - 0.5) * 0.6 26 const distance = 28 + Math.random() * 32 27 const tx = Math.cos(angle) * distance 28 const ty = Math.sin(angle) * distance 29 const size = 0.6 + Math.random() * 0.8 // vary particle size 30 31 // Random color from palette 32 const color = particleColors[Math.floor(Math.random() * particleColors.length)]; 33 (particle as SVGElement & { style: CSSStyleDeclaration }).style.color = color 34 35 particle.animate([ 36 { 37 transform: `translate(0px, 0px) scale(${size})`, 38 opacity: 1, 39 visibility: 'visible' 40 }, 41 { 42 transform: `translate(${tx * 0.4}px, ${ty * 0.4}px) scale(${size * 1.1})`, 43 opacity: 1, 44 offset: 0.2 45 }, 46 { 47 transform: `translate(${tx}px, ${ty}px) scale(0)`, 48 opacity: 0, 49 visibility: 'hidden' 50 } 51 ], { 52 duration: 500 + Math.random() * 250, 53 easing: 'cubic-bezier(0.25, 1, 0.5, 1)', 54 fill: 'forwards' 55 }) 56 }) 57 58 // Heart beat 59 isBeating.value = true 60 setTimeout(() => { isBeating.value = false }, 400) 61 } 62 </script> 63 64 <template> 65 <div class="sponsor-wrapper"> 66 <a 67 href="https://github.com/sponsors/notamitgamer" 68 class="sponsor-button" 69 target="_blank" 70 rel="noopener noreferrer" 71 title="Sponsor Amit Dutta on GitHub" 72 @mouseenter="triggerBurst" 73 @click="triggerBurst" 74 :class="{ 'mobile-pill': isTouchDevice }" 75 > 76 <svg 77 xmlns="http://www.w3.org/2000/svg" 78 viewBox="4 7 16 12" 79 class="icon heart-icon" 80 :class="{ beating: isBeating }" 81 > 82 <path 83 fill="currentColor" 84 d="M12 18.445a.778.778 0 0 1-.34-.078C11.39 18.235 5 15.077 5 9.889a3.889 3.889 0 0 1 6.638-2.75L12 7.5l.362-.361A3.889 3.889 0 0 1 19 9.889c0 5.17-6.387 8.344-6.66 8.478a.778.778 0 0 1-.34.078z" 85 /> 86 </svg> 87 <span>Sponsor</span> 88 </a> 89 90 <!-- Burst particles — desktop only --> 91 <svg 92 v-if="!isTouchDevice" 93 v-for="i in 12" 94 :key="i" 95 ref="particles" 96 xmlns="http://www.w3.org/2000/svg" 97 viewBox="4 7 16 12" 98 class="particle" 99 > 100 <path 101 fill="currentColor" 102 d="M12 18.445a.778.778 0 0 1-.34-.078C11.39 18.235 5 15.077 5 9.889a3.889 3.889 0 0 1 6.638-2.75L12 7.5l.362-.361A3.889 3.889 0 0 1 19 9.889c0 5.17-6.387 8.344-6.66 8.478a.778.778 0 0 1-.34.078z" 103 /> 104 </svg> 105 </div> 106 </template> 107 108 <style scoped> 109 .sponsor-wrapper { 110 position: relative; 111 display: inline-flex; 112 align-items: center; 113 margin-left: 8px; 114 } 115 116 .sponsor-button { 117 display: inline-flex; 118 align-items: center; 119 gap: 5px; 120 padding: 5px 12px 5px 10px; 121 background-color: transparent; 122 color: var(--vp-c-text-1, #3c3c43); 123 font-size: 13px; 124 font-weight: 500; 125 border-radius: 20px; 126 transition: 127 background-color 0.2s ease, 128 color 0.2s ease; 129 text-decoration: none; 130 position: relative; 131 z-index: 2; 132 white-space: nowrap; 133 cursor: pointer; 134 } 135 136 .sponsor-button:hover { 137 background-color: rgba(255, 71, 114, 0.1); 138 color: #ff4772; 139 } 140 141 /* Heart icon */ 142 .heart-icon { 143 width: 15px; 144 height: 15px; 145 color: #ff4772; 146 flex-shrink: 0; 147 transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); 148 } 149 150 .sponsor-button:hover .heart-icon { 151 transform: scale(1.2); 152 } 153 154 /* Beat animation triggered on click */ 155 .heart-icon.beating { 156 animation: heartbeat 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); 157 } 158 159 @keyframes heartbeat { 160 0% { transform: scale(1); } 161 40% { transform: scale(1.4); } 162 70% { transform: scale(0.9); } 163 100% { transform: scale(1.2); } 164 } 165 166 /* Mobile: static pink pill, no hover/animation */ 167 .mobile-pill { 168 background-color: rgba(255, 71, 114, 0.12); 169 color: #ff4772; 170 } 171 172 .mobile-pill .heart-icon { 173 color: #ff4772; 174 } 175 176 .particle { 177 position: absolute; 178 top: 50%; 179 left: 18px; /* roughly where the heart icon center sits */ 180 width: 10px; 181 height: 10px; 182 margin-top: -5px; 183 margin-left: -5px; 184 color: #ff4772; 185 visibility: hidden; 186 opacity: 0; 187 pointer-events: none; 188 z-index: 1; 189 } 190 </style>