Here, I took my twitter profile picture and used @enjalot‘s image downscaling tool to reduce the picture to 50x50 pixels. I then applied Atkinson Dithering, and mapped one of my tweets (sorted chronologically) to each filled pixel.
Hover over a circle to see tweet details.
–
Built with blockbuilder.org
forked from sxywu‘s block: Image processing fun #1
forked from sxywu‘s block: Image processing fun #2
forked from sxywu‘s block: Image processing fun #3
forked from sxywu‘s block: Image processing fun #4
forked from sxywu‘s block: Image processing fun #5
forked from sxywu‘s block: Image processing fun #6
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<style>
* {
font-family: Helvetica;
}
</style>
</head>
<body>
<script>
// taken directly from nbremer's occupationcanvas code
//Generates the next color in the sequence, going from 0,0,0 to 255,255,255.
//From: https://bocoup.com/weblog/2d-picking-in-canvas
var nextCol = 1;
function genColor(){
var ret = [];
// via //stackoverflow.com/a/15804183
if(nextCol < 16777215){
ret.push(nextCol & 0xff); // R
ret.push((nextCol & 0xff00) >> 8); // G
ret.push((nextCol & 0xff0000) >> 16); // B
nextCol += 100; // This is exagerated for this example and would ordinarily be 1.
}
var col = "rgb(" + ret.join(',') + ")";
return col;
}
d3.json('twitter_profile3.json', function(image) {
d3.json('tweets.json', function(tweets) {
// some defaults for the image
var imageSize = Math.sqrt(image.length);
var scaleFactor = Math.floor(500 / imageSize);
var data = [];
var threshold = 158;
var padding = 20;
// set up canvas and hidden canvas
var canvas = d3.select('body').append('canvas')
.on('mousemove', mousemove).node();
canvas.width = imageSize * scaleFactor;
canvas.height = imageSize * scaleFactor;
var ctx = canvas.getContext('2d');
var hiddenCanvas = d3.select('body').append('canvas')
.style('display', 'none')
.node();
hiddenCanvas.width = imageSize * scaleFactor;
hiddenCanvas.height = imageSize * scaleFactor;
var hiddenCtx = hiddenCanvas.getContext('2d');
// then set up where tweet text will be shown
var tweetDiv = d3.select('body').append('div')
.style({
'width': (window.innerWidth - canvas.width - 3 * padding) + 'px',
'display': 'inline-block',
'vertical-align': 'top',
'padding': padding + 'px'
});
// first process the tweets
var minOpacity = _.min(tweets, function(tweet) {
return tweet.stats.favorites;
});
minOpacity = minOpacity.stats.favorites + 1;
var maxOpacity = _.max(tweets, function(tweet) {
return tweet.stats.favorites;
});
maxOpacity = maxOpacity.stats.favorites + 1;
var opacityScale = d3.scale.log()
.domain([minOpacity, maxOpacity])
.range([.25, 1]);
var tweetColors = {
'reply': [248,148,6], // orange
'retweet': [81,163,81], // green
'tweet': [0,136,204] // blue
};
var colToTweet = {};
tweets = _.chain(tweets)
.sortBy(function(tweet) {
tweet.date = new Date(tweet.created_at);
tweet.opacity = opacityScale(tweet.stats.favorites + 1);
if (tweet.retweet || tweet.quote) {
tweet.type = 'retweet';
} else if (tweet.in_reply_to) {
tweet.type = 'reply';
} else {
tweet.type = 'tweet';
}
// and then remember the tweet by its unique color
tweet.uniqColor = genColor();
colToTweet[tweet.uniqColor] = tweet;
return tweet.date;
}).sortBy(function(tweet, i) {
tweet.index = i;
return -tweet.date;
}).value();
// turn it grayscale first
_.each(image, function(pixel) {
data.push(Math.max(pixel[0], pixel[1], pixel[2]));
});
// Atkinson dithering
var tweetIndex = 0;
var tweetMap = {};
_.each(data, function(oldPixel, i) {
var newPixel = oldPixel > threshold ? 255 : 0;
var error = (oldPixel - newPixel) >> 3;
data[i] = newPixel;
data[i + 1] += error;
data[i + 1] += error;
data[i + imageSize - 1] += error;
data[i + imageSize] += error;
data[i + imageSize + 1] += error;
data[i + imageSize + 2] += error;
if (!newPixel) {
// if the pixel is black, then keep track of
// its corresponding tweet
tweetMap[i] = tweets[tweetIndex];
tweetIndex += 1;
}
});
data = data.slice(0, imageSize * imageSize);
drawCanvas();
function drawCanvas() {
//first clear canvas
ctx.fillStyle = "#fff";
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
_.each(data, function(pixel, i) {
var tweet = tweetMap[i];
if (tweet) {
var x = (i % imageSize) * scaleFactor + scaleFactor / 2;
var y = Math.floor(i / imageSize) * scaleFactor + scaleFactor / 2;
// first fill the visible canvas
ctx.fillStyle = 'rgba(' + tweetColors[tweet.type].join(',') +
',' + tweet.opacity + ')';
ctx.beginPath();
ctx.arc(x, y, scaleFactor * tweet.opacity, 0, 2 * Math.PI, true);
ctx.fill();
if (tweet.hovered) {
// if it's hovered, give it a stroke
ctx.strokeStyle = 'rgb(255,216,75)';
ctx.lineWidth = 3;
ctx.stroke();
}
// then the hidden canvas
hiddenCtx.fillStyle = tweet.uniqColor;
hiddenCtx.beginPath();
hiddenCtx.fillRect(x - scaleFactor / 2, y - scaleFactor / 2,
scaleFactor, scaleFactor);
}
});
}
var currentTweet;
var dateFormat = d3.time.format("%Y-%m-%d");
function mousemove() {
var col = hiddenCtx.getImageData(d3.event.offsetX, d3.event.offsetY, 1, 1).data;
var color = 'rgb(' + col[0] + "," + col[1] + ","+ col[2] + ")";
var tweet = colToTweet[color];
// we only want to re-render if hovered tweet is different from current tweet
if (tweet && (!currentTweet || tweet.id !== currentTweet.id)) {
// first clean up currentTweet (now previous tweet)
if (currentTweet) {
currentTweet.hovered = false;
}
currentTweet = tweet;
tweet.hovered = true;
drawCanvas();
var tweetString = '<strong>tweet #' + tweet.index + '</strong>: ';
tweetString += dateFormat(tweet.date) + '</br>';
tweetString += '<p>' + tweet.text + '</p>';
tweetString += '<p>' + tweet.stats.favorites + ' favorites, ' +
tweet.stats.retweets + ' retweets</p>';
tweetDiv.html(tweetString);
}
}
});
});
</script>
</body>