Skip to content

Instantly share code, notes, and snippets.

@vijayrajanna
Created August 1, 2015 23:00

Revisions

  1. vijayrajanna created this gist Aug 1, 2015.
    9 changes: 9 additions & 0 deletions Gravity Points.markdown
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,9 @@
    Gravity Points
    --------------
    http://jsdo.it/akm2/5SPA

    Forked from [AKM2](http://codepen.io/akm2/)'s Pen [Gravity Points](http://codepen.io/akm2/pen/rHIsa/).

    A [Pen](http://codepen.io/vijayrajanna/pen/RPEZEJ) by [Vijay Rajanna](http://codepen.io/vijayrajanna) on [CodePen](http://codepen.io/).

    [License](http://codepen.io/vijayrajanna/pen/RPEZEJ/license).
    2 changes: 2 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    <canvas id="c"></canvas>
    <div class="info">Click to add gravity point.</div>
    531 changes: 531 additions & 0 deletions script.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,531 @@
    /**
    * requestAnimationFrame
    */
    window.requestAnimationFrame = (function(){
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function (callback) {
    window.setTimeout(callback, 1000 / 60);
    };
    })();


    /**
    * Vector
    */
    function Vector(x, y) {
    this.x = x || 0;
    this.y = y || 0;
    }

    Vector.add = function(a, b) {
    return new Vector(a.x + b.x, a.y + b.y);
    };

    Vector.sub = function(a, b) {
    return new Vector(a.x - b.x, a.y - b.y);
    };

    Vector.scale = function(v, s) {
    return v.clone().scale(s);
    };

    Vector.random = function() {
    return new Vector(
    Math.random() * 2 - 1,
    Math.random() * 2 - 1
    );
    };

    Vector.prototype = {
    set: function(x, y) {
    if (typeof x === 'object') {
    y = x.y;
    x = x.x;
    }
    this.x = x || 0;
    this.y = y || 0;
    return this;
    },

    add: function(v) {
    this.x += v.x;
    this.y += v.y;
    return this;
    },

    sub: function(v) {
    this.x -= v.x;
    this.y -= v.y;
    return this;
    },

    scale: function(s) {
    this.x *= s;
    this.y *= s;
    return this;
    },

    length: function() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
    },

    lengthSq: function() {
    return this.x * this.x + this.y * this.y;
    },

    normalize: function() {
    var m = Math.sqrt(this.x * this.x + this.y * this.y);
    if (m) {
    this.x /= m;
    this.y /= m;
    }
    return this;
    },

    angle: function() {
    return Math.atan2(this.y, this.x);
    },

    angleTo: function(v) {
    var dx = v.x - this.x,
    dy = v.y - this.y;
    return Math.atan2(dy, dx);
    },

    distanceTo: function(v) {
    var dx = v.x - this.x,
    dy = v.y - this.y;
    return Math.sqrt(dx * dx + dy * dy);
    },

    distanceToSq: function(v) {
    var dx = v.x - this.x,
    dy = v.y - this.y;
    return dx * dx + dy * dy;
    },

    lerp: function(v, t) {
    this.x += (v.x - this.x) * t;
    this.y += (v.y - this.y) * t;
    return this;
    },

    clone: function() {
    return new Vector(this.x, this.y);
    },

    toString: function() {
    return '(x:' + this.x + ', y:' + this.y + ')';
    }
    };


    /**
    * GravityPoint
    */
    function GravityPoint(x, y, radius, targets) {
    Vector.call(this, x, y);
    this.radius = radius;
    this.currentRadius = radius * 0.5;

    this._targets = {
    particles: targets.particles || [],
    gravities: targets.gravities || []
    };
    this._speed = new Vector();
    }

    GravityPoint.RADIUS_LIMIT = 65;
    GravityPoint.interferenceToPoint = true;

    GravityPoint.prototype = (function(o) {
    var s = new Vector(0, 0), p;
    for (p in o) s[p] = o[p];
    return s;
    })({
    gravity: 0.05,
    isMouseOver: false,
    dragging: false,
    destroyed: false,
    _easeRadius: 0,
    _dragDistance: null,
    _collapsing: false,

    hitTest: function(p) {
    return this.distanceTo(p) < this.radius;
    },

    startDrag: function(dragStartPoint) {
    this._dragDistance = Vector.sub(dragStartPoint, this);
    this.dragging = true;
    },

    drag: function(dragToPoint) {
    this.x = dragToPoint.x - this._dragDistance.x;
    this.y = dragToPoint.y - this._dragDistance.y;
    },

    endDrag: function() {
    this._dragDistance = null;
    this.dragging = false;
    },

    addSpeed: function(d) {
    this._speed = this._speed.add(d);
    },

    collapse: function(e) {
    this.currentRadius *= 1.75;
    this._collapsing = true;
    },

    render: function(ctx) {
    if (this.destroyed) return;

    var particles = this._targets.particles,
    i, len;

    for (i = 0, len = particles.length; i < len; i++) {
    particles[i].addSpeed(Vector.sub(this, particles[i]).normalize().scale(this.gravity));
    }

    this._easeRadius = (this._easeRadius + (this.radius - this.currentRadius) * 0.07) * 0.95;
    this.currentRadius += this._easeRadius;
    if (this.currentRadius < 0) this.currentRadius = 0;

    if (this._collapsing) {
    this.radius *= 0.75;
    if (this.currentRadius < 1) this.destroyed = true;
    this._draw(ctx);
    return;
    }

    var gravities = this._targets.gravities,
    g, absorp,
    area = this.radius * this.radius * Math.PI, garea;

    for (i = 0, len = gravities.length; i < len; i++) {
    g = gravities[i];

    if (g === this || g.destroyed) continue;

    if (
    (this.currentRadius >= g.radius || this.dragging) &&
    this.distanceTo(g) < (this.currentRadius + g.radius) * 0.85
    ) {
    g.destroyed = true;
    this.gravity += g.gravity;

    absorp = Vector.sub(g, this).scale(g.radius / this.radius * 0.5);
    this.addSpeed(absorp);

    garea = g.radius * g.radius * Math.PI;
    this.currentRadius = Math.sqrt((area + garea * 3) / Math.PI);
    this.radius = Math.sqrt((area + garea) / Math.PI);
    }

    g.addSpeed(Vector.sub(this, g).normalize().scale(this.gravity));
    }

    if (GravityPoint.interferenceToPoint && !this.dragging)
    this.add(this._speed);

    this._speed = new Vector();

    if (this.currentRadius > GravityPoint.RADIUS_LIMIT) this.collapse();

    this._draw(ctx);
    },

    _draw: function(ctx) {
    var grd, r;

    ctx.save();

    grd = ctx.createRadialGradient(this.x, this.y, this.radius, this.x, this.y, this.radius * 5);
    grd.addColorStop(0, 'rgba(0, 0, 0, 0.1)');
    grd.addColorStop(1, 'rgba(0, 0, 0, 0)');
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius * 5, 0, Math.PI * 2, false);
    ctx.fillStyle = grd;
    ctx.fill();

    r = Math.random() * this.currentRadius * 0.7 + this.currentRadius * 0.3;
    grd = ctx.createRadialGradient(this.x, this.y, r, this.x, this.y, this.currentRadius);
    grd.addColorStop(0, 'rgba(0, 0, 0, 1)');
    grd.addColorStop(1, Math.random() < 0.2 ? 'rgba(255, 196, 0, 0.15)' : 'rgba(103, 181, 191, 0.75)');
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.currentRadius, 0, Math.PI * 2, false);
    ctx.fillStyle = grd;
    ctx.fill();
    ctx.restore();
    }
    });


    /**
    * Particle
    */
    function Particle(x, y, radius) {
    Vector.call(this, x, y);
    this.radius = radius;

    this._latest = new Vector();
    this._speed = new Vector();
    }

    Particle.prototype = (function(o) {
    var s = new Vector(0, 0), p;
    for (p in o) s[p] = o[p];
    return s;
    })({
    addSpeed: function(d) {
    this._speed.add(d);
    },

    update: function() {
    if (this._speed.length() > 12) this._speed.normalize().scale(12);

    this._latest.set(this);
    this.add(this._speed);
    }

    // render: function(ctx) {
    // if (this._speed.length() > 12) this._speed.normalize().scale(12);

    // this._latest.set(this);
    // this.add(this._speed);

    // ctx.save();
    // ctx.fillStyle = ctx.strokeStyle = '#fff';
    // ctx.lineCap = ctx.lineJoin = 'round';
    // ctx.lineWidth = this.radius * 2;
    // ctx.beginPath();
    // ctx.moveTo(this.x, this.y);
    // ctx.lineTo(this._latest.x, this._latest.y);
    // ctx.stroke();
    // ctx.beginPath();
    // ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
    // ctx.fill();
    // ctx.restore();
    // }
    });



    // Initialize

    (function() {

    // Configs

    var BACKGROUND_COLOR = 'rgba(11, 51, 56, 1)',
    PARTICLE_RADIUS = 1,
    G_POINT_RADIUS = 10,
    G_POINT_RADIUS_LIMITS = 65;


    // Vars

    var canvas, context,
    bufferCvs, bufferCtx,
    screenWidth, screenHeight,
    mouse = new Vector(),
    gravities = [],
    particles = [],
    grad,
    gui, control;


    // Event Listeners

    function resize(e) {
    screenWidth = canvas.width = window.innerWidth;
    screenHeight = canvas.height = window.innerHeight;
    bufferCvs.width = screenWidth;
    bufferCvs.height = screenHeight;
    context = canvas.getContext('2d');
    bufferCtx = bufferCvs.getContext('2d');

    var cx = canvas.width * 0.5,
    cy = canvas.height * 0.5;

    grad = context.createRadialGradient(cx, cy, 0, cx, cy, Math.sqrt(cx * cx + cy * cy));
    grad.addColorStop(0, 'rgba(0, 0, 0, 0)');
    grad.addColorStop(1, 'rgba(0, 0, 0, 0.35)');
    }

    function mouseMove(e) {
    mouse.set(e.clientX, e.clientY);

    var i, g, hit = false;
    for (i = gravities.length - 1; i >= 0; i--) {
    g = gravities[i];
    if ((!hit && g.hitTest(mouse)) || g.dragging)
    g.isMouseOver = hit = true;
    else
    g.isMouseOver = false;
    }

    canvas.style.cursor = hit ? 'pointer' : 'default';
    }

    function mouseDown(e) {
    for (var i = gravities.length - 1; i >= 0; i--) {
    if (gravities[i].isMouseOver) {
    gravities[i].startDrag(mouse);
    return;
    }
    }
    gravities.push(new GravityPoint(e.clientX, e.clientY, G_POINT_RADIUS, {
    particles: particles,
    gravities: gravities
    }));
    }

    function mouseUp(e) {
    for (var i = 0, len = gravities.length; i < len; i++) {
    if (gravities[i].dragging) {
    gravities[i].endDrag();
    break;
    }
    }
    }

    function doubleClick(e) {
    for (var i = gravities.length - 1; i >= 0; i--) {
    if (gravities[i].isMouseOver) {
    gravities[i].collapse();
    break;
    }
    }
    }


    // Functions

    function addParticle(num) {
    var i, p;
    for (i = 0; i < num; i++) {
    p = new Particle(
    Math.floor(Math.random() * screenWidth - PARTICLE_RADIUS * 2) + 1 + PARTICLE_RADIUS,
    Math.floor(Math.random() * screenHeight - PARTICLE_RADIUS * 2) + 1 + PARTICLE_RADIUS,
    PARTICLE_RADIUS
    );
    p.addSpeed(Vector.random());
    particles.push(p);
    }
    }

    function removeParticle(num) {
    if (particles.length < num) num = particles.length;
    for (var i = 0; i < num; i++) {
    particles.pop();
    }
    }


    // GUI Control

    control = {
    particleNum: 50
    };


    // Init

    canvas = document.getElementById('c');
    bufferCvs = document.createElement('canvas');

    window.addEventListener('resize', resize, false);
    resize(null);

    addParticle(control.particleNum);

    canvas.addEventListener('mousemove', mouseMove, false);
    canvas.addEventListener('mousedown', mouseDown, false);
    canvas.addEventListener('mouseup', mouseUp, false);
    canvas.addEventListener('dblclick', doubleClick, false);


    // GUI

    gui = new dat.GUI();
    gui.add(control, 'particleNum', 0, 300).step(1).name('Particle Num').onChange(function() {
    var n = (control.particleNum | 0) - particles.length;
    if (n > 0)
    addParticle(n);
    else if (n < 0)
    removeParticle(-n);
    });
    gui.add(GravityPoint, 'interferenceToPoint').name('Interference Between Point');
    gui.close();


    // Start Update

    var loop = function() {
    var i, len, g, p;

    context.save();
    context.fillStyle = BACKGROUND_COLOR;
    context.fillRect(0, 0, screenWidth, screenHeight);
    context.fillStyle = grad;
    context.fillRect(0, 0, screenWidth, screenHeight);
    context.restore();

    for (i = 0, len = gravities.length; i < len; i++) {
    g = gravities[i];
    if (g.dragging) g.drag(mouse);
    g.render(context);
    if (g.destroyed) {
    gravities.splice(i, 1);
    len--;
    i--;
    }
    }

    bufferCtx.save();
    bufferCtx.globalCompositeOperation = 'destination-out';
    bufferCtx.globalAlpha = 0.35;
    bufferCtx.fillRect(0, 0, screenWidth, screenHeight);
    bufferCtx.restore();

    // パーティクルをバッファに描画
    // for (i = 0, len = particles.length; i < len; i++) {
    // particles[i].render(bufferCtx);
    // }
    len = particles.length;
    bufferCtx.save();
    bufferCtx.fillStyle = bufferCtx.strokeStyle = '#fff';
    bufferCtx.lineCap = bufferCtx.lineJoin = 'round';
    bufferCtx.lineWidth = PARTICLE_RADIUS * 2;
    bufferCtx.beginPath();
    for (i = 0; i < len; i++) {
    p = particles[i];
    p.update();
    bufferCtx.moveTo(p.x, p.y);
    bufferCtx.lineTo(p._latest.x, p._latest.y);
    }
    bufferCtx.stroke();
    bufferCtx.beginPath();
    for (i = 0; i < len; i++) {
    p = particles[i];
    bufferCtx.moveTo(p.x, p.y);
    bufferCtx.arc(p.x, p.y, p.radius, 0, Math.PI * 2, false);
    }
    bufferCtx.fill();
    bufferCtx.restore();

    // バッファをキャンバスに描画
    context.drawImage(bufferCvs, 0, 0);

    requestAnimationFrame(loop);
    };
    loop();

    })();
    1 change: 1 addition & 0 deletions scripts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    <script src="http://dat-gui.googlecode.com/git/build/dat.gui.min.js"></script>
    28 changes: 28 additions & 0 deletions style.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    body {
    font-family: Helvetica sans-serif;
    padding: 0;
    margin: 0;
    background-color: #222;
    overflow: hidden;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    }

    canvas {
    position: absolute;
    top: 0;
    left: 0;
    }

    .info {
    position: absolute;
    top: 0;
    left: 0;
    padding: 5px 15px;
    color: #eee;
    font-size: 13px;
    background-color: rgba(0, 0, 0, .5);
    }