Week 2 – Maneuvring Around Difficulties

Intro

Sometimes, unexpected pop-ups may not be serendipities but discoveries of limits that propel us to set off on a new adventure.

My expenditure this time didn’t turn out to be very smooth in terms of my realizing my initial vision and how that vision deviates from my expectations. On the other hand, I believe these sorts of experiences would be quite beneficial as a lesson learned to guide future planning.

As my initial vision (or product concept) changed many times in the course, it’s rather difficult to start this documentary with a definitive concept aforesaid. Therefore, I will briefly touch on the concept of each of the prototypes alongside my process description.

process

A Definitive Heart in 3D Space

In the first stage of my development, my goal was to construct a framework of basic interactions that could satisfy my needs later on – which was attempted later on but eventually did not stand for my final product. Hence, I stuck to the idea of generating a visible coordinate system in 3D space at the beginning and wrote a basic ‘arrow generating-shooting’ mechanism.

The loops in this prototype are mostly for displaying every ‘arrow’ I stored in an array and the axis plane in each frame.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let arrowArray = [];
function draw() {
...
// Display all arrows in arrowArray for 1 frame
if (arrowArray[0]) {
for (let i = 1; j = arrowArray[i] ;i += 1) {
if (j._3Dz < -50) {
arrowArray.splice(i, 1); // Remove the arrow that is behind the plane
} else {
j.arrowDisplay(); // A method in Arrow class
}
}
}
}
// Draw axis plane on a Graphics object
function drawAxis() {
// translate to 2D coordinate system
axis.translate(-axis.width / 2, -axis.height / 2);
axis.background(10);
// draw the axises
axis.stroke('white');
axis.line(0, axis.height / 2, axis.width, axis.height / 2);
axis.line(axis.width / 2, 0, axis.width / 2, axis.height);
// draw the marks on the y axis
for (let i = 0; i < width; i += axis.height / 20) {
axis.line(axis.width / 2,
0 + i,
axis.width / 2 + 5,
0 + i);
}
// draw the marks on the x axis
for (let i = 0; i < width; i += axis.width / 20) {
axis.line(0 + i,
axis.height / 2,
0 + i,
axis.height / 2 - 5);
}
}
let arrowArray = []; function draw() { ... // Display all arrows in arrowArray for 1 frame if (arrowArray[0]) { for (let i = 1; j = arrowArray[i] ;i += 1) { if (j._3Dz < -50) { arrowArray.splice(i, 1); // Remove the arrow that is behind the plane } else { j.arrowDisplay(); // A method in Arrow class } } } } // Draw axis plane on a Graphics object function drawAxis() { // translate to 2D coordinate system axis.translate(-axis.width / 2, -axis.height / 2); axis.background(10); // draw the axises axis.stroke('white'); axis.line(0, axis.height / 2, axis.width, axis.height / 2); axis.line(axis.width / 2, 0, axis.width / 2, axis.height); // draw the marks on the y axis for (let i = 0; i < width; i += axis.height / 20) { axis.line(axis.width / 2, 0 + i, axis.width / 2 + 5, 0 + i); } // draw the marks on the x axis for (let i = 0; i < width; i += axis.width / 20) { axis.line(0 + i, axis.height / 2, 0 + i, axis.height / 2 - 5); } }
let arrowArray = [];

function draw() {
...
// Display all arrows in arrowArray for 1 frame
  if (arrowArray[0]) {
    for (let i = 1; j = arrowArray[i] ;i += 1) {
      if (j._3Dz < -50) {
        arrowArray.splice(i, 1); // Remove the arrow that is behind the plane
      } else {
        j.arrowDisplay(); // A method in Arrow class
      }
    }
  }
}

// Draw axis plane on a Graphics object
function drawAxis() {
  // translate to 2D coordinate system
  axis.translate(-axis.width / 2, -axis.height / 2);
  
  axis.background(10);
  
  // draw the axises
  axis.stroke('white');
  axis.line(0, axis.height / 2, axis.width, axis.height / 2);
  axis.line(axis.width / 2, 0, axis.width / 2, axis.height);
  
  // draw the marks on the y axis
  for (let i = 0; i < width; i += axis.height / 20) {
    axis.line(axis.width / 2, 
         0 + i, 
         axis.width / 2 + 5, 
         0 + i);
  }
  
  // draw the marks on the x axis
  for (let i = 0; i < width; i += axis.width / 20) {
    axis.line(0 + i, 
         axis.height / 2,
         0 + i,
         axis.height / 2 - 5);
  }
}

Here, I adopted a math function that has the graphic appearance of a heart to test out the prototype. As the heart function could have multiple y values for one x value, I used the unit circle to generate the coordinates of the points on the function.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Heart shape function
function functionHeartX(t) {
return 160 * pow(sin(t), 3);
}
function functionHeartY(t) {
return 150 * cos(t) - 55 * cos(2 * t) - 30 * cos(3 * t) - 10 * cos(4 * t);
}
// Heart shape function function functionHeartX(t) { return 160 * pow(sin(t), 3); } function functionHeartY(t) { return 150 * cos(t) - 55 * cos(2 * t) - 30 * cos(3 * t) - 10 * cos(4 * t); }
// Heart shape function
function functionHeartX(t) {
  return 160 * pow(sin(t), 3);
}

function functionHeartY(t) {
  return 150 * cos(t) - 55 * cos(2 * t) - 30 * cos(3 * t) - 10 * cos(4 * t);
}

Besides, I also tried some basic lighting to hue the ‘arrows’ with redish-pinky color. It was quite interesting to see how the gamma factor affects the actual light color.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
lights();
ambientLight(50); // as a gamma factor
pointLight(
255, 0, 0, // color
0, 0, 0 // position
);
lights(); ambientLight(50); // as a gamma factor pointLight( 255, 0, 0, // color 0, 0, 0 // position );
lights();
ambientLight(50); // as a gamma factor
pointLight(
  255, 0, 0, // color
  0, 0, 0 // position
);

A Probability Heart in 2D Space

Then, I started a new prototype to specifically realize generating random positions based on the probability distribution in a 2D space (in order to later adopt into the 3D space). Multiple probability functions were tested in this stage, including:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Gaussian probability function
function gaussianProbability(distance) {
if (distance > maxDistance) {
return 0; // No points beyond maxDistance
}
return exp(-pow(distance, 2) / (2 * pow(sigma, 2))); // Gaussian decay
}
// Quadratic probability function
function quadraticProbability(distance) {
if (distance > maxDistance) {
return 0; // No points beyond maxDistance
}
return max(0, 1 - pow(distance / maxDistance, 2)); // Quadratic decay
}
// Gaussian probability function function gaussianProbability(distance) { if (distance > maxDistance) { return 0; // No points beyond maxDistance } return exp(-pow(distance, 2) / (2 * pow(sigma, 2))); // Gaussian decay } // Quadratic probability function function quadraticProbability(distance) { if (distance > maxDistance) { return 0; // No points beyond maxDistance } return max(0, 1 - pow(distance / maxDistance, 2)); // Quadratic decay }
// Gaussian probability function
function gaussianProbability(distance) {
  if (distance > maxDistance) {
    return 0; // No points beyond maxDistance
  }
  return exp(-pow(distance, 2) / (2 * pow(sigma, 2))); // Gaussian decay
}

// Quadratic probability function
function quadraticProbability(distance) {
  if (distance > maxDistance) {
    return 0; // No points beyond maxDistance
  }
  return max(0, 1 - pow(distance / maxDistance, 2)); // Quadratic decay
}

It is also noticeable that the default random() function could be used as such to mimic the probability realization:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Add point based on probability
if (random() < probability) {
points.push(createVector(x + width / 2, -y + height / 2)); // Store the point
}
// Add point based on probability if (random() < probability) { points.push(createVector(x + width / 2, -y + height / 2)); // Store the point }
// Add point based on probability
 if (random() < probability) {
   points.push(createVector(x + width / 2, -y + height / 2)); // Store the point
}

A Probability Heart in 3D Space

After that, it was much easier to adopt the probability parts into the 3D version (and to be presented as a cliche gift in a long distance relationship):

A Grayscale Webcam Downgrader

# Please access the p5js page directly from the instance to allow webcam/mic usage for the following demonstration.

On top of that, I started to experiment with the webcam as an input:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Create a video capture from the webcam
video = createCapture(VIDEO, { flipped:true });
video.hide(); // Hide the default video element that appears under the canvas
// Create a video capture from the webcam video = createCapture(VIDEO, { flipped:true }); video.hide(); // Hide the default video element that appears under the canvas
// Create a video capture from the webcam
video = createCapture(VIDEO, { flipped:true });
video.hide(); // Hide the default video element that appears under the canvas

My intentions in this stage is: 1. convert the video into grayscale values instead of RGBs (as the grayscale values can be later on mapped into probabilities); 2. downgrade the video resolution into a mosaic (as, for probability generation purposes, it would save a lot of time and be even more beneficial for the depiction of the overall shape that I planned to deliver with a collective of ‘arrows’).

To achieve the first goal, we have to operate directly on the pixel array that holds the RGB values for each pixel of each frame in the video with a time complexity of O(n) (instead of calling a filter() method for the displayed effect on the video stream when showing the video).

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function videoToGray() {
// Load the pixels from the video
video.loadPixels();
// Check if the video has pixels loaded
if (video.pixels.length > 0) {
// Convert to grayscale
for (let i = 0; i < video.pixels.length; i += 4) {
let r = video.pixels[i]; // Red
let g = video.pixels[i + 1]; // Green
let b = video.pixels[i + 2]; // Blue
// Calculate grayscale value
let gray = (r + g + b) / 3;
// Set the pixel color to the grayscale value
video.pixels[i] = gray; // Red
video.pixels[i + 1] = gray; // Green
video.pixels[i + 2] = gray; // Blue
// pixels[i + 3] stays the same (Alpha)
}
// Update the video pixels
video.updatePixels();
}
}
function videoToGray() { // Load the pixels from the video video.loadPixels(); // Check if the video has pixels loaded if (video.pixels.length > 0) { // Convert to grayscale for (let i = 0; i < video.pixels.length; i += 4) { let r = video.pixels[i]; // Red let g = video.pixels[i + 1]; // Green let b = video.pixels[i + 2]; // Blue // Calculate grayscale value let gray = (r + g + b) / 3; // Set the pixel color to the grayscale value video.pixels[i] = gray; // Red video.pixels[i + 1] = gray; // Green video.pixels[i + 2] = gray; // Blue // pixels[i + 3] stays the same (Alpha) } // Update the video pixels video.updatePixels(); } }
function videoToGray() {
  // Load the pixels from the video
  video.loadPixels();

  // Check if the video has pixels loaded
  if (video.pixels.length > 0) {
    // Convert to grayscale
    for (let i = 0; i < video.pixels.length; i += 4) {
      let r = video.pixels[i];     // Red
      let g = video.pixels[i + 1]; // Green
      let b = video.pixels[i + 2]; // Blue

      // Calculate grayscale value
      let gray = (r + g + b) / 3;

      // Set the pixel color to the grayscale value
      video.pixels[i] = gray;       // Red
      video.pixels[i + 1] = gray;   // Green
      video.pixels[i + 2] = gray;   // Blue
      // pixels[i + 3] stays the same (Alpha)
    }

    // Update the video pixels
    video.updatePixels();
  }
}

As for the second goal, however, it was also at this stage that I did a lot of technical idle work. I started to write my method for downgrading the video resolution right away without checking the features of the video.size() method, and ended up with a function that loops through the pixel arrays with a time complexity of O(n^2) (which became very time-consuming when the original video resolution is relatively high):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function videoToMosaic(mosaicSize) {
// Load the pixels from the video
video.loadPixels();
// Check if the video has pixels loaded
if (video.pixels.length > 0) {
// Clear the mosaicPixels array
mosaicPixels = new Uint8ClampedArray(video.pixels.length);
// Loop through the canvas in blocks
for (let y = 0; y < height; y += mosaicSize) {
for (let x = 0; x < width; x += mosaicSize) {
// Calculate the average color for the block
let r = 0, g = 0, b = 0;
let count = 0;
// Loop through the pixels in the block
for (let j = 0; j < mosaicSize; j++) {
for (let i = 0; i < mosaicSize; i++) {
let pixelX = x + i;
let pixelY = y + j;
// Check if within bounds
if (pixelX < width && pixelY < height) {
let index = (pixelX + pixelY * video.width) * 4;
r += video.pixels[index]; // Red
g += video.pixels[index + 1]; // Green
b += video.pixels[index + 2]; // Blue
count++;
}
}
}
// Calculate average color
if (count > 0) {
r = r / count;
g = g / count;
b = b / count;
}
// Set the color for the entire block in the mosaicPixels array
for (let j = 0; j < mosaicSize; j++) {
for (let i = 0; i < mosaicSize; i++) {
let pixelX = x + i;
let pixelY = y + j;
// Check if within bounds
if (pixelX < width && pixelY < height) {
let index = (pixelX + pixelY * video.width) * 4;
mosaicPixels[index] = r; // Set Red
mosaicPixels[index + 1] = g; // Set Green
mosaicPixels[index + 2] = b; // Set Blue
mosaicPixels[index + 3] = 255; // Set Alpha to fully opaque
}
}
}
}
}
function videoToMosaic(mosaicSize) { // Load the pixels from the video video.loadPixels(); // Check if the video has pixels loaded if (video.pixels.length > 0) { // Clear the mosaicPixels array mosaicPixels = new Uint8ClampedArray(video.pixels.length); // Loop through the canvas in blocks for (let y = 0; y < height; y += mosaicSize) { for (let x = 0; x < width; x += mosaicSize) { // Calculate the average color for the block let r = 0, g = 0, b = 0; let count = 0; // Loop through the pixels in the block for (let j = 0; j < mosaicSize; j++) { for (let i = 0; i < mosaicSize; i++) { let pixelX = x + i; let pixelY = y + j; // Check if within bounds if (pixelX < width && pixelY < height) { let index = (pixelX + pixelY * video.width) * 4; r += video.pixels[index]; // Red g += video.pixels[index + 1]; // Green b += video.pixels[index + 2]; // Blue count++; } } } // Calculate average color if (count > 0) { r = r / count; g = g / count; b = b / count; } // Set the color for the entire block in the mosaicPixels array for (let j = 0; j < mosaicSize; j++) { for (let i = 0; i < mosaicSize; i++) { let pixelX = x + i; let pixelY = y + j; // Check if within bounds if (pixelX < width && pixelY < height) { let index = (pixelX + pixelY * video.width) * 4; mosaicPixels[index] = r; // Set Red mosaicPixels[index + 1] = g; // Set Green mosaicPixels[index + 2] = b; // Set Blue mosaicPixels[index + 3] = 255; // Set Alpha to fully opaque } } } } }
function videoToMosaic(mosaicSize) {
  // Load the pixels from the video
  video.loadPixels();
  
  // Check if the video has pixels loaded
  if (video.pixels.length > 0) {
    // Clear the mosaicPixels array
    mosaicPixels = new Uint8ClampedArray(video.pixels.length);
    
    // Loop through the canvas in blocks
    for (let y = 0; y < height; y += mosaicSize) {
      for (let x = 0; x < width; x += mosaicSize) {
        // Calculate the average color for the block
        let r = 0, g = 0, b = 0;
        let count = 0;

        // Loop through the pixels in the block
        for (let j = 0; j < mosaicSize; j++) {
          for (let i = 0; i < mosaicSize; i++) {
            let pixelX = x + i;
            let pixelY = y + j;

            // Check if within bounds
            if (pixelX < width && pixelY < height) {
              let index = (pixelX + pixelY * video.width) * 4;
              r += video.pixels[index];       // Red
              g += video.pixels[index + 1];   // Green
              b += video.pixels[index + 2];   // Blue
              count++;
            }
          }
        }

        // Calculate average color
        if (count > 0) {
          r = r / count;
          g = g / count;
          b = b / count;
        }

        // Set the color for the entire block in the mosaicPixels array
        for (let j = 0; j < mosaicSize; j++) {
          for (let i = 0; i < mosaicSize; i++) {
            let pixelX = x + i;
            let pixelY = y + j;

            // Check if within bounds
            if (pixelX < width && pixelY < height) {
              let index = (pixelX + pixelY * video.width) * 4;
              mosaicPixels[index] = r;        // Set Red
              mosaicPixels[index + 1] = g;    // Set Green
              mosaicPixels[index + 2] = b;    // Set Blue
              mosaicPixels[index + 3] = 255;   // Set Alpha to fully opaque
            }
          }
        }
      }
    }

At the end of the day, it turned out that I could simply lower the hardware resolution with video.size() method:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
video.size(50, 50); // Set the size of the video
video.size(50, 50); // Set the size of the video
video.size(50, 50); // Set the size of the video

Random Points Generated Based on Probability

Next, I wrote a prototype to see how the mosaic of probabilities could guide the random generation. The core code at this stage is to map the x-y coordinates on the canvas to the mosaic pixel array and map the grayscale value to probability (the greater the grayscale, the lower the probability, as the brighter the mosaic, the fewer the points):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function mapToPixelIndex(x, y) {
// Map the y-coordinate to the pixel array
let pixelX = Math.floor((x + windowWidth / 2) * j / windowWidth);
let pixelY = Math.floor((y + windowHeight / 2) * k / windowHeight);
// Ensure the pixel indices are within bounds
pixelX = constrain(pixelX, 0, j - 1);
pixelY = constrain(pixelY, 0, k - 1);
// Convert 2D indices to 1D index
return pixelX + pixelY * j;
}
function shouldDrawPoint(x, y) {
let index = mapToPixelIndex(x, y);
let grayscaleValue = pixelArray[index];
// Convert grayscale value to probability
let probability = 1 - (grayscaleValue / 255);
// Decide to draw the point based on probability
return random() < probability; // Return true or false based on random chance
}
function mapToPixelIndex(x, y) { // Map the y-coordinate to the pixel array let pixelX = Math.floor((x + windowWidth / 2) * j / windowWidth); let pixelY = Math.floor((y + windowHeight / 2) * k / windowHeight); // Ensure the pixel indices are within bounds pixelX = constrain(pixelX, 0, j - 1); pixelY = constrain(pixelY, 0, k - 1); // Convert 2D indices to 1D index return pixelX + pixelY * j; } function shouldDrawPoint(x, y) { let index = mapToPixelIndex(x, y); let grayscaleValue = pixelArray[index]; // Convert grayscale value to probability let probability = 1 - (grayscaleValue / 255); // Decide to draw the point based on probability return random() < probability; // Return true or false based on random chance }
function mapToPixelIndex(x, y) {
  // Map the y-coordinate to the pixel array
  let pixelX = Math.floor((x + windowWidth / 2) * j / windowWidth);
  let pixelY = Math.floor((y + windowHeight / 2) * k / windowHeight);
  
  // Ensure the pixel indices are within bounds
  pixelX = constrain(pixelX, 0, j - 1);
  pixelY = constrain(pixelY, 0, k - 1);
  
  // Convert 2D indices to 1D index
  return pixelX + pixelY * j;
}

function shouldDrawPoint(x, y) {
  let index = mapToPixelIndex(x, y);
  let grayscaleValue = pixelArray[index];
  
  // Convert grayscale value to probability
  let probability = 1 - (grayscaleValue / 255);
  
  // Decide to draw the point based on probability
  return random() < probability; // Return true or false based on random chance
}

A Probability Webcam in 3D space

‘Eventually’, I incorporated all the components together to try out my initial vision of creating a fluid, responsive, and vague but solid representation of images from the webcam with arrows generated based on probabilities flying towards an axis plane.

Unfortunately, although the product with many tuning of variables like the amount of arrows, the spread and speed of the arrows, and the lapse of arrows on the axis, etc., there could be a vague representation captured (aided by re-mapping & stretching out the probabilities with more contrasting grayscale values), the huge amount of 3D objects required to shape a figure significantly undermines the experience of running the product.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function applyHighContrast(array) {
// Stretch the grayscale values to increase contrast
let minVal = Math.min(...array);
let maxVal = Math.max(...array);
// Prevent division by zero if all values are the same
if (minVal === maxVal) {
return array.map(() => 255);
}
// Apply contrast stretching with a scaling factor
const contrastFactor = 6; // Increase this value for more contrast
return array.map(value => {
// Apply contrast stretching
let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor;
// Clip the value to ensure it stays within bounds
return constrain(Math.round(stretchedValue), 0, 255);
});
}
function applyHighContrast(array) { // Stretch the grayscale values to increase contrast let minVal = Math.min(...array); let maxVal = Math.max(...array); // Prevent division by zero if all values are the same if (minVal === maxVal) { return array.map(() => 255); } // Apply contrast stretching with a scaling factor const contrastFactor = 6; // Increase this value for more contrast return array.map(value => { // Apply contrast stretching let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor; // Clip the value to ensure it stays within bounds return constrain(Math.round(stretchedValue), 0, 255); }); }
function applyHighContrast(array) {
  // Stretch the grayscale values to increase contrast
  let minVal = Math.min(...array);
  let maxVal = Math.max(...array);

  // Prevent division by zero if all values are the same
  if (minVal === maxVal) {
    return array.map(() => 255);
  }

  // Apply contrast stretching with a scaling factor
  const contrastFactor = 6; // Increase this value for more contrast
  return array.map(value => {
    // Apply contrast stretching
    let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor;
    
    // Clip the value to ensure it stays within bounds
    return constrain(Math.round(stretchedValue), 0, 255);
  });
}

Besides, the 3D space did not benefit the demonstration of this idea but hindered it as the perspectives of the arrow farther away from the focus would make them occupy more visual space and disturb the probability distribution. 

A Probability Webcam in 2D space

At the end of the day, after trying the ortho() method in the 3D version (which makes the objects appear without the affecting perspectives), I realized that reconstructing a 2D version was the right choice to better achieve my goals.

In this latest 2D version, I gave up the idea of drawing the axis plane and introduced the concept of probability distribution affected by ‘mic level’.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function setup() {
...
// Create an audio from mic
audio = new p5.AudioIn();
audio.start(); // start mic
}
function draw() {
...
// Map the audio level to contrast factor
let level = map(audio.getLevel(), 0, 1, 1.05, 30);
// Apply high contrast transformation
pixelArray = applyHighContrast(pixelArray, level);
...
}
function applyHighContrast(array, contrastFactor) {
// Stretch the grayscale values to increase contrast
let minVal = Math.min(...array);
let maxVal = Math.max(...array);
// Prevent division by zero if all values are the same
if (minVal === maxVal) {
return array.map(() => 255);
}
// Apply contrast stretching with a scaling factor
return array.map(value => {
// Apply contrast stretching
let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor;
// Clip the value to ensure it stays within bounds
return constrain(Math.round(stretchedValue), 0, 255);
});
}
function setup() { ... // Create an audio from mic audio = new p5.AudioIn(); audio.start(); // start mic } function draw() { ... // Map the audio level to contrast factor let level = map(audio.getLevel(), 0, 1, 1.05, 30); // Apply high contrast transformation pixelArray = applyHighContrast(pixelArray, level); ... } function applyHighContrast(array, contrastFactor) { // Stretch the grayscale values to increase contrast let minVal = Math.min(...array); let maxVal = Math.max(...array); // Prevent division by zero if all values are the same if (minVal === maxVal) { return array.map(() => 255); } // Apply contrast stretching with a scaling factor return array.map(value => { // Apply contrast stretching let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor; // Clip the value to ensure it stays within bounds return constrain(Math.round(stretchedValue), 0, 255); }); }
function setup() {
  ...

  // Create an audio from mic
  audio = new p5.AudioIn();
  audio.start(); // start mic
}

function draw() {
  ...

  // Map the audio level to contrast factor
  let level = map(audio.getLevel(), 0, 1, 1.05, 30);
  // Apply high contrast transformation
  pixelArray = applyHighContrast(pixelArray, level);
  ...
}

function applyHighContrast(array, contrastFactor) {
  // Stretch the grayscale values to increase contrast
  let minVal = Math.min(...array);
  let maxVal = Math.max(...array);

  // Prevent division by zero if all values are the same
  if (minVal === maxVal) {
    return array.map(() => 255);
  }

  // Apply contrast stretching with a scaling factor
  return array.map(value => {
    // Apply contrast stretching
    let stretchedValue = ((value - minVal) * (255 / (maxVal - minVal))) * contrastFactor;
    
    // Clip the value to ensure it stays within bounds
    return constrain(Math.round(stretchedValue), 0, 255);
  });
}

reflection

TBH, the technical explorations did consume much of my time this week, and it came to me later to realize that I could have thought of more about the theme ‘loop’ before getting started – as it appears now to be a very promising topic to delve deeper into. Nevertheless, I believe our workflow of production could be like this from time to time, and it is crucial to maintain this balance thoughout.

 

 

 

Week 1 – Self-Portrait: The Sword of Damocles

Intro

Sometimes I wonder if there is indeed some force behind coincidence – especially when those coincidences are of my favorability.

Right before I set off for my adventurous journey to Abu Dhabi, I visited an exhibition in Shanghai, in which a collection of portraits and self-portraits were borrowed from Centre Pompidou. From those who devoted their efforts to whether capture the subject faithfully or replicate their physical appearance to those who indulged their artistic preference, streaks of character, habits, and egos into the paints, the portraits collectively reminded me of that recurrent theme in every art form, I presume – the tension between the art and the artist. Aside from the cliche saying from Oscar Wild (although his definitive statement is, even for an ambivalent person like I am, attractive enough), I would stick to leaving the answer for now.

On the other hand, even if I cannot answer the big question for now (and, in fact, it could be forever), I can indeed establish a bit of myself under this daunting siding game. One of my takeaways from my first acting class today is that as a narrator, despite our habits of storytelling and the eventual manner we picked, it is our responsibility to Stay True in terms of this present moment, the soul of our story. I’ll not delve into the other takeaway of being courageous to leave others the right to tell our stories for now but make use of the first one promptly.

That being said, when it comes to composing my own self-portrait, which I’ve never tried, I’d be glad to take full responsibility for representing myself at this moment. To me, this is something that demands great courage as well. Looking back at how I could probably gain this courage, I’d say it’s the fact that I, at the end of the day, tried to admit the lack of it for so many years – from representing myself, playing music, to speaking up when I’d love to, dealing with interpersonal relationships, and to live and love, had brought me that serenity and tranquility to seeking for courage. It seems for many of us, at least many people I know, as soon as the concept of being courageous and brave was instilled into our heads, we started to self-exert as well as receive external expectations to “be courageous and brave” right away. And simply, it is impossible. It is impossible to develop that true courage and bravery by merely being told how a courageous person would possibly behave instead of honing oneself step by step and again and again in this world of reality.

Product Concept

Despite my rationale (or more of like my self-prep talk), it’s time to tackle the problem (if not the question), and I’ll try to put it as plainly as possible. At this moment, I’d be willing to conceptualize myself into a set of symbols, figures, or whatever you may call some figurative representations that could be found in real life. Although I could make a list of the symbols I’m about to include in this portrait right here, I’d like to leave it till the end and see if my conceptualization and abstraction make sense to you.

Nevertheless, the overall concept is MEs versus an ideological black hole that I’ve found as the Sword of Damocles in my life. That being said, whenever the mouse is pressed, my hovering incarnations in this blended world of 2D and 3D shapes will initiate their attack toward the black hole despite their shaky, timid, and hyped state, setting off on a journey with no way back.

Technically speaking, my using 2D shapes as my personal representatives while letting the black hole be 3D results from the simple attempt to utilize both 2D and 3D drawing/shaping functions somewhere around this project (plus, it would be quite time-consuming if I used basic 3D shapes to abstract the symbols I’d like to include). However, I can indeed also justify this choice in terms of its symbolic connotation. To reflect the nature of this colossal hazard, bain of insecurity, and ineffable abyss that lies completely out of my mortal control, I placed the threat one dimension higher than myself.

Metaphorically, MEs’ attacking the black hole embodies the courage I’d like to attain despite what comes next. As for the ending of this one-act play, maybe it’s MEs’ begone from this world? Or maybe it’s MEs entering a brand-new universe. At least if the black hole could be gone from this particular universe, it would be a happy ending for both me and the rest.

coding highlight

Despite the fact that I was not familiar with js, I would self-reckon as someone who is used to product-oriented coding; in other words, I tried to utilize as much as possible at this moment from the source to get to my ideal product as closely as possible. Although, from time to time, this mindset could lead me to a hasty organization and poor-looking codes, I do believe it’s beneficial for me to realize (at least part of) my vision quickly.

In this product, I took the black hole as a starting point. While calling the function shpere() is rather an easy deal, it did take me some time to figure out how to maintain both 2D and 3D shapes on the same canvas. The answer turned out to be using push()/pop() to create an individual drawing group. Another takeaway is the general approach to creating animation with p5js – thinking from a perspective of frame generation, specifying the animation frame by frame. And this led me to use variables to store the current status and then update the variables to achieve animation (see the usage of rotateX()/rotateY()). On top of that, to add texture() to the sphere, I tried with loadImage() in the preload() function (and thus used an image as the background as well).

*: all image files used in this project are generated with DALL-E3

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// draw the sphere with texture
function blackhole() {
// Increment the rotation angles
angleXrotate += 0.005; // Adjust the speed by changing this value
angleYrotate += 0.005; // Adjust the speed by changing this value
push(); // save the current state
translate(0, -200, 0); // translate the coordinate system temporarily to place the sphere
rotateX(angleXrotate); // Rotate around X-axis
rotateY(angleYrotate); // Rotate around Y-axis
texture(imgBlackhole); // adding texture to the sphere
stroke('white'); // contour the sphere
strokeWeight(0.25);
sphere(100); // placing the sphere on the canvas with r = 100
pop(); // return to the previous state
}
// draw the sphere with texture function blackhole() { // Increment the rotation angles angleXrotate += 0.005; // Adjust the speed by changing this value angleYrotate += 0.005; // Adjust the speed by changing this value push(); // save the current state translate(0, -200, 0); // translate the coordinate system temporarily to place the sphere rotateX(angleXrotate); // Rotate around X-axis rotateY(angleYrotate); // Rotate around Y-axis texture(imgBlackhole); // adding texture to the sphere stroke('white'); // contour the sphere strokeWeight(0.25); sphere(100); // placing the sphere on the canvas with r = 100 pop(); // return to the previous state }
// draw the sphere with texture
function blackhole() {
  // Increment the rotation angles
  angleXrotate += 0.005; // Adjust the speed by changing this value
  angleYrotate += 0.005; // Adjust the speed by changing this value

  push(); // save the current state
  
  translate(0, -200, 0); // translate the coordinate system temporarily to place the sphere
  
  rotateX(angleXrotate); // Rotate around X-axis
  rotateY(angleYrotate); // Rotate around Y-axis
  
  texture(imgBlackhole); // adding texture to the sphere
  
  stroke('white'); // contour the sphere
  strokeWeight(0.25);
  
  sphere(100); // placing the sphere on the canvas with r = 100
  
  pop(); // return to the previous state

}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let imgBlackhole; // variable to hold image texture for the sphere
let imgUniverse; // variable to hold image for the background
function preload() {
// load image files
imgBlackhole = loadImage('blackhole.jpg');
imgUniverse = loadImage('the universe.jpg');
}
let imgBlackhole; // variable to hold image texture for the sphere let imgUniverse; // variable to hold image for the background function preload() { // load image files imgBlackhole = loadImage('blackhole.jpg'); imgUniverse = loadImage('the universe.jpg'); }
let imgBlackhole; // variable to hold image texture for the sphere
let imgUniverse; // variable to hold image for the background

function preload() {
  // load image files
  imgBlackhole = loadImage('blackhole.jpg');
  imgUniverse = loadImage('the universe.jpg');
}

As for the ME representatives, I break it down into two parts: the static drawing and the animation. Initially, I adopted the animation approach mentioned in the black hole section, only used functions to hold the drawing operations and called them in the draw() function. However, as my drawings are rather complex (although they have abstract appearances), I found that the code performance had started to decline. Therefore, I reorganized the code by placing the drawings into p5.Graphics object (that provides a dedicated drawing surface), in order to improve efficiency. This also enabled me to process the objects individually as a whole later on. Following is one example of my utilizing p5.Graphics object to draw an abstract guitar.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function drawGuitar() {
let c = color('#bd6d36');
let c1 = color('#592c0e');
let c2 = color('#814116');
// Create a graphics object guitar
guitar = createGraphics(100, 150);
// Draw to the graphics object guitar
// Body of the guitar
guitar.fill(c); // Brown color
guitar.noStroke()
guitar.beginShape();
guitar.vertex(25, 100); // Start at bottom left
guitar.bezierVertex(10, 75, 10, 50, 25, 25); // Left curve
guitar.bezierVertex(75, 0, 75, 50, 25, 25); // Right curve
guitar.bezierVertex(90, 50, 90, 75, 25, 100); // Bottom curve
guitar.endShape(CLOSE);
// Neck of the guitar
guitar.fill(c2); // Mid-dark brown color
guitar.rect(25, 5, 8, 80); // Neck
// Sound hole
guitar.fill(c1); // Dark sound hole
guitar.ellipse(25, 50, 10, 25); // Draw sound hole
// Strings
guitar.stroke('white'); // White color for strings
guitar.strokeWeight(1);
for (let i = 0; i < 6; i++) {
guitar.line(25 + 2 * i, 0, 25 + 2 * i, 60); // Draw strings
}
}
function drawGuitar() { let c = color('#bd6d36'); let c1 = color('#592c0e'); let c2 = color('#814116'); // Create a graphics object guitar guitar = createGraphics(100, 150); // Draw to the graphics object guitar // Body of the guitar guitar.fill(c); // Brown color guitar.noStroke() guitar.beginShape(); guitar.vertex(25, 100); // Start at bottom left guitar.bezierVertex(10, 75, 10, 50, 25, 25); // Left curve guitar.bezierVertex(75, 0, 75, 50, 25, 25); // Right curve guitar.bezierVertex(90, 50, 90, 75, 25, 100); // Bottom curve guitar.endShape(CLOSE); // Neck of the guitar guitar.fill(c2); // Mid-dark brown color guitar.rect(25, 5, 8, 80); // Neck // Sound hole guitar.fill(c1); // Dark sound hole guitar.ellipse(25, 50, 10, 25); // Draw sound hole // Strings guitar.stroke('white'); // White color for strings guitar.strokeWeight(1); for (let i = 0; i < 6; i++) { guitar.line(25 + 2 * i, 0, 25 + 2 * i, 60); // Draw strings } }
function drawGuitar() {
  let c = color('#bd6d36');
  let c1 = color('#592c0e');
  let c2 = color('#814116');
  
  // Create a graphics object guitar
  guitar = createGraphics(100, 150);

  // Draw to the graphics object guitar
  // Body of the guitar
  guitar.fill(c); // Brown color
  guitar.noStroke()
  guitar.beginShape();
  guitar.vertex(25, 100); // Start at bottom left
  guitar.bezierVertex(10, 75, 10, 50, 25, 25); // Left curve
  guitar.bezierVertex(75, 0, 75, 50, 25, 25); // Right curve
  guitar.bezierVertex(90, 50, 90, 75, 25, 100); // Bottom curve
  guitar.endShape(CLOSE);

  // Neck of the guitar
  guitar.fill(c2); // Mid-dark brown color
  guitar.rect(25, 5, 8, 80); // Neck
  
  // Sound hole
  guitar.fill(c1); // Dark sound hole
  guitar.ellipse(25, 50, 10, 25); // Draw sound hole

  // Strings
  guitar.stroke('white'); // White color for strings
  guitar.strokeWeight(1);
  for (let i = 0; i < 6; i++) {
    guitar.line(25 + 2 * i, 0, 25 + 2 * i, 60); // Draw strings
  }
  
}

That being said, as I started working on the animation for MEs, p5.Graphics objects made it easier for me to instantiate them in class in order to define different behaviors (aka. hovering around the mouse and attacking the black hole). While I will not delve into every function I wrote for the MEs, I would like to briefly outline its operation logic. As the centerpiece, a class called Hoveringme includes six variables as parameters and two methods. The X/Y variables are used to track the relative coordinate of the object to the mouse and its absolute coordinate on the canvas. The show() method is my version of the draw() function for this particular class and will be called in the draw() function in every frame. The attack() method only updates the instance status (whether it’s ‘hovering’ or ‘attacking’), which, as a condition, determines the exact operation in the show() method.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// the hovering MEs' class
class Hoveringme {
constructor(relativeX, relativeY, shape, status) {
this.relativeX = relativeX;
this.relativeY = relativeY;
this.shape = shape;
this.status = 'hovering';
this.displayedX = 0
this.displayedY = 0;
}
// show the MEs
show() {
push();
/*
...
*/
pop();
}
// ME to attack the blackhole
attack() {
this.status = 'attacking';
}
}
// the hovering MEs' class class Hoveringme { constructor(relativeX, relativeY, shape, status) { this.relativeX = relativeX; this.relativeY = relativeY; this.shape = shape; this.status = 'hovering'; this.displayedX = 0 this.displayedY = 0; } // show the MEs show() { push(); /* ... */ pop(); } // ME to attack the blackhole attack() { this.status = 'attacking'; } }
// the hovering MEs' class
class Hoveringme {
  constructor(relativeX, relativeY, shape, status) {
    this.relativeX = relativeX;
    this.relativeY = relativeY;
    this.shape = shape;
    this.status = 'hovering';
    this.displayedX = 0
    this.displayedY = 0;
  }
  
  // show the MEs
  show() {
    push();
    /*
    ...
    */
    pop();
    
  }
  
  // ME to attack the blackhole
  attack() {
    this.status = 'attacking';
    
  }
  
}

Just to add a bit about my animations, which are broken down into two statuses as mentioned. First, the hovering status includes random() in the coordinates added on mouseX/Y

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (this.status === 'hovering') {
// Update the ME's position around the mouse
this.displayedX = mouseX + this.relativeX + random(-10, 10) * 0.05;
this.displayedY = mouseY + this.relativeY + random(-10, 10) * 0.05;
}
if (this.status === 'hovering') { // Update the ME's position around the mouse this.displayedX = mouseX + this.relativeX + random(-10, 10) * 0.05; this.displayedY = mouseY + this.relativeY + random(-10, 10) * 0.05; }
if (this.status === 'hovering') {
      // Update the ME's position around the mouse
      this.displayedX = mouseX + this.relativeX + random(-10, 10) * 0.05; 
      this.displayedY = mouseY + this.relativeY + random(-10, 10) * 0.05; 
      
    }

On the other hand, to achieve the attacking effect, the mouseX/Y has to be excluded (otherwise, the translation/movement of the object will always be relative to the mouse’s position).

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
else if (this.status === 'attacking') {
let spherePosition = createVector(0, -200, 0); // Sphere's position
let targetX = spherePosition.x + width / 2; // Sphere's X position in 2D
let targetY = spherePosition.y + height / 2; // Sphere's Y position in 2D
// Update the ME's position towards the target
this.displayedX += (targetX - (this.displayedX)) * 0.05; // Move towards target X
this.displayedY += (targetY - (this.displayedY)) * 0.05; // Move towards target Y
}
else if (this.status === 'attacking') { let spherePosition = createVector(0, -200, 0); // Sphere's position let targetX = spherePosition.x + width / 2; // Sphere's X position in 2D let targetY = spherePosition.y + height / 2; // Sphere's Y position in 2D // Update the ME's position towards the target this.displayedX += (targetX - (this.displayedX)) * 0.05; // Move towards target X this.displayedY += (targetY - (this.displayedY)) * 0.05; // Move towards target Y }
else if (this.status === 'attacking') {
      let spherePosition = createVector(0, -200, 0); // Sphere's position
      let targetX = spherePosition.x + width / 2; // Sphere's X position in 2D
      let targetY = spherePosition.y + height / 2; // Sphere's Y position in 2D
      
      // Update the ME's position towards the target
      this.displayedX += (targetX - (this.displayedX)) * 0.05; // Move towards target X
      this.displayedY += (targetY - (this.displayedY)) * 0.05; // Move towards target Y
}

The other part of the attacking process is to decide if the object has collided with the black hole and, if so, remove it from the canvas. I achieved this by holding all ME instances in an array and then, when needed, removing them along with removing elements from the array (splice ()). (For clearer demonstration, I’ve also placed the instantiating function callingme() here.) In fact, whenever the code deals with the instances, it accesses them by calling the first item in the array objectsme.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let objectsme = []; // array to hold ME graphics objects
// create ME instances, add to array objectsme, and create graphics objects
function callingme() {
guitarObject = new Hoveringme(20, 20, 'guitar');
racketObject = new Hoveringme(-40, -60, 'racket');
rectObject1 = new Hoveringme(-70, -10);
rectObject2 = new Hoveringme(-80, 90);
rectObject3 = new Hoveringme(60, -60);
objectsme.push(guitarObject, racketObject, rectObject1, rectObject2, rectObject3);
drawGuitar();
drawRacket();
}
// in Hoveringme.show() method
if (((this.displayedX - targetX)**2 < 200) & ((this.displayedY - targetY)**2 < 200)) {
objectsme.splice(objectsme[0], 1);
}
let objectsme = []; // array to hold ME graphics objects // create ME instances, add to array objectsme, and create graphics objects function callingme() { guitarObject = new Hoveringme(20, 20, 'guitar'); racketObject = new Hoveringme(-40, -60, 'racket'); rectObject1 = new Hoveringme(-70, -10); rectObject2 = new Hoveringme(-80, 90); rectObject3 = new Hoveringme(60, -60); objectsme.push(guitarObject, racketObject, rectObject1, rectObject2, rectObject3); drawGuitar(); drawRacket(); } // in Hoveringme.show() method if (((this.displayedX - targetX)**2 < 200) & ((this.displayedY - targetY)**2 < 200)) { objectsme.splice(objectsme[0], 1); }
let objectsme = []; // array to hold ME graphics objects

// create ME instances, add to array objectsme, and create graphics objects
function callingme() {
  guitarObject = new Hoveringme(20, 20, 'guitar');
  racketObject = new Hoveringme(-40, -60, 'racket');
  rectObject1 = new Hoveringme(-70, -10);
  rectObject2 = new Hoveringme(-80, 90);
  rectObject3 = new Hoveringme(60, -60);
  objectsme.push(guitarObject, racketObject, rectObject1, rectObject2, rectObject3); 
  drawGuitar();
  drawRacket();
 }

// in Hoveringme.show() method
if (((this.displayedX - targetX)**2 < 200) & ((this.displayedY - targetY)**2 < 200)) {
   objectsme.splice(objectsme[0], 1);
}

reflections, and so on

I would say I’m quite satisfied with this production for now, whether in terms of technical tryouts or basically achieving my message-conveying intention. Still, there is much room for improvement – both technically and beyond. I could have tried with more 3D features, alpha channel, typography, etc. I could have also designed a better disappearing for the black hole when MEs had made the attacks. But overall, I believe this is quite an interesting exploration.

Continue reading “Week 1 – Self-Portrait: The Sword of Damocles”