To implement Text Rain in Processing, I created a few helper classes. First, I create a Character class that draws a character at its current x and y location and detects whether or not the character is free falling or if it landed on a dark spot. It does so by checking the brightness of the pixel directly below it and setting a boolean within the class. I also created a camera class so that I could test the application out with different types of cameras, namely black -and-white and grayscale. I had some issues with thresholding background brightness so I tried mapping each pixel’s brightness to an exponential function to create a bigger differentiation between light and dark values but I still find that I need to adjust the threshold based on the location in which I am running the project.
Text Rain from Erica Lazrus on Vimeo.
Below is my code which can also be downloaded here:
Main text rain class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | import processing.video.*; Capture camera; CameraBlackWhite bwCamera; CameraGrayscale gsCamera; color bgColor; String text; Character[] characterSet1; Character[] characterSet2; Character[] characterSet3; public void setup() { size(640, 480, P2D); smooth(); int threshold = 50; gsCamera = new CameraGrayscale(this, threshold); gsCamera.startVideo(); bgColor = color(#ffffff); text = "We are synonyms for limbs' loosening of syntax, and yet turn to nothing: It's just talk."; characterSet1 = new Character[text.length()]; characterSet2 = new Character[text.length()]; characterSet3 = new Character[text.length()]; for (int i=0; i < text.length(); i++) { char c = text.charAt(i); color col = color(random(255), random(255), random(255)); float speed = random(1, 6); characterSet1[i] = new Character(c, col, 14, 5 + i*7.25, speed, threshold); characterSet2[i] = new Character(c, col, 14, 5 + i*7.25, speed, threshold); characterSet3[i] = new Character(c, col, 14, 5 + i*7.25, speed, threshold); characterSet1[i].start(); } } public void draw() { background(bgColor); update(); render(); } public void update() { gsCamera.update(); for (int i=0; i < text.length(); i++) { characterSet1[i].update(); if (characterSet1[i].getCurYPos() > height/2) { characterSet2[i].start(); } else if (characterSet2[i].getCurYPos() - textAscent() >= height || characterSet2[i].getCurYPos() < 0) { characterSet2[i].setCurYPos(0-(textAscent() + textDescent())); characterSet2[i].stop(); } characterSet2[i].update(); } } public void render() { for (int i=0; i < text.length(); i++) { characterSet1[i].render(); characterSet2[i].render(); characterSet3[i].render(); } } |
Abstract camera class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public abstract class Camera { private Capture video; private int numPixels; private int threshold; public void startVideo() { getVideo().start(); } public abstract void update(); public abstract void render(); public Capture getVideo() { return this.video; } public void setVideo(Capture video) { this.video = video; } public int getNumPixels() { return this.numPixels; } public void setNumPixels(int numPixels) { this.numPixels = numPixels; } public int getThreshold() { return this.threshold; } public void setThreshold(int threshold) { this.threshold = threshold; } } |
Black and white camera class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class CameraBlackWhite extends Camera { private color BLACK = color(#000000); private color WHITE = color(#ffffff); public CameraBlackWhite(Text_Rain_2_0 applet, int threshold) { setVideo(new Capture(applet, width, height)); setNumPixels(getVideo().width * getVideo().height); setThreshold(threshold); } public void update() { if (getVideo().available()) { getVideo().read(); getVideo().loadPixels(); loadPixels(); float pixelBrightness; for (int i=0; i < getNumPixels(); i++) { int pixelX = i % width; int pixelY = i / width; pixelBrightness = brightness(getVideo().pixels[i]); pixelBrightness = pow(pixelBrightness, 3); pixelBrightness = map(pixelBrightness, 0, 16581375, 0, 255); if (pixelBrightness > getThreshold()) { pixels[(width-1-pixelX) + pixelY*getVideo().width] = WHITE; } else { pixels[(width-1-pixelX) + pixelY*getVideo().width] = BLACK; } } updatePixels(); } } public void render() { } } |
Grayscale camera class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class CameraGrayscale extends Camera { private color BLACK = color(#000000); private color WHITE = color(#ffffff); public CameraGrayscale(Text_Rain_2_0 applet, int threshold) { setVideo(new Capture(applet, width, height)); setNumPixels(getVideo().width * getVideo().height); setThreshold(threshold); } public void update() { if (getVideo().available()) { getVideo().read(); getVideo().loadPixels(); loadPixels(); float pixelBrightness; for (int i=0; i < getNumPixels(); i++) { int pixelX = i % width; int pixelY = i / width; pixelBrightness = brightness(getVideo().pixels[i]); pixelBrightness = pow(pixelBrightness, 3); pixelBrightness = map(pixelBrightness, 0, 16581375, 0, 255); if (pixelBrightness > getThreshold()) { pixels[(width-1-pixelX) + pixelY*getVideo().width] = WHITE; } else { pixels[(width-1-pixelX) + pixelY*getVideo().width] = color(pixelBrightness); } } updatePixels(); } } public void render() { } } |
Character class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | public class Character { private char c; private color col; private int sz; private float xPos; private float curYPos; private float ySpeed; private int threshold; private boolean falling; public Character(char c, color col, int sz, int threshold) { setC(c); setCol(col); setSz(sz); setXPos(0); setCurYPos(0-(textAscent() + textDescent())); setYSpeed(0); setThreshold(threshold); setFalling(false); } public void start() { setFalling(true); } public void stop() { setFalling(false); } public Character(char c, color col, int sz, float xPos, float ySpeed, int threshold) { setC(c); setCol(col); setSz(sz); setXPos(xPos); setCurYPos(textAscent() + textDescent()); setYSpeed(ySpeed); setThreshold(threshold); setFalling(true); } public void update() { if (getCurYPos() < 0 || ceil(getCurYPos() + 1) >= height || isLocationBright((int)getXPos(), (int)getCurYPos() + 1)) { setFalling(true); } else { setFalling(false); } if (isFalling()) { textSize(getSz()); float newYPos = getCurYPos() + getYSpeed(); setCurYPos(newYPos); if ((newYPos - textAscent())> height) { setCurYPos(0-(textAscent() + textDescent())); } } else { setCurYPos(findFirstBrightSpot((int)getXPos(), (int)getCurYPos())); } } public void render() { fill(getCol()); textSize(getSz()); textAlign(CENTER, BOTTOM); text(getC(), getXPos(), getCurYPos()); } public boolean isLocationBright(int x, int y) { int testValue = get(x, y); float testBrightness = brightness(testValue); return (testBrightness > getThreshold()); } public int findFirstBrightSpot(int x, int y) { int yPos; for (yPos = y; yPos > 0; yPos--) { if (isLocationBright(x, yPos)) break; } return yPos; } public char getC() { return this.c; } public void setC(char c) { this.c = c; } public color getCol() { return this.col; } public void setCol(color col) { this.col = col; } public int getSz() { return this.sz; } public void setSz(int sz) { this.sz = sz; } public float getXPos() { return xPos; } public void setXPos(float xPos) { this.xPos = xPos; } public float getCurYPos() { return this.curYPos; } public void setCurYPos(float curYPos) { this.curYPos = curYPos; } public float getYSpeed() { return this.ySpeed; } public void setYSpeed(float ySpeed) { this.ySpeed = ySpeed; } public int getThreshold() { return this.threshold; } public void setThreshold(int threshold) { this.threshold = threshold; } public boolean isFalling() { return this.falling; } public void setFalling(boolean falling) { this.falling = falling; } } |