Final Project: Horror Journal

It’s the final project! My project, named “Horror Journal”, is a personal diary simulator of sorts. This project is an interactive version of the novel “The Horla”. The diary has 39 entries in total, spanning from May to September.  Find out more about how it works below!

View post on imgur.com

Moving Through the Entries: 

Right in front of the user are two sheets of paper labeled “Previous Entry” and “Next Entry”. Below the hand icons printed on each paper is a Force Sensing Resistor (FSR) wired to the Arduino. Without user interaction, Arduino communicates a “0” to Processing. When the user pushes on the sheet, the FSR generates higher values. Once a specified threshold is passed, Arduino communicates a “1” to Processing. Processing then detects the “1” and switches to the next or previous diary entry depending on the user interaction.

Summoning the Demon: 

In the novel being played in the project, the character writing the diary entries is experiencing some sort of mysterious illness. By the end of the story, we learn that the character is being possessed by some sort of demon who he names “The Horla”. Since this is the biggest plot point in the story, I thought it would only make sense to incorporate it in the user interaction. For this interaction, I used a webcam, a Big Demon Glove (also known as a beanie), and a servo. In Processing, I read frames from the webcam and applied frame differencing on them to detect motion within the area captured by the webcam. This area is outlined by the big red sheet seen on the table. The servo I’m using is taped on the Big Demon Glove. On the servo, a small paper strip which reads “The Horla” is taped. When the user places the Big Demon Glove on the red sheet area, the camera detects the motion of servo, or the paper strip more accurately. This triggers a red strip on the screen to grow. Once the red strip reaches the bottom of the screen, the interaction is triggered: the screen turns red and the voice of the narrator turns deeper, imitating a stereotypical demonic voice. Pictures of this interaction can be seen below:

View post on imgur.com

View post on imgur.com

The Interface: 

Aside from the interactions, my project consists of the Processing screen, which displays the text of the diary entry being played, and audio readings of the current entry. The screen consists of yellow, red, and black colors. Originally, I wanted to create a more elaborate screen interface that simulates a personal room environment. I quickly realized that going with a simpler interface would make more sense with the interactions I planned. Additionally, the simpler interface looked generally cleaner and easier to follow. I was happy to hear that other people agreed with me.

Overall Thoughts: 

I’m glad I went with this project. I have received variety of reactions at the showcase ranging from “This is a very specific project” to “Wow! [the demonic voice] is creepy”. I noticed that a video game type project might have been more approachable in the showcase, but I still had many people come over to take a look at my project. One limitation might have been that no one would actually be able to sit through the entire story being narrated in my project since it is so lengthy. I did think about this possibility in my design phase. I thought about several solutions such as cutting down on the content of the story, but keeping everything in and allowing the user to navigate through the content on their own made more sense to me. I also wish I had booked a speaker early on. When I tested my project in the lobby, it sounded fine, but I did not anticipate how loud it would actually be during the showcase. Either way, I was able to present my project to everyone who came by, so I’m glad it worked out.

Credits: 

Audio Reading:

Text:

http://www.eastoftheweb.com/short-stories/UBooks/Horl.shtml

Processing Code: 

import processing.sound.*;
import processing.video.*;
import processing.serial.*;

Serial myPort;
int prev_left=0;
int prev_right=1;
int left=0;
int right=0;
boolean onOff=false;
boolean onOff2=false;

Capture video;
int vidX, vidY;
PImage prevFrame;
float motion_threshold=20;

SoundFile s_bg, s_clock, s_breathing, s_drinkwater;
SoundFile[] entries_audio;
PImage paper;
PFont font;
String[] entries;
int entries_cnt = 39;
int paper_offset_y = 0;
int page_start_i = 0;

int letter_i = 0;

int red_level = 0;

int vid_width = 640;
int vid_height = 480;

color bg_yellow = color(229, 229, 144);
color bg_red = color(183, 0, 0);
color bg_color = bg_yellow;
color text_color = color(0);
color bar_color = bg_red;
float bg_phase = 0;
float bar_phase = 0;
float text_phase = 0;
float audio_phase = 0.8;

int entry_switch_offset = 0;
int entry_switch_threshold = 10;

int prev_entry = 0;
int curr_entry = 0;

int text_speed = 2;

float avgX = 0;
float avgY = 0;

String may = "8, 12, 16, 18, 25";
String june = "2, 3";
String july = "2, 3, 4, 5, 6, 10,\n12, 14, 16, 19, 21, 30";
String august = "2, 4, 6, 7, 8, 9,\n10, 11, 12, 13, 14, 15,\n16, 17, 18, 19, 20, 21,\n22";
String september = "10";

void setup() {
  fullScreen();
  background(bg_color);

  printArray(Serial.list());
  String portname=Serial.list()[0];
  //println(portname);
  myPort = new Serial(this, portname, 9600);
  myPort.clear();
  myPort.bufferUntil('\n');

  String[] cameras = Capture.list();
  video = new Capture(this, width, height, cameras[1]);
  video.start();
  prevFrame=createImage(width, height, RGB);

  paper = loadImage("paper.jpg");

  font = createFont("SyneMono-Regular.ttf", 25);
  textFont(font);

  entries_audio = new SoundFile[entries_cnt];
  for (int i=0; i<entries_cnt; i++) {
    entries_audio[i] = new SoundFile(this, "entries_audio/"+i+".mp3");
  }
  s_bg = new SoundFile(this, "sound/Ambience/night-crickets-ambience-on-rural-property.wav");
  s_clock = new SoundFile(this, "sound/Ambience/loopable-ticking-clock.wav");
  s_breathing = new SoundFile(this, "sound/Human/breath-male.wav");
  s_drinkwater = new SoundFile(this, "sound/Human/drink-sip-and-swallow.wav");
  s_bg.loop(1, 0.5);
  s_clock.loop(1, 0.1);

  entries = new String[entries_cnt];
  for (int i=0; i<entries_cnt; i++) {
    entries[i] = readFile("entries/"+i+".txt");
  }
}

void draw() {
  background(bg_color);
  entry_switch_offset++;
  textSpeed();
  drawPaper();
  entrySwitch();
  detectMotion();
  playEntry(entries_audio[prev_entry], entries_audio[curr_entry]);
  displayCalendar();
  prev_left = left;
  prev_right = right;
}

void serialEvent(Serial myPort) {
  String s=myPort.readStringUntil('\n');
  s=trim(s);
  if (s!=null) {
    int values[]=int(split(s, ','));
    if (values.length==2) {
      left=(int)values[0];
      right=(int)values[1];
    }
  }
  myPort.write(int(onOff)+","+int(onOff2)+"\n");
}


void drawPaper() {
  imageMode(CENTER);
  writeText(entries[curr_entry]);
}

void writeText(String text) {
  int x = (width/2) - (paper.width/6) + 70;
  int y = (height/2) - (paper.height/6) + 70;
  int char_width = 0;
  int char_row = 0;
  String date = "";
  int c = 0;
  while (text.charAt(c) != '.') {
    date = date + text.charAt(c);
    c++;
  }
  if (page_start_i == 0) {
    page_start_i = c+2;
  }

  pushMatrix();
  textSize(40);
  text(date, x, 80);
  popMatrix();

  pushMatrix();
  textSize(25);
  translate(x, y + paper_offset_y);
  fill(text_color);

  if (entry_switch_offset > entry_switch_threshold) {
    if (frameCount%text_speed == 0 && letter_i < text.length() && text.charAt(letter_i) != ' ') {
      letter_i++;
    } else if (letter_i < text.length() && text.charAt(letter_i) == ' ') {
      letter_i++;
    }
    for (int i=page_start_i; i < letter_i; i++) {
      char_width += textWidth(text.charAt(i));
      text(text.charAt(i), char_width, char_row*30);


      if (x + char_width >= (width/2) + (paper.width/6) - 160 && text.charAt(i) == ' ') {
        char_row++;
        char_width = 0;
      }
      if (text.charAt(i) == '\n') {
        char_row++;
        char_width = 0;
      }
    }

    if (char_row > 19) {
      page_start_i = letter_i;
    }
  }
  popMatrix();
}

String readFile(String path) {
  String s = "";
  String[] arr = loadStrings(path);
  for (int i=0; i<arr.length; i++) {
    s = s + '\n' + arr[i];
  }
  return s;
}

void detectMotion() {
  if (video.available()) {
    prevFrame.copy(video, 0, 0, width, height, 0, 0, width, height);
    prevFrame.updatePixels();
    video.read();
  }
  video.loadPixels();
  prevFrame.loadPixels();
  loadPixels();
  float totalMotion=0;
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      int loc = (video.width-x-1)+(y*width);
      color pix=video.pixels[loc];
      color prevPix=prevFrame.pixels[loc];
      float r1=red(pix);
      float g1=green(pix);
      float b1=blue(pix);
      float r2=red(prevPix);
      float g2=green(prevPix);
      float b2=blue(prevPix);
      float diff=dist(r1, g1, b1, r2, g2, b2);
      totalMotion+=diff;
      if (diff>motion_threshold) {
        avgX += x;
        avgY += y;
      }
    }
  }
  float avgMotion=totalMotion/pixels.length;
  avgX = avgX/pixels.length;
  avgY = avgY/pixels.length;
  fill(0);
  text(avgX + ' ' + avgY, 200, 800);
  if (avgMotion>motion_threshold && frameCount%1 == 0 && red_level <= height+10) {
    red_level += 10;
  } else if (frameCount%2 == 0 && red_level >= -5) {
    red_level -= 3;
  }
  video.updatePixels();
  prevFrame.updatePixels();
  updatePixels();
  if (avgMotion>motion_threshold) {
    fill(0);
    ellipse(width/2, 100, 30, 30);
  }
  pushMatrix();
  barSwitch();
  fill(bar_color);
  noStroke();
  rect(400, 0, 15, red_level);
  popMatrix();
  bgSwitch();
}

void bgSwitch() {
  if (red_level >= height) {
    bg_color = bg_red;
    bg_phase = 0;
    text_color = color(255);
  } else if (bg_color != bg_yellow && bg_phase<1) {
    bg_phase = phaseFade(bg_phase);
    text_phase = phaseFade(text_phase);
    bg_color = colorFade(bg_color, bg_red, bg_yellow, bg_phase);
    text_color = colorFade(text_color, color(255), color(0), text_phase);
  } else {
    bg_color = bg_yellow;
    text_color = color(0);
    bg_phase = 0;
    text_phase = 0;
  }
}

void barSwitch() {
  if (red_level >= height) {
    bar_color = bg_yellow;
    bar_phase = 0;
  } else if (bar_color != bg_red && bar_phase<1) {
    bar_color = colorFade(bar_color, bg_yellow, bg_red, bar_phase);
    bar_phase = phaseFade(bar_phase);
  } else {
    bar_color = bg_red;
    bar_phase = 0;
  }
}

color colorFade(color curr, color from, color to, float phase) {
  if (frameCount%10 == 0) {
    return lerpColor(from, to, phase);
  }
  return curr;
}

float phaseFade(float phase) {
  if (frameCount%10 == 0) {
    return phase + 0.01;
  }
  return phase;
}

void playEntry(SoundFile preventry, SoundFile currentry) {
  audioSpeed();
  if (!currentry.isPlaying()) {
    if (preventry.isPlaying()) {
      preventry.stop();
    }
    currentry.play();
  }
}

void audioSpeed() {
  if (red_level >= height && bg_color == bg_red) {
    audio_phase = 0.8;
  } else if ( bg_color != bg_yellow && frameCount%10 == 0) {
    audio_phase += 0.002;
  } else if (bg_color == bg_yellow) {
    audio_phase = 1;
  }
  entries_audio[curr_entry].rate(audio_phase);
}

void entrySwitch() {
  if (left == 1 & prev_left == 0  && curr_entry > 0) {
    prev_entry = curr_entry;
    curr_entry--;
    page_start_i = 0;
    letter_i = 0;
  }
  if (right == 1 && prev_right == 0  && curr_entry < entries_cnt-1) {
    prev_entry = curr_entry;
    curr_entry++;
    page_start_i = 0;
    letter_i = 0;
  }
}

void textSpeed() {
  if (red_level >= height) {
    text_speed = 4;
  } else {
    text_speed = 3;
  }
}

void displayCalendar() {
  int offset = 40;
  text("May", 50, 150);
  text("June", 50, 250);
  text("July", 50, 350);
  text("August", 50, 500);
  text("September", 50, 720);
  fill(text_color);
  text(may, 50, 150+offset);
  text(june, 50, 250+offset);
  text(july, 50, 350+offset);
  text(august, 50, 500+offset);
  text(september, 50, 720+offset);
  
  fill(bar_color);
}

Arduino Code:

#include <Servo.h>

#define right_fsr_pin A1
#define left_fsr_pin A0
#define threshold 900

int right_fsr_val;
int left_fsr_val;

int left = 0;
int right = 0;

Servo myServo;
const int servoPin = 3;

long currmillis = 0;
int angle = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("0,0");
  myServo.attach(servoPin);
}

void loop() {
  while (Serial.available()) {
    right = Serial.parseInt();
    left = Serial.parseInt();
    if (Serial.read() == '\n') {
      left_fsr_val = analogRead(left_fsr_pin);
      delay(1);
      right_fsr_val = analogRead(right_fsr_pin);
      delay(1);

      if (left_fsr_val >= threshold) {
        left_fsr_val = 1;
      } else {
        left_fsr_val = 0;
      }
      if (right_fsr_val >= threshold) {
        right_fsr_val = 1;
      } else {
        right_fsr_val = 0;
      }

      Serial.print(left_fsr_val);
      Serial.print(',');
      Serial.println(right_fsr_val);
    }
  }

    if (currmillis + 1000 <= millis()) {
      if (angle <= 10) {
        angle = 180;
      } else {
        angle = 0;
      }
      myServo.write(angle);
      currmillis = millis();
    }
}

 

Leave a Reply