view source

JavaScript

(function(window, document, requestAnimationFrame) {
    
    'use strict';
    
    var N_MAX = 30; // 円の最大数
    var R_MAX = 50; // 半径の最大サイズ
    var R_MIN = 10; // 半径の最小サイズ
    var F = 0.75; // 反発係数
    var G = 0.5; // 重力
    var CREATE_INTERVAL = 1000; // 円を生成する間隔

    var canvas, context;
    var circles = [];
    var time;

    window.addEventListener('DOMContentLoaded', function() {
        canvas = document.getElementById('canvas');
        context = canvas.getContext('2d');

        window.addEventListener('resize', resize, false);
        resize(null);
        
        time = new Date().getTime();
        requestAnimationFrame(loop);

        canvas.addEventListener('click', function(e){
          var rect = e.target.getBoundingClientRect();
          var x = e.clientX - rect.left;
          var y = e.clientY - rect.top;
          console.log([x,y])
          guide(x,y);
        });
    }, false);
    

    function resize(e) {
        canvas.width  = window.innerWidth -40;
        canvas.height = window.innerHeight -40;
        context.fillStyle = 'red';
    }

    function guide(x,y) {
        var c = circles[circles.length-1];
        c.x = x;
        c.y = y;
    }
    function loop() {
        var w = canvas.width;
        var h = canvas.height;
        var ctx = context;
        
        ctx.save();
        ctx.fillStyle = 'white';
        ctx.fillRect(0, 0, w, h);
        ctx.restore();
        
        // 一定時間ごとに円を生成
        var now = new Date().getTime();
        if (now - time > CREATE_INTERVAL) {
            createCircle();
            time = now;
        }
        
        var i, ilen = circles.length - 1;
        var j, jlen = ilen + 1;
        var c;
        
        // 円同士の衝突を計算
        for (i = 0; i < ilen; i ++) {
            c = circles[i];
            for (j = i + 1; j < jlen; j++) {
                calcCollisionCC(c, circles[j]);
            }
        }
        
        ctx.beginPath();
        for (i = 0, ilen = jlen; i < ilen; i ++) {
            c = circles[i];
            
            c.x += c.vx;
            c.y += c.vy;
            c.vy += G;
            
            // 削除候補の円を小さくする, 半径がなくなったらリストから削除
            if (c.death) {
                c.r -= c.r * 0.3;
                if (c.r <= 0) {
                    circles.splice(i, 1);
                    ilen--;
                    i--;
                    continue;
                }
            }
            
            // 壁と地面で反射
            if (c.x - c.r < 0) {
                c.x = c.r;
                c.vx *= -1;
            }
            if (c.x + c.r > w) {
                c.x = w - c.r;
                c.vx *= -1;
            }
            if (c.y + c.r > h) {
                c.y = h - c.r;
                c.vy *= -F;
                c.vx *= 0.98;
            }
            
            ctx.moveTo(c.x, c.y);
            ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2, false);
        }
        ctx.fill();

        requestAnimationFrame(loop);
    }
    
    /**
     * 円を生成する
     */
    function createCircle() {
        var r = randunif(R_MAX, R_MIN);
        var c = new Circle(randunif(canvas.width - r, r), -r, r);
        // 画面内のランダムな座標への移動ベクトルを与える
        var vx = randunif(canvas.width, 0) - c.x;
        var vy = randunif(canvas.height, 0) - c.y;
        var vlen = Math.sqrt(vx * vx + vy * vy);
        if (vlen) vlen = 1 / vlen * randunif(20, 5);
        c.vx = vx * vlen;
        c.vy = vy * vlen;
        circles.push(c);
        
        // 最大数を超えていれば古いボールを削除
        if (circles.length > N_MAX) {
            var i = 0;
            var len = circles.length;
            while (i < len) {
                if (!circles[i].death) {
                    circles[i].death = true;
                    break;
                }
                i++;
            }
        }
    }
    
    /**
     * 円同士の衝突を計算する
     * 
     * @see http://hakuhin.jp/as/collide.html#COLLIDE_00
     */
    function calcCollisionCC(a, b) {
        var vx = a.x - b.x;
        var vy = a.y - b.y;
        var vlen = Math.sqrt(vx * vx + vy * vy);
        var r = a.r + b.r;
        
        if (vlen < r) {
            if (vlen) {
                vx /= vlen;
                vy /= vlen;
            }
            var s = (r - vlen) * 0.5;
            vx *= s;
            vy *= s;
            
            a.x += vx;
            a.y += vy;
            b.x -= vx;
            b.y -= vy;
            
            vx = b.x - a.x;
            vy = b.y - a.y;
            
            var u = vx * vx + vy * vy;
            var t = -(vx * a.vx + vy * a.vy) / u;
            var arx = a.vx + vx * t;
            var ary = a.vy + vy * t;
            
            t = -(-vy * a.vx + vx * a.vy) / u;
            var amx = a.vx - vy * t;
            var amy = a.vy + vx * t;
            
            t = -(vx * b.vx + vy * b.vy) / u;
            var brx = b.vx + vx * t;
            var bry = b.vy + vy * t;
            
            t = -(-vy * b.vx + vx * b.vy) / u;
            var bmx = b.vx - vy * t;
            var bmy = b.vy + vx * t;
            
            var avx = (a.r * amx + b.r * bmx + bmx * F * b.r - amx * F * b.r) / r;
            var bvx = -F * (bmx - amx) + avx;
            var avy = (a.r * amy + b.r * bmy + bmy * F * b.r - amy * F * b.r) / r;
            var bvy = -F * (bmy - amy) + avy;
            
            a.vx = avx + arx;
            a.vy = avy + ary;
            b.vx = bvx + brx;
            b.vy = bvy + bry;
            
            return true;
        }
        
        return false;
    }
    
    
    // Helpers
    
    function randunif(max, min) {
        return Math.random() * (max - min) + min;
    }
    
    function randint(max, min) {
        return (Math.random() * (max - min + 1) + min) | 0;
    }
    
    
    // Object

    function Circle(x, y, radius) {
        this.x = x || 0;
        this.y = y || 0;
        this.r = radius || 0; // 半径, 質量を兼ねる
        this.vx = 0;
        this.vy = 0;
        this.death = false;
    }

})(
    window,
    window.document,
    (function(){
        return  window.requestAnimationFrame       || 
                window.webkitRequestAnimationFrame || 
                window.mozRequestAnimationFrame    || 
                window.oRequestAnimationFrame      || 
                window.msRequestAnimationFrame     || 
                function (callback) {
                    window.setTimeout(callback, 1000 / 60);
                };
    })()
);

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-canvas-collision/

ABOUT

hi0a.com 「ひまアプリ」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

動く便利なものが好きなだけで技術自体に興味はないのでコードは汚いです。

途中放置や実験状態、仕様変更、API廃止等で動かないページもあります。