block by sxywu a3b6a4715c14f5d945b4

Image processing fun #7

Full Screen

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

index.html

<!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>