xxxxxxxxxx
427
const CUBE_FACES = 6;
const CUBE_LAYERS = 3;
const CUBIE_SIZE = 60;
const CUBIE_SPACING = 0;
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 clockwise = true;
let cubieSpacingSlider;
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);
// cube.cubies.forEach((cubie) => {
// cubie.faces.forEach((face, i) => (face.color = sample(COLORS)));
// });
// debugMode(GRID, 400);
}
function draw() {
background(clockwise ? 255 : 220);
scale(1, -1, 1); // Invert the Y-axis.
rotateX(PI / 6);
rotateY(-PI / 6);
cube.render();
show3dAxes(Math.min(windowWidth, windowHeight) * 0.6);
}
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()));
}
}
// Redraws the Canvas when resized.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
///////////////////////
// Utility functions //
///////////////////////
function repeat(times, fn) {
result = [];
for (let i = 0; i < times; i++) result.push(fn(i));
return result;
}
function matrixByVectorMult(matrix, vector) {
return matrix.map((row) =>
row.reduce((acc, value, i) => acc + value * vector[i], 0)
);
}
function arrow({
source = [0, 0, 0],
target,
tipRadius,
tipHeight,
thickness = 1,
tickWidth,
color,
} = {}) {
const TICK_HEIGHT = 20;
if (!(source instanceof p5.Vector)) source = createVector(source);
if (!(target instanceof p5.Vector)) target = createVector(target);
const arrowLength = source.dist(target);
const ticks = tickWidth === undefined ? 0 : arrowLength / tickWidth;
if (arrowLength === 0) return;
if (tipRadius === undefined) tipRadius = arrowLength / 75;
if (tipHeight === undefined || tipHeight >= arrowLength)
tipHeight = tipRadius * 2.5;
const xAxis = createVector(1, 0, 0);
const yAxis = createVector(0, 1, 0);
const resultingVectorFromOrigin = p5.Vector.sub(target, source);
const xzProjection = createVector(
resultingVectorFromOrigin.x,
0,
resultingVectorFromOrigin.z
);
const xAxisXzProjectionAngle = xAxis.angleBetween(xzProjection);
const yAxisResultingVectorFromOriginAngle = yAxis.angleBetween(
resultingVectorFromOrigin
);
isolateTransformations(() => {
translate(target);
if (!Object.is(xAxisXzProjectionAngle, NaN)) {
// Place x-axis exactly over the X-Z projection's position.
rotateY(
(resultingVectorFromOrigin.z > 0 ? -1 : 1) * xAxisXzProjectionAngle
);
}
if (!Object.is(yAxisResultingVectorFromOriginAngle, NaN)) {
// Place y-axis over the resulting vector from origin's position.
rotateZ(
(resultingVectorFromOrigin.x > 0 ? 1 : -1) *
yAxisResultingVectorFromOriginAngle
);
}
// Move to the base of the tip.
translate(0, -tipHeight, 0);
// Draw the arrow body.
stroke(color);
strokeWeight(thickness);
line(0, 0, 0, -(arrowLength - tipHeight));
repeat(ticks, (i) => {
const y =
-(arrowLength - tipHeight) + ((i + 1) * arrowLength) / (ticks + 1);
line(-TICK_HEIGHT / 2, y, TICK_HEIGHT / 2, y);
});
// Draw the arrow tip.
fill(color);
noStroke();
cone(tipRadius, tipHeight);
});
}
function show3dAxes(width, ticksCount) {
const tickWidth = width / ticksCount;
// x-axis.
arrow({
source: [-width / 2, 0, 0],
target: [width / 2, 0, 0],
color: "red",
tickWidth,
});
// y-axis.
arrow({
source: [0, -width / 2, 0],
target: [0, width / 2, 0],
color: "green",
tickWidth,
});
// z-axis.
arrow({
source: [0, 0, -width / 2],
target: [0, 0, width / 2],
color: "blue",
tickWidth,
});
}
function isolateTransformations(fn) {
push();
fn();
pop();
}
function createCustomSlider({
min,
max,
initial,
step,
x,
y,
lengthInPixels,
text,
textColor = "#FFF",
}) {
const slider = createSlider(min, max, initial, step);
slider.position(x, y);
slider.style("width", `${lengthInPixels}px`);
const span = createSpan(`(${text})`);
span.style("color", textColor);
span.position(x + lengthInPixels + 5, y);
const sliderInputFn = () => {
span.html(`${text} (${slider.value()})`);
};
sliderInputFn();
slider.input(sliderInputFn);
return slider;
}