A Pen by Lakindu Akash on CodePen.
Created
June 9, 2018 18:16
-
-
Save lakinduakash/54382cbf716cb48ac467ba1f4ac1f8de to your computer and use it in GitHub Desktop.
Gravity Points
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<canvas id="c"></canvas> | |
<div class="info">Click to add gravity point.</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://codepen.io/akm2/pen/KzYMdN"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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