[Final Project] Pen Plotter

Concept

We (Samyam and Tim) designed a physical device that lets a user draw a sketch of their choice on p5.js and replicate it physically using a polar coordinate pen (plotter). In essence, the user can interact and control the device using a sketch as simple as an Archimedean spiral or as complex as a zine design of an engineering creation. It boils down to the user’s choice. Besides, the interconnectivity of the sketch, inspired by the Eulerian trial, adds to the artistic element of the system.

Drawing Platform

The project was primarily divided into four sections:

  1. Hardware and System Design
  2. p5.js Development
  3. Arduino Development
  4. Implementation and interaction between components

Hardware/ Device Design

The system comprises of a bunch of mechanical components and electrical motors that complement the functionality of the device. The device is based on a 2-DOF system in which the plotter moves to and fro on a linear rail, while the bottom plate follows the rotational motion orthogonal to the linear rail. The movement of the base plate facilitates a variety of designs, thus allowing the user to sketch any form of drawing.

First, the 3D model of the device was completed using Autodesk Fusion 360 – afterward, the components were either laser cut or 3D printed. We laser cut the rack and pinion as well as the legs of the device, while the spacer between the laser-cut components, rotating base plate and the plotter holding components were 3D printed. In addition, we have purchased a linear guide rail, step motors and stepper-motor-driver board for precision drawing. Then, all of the components, including servo motors, were assembled to complete the physical design.

The motors and the driver boards are connected to the Arduino board, which is connected to p5.js to allow the transmission of user input into the physical system.

p5.js Coding

All the p5.js coding files are divided into two main JavaScript files:

  1. sketch.js: Contains the majority of the algorithm
  2. keyboard_keys.js: Contains the functions to track cursor movement

In addition, there is one “p5.web-serial.js” file that contains web serial functions adapted from the public web-serial library (Link).

The majority of the device’s software design depends on the p5.js component — from storing user input to transmitting data to the Arduino in the most efficient format.

At first, the user sketches a drawing using a mouse, a trackpad or a smart stylus. The interconnectivity of the design allows the user to connect all the components of the sketch. A simple if-conditional ensures that the sketch is being drawn, thus storing cartesian coordinates of the sketch only when the different points are registered; in return, this improves the efficiency of the system together as well as restricts the possibility of redundancy and system overloading. This functionality is further dependent on manually created functions like “mousePressed()” and “mouseReleased()” that alter a boolean “isClicked” based on the status of the drawing.

Here, we have moved the origin to the center of the canvas to replicate real-life situations. All the cartesian coordinates are then stored in an array titled “Positions[]” in a vector format. Since the physical platform is based on polar coordinates, all the coordinates need to be converted to the said format. Thus, a separate function named “cartToPolar()” is written that takes cartesian x and y coordinates as arguments and returns a list containing two equivalent attributes – radius and the angle of inclination.

function cartToPolar(x, y) {
  let radius, angle;
  let old_angle;
  let curr_angle;

  radius = sqrt(sq(x) + sq(y));

  angle = atan(abs(y) / abs(x));

  if (x >= 0 && y >= 0) {
    angle = angle;
    firstQuad = true;

    if (fourthQuad) {
      fullRotationNum++;
      fourthQuad = false;
    }

  } else if (x <= 0 && y >= 0) {
    angle = 180 - angle;
  } else if (x <= 0 && y <= 0) {
    angle = 180 + angle;
  } else if (x >= 0 && y <= 0) {
    angle = 360 - angle;
    fourthQuad = true;

    if (firstQuad) {
      fullRotationNum--;
      firstQuad = false;
    }
  }
  
  angle += fullRotationNum * 360;
  
  if(firstAngle){
    firstAngle = false;
    tempAngle = angle;
  }else{
    if(tempAngle - angle > 180){
      fullRotationNum++;
      angle += 360;
    }if(tempAngle - angle < -180){
      fullRotationNum--;
      angle -= 360;
    }
    tempAngle = angle;
  }

  let temp_list = [];
  temp_list[0] = map(radius, 0, sqrt(2 * sq(width / 2)), 0, disc_radius); // Mapped radius
  temp_list[1] = angle;

  return temp_list;
}


Here, we can see that the function receives x and y coordinates as arguments. Immediately after that, the radius is computed using a simple mathematical formula (d = (x^2 + y^2)^(0.5)), while the absolute value of the radius is determined using trigonometric relations. In order to compute the exact value of the angle, the quadrant of the coordinates is determined using a couple of if-statements and the absolute value computed above is modified. The process of altering the angle’s value is special in the first and the fourth quadrant. In the case of the first quadrant, a boolean ‘firstQuad’ is set to true and a variable ‘fullRotationNum’ is incremented that controls the rotation of the step motor fixed on the bottom plate. Similarly, in the case of the fourth quadrant, the boolean ‘fourthQuad’ is set to true and the variable ‘fullRotationNum’ is decremented. Since the rotation on a plane is calculated on a scale of 2 PI (360 degrees), this variable maintains the rotation angle without altering the motion of the step motor. Afterward, the true angle is increased based on the value of this variable.

Then the second set of if-conditionals is implemented to solve a bug in the system. Initially, without these statements, the bottom stepper motor would retract occasionally during a massive jump in the angular value, so these conditions manually compare the value of the angle with the old value stored in ‘tempAngle’ and if the jump is of a particular value, certain states of the if-conditions are executed, i.e., increasing and decreasing the value of ‘fullRotationNum’ and ‘angle’ respectively.

Finally, the radius is mapped to a scale of the maximum radius of the physical disc and the list containing two values – “mapped radius” and “modified angle” is returned to the callee function. Then, the data are transmitted to the Arduino after the user clicks the ‘SEND DATA’ button.

function button(text_, x, y, w, h) {
  // Checks if the cursor is within the button or not
  let isWithinButton =
    mouseX < x + w && mouseX > x && mouseY < y + h && mouseY > y;

  // Hover Effect
  if (isWithinButton) {
    fill(143, 148, 123);
    cursor(HAND);
  } else {
    fill("grey");
    cursor(ARROW);
  }

  // Button Setting
  stroke("black");
  strokeWeight(2);
  rect(x, y, w, h, 5);

  // Text inside the button
  textFont("Helvetica");
  stroke(5);
  textSize(25);
  fill("white");
  text(text_, x + 18, y + 32);

  // Return a boolean value
  return isWithinButton;
}

The algorithm behind the ‘SEND DATA’ button is coded manually in a function titled ‘send_data_button’. This function relies on a mother function ‘button’ that takes (1) the name of the button, (2) (3) x and y coordinates of the button and (4) (5) the height and width of the button. In order to create a hover effect, the size and color of the button are altered using if-conditions. Then, the function compares the location of the cursor with the location of the button and returns a boolean value indicating whether the cursor is inside or outside the button (true indicates inside and false indicates outside). Thus, when ‘send_data_button’ is coded, it first creates the button and then begins the data transmission process.

let first_point = false;

// Function that sends data to arduino
function send_data_button() {
  let x = canvas_w - 210;
  let y = canvas_h - 70;
  let w = 180;
  let h = 50;

  // If the cursor is within the button button() function returns 1, else 0;
  let sendBool = button("SEND DATA", x, y, w, h);

  // Sending the data if the cursor iswithin the button and mouse is clicked
  if (sendBool && mouseIsPressed && !first_point) {
    serial.write(String("H"));
    first_point = true;
    startSending = true;
    
    button_bool = true;
    print("Homing Machine...");
    
  
  }
  else
    button_bool = false;
}

The entire process of sending data to the Arduino depends on two boolean variables – “startSending” and “drawingFinished”. At the beginning of the transmission, a single character ‘H’ is transmitted to the Arduino in the form of a string. It indicates that the first coordinate is ready to be sent thus setting the variable ‘firstPoint’ to true inside the ‘send_data_button’ and displaying a message on the console that says ‘Homing Machine’, which essentially determines the position of the absolute origin or reference point of the plotter (it is completed every time a sketch has to be drawn). Then, the program continues inside the native draw() function.

let tmp = 1;
function draw() 
{
  background(203,203,205);
  myCirc();
  instruction_txt();
  
  //Buttons
  send_data_button();
  reset_button();

  // Translate the origin point to the center of the screen
  translate(width/2, height/2);
  
  
  // Restricting the sketch within the canvas
  let d_comp = pow((sq(mouseX - (width/2)) + sq(mouseY - (width/2))), 0.5);
  if (isClicked && (d_comp >= (sketch_radius/2)))
  {
    // If the cursor is outside of the circle and the button, execute this condition
    if (!button_bool)
    {
      isClicked = false;
      print("Draw within the canvas!");
    }
    
    // If the cursor is outside of the circle but within the button, exectute this condition
    else
    {
      isClicked = false;
      print("Button clicked!");
    }
    
  }
    

  // Make sure the mouse is clicked and cursor position is different
  if (isClicked && mouseX !== pmouseX && mouseX !== pmouseY) 
  {
    
    // Create a vector and add it to the list
    // let pt = createVector(mouseX, mouseY);          // When origin is at the top-left corner
    let pt = createVector(mouseX - width / 2, mouseY - height / 2);
    positions.push(pt);

    // Handle the case when x = 0
    if (pt.x == 0) pt.x = 0.01;

    // Mapping Cartesian to Polar and appending it in mappedPositions array
    let temp_list = [];
    temp_list = cartToPolar(pt.x, pt.y);
    let pt_mapped = createVector(temp_list[0] * one_px_mm, temp_list[1]);
    mappedPositions.push(pt_mapped);
    
    
    print("\nCounter: " + tmp);
    // Printing co-ordinates stored in the list(s)
    print("Cartesian: x: " + pt.x + " and y: " + pt.y);
    print("Polar:     r: " + pt_mapped.x + " and Angle: " + pt_mapped.y);
    tmp++;
  }

  // Draw Settings
  noFill();
  strokeWeight(5);
  strokeJoin(ROUND);

  // Go through the list of vectors and plot them
  beginShape();
  for (let i = 0; i < positions.length; i++) {
    let pt = positions[i];
    curveVertex(pt.x, pt.y);
  }
  endShape();

  
  
  // Data Transmission 
  if (startSending)
  // if (startSending) {
    if (inData == "0") 
    {
      let temp_var =
        str(mappedPositions[i].x) + "," + str(mappedPositions[i].y);
      let percent = int((i / mappedPositions.length) * 100);
      print("[" + percent + "% completed] " + temp_var);          // Progress on Console

      serial.write(String(temp_var));

      i += 1;
      
      
      // Check if all the points are trasmitted
      if (i == mappedPositions.length) {
        startSending = false;
        drawingFinished = true;
      }

      inData = "1";        // Reset the watch-dog variable
      
      
      if (i >= 1)
        first_point = false;
    }
  }
  
  // Change the settings after completing the drawing
  if (drawingFinished) {
    serial.write(String("E"));
    print("completed!");
    i = 0;

    startSending = false;
    drawingFinished = false;

    firstQuad = false;
    fourthQuad = false;
    fullRotationNum = 0;
  }

 

Just after plotting the points of the sketch on the p5.js canvas by looping through the cartesian list, the program checks the status of the ‘startSending’ boolean – if it is set to true and the value of value received from Arduino is ‘0’, it concatenates the radius and the angle of the point separated by a comma. This concatenated string, stored in a variable called ‘temp_var’, is sent to the Arduino (in the form of a string). This process of transmitting the entire message in the form of a string is done by calling the “String()” function of p5.js. Without this explicit call of the function, the message transmission is interrupted, thus the solution. Afterward, the status of the ‘firstPoint’ is set to false after the first coordinate has been sent. Similarly, one if-condition checks if all coordinates have been sent – when it evaluates to true, “drawingFinished” is set to true whereas “startSending” is set to false.

In addition to the functions/ methodologies described above, the program consists of a variety of serial communication functions like portConnect() and portDisconnnect() to track the physical connection between p5.js and the Arduino. At first makePortbutton() is called to create a button on the canvas that allows the user to select the wired connection. One important function is ‘serialEvent()’ function that tracks the data received from the Arduino and stores it in ‘inData’ variable. All these functions are mainly called in the setup() function and display corresponding messages on the console. Essentially, these functions facilitate the error-handling functionality of the system, thus avoiding unintended interruptions of the process.

Arduino Coding

The Arduino board is coded to control the motion of two major components in the physical system: (1) a servo motor (connected to the vertically mobile plotter) and (2) two stepper motors (connected to the linear rail and the bottom plate).

At the very beginning of the program, two libraries (AccelStepper.h and Servo.h) are loaded in the project — we will use the methods of these libraries throughout the sketching process. The system uses two stepper motors as described earlier so global variables are fixed at the beginning of the code for individual motors. Afterward, basic settings for the step motors are set up — for example, determining the MotorInterfaceType (that allows four wire connections in a half-step mode) for the step motor, creating two individual instances of stepper motor classes from the imported library (armStepper and plateStepper respectively) and creating an object of the servo class called ‘penServo’. The AccelStepper library allows us to connect multiple stepper motors with controlled acceleration and deceleration. Similarly, different global variables are instantiated that are required throughout the sketching process.

Inside the setup() function, pinModes for input and output are set up. Similarly, the maxspeed and maxAcceleration of both the stepper motors are set to 1000 steps per cycle and 200 steps per second squared respectively. Also, the name (number) of the pin attached to the servo and the control of its shaft is controlled via attach() and write() methods of the servo library.

void homeMachine(byte _servoAngle) {
  penServo.write(90);
  armStepper.setSpeed(-400);
  armStepper.runSpeed();
  if (digitalRead(limitSw) == 1) {
    armStepper.setCurrentPosition(0);
    armStepper.moveTo(0);
    armStepper.runToPosition();
    centerPen(_servoAngle);
    Serial.write(0);
  }
}

 

Inside the draw() function, the program homes the machine using a manual function titled ‘homeMachine()’ that takes ‘servo angle’ as an argument. This function sets the angle of the servo to 90 and the speed of the steppers to -400. Then it checks if the value stored in ‘limitSw’ ( variable that stores input value) is ‘1’ (when the switch is pressed), the stepper attached to the plotter (arm) moves to the origin point and a reference point is established by calling a function called ‘centerPen()’ that takes servo angle as input.

void centerPen(byte _servoAngle) {
  armStepper.moveTo((int)(25 / halfStepToMm));
  armStepper.runToPosition();
  armStepper.setCurrentPosition(0);
  armStepper.setMaxSpeed(1000);
  penServo.write(_servoAngle);

  homing = false;
}

 

In the linear rail, 0.02047 mm equals 1 step, thus using this value, the function ‘centerPen()’ moves the pen to the location of the reference point, while the value received from the argument controls the angular movement of the motor. After this function, the ‘homing’ variable is set to false marking the end of the first step.

if (Serial.available() > 0) {
    input = Serial.readString();

    if (input == "H") {
      homing = true;
      firstPoint = true;
      machineStart = true;
    } else if (input == "E") {
      homing = true;
      machineStart = false;
      firstPoint = true;
      drawing = false;ic
    }
  }

  if (machineStart && input.length() > 3) {
    coordVal[0] = input.substring(0, input.indexOf(",")).toFloat(); // r
    coordVal[1] = input.substring(input.indexOf(",") + 1, input.length()).toFloat();  //theta

    if (firstPoint) {
      penServo.write(90);
      armStepper.setCurrentPosition(0);
      plateStepper.setCurrentPosition(0);
      firstPoint = false;
      angleTemp = coordVal[1];
    } else if (drawing) {
      penServo.write(110);
    }

    if (abs(angleTemp - coordVal[1]) > 90) {
      drawing = false;
      d.write(95);
    }

    armStepper.setMaxSpeed(1000);
    plateStepper.setMaxSpeed(1000);

    armStepper.moveTo((int) (coordVal[0] / halfStepToMm));
    armStepper.run();
    plateStepper.moveTo(-1 * (int) (coordVal[1] / halfStepToDegrees));
    plateStepper.run();

    if (armStepper.distanceToGo() == 0 && plateStepper.distanceToGo() == 0) {
      angleTemp = coordVal[1];
      Serial.write(0);
      drawing = true;
      input = "";
    }
  }

The program then checks the data received from p5.js. If the value is “H”, homing, machineStart and firstpoint variables are set to true thus homing the machine before every sketch. Similarly, if the value is “E”, corresponding attributes are altered to facilitate different functionality of the device.

Based on the attributes set above and the data received from p5.js, the program proceeds. The message is sliced and ‘radius’ and ‘angle’ are stored in an array coordVal[].

If the firstPoint variable is set to true, the servo angle is set to 90 and both the stepper motors are moved to position 0 and the angular value is stored temporarily; in the case, it’s not the first point, the servo angle is set to 110. Then, the angular value stored in the temporary variable and in the arrays are compared, and the global variable ‘drawing’ is set to ‘false’ if the difference is greater than 90.

Afterward, the positions of the stepper motors are changed based on the values received from p5.js. Using the radius of the point, the plotter’s target position is set; also, the plate’s target position is set using the radius and halfStepToDegrees conversion. Then, if the distance(s) from the current position to the target position of both the stepper motors are 0 respectively, the drawing variable is set to true and a string ‘0’ is sent to p5.js. This way, the device moves based on the input from p5.js and a sketch is replicated on the device.

Communication between Arduino and p5.js

For this project, Arduino and p5.js communicate with one another in two ways – in other words, it is a bidirectional communication. At first, when the user clicks the “SEND DATA” button on the p5.js canvas, p5.js sends a character “H” in the form of a string followed by a series of strings in the format <radius, angle> (here bracket do not hold any significance). Once the first coordinate is sent, Arduino verifies its authenticity, and if it is a valid one, the stepper motors and the servo motor take a new position(s). Upon successful completion of the movement, Arduino sends back “0” as a string to p5.js. This instructs p5.js to send a second coordinate and so forth. This way, two-way communication takes place before sending a new set of coordinates.

Proud Aspects

The project has been a demanding undertaking. From designing hardware components and waiting for hours to get parts printed to designing the software that dictates the functioning of the device has been an arduous task. Despite the number of bugs faced, we were able to find our way through, and here is our final project.

During this time, the phase that required a notable amount of time was ‘cartToPolar(x, y)’ function that we manually coded. As described earlier, it transforms ordinates of individual cartesian coordinates into polar form since the machine understands polar coordinates only. Thus, this function was a no-brainer for the device to function. We initially coded the function based on our assumptions. However, as moved to the user-testing phase, we faced a few bugs that would not hinder the performance per se but rather would reduce the charm of the project. Thus, after hours and even days of debugging, we were able to finalize the project ideas. Now, this function controls the motion of the device. It takes a set of extremely raw values and transforms them into a value that the drawing platform understands. Besides, the function handles the extreme conditions of the radii values; this way, we were able to avoid situations where the program may crash. It took a massive amount of our time as well as Mathematics oriented brainstorming.

Similarly, building the whole platform was a task in itself. We were assembling parts built from different processes — laser cutting and 3D printing — thus, when the device took a shape, it was an eternal feeling. We were able to precisely determine its structure beforehand and correctly build its physical structure. It is something that we are definitely happy about.

Interaction

In this project, the user is in charge of the drawing. There is a circle on the p5.js canvas; this is where the user can draw any sketch of their choice — here we are basically converting our square canvas to a circular one to replicate the base plate of the device. Once, the drawing is completed, the user can click ‘SEND DATA’ button and wait for the drawing to complete.

Future Improvements

In this project, we were able to reduce the time the device requires to draw a sketch by half. The device consists of numerous individual elements, thus after carefully redoing software algorithms, we were able to bring down the time complexity of the physical device. That said, the device still takes some time to complete the drawing. Thus, our future priority would be to reduce the time even more so that it functions at its peak speed. This could be done by recording every single point through which the cursor passes – at present, the speed of the cursor determines the number of points recorded, so this could be solved in future iterations to reduce the noise.

Similarly, the parts are 3D printed and the components we purchased belonged to a comparatively affordable category. Thus, in future iteration(s), we would build the physical components using more expensive and reliable components, thus avoiding the noise that these components catch at present. Even though the noise is negligible, at some point, the user may notice it. Thus, our priority would be to decrease the noise using high-quality components.

Individual Contribution

Task Person
Hardware Design & Assembly Tim
Arduino Tim
p5.js Samyam
Design Implementation Samyam
Hardware + Software Communication Both
User Testing Both
Blogs/ Documentation and Video Samyam
Bugs and Final Polishing Both
Hardware Casing Tim

Reflection

This project took a total of more than two weeks to complete — from designing its raw structure to actually building it and integrating it with the software component. Sometimes, we were stuck on a bug for days even though the idea would work theoretically, while other times, every testing would go as planned. Nevertheless, it was an interesting project and we are glad about the way the project turned out to be.

Yay! It’s working.

Watch the user testing here:

Final Sketch:

Final Sketch

Find the GitHub link to the project here.

Final Projects User Testing

Progress

Following our last blog post, we (Samyam and Tim) are almost done with the project. The project was divided into three different major components:

    1. Building Hardware
    2. p5.js component
    3. Arduino component

After assembling the 3D printed and laser-cut pieces with motors, we have made some changes in the p5.js component – it has been adjusted so that the latency in data transfer does not affect the bidirectional communication between p5.js and the Arduino. In the same way, the Arduino code has been modified to facilitate smooth communication between those two components. Once the user clicks the SEND DATA button, p5.js sends data to the drawing arm via the Arduino, which in return sends a confirmation message to p5.js; this way, p5.js knows when to send the next message. In the meantime, the physical arm keeps on drawing the sketch. When all the data are sent from p5.js, the completed sketch can be found on the base plate of the platform.

User Testing

Here, after setting up the device, we draw a sketch of a star with a curved hook on one edge. Once the user clicks on SEND DATA button, mapped coordinates (mapped to polar coordinates) are relayed to the Arduino, and the sketch is drawn on the plate. The progress of the sketch can be tracked using the p5.js console, where the completion percentage as well as the coordinates transferred are displayed.

Find the user-testing video here along with the final sketch drawn on the plate.

The completed sketch:

Final Sketch

Future Plans

The device is functional and working as expected. Thus, we plan on further enhancing the hardware as well as software components of the project. The time the device takes to complete the sketch immensely depends on the complexity of the sketch, thus our plan is to improvise its performance as much as possible. In addition, the base plate will be re-engineered so that the user can place a paper for better visibility of the final sketch.

Final Project Update 1

Concept/ Idea

For the project, we (Tim and Samyam) have decided to develop a system that lets the user sketch a diagram or shape on the p5.js canvas. This sketch then will be used to control the physical arm of our drawing platform. To accomplish this, we will send canvas data from p5.js to Arduino, controlling different parts of the drawing device.

The striking feature of the project is the interconnectivity of the drawing. The user can draw as many shapes or sketches as they desire, but everything will remain connected to each other. This idea is inspired by the Eulerian trail, where the floor is divided into multiple sections when multiple people step on it.

 

Hardware Design

Following the previous post, we have finalized our concept to be a polar coordinate pen plotter that plots a shape drawn on the computer screen. Unlike our previous design, we have decided to make a 2-DOF system (linear movement on one axis, and rotational motion around an orthogonal axis). Although the mechanical design of the machine became more complex, this will make the software much simpler. For the design, we used Autodesk Fusion 360 to create a 3d model, which was manufactured with a 3d printer and a laser cutter. For the precision of the plotter, we purchased a linear guide rail, commonly used in CNC machines and 3d printers; we used a stepper motor for controlled movement. Below is a 3 d model design for the project:

A rack and pinion system was used, so as a gear connected to a stepper motor rotates, it will move the whole pen carrier along a linear rail.

One distinctive feature of this plotter is that the position of the rotating (base) plate can be adjusted freely. This allows the user to produce different drawings every time he or she moves the base plate.

Because drawings on the screen use cartesian coordinates, we had to convert XY coordinates into polar coordinates. This was very simple because T:(X,Y) –> (r, θ), where r = sqrt(x^2+y^2) and θ = tan^-1(y/x). r will determine the movement of a linear DOF and θ will determine how much the base plate will rotate. Do you see how simple the calculation got compared to the previous design?

Making Parts

As mentioned above, a laser cutter and 3d printer was used to make parts.

Prototype

Laser Cutting

Due to the high rigidity of the acrylic plates, we decided to make the main body with acrylic parts. Based on our design, we were able to laser-cut the parts that we needed.

3D Printing

Parts that cannot be made with laser cutters were made using 3d printers. This included a base plate, a spacer between acrylic parts, and a pen moving mechanism.

P5.js Component

The preliminary phase of the project depends on the functionalities of the p5.js component. The user can use the mouse to draw shapes on the p5.js canvas; the shape will be replicated in the actual design using the Arduino component.

In order to sketch a shape on the canvas, the user can click the mouse and drag the cursor to draw the desired shape. This is regulated by two functions titled “mousePressed()” and “mouseReleased()” respectively. When the mouse is clicked, the global variable “isclicked” is set to True and when the mouse click is released, “isClicked” is set to False. This way, the global variable keeps track of the sketch being drawn.

Once the mouse is clicked, the program checks the position of the cursor – if it is static while being tracked, the position is ignored, i.e., the user must move the cursor, while the mouse is being clicked, so that a sketch can be drawn on the screen. It is facilitated by the following code:

// Make sure the mouse is clicked and cursor position is different
if (isClicked && mouseX !== pmouseX && mouseX !== pmouseY)
{
  // Create a vector and add it to the list
  // let pt = createVector(mouseX, mouseY);          // When origin is at the top-left corner
  let pt = createVector(mouseX - width/2, mouseY - height/2);
  positions.push(pt);
  // console.log(pt.x, pt.y);
  
  
  // Mapping Cartesian to Polar and appending it in mappedPositions array
  let temp_list = [];
  temp_list = cartToPolar(pt.x, pt.y)
  let pt_mapped = createVector((temp_list[0] * one_px_mm), temp_list[1]);
  mappedPositions.push(pt_mapped);
 
}

Here, we can see, if the aforementioned conditions are met, a new vector based on the cartesian location of the cursor is created and appended to the (global variable) list “positions”. The entire canvas is translated to the middle of the canvas, that’s why a certain amount is subtracted to the x and y coordinates before appending to the list. Similarly, with the help of a function titled “cartToPolar()”, each vector in the cartesian coordinates is converted to polar coordinates equivalent and appended to a list titled “mappedPositions[]”.

function cartToPolar(x, y)
{
  let radius, angle;
  
  radius = sqrt(sq(x) + sq(y));
  // console.log(radius);
  
  angle = atan(y/x);
  // console.log(angle);
  
  let temp_list = [];
  temp_list[0] = map(radius, 0, sqrt(2 * sq(width/2)), 0, disc_radius);         // Mapped radius
  temp_list[1] = angle;
  
  return temp_list;
  
}

The program also consists of two manually coded buttons — RESET and SEND DATA. The former reset the canvas and resets the required variables so that the sketch can be redrawn from the beginning. While the latter is used to send data to the Arduino connected to the system. Both these functions derive from a template function titled “button()” that takes text, x, y coordinates, height and width of the button as arguments. To develop the SEND DATA functionality, a function named “send_data_button()” calls the main button function with the required parameters. The main function returns a boolean value [1 if the cursor is within the button and 0 otherwise]. If the boolean value is 1 and the button is pressed, the values in the “mappedPositions[]” list are forwarded to the Arduino using an inbuilt serial.write() function. In each iteration, the angle of the coordinate is transmitted following its radius.

function reset_button()
{
  let x = 26;
  let y = canvas_h - 70;
  let w = 125;
  let h = 50;
  
  // If the cursor is within the button button() function returns 1, else 0;
  let resetBool = button("RESET", x, y, w, h);
  
  // Resetting sketch if the cursor iswithin the button and mouse is clicked
  if (resetBool && mouseIsPressed)
  {
    positions = [];
    mappedPositions = [];
    isClicked = false;
  }
  
}

 

The p5.js component also comprises different functions like choosePort(), openPort(), serialEvent() and so forth to keep track of the port selection, data transmission as well as error handling.

Arduino Component

Arduino is connected to 2 stepper motors, each controlled by different stepper drivers. The Arduino will read data sent from p5js and move the pen and plate accordingly. Total of 2 switches will be connected to the board, both for homing the pen carrier (one push button and one limit switch).

When the button is pressed, the Arduino will begin to move the pen carrier back until the carrier hits the limit switch. Once the limit switch is closed, the Arduino will move the pen carrier slightly front and move the carrier back until it hits the switch again. The second part will be done at a much slower speed for accuracy. Homing a pen carrier is significant because the machine must always be aware of the pen’s current position. Homing allows the machine to always start from the same initial position.

This is the sample code showing how the code will work (no motors added yet):

const int limitSw = 7;

const int homeButt = 8;

long long currentTime = 0;

bool homing = false;

bool reachEnd = false;

void setup() {

  // put your setup code here, to run once:

  pinMode(limitSw, INPUT);

  pinMode(homeButt, INPUT);

  Serial.begin(9600); 

}


void loop() {

  // put your main code here, to run repeatedly:

  if(!homing && digitalRead(homeButt) == 1){

    homing = true;

  }


  if(homing){

    if(!reachEnd){

      Serial.println("homing fast");

      if(digitalRead(limitSw) == 1){

        Serial.println("SW pressed");

        reachEnd = true;

        currentTime = millis();

      }

    }

    else{

      Serial.println("homing slow");

      if(digitalRead(limitSw) == 1 && millis() - currentTime > 500){

        Serial.println("SW slow pressed. homing complete");

        homing = false;

        reachEnd = false;

      }

    }

  }

}

The other part of the code will include moving each part accordingly to the coordinate values from p5js. This would be relatively easy to achieve because we can calculate how much each part will move in mm when the motor turns by half or a single step.

Further coding can be done once we are ready with fully assembled hardware.

Future Plan

The time it takes to 3D print parts is the only problem we are facing until now. Once we are done printing our parts, we will be able to assemble the parts together soon.

For now, the future plan includes the functionality to successfully coordinate the Arduino component with the data sent from the p5.js component. Since the canvas sketch is drawn based on cartesian coordinates, while the physical drawing is based on polar coordinates, we are planning to program the project so that artistic output is achieved.

Demo

The software phase of the project can be tested here using this link. The project will be completed once the printing phase is completed.

Final Project Concept

Concept

For this project, I would like to make a drawing platform, in which as the user moves the pen connected to the device, the drawing appears on the p5js. To make the drawing more interesting and unique, I am going to add Perlin noise to the lines. Shown below is the rough diagram of the platform.

The device will consist of 2 arms (blue and orange), 2 joints (with potentiometers (green)), and a pen. Joint at the top is a fixed point so position of the arms will vary depending on the movement of the pen. Each joint is connected to a potentiometer in order to read the angle value. Since we will know the length of each arm and the angle between each arm, we will able to convert the values (polar) into a cartesian value that is easy to display on p5js.

For Perlin noise, I will simply add or substract random integers from the calculated points.

Interaction Between Arduino and P5JS

  1. Arduino reads potentiometer value
  2. Arduino sends sensor values to p5js
  3. P5js converts sensor values into cartesian coordinates
  4. P5js draws points on the canvas with some Perlin noise

I am very excited to work on this project, but I am worried about how smooth potentiometer will rotate, which may make moving a pen hard.

[Assignment 10] Creative Instrument

Concept

For this project, we decided on creating a creative musical instrument using a distance sensor. The device calculates the distance of an object (in this case, the object behaves as a percussion beater), and based on its location, tunes of varying frequencies are produced. The device allows the user to set the base note using a pushdown switch; once the switch is pressed, the distance calculated at that phase is used as an initial point. Thus, the user can move the beater to and fro in order to create a musical track. Similarly, using the potentiometer, the user can further control the duration and type of sound produced by the buzzer.

The devices used in the system are:

    • One ultrasonic distance sensor
    • One potentiometer
    • One piezo buzzer
    • One pushdown switch
    • One 10 000 ohm resistor

The design of the instrument is based on the schematics as shown below:

Codes

Our code mainly consists of 4 parts: ultrasonic sensor reading, button reading, potentiometer reading, and piezo buzzer output. We first started with ultrasonic sensors, referring to online code for simple distance measurement (Reference). We have slightly modified the original code to our needs.

int findDistance() 
{
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);

  // Sets the trigPin on HIGH state for 10 micro seconds
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  // Reads the echoPin, returns the sound wave travel time in microseconds
  long duration = pulseIn(echoPin, HIGH);
  return (duration * 0.034 / 2);
}

This code above is a function that we used to measure distance using the ultrasonic sensor for each loop. Depending on the position of the object, the buzzer will produce different pitches of sound.

Then, the button comes in. Button is used to offset the starting position of the instrument. By placing an object in front of the sensor and pushing the button, the object’s position will automatically be set as a C4 note. Using this button, a player can adjust the instrument in a way that he or she is most comfortable with.

  if (millis() - currentTime > 200 || currentTime == 0) 
  {
    // If the switch is pressed, reset the offset
    if (digitalRead(11) == 1) 
    {
      distanceOffset = findDistance();
      Serial.print("new offset: ");
      Serial.println(distanceOffset);
      currentTime = millis();
    }
  }

  int distance = findDistance() - distanceOffset;

As shown above, when the button is pressed, the program will subtract the offset from the sensor reading.

int pitch_ = intoNotes(map(distance, 0, 20, 1, 7)); 

int pitch = pitch_ * myList[0];
int toneDuration = myList[1];

if(distance <= 20)
  {
    tone(pPin, pitch,toneDuration);                              // Controls 1. PinNumber, 2. Frequency, 3. Time Duration
  }
else
  {
    noTone(pPin);
  }

float intoNotes(int x)
{
  switch(x)
  {
    case 1:
      return 261.63;
    case 2:
      return 293.66;
    case 3:
      return 329.63;
    case 4:
      return 349.23;
    case 5:
      return 392.00;
    case 6:
      return 440.00;
    case 7:
      return 493.88;
    default:
      return 0;
  }
}

The code above is how a program produces different pitches depending on the distance. The program maps 20 cm into 7 different integers; depending on the value of the mapped value, intoNotes() will return a specific pitch value accordingly. If the distance is above 20cm, the instrument will simply not produce a sound.

The code consists of two more functionalities which is facilitated by the function titled “noteModifier()”. It is a void function that takes the data read from the potentiometer as an argument.

void noteModifier(int volt)
{
  float val1 = 0;

  if (volt > 4 && volt <= 4.6)
    val1 = 5;
  else if (volt > 3 && volt <= 4)
    val1 = 4;
  else if (volt > 2 && volt <= 3)
    val1 = 3;
  else if (volt > 1 && volt <= 2)
    val1 = 2;
  else
    val1 = 1;

  myList[0] = val1;
  myList[1] = val1 * 1000;
  
}

 

As a potentiometer is an analog sensor, it is connected to the A0 pin for a contiguous set of data that ranges from 0 to 1023. Before using this data as input, the values are mapped to a new scale of (1.0 to 5.0) V as the maximum emf in the circuit is 5V. The conversion is done by these lines of code at the beginning of the loop() function.

pVoltRead = analogRead(potentioPin);
trueVolt = (pVoltRead * 5.0/1023);
Serial.println(trueVolt);

Once the true volt in the potentiometer is calculated, the function noteModifier() uses a set of if-conditions to determine the value of a variable named ‘val1’. As seen in the code, the value of ‘val1’ varies based on the range of the true volt argument. The true purpose of using val1 is to alter the content of a global list “myList[]” declared at the beginning of the project. The list consists of two elements: (1) the first element is a coefficient that scales the frequency of the sound produced by the buzzer and (2) the second element is the duration of a single tone produced by the buzzer.

This way, the function determines two primary arguments — frequency and time duration — of the Arduino’s inbuilt function tone().

The idea here is to replicate the real-life application of the project. A user can rotate the knob in the potentiometer; consequently, different tones are produced for varying lengths of time. In short, including a potentiometer provides one additional layer of user interaction to the system. The second demo clip shows the functionality of the potentiometer and how it can be used to produce different tunes.

Reflection / Future Improvements

Working on this assignment was very entertaining and exciting. We were able to use different sensors in the kit and use them to create an object that we can physically interact with, unlike p5js. We believe the instrument came out as we initially expected, but there are a few points that we can improve upon:

      • Ultrasonic Sensor
        • Ultrasonic sensor uses sound waves to measure the distance. This means the measurement can be inaccurate and unreliable depending on the surface of the object the wave is hitting. Due to this, we found out our instrument malfunctions when trying to play it with our own hands. To improve this, we would like to use a laser distance sensor that is less affected by the object’s surface.
      • Piezo Buzzer
        • Piezo Buzzer is very simple to use, but its sound quality is great. If we use a better speaker, we may be able to add more interesting functionalities, such as producing different sounds of instruments when a button is pressed.

Watch demos of the instrument here:

Without potentiometer:

With potentiometer:

[Assignment 9] Police Light

Concept

For this assignment, I made a police light using two LEDs, a button, and a potentiometer. When the button is pressed once, the blue LED turns on; when the button is pressed twice, the blue LED turns off and the red LED turns on; when the button is pressed three times, the two LEDs turn on and off alternatively. Using the potentiometer, the user can also control how fast the two LEDs will turn on and off.

Codes

As mentioned above, this police light has 3 modes: two single-light modes and alternating lights modes. This mode (0, 1, and 2) will change whenever the user presses the button. Since Arduino will read a single button press as a series of multiple “1s,” I used millis() to make sure Arduino reads only a single 1 for every 200ms. This means Arduino will ignore any 1s that come after the first 1 within 200ms. When 1 is read, the mode will increase by 1 and back to 0 after 2. Variable blueLED is a boolean that is used to keep track of which single LED to turn on and off.

if (currentTime == 0 || millis() - currentTime > 200) {
    if (switchVal) {
      mode = ++mode % 3;
      if (mode != 2) blueLED = !blueLED; 
    }
    currentTime = millis();
  }

Then, the program will turn LEDs on and off according to the current mode. For mode 0 and 1:

if (mode != 2) {
  if (blueLED) {
    digitalWrite(13, HIGH);
    digitalWrite(12, LOW);
  } else {
    digitalWrite(13, LOW);
    digitalWrite(12, HIGH);
  }
}

For mode 2, two LEDs turns on and off alternatively. I could have used delay() to achieve this effect, but I decided to not use such function because Arduino cannot read any other sensor data at the same time. Instead, I used millis() to do this.  pVal is a analog reading of potentiometer and the input was mapped to a value between 0 and 800. LEDs will turn on and off for every pVal ms.

int pVal = map(analogRead(A0), 1023, 0, 0, 800);

else {
  if (millis() - ledTime > pVal) {
    if (ledOnOff) {
      digitalWrite(13, HIGH);
      digitalWrite(12, LOW);
    } else {
      digitalWrite(13, LOW);
      digitalWrite(12, HIGH);
    }

    ledTime = millis();
    ledOnOff = !ledOnOff;
  }
}

Future Improvements

This project turned out to be better than I first expected. I am very satisfied about the fact that the button is functional even when LEDs are alternatively lighting up by not using delay(). For future improvements, I may be able to add more complex patterns to the LEDs, which seems challenging because no delay()s can be used. Additionally, adding piezo speaker included in the kit will make this police light more interesting and realistic.

[Assignment 8] Water Switch

Concept

For this assignment, I have created an electrical switch that uses water to switch on and off. Unlike conventional mechanical switches that require us to use our hands, this switch uses water. As shown below, this switch has two wires that are taped to each other.

 

Without a conductive medium, i.e. water, in between them, electricity will not flow because air has high resistance. Only when both wires touch water, the circuit will be closed and LED will turn on. Shown below is a video of me turning LED on and off using this switch.

Reflection / Future Improvements

One advantage of this switch over other conventional switches is that resistors can be removed from the circuit. Since medium is required for the switch to operate, the medium itself can act as a resistor. By controlling the resistance of the medium (e.g. adding salt to water), user can freely control the brightness of the LED.

One of the many aspects of this switch to improve is finding a way not to make water droplets to remain on the wires even when they are removed from the water. Because water adheres to wires, I realized the LED will be on even when the wires are pulled off from the water. One possible solution to this would be making the gap between the wires larger, so water droplets on the wire cannot close the circuit.

[Tetris] Update 2 2022/10/10 — Final Version

Concept

For this project, I have created an 8-bit style Tetris game. The general concept of the game is similar to the original game, but there are two additional features in this game. First, the game sometimes generates a random block that does not look like a traditional Tetris block. The block may consist of more than 5 blocks and some empty spaces. This is a challenge that this game gives to the players. Secondly, there is a bomb block that removes all blocks. If the bomb is in row n, it will clear all blocks from row n-2 to n+2, removing 5 lines total. This feature is added because randomized blocks may be too difficult for the users. For additional concepts, please refer to my previous posts.

Code

**note: I will only discuss codes that are new or different from the previous post I made on Tetris. I recommend reading the previous first and coming back.**

For update 2, I started off by fixing these one minor bug:

  • Block cannot be moved as soon as it reaches the bottom.

This happened because the code generates new block as soon as the block touches the ground (when block cannot go further down). In order to solve this, I added a condition which the code will wait for the new keyboard input if it cannot go further down for a specific amount of time. If no key input is given in that time range, the code will then generate the block. Bolded code shows the new lines that I have added.

if (millis() - counterBlockDown > 1000 / this.speed) {
        fixBlock = true;
        counterBlockDown = millis();
      }
      if (fixBlock) {
        //Once block cannot be moved down further, update game field
        for (let i = 0; i < 4; i++) {
          for (let j = 0; j < 4; j++) {
            if (this.shapeArr[i][j] == 1) board[this.y + i][this.x + j] = 1;
          }
        }
        //generate new block
        generateBlock = true;
        fixBlock = false;
      }
    }

Not to mention, there are tons of new features that I added in this update:

  • Soft drop

//soft drop
if (keyIsDown(DOWN_ARROW)) {
  if (myBlock != null) myBlock.speed = blockSpeed * 3;
} else {
  if (myBlock != null) myBlock.speed = blockSpeed;
}

When down arrow key is pressed, the block will move down with the speed that is 3 times its initial speed. Not to mention, block’s speed is in variable because initial speed will increase as level goes up.

  • Hard drop

When spacebar key is pressed, the block will instantly move down to the bottom. Notice that I used 32 for keyCode because spacebar is not mapped in p5js.

//Hard drop when spacebar pressed
if (keyCode == 32) {
  if (myBlock != null) {
    let yPos = myBlock.y;
    while (myBlock.validMove(0, 1, 0)) {
      myBlock.y = yPos;
      yPos++;
    }
    hardDropMusic.play();
  }
}

For  this, additional calculation is done to find the lowest possible position for the block. The program will essentially start from the lowest position and see if the block can fit there. If block cannot be fitted, the block will moved 1 unit up to see if it fits there. This process repeated until the block finally fits.

  • Rotation

rotate() {
    for (let i = 0; i < 2; i++) {
      for (let j = i; j < 4 - i - 1; j++) {
        // Swap elements of each cycle
        // in clockwise direction
        let temp = this.shapeArr[i][j];
        this.shapeArr[i][j] = this.shapeArr[3 - j][i];
        this.shapeArr[3 - j][i] = this.shapeArr[3 - i][3 - j];
        this.shapeArr[3 - i][3 - j] = this.shapeArr[j][3 - i];
        this.shapeArr[j][3 - i] = temp;
      }
    }
  }

  //rotate block with condition check
  rotateBlock() {
    if (!this.validMove(0, 0, 1)) {
      for (let i = 0; i < 3; i++) this.rotate();
    }
  }

rotate() uses a rotation function that I have found in:
https://www.geeksforgeeks.org/rotate-a-matrix-by-90-degree-in-clockwise
-direction-without-using-any-extra-space
.

In order to rotate blocks, there must be additional condition that will check if the rotation is a valid movement at the given position of the block. Bolded code shows the new condition I have added to do this.

//Checks if block can be moved in a given direction
  validMove(dX, dY, dR) {
    if (dR == 0) {
      for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) { if (this.y + i >= 0 && this.x + j >= 0) {
            if (
              /*ignores all empty spaces. 1s must always be within the 
            boundary of game field and must not overlap with non-empty,
            or 1s, when moved*/
              this.shapeArr[i][j] != 0 &&
              (board[this.y + i + dY][this.x + j + dX] != 0 ||
                this.y + i + dY >= boardHeight ||
                this.x + j + dX < 0 || this.x + j + dX >= boardWidth)
            ) {
              if (this.shapeArr[i][j] == 2){
                this.explode();
              }
              return false;
            }
          }
        }
      }
    } else {
      this.rotate();
      if (this.y >= 0) {
        for (let i = 0; i < 4; i++) {
          for (let j = 0; j < 4; j++) {  if ( /*ignores all empty spaces. 1s must always be within the boundary of game field and must not overlap with non-empty, or 1s, when moved*/ this.shapeArr[i][j] == 1 && (board[this.y + i][this.x + j] != 0 || this.y + i >= boardHeight ||
                this.x + j < 0 || this.x + j >= boardWidth)
            ) {
              return false;
            }
            if (this.shapeArr[i][j] == 2) return false; //bomb cannot be rotated
          }
        }
      } else return false;
    }
    return true;
  }

validMove() will first check if the block is being rotated or translated. If block is being rotated, i.e. third parameter is 1, it will first rotate the block and check if the block is not overlapped with other blocks and is within the width and height of the game field. If block satisfies all conditions, validMove() returns true. Else, it returns false. Since validMove() actually rotates block to check condition, rotateBlock() will rotate block 3 more times to reset the block status if no rotation can be made.

  • Random block

As level goes up, the game will start to generate random a 4 by 4 block that may contain empty spaces like such:

Each time a random block is generated, the block will have randomized shape. This feature was added to increase the difficulty of the game and make the game more interesting/different every time.

else if (_type == "R") {
      //Random 4*4 block
      for (let i = 0; i < 4; i++) {
        shapeArr[i] = [
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
        ];
      }
    }

Block object now has new type called R.

let blockType = random(0, 10);

if (blockType <= 11 - level * 0.5)
      myBlock = new block(5, -4, random(blockTypeArr), blockSpeed);
    else myBlock = new block(5, -4, random(specialTypeArr), blockSpeed);

This is a updated condition for generating block.  blockType is a random number between 0 and 10. Regular blocks will be generated if blockType is less or equal to 11- level * 0.5 and random blocks or bomb (will be discussed in the next section) will be generated else. Note that  the probability of getting random blocks or bomb increases as level goes up. For this reason, random block and bomb will only appear from level 3.

  • Bomb

Bomb is a special type of block that will remove blocks nearby. If block is at row n, it will remove all blocks from row n-2 to row n+2.

else if (_type == "bomb") {
      for (let i = 0; i < 4; i++) {
        shapeArr[3][2] = 2;
      }
    }

/*explode bomb block. if bomb is at (x,y), it will destroy every row
from x-2 to x+2*/
explode() {
  let go = true;
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.shapeArr[i][j] == 2 && go) {
        go = false;
        for (let k = this.y + i - 2; k <= this.y + i + 2; k++) {
          if (k >= 0 && k < boardHeight) {
            for (let l = 0; l < boardWidth; l++) {
              board[k][l] = -1;
            }
          }
        }
      }
    }
  }
}

Block object now has new type called bomb. Chance of generating random block and bomb is both 50%.

  • Line Clearing

Every time a game field is updated, the program will look for any completed lines. If there is a completed, the game field will be updated. The update will make game field to hold value -1 instead of 1.

//Check if there is a line cleared
function lineClear() {
  for (let i = 0; i < boardHeight; i++) {
    if (board[i][0] == 1) {
      for (let j = 0; j < boardWidth; j++) {
        if (board[i][j] == 1) {
          if (j == boardWidth - 1) {
            for (let k = 0; k < boardWidth; k++) board[i][k] = "-1";                                
              counterLineClear = millis(); 
          } 
        } else break; 
      } 
    } 
  } 
}

Because the game filed can now hold -1, I added new condition to make sure the game can display -1s as a grey block. Then, the game filed will start to remove grey blocks line by line at each frame. This code is shown as bold code below.

//visually display the gameboard.
function displayBoard() {
  blockImg = blockImg.get(0, 0, 20, 20);
  push();
  translate(25, 30);
  for (let i = 0; i < boardHeight; i++) {
    for (let j = 0; j < boardWidth; j++) { 
      if (board[i][j] == 1) image(blockImg, blockSize * j, blockSize *      
      i); 
      else if (board[i][j] == -1) { image(greyBlockImg, blockSize * j,       
      blockSize * i); 
        removeLine(i); 
      }
    } 
  } 
  pop(); 
} 

function removeLine(index) { if (millis() - counterLineClear > 400) {
    for (let i = index; i > 1; i--) {
      for (let j = 0; j < boardWidth; j++) {
        board[i][j] = board[i - 1][j];
      }
    }
    lineClearMusic.play();
    lines++;
    score += scorePerLine;
    counterLineClear = millis();
  }
}
  • Score, level, and line cleared counting

For each line cleared, the game will update score, level, and line cleared displayed on the right side. For each line cleared, the score will increase more as level goes up. Level increases by 1 for each 10 lines cleared. As mentioned above, block speed will also increase as level goes up.

  scorePerLine = int(level * 0.5) + 1;
  level = int(lines / 10) + 1;
  blockSpeed = int(level * 1.25) + 2;
  • Game Over

A condition that checks if game is over. If the position of non-empty block unit is below game field boundary, the game is over.

else {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.y + i < 0 && this.shapeArr[i][j] == 1) {
        gameOver = true;
        break;
      }
    }
  }
...

This  condition is within blockDown() method of the block object.

  • Sound effect

Last feature that was added to this game is sound effect for background music, the block is moved and soft/hard dropped, and game over. Code will not be demonstrated because the codes for sound effect is scattered throughout the entire program.

Future Improvements

Overall, I am very satisfied with I have made for this project, but here are some things that I would love to fix in the future:

  1. I realized when this program is played with chrome browser, the sound effect will slow down as time progresses. I found out this is the problem with the chrome itself.
  2. Add settings page so players can change sound effects.
  3. Due to the time limit, I failed to include a feature which shows next 2 blocks that will show up in the game. I would love to add this feature in the future.
  4. Include hold feature so user can hold the block and use it later when needed.
  5. The rotation of the blocks is not centered because rotation is done as if I rotating a matrix.

[Tetris] Update 1 2022/10/3 — Game Interface & Block Display

Concept

For this project, I created a Tetris. Tetris is a simple game, in which users move or rotate blocks to fill a game field. Once a horizontal line is created with blocks, blocks on the line will be removed and the user will gain a certain amount of points. Users will gain points if:

  1. Removes blocks by making a horizontal line of blocks.
  2. Uses hard drop
  3. Extra points if multiple lines are removed at once

Although I am using a high-resolution screen, I wanted to give a “retro” appearance to the game (8bit). To achieve this effect, I used 8-bit style fonts and images.

Tetris (NES) - online game | RetroGames.cz

Code

Coding the game was indeed complex because the game required not only game logic, which I partially implemented, but also a friendly user interface. For this reason, I used 3 js. files, each responsible of running the code (sketch.js), displaying game interface (Page.js), and running game (Game.js).

Before making an actual “gaming” part of the project, I made the game interface of the game first. For this project, I used Press Start 2P font downloaded from https://fonts.google.com/specimen/Press+Start+2P.

1. sketch.js

<2022/10/3> UPDATE 1

This file manages how different pages will be displayed based on the user input. Depending on the user input, the game will show the main page, game page, instruction page, interrupt page, or game.

function draw() {
  background(0);
  if (!escapePressed) {
    //if user did not press ESC
    if (currentPageIndex == 0) {
      //MAIN PAGE
      displayMain();
      if (mySelectorMain != null) {
        //draw selector object
        mySelectorMain.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorMain = new selector(150, 340, 21);
      }
    } else if (currentPageIndex == 1) {
      //START GAME selected
      startGame(); //start game based on Game.js
    } else if (currentPageIndex == 2) {
      //HOW TO PLAY selected
      displayInstructions(); //display instructions
    }
  } else {
    if (currentPageIndex == 1) {
      //when user is in the middle of the game
      displayReturn("EXIT GAME AND RETURN TO MAIN?");
      if (mySelectorESC != null) {
        //draw selector object
        mySelectorESC.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorESC = new selector(230, 310, 21);
      }
    } else if (currentPageIndex == 2) {
      //when user is reading instructions
      displayReturn("QUIT READING AND RETURN TO MAIN?");
      if (mySelectorESC != null) {
        //draw selector object
        mySelectorESC.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorESC = new selector(230, 310, 21);
      }
    }
  }
}

Code above will display appropriate pages based on the current index of the game and ESC input(0: MAIN PAGE, 1: GAME, 2: INSTRUCTION). If ESC is pressed, the game will display RETURN TO HOME PAGE with appropriate texts.

//When keyboard input is given
function keyPressed() {
  if (keyCode == DOWN_ARROW) {
    if (currentPageIndex == 0) {
      /*Main page. Move selector accordingly. Selector will move 
      to HOW TO PLAY when it was previously at START GAME, and vice versa*/
      if (mySelectorMain.y == 340) mySelectorMain.y = 400;
      else mySelectorMain.y = 340;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*if ESC is pressed in the middle of the game or in the instruction,
      move selector accordingly.Selector will move to NO when it was previously 
      at YES, and vice versa*/
      if (mySelectorESC.y == 310) mySelectorESC.y = 370;
      else mySelectorESC.y = 310;
    }
  }
  if (keyCode == UP_ARROW) {
    if (currentPageIndex == 0) {
      /*Main page. Move selector accordingly. Selector will move 
      to HOW TO PLAY when it was previously at START GAME, and vice versa*/
      if (mySelectorMain.y == 340) mySelectorMain.y = 400;
      else mySelectorMain.y = 340;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*if ESC is pressed in the middle of the game or in the instruction,
      move selector accordingly.Selector will move to NO when it was previously 
      at YES, and vice versa*/
      if (mySelectorESC.y == 310) mySelectorESC.y = 370;
      else mySelectorESC.y = 310;
    }
  }
  if (keyCode == ENTER) {
    if (currentPageIndex == 0) {
      //user selects START GAME. Change currentPageIndex to 1
      if (mySelectorMain.y == 340) currentPageIndex = 1;
      //user selects HOW TO PLAY. Change currentPageIndex to 2
      else currentPageIndex = 2;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*ESC is pressed in the middle of the game or in the instruction.
      If user selects YES, change currentPageIndex to 0 (returning to MAIN PAGE);
      if user selects NO, simply return to the current page*/
      if (mySelectorESC.y == 310) {
        //YES
        currentPageIndex = 0;
        escapePressed = false;
      } else {
        //NO
        //user selects NO
        escapePressed = false;
      }
    }
    //Reset selector objects' position (DEFAULT: First option)
    mySelectorMain = null;
    mySelectorESC = null;
  }
  //If ESC is pressed, change escapePressed to true;
  if (keyCode == ESCAPE) {
    if (currentPageIndex != 0) escapePressed = true;
  }
}

Code above deals with user inputs and how the game will react to each  input. For this version of the game, user has an option to select different options using up/down arrows and enter. Variables with name including “selector” are objects of triangles that is shown next to the options. It will be further explained in PAGE.JS section.

2. Page.js

<2022/10/3> UPDATE 1

This file manages how each page will visually displayed to the user. It is where all positions and alignments of window/texts are shown. The page starts with preload() to load fonts and images that will be used through out the game.

function preload() {
  font = loadFont("assets/8Bit.ttf"); //load textfont to p5js
  blockImg = loadImage("assets/block.png");  //load single block unit image to p5js
}

Most of the codes in this page manages positions and alignments of the visual elements for each page (MAIN, GAME, INSTRUCTION, and INTTERUPTION).

//MAIN Page
function displayMain() {
  currentPageIndex = 0;

  //display main title of the game: TETRIS
  setFontStyle(titleFontSize);
  text("TETRIS", width / 2, 250);

  //display options
  setFontStyle(normalFontSize);
  text("START GAME", width / 2, 350);
  text("HOW TO PLAY", width / 2, 410);
}

//Instruction Page
function displayInstructions() {
  setFontStyle(titleFontSize - 40);
  text("HOW TO PLAY", width / 2, 95);

  createWindow(width / 2, 320, 530, 370);

  setFontStyle(normalFontSize);
  text(
    "↑ : ROTATE BLOCKS\n\n\n← : MOVE BLOCKS LEFT\n\n\n→ : MOVE BLOCKS RIGHT\n\n\n↓ : SOFT DROP\n\n\nSPACE : HARD DROP",
    width / 2,
    180
  );

  setFontStyle(normalFontSize - 7);

  if (millis() - currentMillis > 800) {
    currentMillis = millis();
    blink = !blink;
  }

  if (blink) {
    fill(0);
    stroke(0);
  } else {
    fill(255);
    stroke(255);
  }

  text("PRESS ESC TO RETURN", width / 2, 560);
}

//Game
function displayGameBackground(score, level, lines) {
  createWindow(width / 4 + 25, height / 2, width / 2 + 6, height - 60 + 6);

  setFontStyle(normalFontSize);
  text("——— NEXT ———", 462.5, 50);

  //Next blocks
  createWindow(400, 125, 106, 106);
  createWindow(525, 125, 106, 106);

  //Score, level, line cleared
  createWindow(462.5, 385, 231, 376);

  setFontStyle(normalFontSize);
  text("SCORE", 462.5, 245);
  text(score, 462.5, 290); //display score
  text("LEVEL", 462.5, 358.3);
  text(level, 462.5, 403.3); //display level of the round
  text("LINES", 462.5, 481.7);
  text(lines, 462.5, 526.7); //display total # of lines cleared in the round
}

//Return to Main Question Page
function displayReturn(_text) {
  //Create window
  createWindow(width / 2, height / 2, 400, 300);

  setFontStyle(normalFontSize);

  //Text Question
  text(_text, width / 2, 220, 350);

  //Options
  text("YES", width / 2, 320);
  text("NO", width / 2, 380);
}
//default setting of the font
function setFontStyle(size) {
  strokeWeight(1);
  textFont(font);  //set font style
  fill(255);
  textWrap(WORD);  //wrap text
  textAlign(CENTER);  //align text to center
  textSize(size);  //change text size
}

//create a rectangular window where texts will be displayed
function createWindow(x, y, w, h) {
  fill(0);
  stroke(255);  //white border
  strokeWeight(6);  //border weight of the rectangle
  rectMode(CENTER);  //rectangle is centered
  rect(x, y, w, h);
}

setFontStlye(size) and createWindow(x,y,w,h) are functions, where size is textSize, x and y are coordinates of the rectangle’s/window’s center, and w and h are width and height of the window.

3. Game.JS

<2022/10/3> UPDATE 1

This file contains all the game logics and runs Tetris game. The game consists of 2 main parts: block objects and game field array. Block objects are used to display blocks in motion. Game field array is what the program use to visualize the game. Once blocks cannot further be moved, it will be stored into game field array, so no further calculations will be needed in the future.

Game logic follow procedures listed below:

  • Create an empty game field array. (0: empty space, 1: something is present)
board = [];
for (let i = 0; i < boardHeight + 1; i++) {
  board[i] = [];
  for (let j = 0; j < boardWidth; j++) {
    board[i][j] = 0;
  }
}
  • User selects START GAME. Game Starts based on SCRIPT.JS
else if (currentPageIndex == 1) { 
//START GAME selected 
startGame(); 
}
  • Load background layout of the game (shapes and texts) based on PAGE.JS
function startGame() {
  displayGameBackground(0, 1, 0, 1);
  //Code is partially shown for demonstration
}
  • Generate, display, and move block object. Types of the block is randomly generated from the array that contains 7 different block types. Up until now, block will move only vertically down every time interval. When the block reaches bottom, or cannot be further moved, a new random block will be generated.
let blockTypeArr = ["I", "J", "L", "O", "S", "T", "Z"]; //Block Type Array

//GAME
function startGame() {
  displayGameBackground(0, 1, 0, 1); //display background interface

  if (generateBlock) {
    //If new block is needed, create a random shaped block
    myBlock = new block(int(random(0, 11)), 10, random(blockTypeArr), 4);
    generateBlock = false;
  }

  myBlock.drawBlock();
  myBlock.blockDown();
  displayBoard();
}

Most of the game logic is handled by block class below. For more, read the commented lines.

//block object for tetris blocks
class block {
  constructor(_x, _y, _type, speed) {
    this.x = _x;
    this.y = _y;
    this.type = _type;
    this.shapeArr = [];
    this.speed = 1;
  }
  
  /*this class method will fill shapeArr based on the
  type of the block. shapeArr is a 4*4 array, in which
  0: empty, 1: block*/
  shapeType(_type) {
    let shapeArr = [];
    /*example L block
      0 0 0 0
      0 1 0 0
      0 1 0 0
      0 1 1 0*/
    for (let i = 0; i < 4; i++) {
      shapeArr[i] = [0, 0, 0, 0];
    }
    if (_type == "I") {
      shapeArr[0][0] = 1;
      shapeArr[1][0] = 1;
      shapeArr[2][0] = 1;
      shapeArr[3][0] = 1;
    } else if (_type == "J") {
      shapeArr[1][2] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "L") {
      shapeArr[1][1] = 1;
      shapeArr[2][1] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "O") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "S") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][0] = 1;
      shapeArr[3][1] = 1;
    } else if (_type == "T") {
      shapeArr[2][1] = 1;
      shapeArr[3][0] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "Z") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][2] = 1;
      shapeArr[3][3] = 1;
    }
    this.shapeArr = shapeArr;
  }

  //Checks if block can be moved in a given direction
  validMove(dX, dY) {
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) { 
if ( /*ignores all empty spaces. 1s must always be within the boundary of game field and must not overlap with non-empty, or 1s, when moved*/ this.shapeArr[i][j] == 1 && (board[this.y + i + 1][this.x + j] != 0 || this.y + i + dY >= boardHeight ||
            this.x + j + dX < 0 || this.x + j + dX >= boardWidth)
        ) {
          return false;
        }
      }
    }
    return true;
  }

  //Move block down for every time interval
  blockDown() {
    if (this.validMove(0, 1)) {
      if (millis() - counter > 400 / this.speed) {
        this.y++;
        counter = millis();
      }
    } else {
      //Once block cannot be moved down further, update game field
      for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
          if (this.shapeArr[i][j] == 1) board[this.y + i][this.x + j] = 1;
        }
      }
      //generate new block
      generateBlock = true;
    }
  }

  //Move Block left or right. left when direction is -1, right when direction is 1
  blockLeftRight(dX) {
    if (direction == -1) {
    }
  }

  //draw blocks. for each 1s in the shapeArr, block image will be placed
  drawBlock() {
    this.shapeType(this.type);
    blockImg = blockImg.get(0, 0, 20, 20);
    push();
    translate(25, 30);
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (this.shapeArr[i][j] == 1)
          image(blockImg, blockSize * (this.x + j), blockSize * (this.y + i));
      }
    }
    pop();
  }
}

//visually display the gameboard.
function displayBoard() {
  blockImg = blockImg.get(0, 0, 20, 20);
  push();
  translate(25, 30);
  for (let i = 0; i < boardHeight; i++) {
    for (let j = 0; j < boardWidth; j++) {
      if (board[i][j] == 1) image(blockImg, blockSize * j, blockSize * i);
    }
  }
  pop();
}

Future Improvements

There are tons of more things to implement in this game:

    1. Moving blocks horizontally using key inputs.
    2. Rotating blocks using key inputs.
    3. This means more logic must be added to the movement validation method.
    4. Add conditions that will check if game is over or not.
    5. Add conditions that will check if user had made a horizontal line using blocks. Then, the program must be able to remove that line and shift whole game filed down by the number of removed lines unit(s).
    6. The game will not reset even if user returns to the main page in the middle of the game. This can be fixed by resetting every variable at the start of each game.

[Assignment 4] Population By Cities

Concept

For this assignment, I did data visualization of population by cities based on this dataset: Population by Cities (downloaded from https:// www.kaggle.com/data sets/i2i2i2/cities-of-the-world?select=cities15 000.csv). The inspiration came from pointillism, an art technique in which small dots are used to form an image. I was curious if I would be able to draw a world map by putting points on where the cities are located. I marked cities with large populations in red and blue colors to indicate these cities are large. In the end, I was able to generate a world map only using small points on the canvas as shown above.

Code

This assignment was rather straightforward to code because it did not require any mathematical logics, unlike my previous works. This code starts by loading dataset into an array. Then, the program will find minimum and maximum values of latitude, longitude, and population of the cities. Different from the class example, I used an array to store minimum and maximum values, because I would have to declare 6 different variables without arrays. Function findMinMax() is used to find these values from the dataset. The function will break down each row of dataset for every “,” it has. Then, the program goes through the entire entity in a given index to find min and max. In the end, the function returns an array that contains min and max values.

let dataArr = []; //Stores city population and location data

//0: min value, 1: max value
let latRangeArr = []; //latitude range
let longRangeArr = []; //longitude range
let popRangeArr = []; //population range

//In preload()
dataArr = loadStrings("data.csv"); //load data

//In setup()
latRangeArr = findMinMax(dataArr, 1);, 
longRangeArr = findMinMax(dataArr, 2);
popRangeArr = findMinMax(dataArr, 3);

function findMinMax(myArr, index) {
  let returnArr = [];

  let minN = Number.MAX_VALUE;
  let maxN = Number.MIN_VALUE;

  for (let i = 1; i < myArr.length; i++) {
    row = split(myArr[i], ",");

    let temp = float(row[index]);

    if (temp < minN) minN = temp; if (temp > maxN) maxN = temp;
  }

  returnArr[0] = minN;
  returnArr[1] = maxN;

  return returnArr;
}

Calculated min and max will be used to scale up or down the data based on the canvas size.

Very next step I took is to put points on the canvas using latitudes and longitudes. Each point was created using classes because each point must contain information about latitude, longitude, and population. Class method drawCity() will actually draw points on a canvas when called.

class City {
  constructor(long, lat, pop) {
    this.long = long;  //longitude
    this.lat = lat;  //latitude
    this.pop = pop;  //population
  }

  drawCity() {
    //I removed some part for the purpose of demonstration
    circle(this.long, this.lat, r);  //draw circles
  }
}

Using for() loop, the program creates City objects and stores them into an array. map() was used to scale longitude and latitude data relative to the width and height of p5js canvas. The population data was scaled from 0 to 100, so I can known population of cities in percentile for later.

for (let i = 1; i < dataArr.length; i++) {
    row = split(dataArr[i], ",");
    //create City object and store in array. map() is used to fit values into a given range
    cityArr[i] = new City(
      map(float(row[2]), longRangeArr[0], longRangeArr[1], 0, width),
      map(float(row[1]), latRangeArr[0], latRangeArr[1], height, 0),
      map(float(row[3]), 0, popRangeArr[1], 0, 100)
    );

    cityArr[i].drawCity();  //draw City objects
  }

At this point, I was able to generate a world map using only points. Then, I changed drawCity() as shown below to mark large population cities in reds and blues. Top 30% cities is drawn red and top 30%~60% cities were drawn blue. For cities that are not within the range was drawn in grey circles. Not to mention, because there were so many cities, I had to increase the size of red/blue circles in order to make them visible.

drawCity() {
    let r = 0.5;
    noFill();
    strokeWeight(1.25);
    stroke(100, 100, 100);
    if (this.pop > 70) {  //display top 30% (population) cities as red circles 
      strokeWeight(2.5);
      stroke(220, 0, 0);
      r = 4;
    } else if (this.pop > 40) {  //display top 30%~60% (population) cities as blue circles
      strokeWeight(2.5);
      stroke(0, 0, 220);
      r = 3;
    }
    circle(this.long, this.lat, r);  //draw circles
  }

Reflection / Future Improvements

This project was very satisfying to just look at it. I really like the fact that small points placed in different coordinates can actually produce a map of our world. Though some countries, such as Greenland, is not visible due to the missing information, anyone who sees this project will be able to recognize the world map. It was also surprising to see capital of my home country—Seoul, South Korea—is within top 30%~60% cities in terms of the population size.

In the I would like find a more complete dataset, which will allow me to make a better map that includes countries that are not displayed in this project. Not to mention, I do not like the colors used here. Although I wanted highly populated cities to be visible on the map, red and blue are not in harmony with grey color. Next time, I would like to find a better color combination.