xxxxxxxxxx
269
// https://processing.org/tutorials/transform2d
const CUBE_FACES = 6;
const CUBE_LAYERS = 3;
const CUBIE_SIZE = 60;
const AXES_3D = ["x", "y", "z"];
const FACES = {
front: 0,
right: 1,
back: 2,
left: 3,
top: 4,
bottom: 5,
};
const FACE_COLORS = {
[FACES.front]: "green",
[FACES.right]: "red",
[FACES.back]: "blue",
[FACES.left]: "orange",
[FACES.top]: "white",
[FACES.bottom]: "yellow",
};
const FACE_NORMALS = {
[FACES.front]: [0, 0, 1], // Normal unit vector towards +z
[FACES.right]: [1, 0, 0], // Normal unit vector towards +x
[FACES.back]: [0, 0, -1], // Normal unit vector towards -z
[FACES.left]: [-1, 0, 0], // Normal unit vector towards -x
[FACES.top]: [0, 1, 0], // Normal unit vector towards +y
[FACES.bottom]: [0, -1, 0], // Normal unit vector towards -y
};
const HALF_PI_ROTATION_MATRICES = {
// Clockwise rotation
true: {
x: [
[1, 0, 0],
[0, 0, 1],
[0, -1, 0],
],
y: [
[0, 0, -1],
[0, 1, 0],
[1, 0, 0],
],
z: [
[0, 1, 0],
[-1, 0, 0],
[0, 0, 1],
],
},
// Counter-clockwise rotation
false: {
x: [
[1, 0, 0],
[0, 0, -1],
[0, 1, 0],
],
y: [
[0, 0, 1],
[0, 1, 0],
[-1, 0, 0],
],
z: [
[0, -1, 0],
[1, 0, 0],
[0, 0, 1],
],
},
};
let cube;
let cubieSpacingSlider;
let clockwise = true;
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
createEasyCam();
cubieSpacingSlider = createCustomSlider({
min: 0,
max: 80,
initial: 0,
step: 1,
x: 0,
y: 0,
lengthInPixels: 150,
text: "Cubie spacing",
textColor: "#000",
});
axisSelectionRadio = createRadio();
AXES_3D.forEach((axis) => axisSelectionRadio.option(axis, axis));
axisSelectionRadio.position(0, 25);
axisSelectionRadio.selected("x");
const clockwiseButton = createButton("Reverse orientation");
clockwiseButton.position(5, 50);
clockwiseButton.style("width", "140px");
clockwiseButton.mousePressed(() => {
clockwise = !clockwise;
});
repeat(CUBE_LAYERS, (layer) => {
const button = createButton((layer + 1).toString());
button.position(layer * 35 + 5, 80);
button.style("width", "30px");
button.id(`${AXES_3D[layer]}-axis`);
button.mousePressed(() => {
if (layer + 1 <= CUBE_LAYERS)
cube.rotate(
axisSelectionRadio.value(),
cube.axisLimit() - (layer + 1) + 1,
clockwise
);
});
});
cube = new RubikCube(CUBIE_SIZE, CUBE_LAYERS);
}
function draw() {
background(clockwise ? 255 : 220);
scale(1, -1, 1);
lights();
rotateX(PI / 6);
rotateY(-PI / 6);
show3dAxes(min(windowWidth, windowHeight) * 0.9, 7);
cube.render();
}
// Redraws the Canvas when resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
function keyPressed() {
if (keyCode === SHIFT) {
clockwise = !clockwise;
return;
}
const numericKey = keyCode - 48;
const index = numericKey === 0 ? 10 : numericKey;
if (numericKey > cube.layers) return;
const adjustedIndex = cube.axisLimit() - index + 1;
cube.rotate(axisSelectionRadio.value(), adjustedIndex, clockwise);
}
class RubikCube {
constructor(cubieSize, layers) {
this.cubieSize = cubieSize;
this.layers = layers;
this.cubies = [];
this._forEachAxisIndex((x) =>
this._forEachAxisIndex((y) =>
this._forEachAxisIndex((z) =>
this.cubies.push(new Cubie(this, x, y, z, cubieSize))
)
)
);
}
axisLimit() {
return (this.layers - 1) / 2;
}
render() {
this.cubies.forEach((cubie) => cubie.render());
}
rotate(axis, axisIndex, clockwise) {
const layerCubies = this.cubies.filter(
(cubie) => cubie.position[axis] === axisIndex
);
const rotationMatrix = HALF_PI_ROTATION_MATRICES[clockwise][axis];
layerCubies.forEach((cubie) => cubie.rotate(rotationMatrix));
}
_forEachAxisIndex(fn) {
for (let i = -this.axisLimit(); i <= this.axisLimit(); i++) fn(i);
}
}
class Cubie {
constructor(cube, x, y, z, size) {
this.cube = cube;
this.position = createVector(x, y, z);
this.size = size;
this.faces = repeat(
CUBE_FACES,
(i) => new CubieFace(this, size, FACE_COLORS[i], FACE_NORMALS[i])
);
}
render() {
const cubieSpacing = cubieSpacingSlider.value();
isolateTransformations(() => {
// Translate to the center of the 3d cubie.
translate(p5.Vector.mult(this.position, this.size + cubieSpacing));
this.faces.forEach((face) => face.render());
});
}
rotate(matrix) {
this.position.set(matrixByVectorMult(matrix, this.position.array()));
this.faces.forEach((face) => face.rotate(matrix));
}
}
class CubieFace {
constructor(cubie, size, color, normal) {
this.cubie = cubie;
this.size = size;
this.color = color;
this.normal = createVector(normal);
}
render() {
isolateTransformations(() => {
fill(this.color);
// Translate to the center of the 2d face.
translate(p5.Vector.mult(this.normal, this.size / 2));
strokeWeight(5);
box(
abs(this.normal.x) === 1 ? 0 : this.size,
abs(this.normal.y) === 1 ? 0 : this.size,
abs(this.normal.z) === 1 ? 0 : this.size
);
// Draw the normal vector from the center of the 2d face.
arrow({
target: p5.Vector.mult(this.normal, this.size / 3),
color: "black",
tipRadius: 2,
tipHeight: 7,
thickness: 0.5,
});
});
}
rotate(matrix) {
this.normal.set(matrixByVectorMult(matrix, this.normal.array()));
}
}
function matrixByVectorMult(matrix, vector) {
return matrix.map((row) =>
row.reduce((acc, value, i) => acc + value * vector[i], 0)
);
}