|
|
|
/* |
|
SANDER SAYS: |
|
NO WARRANTIES EXPRESSED OR IMPLIED |
|
FOR USING THIS CODE: "ALL THIS" |
|
HAS HAPPENED BEFORE, AND IT WILL |
|
HAPPEN AGAIN... BUT IT DOESN'T |
|
MATTER - BECAUSE WE ARE IN THIS |
|
TOGETHER. EVERY PATH IS THE RIGHT |
|
PATH: EVERYTHING COULD HAVE BEEN |
|
ANYTHING ELSE, AND IT WOULD HAVE |
|
JUST AS MUCH MEANING. ENJOY. SHARE. |
|
COMPLIMENTS? PARTY INVITATIONS? |
|
RIGHT ON! CONTACT @HYPERABSOLUTE |
|
ON TWITTER OR VISIT UXRIG.COM |
|
STAY AWESOME | HYPERABSOLUTE |
|
*/ |
|
/** |
|
* 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(); |
|
|
|
})(); |
|
|
|
|
|
|
|
social("codepen/sander-nizni", |
|
"twitter/hyperabsolute", |
|
"linkedin/sandernizni", |
|
"instagram/hyperabsolute", |
|
"facebook/sander.nizni", |
|
"/sandernizni.wordpress.com", |
|
"light", "Sander says... Try it in full screen, dawg."); |