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]);
}

Leave a Reply