A social experiment based on Twitter API. Got inspired by 500 Chrome Experiments. Used basic physics (spring/ease and friction). No libs needed. Hover the particles to change the tweet.
A Pen by seth kontny on CodePen.
<body> | |
<div class = "tweets"><p class = "mentions">Loading tweets...</p></div> | |
<audio id = "popOne" no-controls> | |
<source src = "http://s.cdpn.io/16395/popOne.mp3" type = "audio/mpeg"> | |
<source src = "http://s.cdpn.io/16395/popOne.ogg" type = "audio/ogg"> | |
</audio> | |
<audio id = "popTwo" no-controls> | |
<source src = "http://s.cdpn.io/16395/popTwo.mp3" type = "audio/mpeg"> | |
<source src = "http://s.cdpn.io/16395/popTwo.ogg" type = "audio/ogg"> | |
</audio> | |
</body> |
/* | |
* Copyright MIT © <2013> <Francesco Trillini> | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated | |
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation | |
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and | |
* to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE | |
* FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
;$(function($) { | |
var canvas, context, audioContext, buffer, particles = [], audio = ['popOne', 'popTwo'], tweets = [], mouse = { x: -99999, y: -99999 }, type = ['circle', 'rumble'], skip = step = 0, closestIndex = -1, force = 1, enableWebAudioAPI = isLoading = release = true, played = false, lastTime = lastDownload = $.now(), FPS = 60; | |
/* | |
* List colors. | |
*/ | |
var colors = [ | |
'#7c4960', | |
'#ffec00', | |
'#e90055', | |
// 5 more blue color | |
'#5ccfea', | |
'#5ccfea', | |
'#5ccfea', | |
'#5ccfea', | |
'#a94eb5', | |
'#ceff00', | |
'#bce3de', | |
// 2 more orange color | |
'#ffb600', | |
'#ffb600', | |
// 2 more black color | |
'#000000', | |
'#000000' | |
]; | |
/* | |
* Init. | |
*/ | |
function init() { | |
var body = document.querySelector('body'); | |
canvas = document.createElement('canvas'); | |
canvas.width = innerWidth; | |
canvas.height = innerHeight; | |
canvas.style.position = 'absolute'; | |
canvas.style.top = 0; | |
canvas.style.bottom = 0; | |
canvas.style.left = 0; | |
canvas.style.right = 0; | |
canvas.style.zIndex = -1; | |
canvas.style.background = 'rgb(255, 255, 255);'; | |
body.appendChild(canvas); | |
// Browser supports canvas? | |
if(!!(capable)) { | |
context = canvas.getContext('2d'); | |
// Events | |
if('ontouchmove' in window) { | |
document.addEventListener('touchmove', self.onTouchMove, false); | |
} | |
else { | |
document.addEventListener('mousemove', onMouseMove, false); | |
} | |
window.onresize = onResize; | |
// Todo | |
preloadAudio(); | |
createParticles(); | |
} | |
else { | |
console.error('Sorry, your browser sucks :('); | |
} | |
} | |
/* | |
* Checks if browser supports canvas element. | |
*/ | |
function capable() { | |
return canvas.getContext && canvas.getContext('2d'); | |
} | |
/* | |
* On resize window event. | |
*/ | |
function onResize() { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
} | |
/* | |
* Mouse move event. | |
*/ | |
function onMouseMove(event) { | |
event.preventDefault(); | |
mouse.x = event.pageX - canvas.offsetLeft; | |
mouse.y = event.pageY - canvas.offsetTop; | |
} | |
/* | |
* Touch move event. | |
*/ | |
function onTouchMove(event) { | |
event.preventDefault(); | |
mouse.x = event.touches[0].pageX - canvas.offsetLeft; | |
mouse.y = event.touches[0].pageY - canvas.offsetTop; | |
} | |
/* | |
* Get the audio file via Ajax. | |
*/ | |
function preloadAudio() { | |
try { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
var request = new XMLHttpRequest(); | |
request.open('GET', 'http://s3-us-west-2.amazonaws.com/s.cdpn.io/16395/popOne.ogg', true); | |
request.responseType = 'arraybuffer'; | |
request.onload = function() { | |
audioContext.decodeAudioData(request.response, function(chunk) { | |
buffer = chunk; | |
}, function() { | |
$.error('Failed to get audio file :('); | |
}); | |
}; | |
request.send(); | |
} | |
catch(Exception) { | |
enableWebAudioAPI = false; | |
} | |
} | |
/* | |
* Create particles. | |
*/ | |
function createParticles() { | |
var colums, rows, columsSpacing, rowsSpacing, columsPadding, rowsPadding; | |
colums = 15; | |
rows = 15; | |
columsSpacing = (innerWidth || canvas.width) * 0.5 / colums; | |
rowsSpacing = (innerHeight || canvas.height) * 0.5 / rows; | |
columsPadding = 7; | |
rowsPadding = 3; | |
// Iterate through the grid | |
for(var colum = columsPadding, columsLen = colums + columsPadding; colum < columsLen; colum++) { | |
for(var row = rowsPadding, rowsLen = rows + rowsPadding; row < rowsLen; row++) { | |
var x, y, shape, radius; | |
x = colum * columsSpacing + columsSpacing * 0.5; | |
y = row * rowsSpacing + rowsSpacing * 0.5; | |
shape = type[~~(Math.random() * type.length)]; | |
radius = shape === 'circle' ? randomBetween(2, 10) : randomBetween(2, 10) * 2; | |
particles.push({ | |
x: x, | |
y: y, | |
goalX: x, | |
goalY: y, | |
centerX: x, | |
centerY: y, | |
gridX: x, | |
gridY: y, | |
vx: 0, | |
vy: 0, | |
radius: radius, | |
towardsRadius: radius, | |
color: colors[~~(Math.random() * colors.length)], | |
alpha: 0.0, | |
orbit: ~~(Math.random() * 70), | |
speed: 0.06 + Math.random() * 0.08, | |
angle: 0, | |
over: false, | |
type: shape | |
}); | |
} | |
} | |
loop(); | |
} | |
/* | |
* Download tweets. | |
*/ | |
function downloadTweets() { | |
// Schedule every 5 secs | |
if($.now() - lastDownload > 5000) { | |
// Reset the array, and re-fill again with newest tweets | |
tweets = []; | |
$.getJSON('http://francescotrillini.it/assets/oauth/index.php', function(data) { | |
for(var tweet = 0, limit = 100; tweet < limit; tweet++) | |
tweets.push(data.statuses[tweet]); | |
}); | |
lastDownload = $.now(); | |
} | |
} | |
/* | |
* Load a random tweet. | |
*/ | |
function loadTweet(color) { | |
var tweet = tweets[~~(Math.random() * tweets.length)]; | |
// Wait a moment...still loading | |
if(tweet !== undefined) { | |
var text = tweet.text.replace(/http:\/\/(\S+)/, "<a href = \"http://$1\">http://$1</a>"); | |
text = text.replace(/@(\S+)/, "<a href = \"http://twitter.com/$1\">@$1</a>"); | |
$('.tweets').html('<p class = "mentions">' + text + ' by ' + '<a href = "http://twitter.com/' + tweet.user.screen_name + '"> ' + tweet.user.screen_name + '</a>' + '</p>'); | |
isLoading = false; | |
} | |
$('a').css({ | |
'color': color || colors[~~(Math.random() * colors.length)], | |
'text-decoration': 'none', | |
'font-size': '1.7em' | |
}); | |
} | |
/* | |
* Play the audio. | |
*/ | |
function play() { | |
if(enableWebAudioAPI) { | |
var source = audioContext.createBufferSource(); | |
source.playbackRate.value = Math.pow(Math.random(), 2) * 0.9 + 0.25; | |
source.gain.value = 0.4; | |
source.buffer = buffer; | |
source.connect(audioContext.destination); | |
source.noteOn(0); | |
} | |
else { | |
var media = document.querySelector('#' + audio[~~(Math.random() * audio.length)]); | |
media.play(); | |
} | |
} | |
/* | |
* Loop logic. | |
*/ | |
function loop() { | |
downloadTweets(); | |
// If it's the first time, load automatically a random tweet | |
if(isLoading) | |
loadTweet(); | |
clear(); | |
update(); | |
render(); | |
requestAnimFrame(loop); | |
} | |
/* | |
* Clear the whole screen. | |
*/ | |
function clear() { | |
context.fillStyle = 'rgba(255, 255, 255, 0.2)'; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
}; | |
/* | |
* Update the particles. | |
*/ | |
function update() { | |
particles.forEach(function(particle, index) { | |
/* | |
* ========== Initial step. ========== | |
*/ | |
if(step > 100) { | |
// Toggle visibility | |
particles[skip].alpha = 1.0; | |
if(skip < particles.length -1) { | |
skip += 1; | |
// Reset the current step | |
step = 0; | |
} | |
else | |
skip = particles.length - 1; | |
} | |
/* | |
* ========== Behaviour when the mouse is in/out the particle. ========== | |
*/ | |
// If the mouse is close to the particle... | |
if(distanceTo(particle, mouse) < 60 && closestIndex !== index && particle.alpha === 1.0) { | |
// Add forces | |
particle.orbit = ~~(Math.random() * 70); | |
particle.speed = 0.06 + Math.random() * 0.08; | |
if(!release) { | |
var currentClosestParticle, maxRadius; | |
currentClosestParticle = particles[closestIndex]; | |
maxRadius = currentClosestParticle.type === 'circle' ? 100 : 150; | |
if(distanceTo(currentClosestParticle, mouse) < 60) { | |
if(!played) { | |
try { | |
play(); | |
} | |
catch(Exception) {} | |
loadTweet(currentClosestParticle.color); | |
played = true; | |
} | |
currentClosestParticle.over = true; | |
currentClosestParticle.speed = 0; | |
// Towards to max radius | |
currentClosestParticle.radius += (maxRadius - currentClosestParticle.radius) * 0.09; | |
// Reached the towards add a gravity field | |
if(~~currentClosestParticle.radius === maxRadius - 1) | |
force = 150; | |
} | |
} | |
else { | |
closestIndex = index; | |
release = false; | |
} | |
} | |
// If the mouse is out the particle... | |
else if(distanceTo(particle, mouse) > 70 && closestIndex === index && particle.alpha === 1.0) { | |
force = 1; | |
if(distanceTo(particles[closestIndex], mouse) > 70) { | |
particles[closestIndex].over = false; | |
particles[closestIndex].speed = 0.06 + Math.random() * 0.08; | |
release = true; | |
played = !release; | |
} | |
} | |
// Restore back to original radius | |
if(!particle.over) | |
particle.radius += (particle.towardsRadius - particle.radius) * 0.5; | |
/* | |
* ========== Transitions (grid, circle, heart). ========== | |
*/ | |
var angle, steps, ease = 0.01, friction = 0.96; | |
angle = Math.atan2(particle.y - mouse.y, particle.x - mouse.x); | |
steps = Math.PI * 2 * index / particles.length; | |
// Inverse polar system | |
particle.x += Math.cos(angle) * force / distanceTo(particle, mouse) + (particle.goalX - particle.x) * 0.08; | |
particle.y += Math.sin(angle) * force / distanceTo(particle, mouse) + (particle.goalY - particle.y) * 0.08; | |
// Interactive orbit force when the mouse is far away from particles | |
if(distanceTo(particle, mouse) > 60) { | |
particle.goalX = particle.centerX + Math.cos(index + particle.angle) * particle.orbit; | |
particle.goalY = particle.centerY + Math.sin(index + particle.angle) * particle.orbit; | |
} | |
// Rotation | |
particle.angle += particle.speed; | |
// Loss forces | |
particle.speed = Math.max(particle.speed - 0.00005, 0); | |
particle.orbit += (1 - particle.orbit) * 0.001; | |
// Circle | |
if($.now() - lastTime > 6000 && $.now() - lastTime < 12000) { | |
// Ease | |
particle.vx += ((innerWidth || canvas.width) * 0.5 + 170 * Math.cos(steps) - particle.centerX) * ease; | |
particle.vy += (250 + 170 * Math.sin(steps) - particle.centerY) * ease; | |
// Friction | |
particle.vx *= friction; | |
particle.vy *= friction; | |
particle.centerX += particle.vx; | |
particle.centerY += particle.vy; | |
} | |
// Heart | |
if($.now() - lastTime > 12000 && $.now() - lastTime < 18000) { | |
// Ease | |
particle.vx += ((innerWidth || canvas.width) * 0.5 + 180 * Math.pow(Math.sin(index), 3) - particle.centerX) * ease; | |
particle.vy += (250 + 10 * ( - (15 * Math.cos(index) - 5 * Math.cos(2 * index) - 2 * Math.cos(3 * index) - Math.cos(4 * index))) - particle.centerY) * ease; | |
// Friction | |
particle.vx *= friction; | |
particle.vy *= friction; | |
particle.centerX += particle.vx; | |
particle.centerY += particle.vy; | |
} | |
// Grid | |
if($.now() - lastTime > 18000 && $.now() - lastTime < 24000) { | |
// Ease | |
particle.vx += (particle.gridX - particle.centerX) * ease; | |
particle.vy += (particle.gridY - particle.centerY) * ease; | |
// Friction | |
particle.vx *= friction; | |
particle.vy *= friction; | |
particle.centerX += particle.vx; | |
particle.centerY += particle.vy; | |
} | |
// Reset 'em all | |
if($.now() - lastTime > 24000) | |
lastTime = $.now(); | |
}); | |
step += 200; | |
} | |
/* | |
* Render the particles. | |
*/ | |
function render() { | |
[].forEach.call(particles, function(particle, index) { | |
context.save(); | |
context.globalAlpha = particle.alpha; | |
context.translate(particle.x, particle.y); | |
context.rotate(45 * Math.PI / 180); | |
context.fillStyle = particle.color; | |
context.beginPath(); | |
particle.type === 'circle' ? context.arc(-2, -2, particle.radius, 0, Math.PI * 2) : context.rect(particle.radius / -2, particle.radius / -2, particle.radius, particle.radius); | |
context.closePath(); | |
context.fill(); | |
context.restore(); | |
}); | |
} | |
/* | |
* Distance between two points. | |
*/ | |
function distanceTo(pointA, pointB) { | |
var dx = Math.abs(pointA.x - pointB.x); | |
var dy = Math.abs(pointA.y - pointB.y); | |
return Math.sqrt(dx * dx + dy * dy); | |
} | |
/* | |
* Useful function for random integer between [min, max]. | |
*/ | |
function randomBetween(min, max) { | |
return ~~(Math.random() * (max - min + 1) + min); | |
} | |
/* | |
* Request new frame by Paul Irish. | |
* 60 FPS. | |
*/ | |
window.requestAnimFrame = (function() { | |
return window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
function(callback) { | |
window.setTimeout(callback, 1000 / FPS); | |
}; | |
})(); | |
window.addEventListener ? window.addEventListener('load', init, false) : window.onload = init; | |
})(jQuery); |
@import url(http://fonts.googleapis.com/css?family=Lato:900); | |
*, *:after, *:before { | |
box-sizing: border-box; | |
} | |
html { | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
} | |
body { | |
-webkit-user-select:none; | |
-moz-user-select:none; | |
} | |
.tweets { | |
width: 100%; | |
height: auto; | |
position: absolute; | |
top: calc(80% - 50px); | |
left: 0; | |
margin: 0; | |
font-size: 1.4em; | |
z-index: 1; | |
} | |
.mentions { | |
color: rgb(200, 200, 200); | |
font-family: 'Lato'; | |
text-align: center; | |
padding-left: 5px; | |
} |
A social experiment based on Twitter API. Got inspired by 500 Chrome Experiments. Used basic physics (spring/ease and friction). No libs needed. Hover the particles to change the tweet.
A Pen by seth kontny on CodePen.