// recentevents
// ed burton, 2003

// Recentevents dissolves three texts into a fluid suspension
// of letter tokens. The dissolved texts are drawn from a live source, 
// updated on the hour, every hour. In their gathering stream, 
// these tokens grow sticky tendrils towards potential neighbours,
// coagulating to form clots of recovered text. Popular clots 
// finding numerous neighbours grow spidery forked tendrils 
// as they compete to coalesce. 

// Instructions: If you wish, you may drag the clots to help
// or hinder their progress. Allow up to a quarter of an hour
// for coagulation to complete. Refresh or revisit the piece
// after an hour for more recent events. 

// "bad news comes in threes"

static final int numberOfEvents=3;
String allEvents;

TokenList tokens;
GlyphList glyphs;
GlyphList glyphQueue;

float viscosity=0.85f;
float flowSpeed=0.5f;
float flowPressure=0.1f;
float repulsivefForceRadius;
float externalForce=0.2f;
float internalForce=0.4f;
float tendrilTension=0.9f;
float verticalTendrilBias=4.0f;

int injectionFrequency=50;
int maxGlyphsActive=60;
float glyphIncubationDuration=3000;
int frameCount;

BFont font;
int fontSize=13;
int tokenHeight=fontSize;
int tokenTailHeight=fontSize/4;

Glyph draggedGlyph=null;
int mouseDragDx,mouseDragDy;
int draggedCount;

void setup() {
  size(200,600);
  repulsivefForceRadius=(min(width,height))/4;
  background(#ffffff);
  fill(#000000);
  font = loadFont("OCR-B.vlw.gz");
  setFont(font, fontSize);
  hint(SMOOTH_IMAGES);
  getRecentEvents();
}

void getRecentEvents() {
  String recentEvents="";
  try {
    recentEvents=readRecentEvents("news.xml"); // rss (rich site summary) news feed format
  } catch (Exception e1) {
    println("input error: "+e1);
    try {
      recentEvents=readRecentEvents("oldnews.xml"); // backup incase of filelock during update
    }catch (Exception e2) {
      println("backup input error: "+e2);
    }
  }
  parseRecentEvents(recentEvents);
}

String readRecentEvents(String filename) throws IOException {
  String lines="";
  InputStream input = loadStream(filename);
  InputStreamReader isr = new InputStreamReader(input);
  BufferedReader reader = new BufferedReader(isr);
  String line;
  while ((line = reader.readLine()) != null) {
    lines+=line;
  }
  return lines;
}

void parseRecentEvents(String lines) {
  int n=0;
  int i=0;
  allEvents="";
  while (n<numberOfEvents && i>=0 && i<lines.length()) {
    int i_start=i=lines.indexOf("<item>",i);
    int i_end=lines.indexOf("</item>",i_start);
    if (i_start>=0 && i_end>i_start ) {
      int d_start=i=lines.indexOf("<description>",i_start);
      if (d_start>i_start && d_start<i_end) {
        int d_end=i=lines.indexOf("</description>",d_start);
        if (d_end<i_end) {
          String event=lines.substring(d_start+"<description>".length(),d_end);;
          event.trim();
          allEvents+=event+'\n';
          n++;
        }
      }
    }
  }
  for (i=n;i<numberOfEvents;i++) {
    allEvents+="No news is good news.\n";
  }
}

void loop() {
  if (glyphs==null) {
    seedTokensAndGlyphs();
  }
  if (glyphQueue.count>0 && frameCount++%injectionFrequency==0 && glyphs.count<maxGlyphsActive) {
    injectNewGlyph();
  }

  applyFlow();

  applyRepulsion();

  updatePositions();

  applyAttraction();

  drawGlyphs();

  if (draggedGlyph!=null) {
    draggedCount++;
  }
}

void mousePressed() {
  draggedGlyph=null;
  for (int i=0;i<glyphs.count && draggedGlyph==null;i++) {
    if (glyphs.glyph(i).contains(mouseX,mouseY)){
      draggedGlyph=glyphs.glyph(i);
      draggedGlyph.x=floor(draggedGlyph.x+0.5);
      draggedGlyph.y=floor(draggedGlyph.y+0.5);
      mouseDragDx=(int)(mouseX-draggedGlyph.x);
      mouseDragDy=(int)(mouseY-draggedGlyph.y);
      draggedCount=0;
    }
  }

}

void mouseDragged() {
  if (draggedGlyph!=null) {
    draggedGlyph.x=mouseX-mouseDragDx;
    draggedGlyph.y=mouseY-mouseDragDy;
    draggedGlyph.xv=(mouseX-pmouseX)/max(1.0,draggedCount);
    draggedGlyph.yv=(mouseY-pmouseY)/max(1.0,draggedCount);
    draggedGlyph.updateBounds();
  }
  draggedCount=0;
}

void mouseReleased() {
  draggedGlyph=null;
}

void drawGlyphs() {
  for (int i=0;i<glyphs.count;i++) {
    glyphs.glyph(i).drawUnderline();
  }
  for (int i=0;i<glyphs.count;i++) {
    glyphs.glyph(i).drawText();
  }
}

void seedTokensAndGlyphs() {
  glyphs=new GlyphList();
  tokens=new TokenList();

  glyphQueue=new GlyphList();
  for (int i=0;i<allEvents.length();i++) {
    String newText=allEvents.charAt(i)+"";
    if (!newText.equals("\n")) {
      Token token=tokens.findToken(newText);
      if (token==null) {
        token=new Token(newText);
        token.initialise();
        tokens.add(token);
      }
      Glyph glyph=new Glyph(token,i,i);
      glyph.x=random(0,width);
      glyph.y=random(0,height);
      if (random(2)<1)
      glyph.y+=height;
      else
      glyph.y-=height;
      glyph.updateBounds();
      glyphQueue.addAtRandomPosition(glyph);
    }
  }
}

void injectNewGlyph() {
  Glyph glyph=glyphQueue.glyph(glyphQueue.count-1);
  glyphs.add(glyph);
  glyphQueue.deleteIndex(glyphQueue.count-1);
}

void applyFlow() {
  for (int i=0;i<glyphs.count;i++) {
    if (draggedGlyph!=glyphs.glyph(i)) {
      float peripheralFactor=(abs((glyphs.glyph(i).bounds[0].y+glyphs.glyph(i).bounds[0].h/2)
      -(height/2.0)))/(height/2.0);
      glyphs.glyph(i).xv+=flowSpeed*(peripheralFactor-1);
      glyphs.glyph(i).yv+=peripheralFactor*flowPressure*(glyphs.glyph(i).y<height/2?1:-1);
    }
  }
}

void applyRepulsion() {
  for (int i1=0;i1<glyphs.count;i1++) {
    for (int i2=i1+1;i2<glyphs.count;i2++) {
      Vector greatestForceVector=new Vector();
      float greatestMagnitude=0;
      for (int r1=0;r1<3;r1++) {
        if (glyphs.glyph(i1).bounds[r1]!=null ) {
          for (int r2=0;r2<3;r2++) {
            if (glyphs.glyph(i2).bounds[r2]!=null ) {
              Vector forceVector=new Vector();
              float magnitude=glyphs.glyph(i1).bounds[r1].forceMagnitudeTo(glyphs.glyph(i2).bounds[r2], repulsivefForceRadius, externalForce,  internalForce, forceVector);
              if (magnitude!=0 && (greatestMagnitude==0 || magnitude>greatestMagnitude )) {
                greatestMagnitude=magnitude;
                greatestForceVector.dx=forceVector.dx;
                greatestForceVector.dy=forceVector.dy;
              }
            }
          }
        }
      }
      if (greatestMagnitude!=0) {
        if (glyphs.glyph(i1)!=draggedGlyph) {
          glyphs.glyph(i1).xv-=greatestForceVector.dx;
          glyphs.glyph(i1).yv-=greatestForceVector.dy;
        }
        if (glyphs.glyph(i2)!=draggedGlyph) {
          glyphs.glyph(i2).xv+=greatestForceVector.dx;
          glyphs.glyph(i2).yv+=greatestForceVector.dy;
        }
      }
    }
  }
}

void applyAttraction() {
  for (int i1=0;i1<glyphs.count;i1++) {
    for (int i2=0;i2<glyphs.count;i2++) {
      if (i1!=i2) {
        if (glyphs.glyph(i1).castAttractiveTendrilTo(glyphs.glyph(i2))) {
          i2--;
        }
      }
    }
  }
}

void updatePositions() {
  for (int i=0;i<glyphs.count;i++) {
    if (draggedGlyph!=glyphs.glyph(i)) {
      glyphs.glyph(i).xv*=viscosity;
      glyphs.glyph(i).yv*=viscosity;
      glyphs.glyph(i).x+=glyphs.glyph(i).xv;
      glyphs.glyph(i).y+=glyphs.glyph(i).yv;
      glyphs.glyph(i).updateBounds();
    }
  }
}

void deleteGlyph(Glyph glyph) {
  glyph.token.removeGlyphInstance(glyph);
  glyphs.delete(glyph);
}

float distance(float x1, float y1, float x2, float y2) {
  return sqrt(sq(x1-x2) + sq(y1-y2));
}

class Token {
  String text;
  float tokenWidth;
  GlyphList glyphInstances;

  Token(String t_in) {
    text=t_in;
    glyphInstances=new GlyphList();
  }

  void initialise() {
    calculateWidth();
  }

  void calculateWidth() {
    tokenWidth=0;
    for (int c=0;c<text.length();c++) {
      tokenWidth+=font.charWidth(text.charAt(c));
    }
  }

  void removeGlyphInstance(Glyph glyph) {
    glyphInstances.delete(glyph);
    if (glyphInstances.count<=0)
    tokens.delete(this);
  }
}

class Glyph {
  Token token;
  float x,y,xv,yv;

  int startEventIndex;
  int endEventIndex;
  float lifeSpan;

  GlyphRectangle[] bounds=new GlyphRectangle[3];

  Glyph (Token token,int startEventIndex,int endEventIndex) {
    this.token=token;
    this.startEventIndex=startEventIndex;
    this.endEventIndex=endEventIndex;
    token.glyphInstances.add(this);
  }

  Glyph isAPotentialPartner(Glyph glyph) {
    for (int i=0;i<glyph.token.glyphInstances.count;i++) {
      if ((endEventIndex+1) == glyph.token.glyphInstances.glyph(i).startEventIndex ) {
        return glyph.token.glyphInstances.glyph(i);
      }
    }
    return null;
  }

  boolean castAttractiveTendrilTo(Glyph glyph) {
    Glyph equivalentPartner=null;
    if ((equivalentPartner=isAPotentialPartner(glyph))==null) {
      return false;
    }

    stroke(#ff0000);
    float tx=x+width;
    float ty=y;
    do{
      tx-=width;
      ty+=tokenHeight;
    } while (tx+token.tokenWidth>width) ;
    tx+=token.tokenWidth;

    float nx=glyph.x;
    float ny=glyph.y+tokenHeight;

    float txx=tx;
    float tyy=ty;

    if (abs(nx-tx)>=width/2) {
      if (txx<nx) {
        while (abs(txx-nx)>width/2){
          txx+=width;
          tyy-=tokenHeight;
        }
      }else {
        while (abs(txx-nx)>width/2){
          txx-=width;
          tyy+=tokenHeight;
        }
      }

    }

    float d=distance(txx,tyy/verticalTendrilBias,nx,ny/verticalTendrilBias);
    if (d>(width/2.0f)) {
      return false;
    }
    if (d<=1/verticalTendrilBias) {
      if (equivalentPartner!=glyph)
      glyph.exchangeMeaningWith(equivalentPartner);
      mergeWithPartner(glyph);
      return true;
    }

    float urgency = tendrilTension*(((min(lifeSpan,glyphIncubationDuration)/glyphIncubationDuration)
    *(min(glyph.lifeSpan,glyphIncubationDuration)/glyphIncubationDuration)));

    float f=1.0f-(d/(width/2.0f));

    float xf=f*((txx-nx)/d)*urgency ;
    float yf=f*(((tyy-ny)/verticalTendrilBias)/d)*urgency;

    if (this!=draggedGlyph) {
      xv-=xf;
      yv-=yf;
    }
    if (glyph!=draggedGlyph) {
      glyph.xv+=xf;
      glyph.yv+=yf;
    }

    stroke(#ff0000);
    brokenBezier(txx,tyy,nx,ny,f);
    return false;
  }

  void mergeWithPartner(Glyph glyph) {
    String newText=token.text+glyph.token.text;
    endEventIndex=glyph.endEventIndex;
    token.removeGlyphInstance(this);
    token=tokens.findToken(newText);
    if (token==null) {
      token=new Token(newText);
      token.initialise();
      tokens.add(token);
    }
    token.glyphInstances.add(this);
    lifeSpan=(lifeSpan+glyph.lifeSpan)/2;
    xv=(xv+glyph.xv)/2;
    yv=(yv+glyph.yv)/2;
    if (glyph==draggedGlyph) {
      draggedGlyph=this;
      mouseDragDx=(int)(mouseX-draggedGlyph.x);
      mouseDragDy=(int)(mouseY-draggedGlyph.y);
    }
    deleteGlyph(glyph);
  }

  void exchangeMeaningWith(Glyph glyph){
    int temporaryStartEventIndex=startEventIndex;
    int temporaryEndEventIndex=endEventIndex;

    startEventIndex=glyph.startEventIndex;
    endEventIndex=glyph.endEventIndex;

    glyph.startEventIndex=temporaryStartEventIndex;
    glyph.endEventIndex=temporaryEndEventIndex;
  }

  void brokenBezier(float tx,float ty, float nx, float ny, float f) {
    f=(sq(f))/2;//min(f,0.5);
    float cw=(abs(ty-ny)+abs(tx-nx))/2;
    float txc=tx+cw;
    float nxc=nx-cw;

    int steps=(int)ceil((f*cw));
    float limit=f;
    float step=limit/steps;
    float nixx=nx;
    float tixx=tx;
    float niyy=ny;
    float tiyy=ty;
    float text=step;
    for (int c=0;c<steps;c++, text+=step) {
      float nt=1-text;

      float b0=nt*nt*nt;
      float b1=3*text*(nt*nt);
      float b2=3*text*text*nt;
      float b3=text*text*text;

      float nix=b0*nx+b1*nxc+b2*txc+b3*tx;
      float tix=b0*tx+b1*txc+b2*nxc+b3*nx;
      float niy=b0*ny+b1*ny+b2*ty+b3*ty;
      float tiy=b0*ty+b1*ty+b2*ny+b3*ny;

      wrappingLine (tix,tiy,tixx,tiyy);
      wrappingLine (nix,niy,nixx,niyy);
      tixx=tix;
      tiyy=tiy;
      nixx=nix;
      niyy=niy;
    }
  }

  void updateBounds() {
    while (x<0) {
      x+=width;
      y-=tokenHeight;
    }
    while (x>width) {
      x-=width;
      y+=tokenHeight;
    }

  if (bounds[0]==null) { bounds[0]=new GlyphRectangle(x,y,width>(x+token.tokenWidth)?token.tokenWidth:width-x,tokenHeight);}
  else {bounds[0].setBounds(x,y,width>(x+token.tokenWidth)?token.tokenWidth:width-x,tokenHeight);}

    if (width<(x+token.tokenWidth)) {
      if (token.tokenWidth-x>width) {
        float lines=floor((token.tokenWidth-(width-x))/width);
        if (bounds[1]==null) {
          bounds[1]=new GlyphRectangle(0.0,y+tokenHeight,width,tokenHeight*lines);
        } else {
          bounds[1].setBounds(0.0,y+tokenHeight,width,tokenHeight*lines);
        }

        if (bounds[2]==null) {
          bounds[2]=new GlyphRectangle(0.0,y+tokenHeight+bounds[1].h,(token.tokenWidth-(width-x))-((width*lines)),tokenHeight);
        } else {
        bounds[2].setBounds(0.0,y+tokenHeight+bounds[1].h,(token.tokenWidth-(width-x))-((width*lines)),tokenHeight);}
        bounds[0].h+=bounds[1].h;
        bounds[2].h+=bounds[1].h;
        bounds[2].y-=bounds[1].h;
        if (bounds[2].w<bounds[0].x) {
          bounds[1].x+=bounds[2].w;
          bounds[1].w-=(bounds[0].w+bounds[2].w);
        } else {
          float temp=bounds[2].w;
          bounds[2].w=bounds[0].x;
          bounds[0].x=temp;
          bounds[0].w=width-temp;

          bounds[1].x=bounds[2].w;
          bounds[1].w=bounds[0].x-bounds[1].x;
          bounds[1].y-=tokenHeight;
          bounds[1].h+=tokenHeight*2;
        }
      } else {
        bounds[1]=null;
        if (bounds[2]==null) {
          bounds[2]=new GlyphRectangle(0,y+tokenHeight,x-(width-token.tokenWidth),tokenHeight);
        } else {
          bounds[2].setBounds(0.0,y+tokenHeight,x-(width-token.tokenWidth),tokenHeight);
        }
      }
    } else {
      bounds[1]=null;
      bounds[2]=null;
    }
  }

  void drawText() {
    lifeSpan++;
    float tx=x+width;
    float ty=(y)-tokenTailHeight;
    do{
      tx-=width;
      ty+=tokenHeight;
      text(token.text, tx,ty);
    } while (tx+token.tokenWidth>width) ;
  }

  void drawUnderline() {
    stroke(#ff0000);
    float tx=x+width;
    float ty=y;
    do{
      tx-=width;
      ty+=tokenHeight;
      line(tx,ty,tx+token.tokenWidth,ty);
    } while (tx+token.tokenWidth>width) ;
  }

  void wrappingLine(float x1,float y1, float x2,float y2) {
    if (x2>0 || x1>0 || x2<width || x1<width) {
      line (x1,y1,x2,y2);
    }
    if (x2>width || x1>width) {
      line (x1-width,y1+tokenHeight,x2-width,y2+tokenHeight);
    }
    if (x2<0 || x1<0) {
      line (x1+width,y1-tokenHeight,x2+width,y2-tokenHeight);
    }
  }

  boolean contains(int x, int y) {
    for (int i=0;i<bounds.length;i++) {
      if (bounds[i]!=null) {
        if (bounds[i].contains(x,y)) {
          return true;
        }
      }
    }
    return false;
  }

  float interpolate(float a, float b, float text) {
    return a*(1-text)+b*text;
  }
}

class ObjectList {
  protected Object[] data;
  int count;
  private static final int initialListCapacity=32;

  ObjectList() {
    data=new Object[initialListCapacity];
  }

  void add(Object o) {
    if (count == data.length) {
      Object temp[] = new Object[count*2];
      System.arraycopy(data, 0, temp, 0, count);
      data = temp;
    }
    data[count++] = o;
  }

  void delete(Object o) {
    int index=index(o);
    if (index>=0) {
      deleteIndex(index);
    }
  }

  void deleteIndex(int index) {
    if (index<count-1) {
      System.arraycopy(data, index+1, data, index, (count-index)-1);
    }
    count--;
  }

  int index(Object o) {
    for (int i=0;i<count;i++) {
      if (data[i]==o) {
        return i;
      }
    }
    return -1;
  }
}

class GlyphList extends ObjectList{

  GlyphList() {

  }

  Glyph glyph(int index) {
    return (Glyph)data[index];
  }

  void addAtRandomPosition(Object o) {
    int insertAt=(int)random(count);
    Object temp[];
    if (count == data.length) {
      temp = new Object[count*2];
    } else {
      temp = new Object[data.length];
    }
    if (insertAt>0) {
      System.arraycopy(data, 0, temp, 0, insertAt);
    }
    if (insertAt<count) {
      System.arraycopy(data, insertAt, temp, insertAt+1, count-insertAt);
    }
    data = temp;

    data[insertAt] = o;
    count++;
  }
}

class TokenList extends ObjectList{

  TokenList() {

  }

  Token token(int index) {
    return (Token)data[index];
  }

  Token findToken(String text) {
    for (int i=0;i<count;i++) {
      if (text.equals(token(i).text)) {
        return (Token)data[i];
      }
    }
    return null;
  }
}

class GlyphRectangle {
  float x,y,w,h;
  GlyphRectangle(float x,float y,float w,float h) {
    this.x=x;
    this.y=y;
    this.h=h;
    this.w=w;
  }

  void setBounds(float x,float y,float w,float h) {
    this.x=x;
    this.y=y;
    this.h=h;
    this.w=w;
  }

  boolean contains(int x, int y) {
    return (x>=this.x && x<=this.x+this.w && y>=this.y && y<=this.y+this.h);
  }

  boolean contains(float x, float y) {
    return (x>=this.x && x<=this.x+this.w && y>=this.y && y<=this.y+this.h);
  }

  public boolean intersects(GlyphRectangle r) {
    return !((r.x + r.w <= x) ||
    (r.y + r.h <= y) ||
    (r.x >= x + w) ||
    (r.y >= y + h));
  }

  float forceMagnitudeTo(GlyphRectangle r_in, float outsideRadius, float outsideForce, float insideForce, Vector resultVector) {
    if (intersects(r_in)) {
      float up=(r_in.y+r_in.h)-y;
      float down=(y+h)-r_in.y;
      if (up<down ) {
        resultVector.dy=-(outsideForce+(up/max(h,r_in.h))*insideForce);
        return -resultVector.dy;
      } else{
        resultVector.dy=outsideForce+(down/max(h,r_in.h))*insideForce;
        return resultVector.dy;
      }
    }

    GlyphRectangle r=new GlyphRectangle(r_in.x,r_in.y,r_in.w,r_in.h);

    float x1=x+w/2;
    float x2=r.x+r.w/2;
    float y1=y+h/2;
    float y2=r.y+r.h/2;

    if (x1-x2>width/2){
      x2+=width;
      r.x+=width;
      y2-=tokenHeight;
      r.y-=tokenHeight;
    }else{
      if (x1-x2<-width/2) {
        x2-=width;
        r.x-=width;
        y2+=tokenHeight;
        r.y+=tokenHeight;
      }
    }

    if (y1<r.y) {
      y1=r.y;
    } else {
      if (y1>r.y+r.h) {
        y1=r.y+r.h;
      }
    }
    if (y2<y) {
      y2=y;
    } else {
      if (y2>y+h) {
        y2=y+h;
      }
    }

    if (y1>y&&y1<y+h || y2>r.y&&y2<r.y+r.h) {
      resultVector.dy=0;
    } else {
      resultVector.dy=y1-y2;
      if (abs(resultVector.dy)>outsideRadius) return 0;
    }

    if (x1<r.x) {
      x1=r.x;
    } else {
      if (x1>r.x+r.w) {
        x1=r.x+r.w;
      }
    }
    if (x2<x) {
      x2=x;
    } else {
      if (x2>x+w) {
        x2=x+w;
      }
    }

    if (x1>x&&x1<x+w || x2>r.x&&x2<r.x+r.w) {
      resultVector.dx=0;
    } else {
      resultVector.dx=x1-x2;
      if (abs(resultVector.dx)>outsideRadius) {
        return 0;
      }
    }

    float d;
    if (resultVector.dx==0) {
      d=abs(resultVector.dy);
    } else
    if (resultVector.dy==0) {
      d=abs(resultVector.dx);

    }
    else d=sqrt(sq(resultVector.dx)+sq(resultVector.dy));

    if (d>outsideRadius || d<=0.0001) {
      return 0;
    }
    float magnitude=((outsideRadius-d)/outsideRadius)*outsideForce;

    resultVector.dx=(resultVector.dx/d)*magnitude;
    resultVector.dy=(resultVector.dy/d)*magnitude;;

    return magnitude;
  }
}

class Vector {
  float dx;
  float dy;

  Vector(float dx,float dy) {
    this.dx=dx;
    this.dy=dy;
  }

  Vector() {
    this(0f,0f);
  }
}


launch project

artists' comments