Skip to content

Instantly share code, notes, and snippets.

@stanwmusic
Created October 24, 2017 20:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stanwmusic/13f0313e49162fe5a2b626a560ee8bdb to your computer and use it in GitHub Desktop.
Save stanwmusic/13f0313e49162fe5a2b626a560ee8bdb to your computer and use it in GitHub Desktop.
Gravity Points
<canvas id="c"></canvas>
<div class="info">Click to add gravity point.</div>
/**
* 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: 100
};
// 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, 500).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();
})();
<script src="https://codepen.io/akm2/pen/KzYMdN"></script>
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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment