My final project idea was to create a magical book with my drawings on some of the pages, and have the drawings come to life through animation. Since a few people have asked me in the showcase, I came up with this idea mainly because of the first drawing that I used. It’s a pen sketch of one of the rooms in the Shakespeare and company bookstore, which I visited in Paris. I did my class research on the bookstore, and so found that artists, writers, and travelers were free to stay at the bookstore and sleep in the beds as long as they wished for, as along as they read one book a day, wrote a one page autobiography for the bookstore’s archives, and helped out a little around the shop. Another thing about the bookstore was that people would leave notes in a box, that the book-keepers would place into random books, to be found by readers. So looking at this drawing always made me imagine the people who stayed there, and the books they read. It all seemed very magical to me, so I decided to bring the drawing to life, with stop motion animation. I’ve been intrigued by storytelling through art lately, so this was the perfect way to experiment with it.
Disclaimer: as you’ll see in this project, I’m a big Harry Potter fan.
I started with the idea of using AR markers by placing them on the side of the page and having the drawing projected onto each page but then I decided that I really want to focus on creating the feeling of each of the drawings coming to life, so I was determined to find a way to use my drawing itself as the marker. Through research and trial and error, I finally got it working properly.
I tried out different things and learnt that AR markers actually don’t need to be in square form, only the edge needs to be a square. So I inserted the final drawing marker into the data folder and changed the code, edge percentage, and positioning, and size. I used example AR code that displayed a cube on top of the marker, and changed the code so as to have a video file play on top of the marker. While this worked perfectly fine on the screen, when projected, I found out there was a big issue I hadn’t considered. That if the video was projected on the same place that the camera was facing, the camera would pick up the projection and so the projector would project that, and it created an endless loop of a mirror effect. In order to fix this, I decided to have the projector project on the drawing on one page, and the camera pick up the marker on the other page, which would be either a drawing or a page number. I eventually went with the page number so as to not distract from the animated drawings.
Having the several drawings was much more complicated than having just one drawing, so I tried different methods, such as arraylist, array, and for loop. It feels amazing to be able to go through the code and actually understand what everything is doing, because a few months ago coding was so foreign to me.
While setting up, I placed the tripods as far away from the book as possible, so as to create the illusion that they are not there, and allow the audience to be immersed into the ‘magic’. I accidentally printed the drawings in black and white, and then decided that it looked better this way, because it would make the style of the different drawings match more. I also printed a specific quote for each page, that created sort of story behind each drawing, to relate it to magic.
I user tested my product and found that everything was working fine, except that the projection wasn’t going white when the marker was not detected, and instead showed what the camera was picking up continuously.
This was something that I was able to fix easily, with adding just one more line of code, but it felt great because a few weeks ago, I would have not at all been able to navigate my way through code, and now I could.
After fixing the issue, I did another user test;
This time everything worked perfectly fine. However I felt that I want it to be a bit longer, and have some sort of finish, since people kept going to the next page, so I decided to add one more animation, that would make it a bit longer, create a sense of a finish, and bring it all back to the theme of the book; magic.
I displayed this one in color, and without a background drawing, so as to make it stand out as the ending (animation of a pickup truck leaving a note that says “It’s magic” before driving away, as though to say how the book operates).
The showcase was a lot of fun because I got to see people interact with my product. I loved that it made them smile. Several of the people actually jumped back when they saw the drawings were moving, which was so entertaining to watch. People kept looking around to find out how it’s working, and the funny thing was that a few of them looked directly at the projector and camera but did not notice the drawing was being projected through them. When they would ask me how it works, I would tell them it will be revealed at the end (the note that said “it’s magic!” – which Aaron said was cheesy, but I find hilarious – before telling them how it really worked).
I was a bit worried that when turning the page, people might hold the place where the marker (page number) is, and the animation wouldn’t appear, but thankfully out of everyone maybe only two people did this, who I had to explain it to. It was really interesting to see how people interacted with it. Some people were not sure if they should open it, others kept flipping the pages back and forth to see if the animations would still appear on each drawing, which they did, some people really spent time taking in the quotes, while others were too distracted by the drawing to read the quotes, some people kept touching the number because they thought it’s some sort of button or pressure sensor, and some people kept waving their hand over the drawing to see what would happen. It also made me happy how some people took videos of it, or went to get someone else to come see it as well. I was also happy because a friend of mine who basically does nothing but criticize me was very amused by it, and was finally impressed!
*ps the person in this photo isn’t the friend I’m talking about
I’m really glad that my product had the effect on people that I wanted. I can’t believe how far I’ve come considering I could not understand any coding before this semester. Coding was such a foreign language to me and I’ve learnt a lot. In the beginning of this semester I could not even understand what the example code was saying, but now I can go through this entire code and know what’s going on and what each line is doing. I’m really thankful for your patience and support Aaron. And to the entire class and instructors, because everyone was always really supportive. Thank you.
I user tested my product and found that everything was working fine, except that the projection wasn’t going white when the marker was not detected, and instead showed what the camera was picking up continuously.
This was something that I was able to fix easily, with adding just one more line of code, but it felt great because a few weeks ago, I would have not at all been able to navigate my way through code, and now I could.
After fixing the issue, I did another user test;
This time everything worked perfectly fine. However I felt that I want it to be a bit longer, and have some sort of finish, since people kept going to the next page, so I decided to add one more animation, that would make it a bit longer, create a sense of a finish, and bring it all back to the theme of the book; magic.
I’m glad I added the last animation because I think it really helped create an ending. A lot of little challenges arised during this project and I’m so happy that it turned out how I envisioned it.
I have always been fascinated with using nature as a medium for interaction and a source for new creation, especially in the area of music.
WaterBox is a musical loop station where depending on the user’s physical interaction with water plays different loops either individually or simultaneously. And, the track type changes depending on the roughness of the waves and ripples of the water surface. The final product is created using Kinect v2 to capture the waves of the water surface by its depth, and Touche Advanced Touch Sensor with Arduino for the capacitative sensor and interaction with water. Through the WaterBox, I wanted to share the rich feeling of interacting with water and fun of creating different music with physical motion of your hands in the water.
In terms of technical side, the project entails the use of Arduino Uno, a Kinect v2, a container to contain shallow level of water, and a stand that will hold the Kinect in place above the container pointing towards the water. The container was created with transparent acrylic with 18cm x 18cm x 15cm in dimension where the bottom was colored blue for clear detection of the Kinect v2.
The ideal instance of a user interaction would be where users would play around with the water, creating different waves and ripples on the surface. And, depending on the number of fingers (or, to be more precise, the surface area of your hand), different loops will be played on top of one another. Meanwhile, the kinect will capture the depth of the water, and depending on the roughness of the waves, it will change the track of the loops played.
Here are some of user-testing footages:
Development Stages
Initial Stage of using water with Kinect & Sound
Connecting Touche Sensor with Water & Sound
<Source Code>
Processing*
Main Code
Graph Class Library (Touche Advance Sensor)
Serial Link (Touche Advanced Sensor)
import org.openkinect.processing.*;
import processing.sound.*;
int trackType = 6;
int trackCount = 4;
int currentTrackType = 0;
int counter = 0;
String trackName;
/*
============ Touche Advanced Touch Sensor ==============
Source: https://github.com/Illutron/AdvancedTouchSensing
========================================================
*/
Graph MyArduinoGraph = new Graph(150, 80, 500, 300, color (200, 20, 20));
float[][] gesturePoints = new float[4][2];
float[] gestureDist = new float[4];
String[] names = {"Nothing", "One Finger", "Two Finger", "Hand In Water"};
/* ===================================================== */
/* ==== Music Loops ==== */
SoundFile[][] soundPlayers = new SoundFile[trackType][trackCount];
//array to hold the volumes/rate for each track
float[] volumes = new float[trackCount];
//array to hold the volume/rate destinations, to smoothly fade in and out
float[] volumeDestinations = new float[trackCount];
/* ===================== */
/* ==== Kinect Code ==== */
Kinect2 kinect2;
// Min & Max Threshold for Calibration
float minThresh = 570;
float maxThresh = 690;
boolean isSwitched;
PImage img, video;
/* ====================== */
void setup() {
size(1000, 900);
// ==== Touche Setup Code ==== //
PortSelected=8;
SerialPortSetup();
// ============================ //
for (int j=0; j < 6; j++)
{
// ==== NAMING EACH TRACK IN THE SOUND PLAYER 2D ARRAY ==== //
if (j==0)
{
trackName = "deep";
}
else if (j==1)
{
trackName = "edm";
}
else if (j==2)
{
trackName = "hiphop";
}
else if (j==3)
{
trackName = "jazz";
}
else if (j==4)
{
trackName = "latin";
}
else {
trackName = "slow";
}
for (int i=0; i < soundPlayers[j].length; i++)
{
String name = trackName;
name += str(i);
name += ".wav";
println(name);
soundPlayers[j][i] = new SoundFile(this, name);
soundPlayers[j][i].loop();
soundPlayers[j][i].amp(0.001);
volumes[i]=0;
volumeDestinations[i]=0;
}
}
// Kinect Initialization
kinect2 = new Kinect2(this);
kinect2.initRegistered();
kinect2.initDepth();
kinect2.initDevice();
img = createImage(kinect2.depthWidth, kinect2.depthHeight, RGB);
isSwitched = false;
}
void draw() {
background(255);
int pixelCount = 0;
float minDepthLevel = maxThresh;
// Getting soundIndex for Touche Sensor
int soundIndex = advancedTouch();
/* ===================================================== */
img.loadPixels();
video = kinect2.getRegisteredImage();
int[] depth = kinect2.getRawDepth();
// Detect Pixels using Kinect
for (int x = 0; x < kinect2.depthWidth; x++)
{
for (int y = 0; y < kinect2.depthHeight; y++)
{
int index = x + y * kinect2.depthWidth;
int d = depth[index];
if (d > minThresh
&& d < maxThresh
&& red(video.pixels[index]) < 100
&& green(video.pixels[index]) > 180
&& blue(video.pixels[index]) > 170
// Boundaries to capture only the box on the screen
&& 215 < x
&& 350 > x
&& 125 < y
&& 265 > y
) {
pixelCount++;
img.pixels[index] = color(red(video.pixels[index]), green(video.pixels[index]), blue(video.pixels[index]));
if (d < minDepthLevel)
{
minDepthLevel = d;
}
}
else
{
img.pixels[index] = color(0);
}
}
}
/* ========DEBUG CODE======== */
int mouseIndex = mouseX%kinect2.depthWidth + mouseY%kinect2.depthHeight * kinect2.depthWidth;
text(red(video.pixels[mouseIndex]), 600, 600);
text(green(video.pixels[mouseIndex]), 600, 650);
text(blue(video.pixels[mouseIndex]), 600, 700);
text(mouseX, 600, 750);
text(mouseY-480, 680, 750);
text(pixelCount, 600, 780);
text(minDepthLevel, 680, 780);
text(currentTrackType, 880, 780);
/* ========================== */
img.updatePixels();
// Uncomment below if you want to see whether the Kinect is catching the water pixels
//image(img, 0, 480);
noStroke();
/* ========== Calibration Needed depending on space / light ========== */
// If the catched pixcelCount is in between 300 and 400 & the whole hand is in the water,
// then change the track type.
if (
pixelCount >= 300
&& pixelCount <= 400
&& minDepthLevel <= 590
&&soundIndex == 3
&& !isSwitched)
{
counter++;
if(counter > 800) {
isSwitched = true;
currentTrackType++;
currentTrackType %= 6;
}
}
else if (isSwitched)
{
isSwitched = false;
counter = 0;
}
/* =================================================================== */
if (soundIndex > 1)
{
for (int i=1; i< soundIndex+1; i++)
{
volumeDestinations[i-1] = 1;
if (soundIndex == 3)
{
volumeDestinations[i] = 1;
}
}
}
else if (soundIndex == 1)
{
volumeDestinations[0] = 1;
}
for (int j=0; j<soundPlayers.length; j++)
{
for (int i=0; i<soundPlayers[j].length; i++)
{
if (j != currentTrackType)
{
// set other tracks amplitude to zero
soundPlayers[j][i].amp(0);
}
else
{
//set volume
volumes[i]=smoothing(volumes[i], volumeDestinations[i]);
soundPlayers[j][i].amp(volumes[i]);
//continuously fade volume out
volumeDestinations[i]-=.1;
//constrian the fade out to 0
volumeDestinations[i] = constrain(volumeDestinations[i],0,1);
}
}
}
}
//smoothing for fading in and out
float smoothing(float current, float destination) {
current += (destination-current)*.5;
return current;
}
int advancedTouch() {
/* ===============================================================
Touche Advanced Touch Sensor Code
=============================================================== */
if ( DataRecieved3 ) {
pushMatrix();
pushStyle();
MyArduinoGraph.yMax=600;
MyArduinoGraph.yMin=-200;
MyArduinoGraph.xMax=int (max(Time3));
//MyArduinoGraph.DrawAxis();
MyArduinoGraph.smoothLine(Time3, Voltage3);
popStyle();
popMatrix();
/* ====================================================================
Gesture compare
==================================================================== */
float totalDist = 0;
int currentMax = 0;
float currentMaxValue = -1;
for (int i = 0; i < 4;i++) {
//println("Index: " + i);
//println(gesturePoints[i][0], gesturePoints[i][1]);
// Calibration for each category
if (mousePressed && mouseX > 750 && mouseX<800 && mouseY > 100*(i+1) && mouseY < 100*(i+1) + 50)
{
fill(255, 0, 0);
gesturePoints[i][0] = Time3[MyArduinoGraph.maxI];
gesturePoints[i][1] = Voltage3[MyArduinoGraph.maxI];
}
else
{
fill(255, 255, 255);
}
//calucalte individual dist
gestureDist[i] = dist(Time3[MyArduinoGraph.maxI], Voltage3[MyArduinoGraph.maxI], gesturePoints[i][0], gesturePoints[i][1]);
totalDist = totalDist + gestureDist[i];
if(gestureDist[i] < currentMaxValue || i == 0)
{
currentMax = i;
currentMaxValue = gestureDist[i];
}
}
totalDist=totalDist/3;
for (int i = 0; i < 4;i++)
{
float currentAmount = 0;
currentAmount = 1-gestureDist[i]/totalDist;
if(currentMax == i) {
fill(0,0,0);
//text(names[i],50,450);
fill(currentAmount*255.0f, 0, 0);
}
else {
fill(255,255,255);
}
stroke(0, 0, 0);
rect(750, 100 * (i+1), 50, 50);
fill(0,0,0);
textSize(30);
text(names[i],810,100 * (i+1)+25);
fill(255, 0, 0);
rect(800,100* (i+1), max(0,currentAmount*50),50);
}
return currentMax;
}
return 0;
}
/* =================================================================================
The Graph class contains functions and variables that have been created to draw
graphs. Here is a quick list of functions within the graph class:
Graph(int x, int y, int w, int h,color k)
DrawAxis()
Bar([])
smoothLine([][])
DotGraph([][])
LineGraph([][])
=================================================================================*/
class Graph
{
float maxY = 0;
float maxX = 0;
int maxI = 0;
boolean Dot=true; // Draw dots at each data point if true
boolean RightAxis; // Draw the next graph using the right axis if true
boolean ErrorFlag=false; // If the time array isn't in ascending order, make true
boolean ShowMouseLines=true; // Draw lines and give values of the mouse position
int xDiv=5, yDiv=5; // Number of sub divisions
int xPos, yPos; // location of the top left corner of the graph
int Width, Height; // Width and height of the graph
color GraphColor;
color BackgroundColor=color(255);
color StrokeColor=color(180);
String Title="Title"; // Default titles
String xLabel="x - Label";
String yLabel="y - Label";
float yMax=1024, yMin=0; // Default axis dimensions
float xMax=10, xMin=0;
float yMaxRight=1024, yMinRight=0;
Graph(int x, int y, int w, int h, color k) { // The main declaration function
xPos = x;
yPos = y;
Width = w;
Height = h;
GraphColor = k;
}
void DrawAxis() {
/* =========================================================================================
Main axes Lines, Graph Labels, Graph Background
========================================================================================== */
fill(BackgroundColor);
color(0);
stroke(StrokeColor);
strokeWeight(1);
int t=60;
rect(xPos-t*1.6, yPos-t, Width+t*2.5, Height+t*2); // outline
textAlign(CENTER);
textSize(18);
float c=textWidth(Title);
fill(BackgroundColor);
color(0);
stroke(0);
strokeWeight(1);
rect(xPos+Width/2-c/2, yPos-35, c, 0); // Heading Rectangle
fill(0);
text(Title, xPos+Width/2, yPos-37); // Heading Title
textAlign(CENTER);
textSize(14);
text(xLabel, xPos+Width/2, yPos+Height+t/1.5); // x-axis Label
rotate(-PI/2); // rotate -90 degrees
text(yLabel, -yPos-Height/2, xPos-t*1.6+20); // y-axis Label
rotate(PI/2); // rotate back
textSize(10);
noFill();
stroke(0);
smooth();
strokeWeight(1);
//Edges
line(xPos-3, yPos+Height, xPos-3, yPos); // y-axis line
line(xPos-3, yPos+Height, xPos+Width+5, yPos+Height); // x-axis line
stroke(200);
if (yMin<0) {
line(xPos-7, // zero line
yPos+Height-(abs(yMin)/(yMax-yMin))*Height, //
xPos+Width,
yPos+Height-(abs(yMin)/(yMax-yMin))*Height
);
}
if (RightAxis) { // Right-axis line
stroke(0);
line(xPos+Width+3, yPos+Height, xPos+Width+3, yPos);
}
/* =========================================================================================
Sub-devisions for both axes, left and right
========================================================================================== */
stroke(0);
for (int x=0; x<=xDiv; x++) {
/* =========================================================================================
x-axis
========================================================================================== */
line(float(x)/xDiv*Width+xPos-3, yPos+Height, // x-axis Sub devisions
float(x)/xDiv*Width+xPos-3, yPos+Height+5);
textSize(10); // x-axis Labels
String xAxis=str(xMin+float(x)/xDiv*(xMax-xMin)); // the only way to get a specific number of decimals
String[] xAxisMS=split(xAxis, '.'); // is to split the float into strings
text(xAxisMS[0]+"."+xAxisMS[1].charAt(0), // ...
float(x)/xDiv*Width+xPos-3, yPos+Height+15); // x-axis Labels
}
/* =========================================================================================
left y-axis
========================================================================================== */
for (int y=0; y<=yDiv; y++) {
line(xPos-3, float(y)/yDiv*Height+yPos, // ...
xPos-7, float(y)/yDiv*Height+yPos); // y-axis lines
textAlign(RIGHT);
fill(20);
String yAxis=str(yMin+float(y)/yDiv*(yMax-yMin)); // Make y Label a string
String[] yAxisMS=split(yAxis, '.'); // Split string
text(yAxisMS[0]+"."+yAxisMS[1].charAt(0), // ...
xPos-15, float(yDiv-y)/yDiv*Height+yPos+3); // y-axis Labels
/* =========================================================================================
right y-axis
========================================================================================== */
if (RightAxis) {
color(GraphColor);
stroke(GraphColor);
fill(20);
line(xPos+Width+3, float(y)/yDiv*Height+yPos, // ...
xPos+Width+7, float(y)/yDiv*Height+yPos); // Right Y axis sub devisions
textAlign(LEFT);
String yAxisRight=str(yMinRight+float(y)/ // ...
yDiv*(yMaxRight-yMinRight)); // convert axis values into string
String[] yAxisRightMS=split(yAxisRight, '.'); //
text(yAxisRightMS[0]+"."+yAxisRightMS[1].charAt(0), // Right Y axis text
xPos+Width+15, float(yDiv-y)/yDiv*Height+yPos+3); // it's x,y location
noFill();
}
stroke(0);
}
}
/* =========================================================================================
Bar graph
========================================================================================== */
void Bar(float[] a, int from, int to) {
stroke(GraphColor);
fill(GraphColor);
if (from<0) { // If the From or To value is out of bounds
for (int x=0; x<a.length; x++) { // of the array, adjust them
rect(int(xPos+x*float(Width)/(a.length)),
yPos+Height-2,
Width/a.length-2,
-a[x]/(yMax-yMin)*Height);
}
}
else {
for (int x=from; x<to; x++) {
rect(int(xPos+(x-from)*float(Width)/(to-from)),
yPos+Height-2,
Width/(to-from)-2,
-a[x]/(yMax-yMin)*Height);
}
}
}
void Bar(float[] a ) {
stroke(GraphColor);
fill(GraphColor);
for (int x=0; x<a.length; x++) { // of the array, adjust them
rect(int(xPos+x*float(Width)/(a.length)),
yPos+Height-2,
Width/a.length-2,
-a[x]/(yMax-yMin)*Height);
}
}
/* =========================================================================================
Dot graph
========================================================================================== */
void DotGraph(float[] x, float[] y) {
for (int i=0; i<x.length; i++) {
strokeWeight(2);
stroke(GraphColor);
noFill();
smooth();
ellipse(
xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height,
2, 2
);
}
}
/* =========================================================================================
Streight line graph
========================================================================================== */
void LineGraph(float[] x, float[] y) {
for (int i=0; i<(x.length-1); i++) {
strokeWeight(2);
stroke(GraphColor);
noFill();
smooth();
line(xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height,
xPos+(x[i+1]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i+1]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height);
}
}
/* =========================================================================================
smoothLine
========================================================================================== */
void smoothLine(float[] x, float[] y) {
float tempyMax=yMax, tempyMin=yMin;
if (RightAxis) {
yMax=yMaxRight;
yMin=yMinRight;
}
int xlocation=0, ylocation=0;
// if(!ErrorFlag |true ){ // sort out later!
beginShape();
strokeWeight(6);
stroke(GraphColor);
noFill();
smooth();
maxY = 0;
//find max
for (int i=0; i<x.length; i++) {
if (maxY < y[i])
{
maxY =y[i];
maxI = i;
}
}
for (int i=0; i<x.length; i++) {
/* ===========================================================================
Check for errors-> Make sure time array doesn't decrease (go back in time)
===========================================================================*/
if (i<x.length-1) {
if (x[i]>x[i+1]) {
ErrorFlag=true;
}
}
/* =================================================================================
First and last bits can't be part of the curve, no points before first bit,
none after last bit. So a streight line is drawn instead
================================================================================= */
if (i==0 || i==x.length-2)line(xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height,
xPos+(x[i+1]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i+1]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height);
/* =================================================================================
For the rest of the array a curve (spline curve) can be created making the graph
smooth.
================================================================================= */
curveVertex( xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height);
/* =================================================================================
If the Dot option is true, Place a dot at each data point.
================================================================================= */
if (i == maxI)
{
ellipse(
xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height,
20, 20
);
}
if (Dot)ellipse(
xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width,
yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height,
2, 2
);
/* =================================================================================
Highlights points closest to Mouse X position
=================================================================================*/
if ( abs(mouseX-(xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width))<5 ) {
float yLinePosition = yPos+Height-(y[i]/(yMax-yMin)*Height)+(yMin)/(yMax-yMin)*Height;
float xLinePosition = xPos+(x[i]-x[0])/(x[x.length-1]-x[0])*Width;
strokeWeight(1);
stroke(240);
// line(xPos,yLinePosition,xPos+Width,yLinePosition);
strokeWeight(2);
stroke(GraphColor);
ellipse(xLinePosition, yLinePosition, 4, 4);
}
}
endShape();
yMax=tempyMax;
yMin=tempyMin;
float xAxisTitleWidth=textWidth(str(map(xlocation, xPos, xPos+Width, x[0], x[x.length-1])));
if ((mouseX>xPos&mouseX<(xPos+Width))&(mouseY>yPos&mouseY<(yPos+Height))) {
if (ShowMouseLines) {
// if(mouseX<xPos)xlocation=xPos;
if (mouseX>xPos+Width)xlocation=xPos+Width;
else xlocation=mouseX;
stroke(200);
strokeWeight(0.5);
fill(255);
color(50);
// Rectangle and x position
line(xlocation, yPos, xlocation, yPos+Height);
rect(xlocation-xAxisTitleWidth/2-10, yPos+Height-16, xAxisTitleWidth+20, 12);
textAlign(CENTER);
fill(160);
text(map(xlocation, xPos, xPos+Width, x[0], x[x.length-1]), xlocation, yPos+Height-6);
// if(mouseY<yPos)ylocation=yPos;
if (mouseY>yPos+Height)ylocation=yPos+Height;
else ylocation=mouseY;
// Rectangle and y position
stroke(200);
strokeWeight(0.5);
fill(255);
color(50);
line(xPos, ylocation, xPos+Width, ylocation);
//int yAxisTitleWidth=int(textWidth(str(map(ylocation, yPos, yPos+Height, y[0], y[y.length-1]))) );
rect(xPos-15+3, ylocation-6, -60, 12);
textAlign(RIGHT);
fill(GraphColor);//StrokeColor
// text(map(ylocation,yPos+Height,yPos,yMin,yMax),xPos+Width+3,yPos+Height+4);
text(map(ylocation, yPos+Height, yPos, yMin, yMax), xPos -15, ylocation+4);
if (RightAxis) {
stroke(200);
strokeWeight(0.5);
fill(255);
color(50);
rect(xPos+Width+15-3, ylocation-6, 60, 12);
textAlign(LEFT);
fill(160);
text(map(ylocation, yPos+Height, yPos, yMinRight, yMaxRight), xPos+Width+15, ylocation+4);
}
noStroke();
noFill();
}
}
}
void smoothLine(float[] x, float[] y, float[] z, float[] a ) {
GraphColor=color(188, 53, 53);
smoothLine(x, y);
GraphColor=color(193-100, 216-100, 16);
smoothLine(z, a);
}
}
import processing.serial.*;
int SerialPortNumber=2;
int PortSelected=2;
/* =================================================================================
Global variables
=================================================================================*/
int xValue, yValue, Command;
boolean Error=true;
boolean UpdateGraph=true;
int lineGraph;
int ErrorCounter=0;
int TotalRecieved=0;
/* =================================================================================
Local variables
=================================================================================*/
boolean DataRecieved1=false, DataRecieved2=false, DataRecieved3=false;
float[] DynamicArrayTime1, DynamicArrayTime2, DynamicArrayTime3;
float[] Time1, Time2, Time3;
float[] Voltage1, Voltage2, Voltage3;
float[] current;
float[] DynamicArray1, DynamicArray2, DynamicArray3;
float[] PowerArray= new float[0]; // Dynamic arrays that will use the append()
float[] DynamicArrayPower = new float[0]; // function to add values
float[] DynamicArrayTime= new float[0];
String portName;
String[] ArrayOfPorts=new String[SerialPortNumber];
boolean DataRecieved=false, Data1Recieved=false, Data2Recieved=false;
int incrament=0;
int NumOfSerialBytes=8; // The size of the buffer array
int[] serialInArray = new int[NumOfSerialBytes]; // Buffer array
int serialCount = 0; // A count of how many bytes received
int xMSB, xLSB, yMSB, yLSB; // Bytes of data
Serial myPort; // The serial port object
/* =================================================================================
A once off serail port setup function. In this case the selection of the speed,
the serial port and clearing the serial port buffer
=================================================================================*/
void SerialPortSetup() {
// text(Serial.list().length,200,200);
portName= Serial.list()[PortSelected];
ArrayOfPorts=Serial.list();
println(ArrayOfPorts);
myPort = new Serial(this, portName, 115200);
//delay(50);
//myPort.clear();
//myPort.buffer(20);
}
/* ============================================================
serialEvent will be called when something is sent to the
serial port being used.
============================================================ */
void serialEvent(Serial myPort) {
while (myPort.available ()>0)
{
/* ============================================================
Read the next byte that's waiting in the buffer.
============================================================ */
int inByte = myPort.read();
myPort.write(0);
if (inByte==0)serialCount=0;
if (inByte>255) {
println(" inByte = "+inByte);
exit();
}
// Add the latest byte from the serial port to array:
serialInArray[serialCount] = inByte;
serialCount++;
Error=true;
if (serialCount >= NumOfSerialBytes ) {
serialCount = 0;
TotalRecieved++;
int Checksum=0;
// Checksum = (Command + yMSB + yLSB + xMSB + xLSB + zeroByte)%255;
for (int x=0; x<serialInArray.length-1; x++) {
Checksum=Checksum+serialInArray[x];
}
Checksum=Checksum%255;
if (Checksum==serialInArray[serialInArray.length-1]) {
Error = false;
DataRecieved=true;
}
else {
Error = true;
// println("Error: "+ ErrorCounter +" / "+ TotalRecieved+" : "+float(ErrorCounter/TotalRecieved)*100+"%");
DataRecieved=false;
ErrorCounter++;
println("Error: "+ ErrorCounter +" / "+ TotalRecieved+" : "+float(ErrorCounter/TotalRecieved)*100+"%");
}
}
if (!Error) {
int zeroByte = serialInArray[6];
// println (zeroByte & 2);
xLSB = serialInArray[3];
if ( (zeroByte & 1) == 1) xLSB=0;
xMSB = serialInArray[2];
if ( (zeroByte & 2) == 2) xMSB=0;
yLSB = serialInArray[5];
if ( (zeroByte & 4) == 4) yLSB=0;
yMSB = serialInArray[4];
if ( (zeroByte & 8) == 8) yMSB=0;
// println( "0\tCommand\tyMSB\tyLSB\txMSB\txLSB\tzeroByte\tsChecksum");
// println(serialInArray[0]+"\t"+Command +"\t"+ yMSB +"\t"+ yLSB +"\t"+ xMSB +"\t"+ xLSB+"\t" +zeroByte+"\t"+ serialInArray[7]);
// >=====< combine bytes to form large integers >==================< //
Command = serialInArray[1];
xValue = xMSB << 8 | xLSB; // Get xValue from yMSB & yLSB
yValue = yMSB << 8 | yLSB; // Get yValue from xMSB & xLSB
// println(Command+ " "+xValue+" "+ yValue+" " );
/*
How that works: if xMSB = 10001001 and xLSB = 0100 0011
xMSB << 8 = 10001001 00000000 (shift xMSB left by 8 bits)
xLSB = 01000011
xLSB | xMSB = 10001001 01000011 combine the 2 bytes using the logic or |
xValue = 10001001 01000011 now xValue is a 2 byte number 0 -> 65536
*/
/* ==================================================================
Command, xValue & yValue have now been recieved from the chip
================================================================== */
switch(Command) {
/* ==================================================================
Recieve array1 and array2 from chip, update oscilloscope
================================================================== */
case 1: // Data is added to dynamic arrays
DynamicArrayTime3=append( DynamicArrayTime3, (xValue) );
DynamicArray3=append( DynamicArray3, (yValue) );
break;
case 2: // An array of unknown size is about to be recieved, empty storage arrays
DynamicArrayTime3= new float[0];
DynamicArray3= new float[0];
break;
case 3: // Array has finnished being recieved, update arrays being drawn
Time3=DynamicArrayTime3;
Voltage3=DynamicArray3;
// println(Voltage3.length);
DataRecieved3=true;
break;
/* ==================================================================
Recieve array2 and array3 from chip
================================================================== */
case 4: // Data is added to dynamic arrays
DynamicArrayTime2=append( DynamicArrayTime2, xValue );
DynamicArray2=append( DynamicArray2, (yValue-16000.0)/32000.0*20.0 );
break;
case 5: // An array of unknown size is about to be recieved, empty storage arrays
DynamicArrayTime2= new float[0];
DynamicArray2= new float[0];
break;
case 6: // Array has finnished being recieved, update arrays being drawn
Time2=DynamicArrayTime2;
current=DynamicArray2;
DataRecieved2=true;
break;
/* ==================================================================
Recieve a value of calculated power consumption & add it to the
PowerArray.
================================================================== */
case 20:
PowerArray=append( PowerArray, yValue );
break;
case 21:
DynamicArrayTime=append( DynamicArrayTime, xValue );
DynamicArrayPower=append( DynamicArrayPower, yValue );
break;
}
}
}
redraw();
// }
}
Arduino* (Touche Advanced Sensor)
//****************************************************************************************
// Illutron take on Disney style capacitive touch sensor using only passives and Arduino
// Dzl 2012
//****************************************************************************************
// 10n
// PIN 9 --[10k]-+-----10mH---+--||-- OBJECT
// | |
// 3.3k |
// | V 1N4148 diode
// GND |
// |
//Analog 0 ---+------+--------+
// | |
// 100pf 1MOmhm
// | |
// GND GND
#define SET(x,y) (x |=(1<<y)) //-Bit set/clear macros
#define CLR(x,y) (x &= (~(1<<y))) // |
#define CHK(x,y) (x & (1<<y)) // |
#define TOG(x,y) (x^=(1<<y)) //-+
#define N 160 //How many frequencies
float results[N]; //-Filtered result buffer
float freq[N]; //-Filtered result buffer
int sizeOfArray = N;
void setup() {
TCCR1A=0b10000010; //-Set up frequency generator
TCCR1B=0b00011001; //-+
ICR1=110;
OCR1A=55;
pinMode(9,OUTPUT); //-Signal generator pin
pinMode(8,OUTPUT); //-Sync (test) pin
Serial.begin(115200);
for(int i=0;i<N;i++) //-Preset results
results[i]=0; //-+
}
void loop()
{
unsigned int d;
int counter = 0;
for(unsigned int d=0;d<N;d++) {
int v=analogRead(0); //-Read response signal
CLR(TCCR1B,0); //-Stop generator
TCNT1=0; //-Reload new frequency
ICR1=d; // |
OCR1A=d/2; //-+
SET(TCCR1B,0); //-Restart generator
results[d]=results[d]*0.5+(float)(v)*0.5; //Filter results
freq[d] = d;
}
PlottArray(1,freq,results);
TOG(PORTB,0); //-Toggle pin 8 after each sweep (good for scope)
}
byte yMSB=0, yLSB=0, xMSB=0, xLSB=0, zeroByte=128, Checksum=0;
void SendData(int Command, unsigned int yValue,unsigned int xValue){
/* >=================================================================<
y = 01010100 11010100 (x & y are 2 Byte integers)
yMSB yLSB send seperately -> reciever joins them
>=================================================================< */
yLSB=lowByte(yValue);
yMSB=highByte(yValue);
xLSB=lowByte(xValue);
xMSB=highByte(xValue);
/* >=================================================================<
Only the very first Byte may be a zero, this way allows the computer
to know that if a Byte recieved is a zero it must be the start byte.
If data bytes actually have a value of zero, They are given the value
one and the bit in the zeroByte that represents that Byte is made
high.
>=================================================================< */
zeroByte = 128; // 10000000
if(yLSB==0){ yLSB=1; zeroByte=zeroByte+1;} // Make bit 1 high
if(yMSB==0){ yMSB=1; zeroByte=zeroByte+2;} // make bit 2 high
if(xLSB==0){ xLSB=1; zeroByte=zeroByte+4;} // make bit 3 high
if(xMSB==0){ xMSB=1; zeroByte=zeroByte+8;} // make bit 4 high
/* >=================================================================<
Calculate the remainder of: sum of all the Bytes divided by 255
>=================================================================< */
Checksum = (Command + yMSB + yLSB + xMSB + xLSB + zeroByte)%255;
if( Checksum !=0 ){
int inByte = Serial.read();
Serial.write(byte(0)); // send start bit
Serial.write(byte(Command)); // command eg: Which Graph is this data for
Serial.write(byte(yMSB)); // Y value's most significant byte
Serial.write(byte(yLSB)); // Y value's least significant byte
Serial.write(byte(xMSB)); // X value's most significant byte
Serial.write(byte(xLSB)); // X value's least significant byte
Serial.write(byte(zeroByte)); // Which values have a zero value
Serial.write(byte(Checksum)); // Error Checking Byte
}
}
void PlottArray(unsigned int Cmd,float Array1[],float Array2[]){
SendData(Cmd+1, 1,1); // Tell PC an array is about to be sent
delay(1);
for(int x=0; x < sizeOfArray; x++){ // Send the arrays
SendData(Cmd, round(Array1[x]),round(Array2[x]));
//delay(1);
}
SendData(Cmd+2, 1,1); // Confirm arrrays have been sent
}
My final project was meant to be a very loosely based game on collecting water and watching a plant grow and having a real life replica of the plant mimicking this movement.
Initial screen:
The first stage:
The second stage:
Final screen:
The layout at the showcase:
After the user-testing I did in class and outside of it, I realized some fundamental problems with the game. Firstly, the motive of the game was not really clear to the people interacting with it until after they started the game; however, by that point it was pretty intuitive that they had to move the plant in real life to see the change on screen. Also, I felt as if labelling the project a ‘game’ implied that there was a way to win (getting a certain number of drops within a certain number of time); therefore, when there was no real indication if people won or loss, they kind of didn’t know what to do. This was an error on my part as I should’ve made a very clear winning/losing situation or just a win-win situation but with a more obvious ending. For this, I wanted a flower to grow to portray the final stage of growth but I didn’t know how I could implement this in the short amount of time I had considering I would have to change a lot of the code.
I think what did work was the implementation of handles on the pot, this was a clear indicator that people had to hold it from both sides. The arrows on the board it rested on, also helped. Moreover, once people did start, a lot of them appreciated the calmness they felt in doing the act and a lot go gasps of excitement were heard when the plant did grow on screen. A few people commented on the good color palette/’clean’ look of the whole experiment and I felt very proud of that because neatness is pretty important to me personally. Another viewer also commented how the movement of this game closely resembled activities that are used with patients that are exercising mobility of body parts and how this could extend to that application which I found very, very cool.
Personally, although a few aspects of the project did not end up looking like I had hoped they would, I have never felt more proud being present around people looking at my work and smiling/laughing because of it — it felt amazing. It especially felt great when Craig and Sarah told me I should be proud of myself and I was! — I worked on this code for 2 weeks straight (with help) but also did most of it by myself which I never thought I would be able to say about something that looked like this. I think that’s something that this course taught me, there’s a lot of stuff I CAN do, I just need to stop telling myself that I can’t. So overall, I know there’s a lot to be improved upon but I think I’m still pretty happy with the result. With special thanks to Aaron, Jack, Ali, Yousra, the UNIX lab and Daniel Shiffman.
Code:
Game
import processing.video.*;
import jp.nyatla.nyar4psg.*;
Capture cam;
MultiMarker nya;
Catcher catcher;
Timer timer;
Drop[] drops;
//End[] ends = new End[10];;
int totalDrops = 0;
int numCaught = 0;
PImage flowers;
PImage bFlowers;
PImage background;
PImage black;
//PImage end;
int level;
// 1: First plant stage
// 2: Second plant stage
int time;
String timeString = "00";
//String counter = "00";
int initialTime;
int interval = 1;
int totalTime = 60000;
float x=0;
int value = 32;
int intTimeString;
int gameScreen;
// 0: Initial Screen
// 1: Game Screen
// 2: Game-over Screen
void setup() {
fullScreen(P3D);
//size(640,480,P3D);
fill(0);
background = loadImage("background.jpg");
catcher = new Catcher(60); // Create the catcher with a radius of __
drops = new Drop[2500];
timer = new Timer(300);
flowers = loadImage("flowers.png");
bFlowers = loadImage("bFlowers.png");
//end = loadImage("end.png");
initialTime = millis();
//for (int = i; i < ends.length; i++) {
// ends[i] = new End(100+i*100, 300, random(32,72));
//}
cam=new Capture(this, 1440, 900);
nya=new MultiMarker(this, 1440, 900, "camera_para.dat", NyAR4PsgConfig.CONFIG_PSG);
nya.addNyIdMarker(0, 80);
cam.start();
}
void draw() {
background(0);
if (gameScreen == 0) {
initScreen();
} else if (gameScreen == 1) {
if (cam.available() !=true) {
}
gameScreen();
cam.read();
nya.detect(cam);
pushStyle();
imageMode(CORNER);
//nya.drawBackground(cam);
popStyle();
if ((!nya.isExist(0))) {
} else {
x += (nya.object2ScreenCoordSystem(0, 0, 0, 0).x-x)*.1;
//println(x);
}
catcher.setLocation(width-x);
catcher.display();
} else if (gameScreen == 2) {
gameOverScreen();
}
}
void initScreen() {
background(255);
image(bFlowers, width/2, height/3);
imageMode(CENTER);
fill(50, 100, 150);
textSize(18);
textAlign(CENTER);
text("MOVE THE PLANT SLOWLY TO COLLECT AS MANY WATER DROPS AS YOU CAN", width/2, height/2);
text("PRESS SPACEBAR TO START", width/2, height/1.7);
}
void keyPressed() {
if (value == 32) {
startGame();
time = 0;
}
}
void startGame() {
gameScreen=1;
}
void gameScreen() {
imageMode(CORNER);
image(background, 0, 0);
catcher.display();
if (timer.isFinished()) {
drops[totalDrops] = new Drop();
totalDrops ++ ;
if (totalDrops >= drops.length) {
totalDrops = 0;
}
timer.start();
}
for (int i = 0; i < totalDrops; i++ ) {
drops[i].move();
drops[i].display();
if (catcher.intersect(drops[i])) {
drops[i].caught();
numCaught++;
}
}
if (numCaught==15) {
level = 1;
} else if (numCaught==25) {
level = 2;
}
if (millis() - initialTime > interval)
{
time += 1;
timeString = nf(time, 2);
initialTime = millis();
}
intTimeString = parseInt(timeString)/60;
timeString = Integer.toString(intTimeString);
text("TIME: " + timeString, width/9, height/8);
textSize(22);
if ((time>=totalTime) || (numCaught>=30)) {
gameOverScreen();
}
}
void gameOverScreen() {
cam.stop();
background(255);
fill(50, 100, 150);
textAlign(CENTER);
textSize(22);
text("GAME OVER", width/2, height/2.3);
textSize(20);
text("PRESS THE SCREEN TO REPLAY", width/2, height/2);
imageMode(CORNER);
//or (int = i; i < ends.length; i++)
//ends[i].ascend();
//ends[i].display();
//ends[i].top();
}
void mousePressed() {
gameScreen = 1;
//loop();
Catcher class
class Catcher {
float r; // radius
color col; // color
float x, y; // location
int w = 200;
int h = 580 ;
Catcher(float tempR) {
r = tempR;
col = color(50, 10, 10, 150);
x = 0;
y = height - 200;
}
void setLocation(float tempX) {
x = tempX;
}
void display() {
stroke(0);
fill(col);
int startPoint = 200*level;
PImage flower = flowers.get(startPoint, - height/3, w, h);
image(flower, x, y);
imageMode(CENTER);
}
// A function that returns true or false based on if the catcher intersects a raindrop
boolean intersect(Drop d) {
// Calculate distance
float distance = dist(x, y + 120, d.x, d.y);
if (distance < r + d.r) {
return true;
} else {
return false;
}
}
}
Drops class
class Drop {
float x, y; // Variables for location of raindrop
float speed; // Speed of raindrop
color c;
float r; // Radius of raindrop
Drop() {
r = 8; // All raindrops are the same size
x = random(width); // Start with a random x location
y = -r*4; // Start a little above the window
speed = random(3, 6); // Pick a random speed
c = color(50, 100, 150); // Color
}
// Move the raindrop down
void move() {
// Increment by speed
y += speed;
}
// Check if it hits the bottom
boolean reachedBottom() {
// If we go a little beyond the bottom
if (y > height + r*4) {
return true;
} else {
return false;
}
}
// Display the raindrop
void display() {
// Display the drop
fill(c);
noStroke();
for (int i = 2; i < r; i++ ) {
ellipse(x, y + i*4, i*2, i*2);
}
}
// If the drop is caught
void caught() {
// Stop it from moving by setting speed equal to zero
speed = 0;
// Set the location to somewhere way off-screen
y = -1000;
}
}
Timer class
class Timer {
int savedTime; // When Timer started
int totalTime; // How long Timer should last
Timer(int tempTotalTime) {
totalTime = tempTotalTime;
}
// Starting the timer
void start() {
// When the timer starts it stores the current time in milliseconds.
savedTime = millis();
}
boolean isFinished() {
// Check how much time has passed
int passedTime = millis()- savedTime;
if (passedTime > totalTime) {
return true;
} else {
return false;
}
}
int getTime() {
return millis()/600;
}
}
A class of flowers I was considering ending at the end screen:
I had Dhabia, Nick and Pangna user test my project. I think that the key takeaways that I got were the following:
Adding signifiers or instructions of some sorts so users would know when to pet the animal or harvest resources and how they would do this.
Featuring my (somewhat vague) instructions in a more prominent location on the screen.
Fixing boundary issues between the player and the resources and animals.
Some issues with the touch sensor, thinking of possibly making the hit location of the “hammer” smaller.
I think that the most important thing that can contribute to user experience is my use of instructions. Furthermore, people did not engage with the game as I thought they would. I wanted the users to pet the animals and then harvest the resources at each level, to fully see how the environment changes throughout the game, however, they often just wanted to complete the game as fast as they could.
User testing was extremely useful because a lot of what I have been doing for the past few days has become very narrow. I think it has been really hard to step out of my biases, my experiences and my intentions for the game that I neglected instructions and very basic things. I am lucky because there is very little that I have to do for the project to be complete by Thursday so I can really focus on fine-tuning the little things before the show.
So… I’m definitely not close to finishing my game and I know it’s missing a lot of things but I decided to test it out anyway so far. All of my users said the same things which I kind of already knew but:
1). Although the collecting droplets is pretty intuitive, there should be an initial start screen with instructions/info and “press start”.
2). The timer should count down instead of up (right now is confused as a score). So, could also include a score.
3). There needs to be a “try again” or “you win” kind of ending because people can’t actually tell how they’ve done.
While testing my final project, I ran into a few issues:
The soundtrack that plays as soon as the user steps in front of the exhibit is longer than the actual interaction.
Solution: Make the soundtracks shorter, test them again.
2. Two users began looking at the exhibit from the left, whereas one began from the right side. This is an issue because the left side is where the “first” soundtrack plays.
Solution: to fix it so that it wouldn’t matter which direction the user is coming from.
3. The user assumes there’s something they can touch, play with, etc. whereas the only actual interactions are based on sensors, distance, and body motion.
Solution: Improve the iPhone exhibit and add a more interactive component, since the motor and the sensor controlling the phone from under a glass cover is not a very straightforward interaction, and two users did not realize what was going on, or that their distance from the sensor was controlling the motor.
4. For someone who doesn’t focus on the background soundtrack, it is not clear what exactly is happening, or what the context of the whole “Future Museum” exhibit thing is. There need to be more visual cues.
Solution: Provide some form of description, or instructions? (Not sure about this one yet)
5. The webcam on the ‘Snapchat simulator’ kept lagging, and the program was running slow. Also, the camera was flipped and a little bit too zoomed so it didn’t feel very natural or selfie-like.
Solution: I think I’ll be able to, with some help, fix the camera flip situation. However, I was told that Processing doesn’t work very fast with cameras and video, so it is possible that I won’t be able to significantly improve the speed. I’ll have to ask Jack for help.
Our user testing pointed out a couple of imperfections that, however, can be relatively easily fixed:
the users do not know they are supposed to hold the button to scream – instructions said only “Scream as loud as you are stressed”.
also, instructions overall need to be more visible and readable, some more clearly formulated (which we wanted to print out and paste only after user testing anyway, exactly for this reason)
the visuals on the screen are a little difficult to see and to distinguish, what’s happening. Solution for this: adjust the colors and the mapped values that affect the speed and radius of the path. We need to make more clear that people are submitting some data, all of which are visualized.
the program is getting a little laggy with more inputs – using an iMac instead of a laptop with a larger monitor can be a potential solution
The first version of the videogame was done and we tested with three users.
The feedback given by user 1 was: Make the cursor brighter, avoid mirror effect with the cursor, have fewer targets that go faster, create random motions with the targets, and instructions so the user knows how to play;
The feedback given by user 2 was: Try to add more animations special effects so it feels more like a videogame, Make the cursor brighter and avoid the mirror effect.
The feedback given by user 3 (Jack) was: Modify the algorithm(Right now I am detecting the closer object in the Kinect in certain thresholds, but Jack told me that I would be a better idea to detect the change in the motion by making a comparison among different frame counts), to make the cursor more noticeable, and to declare the animations in the set up to make the code runs smoother
Conclusion and observations of the user tests:
We need to improve the design because users take time to figure it out what’s going on and it is still intuitive.
We will try the algorithm suggested by Jack to see if the movement detection accuracy improves.
We will use Jack advice to add properly the animation needed to improve the experience of the game.