my website at ewancroft.uk
1<script lang="ts">
2 import { happyMacStore } from '$lib/stores';
3
4 let isVisible = $state(false);
5 let position = $state(-100);
6
7 // Watch the store for when it's triggered (24 clicks)
8 $effect(() => {
9 const state = $happyMacStore;
10 if (state.isTriggered && !isVisible) {
11 startAnimation();
12 }
13 });
14
15 function playBeep() {
16 try {
17 const audioContext = new AudioContext();
18 const now = audioContext.currentTime;
19
20 // Tributary recreation of the classic Mac startup chord
21 // This is NOT the original sound - it's an approximation using Web Audio API
22 // The original Mac beep was a major chord: F4, A4, C5
23 // Frequencies: ~349 Hz, ~440 Hz, ~523 Hz
24 const frequencies = [349, 440, 523];
25 const masterGain = audioContext.createGain();
26 masterGain.connect(audioContext.destination);
27 masterGain.gain.value = 0.15;
28
29 // Create three oscillators for the chord
30 frequencies.forEach((freq) => {
31 const oscillator = audioContext.createOscillator();
32 const gainNode = audioContext.createGain();
33
34 oscillator.type = 'sine'; // Original Mac used sine waves
35 oscillator.frequency.value = freq;
36
37 // ADSR envelope for a more authentic sound
38 gainNode.gain.setValueAtTime(0, now);
39 gainNode.gain.linearRampToValueAtTime(0.3, now + 0.02); // Attack
40 gainNode.gain.exponentialRampToValueAtTime(0.01, now + 1.0); // Decay
41
42 oscillator.connect(gainNode);
43 gainNode.connect(masterGain);
44
45 oscillator.start(now);
46 oscillator.stop(now + 1.0);
47 });
48 } catch (e) {
49 // Fail silently if audio context isn't available
50 console.log('Audio playback not available');
51 }
52 }
53
54 function startAnimation() {
55 // Play the beep first
56 playBeep();
57
58 isVisible = true;
59 position = -100;
60
61 // Animate across screen (takes about 15 seconds)
62 const duration = 15000;
63 const startTime = Date.now();
64
65 function animate() {
66 const elapsed = Date.now() - startTime;
67 const progress = Math.min(elapsed / duration, 1);
68
69 // Move from -100 to window width + 100
70 position = -100 + (window.innerWidth + 200) * progress;
71
72 if (progress < 1) {
73 requestAnimationFrame(animate);
74 } else {
75 isVisible = false;
76 // Reset the store so it can be triggered again
77 happyMacStore.reset();
78 }
79 }
80
81 requestAnimationFrame(animate);
82 }
83</script>
84
85{#if isVisible}
86 <div class="happy-mac" style="left: {position}px">
87 <!--
88 Happy Mac SVG
89 Original by NiloGlock at Italian Wikipedia
90 License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
91 Source: https://commons.wikimedia.org/wiki/File:Happy_Mac.svg
92 -->
93 <svg
94 width="60"
95 height="78"
96 viewBox="0 0 8.4710464 10.9614"
97 xmlns="http://www.w3.org/2000/svg"
98 class="mac-icon"
99 >
100 <g transform="translate(-5.3090212,-4.3002038)">
101 <g transform="matrix(0.06455006,0,0,0.06455006,7.6050574,7.0900779)">
102 <path
103 d="m -30.937651,99.78759 h 122 v 26.80449 h -122 z"
104 style="fill:#000000;fill-opacity:1;stroke-width:2.38412714"
105 />
106 <g transform="translate(-56.456402,-31.41017)">
107 <path
108 style="fill:#555555;fill-opacity:1;stroke:none;stroke-width:0.17674622"
109 d="m 33.668747,136.75006 v 4.69998 h 31.950504 v -4.69998 z m 41.740088,4.69998 V 146.15 h 11.145573 v -4.69996 z M 91.152059,146.15 v 6.29987 H 102.47075 V 146.15 Z"
110 />
111 <path
112 style="fill:#444444;fill-opacity:1;stroke:none;stroke-width:0.15800072"
113 d="m 65.619251,136.75006 v 4.69998 H 86.554408 V 146.15 h 15.916342 v 6.29987 h 20.86023 V 146.15 h -15.87449 v -4.69996 H 91.152059 v -4.69998 z"
114 />
115 <path
116 style="fill:#222222;fill-opacity:1;stroke:none;stroke-width:0.21712606"
117 d="m 91.152059,136.75006 v 4.69998 H 107.45649 V 146.15 h 15.87449 v 6.29987 h 16.03777 v -6.29987 -4.69996 -4.69998 z"
118 />
119 <path
120 style="fill:#777777;fill-opacity:1;stroke:none;stroke-width:0.20201708"
121 d="M 33.668747,141.45004 V 146.15 h 41.740088 v -4.69996 z M 75.408835,146.15 v 6.29987 H 91.152059 V 146.15 Z"
122 />
123 <path
124 d="m 33.668823,146.14999 h 41.74001 v 6.3 h -41.74001 z"
125 style="fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.23388879"
126 />
127 </g>
128 <path
129 d="M -30.969854,-37.120319 H 91.062349 V 99.787579 H -30.969854 Z"
130 style="fill:#cccccc;fill-opacity:1;stroke-width:0.26458332"
131 />
132 <path
133 d="M -15.075892,-21.040775 H 74.98512 v 67.75 h -90.061012 z"
134 style="fill:#ccccff;fill-opacity:1;stroke-width:0.26458332"
135 />
136 <path
137 transform="scale(0.26458333)"
138 d="M 102.17383,-23.402344 V 59.882812 H 83.148438 V 78.779297 H 102.17383 120 120.0508 V -23.402344 Z"
139 style="fill:#000000;fill-opacity:1;stroke-width:0.93718952"
140 />
141 <path
142 d="M -30.969856,-43.220318 H 91.062347 v 6.1 H -30.969856 Z"
143 style="fill:#000000;fill-opacity:1;stroke-width:1.13749063"
144 />
145 <path
146 d="M -15.075892,-27.140776 H 74.98512 v 6.1 h -90.061012 z"
147 style="fill:#444444;fill-opacity:1;stroke-width:0.97719014"
148 />
149 <path
150 d="m -21.040775,15.075892 h 67.75 v 6.1 h -67.75 z"
151 style="fill:#444444;fill-opacity:1;stroke-width:0.84755003"
152 transform="rotate(90)"
153 />
154 <path
155 d="m -21.040775,-81.085121 h 67.75 v 6.1 h -67.75 z"
156 style="fill:#ffffff;fill-opacity:1;stroke-width:0.84755009"
157 transform="rotate(90)"
158 />
159 <path
160 d="m -15.07589,46.709225 h 90.061013 v 6.1 H -15.07589 Z"
161 style="fill:#ffffff;fill-opacity:1;stroke-width:0.9771902"
162 />
163 <path
164 d="m 31.655506,73.81324 h 43.400002 v 5 H 31.655506 Z"
165 style="fill:#000000;fill-opacity:1;stroke-width:0.26445001"
166 />
167 <path
168 d="m 31.655506,78.81324 h 43.400005 v 6 H 31.655506 Z"
169 style="fill:#ffffff;fill-opacity:1;stroke-width:0.28969046"
170 />
171 <path
172 d="m -21.133041,73.785721 h 11.060395 v 5 h -11.060395 z"
173 style="fill:#00bb00;fill-opacity:1;stroke-width:0.13350084"
174 />
175 <path
176 d="m -21.133041,78.785721 h 11.060396 v 6 h -11.060396 z"
177 style="fill:#dd0000;fill-opacity:1;stroke-width:0.14624284"
178 />
179 <path
180 d="M 5.8799295,-6.1919641 H 10.87993 V 5.0080357 H 5.8799295 Z"
181 style="fill:#000000;fill-opacity:1;stroke-width:0.26576424"
182 />
183 <path
184 d="m 47.880306,-6.1919641 h 6.1 V 5.0080357 h -6.1 z"
185 style="fill:#000000;fill-opacity:1;stroke-width:0.29354623"
186 />
187 <path
188 d="m 10.8871,25.947487 h 5 v 6 h -5 z"
189 style="fill:#000000;fill-opacity:1;stroke-width:0.19451953"
190 />
191 <path
192 d="m 38.149635,25.944651 h 4.75 v 6.002836 h -4.75 z"
193 style="fill:#000000;fill-opacity:1;stroke-width:0.18963902"
194 />
195 <path
196 d="m 15.8871,31.947487 h 22.262533 v 5.011021 H 15.8871 Z"
197 style="fill:#000000;fill-opacity:1;stroke-width:11.12128639"
198 />
199 <path
200 d="M -37.120319,30.969854 H 99.787579 v 4.6 H -37.120319 Z"
201 style="fill:#000000;fill-opacity:1;stroke-width:1.04625833"
202 transform="rotate(90)"
203 />
204 <path
205 d="M -37.120331,-95.662346 H 99.787582 v 4.6 H -37.120331 Z"
206 style="fill:#000000;fill-opacity:1;stroke-width:1.04625833"
207 transform="rotate(90)"
208 />
209 </g>
210 </g>
211 </svg>
212 </div>
213{/if}
214
215<style>
216 .happy-mac {
217 position: fixed;
218 bottom: 0;
219 z-index: 9999;
220 pointer-events: none;
221 animation: hop 0.6s ease-in-out infinite;
222 }
223
224 .mac-icon {
225 filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.3));
226 }
227
228 @keyframes hop {
229 0%,
230 100% {
231 transform: translateY(0) rotate(0deg) scaleY(1) scaleX(1);
232 }
233 25% {
234 transform: translateY(-10px) rotate(2deg) scaleY(1.15) scaleX(0.9);
235 }
236 50% {
237 transform: translateY(-20px) rotate(5deg) scaleY(1) scaleX(1);
238 }
239 75% {
240 transform: translateY(-10px) rotate(2deg) scaleY(0.85) scaleX(1.1);
241 }
242 }
243
244 /* Add a little tilt alternation */
245 .happy-mac:hover {
246 animation: hop 0.3s ease-in-out infinite;
247 }
248</style>