Skip to content

Instantly share code, notes, and snippets.

@sethkontny
Created October 14, 2013 04:15
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 sethkontny/6970674 to your computer and use it in GitHub Desktop.
Save sethkontny/6970674 to your computer and use it in GitHub Desktop.
A Pen by seth kontny.
<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;
}

TweetLove

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.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment