/**
* Copyright (c) 2011, Stewart Lord
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL STEWART LORD BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
dojo.require("dijit._Widget");
dojo.declare("visibleFeeds", [dijit._Widget], {
url: "",
data: null,
format: "json",
fields: {},
messages: [],
maxMessages: 10,
maxTextLength: 200,
messageWidth: 300,
messageHeight: 300,
minScale: .75,
maxScale: 2,
duration: {
color: 5000,
rotation: 2000,
opacity: 2000,
show: 5000,
},
sparseness: 1,
currentHub: null,
currentAngle: 0,
currentLeft: 0,
currentTop: 0,
startup: function(){
// make ourselves css targetable
dojo.addClass(this.domNode, 'visible-feeds');
// make stage movable
dojo.style(this.domNode, 'position', 'absolute');
// change background color.
this.setTransition(dojo.body(), 'all', this.duration.color + 'ms');
window.setInterval(dojo.hitch(this, 'changeBackground'), this.duration.color * 2);
this.changeBackground();
// load messages (kicks off animation)
this.loadMessages();
},
changeBackground: function(){
var color = Math.round((0x111111 * Math.random()) + 0xeeeeee).toString(16);
dojo.style(dojo.body(), 'background-color', '#' + color);
},
loadMessages: function(){
dojo.xhrGet({
url: this.url,
handleAs: this.format == 'rss' ? 'xml' : this.format,
load: dojo.hitch(this, 'loadHandler'),
error: dojo.hitch(this, function(){
alert("Failed to load: " + this.url);
})
});
},
loadHandler: function(response){
if (!response) {
throw "Empty response";
}
// accommodate rss data
var data = (this.format == 'rss')
? dojo.query('item', response)
: response;
// only keep maxMessages entries.
this.data = data.slice(0, this.maxMessages);
this.loadMessage();
},
loadMessage: function(){
var data = this.data.shift();
if (!data) return;
// if message already exists, show it
// otherwise, create a new one.
var message = this.findMessage(data);
if (!message) {
message = this.createMessage(data);
}
// fade-out the old messages and fade-in the new.
window.setTimeout(dojo.hitch(this, function(){
for (var i in this.messages) {
dojo.style(this.messages[i], 'opacity', '.15');
}
dojo.style(message, 'opacity', 1);
}), 10);
// rotate to the angle of the message
// (spin halfway now, rest on show).
var angle = parseInt(dojo.attr(message, 'angle'));
var targetAngle = (angle * -1);
var adjustAngle = targetAngle - Math.round((targetAngle - this.currentAngle) * .5);
var rotate = adjustAngle + 'deg';
this.currentAngle = targetAngle;
// center the message
// (move 2/3rd's now, rest on show).
var targetOffset = this.getCenterOffset(message, this.minScale);
var adjustLeft = this.currentLeft + Math.round((targetOffset.left - this.currentLeft) * .66);
var adjustTop = this.currentTop + Math.round((targetOffset.top - this.currentTop) * .66);
var translate = adjustLeft + 'px,' + adjustTop + 'px';
this.currentLeft = targetOffset.left;
this.currentTop = targetOffset.top;
// animate (move, spin and pull back)
// consumes 1/3rd of the animation time
this.setTransition(
this.domNode,
'all',
Math.round(this.duration.rotation * .33) + 'ms',
'cubic-bezier(0,0,.1,1)'
);
window.setTimeout(dojo.hitch(this, function(){
this.setTransform(this.domNode, this.minScale, rotate, translate);
}), 0);
// complete animation (spin/move more and zoom in)
// uses remaining 2/3rd's of animation time
window.setTimeout(
dojo.hitch(this, function(){this.zoomMessage(message);}),
Math.round(this.duration.rotation * .33)
);
},
createMessage: function(data){
// remove old message if over max messages.
if (this.messages.length >= this.maxMessages){
this.removeOldest();
}
// position relative to previous message (rotated 90 degrees)
var position = this.getNextPosition();
var angle = this.getNextAngle();
var message = dojo.create('div', {
id: this.domNode.id + '_' + position.x + '_' + position.y,
x: position.x,
y: position.y,
angle: angle,
innerHTML: this.formatMessage(data),
data: data instanceof Node
? new XMLSerializer().serializeToString(data)
: dojo.toJson(data)
});
// make message clickable.
dojo.query('p.body', message).connect('onclick', dojo.hitch(this,
function(){window.open(this.getLink(data));}
));
// fallback to default image.
dojo.query('img', message).connect('onerror', dojo.hitch(this,
function(e){e.target.src = this.getDefaultImage();}
));
// style the message.
dojo.place(message, this.domNode);
dojo.addClass(message, 'message');
dojo.style(message, {
position: 'absolute',
left: position.left + 'px',
top: position.top + 'px',
width: this.messageWidth + 'px',
height: this.messageHeight + 'px',
opacity: 0,
});
this.setTransform(message, null, angle + 'deg');
this.setTransition(message, 'opacity', this.duration.opacity + 'ms');
this.messages.push(message);
return message;
},
zoomMessage: function(message){
// fudge the angle +/- 5 degrees.
var slant = Math.round(Math.random() * 10) - 5;
var rotate = (this.currentAngle + slant) + 'deg';
var offset = this.getCenterOffset(message, this.maxScale);
var translate = offset.left + 'px,' + offset.top + 'px';
this.setTransition(
this.domNode,
'all',
Math.round(this.duration.rotation * .66) + 'ms',
'cubic-bezier(0.5,0,0.5,1)'
);
this.setTransform(this.domNode, this.maxScale, rotate, translate);
// if data remaining, load next message; otherwise, load more
var timeout = this.duration.show + this.duration.rotation;
if (this.data.length){
window.setTimeout(dojo.hitch(this, 'loadMessage'), timeout);
} else {
window.setTimeout(dojo.hitch(this, 'loadMessages'), timeout);
}
},
formatMessage: function(data){
var body = this.getBody(data);
var author = this.getAuthor(data);
var time = this.getTime(data);
var image = this.getImage(data);
return "<p class=body>" + body + "</p><p class=byline>"
+ "<span class=image>" + image + "</span>"
+ "<span class=author>" + author + "</span>, "
+ "<span class=time>" + time + "</span></p>";
},
getValue: function(field, data){
var field = this.fields[field] || field;
if (this.format == 'rss'){
var nodes = dojo.query(field, data);
if (nodes[0] && nodes[0].childNodes && nodes[0].childNodes[0])
return nodes[0].childNodes[0].nodeValue;
}
if (this.format == 'json')
return data[field];
},
getBody: function(data){
return this.truncate(this.getValue('body', data), this.maxTextLength, '...');
},
getAuthor: function(data){
return this.getValue('author', data);
},
getTime: function(data){
return this.getTimeAgo(new Date(this.getValue('time', data)));
},
getImage: function(data){
return "<img valign=middle src='" + this.getValue('image', data) + "'>";
},
getLink: function(data){
return this.getValue('link', data);
},
getTimeAgo: function(date){
var times = [
[60, 0, 'just now'],
[120, 0, '1 minute ago'],
[3600, 60, 'minutes ago'],
[7200, 0, '1 hour ago'],
[86400, 3600, 'hours ago'],
[172800, 0, 'yesterday'],
[604800, 86400, 'days ago'],
[1209600, 0, 'last week'],
[2419200, 604800, 'weeks ago'],
[4838400, 0, 'last month'],
[29030400, 2419200, 'months ago'],
];
var seconds = Math.round((new Date().getTime() - date.getTime()) / 1000);
for (var time in times) {
time = times[time];
if (seconds < time[0]) {
if (time[1]) {
return Math.floor(seconds / time[1]) + " " + time[2];
}
return time[2];
}
}
return date.toDateString();
},
getDefaultImage: function(data){
return "data:image/png;base64,"
+ "iVBORw0KGgoAAAANSUhEUgAAAEAAAAA/CAYAAABQHc7KAAAAGXRFWHRTb2Z"
+ "0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+JJREFUeNrkm19IU1Ecx8"
+ "+225wTI4LlSjJSyf6gQc0YPVQGNQgMoqd66qEH+/eUvdVTEYT1EkQU9OdBq"
+ "KSgwB5WaIOihwoiJSnSWoUPNU1dxBqbW9+fO4LoKjfvPec394MPgt57rr/P"
+ "Pffc8+ceWzqdFsUcdlHkUfQCjFxP6OvrE+FwWBiGke81K8BCUCV/UiTAd/A"
+ "VREAy10LHx8eFx+MRPp/PWgHBYFB0dHSIsrKy2Z5iA00gALaDWkAnL8hybF"
+ "RK6AaPwSMQn81FYrGY8Pv91gtwOp0Tybvd7v8dWgL2gUNg0yyLpxqxTnKMK"
+ "hy4Bq6D0X8+y3a7cLlcbNqAreApuJFD8tliLbgAXoK9hdAIUnU/CbpAo4nl"
+ "0mNzF1wGbs4CroLTwGFRzWoB90EpRwEnwEEFb64d4CI3AQ3yzqsKEr2bk4A"
+ "22eqrjDYz2gMzBGwBOzV04laB/RwEHNbYk22Za4M7VwHlsgboCmp7anQKoI"
+ "sv1SiAutP1OgXoTH4ylusUUM1AQK1OAZUMBFTqFFDKQIBLpwCbKPCw67RvU"
+ "jh1CnAU+k2cq4B0sQqolqO/AAMBDXJg5Mvn5Hymdmne7iFYzaQdWwRa5RB5"
+ "I/hodQ1oZJT8dBEBFY9AveAb61UIcDIW4FQh4DNjAWEVAnpBiqmAVyoEDIB"
+ "PDJP/Bd6oEEBrdc8YCqDkB1V1hEIMBXSp7Al2giFGydNy+h2VAij5W4wEPA"
+ "FvVQ8kLoHfTAS06RhJvReZNYGYxsRpNHpKZD6m0DIcpvX/DbIKqo53wA/O6"
+ "JwPmPxHbmsQEAQvtE4mTHscVEevGYXYTayOPxUL6OEkgD5t61eY/JDskrMR"
+ "QIOj5woFvAY/OAkQit8EpnXFzRTQrah7TLXtAUcBI+CmAgGd+XZ7rRYgZKf"
+ "EylciPfetZhZotoAxcMVCAffAB84CKKIWChg1u0ArBFg5a+wsdgGuQhDgtV"
+ "DAkkIQUGOhgJXcBVB5Vq4b0jabxZwF0FL1GgsFUPJNnAUcEfktuecSR7UKc"
+ "Dj++lXMHnBAQVd4GzieNRl77vfT6OnJbV4hEolMl0CvPZocPavg7k/GeeAB"
+ "5+QYZCL5aDQqsuVDu2Nps1ddXd0MSbZAIKdvChyGYZSjkBrZIG0Gu0Rmc5O"
+ "O+CIHR7RU9wmJDiQSCeotJqYelEwmhdfrFe3t7TNrAMzQF9/LJHHZ2aDf0Q"
+ "aIammavr5YIY+hhqhC8IgqWfsmPtm32WzDyGdYZDZhDsjaMYIa259KpWKhU"
+ "GgMfyc53wAdl7A1NzcPyqRcYh4HPQbxeFzIzeJ0o2k1OW7IuzrvA7Vj6sbK"
+ "Eon4I8AA5ufCOT9/vtsAAAAASUVORK5CYII=";
},
getNextPosition: function(){
// get grid coords of current hub message
if (!this.messages.length) {
return {x: 0, y: 0, left: 0, top: 0};
} else if (!this.currentHub) {
this.currentHub = this.messages[0];
}
var x = parseInt(dojo.attr(this.currentHub, 'x'));
var y = parseInt(dojo.attr(this.currentHub, 'y'));
// identify unoccupied positions around the hub
var positions = this.getPositionsAround(x, y);
var available = this.getOpenPositions(positions);
// if nothing available, move the hub and try again
// find the message with openings closest to the center
if (available.length <= this.sparseness) {
this.currentHub = this.getCentermostOpenHub();
return this.getNextPosition();
}
// randomly select from available spots.
var position = available[Math.floor(Math.random() * available.length)];
position.left = position.x * this.messageWidth;
position.top = position.y * this.messageHeight;
return position;
},
getOpenHubs: function(){
var open = [];
for (var i in this.messages) {
var message = this.messages[i];
// get grid coords of message
var x = parseInt(dojo.attr(message, 'x'));
var y = parseInt(dojo.attr(message, 'y'));
// identify unoccupied positions around message
var positions = this.getPositionsAround(x, y);
var available = this.getOpenPositions(positions);
if (available.length > this.sparseness)
open.push({i: i, x: x, y: y});
}
return open;
},
getPositionsAround: function(x, y){
return [
{x: x, y: y-1}, // above
{x: x+1, y: y}, // right
{x: x, y: y+1}, // below
{x: x-1, y: y} // left
];
},
getOpenPositions: function(positions){
var available = [];
for (var i in positions) {
var x = positions[i].x;
var y = positions[i].y;
if (!dojo.byId(this.domNode.id + '_' + x + '_' + y))
available.push(positions[i]);
}
return available;
},
getCentermostOpenHub: function(){
var openings = this.getOpenHubs();
var centermost = openings[0];
for (var i in openings) {
var opening = openings[i];
if (Math.abs(opening.x) + Math.abs(opening.y) <
Math.abs(centermost.x) + Math.abs(centermost.y)) {
centermost = opening;
}
}
return this.messages[centermost.i];
},
getNextAngle: function(){
var hub = this.currentHub;
var angle = hub ? parseInt(dojo.attr(hub, 'angle')) + 90 : 0;
angle = angle % 360;
if (this.currentAngle == (angle * -1)) {
angle += 180;
}
return angle % 360;
},
getCenterOffset: function(message, scale){
// get offset of message relative to top-left of stage
var left = parseInt(dojo.style(message, 'left'));
var top = parseInt(dojo.style(message, 'top'));
left = left + (this.messageWidth / 2);
top = top + (this.messageHeight / 2);
// account for angle of rotation.
var angle = this.currentAngle % 360;
if (angle < 0) angle += 360;
if (angle == 90 || angle == 180) {
top = top * -1;
}
if (angle == 180 || angle == 270) {
left = left * -1;
}
if ((angle / 90) % 2 != 0) {
var temp = top;
top = left;
left = temp;
}
// scale it.
left = left * scale;
top = top * scale;
// center in viewport.
var viewport = dijit.getViewport();
left = (viewport.w / 2) - left;
top = (viewport.h / 2) - top;
left = Math.round(left / scale);
top = Math.round(top / scale);
return {left: left, top: top};
},
setTransform: function(node, scale, rotate, translate){
scale = scale ? 'scale(' + scale + ')' : '';
rotate = rotate ? 'rotate(' + rotate + ')' : '';
translate = translate ? 'translate(' + translate + ')' : '';
var style = scale + ' ' + translate + ' ' + rotate;
dojo.style(node, {
webkitTransform: style,
MozTransform: style
});
},
setTransition: function(node, property, duration, easing){
easing = easing ? easing : '';
var style = property + ' ' + duration + ' ' + easing;
dojo.style(node, {
WebkitTransition: style,
MozTransition: dojo.trim(style)
});
},
truncate: function(input, length, trailing){
input = dojo.trim(input);
if (input.length > length){
output = input.substr(0, length);
if (input.charAt(length).match(/\S/) && output.match(/\s/)) {
output = output.replace(/\S+$/, '', output);
}
return dojo.trim(output) + trailing;
}
return input;
},
findMessage: function(data){
data = data instanceof Node
? new XMLSerializer().serializeToString(data)
: dojo.toJson(data);
for (var message in this.messages){
message = this.messages[message];
if (dojo.attr(message, 'data') == data)
return message;
}
},
removeOldest: function(){
var oldest = this.messages.shift();
if (oldest) {
dojo.style(oldest, 'opacity', '0');
window.setTimeout(
function(){dojo.destroy(oldest)},
this.duration.opacity
);
}
}
});