PageSourceSearch

https://www.kermisfm.nl/assets/scripts/ducks.js

js kermisfm.nl collected 2026-10-02 13:20:47 UTC 17,125 bytes, 506 lines download raw bytes

1(function() {
2    var container = document.getElementById('js-footer-ducks');
3    if (!container) return;
4
5    // ── Constants ──
6    var DUCK_SIZE = 36;
7    var DUCK_SVG = '<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">' +
8        '<ellipse cx="22" cy="26" rx="16" ry="10" fill="#FFD700"/>' +
9        '<ellipse cx="24" cy="24" rx="8" ry="6" fill="#FFC107"/>' +
10        '<ellipse cx="10" cy="18" rx="5" ry="7" fill="#FFD700"/>' +
11        '<circle cx="9" cy="11" r="7" fill="#FFD700"/>' +
12        '<circle cx="7" cy="10" r="1.5" fill="#2D2D2D"/>' +
13        '<path d="M2 13 L0 11.5 L2 10 Q4 11.5 2 13Z" fill="#FF8F0B"/>' +
14        '<path d="M36 20 L40 16 L38 22Z" fill="#FFC107"/>' +
15        '</svg>';
16
17    var GOLDEN_DUCK_SVG = '<svg width="40" height="36" viewBox="0 0 40 36" fill="none" xmlns="http://www.w3.org/2000/svg">' +
18        '<ellipse cx="22" cy="26" rx="16" ry="10" fill="#FFD700"/>' +
19        '<ellipse cx="24" cy="24" rx="8" ry="6" fill="#FFEA00"/>' +
20        '<ellipse cx="10" cy="18" rx="5" ry="7" fill="#FFD700"/>' +
21        '<circle cx="9" cy="11" r="7" fill="#FFD700"/>' +
22        '<circle cx="7" cy="10" r="1.5" fill="#2D2D2D"/>' +
23        '<path d="M2 13 L0 11.5 L2 10 Q4 11.5 2 13Z" fill="#FF6B00"/>' +
24        '<path d="M36 20 L40 16 L38 22Z" fill="#FFEA00"/>' +
25        '<circle cx="14" cy="22" r="2" fill="#FFF8E1" opacity="0.6"/>' +
26        '<circle cx="28" cy="28" r="1.5" fill="#FFF8E1" opacity="0.4"/>' +
27        '</svg>';
28
29    var IDLE_CONFIG = { spawnMin: 2500, spawnMax: 5000, swimMin: 12000, swimMax: 20000, maxDucks: 5 };
30
31    var LEVELS = [
32        { duration: 15000, spawnMin: 2500, spawnMax: 5000, swimMin: 14000, swimMax: 20000, maxDucks: 3 },
33        { duration: 15000, spawnMin: 2000, spawnMax: 4000, swimMin: 11000, swimMax: 17000, maxDucks: 4 },
34        { duration: 15000, spawnMin: 1500, spawnMax: 3500, swimMin: 9000,  swimMax: 14000, maxDucks: 5 },
35        { duration: 15000, spawnMin: 1200, spawnMax: 3000, swimMin: 7000,  swimMax: 12000, maxDucks: 5 },
36        { duration: 20000, spawnMin: 1000, spawnMax: 2500, swimMin: 5500,  swimMax: 10000, maxDucks: 6 },
37        { duration: 20000, spawnMin: 800,  spawnMax: 2000, swimMin: 4000,  swimMax: 8000,  maxDucks: 7 }
38    ];
39
40    var POINTS_PER_DUCK = 10;
41    var GOLDEN_POINTS = 50;
42    var GOLDEN_CHANCE = 0.08; // 8% chance per spawn
43    var GOLDEN_SPEED_MULTIPLIER = 0.5; // swims twice as fast
44    var STREAK_BONUS_THRESHOLD = 3;
45    var STREAK_BONUS_MULTIPLIER = 5;
46    var LEVEL_BONUS = 25;
47    var STREAK_TIMEOUT = 3000;
48    var GAME_OVER_IDLE_TIMEOUT = 8000;
49
50    // ── State ──
51    var state = 'IDLE'; // IDLE | PLAYING | GAME_OVER
52    var score = 0;
53    var level = 0;
54    var streak = 0;
55    var highScore = loadHighScore();
56    var activeDucks = [];
57    var spawnTimer = null;
58    var levelTimer = null;
59    var streakTimer = null;
60    var idleReturnTimer = null;
61    var gamePaused = false;
62    var pauseTime = 0;
63    var hudEl = null;
64    var gameOverEl = null;
65
66    // ── Audio (Web Audio API quack) ──
67    var audioCtx = null;
68
69    function getAudioContext() {
70        if (!audioCtx) {
71            try {
72                audioCtx = new (window.AudioContext || window.webkitAudioContext)();
73            } catch(e) {
74                return null;
75            }
76        }
77        return audioCtx;
78    }
79
80    function playQuack(isGolden) {
81        var ctx = getAudioContext();
82        if (!ctx) return;
83
84        var now = ctx.currentTime;
85        var duration = isGolden ? 0.18 : 0.15;
86        var freq = isGolden ? 900 : 680;
87
88        // Oscillator for the quack tone
89        var osc = ctx.createOscillator();
90        var gain = ctx.createGain();
91        osc.type = 'square';
92        osc.frequency.setValueAtTime(freq, now);
93        osc.frequency.exponentialRampToValueAtTime(freq * 0.6, now + duration);
94
95        gain.gain.setValueAtTime(0.12, now);
96        gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
97
98        osc.connect(gain);
99        gain.connect(ctx.destination);
100        osc.start(now);
101        osc.stop(now + duration);
102
103        // Second quack for golden duck
104        if (isGolden) {
105            var osc2 = ctx.createOscillator();
106            var gain2 = ctx.createGain();
107            osc2.type = 'square';
108            osc2.frequency.setValueAtTime(freq * 1.2, now + 0.12);
109            osc2.frequency.exponentialRampToValueAtTime(freq * 0.7, now + 0.12 + duration);
110            gain2.gain.setValueAtTime(0.1, now + 0.12);
111            gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.12 + duration);
112            osc2.connect(gain2);
113            gain2.connect(ctx.destination);
114            osc2.start(now + 0.12);
115            osc2.stop(now + 0.12 + duration);
116        }
117    }
118
119    // ── Helpers ──
120    function randomBetween(min, max) {
121        return Math.floor(Math.random() * (max - min + 1)) + min;
122    }
123
124    function getConfig() {
125        if (state !== 'PLAYING') return IDLE_CONFIG;
126        var idx = Math.min(level, LEVELS.length - 1);
127        return LEVELS[idx];
128    }
129
130    function loadHighScore() {
131        try { return parseInt(localStorage.getItem('duckGameHighScore'), 10) || 0; }
132        catch(e) { return 0; }
133    }
134
135    function saveHighScore(val) {
136        try { localStorage.setItem('duckGameHighScore', val); }
137        catch(e) {}
138    }
139
140    // ── HUD ──
141    function createHUD() {
142        hudEl = document.createElement('div');
143        hudEl.className = 'duck-game-hud';
144        hudEl.innerHTML =
145            '<span class="duck-game-hud__score">Score: 0</span>' +
146            '<span class="duck-game-hud__level">Level 1</span>' +
147            '<span class="duck-game-hud__streak"></span>';
148        container.appendChild(hudEl);
149        hudEl.offsetHeight;
150        hudEl.classList.add('duck-game-hud--visible');
151    }
152
153    function updateHUD() {
154        if (!hudEl) return;
155        hudEl.querySelector('.duck-game-hud__score').textContent = 'Score: ' + score;
156        hudEl.querySelector('.duck-game-hud__level').textContent = 'Level ' + (level + 1);
157        var streakEl = hudEl.querySelector('.duck-game-hud__streak');
158        streakEl.textContent = streak >= STREAK_BONUS_THRESHOLD ? streak + 'x streak!' : '';
159    }
160
161    function flashLevel() {
162        if (!hudEl) return;
163        var levelEl = hudEl.querySelector('.duck-game-hud__level');
164        levelEl.classList.add('duck-game-hud__level--flash');
165        setTimeout(function() {
166            levelEl.classList.remove('duck-game-hud__level--flash');
167        }, 400);
168    }
169
170    function removeHUD() {
171        if (hudEl && hudEl.parentNode) {
172            hudEl.parentNode.removeChild(hudEl);
173            hudEl = null;
174        }
175    }
176
177    // ── Game Over Overlay ──
178    function showGameOver() {
179        var isNewHighScore = score > highScore;
180        if (isNewHighScore) {
181            highScore = score;
182            saveHighScore(highScore);
183        }
184
185        gameOverEl = document.createElement('div');
186        gameOverEl.className = 'duck-game-over';
187        gameOverEl.innerHTML =
188            '<span>Game Over!</span>' +
189            '<span>
189Score: ' + score + '</span>' +
190            '<span>' + (isNewHighScore ? 'Nieuw record!' : 'Record: ' + highScore) + '</span>' +
191            '<button class="duck-game-over__btn">Opnieuw!</button>';
192        container.appendChild(gameOverEl);
193
194        gameOverEl.querySelector('.duck-game-over__btn').addEventListener('click', function(e) {
195            e.stopPropagation();
196            removeGameOver();
197            startGame(0);
198        });
199
200        idleReturnTimer = setTimeout(function() {
201            returnToIdle();
202        }, GAME_OVER_IDLE_TIMEOUT);
203    }
204
205    function removeGameOver() {
206        clearTimeout(idleReturnTimer);
207        idleReturnTimer = null;
208        if (gameOverEl && gameOverEl.parentNode) {
209            gameOverEl.parentNode.removeChild(gameOverEl);
210            gameOverEl = null;
211        }
212    }
213
214    // ── Points Popup ──
215    function showPointsPopup(x, y, points, isGolden) {
216        var popup = document.createElement('div');
217        popup.className = 'duck-points-popup';
218        popup.textContent = '+' + points;
219        if (isGolden) {
220            popup.style.color = '#FFD700';
221            popup.style.fontSize = '18px';
222        }
223        popup.style.left = x + 'px';
224        popup.style.bottom = (container.offsetHeight - y) + 'px';
225        container.appendChild(popup);
226        setTimeout(function() {
227            if (popup.parentNode) popup.parentNode.removeChild(popup);
228        }, 850);
229    }
230
231    // ── Bubbles ──
232    function spawnBubbles(x, bottomY) {
233        var count = randomBetween(3, 6);
234        for (var i = 0; i < count; i++) {
235            var bubble = document.createElement('div');
236            bubble.className = 'duck-bubble';
237            bubble.style.left = (x + randomBetween(-12, 12)) + 'px';
238            bubble.style.bottom = (bottomY + randomBetween(0, 8)) + 'px';
239            bubble.style.width = bubble.style.height = randomBetween(4, 8) + 'px';
240            bubble.style.animationDelay = (i * 0.08) + 's';
241            container.appendChild(bubble);
242            (function(b) {
243                setTimeout(function() {
244                    if (b.parentNode) b.parentNode.removeChild(b);
245                }, 800);
246            })(bubble);
247        }
248    }
249
250    // ── Duck Spawning ──
251    function spawnDuck() {
252        var config = getConfig();
253        var gameActiveDucks = activeDucks.filter(function(d) { return !d.caught; });
254        if (gameActiveDucks.length >= config.maxDucks) return;
255
256        var isGolden = state === 'PLAYING' && Math.random() < GOLDEN_CHANCE;
257
258        var duck = document.createElement('div');
259        duck.className = 'footer-duck';
260        if (isGolden) duck.classList.add('footer-duck--golden');
261        duck.innerHTML = isGolden ? GOLDEN_DUCK_SVG : DUCK_SVG;
262
263        var swimDuration = randomBetween(config.swimMin, config.swimMax);
264        if (isGolden) swimDuration = Math.round(swimDuration * GOLDEN_SPEED_MULTIPLIER);
265
266        var containerWidth = container.offsetWidth;
267        var startX = -DUCK_SIZE;
268        var endX = containerWidth + DUCK_SIZE;
269
270        duck.style.left = startX + 'px';
271        duck.style.animationDelay = randomBetween(0, 2000) + 'ms';
272
273        container.appendChild(duck);
274
275        var duckData = {
276            el: duck,
277            startTime: null,
278            swimDuration: swimDuration,
279            startX: startX,
280            endX: endX,
281            caught: false,
282            golden: isGolden,
283            animId: null
284        };
285        activeDucks.push(duckData);
286
287        function animate(timestamp) {
288            if (gamePaused) {
289                duckData.animId = requestAnimationFrame(animate);
290                return;
291            }
292            if (!duckData.startTime) duckData.startTime = timestamp;
293            var progress = (timestamp - duckData.startTime) / swimDuration;
294
295            if (duckData.caught || !duck.parentNode) {
296                removeDuckData(duckData);
297                return;
298            }
299
300            if (progress >= 1) {
301                duck.parentNode.removeChild(duck);
302                removeDuckData(duckData);
303                if (state === 'PLAYING') {
304                    endGame();
305                }
306                return;
307            }
308
309            duck.style.left = (startX + (endX - startX) * progress) + 'px';
310            duckData.animId = requestAnimationFrame(animate);
311        }
312
313        duckData.animId = requestAnimationFrame(animate);
314
315        duck.addEventListener('click', function(e) {
316            e.preventDefault();
317            e.stopPropagation();
318            if (duckData.caught) return;
319            duckData.caught = true;
320            duck.classList.add('footer-duck--caught');
321
322            playQuack(duckData.golden);
323
324            if (state === 'IDLE') {
325                startGame(duckData.golden ? GOLDEN_POINTS : POINTS_PER_DUCK);
326            } else if (state === 'PLAYING') {
327                catchDuck(duckData, e);
328            }
329
330            playCatchAnimation(duck, duckData);
331        });
332    }
333
334    function catchDuck(duckData, event) {
335        streak++;
336        clearTimeout(streakTimer);
337        streakTimer = setTimeout(function() {
338            streak = 0;
339            updateHUD();
340        }, STREAK_TIMEOUT);
341
342        var points = duckData.golden ? GOLDEN_POINTS : POINTS_PER_DUCK;
343        if (streak >= STREAK_BONUS_THRESHOLD) {
344            points += streak * STREAK_BONUS_MULTIPLIER;
345        }
346        score += points;
347        updateHUD();
348
349        var rect = duckData.el.getBoundingClientRect();
350        var containerRect = container.getBoundingClientRect();
351        showPointsPopup(
352            rect.left - containerRect.left + DUCK_SIZE / 2,
353            rect.top - containerRect.top,
354            points,
355            duckData.golden
356        );
357    }
358
359    function playCatchAnimation(duck, duckData) {
360        var catchStart = null;
361        var startBottom = parseFloat(getComputedStyle(duck).bottom) || -14;
362
363        function catchAnimate(timestamp) {
364            if (!catchStart) catchStart = timestamp;
365            var elapsed = timestamp - catchStart;
366            var duration = 900;
367            var progress = Math.min(elapsed / duration, 1);
368
369            var y;
370            if (progress < 0.35) {
371                var p = progress / 0.35;
372                y = startBottom + (28 * p);
373            } else {
374                var p = (progress - 0.35) / 0.65;
375                y = (startBottom + 28) - (70 * p * p);
376            }
377
378            duck.style.bottom = y + 'px';
379
380            // Spawn bubbles when duck goes below water line
381            if (progress > 0.6 && progress < 0.65) {
382                var duckLeft = parseFloat(duck.style.left) || 0;
383                spawnBubbles(duckLeft + DUCK_SIZE / 2, 8);
384            }
385
386            if (progress < 1) {
387                requestAnimationFrame(catchAnimate);
388            } else {
389                if (duck.parentNode) duck.parentNode.removeChild(duck);
390                removeDuckData(duckData);
391            }
392        }
393
394        requestAnimationFrame(catchAnimate);
395    }
396
397    function removeDuckData(duckData) {
398        var idx = activeDucks.indexOf(duckData);
399        if (idx !== -1) activeDucks.splice(idx, 1);
400    }
401
402    function removeAllDucks() {
403        for (var i = activeDucks.length - 1; i >= 0; i--) {
404            var d = activeDucks[i];
405            if (d.animId) cancelAnimationFrame(d.animId);
406            if (d.el.parentNode) d.el.parentNode.removeChild(d.el);
407        }
408        activeDucks = [];
409    }
410
411    function scheduleSpawn() {
412        clearTimeout(spawnTimer);
413        var config = getConfig();
414        var delay = randomBetween(config.spawnMin, config.spawnMax);
415        spawnTimer = setTimeout(function() {
416            if (state !== 'GAME_OVER') {
417                spawnDuck();
418                scheduleSpawn();
419            }
420        }, delay);
421    }
422
423    // ── Game Flow ──
424    function startGame(initialScore) {
425        state = 'PLAYING';
426        score = initialScore;
427        level = 0;
428        streak = 0;
429
430        removeGameOver();
431        createHUD();
432        updateHUD();
433        scheduleSpawn();
434        scheduleLevelUp();
435    }
436
437    function scheduleLevelUp() {
438        clearTimeout(levelTimer);
439        var config = getConfig();
440        levelTimer = setTimeout(function() {
441            if (state !== 'PLAYING') return;
442            level++;
443            score += LEVEL_BONUS;
444            updateHUD();
445            flashLevel();
446            scheduleSpawn();
447            scheduleLevelUp();
448        }, config.duration);
449    }
450
451    function endGame() {
452        state = 'GAME_OVER';
453
454        clearTimeout(spawnTimer);
455        clearTimeout(levelTimer);
456        clearTimeout(streakTimer);
457        spawnTimer = null;
458        levelTimer = null;
459        streakTimer = null;
460
461        removeAllDucks();
462        removeHUD();
463        showGameOver();
464    }
465
466    function returnToIdle() {
467        state = 'IDLE';
468        removeGameOver();
469        removeAllDucks();
470        removeHUD();
471
472        setTimeout(function() {
473            spawnDuck();
474            scheduleSpawn();
475        }, 1000);
476    }
477
478    // ── Visibility change (pause/resume) ──
479    document.addEventListener('visibilitychange', function() {
480        if (state !== 'PLAYING') return;
481
482        if (document.hidden) {
483            gamePaused = true;
484            pauseTime = performance.now();
485            clearTimeout(spawnTimer);
486            clearTimeout(levelTimer);
487            clearTimeout(streakTimer);
488        } else {
489            gamePaused = false;
490            var pausedFor = performance.now() - pauseTime;
491            for (var i = 0; i < activeDucks.length; i++) {
492                if (activeDucks[i].startTime) {
493                    activeDucks[i].startTime += pausedFor;
494                }
495            }
496            scheduleSpawn();
497            scheduleLevelUp();
498        }
499    });
500
501    // ── Init (IDLE) ──
502    setTimeout(function() {
503        spawnDuck();
504        scheduleSpawn();
505    }, 2000);
506})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.