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