PageSourceSearch

https://fortune-cookie.tokyo/assets/Lottery/js/coin-rain-animation.js?v=1.0.610

js fortune-cookie.tokyo collected 2026-10-07 17:58:14 UTC 8,786 bytes, 233 lines download raw bytes

1/**
2 * コインレインアニメーション共通モジュール
3 *
4 * 使い方:
5 *   var anim = CoinRainAnimation.start(canvasElement, options);
6 *   anim.stop();
7 *
8 * options:
9 *   mode: 'fullscreen' | 'section'
10 *     - fullscreen: canvas を window 全体に描画(ログインPopup等)
11 *     - section:    canvas を親要素の範囲に描画(新規登録ブロック等)
12 *   sectionEl: mode='section' 時の親要素(IntersectionObserver / ResizeObserver 対象)
13 *   coins:    true (デフォルト) / false  - false の場合はコイン降下を無効化し sparkle のみ表示
14 */
15var CoinRainAnimation = (function() {
16    'use strict';
17
18    var COLS = ['#FFD700', '#FFA500', '#FFE44D', '#ffffff', '#FFF3B0', '#FFCC00'];
19
20    function start(canvas, options) {
21        if (!canvas) return { stop: function(){} };
22
23        options = options || {};
24        var mode = options.mode || 'fullscreen';
25        var sectionEl = options.sectionEl || null;
26        var spawnCoins = options.coins !== false;
27
28        var cx = canvas.getContext('2d');
29        var pts = [];
30        var animId = null;
31        var stopped = false;
32        var resizeObserver = null;
33
34        // Canvas サイズ設定
35        function resize() {
36            if (mode === 'section' && sectionEl) {
37                canvas.width = sectionEl.offsetWidth;
38                canvas.height = sectionEl.offsetHeight;
39            } else {
40                canvas.width = window.innerWidth;
41                canvas.height = window.innerHeight;
42            }
43        }
44        resize();
45
46        if (mode === 'section' && sectionEl && typeof ResizeObserver !== 'undefined') {
47            resizeObserver = new ResizeObserver(function() { resize(); });
48            resizeObserver.observe(sectionEl);
49        }
50
51        // 画面内判定(section モード)。IntersectionObserver が画面外と通知している間は rAF 自体を止める。
52        // 非対応環境は常に画面内扱い(従来どおり rAF を回し、下の rect 判定で描画だけ飛ばす)
53        var inView = true;
54        var intersectionObserver = null;
55
56        // パーティクル生成(vTop / vBottom は同じフレームで 1 回だけ計算した値を受け取る)
57        function mkSparkle(vBottom) {
58            var spawnY = (mode === 'section')
59                ? vBottom + Math.random() * 20
60                : canvas.height + Math.random() * 20;
61            return {
62                type: 's',
63                x: Math.random() * canvas.width,
64                y: spawnY,
65                r: Math.random() * 2 + 0.8,
66                col: COLS[Math.floor(Math.random() * COLS.length)],
67                vx: (Math.random() - 0.5) * 0.6,
68                vy: -(Math.random() * 1.2 + 0.4),
69                life: 0
70            };
71        }
72
73        function mkCoin(vTop) {
74            var spawnY = (mode === 'section')
75                ? vTop - 14
76                : -14;
77            return {
78                type: 'c',
79                x: Math.random() * canvas.width,
80                y: spawnY,
81                r: 5 + Math.random() * 4,
82                vy: Math.random() * 1.69 + 0.78,
83                vx: (Math.random() - 0.5) * 0.4,
84                rot: Math.random() * Math.PI * 2,
85                rs: (Math.random() - 0.5) * 0.07,
86                life: 0
87            };
88        }
89
90        function star(x, y, ir, or, n) {
91            cx.beginPath();
92            for (var i = 0; i < n * 2; i++) {
93                var a = i * Math.PI / n - Math.PI / 2;
94                var r = i % 2 ? ir : or;
95                if (i === 0) cx.moveTo(x + r * Math.cos(a), y + r * Math.sin(a));
96                else cx.lineTo(x + r * Math.cos(a), y + r * Math.sin(a));
97            }
98            cx.closePath();
99        }
100
101        function canRun() {
102            return !stopped && inView && document.hidden !== true;
103        }
104
105        // 次のフレームを予約する。冪等(既に予約済みなら何もしない = rAF は常に 1 本)
106        function scheduleNext() {
107            if (animId !== null || !canRun()) return;
108            animId = requestAnimationFrame(loop);
109        }
110
111        function cancelNext() {
112            if (animId === null) return;
113            cancelAnimationFrame(animId);
114            animId = null;
115        }
116
117        // 描画ループ
118        function loop() {
119            animId = null;
120            if (stopped) return;
121
122            // ビューポート範囲(section モード用)。getBoundingClientRect は 1 フレーム 1 回だけ
123            var vTop = 0;
124            var vBottom = canvas.height;
125            if (mode === 'section' && sectionEl) {
126                var rect = sectionEl.getBoundingClientRect();
127                // section モード: 画面外なら描画スキップ(IO の余白 100px の範囲ではここで止まる)
128                if (rect.bottom < 0 || rect.top > window.innerHeight) {
129                    scheduleNext();
130                    return;
131                }
132                vTop = Math.max(-rect.top, 0);
133                vBottom = Math.min(window.innerHeight - rect.top, canvas.height);
134            }
135
136            if (mode === 'section') {
137                cx.clearRect(0, vTop, canvas.width, vBottom - vTop);
138            } else {
139                cx.clearRect(0, 0, canvas.width, canvas.height);
140            }
141
142            if (pts.length < 50 && Math.random() < 0.25) pts.push(mkSparkle(vBottom));
143            if (spawnCoins && pts.length < 50 && Math.random() < 0.08) pts.push(mkCoin(vTop));
144
145            pts = pts.filter(function(p) {
146                p.life++;
147                if (p.type === 's') {
148                    p.x += p.vx; p.y += p.vy;
149                    var boundaryTop = (mode === 'section') ? vTop 
149- 20 : -20;
150                    if (p.y < boundaryTop) return false;
151                    cx.save();
152                    cx.globalAlpha = 0.85;
153                    cx.fillStyle = p.col;
154                    star(p.x, p.y, p.r * 0.4, p.r, 4);
155                    cx.fill();
156                    cx.restore();
157                } else {
158                    p.x += p.vx; p.y += p.vy; p.rot += p.rs;
159                    if (p.y > vBottom + 20) return false;
160                    var fade = Math.min(1, p.life / 8) * 0.85;
161                    var rx = p.r * Math.abs(Math.cos(p.rot * 2.1));
162                    cx.save();
163                    cx.globalAlpha = fade;
164                    cx.translate(p.x, p.y);
165                    cx.rotate(p.rot);
166                    cx.fillStyle = '#FFD700';
167                    cx.beginPath();
168                    cx.ellipse(0, 0, Math.max(rx, 0.5), p.r, 0, 0, Math.PI * 2);
169                    cx.fill();
170                    cx.restore();
171                }
172                return true;
173            });
174
175            scheduleNext();
176        }
177
178        // 非表示タブでは止め、表示に戻ったら(画面内なら)再開する
179        function onVisibilityChange() {
180            if (document.hidden === true) {
181                cancelNext();
182            } else {
183                scheduleNext();
184            }
185        }
186        function onPageShow(e) {
187            if (e && e.persisted) scheduleNext();
188        }
189        document.addEventListener('visibilitychange', onVisibilityChange);
190        window.addEventListener('pageshow', onPageShow);
191
192        if (mode === 'section' && sectionEl && typeof IntersectionObserver === 'function') {
193            try {
194                intersectionObserver = new IntersectionObserver(function(entries) {
195                    inView = entries[entries.length - 1].isIntersecting;
196                    if (inView) {
197                        scheduleNext();
198                    } else {
199                        cancelNext();
200                    }
201                }, { rootMargin: '100px', threshold: 0 });
202                intersectionObserver.observe(sectionEl);
203            } catch (e) {
204                intersectionObserver = null;
205                inView = true;
206            }
207        }
208
209        canvas.style.display = 'block';
210        loop();
211
212        // 停止関数を返す
213        return {
214            stop: function() {
215                stopped = true;
216                cancelNext();
217                if (resizeObserver) {
218                    resizeObserver.disconnect();
219                    resizeObserver = null;
220                }
221                if (intersectionObserver) {
222                    intersectionObserver.disconnect();
223                    intersectionObserver = null;
224                }
225                document.removeEventListener('visibilitychange', onVisibilityChange);
226                window.removeEventListener('pageshow', onPageShow);
227                pts = [];
228            }
229        };
230    }
231
232    return { start: start };
233})();

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.