-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
399 lines (376 loc) · 17.1 KB
/
script.js
File metadata and controls
399 lines (376 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
// script.js: Interactions for the Gummy Bearverse
function randomBetween(min, max){ return Math.random() * (max - min) + min; }
// Burst effect
function makeBurst(x, y){
const root = document.getElementById('burst-root');
const count = Math.floor(randomBetween(8, 16));
for(let i = 0; i < count; i++){
const el = document.createElement('div');
el.className = 'burst';
const size = randomBetween(18, 80);
el.style.width = `${size}px`;
el.style.height = `${size*0.9}px`;
const gummyColors = ['#ff8fa1','#5bff7d','#ffd45c','#6bc3ff','#c87bff'];
el.style.background = gummyColors[Math.floor(Math.random() * gummyColors.length)];
el.style.left = `${x - size/2}px`;
el.style.top = `${y - size/2}px`;
el.style.opacity = '0.95';
const dur = randomBetween(450, 900);
el.style.animation = `scatterBurst ${dur}ms ease-out forwards`;
// add slightly random rotation and border radius
el.style.transform = `rotate(${randomBetween(-45,45)}deg) scale(0.2);`;
root.appendChild(el);
// cleanup after animation
setTimeout(()=>{ root.removeChild(el); }, dur + 50);
}
}
// wired to the button on the index page
document.addEventListener('DOMContentLoaded', ()=>{
// Setup video from data attribute (so we can change in HTML easily)
const videoWin = document.querySelector('.video-window');
if(videoWin){
const vidId = videoWin.dataset.videoId || 'EDJp3KFN_k8';
const iframe = document.getElementById('ytplayer');
const fallback = document.querySelector('.video-fallback');
const ytlink = document.getElementById('yt-link');
if(ytlink) ytlink.href = `https://www.youtube.com/watch?v=${vidId}`;
if(iframe){
// Build a proper embed URL with some recommended params
const params = new URLSearchParams({ rel: '0', enablejsapi: '1', autoplay: '0' });
// When served over http/https, include origin param to improve compatibility with the API
try{ if(location.protocol && location.protocol.startsWith('http')) params.set('origin', location.origin); }catch(e){/* ignore in file:// */}
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
// Lazy-load strategy for maximum cross-browser compat:
// 1) Feature-detect support for `loading` attribute; if supported, set `loading="lazy"` and set src.
// 2) If the browser doesn't support iframe[loading], fall back to IntersectionObserver.
// 3) Last resort: no lazy-loading support — load immediately.
const embedUrl = `https://www.youtube.com/embed/${vidId}?${params.toString()}`;
if('loading' in HTMLIFrameElement.prototype){
// Browser supports iframe.lazy loading — set the attribute and the src
try{ iframe.setAttribute('loading', 'lazy'); }catch(e){}
iframe.src = embedUrl;
} else if('IntersectionObserver' in window){
// Observe the iframe's visibility and set src only once it becomes visible
const io = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting){
iframe.src = embedUrl;
observer.unobserve(entry.target);
}
});
}, {root: null, threshold: 0.01});
io.observe(iframe);
} else {
// Last resort: no lazy-loading support, load immediately
iframe.src = embedUrl;
}
// Provide a fallback link if the embed fails to load (domains that disallow embedding will not navigate successfully)
let loadTimer = setTimeout(()=>{
if(fallback) fallback.style.display = 'block';
}, 4500);
iframe.addEventListener('load', ()=>{
clearTimeout(loadTimer);
if(fallback) fallback.style.display = 'none';
});
iframe.addEventListener('error', ()=>{
if(fallback) fallback.style.display = 'block';
});
}
}
const btn = document.getElementById('burst-btn');
if(btn){
btn.addEventListener('click', (e)=>{
// make burst centered on the button
const rect = e.currentTarget.getBoundingClientRect();
const cx = rect.left + rect.width/2;
const cy = rect.top + rect.height/2;
makeBurst(cx, cy);
// explode nearest falling gummy immediately (or spawn one if none)
explodeNearestToPoint(cx, cy);
});
// keyboard activation
btn.addEventListener('keypress', (e)=>{
if(e.key === 'Enter' || e.key === ' '){
e.preventDefault();
const rect = e.currentTarget.getBoundingClientRect();
makeBurst(rect.left + rect.width/2, rect.top + rect.height/2);
explodeNearestToPoint(rect.left + rect.width/2, rect.top + rect.height/2);
}
});
}
// make whole-page gummy bursts when clicking on body — just for fun
document.body.addEventListener('click', (e)=>{
if(e.target.id === 'burst-btn') return; // button handled above
makeBurst(e.clientX, e.clientY);
});
// Keyboard-friendly: press 'g' to make a burst
document.addEventListener('keydown', (e)=>{
if(e.key === 'g' || e.key === 'G'){
// center of viewport
makeBurst(window.innerWidth/2, window.innerHeight/2);
}
});
});
// --- Exploding gummy helpers ---
function explodeAt(x, y, count){
const root = document.getElementById('gummy-fall-root') || document.body;
if(!root) return;
const pieceCount = count || Math.floor(randomBetween(6, 14));
for(let i = 0; i < pieceCount; i++){
const el = document.createElement('div');
el.className = 'mini-gummy';
const img = document.createElement('img');
const imgSrc = gummyAssets[Math.floor(Math.random()*gummyAssets.length)];
img.src = imgSrc;
img.style.width = '100%';
img.style.height = 'auto';
img.alt = '';
img.draggable = false;
el.appendChild(img);
// place at center
el.style.left = `${Math.round(x)}px`;
el.style.top = `${Math.round(y)}px`;
// start centered
el.style.transform = 'translate(-50%, -50%)';
let size = Math.floor(randomBetween(10, 28));
// clamp size range for mini pieces to avoid unexpectedly large images
size = Math.max(6, Math.min(size, 28));
el.style.width = `${size}px`;
el.style.maxWidth = `${size}px`;
el.style.boxSizing = 'border-box';
el.style.overflow = 'hidden';
const angle = randomBetween(0, Math.PI*2);
const speed = randomBetween(60, 220);
const tx = Math.round(Math.cos(angle) * speed) + 'px';
const ty = Math.round(Math.sin(angle) * speed) + 'px';
const rot = Math.round(randomBetween(-720, 720)) + 'deg';
el.style.setProperty('--tx', tx);
el.style.setProperty('--ty', ty);
el.style.setProperty('--rot', rot);
const dur = Math.floor(randomBetween(600, 1400));
el.style.animation = `miniBurst ${dur}ms cubic-bezier(.15,.6,.24,1) forwards`;
// append only after image load for stable sizing
if(img.complete){
root.appendChild(el);
} else {
img.addEventListener('load', ()=>{ root.appendChild(el); }, { once: true });
}
el.addEventListener('animationend', ()=>{ if(el.parentElement) el.parentElement.removeChild(el); });
}
}
function spawnExplodingGummy(){
const root = document.getElementById('gummy-fall-root') || document.body;
const el = document.createElement('div');
el.className = 'falling-gummy exploding';
const img = document.createElement('img');
const src = gummyAssets[Math.floor(Math.random()*gummyAssets.length)];
img.src = src;
img.alt = '';
img.draggable = false;
el.appendChild(img);
const size = Math.floor(randomBetween(40, 84));
el.style.width = `${size}px`;
el.style.position = 'fixed';
const percent = Math.random() * 100;
const leftPx = Math.round((percent / 100) * (window.innerWidth || document.documentElement.clientWidth) - (size / 2));
el.style.left = `${leftPx}px`;
// start above viewport
const startOffset = Math.round(size * randomBetween(1.2, 1.9) + randomBetween(20, 90));
el.style.top = `-${startOffset}px`;
el.style.willChange = 'transform, opacity';
el.style.backfaceVisibility = 'hidden';
// set initial transform so it starts off-screen
const drift = randomBetween(-40, 40);
el.style.transform = `translate3d(${drift}px, -240vh, 0)`;
const duration = Math.floor(randomBetween(3800, 9200));
const delay = 0; // can randomize
el.style.animation = `gummyFall ${duration}ms linear ${delay}ms forwards`;
root.appendChild(el);
// schedule explosion part-way through the fall
const explodeAtPercent = randomBetween(0.35, 0.75);
const explodeMs = duration * explodeAtPercent + delay;
setTimeout(()=>{
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
// Do a small visual burst (use existing burst for color) and add image pieces
makeBurst(cx, cy);
explodeAt(cx, cy, Math.floor(randomBetween(8, 20)));
if(el.parentElement) el.parentElement.removeChild(el);
}, explodeMs);
}
// Explode the nearest falling gummy to a point (x,y). If none found, spawn a new exploding gummy.
function explodeNearestToPoint(x, y){
const all = Array.from(document.querySelectorAll('.falling-gummy'));
if(!all.length){
spawnExplodingGummy();
return;
}
let nearest = null;
let bestDist = Infinity;
for(const el of all){
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width/2;
const cy = rect.top + rect.height/2;
const dx = cx - x;
const dy = cy - y;
const dist = Math.hypot(dx, dy);
if(dist < bestDist){ bestDist = dist; nearest = el; }
}
if(!nearest){ spawnExplodingGummy(); return; }
// Explode at the nearest element center immediately
const r = nearest.getBoundingClientRect();
const cx = r.left + r.width/2;
const cy = r.top + r.height/2;
makeBurst(cx, cy);
explodeAt(cx, cy, Math.floor(randomBetween(8, 18)));
// Remove the falling gummy and decrement the active counter
if(nearest.parentElement) nearest.parentElement.removeChild(nearest);
window._fallingActive = Math.max(0, (window._fallingActive || 1) - 1);
}
// --- Animated falling gummy background -- spawn from assets/gummy_bear_images (excluding gummy_bears.png) ---
const gummyAssets = [
'assets/gummy_bear_images/gummy_bear_yellow.png',
'assets/gummy_bear_images/gummy_bear_red.png',
'assets/gummy_bear_images/gummy_bear_purple.png',
'assets/gummy_bear_images/gummy_bear_pink.png',
'assets/gummy_bear_images/gummy_bear_orange.png',
'assets/gummy_bear_images/gummy_bear_light_green.png',
'assets/gummy_bear_images/gummy_bear_light_blue.png',
'assets/gummy_bear_images/gummy_bear_green.png',
'assets/gummy_bear_images/gummy_bear_gold.png',
'assets/gummy_bear_images/gummy_bear_dark_purple.png',
'assets/gummy_bear_images/gummy_bear_cola.png',
'assets/gummy_bear_images/gummy_bears_blue.png'
];
function setupFallingBackground(){
const rootId = 'gummy-fall-root';
let root = document.getElementById(rootId);
if(!root){
root = document.createElement('div');
root.id = rootId;
root.setAttribute('aria-hidden', 'true');
// place the falling gummies on top of everything; we use a high z-index
root.style.zIndex = '9999';
// ensure the root covers the viewport
root.style.left = '0'; root.style.top = '0'; root.style.width = '100%'; root.style.height = '100%';
document.body.appendChild(root);
}
const maxActive = 60; // safety cap
// global active count so we can decrement when removing elements externally (e.g., exploding nearest gummy)
window._fallingActive = window._fallingActive || 0;
const spawnInterval = window.matchMedia('(max-width:800px)').matches ? 600 : 320;
// Preload images to reduce flicker when adding elements
const preloadImages = [];
for(const s of gummyAssets){
const im = new Image();
im.src = s;
preloadImages.push(im);
}
function spawnOne(){
if(window._fallingActive >= maxActive) return;
const el = document.createElement('div');
el.className = 'falling-gummy';
const img = document.createElement('img');
// choose random asset (do not use the merged sprite gummy_bears.png)
const src = gummyAssets[Math.floor(Math.random()*gummyAssets.length)];
img.src = src;
img.alt = '';
img.draggable = false;
// ensure stable layout by sizing the image before load and appending only after load (prevents flashing)
img.style.width = '100%';
img.style.height = 'auto';
if(img.complete){
el.appendChild(img);
// start animation
} else {
img.addEventListener('load', ()=>{
el.appendChild(img);
// some browsers place element before styles apply; ensure animation kicks in by forcing a reflow
// (not necessary but increases reliability)
void el.offsetHeight;
}, { once: true });
}
// random left offset (allow off-screen start)
// Position relative to the viewport - choose a random percentage across the viewport
const percent = Math.random() * 100;
el.style.position = 'fixed';
// center element based on its computed size so it falls evenly across the top
// make sure it starts at the top (translate3d will move it above the viewport)
el.style.top = `0px`;
// random size — compute before left so we can center
const size = window.matchMedia('(max-width:800px)').matches ? randomBetween(24,56) : randomBetween(32,80);
el.style.width = `${size}px`;
// offset the starting top so gummies begin a little higher above the viewport when they spawn
// Start offset scales with size so larger gummies start further off-screen
const startOffset = Math.round(size * randomBetween(1.1, 1.6) + randomBetween(10, 60));
el.style.top = `-${startOffset}px`;
// compute left in pixels so we can center the element on the chosen percent
const leftPx = Math.round((percent / 100) * (window.innerWidth || document.documentElement.clientWidth) - (size / 2));
el.style.left = `${leftPx}px`;
// Force hardware acceleration for the element (helps reduce flashing)
el.style.willChange = 'transform, opacity';
el.style.backfaceVisibility = 'hidden';
// random rotation direction and animation duration
const duration = Math.floor(randomBetween(5500, 16000));
const delay = Math.floor(randomBetween(0, 1200));
// Do not set animation yet; we'll apply it after the element is appended to ensure it starts correctly
// speedier fade/rotate variation for each
const localRot = randomBetween(-720,720);
// add slight side drift using a keyframes manually via CSS variable
const drift = randomBetween(-60, 60);
el.style.setProperty('--drift', `${drift}px`);
// set initial transform to match keyframe starting position so the element isn't visible at the top briefly
el.style.transform = `translate3d(${drift}px, -240vh, 0) rotate(0deg)`;
// Append element only after the image has loaded to avoid flashes of empty elements
const attachAndStart = ()=>{
window._fallingActive++;
// Append to DOM to start the animation
root.appendChild(el);
// Apply animation immediately — assigning the starting transform prevents a visual "jump" in most browsers.
el.style.animation = `gummyFall ${duration}ms linear ${delay}ms forwards`;
// When animation ends, don't immediately remove; instead wait until element is fully offscreen
el.addEventListener('animationend', ()=>{ removeWhenOffscreen(el, undefined, fallbackRemover); }, { once: true });
// Fallback: if the animation/RAF pauses (e.g., tab hidden), remove after duration + delay + buffer
const fallbackRemover = setTimeout(()=>{
if(el.parentElement) {
el.parentElement.removeChild(el);
window._fallingActive = Math.max(0, window._fallingActive - 1);
}
}, duration + delay + 3000);
};
// Remove element only after it's fully outside of the viewport
function isFullyOffscreen(rect){
return (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth);
}
function removeWhenOffscreen(element, timeout = 25000, fallbackRemoverId){
const start = performance.now();
function check(){
if(!element.parentElement){
// already removed
return;
}
const rect = element.getBoundingClientRect();
if(isFullyOffscreen(rect) || (performance.now() - start) > timeout){
if(element.parentElement) element.parentElement.removeChild(element);
window._fallingActive = Math.max(0, window._fallingActive - 1);
if(fallbackRemoverId) clearTimeout(fallbackRemoverId);
return;
}
requestAnimationFrame(check);
}
requestAnimationFrame(check);
}
// (Note: animationend listener already attached inside attachAndStart.)
if(img.complete){
// already loaded — append now
attachAndStart();
} else {
img.addEventListener('load', ()=>{ attachAndStart(); }, { once: true });
}
}
// spawn loop — always run spawn timer so gummies fall regardless of tab focus. Browsers may throttle timers in background.
let spawnTimer = setInterval(spawnOne, spawnInterval);
}
// Run setup on DOMContentLoaded
document.addEventListener('DOMContentLoaded', ()=>{ setupFallingBackground(); });