Skip to content

Instantly share code, notes, and snippets.

@elib
Created October 1, 2014 14:42
Show Gist options
  • Save elib/2d932355748c40132685 to your computer and use it in GitHub Desktop.
Save elib/2d932355748c40132685 to your computer and use it in GitHub Desktop.
"Broken VHS" effect for static image
float sidepercent = 6;
//10-20 -> amazing horizontals
//100 -> painterly
float initialChanceOfChunkGrab = 15;
float chanceOfChunkGrab;
float chanceOfChunkGrabChange = 10;
PImage originalImage;
PImage finalImage;
int yoff = 10;
PVector[] points;
void resetPoints() {
PVector[] tmppoints = {
//first two identical
new PVector(0, 50 + yoff),
new PVector(0, 50 + yoff),
new PVector(10, 100 + yoff),
new PVector(0, 151 + yoff),
new PVector(0, 151 + yoff),
new PVector(0, 151 + yoff),
new PVector(0, 151 + yoff),
new PVector(0, 151 + yoff),
new PVector(10, 200 + yoff),
new PVector(5, 300 + yoff),
new PVector(20, 330 + yoff),
new PVector(90, 400 + yoff),
new PVector(90, 400 + yoff)
//new PVector(10, 450 + yoff),
//new PVector(50, 500 + yoff),
//new PVector(20, 700 + yoff),
//new PVector(20, 700 + yoff),
//last two identical
};
points = tmppoints;
int xchangeAmount = 90;
int ychangeAmount = 40;
for(int i = 0; i < points.length; i++) {
points[i].x = (int) constrain(points[i].x + random(xchangeAmount) - xchangeAmount/2, 0, 200);
points[i].y = (int) constrain(points[i].y + random(ychangeAmount) - ychangeAmount/2, 0, 700);
}
}
void setup() {
originalImage = loadImage("photo.jpg");
size(originalImage.width, originalImage.height);
frameRate(1);
}
PImage makeImgSize(PImage img) {
PImage newImage = createImage(img.width, img.height, RGB);
return newImage;
}
PImage cloneImg(PImage img) {
PImage newImg = makeImgSize(img);
newImg.copy(img, 0, 0, img.width, img.height, 0, 0, img.width, img.height);
return newImg;
}
float easeInOut(float t, float duration, float starting, float change) {
return -change/2 * (cos(PI*t/duration) - 1) + starting;
}
int shouldSide(int x, int wid) {
float baserand = (random(30) + 225);
int amt = int(sidepercent * wid / 100.0f);
int whenease = amt/4;
if(x < whenease || x >= wid - whenease) {
return int(baserand);
//return 0;
}
float starting = 1;
float change = -1;
float duration = 3*amt/4;
if (x <= whenease + duration) {
return int( baserand * easeInOut(x - whenease, duration, starting, change));
}
if (x >= wid - whenease - duration) {
return int( baserand * easeInOut(x - wid + whenease, -duration, starting, change));
}
return 0;
}
PImage addSideStatic(PImage img) {
PImage sideStaticImg = cloneImg(img);
PImage noise = makeImgSize(img);
noise.loadPixels();
for(int x = 0; x < noise.width; x++) {
for(int y = 0; y < noise.height; y++) {
noise.pixels[x + (y*noise.width)] = color(random(120),random(120),random(120), shouldSide(x, img.width));
}
}
noise.updatePixels();
sideStaticImg.blend(noise, 0, 0, sideStaticImg.width, sideStaticImg.height, 0, 0, sideStaticImg.width, sideStaticImg.height, BLEND);
return sideStaticImg;
}
PImage addStatic(PImage img) {
PImage staticImg = cloneImg(img);
PImage noise = makeImgSize(img);
noise.loadPixels();
for(int x = 0; x < noise.width; x++) {
for(int y = 0; y < noise.height; y++) {
noise.pixels[x + (y*noise.width)] = color(random(255),random(255),random(255), random(80) + 30);
}
}
noise.updatePixels();
staticImg.blend(noise, 0, 0, staticImg.width, staticImg.height, 0, 0, staticImg.width, staticImg.height, BLEND);
return staticImg;
}
PImage doCombs(PImage img) {
return doSingleComb(img);
}
PImage doSingleComb(PImage img) {
PImage combImage = cloneImg(img);
//decide starting point of comb
int startPoint = int(points[0].y);
//(int) ((img.height / 4) + random(img.height / 8));
//decide ending point of comb
int endPoint = int(points[points.length - 1].y);
//3 * img.height / 4;
//decide width of comb -- usually all of width
int combWidth = img.width;
//decide abspoint of param;
float y0 = ((endPoint - startPoint) / 2) + startPoint;
//params
float a = 1;
float b = a/2 - 1/a;
float c = pow(a, 2)/4 + 1/(pow(a,2));
//decide rate of pickup-new-pixel-colors
float newPixelRate = 0;
color[] currentColors = new color[combWidth];
float beginningOffset = evaluateCurve(startPoint);
gatherColors(combImage, currentColors, startPoint, beginningOffset);
for(int y = startPoint; y < endPoint; y++) {
float offset = evaluateCurve(y);
if(random(1.0f) < 1/chanceOfChunkGrabChange) {
if(random(1.0f) < 0.5f) {
chanceOfChunkGrab *= 2;
} else {
chanceOfChunkGrab /= 2;
}
chanceOfChunkGrab = constrain(chanceOfChunkGrab, 5, initialChanceOfChunkGrab);
}
//float offset = evaluateFunction(y, startPoint, endPoint, a, b, c);
writeColors(combImage, currentColors, y, offset);
//handle new pixel rate
}
return combImage;
}
float evaluateCurve(int y) {
int ind = 1;
boolean found = false;
while(ind < points.length - 2 && !found) {
if(y >= points[ind].y && y <= points[ind+1].y) {
found = true;
} else {
ind ++;
}
}
if(!found) {
return 0;
}
float t = norm(y, points[ind].y, points[ind+1].y);
return curvePoint(points[ind-1].x, points[ind].x, points[ind+1].x, points[ind+2].x, t);
}
void gatherColors(PImage img, color[] currentColors, int y, float offset){
img.loadPixels();
int xoff = (int) offset;
int x = 0;
while(x < (img.width - xoff)) {
int chunksize = setCurrentChunk(y, img, currentColors, x, xoff);
x+=chunksize;
}
for(x = (img.width - xoff); x < img.width; x ++) {
currentColors[x] = 0;
}
}
int setCurrentChunk(int y, PImage img, color[] currentColors, int startx, int xoff) {
int rr = 0, gg = 0, bb = 0;
int chunksize = int(random(15) + 5); //random
if(chunksize + startx >= (img.width - xoff)) {
chunksize = (img.width - xoff) - startx;
}
for(int xtag = 0; xtag < chunksize; xtag++) {
//get average color
color oneColor = img.pixels[(startx + xoff + xtag) + y *(img.width)];
rr += red(oneColor);
gg += green(oneColor);
bb += blue(oneColor);
}
rr /= chunksize;
gg /= chunksize;
bb /= chunksize;
for(int xtag = 0; xtag < chunksize; xtag++) {
currentColors[startx + xtag] = color(rr, gg, bb);
}
return chunksize;
}
void writeColors(PImage img, color[] currentColors, int y, float offset){
img.loadPixels();
int xoff = (int) offset;
xoff = constrain(xoff, 0, img.width - 10);
for(int x = 0; x < (img.width - xoff); x++) {
//chunk it!
if(random(1.0f) < 1 / (chanceOfChunkGrab)) {
setCurrentChunk(y, img, currentColors, x, xoff);
}
img.pixels[(x + xoff) + y *(img.width)] = currentColors[x];
}
img.updatePixels();
}
void draw() {
background(0);
chanceOfChunkGrab = initialChanceOfChunkGrab;
resetPoints();
PImage img = addSideStatic(originalImage);
img = doCombs(img);
img = addStatic(img);
image(img, 0, 0);
//uncomment to save each frame to disk
//save("img_" + hour() + "_" + minute() + "_" + second() + ".png");
}
@elib
Copy link
Author

elib commented Oct 1, 2014

This is "really old" code (January, 2013), written in a few long evenings and then never touched again. Please bear this in mind.

The goal of this Processing script was to simulate a single frame of a very old and/or broken VHS tape, right in the middle of an extreme magnetic corruption. I believe I may have used https://www.youtube.com/watch?v=mES3CHEnVyI and http://maxcapacity.tumblr.com/ as inspiration/reference.

Some sparse notes about the code...

  • The weird point list at the beginning is a hand-defined curve that describes the left-hand starting point of the corruption in the image, which spreads to the end on the right side, in random jumps. Try it on a photo and you'll see that while the image changes drastically each frame, the place where the "broken"-ness starts remains more or less the same.
  • You can play with the initialChanceOfChunkGrab constant to see the effect when changing the frequency of switching "chunks" when smearing the pixels across the image.
  • Finally, you can remove or change the two different kinds of static (the bars of noise on the sides in addSideStatic and the general static covering the image as a final pass in addStatic).

If you'd like to ask me something more specific, please feel free to hit up @elibrody on Twitter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment