SOURCE

(function(env) {

    const PI2 = Math.PI * 2
        , PI_180 = Math.PI / 180
        , imgHash = {}
        , identity = (d) => d
        , padding = 1.5
        , clusterPadding = 15
        ;

    class Utils {

        static preloadImage(url, obj) {
            let img = imgHash[url];
            if (img) {
                obj && (obj.img = img);
                return;
            }
            img = new Image();
            img.onload = () => {
                obj.img = img;
            }
            img.src = url
        }

        static dist(a, b) {
            return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2))
        }
        
        static generateSprite(w, h, c) {

            let tempFileCanvas = document.createElement("canvas");

            tempFileCanvas.width = w;
            tempFileCanvas.height = h;

            let ctx = tempFileCanvas.getContext("2d");
            let g = ctx.createRadialGradient( w/2, h/2, 0, w/2, h/2, w/2 );

            g.addColorStop(0, 'hsla(' + c + ', 75%, 45%, 1)');
            g.addColorStop(0.6, 'hsla(' + c + ', 95%, 30%,' + .1 + ')');
            g.addColorStop(1, 'hsla(226, 55%, 50%, 0)');

            ctx.fillStyle = g;
            ctx.fillRect( 0, 0, w, h);

            return tempFileCanvas;
            
        }

    }

    class Canvas {

        constructor(width, height) {
            this.canvas = document.createElement("canvas");
            this.canvas.width = width;
            this.canvas.height = height;
            this.ctx = this.canvas.getContext("2d");
            document.body.appendChild(this.canvas)
        }

        get width() {
            return this.canvas.width
        }

        get height() {
            return this.canvas.height
        }

        resize(width, height) {
            if (this.width != width)
                this.canvas.width = +width

            if (this.height != height)
                this.canvas.height = +height
        }

    }

    class Point {

        constructor({
            x,
            y,
            color,
            radius
        }) {
            this.x = x;
            this.y = y;
            this.color = color;
            this.radius = radius
        }

    }

    class Person extends Point {

        constructor({
            x,
            y,
            radius,
            name,
            color,
            img
        }) {
            super({
                x,
                y,
                color,
                radius
            });
            this.image = img;
            this.name = name
        }

        get image() {
            return this.img
        }

        set image(value) {
            value instanceof Image
                && (this.img = value) || Utils.preloadImage(value, this)
        }

        render(ctx) {
            let r = this.radius,
                r2 = r * 2;
            ctx.fillStyle = this.color;
            ctx.strokeStyle = this.color;
            ctx.beginPath();
            ctx.arc(this.x, this.y, r, 0, PI2);
            ctx.closePath();
            ctx.stroke();
            ctx.fill();
            if (this.img) {
                ctx.save();
                ctx.clip();
                ctx.drawImage(this.img, this.x - r, this.y - r, r2, r2);
                ctx.restore()
            }
        }
    }

    var hashGrad = {};
    class Particle extends Point {

        constructor({
            x,
            y,
            color,
            radius,
            speed = 1,
            parent,
            ...arg
        }) {
            super({
                x,
                y,
                color,
                radius
            });
            this.parent = parent;
            this.speed = speed;
            this.prop = arg
        }

        render(ctx) {            
            let g = hashGrad[this.color];
            if (!g) {
                /* g = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius/2);
                
                g.addColorStop(0, 'hsla(' + this.color + ', 75%, 45%, 1)');
                g.addColorStop(0.6, 'hsla(' + this.color + ', 95%, 30%,' + .1 + ')');
                g.addColorStop(1, 'hsla(226, 55%, 50%, 0)'); */
                
                hashGrad[this.color] = g = Utils.generateSprite(200, 200, this.color);
            }
            
            // ctx.fillStyle = g;
            // ctx.beginPath();
            // ctx.arc(this.x, this.y, this.radius, PI2, false);
            // ctx.fill();
            let r = this.radius;
            ctx.drawImage(g, this.x - r, this.y - r, r, r);
        }
    }

    class Force {

        constructor(width, height, nodes) {
            let collide5, cluster;
            this.force = d3.layout.force()
                .nodes(nodes)
                .size([width, height])
                //.gravity(.1)
                .charge((d) => -d.radius)
                .chargeDistance(200)
                .on("tick", (e) => {
                    cluster = cluster || this.cluster(.05);
                    let ns = this.nodes;
                    if (ns) {
                        ns.forEach(cluster);
                    }
                    this.force.resume();
                })
        }

        size(width, height) {
            this.force.size([width, height])
        }

        get nodes() {
            return this.force.nodes()
        }
        set nodes(value) {
            this.force.nodes(value)
        }
        
        start() {
            this.force.start()
            return this;
        }
        
        stop() {
            this.force.stop()
            return this;
        }

        clustering(fn) {
            if (!arguments.length)
                return this.clustering.do || identity;
            this.clustering.do = fn;
            return this;
        }

        // Move d to be adjacent to the cluster node.
        cluster(alpha) {
            return (d) => {
                let cluster = this.clustering()(d);
                if (!cluster || cluster === d)
                    return;
                
                // d.x += (cluster.x - d.x) * alpha;
                // d.y += (cluster.y - d.y) * alpha;
                
                // the same
                let x = d.x - cluster.x,
                    y = d.y - cluster.y,
                    l = Math.sqrt(x * x + y * y),
                    r = (d.radius + cluster.radius) * alpha;
                if (l != r) {
                    l = (l - r) / l * alpha;
                    d.x -= x *= l;
                    d.y -= y *= l;
                }
            };
        }
    }

    let w = env.innerWidth;
    let h = env.innerHeight;

    let first = new Person({
        x: w * .5,
        y: h * .5,
        radius: 0,        
        color: "rgba(255, 255, 255, 0)",
        //img: "https://vk.com/images/stickers/101/128.png"
    });
    let second = new Person({
        x: w * .5,
        y: h * .5,
        radius: 0,        
        color: "rgba(255, 255, 255, 0)",
        //img: "https://vk.com/images/stickers/101/128.png"
    });
    

    let ps = [], ps1 = [];
    let c = new Canvas(w, h);
    let force = (new Force(w, h, ps))
        .clustering((d) => {
            return d.parent;
        })
        .start()
        ;
    
    let force1 = (new Force(w, h, ps1))
        .clustering((d) => {
            return d.parent;
        })
        .start()
        ;

    let t = 0,k = 1;
    (function anim() {
        requestAnimationFrame(anim);

        w = env.innerWidth;
        h = env.innerHeight;

        c.resize(w, h);

        let ctx = c.ctx;

        ctx.save();

        ctx.globalCompositeOperation = "destination-out";
        ctx.fillStyle = "rgba(0, 0, 0, .5)";
        ctx.fillRect(0, 0, w, h);

        ctx.globalCompositeOperation = 'lighter';

        let l = ps.length;
        while (l--) {
            ps[l].render(ctx);
        }
        
        l = ps1.length;
        while (l--) {
            ps1[l].render(ctx);
        }
        
        first.render(ctx);
        k = (t % 200 > 190) ? -1 : 1;
        first.y += k * Math.cos(t * PI_180) * 6;
        second.x += k * Math.cos(t++ * PI_180) * 4;
        ctx.restore();
        
    })()
    
    let tns = 0;
    d3.timer(function() {
        let ns = ps;
        let p = first;
        
        if (tns % 2) {
            //ns = ps1;
            p = second;
        }
        
        ns.push(new Particle({
            x: p.x,
            y: p.y,
            color: /* 150 - Math.random() * 150 */ tns % 2 ? 185 : 100,
            radius: 20,
            parent: p,
            speed: Math.random() * 4
        }));
        
        force.start();
        force1.start();
        return tns++ > 400;
    })

    env.addEventListener('resize', function() {
        w = env.innerWidth;
        h = env.innerHeight;

        first.x = w * .5;
        first.y = h * .5;
        
        second.x = w * .5;
        second.y = h * .5;
    }, false);

})(window)
body, html
  height 100%
  width 100%
  overflow hidden
  background #001B36
  background radial-gradient(ellipse at center, #001B36 0%, #000 100%)
console 命令行工具 X clear

                    
>
console