/** Animation of tweets from the 218th AAS meeting. **/ /* Could look at auto-reversing when hit a boundary, rather than pausing, but this is likely as annoying as pausing. */ /* Current page height/time scale values: timeScale = 0.125 page = 76 s 0.25 152 0.5 305 5.1 min 1 611 10.2 min 2 1222 20.4 min 4 2444 40.7 min 8 4888 1 hour 22 min 16 9776 2 hour 43 min 32 19552 5 hour 26 min */ final int dayLen = 24 * 3600; // final PFont twFont = createFont("Georgia", 1); // final PFont bannerFont = createFont("Georgia", 1); PFont twFont; PFont bannerFont; PFont titleFont; // PFont timeFont = titleFont; final PFont timeFont = createFont("Monospaced", 1); // title is 24 for Arial final int titleFontSize = 28; final int timeFontSize = 24; final int bannerFontSize = 18; int colIndex = 0; int[] bgColor = { 0, #cccccc }; int[] fgColor = { #cccccc, 0 }; static final int greenColor = #99cc99; static final int brownColor = #999966; Tweet[] tweets; // info on scrolling and screen placement (mapping of // times to pixels). // // time0 is used to normalize the times (ie the time values // we use are actually relative to time0). // final int time0 = 1306040400; // 2011-05-22 00:00:00 EST used for displaying the clock final int timeStart = 1306083600 - time0; // 2011-05-22 17:00:00 UTC = 2011-05-22 12:00:00 EST // final int timeEnd = 1306472400 - time0; // 2011-05-27 05:00:00 UTC = 2011-05-27 00:00:00 EST final int timeEnd = 1306472400 - (12 * 3600) - time0; // 2011-05-26 12:00:00 EST int timeY0; // y pixel correspondng to the current time (top of the display) int timeY1; // y pixel corresponding to the time that is at tbe bottom of the display float[] timeScales = { 0.125, 0.25, 0.5, 1, 2, 4, 8, 16 }; String[] screenSizes = { "76 s", "2 min 30 s", "5 min", "10 min", "20 min", "41 min", "1 hour 20 min", "2 hour 40 min" }; int timeScalePos = 3; float timeScale = timeScales[timeScalePos]; int speedBoost = 0; int currentTime = timeStart; // absolute limits on the display int minTime; int maxTime; int minDisplayTime; int maxDisplayTime; int timeStep; final int absTimeStep = 5; // base time step in seconds int timeDir = 1; // scroll down the screen to start int pageShift; int maxHeight; final int fontHeight = 14; // have a smaller value for re-tweets? PImage bgImg; // set here rather than dynamically calculated since it is used in // the setup call. final int titleBorder = titleFontSize + 4; // + 4; // used to place items within the title border int[] titleBorderRange = { 4, titleBorder - 4 }; // width of the border used to display the name of the Tweeter final int nameBorder = 140; // state variables boolean animate = true; boolean showBackground = true; boolean showHelp = true; boolean showCredits = false; boolean redraw = false; // testing/development // middle of the "tweet display" area int centerX; int centerY; // width and height of the box used to display the help int helpWidth; int helpHeight; // credits int creditWidth; int creditHeight; final int histogramBorder = 50; /* width = 1024 + nameBorder = 1164 height = 612 + titleBorder + histogramBorder 694 */ void setup() { size(1164, 694); titleFont = loadFont("GentiumPlus-Italic-28.vlw"); twFont = loadFont("GentiumPlus-14.vlw"); bannerFont = loadFont("GentiumPlus-18.vlw"); colorMode(RGB, 255,255,255,1); maxHeight = height + fontHeight - histogramBorder; centerX = 1024/2 + nameBorder; centerY = 612/2 + titleBorder; timeY0 = titleBorder + 1; timeY1 = height - histogramBorder - 1; setupHelp(); setupCredits(); textFont(twFont, fontHeight); loadTweets(); setupHistogram(); colorMode(RGB, 255,255,255,1); // needed? bgImg = loadImage("boston_skyline.jpg"); createTimeStep(); calcPageShift(); setCurrentTime(timeStart); //String[] fl = PFont.list(); //println("Fonts:"); //println(fl); //frameRate(30); //frameRate(20); // frameRate(25); } void draw() { //background(bgColor[colIndex]); background(brownColor); rectMode(CORNERS); noStroke(); fill(0,0.1); rect(0, titleBorder, nameBorder, height - histogramBorder); if (showBackground) { image(bgImg,nameBorder,titleBorder); } // hack/useful? if (redraw) { adjustY(); redraw = false; } // could optimize this indicateStart(); indicateEnd(); for (int i = 0; i < tweets.length; i++) { tweets[i].display(fgColor[colIndex]); } displayTitle(); // displayTime(currentTime); displayDirection(); displayTimeScale(); displayHistogram(); displayTime(currentTime); if (showCredits) { showCredits(); } else if (showHelp) { showHelp(); } if (animate) { // we normalize by the time scale factor here to ensure a // sensible animation speed is retained. /* int dtime = (int) (timeStep * timeScale); if (abs(dtime) < 1) { dtime = timeDir * 1; } // if timeStep were float this would not be needed */ float dtime = timeStep * timeScale; adjustCurrentTime(dtime); } } void keyPressed() { switch (key) { case 'z': decreaseTimeScale(); // animate = false; redraw = true; break; case 'Z': increaseTimeScale(); // animate = false; redraw = true; break; case 'b': showBackground = ! showBackground; break; case 'p': animate = ! animate; break; case 'h': showHelp = ! showHelp; break; case 'c': showCredits = ! showCredits; break; case 's': setCurrentTime(minDisplayTime); break; case 'e': setCurrentTime(maxDisplayTime); break; case 'j': adjustCurrentTime(timeDir * pageShift); break; case 'd': adjustCurrentTime(timeDir * dayLen); break; case '+': increaseSpeed(); break; case '-': decreaseSpeed(); break; case 'r': reverseDirection(); break; case 'i': invertColor(); break; case 'm': recreate_user_colors(); redraw = true; break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': adjustCurrentTime(timeDir * (key - '0') * 3600); break; default: if (key == CODED) { if (keyCode == UP) { adjustCurrentTime(pageShift); } else if (keyCode == DOWN) { adjustCurrentTime(-pageShift); } } } } // The pixel height of the pre- and post- tweet area int preOffset; int postOffset; void loadTweets() { String[] intext = loadStrings("aas.tweets"); int n = intext.length; tweets = new Tweet[n]; for (int i = 0; i < n; i++) { tweets[i] = new Tweet(intext[i]); } // we don't want to display outside the data range, but // this is complicated by the fact that the "pre/post" tweets // are just offsets from the start/end time. // preOffset = (get_nearly() + 2) * get_spacing(); postOffset = (get_nlate() + 2) * get_spacing(); // println("Early tweets = " + get_nearly()); // println("Late tweets = " + get_nlate()); calcDisplayRange(); //println("Max width of usernames = " + maxNameWidth); } String[] helpText = { "Help", "", "h toggle this help", "b toggle the background image", "p pause or restart the animation", // "j jump 2/3 of a page in the scroll direction", "1-9 jump this number of hours in the scroll direction", "d jump one day in the scroll direction", "s/e jump to the start or end of the Tweet collection", "r reverse the scroll direction", "z/Z decrease or increase the time range displayed in one page", "+/- increase or decrease the scroll speed", "i toggle the tweet color between white and black", "m randomize the colors used to display the user name", "c show the credits", "", "Select a point with the mouse on the histogram to jump to that time and", "the page up or down keys will jump a page ahead or backwards. Within the", "main display, click and drag with the mouse will scroll the display area;", "you may want to pause the animation whilst doing this!" }; int[] helpY = new int[helpText.length]; int helpX; String[] creditText = { "Credits", "", "The tweets were extracted using the Twitter search api using", "https://bitbucket.org/doug_burke/grabtweets and the animation", "was created using Processing (http://processing.org/). The fonts", "used for the title and Tweets are from the Gentium typeface.", "", "The background image is used by permission of @johnamatson,", "and a big thank you to all those who tweeted from the meeting.", "", "This visualization was created by Doug Burke (dburke@cfa.harvard.edu)." }; int[] creditY = new int[creditText.length]; int creditX; void showHelp() { showBanner(helpText, helpWidth, helpHeight, helpX, helpY); } void showCredits() { showBanner(creditText, creditWidth, creditHeight, creditX, creditY); } void showBanner(String[] txt, int w, int h, int x, int[] y) { fill(fgColor[colIndex], 0.8); rectMode(CENTER); stroke(#ff0000); rect(centerX, centerY, w, h); noStroke(); textFont(bannerFont, bannerFontSize); textAlign(CENTER); fill(bgColor[colIndex]); for (int i = 0; i < txt.length; i++) { text(txt[i], x, y[i]); } } // TODO: refactor setupHelp and setupCredits void setupHelp() { textFont(bannerFont, bannerFontSize); helpWidth = 0; float h = textAscent() + textDescent(); float l = 0; // 0.5 * h; int dy = (int) (h + l); int delta = dy * helpText.length; helpHeight = (int) (delta * 1.1); int y0 = centerY + (dy - delta) / 2; for (int i = 0; i < helpText.length; i++) { String txt = helpText[i]; int w = (int) textWidth(txt); if (w > helpWidth) { helpWidth = w; } helpY[i] = y0; y0 += dy; } helpWidth = (int) (1.1 * helpWidth); helpX = centerX; } void setupCredits() { textFont(bannerFont, bannerFontSize); creditWidth = 0; float h = textAscent() + textDescent(); float l = 0.5 * h; int dy = (int) (h + l); int delta = dy * creditText.length; creditHeight = (int) (delta * 1.1); int y0 = centerY + (dy - delta) / 2; for (int i = 0; i < creditText.length; i++) { String txt = creditText[i]; int w = (int) textWidth(txt); if (w > creditWidth) { creditWidth = w; } creditY[i] = y0; y0 += dy; } creditWidth = (int) (1.1 * creditWidth); creditX = centerX; } /* * Calculate the y position for the tweet given the * current time, which corresponds to the * top of the screen/timeY0 (i.e. those tweets that are * just appearing when using forward scrolling). */ int timeToPixel(int tweetTime) { int dt = currentTime - tweetTime; return timeY0 + (int) (dt * 1.0 / timeScale); } /* * Convert a pixel value (Y) to a time. */ int pixelToTime(int y) { return currentTime - pixelRangeToTimeRange(y - timeY0); } /* * Convert a range, in pixels, to a time range, in seconds. */ int pixelRangeToTimeRange(int delta) { return (int) (timeScale * delta); } /* * Adjust the position of all the tweets to match the * current time value (that is, the time at the bottom * of the screen). */ void adjustY() { for (int i = 0; i < tweets.length; i++) { tweets[i].adjust(); } } /* * Display the supplied time at the top-left of the display. */ // can't be bothered with printf and assumes 0 <= num < 100 String fNum(int num) { if (num == 0) { return "00"; } else if (num < 10) { return "0" + num; } else { return Integer.toString(num); } } String[] dayName = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri" }; void displayTitle() { // fill(0); fill(greenColor); rectMode(CORNER); noStroke(); rect(0,0,width,titleBorder); // fill(#ffffff); // fill(brownColor); fill(0); textFont(titleFont, titleFontSize); textAlign(RIGHT); text("Tweets from the 218th meeting of the American Astronomical Association", width-5, titleBorder - 8); //text(timeScale, width-5, titleBorderRange[1]); } void displayTime(int time) { String display; if (time < timeStart) { display = "Before"; } else if (time > timeEnd) { display = "After"; } else { // take advantage of time0 being midnight on Sunday morning int dt = time; // - time0; int dayNum = dt / dayLen; int offset = dt % dayLen; int hourNum = offset / 3600; int offset2 = offset % 3600; int minNum = offset2 / 60; int secNum = offset2 % 60; // display = dayName[dayNum] + " " + fNum(hourNum) + ":" + fNum(minNum) + ":" + fNum(secNum); //display = fNum(hourNum) + ":" + fNum(minNum) + ":" + fNum(secNum); display = dayName[dayNum] + "\n" + fNum(hourNum) + ":" + fNum(minNum) + ":" + fNum(secNum); } // fill(#ffffff); fill(#666666); textAlign(CENTER); textFont(timeFont, timeFontSize); textLeading(timeFontSize); // text(display, nameBorder / 2, height - 4); text(display, nameBorder / 2, height - timeFontSize - 5); } /* * Draw a triangle representing the direction that the * tweets are moving. * * If animate is not false then we convert into a "pause" * symbol. */ void drawTriangle(int xc, int hw) { int y1 = titleBorderRange[0]; int y3 = titleBorderRange[1]; int y2 = (y1 + y3) / 2; triangle(xc - hw, y1, xc + hw, y2, xc - hw, y3); /* if (! animate) { fill(0); rectMode(CORNERS); // rect(xc - 3 * hw / 4, y1, xc - hw / 2, y3); // rect(xc + 3 * hw / 4, y1, xc - 3 * hw / 4, y3); rect(xc, y1, xc - hw / 2, y3); } */ } void drawPause(int xc, int hw) { int y1 = titleBorderRange[0]; int y3 = titleBorderRange[1]; rectMode(CORNERS); rect(xc - hw, y1, xc - 2, y3); rect(xc + hw, y1, xc + 2, y3); } void displayDirection() { // int xc = 185; int xc = 15; int hw = 10; // fill(#cccccc); // fill(brownColor); fill(#666666); if (animate) { int sx = hw * 2 + 3; if (timeDir < 0) { hw *= -1; } for (int i = 0; i <= speedBoost; i++ ) { drawTriangle(xc, hw); xc += sx; } } else { drawPause(xc, hw); } } // draw a box indicating that this region is "outside" // the main time range of the display. // // due to the way the routine is called, we only consider // it valid if ymax > ymin. // void indicateRange(int ymin, int ymax) { if (ymax > ymin && ymin <= maxHeight && ymax > titleBorder) { fill(#333399, 0.6); stroke(#333399); rectMode(CORNERS); rect(nameBorder, ymin, width, ymax); } } /* * draw a line at the given time, if visible, * where the time has already been converted to a pixel. * * We also add in a label in the middle. * * TODO: could add a line of text in the middle to * say something like "start of conference" or something * to provide some indication to the user what it is all about. */ void indicateTime(int y, String lbl) { if (y >= titleBorder && y <= maxHeight) { stroke(#ffffff); strokeWeight(4); line(nameBorder + 1, y, width, y); strokeWeight(1); noStroke(); textAlign(CENTER); textFont(titleFont, titleFontSize); int w = (int) textWidth(lbl); float hd = textDescent(); float ha = textAscent(); int h = (int) (hd + ha); fill(#ffffff); rectMode(CENTER); rect(centerX, y, w + 14, h); fill(0); text(lbl, centerX, y + (int) ((ha - hd) / 2)); } } void indicateStart() { int y = timeToPixel(timeStart); indicateRange(y, timeY1); indicateTime(y, "Early Tweets"); } void indicateEnd() { int y = timeToPixel(timeEnd); indicateRange(timeY0, y); indicateTime(y, "Late Tweets"); } // change the current time by delta seconds, // constrained to lie within the display range. // void adjustCurrentTime(int delta) { setCurrentTime(currentTime + delta); } void adjustCurrentTime(float delta) { setCurrentTime(int(currentTime + delta + 0.5)); } // change the current time to the given value, // which is constrained to lie within the 'valid' range // defined by minTime and maxTime. // // It also changes the histogram display range. // void setCurrentTime(int time) { currentTime = constrain(time, minDisplayTime, maxDisplayTime); recenterHistogram(currentTime); adjustY(); if ((currentTime == minDisplayTime && timeDir < 0) || (currentTime == maxDisplayTime && timeDir > 0)) { animate = false; } } /* update the time step after one of the componnts has been changed */ void createTimeStep() { timeStep = timeDir * (2 << speedBoost) * absTimeStep / 2; } void reverseDirection() { timeDir *= -1; createTimeStep(); // if at start or end and paused, then have to reverse direction // and unpause, which is annoying. so try this. animate = true; } void increaseSpeed() { if (animate) { speedBoost += 1; if (speedBoost > 3) { speedBoost = 3; } } else { animate = true; } createTimeStep(); } void decreaseSpeed() { speedBoost -= 1; if (speedBoost < 0) { speedBoost = 0; animate = false; } createTimeStep(); } /* * Swap the colors used for drawing the tweet text and * background area. */ void invertColor() { if (colIndex == 0) { colIndex = 1; } else { colIndex = 0; } } /* * Set up the page shift (two thirds of a page); this depends on the * current time-to-pixel scaling so needs to be updated whenever that * changes (or, perhaps, we just ignore this as a pre-calculated * variable?) */ void calcPageShift() { pageShift = (int) ((currentTime - pixelToTime(timeY1)) * 2.0 / 3.0); } /* * Calculate the minimum and maximum times that can be displayed given * the data and current scaling. This needs to be called whenever * timeScale is updated (at least it does with the current code). */ void calcDisplayRange() { minTime = timeStart - pixelRangeToTimeRange(preOffset); maxTime = timeEnd + pixelRangeToTimeRange(postOffset); int pageTime = pixelRangeToTimeRange(timeY1 - timeY0); // println("pageTime = " + pageTime); minDisplayTime = minTime + pageTime; maxDisplayTime = maxTime - pageTime; maxDisplayTime = maxTime; } /* * Change the scaling between pixels and seconds given the * scale bounds. The delta parameter is used to adjust the * timmeScalePos counter, so is assumed to be -1 or +1. * * We also adjust the current time to ensure that the mid-point * of the display is invariant. */ void setTimeScale(int delta) { int ymid = (timeY1 + timeY0) / 2; int tmid = pixelToTime(ymid); timeScalePos = constrain(timeScalePos + delta, 0, timeScales.length - 1); timeScale = timeScales[timeScalePos]; adjustCurrentTime(tmid - pixelToTime(ymid)); calcPageShift(); calcDisplayRange(); redraw = true; } void decreaseTimeScale() { setTimeScale(-1); } void increaseTimeScale() { setTimeScale(+1); } /* * Can I adaquately show the time-scale? */ void displayTimeScale() { int xs = nameBorder + 1; int dx = 5; int y1 = titleBorderRange[0]; int y3 = titleBorderRange[1]; int y2 = (y1 + y3) / 2; rectMode(CORNERS); stroke(#666666); for (int i = 0; i < timeScales.length; i++) { int xlo = xs + i * 2 * dx; if (i <= timeScalePos) { fill(#666666, 0.8); } else { fill(#666666, 0.2); } rect(xlo, y1, xlo + dx, y2); } int xm = xs + (timeScales.length - 1) * dx + dx / 2; textAlign(CENTER); textFont(timeFont, 12); fill(#666666); text(screenSizes[timeScalePos], xm, y3); } void mouseDragged() { if (mouseX > nameBorder && mouseY >= timeY0 && mouseY <= timeY1) { int ydelta = mouseY - pmouseY; adjustCurrentTime(int(ydelta / timeScale)); } /* * the limited range of timescales means this needs to be smarter * to be useful if (mouseX > nameBorder && mouseY >= timeY0 && mouseY <= timeY1) { int ydelta = mouseY - pmouseY; if (ydelta < 0) { decreaseTimeScale(); } else if (ydelta > 0) { increaseTimeScale(); } } * */ } void mousePressed() { if (mouseY > (height - histogramBorder) && mouseX > nameBorder) { int ntime = histXToTime(mouseX); setCurrentTime(ntime); } }