Final Project :)

Let’s hope this plays well with WordPress…

This really might not play well with WordPress, so if not feel free to download this and run it like any other sketch. You need to have the p5.js file in the same folder as index.html and sketch.js.

Download Link

So here’s my final project. The theme is supposed to be a survival horror sort of game, but taking one look tells you that isn’t quite the case. Everything is pretty barebones and there are no real enemies, so you don’t need to “survive” anything. You were also supposed to pick up a mask on the way out of your “house,” but I decided to avoid loading textures after testing on another PC. There’s a high chance that this may run slowly for you and I didn’t want it to be even worse by adding additional complexity. So now you need to go find a purple “virus crystallization” and take it to a blue square for scientists to research. Once this happens, your screen will turn grey and that’s it. What happens? Who knows, you can decide that for yourself. 

As for controls, it’s pretty much standard PC keyboard controls. Use WASD as if they were the arrow keys, and click your mouse within the sketch to lock it so you can look around. If the game is too dark, press E to turn on the lights. If the game is too small, press F to enter fullscreen. You can’t see much if you fly around because of the render distance, but if you really want to, press G.

Oh, also, there’s no collisions. You can walk through walls if you really want to. This is mostly because I didn’t have the time to completely reinvent my movement and map generation to work with collisions. The main issue was checking multiple objects in one loop where each one would change my velocity vector, often setting it to the default value. Either way, I still think it’s kind of neat to walk around a maze and see what it looks like.

The maze will be 10×10, so go look for a purple square, then head to the blue one. The maze also changes every time you reload the page, so if you find this especially fun feel free to play again.

sketchAAAAAAAA

renderDistance = 2500;

/**
 * @file sketch.js
 * @brief Demo involving 3D movement, lighting, and map creation.
 *
 * @author Lance Yarlott <lcy@andrew.cmu.edu>
 */

/**
 * @brief creates canvas and generates a maze
 */
function setup() {
    createCanvas(600, 600, WEBGL);
    
    //noStroke();
    var cam = createCamera();
    var cam_dq = 20;
    p = makePlayer(cam, cam_dq, 5);

    locked = false;
    walking = true;
    fs = false;
    tiltAngle = 0;
    panAngle = 0;
    lightsOn = true;
    roomSize = 1000;

    ctr = 0;

    var maze = generateMaze(3, 3);
    
    mapArr = new Array();

    for (var i = 0; i < maze.length; i++) {
        mapArr[i] = new Array();
        for (var j = 0; j < maze[i].length; j++) {
            mapArr[i][j] = roomFromCell(maze[i][j], roomSize, "#990000");
            if (maze[i][j].entrance) p.setCell([i, j]);
        }
    }

    p.update();
}

/**
 * @brief aims to draw maze in 3D, currently NON-non-functional :)
 */
function draw() {
    background(0);

    if (!p.won) {
        p.update();

        enableLight(p.camX, p.camY, p.camZ);

        if (lightsOn) {
            ambientLight(20);
        } else {
            ambientLight(100);
        }

        renderMap(mapArr);

        ctr = (ctr + 1) % 360;

        if (mapArr[p.graphCell[0]][p.graphCell[1]].corrupt) {
            p.inventory.push("mask");
            mapArr[p.graphCell[0]][p.graphCell[1]].corrupt = false;
        }

        if (mapArr[p.graphCell[0]][p.graphCell[1]].exit & 
                                p.inventory.includes("mask")) {
            p.won = true;
        }
    } else {
        p.cam.setPosition(0, 5000, 0);
        background(128);
        noLoop();
    }
}

/**
 * @brief though this is a working function, it's a mess
 * 
 * The x and y assignments are absolutely backwards at times and I should fix
 * them. However, if it ain't broke, don't fix it. Which is to say, I don't want
 * to debug this for another 4 hours. Apologies to any grader that has to read
 * this.
 * 
 * @param {int} sizeX horizontal size for array/graph
 * @param {int} sizeY vertical size for array/graph
 */
function generateMaze(sizeX, sizeY) {
    var maze = new Array();

    for (var i = 0; i < sizeY; i++) {
        maze[i] = new Array();
        for (var j = 0; j < sizeX; j++) {
            maze[i][j] = makeCell(j, i, sizeX, sizeY);
        }
    }

    randX = floor(random(0, sizeY));
    if (randX == 0 || randX == sizeY - 1) {
        randY = floor(random(0, sizeX));
    } else {
        randY = floor(random(0, 2)) * (sizeX - 1);
    }

    randEX = floor(random(0, sizeY));
    randEY = floor(random(0, sizeX));

    while (dist(randX, randY, randEX, randEY) < sizeY / 2) {
        randEX = floor(random(0, sizeY));
        randEY = floor(random(0, sizeX));
    }

    randIX = floor(random(0, sizeY));
    randIY = floor(random(0, sizeX));

    maze[randX][randY].entrance = true;
    maze[randEX][randEY].exit = true;
    maze[randIX][randIY].corrupt = true;

    mazeAlgo(maze, maze[randX][randY]);

    return maze;
}

/**
 * @brief creates a graph node
 * 
 * This is a constructor for a simplified graph data structure
 * 
 * @param {int} cx array column
 * @param {int} cy array row
 * @param {int} sizeX width of graph
 * @param {int} sizeY height of graph
 */
function makeCell(cx, cy, sizeX, sizeY) {
    c = {x: cx, y: cy, sx: sizeX, sy: sizeY, 
        neighbors: new Array(), walls: [1, 1, 1, 1], visited: false,
        entrance: false, exit: false, corrupt: false,
        neighborFunction: getNeighbors};
    c.neighbors = c.neighborFunction();
    return c;
}

/**
 * @brief gets all neighbors for a graph cell in the order of NESW
 * 
 * If a neighbor is invalid, replace index with [-1, -1]
 */
function getNeighbors() {
    var x = this.x;
    var y = this.y;
    var sx = this.sx;
    var sy = this.sy;
    var neighbors = new Array();
    neighbors[0] = (y - 1 >= 0) ? [y-1, x] : [-1, -1];
    neighbors[1] = (x + 1 < sx) ? [y, x+1] : [-1, -1];
    neighbors[2] = (y + 1 < sy) ? [y+1, x] : [-1, -1];
    neighbors[3] = (x - 1 >= 0) ? [y, x-1] : [-1, -1];

    return neighbors;
}

/**
 * @brief updates cell visited and walls parameters
 * 
 * "connects" cell 1 to cell 2, setting wall between the two to 0
 * 
 * @param {object} c1 graph cell
 * @param {object} c2 graph cell
 * @param {int} direction 0-3 variable that dictates direction NESW
 */
function connectCells(c1, c2, direction) {
    c1.visited = true;
    c2.visited = true;
    c1.walls[direction] = 0;
    c2.walls[(direction + 2) % 4] = 0;
}

/**
 * @brief checks if a neighbor's array index is invalid
 * 
 * @param {object} c graph object for neighbor check
 * @param {int} n index for c's neighbor array
 */
function isValidNeighbor(c, n) {
    return c.neighbors[n][0] != -1;
}

/**
 * @brief checks if all neighbors for cell c have a visited bit set
 * 
 * @param {object[][]} mazeArr parent array for graph
 * @param {object} c graph cell object
 */
function allNeighborsVisited(mazeArr, c) {
    for (var i = 0; i < c.neighbors.length; i++) {
        if (isValidNeighbor(c, i)) {
            if (!cellFromNeighbor(mazeArr, c, i).visited) {
                return false;
           }
        }
    }
    return true;
}

/**
 * @brief retrieves a neighbor cell from its parent array, based on the cell c
 * 
 * @param {object[][]} A array of cell objects
 * @param {object} c cell object
 * @param {int} n index into c's neighbor array
 */
function cellFromNeighbor(A, c, n) {
    return A[c.neighbors[n][0]][c.neighbors[n][1]];
}

/**
 * @brief recursive function used to generate a maze, DFS algo
 * 
 * @param {object[][]} mazeArr 2d array that holds graph cells 
 * @param {object} startCell graph cell object
 */
function mazeAlgo(mazeArr, startCell) {
    if (allNeighborsVisited(mazeArr, startCell)) {
        startCell.visited = true;
        return 1;
    } else {
        var visitedCells = new Array();
        randCell = floor(random(0, 4));
        while (!isValidNeighbor(startCell, randCell) || 
                cellFromNeighbor(mazeArr, startCell, randCell).visited) {
            randCell = floor(random(0, 4));
        }

        while (!allNeighborsVisited(mazeArr, startCell) & 
                !arraysEqual(visitedCells, [1,1,1,1])) {
            visitedCells[randCell] = 1;
            connectCells(startCell, cellFromNeighbor(mazeArr, startCell, 
                randCell), randCell);

            mazeAlgo(mazeArr, cellFromNeighbor(mazeArr, startCell, randCell));

            while ((!isValidNeighbor(startCell, randCell) || 
                    cellFromNeighbor(mazeArr, startCell, randCell).visited) &
                    !arraysEqual(visitedCells, [1,1,1,1])) {
                randCell = floor(random(0, 4));
                if (visitedCells[randCell] != 1) visitedCells[randCell] = 1;
            }
        }
    }
}

/**
 * @brief Checks equality of elements in an array, type assumed to be int,
 * length assumed to be equal
 * 
 * @param {int[]} A first int array for comparison
 * @param {int[]} B second int array for comparison
 */
function arraysEqual(A, B) {
    for (var i = 0; i < B.length; i++) {
        if (A[i] != B[i]) return false;
    }
    return true;
}

// TODO: make a functional wall and corridor object

/**
 * @brief room object constructor, rooms assumed to be square
 * 
 * TODO: add check for extra walls active in other rooms
 * 
 * @param {int} x x center of room
 * @param {int} y y center, defaults to 250 for the floor
 * @param {int} z z center
 * @param {int} sx x size of room
 * @param {int} sy y size, defaults to 1 for the floor
 * @param {int} sz z size
 * @param {int[]} walls array of active walls
 * @param {p5.Color} color colors the room, passed onto walls
 */
function makeRoom(x, y=250, z, sx, sy=1, sz, walls, color, entr, exit, corrpt) {
    r = {x: x, y: y, z: z, sx: sx, sy: sy, sz: sz, walls: walls, c: color, 
        wallObjs: new Array(), entrance: entr, exit: exit, corrupt: corrpt,
        drawFunc: drawRoom, update: updateRoom};

    for (var i = 0; i < r.walls.length; i++) {
        if (r.walls[i] == 1) {
            switch(i) {
                case 0:
                    r.wallObjs[i] = makeWall(x, y, z-sx/2, sx, sx, 1, false, 
                        "#FFFFFF");
                    break;
                case 1:
                   r.wallObjs[i] = makeWall(x+sx/2, y, z, sx, sx, 1, true, 
                        "#FFFFFF");
                    break;
                case 2:
                    r.wallObjs[i] = makeWall(x, y, z+sx/2, sx, sx, 1, false, 
                        "#FFFFFF");
                    break;
                case 3:
                    r.wallObjs[i] = makeWall(x-sx/2, y, z, sx, sx, 1, true, 
                        "#FFFFFF");
                    break;
                default:
                    r.walls[i] = -1;
                    break;
            }
        }
    }
    return r;
}

/**
 * @brief draws room, starting from the floor, handles computation of wall 
 * position
 */
function drawRoom() {
    // floor
    push();
    translate(this.x, this.y+this.sx/2, this.z);
    fill(this.c);
    box(this.sx, this.sy, this.sz);
    pop();

    // ceiling
    push();
    translate(this.x, this.y-this.sx/2, this.z);
    fill(this.c);
    box(this.sx, this.sy, this.sz);
    pop();

    if (this.corrupt) {
        drawRotatingDiamond([this.x, this.y, this.z], 200, this.c);
    }

    for (var i = 0; i < this.walls.length; i++) {
        if (this.walls[i] == 1) {
            this.wallObjs[i].drawFunc();
        }
    }
}

function updateRoom() {
    for (var i = 0; i < this.walls.length; i++) {
        if (this.walls[i] == 1) {
            p.mvmtVec = handleCollision(getMovementVector(p), 
                p.panTilt[0], p.samples, this.wallObjs[i].bounds);
        }
    }
}

/**
 * @brief constructor for a wall environment object
 * 
 * depth is set to 1 by default to allow light collisions with the object
 * 
 * @param {int} x x position
 * @param {int} y y position
 * @param {int} z z position
 * @param {int} sx x size (width)
 * @param {int} sy y size (height)
 * @param {boolean} rotated dictates whether the wall is rotated 90 degress
 * @param {p5.Color} color p5.js color variable
 */
function makeWall(x, y, z, sx, sy, sz=1, rotated, color) {
    w = {x: x, y: y, z: z, sx: sx, sy: sy, sz: sz, rotated: rotated, c: color,
        bounds: new Array(), drawFunc: drawWall};

    if (!rotated) w.bounds = [[x - sx/2, z - sz], [x + sx/2, z + sz]];
    else w.bounds = [[x - sz, z - sx/2], [x + sz, z + sx/2]];

    return w;
}

/**
 * @brief wall object method to draw wall on canvas.
 */
function drawWall() {
    push();
    translate(this.x, this.y, this.z);
    if (this.rotated) rotateY(radians(90));
    fill(this.c);
    box(this.sx, this.sy, this.sz);
    pop();
}

/**
 * @brief currently creates a room object based on a graph cell
 * 
 * @param {object} c graph cell describing room information
 * @param {int} s size of room
 * @param {p5.Color} color self explanatory
 */
function roomFromCell(c, s, color) {
    r = makeRoom(c.x * s, 0, c.y * s, s, 1, s, c.walls, color,
        c.entrance, c.exit, c.corrupt);
    return r;
}

function mapUpdate(mapArr) {
    for (var i = 0; i < mapArr.length; i++) {
        for (var j = 0; j < mapArr[i].length; j++) {
            if (arraysEqual(p.graphCell, [i, j])) mapArr[i][j].update();
        }
    }
}

/**
 * @brief renders the map while trying to avoid frame drops
 * 
 * @param {object[][]} mapArr array of room objects to be rendered
 */
function renderMap(mapArr) {
    for (var i = 0; i < mapArr.length; i++) {
        for (var j = 0; j < mapArr[i].length; j++) {
            if (dist(p.xyz[0], p.xyz[1], p.xyz[2], mapArr[i][j].x, 
                    mapArr[i][j].y, mapArr[i][j].z) <= renderDistance){
                if (mapArr[i][j].entrance) {
                    mapArr[i][j].c = "#00FF00";
                } else if (mapArr[i][j].exit) {
                    mapArr[i][j].c = "#0000FF";
                } else if (mapArr[i][j].corrupt) {
                    mapArr[i][j].c = "#FF00FF";
                } else if (arraysEqual(p.graphCell, [i, j])) {
                    mapArr[i][j].c = "#FF0000";
                } else {
                    mapArr[i][j].c = "#808080";
                }
                mapArr[i][j].drawFunc();
            }
        }
    }
}

/**
 * SECTION FOR CAMERA CONTROLS
 */

/**
 * @brief enables 3D lighting with a point light at the camera's position 
 * 
 * @param {int} cx camera x position
 * @param {int} cy camera y position
 * @param {int} cz camera z position
 */
function enableLight(cx, cy, cz) {
    var local = p.cam._getLocalAxes();
    var z = local.z;

    lightFalloff(1, 0, 0);
    spotLight(255, 255, 255, cx, cy, cz, 
        -z[0], 
        -z[1], 
        -z[2], 
        Math.PI, 100);
    spotLight(255, 255, 255, cx, cy, cz, 
        -z[0], 
        -z[1], 
        -z[2], 
        Math.PI / 5, 100);
    spotLight(255, 255, 255, cx, cy, cz, 
        -z[0], 
        -z[1], 
        -z[2], 
        Math.PI / 10, 100);
    spotLight(255, 255, 255, cx, cy, cz, 
        -z[0], 
        -z[1], 
        -z[2], 
        Math.PI / 10, 100);
    spotLight(255, 255, 255, cx, cy, cz, 
            -z[0], 
            -z[1], 
            -z[2], 
            Math.PI / 10, 100);
    lightFalloff(4, 0.01, 0);
    pointLight(255, 255, 255, cx, cy, cz);

    ambientMaterial(255, 255, 255);
}

/**
 * @brief toggles between fullscreen and windowed, changing the resolution
 */
function enterFullscreen() {
    fullscreen(!fs);
    if (!fs) {
        // reduced resolution to avoid scroll wheels
        resizeCanvas(1915, 1075);
    } else {
        resizeCanvas(600, 600);
    }
    fs = !fs;
}

/**
 * @brief used to call enterFullscreen()
 */
function keyPressed() {
    if (key == "f") enterFullscreen();
    if (key == "g") walking = !walking;
    if (key == "e") lightsOn = !lightsOn;
}

/**
 * @brief used to end sprinting
 */
function keyReleased() {
    if (keyCode == SHIFT) {
        p.dq = 15;
    }
    return false;
}

function mousePressed() {
    if (!locked) {
        requestPointerLock();
        locked = !locked;
    } else {
        exitPointerLock();
        locked = !locked;
    }
}

/**
 * @brief used to calculate dot product between two vectors
 * 
 * @param {int[]} A vector
 * @param {int[]} B vector
 */
function dot(A, B){
    if (A.length == B.length) {
        var r = 0;
        for (var i = 0; i < A.length; i++) {
            r += (A[i] * B[i]);
        }
        return r;
    }

    return -1;
}

/**
 * @brief calculates the cross product between two vectors
 * 
 * @param {int[]} A vector
 * @param {int[]} B vector
 */
function cross(A, B) {
    if (A.length == B.length) {
        var r = new Array();

        r[0] = A[1] * B[2] - A[2] * B[1];
        r[1] = A[2] * B[0] - A[0] * B[2];
        r[2] = A[0] * B[1] - A[1] * B[0];

        return r;
    }

    return -1;
}

/**
 * @brief calculates norm (length) of a vector
 * 
 * @param {int[]} A vector
 */
function normalize(A) {
    var r = 0;
    for (var i = 0; i < A.length; i++) {
        r += Math.pow(A[i], 2);
    }
    return Math.sqrt(r);
}

/**
 * PLAYER OBJECT SECTION 
 */

/**
 * @brief makes a player object with several useful parameters
 * 
 * @param {p5.Camera} cam object used for looking 
 * @param {int} dq movement rate (dq is notation used in robotics)
 * @param {int} lookRate surprisingly unused, i don't think i need this
 */
function makePlayer(cam, dq, lookRate) {
    p = {cam: cam, dq: dq, lr: lookRate, xyz: new Array(), panTilt: new Array(),
        camX: 0, camY: 0, camZ: 0, graphCell: new Array(), collision: true,
        keys: new Array(), movementPath: new Array(), mvmtVec: [0, 0, 0],
        samples: 100, width: dq, depth: dq, inventory: new Array(), won: false,
        posUpdate: getPlayerPos, angleUpdate: getPlayerPanTilt, 
        cellUpdate: playerGetCell, setCell: setPosFromCell,
        move: playerMove, look: playerLook, update: updatePlayer};
    return p;
}

/**
 * @brief gets 3d position of player and returns an array of these points
 */
function getPlayerPos() {
    return [this.cam.eyeX, this.cam.eyeY, this.cam.eyeZ];
}

/**
 * @brief returns pan and tilt angle of camera for use in other calculations
 */
function getPlayerPanTilt() {
    var local = this.cam._getLocalAxes();
    var x = local.x;
    var z = local.z;
    let xUnit = [1, 0, 0];
    let yUnit = [0, 1, 0];
    let rotatedUnit = cross(x, [0, -1, 0]);

    let xCross = cross(x, xUnit);
    let zCross = cross([-z[0], -z[1], -z[2]], rotatedUnit);
    
    panAngle = acos(dot(x, xUnit));
    if (dot(yUnit, xCross) < 0) panAngle = -panAngle;

    tiltAngle = acos(dot([-z[0], -z[1], -z[2]], rotatedUnit));
    if (dot([-x[0], -x[1], -x[2]], zCross) < 0) tiltAngle = -tiltAngle;

    return [panAngle, tiltAngle];
}

 /**
 * @brief handles player movement
 * 
 * Implemented in this way to allow for use of multiple keys at a time. This is 
 * called in draw() to update camera position on every redraw.
 */
function playerMove() {
    if (keyIsDown(87)) { // w
        this.keys[0] = 1;
    } else this.keys[0] = 0;
    if (keyIsDown(65)) { // a
        this.keys[3] = 1;
    } else this.keys[3] = 0;
    if (keyIsDown(83)) { // s
        this.keys[2] = 1;
    } else this.keys[2] = 0;
    if (keyIsDown(68)) { // d
        this.keys[1] = 1;
    } else this.keys[1] = 0;
    if (keyIsDown(16)) this.dq = constrain(this.dq+0.5, 0, 30); // shift

    this.cam.move(this.mvmtVec[0], this.mvmtVec[1], this.mvmtVec[2]);
}

/**
 * @brief rotates camera based on mouse movement
 * 
 * Tilts and pans the camera, uses camera axes to calculate look angles for the
 * purpose of bounding tilts.
 */
function playerLook() {
    panAngle = this.panTilt[0];
    tiltAngle = this.panTilt[1];

    vertBound = 80;

    this.cam.pan(radians(-movedX/this.lr));
    if (tiltAngle < radians(vertBound) & tiltAngle > radians(-vertBound)) {
        this.cam.tilt(radians(movedY/this.lr));
    } else if (tiltAngle > radians(vertBound) & movedY < 0) {
        this.cam.tilt(radians(movedY/this.lr));
    } else if (tiltAngle < radians(-vertBound) & movedY > 0) {
        this.cam.tilt(radians(movedY/this.lr));
    }
}

/**
 * @brief converts player position to a cell in the graph array
 */
function playerGetCell() {
    xyz = this.posUpdate();
    x = xyz[0];
    z = xyz[2];

    x += roomSize / 2;
    z += roomSize / 4;

    return [floor(z / roomSize), floor(x / roomSize)];
}

/**
 * @brief sets the player position to a specific graph cell
 * 
 * @param {int[]} coords graph cell (x, y)
 */
function setPosFromCell(coords) {
    this.cam.setPosition(coords[1] * roomSize, this.camY, coords[0] * roomSize);
}

/**
 * @brief updates all player parameters
 */
function updatePlayer() {
    this.xyz = this.posUpdate();
    this.panTilt = this.angleUpdate();

    this.camX = this.xyz[0];
    this.camY = walking ? 0 : this.xyz[1];
    this.camZ = this.xyz[2];

    this.cam.setPosition(this.camX, this.camY, this.camZ);

    this.mvmtVec = getMovementVector(this);
    this.graphCell = this.cellUpdate();
    this.move();
    this.look();
}

// CANCELLED: collision detection

/**
 * IDEA: get four walls from around player, possibly adjacent rooms
 * check if player movement vector collides with or passes through a wall plane
 * if it does, truncate the vector at the wall's edge
 * 
 * -----|---> turns into --->|
 */

 /**
  * @brief gets player's movement vector in xyz
  * 
  * @param {object} player 
  */
function getMovementVector(player) {
    var vector = [0, 0, 0];
    if (player.collision) {
        for (var i = 0; i < player.keys.length; i++) {
            switch(i) {
                case 0:
                    if (player.keys[i] == 1) vector[2] -= player.dq;
                    break;
                case 1:
                    if (player.keys[i] == 1) vector[0] += player.dq;
                    break;
                case 2:
                    if (player.keys[i] == 1) vector[2] += player.dq;
                    break;
                case 3:
                    if (player.keys[i] == 1) vector[0] -= player.dq;
                    break;
            }
        }
        return vector;
    }
    // implies collision is off because i'm not going to account for y changes
    // in noclip mode where collision doesn't matter
    // though i might in the future because it's not hard to add
    // the issue is that there's a *small* chance it'll slow the game down
    return []; 
}

/**
 * @brief converts a movement vector plus player position to world coordinates
 * 
 * Based on math given x and z axes. Movement vector forward composed of 
 * dz*cos(a) - dx*sin(a) and side vector composed to dz*sin(z) + dx*cos(a).
 * No need for y movement because this game has no stairs or ramps.
 * 
 * @param {int[]} movementVec assumed to include x, y, and z, uses only x and z
 * @param {int} angle assumed to be panAngle
 */
function vectorToWorldCoords(playerPos, movementVec, angle, samples) {
    let pointsOverTime = new Array();
    var x = p.cam.eyeX;
    var z = p.cam.eyeZ;
    for (var i = 0; i <= samples; i++) {
        var dz = (i / samples) * (movementVec[2]);
        var dx = (i / samples) * (movementVec[0]);
        pointsOverTime[i] = [z + (dz * cos(radians(angle))) + (dx * 
            sin(radians(angle))), x + (dx * cos(radians(angle))) - (dz * 
            sin(radians(angle)))];
    }

    return pointsOverTime;
}

/**
 * @brief locates point where player intersects wall, if they do
 * 
 * @param {int[]} playerPoints 
 * @param {int[][]} boundingBox 
 */
function findIntersect(playerPoints, boundingBox) {
    for (var i = 0; i < playerPoints.length; i++) {
        if (playerPoints[i][1] >= boundingBox[0][0] - 100 &
            playerPoints[i][1] <= boundingBox[1][0] + 100 &&
            playerPoints[i][0] >= boundingBox[0][1] - 100 &&
            playerPoints[i][0] <= boundingBox[1][1] + 100) {
                return playerPoints[i];
        }
    }
    return -1;
}

/**
 * @brief partially working collision function
 * 
 * @param {object} player 
 * @param {object} wall 
 */
function checkCollisionWithWall(player, wall) {
    let mvmtVec = getMovementVector(player);
    let points = vectorToWorldCoords(player.xyz, mvmtVec, 
        player.panTilt[0], player.samples);
    var boundaryPoint = findIntersect(points, wall.bounds);
    if (boundaryPoint != -1) {
        player.cam.setPosition(boundaryPoint[1], player.xyz[1], 
            boundaryPoint[0]);
    }
}

/**
 * @brief is supposed to handle collisions by limiting x or z movement
 * 
 * @param {int[]} movementVec 
 * @param {int} angle 
 * @param {int} samples 
 * @param {int[][]} boundingBox 
 */
function handleCollision(movementVec, angle, samples, boundingBox) {
    let vel = new Array();
    var x = p.cam.eyeX;
    var z = p.cam.eyeZ;
    for (var i = 0; i <= samples; i++) {
        var dz = (i / samples) * (movementVec[2]);
        var dx = (i / samples) * (movementVec[0]);

        xOnly = [z, x + (dx * cos(radians(angle))) - (dz * 
            sin(radians(angle)))];
        yOnly = [z + (dz * cos(radians(angle))) + (dx * 
            sin(radians(angle))), x];


        var xCh = checkCollision(xOnly, boundingBox);
        if (!xCh) {
            vel[0] = dx;
        }

        var yCh = checkCollision(yOnly, boundingBox);
        if (!yCh) {
            vel[2] = dz;
        }

        vel[1] = 0;
    }

    return vel;
}

/**
 * @brief checks if point is within a bounding box
 * 
 * @param {int[]} playerPoints 
 * @param {int[][]} boundingBox 
 */
function checkCollision(playerPoints, boundingBox) {
    if (playerPoints[1] >= boundingBox[0][0] - 100 &
        playerPoints[1] <= boundingBox[1][0] + 100 &&
        playerPoints[0] >= boundingBox[0][1] - 100 &&
        playerPoints[0] <= boundingBox[1][1] + 100) {
            return true;
    }
    return false;
}

/**
 * @brief draws a rotating diamond to represent items
 * 
 * @param {int[]} pos 
 * @param {int} size 
 * @param {p5.Color or p5.Image} texture 
 */
function drawRotatingDiamond(pos, size, tex) {
    push();
    if (typeof tex == "string") {
        fill(tex);
    } else {
        texture(tex);
    }
    translate(pos[0], pos[1] + (100 * sin(radians(ctr))), pos[2]);
    rotateY(radians(ctr));
    cone(size, size, 5);
    translate(0, -size, 0);
    cone(size, -size, 5);
    pop();
}
// TODO: make cube disappear if player shows up, add to inventory
// TODO: make win state

LO-08

Ariel Waldman is a graphic designer who has focused her efforts on space exploration and space in general. She has created Science Hack Day, as well as her own website detailing how people can get involved with space exploration without needing to go into a STEM field directly related to space. Personally, I admire her work on Science Hack Day, as it’s a way for those not in STEM fields to get involved with things that they are interested in without the need for specializing in the related fields. Ariel uses humor to her benefit during presentations. She will make jokes to keep audience engagement high while staying relevant to her topic. Humor is a common strategy to increase engagement, but Ariel’s use of humor is skillful in her incorporation of it into her presentation.

Ariel’s 2018 lecture on space exploration.

Project 07 – Curves

The inspiration behind this project was Viviani’s Curve, which is normally contained in 3D space, but can be represented from its front view on the 2D plane as a parabolic curve. I also animated a few circles moving along the curve to give it a bit more life. You can make some pretty interesting shapes, one of which is pictured below.

AAAAAAAAAAAAAAAAAAAAAAAA

function setup() {
    createCanvas(600, 600);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);

    // want to animate circles along curves
    circlePos = 0; // this follows the angle and is a global
}

function draw() {
    background(0);
    var angleIncr = 0.01; // hopefully leads to high definition curve
    var angle = 0;
    var dx, dy;
    var hw = width / 2; // "half width"
    var hh = height / 2;


    beginShape();
    fill(128, 128, 128, 128); // using RGBA
    stroke(128, 128, 128, 128);
    strokeWeight(1);
    // more understandable than using a for loop
    while (angle < 2*PI) {
        // aiming to use Viviani's Curve
        dx = sin(angle);
        dy = 2 * sin(0.5 * angle);

        // want to scale values based on mousePos
        dx *= mouseX - hw;
        dy *= mouseY - hh;

        vertex(dx + hw, dy + hh); // beginShape() doesn't work with translate()
        vertex(-dx + hw, -dy + hh);
        vertex(dy + hw, dx + hh);
        vertex(-dy + hw, -dx + hh);

        // increment angle
        angle += angleIncr;
    }
    endShape();

    angle = 0;
    // second while outside of beginShape()
    while (angle < 2*PI) {
        // aiming to use Viviani's Curve
        dx = sin(angle);
        dy = 2 * sin(0.5 * angle);

        // want to scale values based on mousePos
        dx *= mouseX - hw;
        dy *= mouseY - hh;

        // draw circle
        if (circlePos == angle) {
            fill(mouseX % 255, mouseY % 255, (mouseX + mouseY) % 255);
            circle(dx + hw, dy + hh, 15);
            circle(-dx + hw, -dy + hh, 15);
            circle(dy + hw, dx + hh, 15);
            circle(-dy + hw, -dx + hh, 15);
        }

        angle += angleIncr;
    }

    circlePos = circlePos + angleIncr;
    if (circlePos > 2*PI) circlePos = 0; // can't modulo properly with PI
}

An image captured when the mouse was at the left side of the screen.

LO7 – Information Visualization

The project that caught my eye this week is Chris Harrison’s WordSpectrum. Its purpose is to visualize bigram data to represent word associations. For example, it takes the words “war” and “peace,” and then places words that appear most frequently as bigrams with these from left to right depending on which word they appear next to more. Due to this, you get “war” and “ii” on the same side of the graphic, then you have “peace” and “officers” on the other side. In the center of the graphic you have words that are used generally equally between the two, and because these words are related to each other, most of the time these words will be conjunctions. For example, the word “and” is near the center for most graphics because people have a tendency to write phrases such as “war and peace,” or “good and evil,” leading to high word associations between the two. I find this project particularly interesting because it shows to at least some degree what people associate with certain words, giving a glimpse into the minds.

A screenshot of the graphic for war and peace. Please note that the right side says peace, and the PDF was misbehaving.

Project 06: Abstract Clock

The idea here was to use a binary representation for a clock. Green is 1, red is 0. The clock hours with the back square as the MSB, while minutes have the back square as the LSB. If the back left square is green, that means it’s 8 o’clock, and if the back right square is green, that means it’s 8:01. The back building, tells whether it is afternoon or not. This is also the return of 3D space in my projects. Hooray.

yeeeeeeeeeeeeet

function setup() {
    createCanvas(600, 600);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
}

/* 
  Idea: Create a small town that tells the time based on what's "open."
  Doors will represent a binary pattern on each side of town.
  A building in the back tells whether it is am or pm.
*/

function draw() {
    background(0);

    fill(255);
    stroke("#FF00FF");

    // fill the floor and make a road
    drawCube(-10000, 200, 1, 20000, 0, 20000);
    fill(128);
    drawCube(-200, 200, 1, 400, 0, 20000);

    // draw am pm  tower
    fill("#800080");
    drawCube(-200, -400, 2400, 400, 600, 400);
    fill(hour() < 12 ? "#800000" : "#008000");
    drawCube(-100, -300, 2400, 200, 200, 0);

    // get time and store it
    let h = hour() % 12;
    let m = minute();

    var hStr = h.toString(2);
    var mStr = m.toString(2);

    // draw cubes and doors
    for (var i = 0; i < 4; i++) {
        fill("#800080");
        drawCube(200, -400, 1800 - (i * 500), 800, 600, 400);
        drawCube(-200, -400, 1800 - (i * 500), -800, 600, 400);
        
        // hour squares
        fill(hStr[i] == 1 ? "#008000" : "#800000");
        drawCube(-200, (i * -100), 1900 - (i * 500), 100, 100, 0);

        // minute squares
        fill(mStr[i] == 1 ? "#008000" : "#800000");
        drawCube(200, (i * -100), 1900 - (i * 500), -100, 100, 0);
        if (i > 1) {
            fill(mStr[i+2] == 1 ? "#008000" : "#800000");
            drawCube(200, 200 + (i * -100), 1900 - (i * 500), -100, 100, 0);
        }
    }

    
}

// the return of the 3d projection matrix

// uses projection matrix seen in this video:
// https://www.youtube.com/watch?v=ih20l3pJoeU
// the video is mostly math about projecting 3d coordinates to 2d coordinates
function projMatrixMult(coords) {
    // aspect ratio
    var a = width / height;

    // field of view
    var fov = HALF_PI;
    f = 1 / tan(fov / 2);

    // range of view
    var zNear = 0.1;
    var zFar = 1000;

    var q = zFar / (zFar - zNear);

    if (coords.length != 3) {
        print("Improper array size.");
        return coords;
    } else {
        // this calculates the result of multiplying [x, y, z, 1]
        // with a 4x4 projection matrix (not shown for lack of use without
        // the math.js extension)
        let projMat = [a * f * coords[0], f * coords[1], 
                       coords[2] * q - zNear * q, coords[2]];
        
        if (coords[2] != 0) {
            projMat[0] /= projMat[3];
            projMat[1] /= projMat[3];
            projMat[2] /= projMat[3];
            projMat[3] /= projMat[3];
            return projMat;
        } else {
            print("z is equal to 0");
            return coords;
        }
    }
}

// self explanatory
// note to self: add rotation parameters
// note to self: generalize for any number of points greater than 4 (later)
function drawCube(x, y, z, wid, hei, d) {
    // push 3d coords to an array
    let cubePoints3D = [];
    cubePoints3D.push([x, y + hei, z]); // front bottom left            0
    cubePoints3D.push([x, y, z]); // front top left                     1
    cubePoints3D.push([x + wid, y + hei, z]); // front bottom right     2
    cubePoints3D.push([x + wid, y, z]); // front top right              3
    cubePoints3D.push([x, y + hei, z + d]); // back bottom left         4
    cubePoints3D.push([x, y, z + d]); // back top left                  5
    cubePoints3D.push([x + wid, y + hei, z + d]); // back bottom right  6
    cubePoints3D.push([x + wid, y, z + d]); // back top right           7

    // get projection and add to list of points
    let cubeProj = [];
    for (let i = 0; i < cubePoints3D.length; i++) {
        cubeProj.push(projMatrixMult(cubePoints3D[i]));
    }

    // this scales the image to be on screen
    for (let i = 0; i < cubeProj.length; i++) {
        cubeProj[i][0] += 1;
        cubeProj[i][1] += 1;

        cubeProj[i][0] *= width / 2;
        cubeProj[i][1] *= height / 2;
    }

    // need 6 quads for the 6 faces of the cube, draw the further quads first
    // realistically, without camera movement or rotation, i can draw only
    // 5 quads and still have it look find regardless of placement
    // if i add rotation, i need to add conditionals for what is drawn first
    
    // back face
    quad(cubeProj[7][0], cubeProj[7][1], cubeProj[6][0], cubeProj[6][1],
        cubeProj[4][0], cubeProj[4][1], cubeProj[5][0], cubeProj[5][1]);
    
    // bottom
    quad(cubeProj[6][0], cubeProj[6][1], cubeProj[4][0], cubeProj[4][1],
        cubeProj[0][0], cubeProj[0][1], cubeProj[2][0], cubeProj[2][1]);

    // top
    quad(cubeProj[7][0], cubeProj[7][1], cubeProj[5][0], cubeProj[5][1],
        cubeProj[1][0], cubeProj[1][1], cubeProj[3][0], cubeProj[3][1]);

    if (x > 0) {
        // left
        quad(cubeProj[5][0], cubeProj[5][1], cubeProj[4][0], cubeProj[4][1],
            cubeProj[0][0], cubeProj[0][1], cubeProj[1][0], cubeProj[1][1]);

        // right
        quad(cubeProj[7][0], cubeProj[7][1], cubeProj[6][0], cubeProj[6][1],
            cubeProj[2][0], cubeProj[2][1], cubeProj[3][0], cubeProj[3][1]);
    } else {
        // right
        quad(cubeProj[7][0], cubeProj[7][1], cubeProj[6][0], cubeProj[6][1],
            cubeProj[2][0], cubeProj[2][1], cubeProj[3][0], cubeProj[3][1]);

        // left
        quad(cubeProj[5][0], cubeProj[5][1], cubeProj[4][0], cubeProj[4][1],
            cubeProj[0][0], cubeProj[0][1], cubeProj[1][0], cubeProj[1][1]);
    }
    
    // front
    quad(cubeProj[3][0], cubeProj[3][1], cubeProj[2][0], cubeProj[2][1],
        cubeProj[0][0], cubeProj[0][1], cubeProj[1][0], cubeProj[1][1]);

    // the lines are no longer necessary
}

LO6: Randomness

Although artificial intelligence is not based entirely on randomness, from my understanding there is still an aspect of randomness that will aid the algorithm in its learning process. This song, “Blue Jeans and Bloody Tears,” is a song that has been generated after analyzing hundreds of Eurovision songs. I know that the randomness of this algorithm is not the main focus, but I believe that it is what led to the creation of this specific song. Most evolutionary algorithms randomly generate their first generation and if this is the case for Sweaty Machines’ AI, then I think that the song was formed in this direction due to the output of the AI. There must have been thousands of other ways this song could have been generated, but it just so happened to form in this manner. I find that complete randomness, while at times interesting, is much better when incorporated into the creation of less abstract works. Using randomness to generate something that anybody can parse and enjoy is truly impressive. For that reason, I appreciate this AI generated Eurovision song.

Feel free to take a listen to the song in question.

Project 05: Wallpaper

My first idea when trying to create a wallpaper was using Penrose tiling, but I couldn’t find a good algorithm without directly copying code, which wouldn’t make for a very good learning exercise. My second idea, using a theme of symmetry, came in the form of recursion. I decided to loosely base a recursive wallpaper off of a Koch snowflake. I think that the resolution is a little too low for it to be properly viewed, but it is still interesting. I’d like to imagine this pattern looped across my wall, each recursive segment being repeated.

sketch

function setup() {
    createCanvas(600, 600);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
}

function draw() {
    background(0);
    // just the rainbow
    let colors = ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', 
                    '#0000FF', '#4B0082', '#9400D3'];

    // move center for ease of programming
    translate(width / 2, height / 2);
    recursiveSnowflake(360, colors);

    noLoop();
}

// use default parameters for recursive depth
function recursiveSnowflake(initSize, colors, depth=0) {
    if (depth === 7) { // base case
        // do nothing
    } else { // recursive case
        fill(colors[depth]);
        drawTriCenter([0, 0], initSize, 0);
        drawTriCenter([0, 0], initSize, 180);
        
        var dAngle = 60;
        for (var i = 0; i < 6; i++) {
            push();
            rotate(radians((dAngle * i)));
            translate(0, Math.sqrt(3) * initSize / 2);
            recursiveSnowflake(initSize / 3, colors, depth+1);
            pop();
        }
    }
}

// draws an equilateral triangle, point facing up
function drawTriCenter(c, sideLength, rot) {
    // math to get tri points from center
    var distance = sideLength / Math.sqrt(3);
    let p1 = [c[0] - distance * cos(radians(90 + rot)), 
                c[1] - distance * sin(radians(90 + rot))];
    let p2 = [c[0] - distance * cos(radians(210 + rot)), 
                c[1] - distance * sin(radians(210 + rot))];
    let p3 = [c[0] - distance * cos(radians(330 + rot)), 
                c[1] - distance * sin(radians(330 + rot))];

    strokeWeight(0);
    
    triangle(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]); 
}

LO 5: 3D Computer Graphics

I’ve been interested in procedural generation for quite a while, mostly due to my interest in creating a 3D roguelike game, and as a result of this I have chosen a demonstration of a procedural generation model in 3D. The generation algorithm in this case is cellular automata, an algorithm that generates forms and structures based on rules applied to each neighboring “cell.” The use of cellular automata in the video below shows a great understanding of the algorithm, giving the various forms being shown. The 2017 video generates forms using blocks, most forms being non-descript or crystalline in structure. My personal favorite forms shown in the video are Clouds 1 and 2 because of their cave-like structures. Despite the interesting nature of the algorithms and forms generated, the creator, Softology, most likely intended this to be a demonstration for their software, Visions of Chaos.

Visualization of cellular automata algorithm.

Project 04 – String Art

For this project, I wanted to create something in 3D space. I was mainly inspired by the MS-DOS screensavers and the Tron aesthetic that’s popular with vaporwave artists.
There are some actions the user can perform in this. Use the WASD keys to control the size and x position of the left ring, and use the arrow keys to control the right ring.
Note: The wireframe scrolling background acts a little weird when the rings change size.

sketch

// predefine canvas width and height for use in other functions
var w = 600;
var h = 600;

// define variables
var angleCtr = 0;
var circleRX = 200;
var circleLX = -200;
var radRX = 200;
var radLX = 200;
var degOfSymmetry = 36;
var rainbowCtr = 0;
var freq = 0.1;
var zCtr = 0;

function setup() {
    createCanvas(w, h);
    background(220);
    text("p5.js vers 0.9.0 test.", 10, 15);
}

function draw() {
    background(0);
    // get rainbow function
    var rr = sin(rainbowCtr) * 127 + 128;
    var rg = sin(rainbowCtr + (2*PI/3)) * 127 + 128;
    var rb = sin(rainbowCtr + (4*PI/3)) * 127 + 128;
    c = color(rr, rg, rb);
    strokeWeight(2);

    // 3d projection effectively doubles canvas size, 0, 0, z is now the center
    // of the canvas

    // make ring 1 in xyz coords
    var centerY = 0; // canvas centers now different
    var centerZ = 1000; // z == 0 is in the camera's "face"
    var distX = circleRX - circleLX;
    let ringOne = [];

    for (i = 0; i < degOfSymmetry; i++) {
        var loopAngle = radians(angleCtr) - (i * 2*PI / degOfSymmetry)
        var tZ = centerZ + radLX * cos(loopAngle);
        var tY = centerY + radLX * sin(loopAngle);
        ringOne.push([circleLX, tY, tZ]);
    }

    // make ring 2 in xyz coords
    let ringTwo = [];

    for (i = 0; i < degOfSymmetry; i++) {
        var loopAngle = radians(-angleCtr) + (i * 2*PI / degOfSymmetry)
        var tZ = centerZ + radRX * cos(loopAngle);
        var tY = centerY + radRX * sin(loopAngle);
        ringTwo.push([circleRX, tY, tZ]);
    }

    // project to xy
    let rOProj = [];
    let rTProj = [];

    for (let i = 0; i < ringOne.length; i++) {
        rOProj.push(projMatrixMult(ringOne[i]));
    }
    for (let i = 0; i < ringTwo.length; i++) {
        rTProj.push(projMatrixMult(ringTwo[i]));
    }

    // this scales the image to be on screen
    for (let i = 0; i < rOProj.length; i++) {
        rOProj[i][0] += 1;
        rOProj[i][1] += 1;

        rOProj[i][0] *= w / 2;
        rOProj[i][1] *= h / 2;
    }
    for (let i = 0; i < rTProj.length; i++) {
        rTProj[i][0] += 1;
        rTProj[i][1] += 1;

        rTProj[i][0] *= w / 2;
        rTProj[i][1] *= h / 2;
    }

    // draw squares for perspective reference
    dLX = radLX * 2;
    dRX = radRX * 2;
    for (let i = 0; i < 50; i++) {
        stroke("purple");
        drawTestCube(circleLX, -radLX, (i * dLX) - zCtr, 0, dLX, dLX);
        drawTestCube(circleRX, -radRX, (i * dRX) - zCtr, 0, dRX, dRX);
        drawTestCube(circleLX, -radLX - dLX, (i * dLX) - zCtr, 0, dLX, dLX);
        drawTestCube(circleRX, -radRX - dRX, (i * dRX) - zCtr, 0, dRX, dRX);
        drawTestCube(circleLX, radLX, (i * dLX) - zCtr, 0, dRX, dLX);
        drawTestCube(circleRX, radRX, (i * dRX) - zCtr, 0, dRX, dRX);
        drawTestCube(circleLX, -radLX - dLX, (i * dLX) - zCtr, distX, 0, dLX);
        drawTestCube(circleLX, radLX + dLX, (i * dRX) - zCtr, distX, 0, dRX);
    }

    // draw line between top-bottom, left-right pairs
    for (let i = 0; i < (rOProj.length + rTProj.length) / 2; i++) {
        fill(c);
        stroke(c);
        circle(rOProj[i][0], rOProj[i][1], 5);
        circle(rTProj[i][0], rTProj[i][1], 5);
        line(rOProj[i][0], rOProj[i][1], rTProj[i][0], rTProj[i][1]);
    } 

    // allow user to control ring shape and size
    if (keyIsPressed) {
        // left ring
        if (key == "w") {
            radLX++;
        }
        if (key == "s") {
            radLX--;
        }
        if (key == "a") {
            circleLX--;
        }
        if (key == "d") {
            circleLX++;
        }
        
        // right ring
        if (keyCode == UP_ARROW) {
            radRX++;
        }
        if (keyCode == DOWN_ARROW) {
            radRX--;
        }
        if (keyCode == LEFT_ARROW) {
            circleRX--;
        }
        if (keyCode == RIGHT_ARROW) {
            circleRX++;
        }
    }
    
    // increment any counters
    angleCtr = (angleCtr + 1) % 360;
    rainbowCtr = (rainbowCtr + freq) % (2*PI); 
    zCtr = (zCtr + 5) % max(dLX, dRX);
}

// disable normal browser key functions when focused
function keyPressed() {
    return false;
}

// uses projection matrix seen in this video:
// https://www.youtube.com/watch?v=ih20l3pJoeU
// the video is mostly math about projecting 3d coordinates to 2d coordinates
function projMatrixMult(coords) {
    // aspect ratio
    var a = w / h;

    // field of view
    var fov = QUARTER_PI;
    f = 1 / tan(fov / 2);

    // range of view
    var zNear = 0.1;
    var zFar = 1000;

    var q = zFar / (zFar - zNear);

    if (coords.length != 3) {
        print("Improper array size.");
        return coords;
    } else {
        // this calculates the result of multiplying [x, y, z, 1]
        // with a 4x4 projection matrix (not shown for lack of use without
        // the math.js extension)
        let projMat = [a * f * coords[0], f * coords[1], 
                       coords[2] * q - zNear * q, coords[2]];
        
        if (coords[2] != 0) {
            projMat[0] /= projMat[3];
            projMat[1] /= projMat[3];
            projMat[2] /= projMat[3];
            projMat[3] /= projMat[3];
            return projMat;
        } else {
            print("z is equal to 0");
            return coords;
        }
    }
}

// self explanatory
function drawTestCube(x, y, z, wid, hei, d) {
    // push 3d coords to an array
    let cubePoints3D = [];
    cubePoints3D.push([x, y + hei, z]); // front bottom left
    cubePoints3D.push([x, y, z]); // front top left
    cubePoints3D.push([x + wid, y + hei, z]); // front bottom right
    cubePoints3D.push([x + wid, y, z]); // front top right
    cubePoints3D.push([x, y + hei, z + d]); // back bottom left
    cubePoints3D.push([x, y, z + d]); // back top left
    cubePoints3D.push([x + wid, y + hei, z + d]); // back bottom right
    cubePoints3D.push([x + wid, y, z + d]); // back top right

    // get projection and add to list of points
    let cubeProj = [];
    for (let i = 0; i < cubePoints3D.length; i++) {
        cubeProj.push(projMatrixMult(cubePoints3D[i]));
    }

    // this scales the image to be on screen
    for (let i = 0; i < cubeProj.length; i++) {
        cubeProj[i][0] += 1;
        cubeProj[i][1] += 1;

        cubeProj[i][0] *= w / 2;
        cubeProj[i][1] *= h / 2;
    }

    // i'm almost certain there's a way this can be done with a for loop
    // but this is fine for a small project
    line(cubeProj[0][0], cubeProj[0][1], cubeProj[1][0], cubeProj[1][1]);
    line(cubeProj[6][0], cubeProj[6][1], cubeProj[7][0], cubeProj[7][1]);
    line(cubeProj[0][0], cubeProj[0][1], cubeProj[2][0], cubeProj[2][1]);
    line(cubeProj[0][0], cubeProj[0][1], cubeProj[4][0], cubeProj[4][1]);
    line(cubeProj[1][0], cubeProj[1][1], cubeProj[5][0], cubeProj[5][1]);
    line(cubeProj[1][0], cubeProj[1][1], cubeProj[3][0], cubeProj[3][1]);
    line(cubeProj[2][0], cubeProj[2][1], cubeProj[3][0], cubeProj[3][1]);
    line(cubeProj[2][0], cubeProj[2][1], cubeProj[6][0], cubeProj[6][1]);
    line(cubeProj[3][0], cubeProj[3][1], cubeProj[7][0], cubeProj[7][1]);
    line(cubeProj[4][0], cubeProj[4][1], cubeProj[5][0], cubeProj[5][1]);
    line(cubeProj[4][0], cubeProj[4][1], cubeProj[6][0], cubeProj[6][1]);
    line(cubeProj[5][0], cubeProj[5][1], cubeProj[7][0], cubeProj[7][1]);
}

LO4 – Sound Art

As a musician who plays music for fun, I have an interest in the production of music as well as DAWs and loop creators. When I saw The Prouduct’s project, “Soundmachines, Creative Sound Production Device,” I was immediately fascinated. It’s a live loop creator that uses three turntables to create musical loops. It seems that it operates based on reflective input at the turntable’s “needle.” When a specific light intensity, or possibly color is reflected back into the needle, a program will play an expected sound. For example, the middle turntable manages bass kicks and hi hats, possibly a snare drum as well but the blue strip was never explicitly shown. The right handles an electronic clicking in a certain rhythm, while the left handles a filter effect on some wave function. I think that the final result of a modern-esque turntable was created more for the creator’s aesthetics than the practicality of the piece, as any loop generator can simply use button toggles. However, I appreciate the turntable design, as it gives a visualization to the loop that is being created.

Video of the project in use.