Ashton C Montgomery

Web Developer

A User Controlled Cube: Experimenting with User Input and Scene Interaction

One of the most fun and challenging projects in my 3D CSS series was A User Controlled Cube. As the name suggests, the goal was to create a cube that users could control with their keyboard’s arrow keys. This project aimed to add interactivity to the 3D scene, allowing users to control the movement of the cube within the space, and it was built using vanilla HTML, CSS, and JavaScript—just like all my previous projects.

Project Overview

The primary goal of this project was to enable user input via the arrow keys to control a 3D cube. While I had explored 3D transformations and scene manipulation in previous projects, this was the first time I focused on creating dynamic user-controlled movement.

Approach & Execution

To start, I created the scene with the cube in much the same way as my previous 3D CSS projects, with the scene and cube elements defined in HTML and styled using CSS. The main challenge, however, was to write the JavaScript functions that would allow the cube to move in response to the arrow keys.

Movement Functions:

The most significant difference in this project was the creation of the movement functions, which didn’t just move the cube itself but also the entire scene. The idea was to keep the cube relatively centered within the screen as it traveled across the scene, no matter which direction the user pressed.

let scene = document.getElementById("scene");
let rotation = 0;

let floor = document.getElementById("floor");
let floorX = 0;
let floorY = 0;
let floorSpeed = 1.5;

let moveableObject = document.getElementById("moveableObject");
let objectX = 0;
let objectZ = 0;
let speed = 1.5;

function updatePosition() {
moveableObject.style.transform = "translateX(" + objectX + "em) translateZ(" + objectZ + "em) rotateY(" + (rotation * -1) + "deg)";
floor.style.transform = "translate(-50%, -50%) rotateX(90deg) translate3D(" + floorX + "em," + floorY + "em, 0)";
scene.style.transform = "rotateY(" + rotation + "deg)";
}

function moveObject() {
document.addEventListener('keydown', (e) => {
let deltaX = 0;
let deltaZ = 0;
let maxMovement = 4.5;
let steps = 2;
// Define movement behavior based on rotation angles
let radians = rotation * (Math.PI / 180);

// Depending on the current rotation angle, calculate movement along the x and z axes
if (rotation == 0) {
deltaX = speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation > 0 && rotation < 45)|| (rotation < 0 && rotation > -45)) {
// Move forward along the Z axis, with a little contribution to X for small angles
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
if ((objectZ == maxMovement && (objectX != maxMovement || objectX != -maxMovement)) || (objectZ == -maxMovement && (objectX != maxMovement || objectX != -maxMovement))) {
if (objectX > 0) {
let movDif = maxMovement - objectX;
if (objectX > maxMovement) {
objectX = maxMovement;
}
objectX += (movDif / steps);
console.log((movDif / steps) + " was added. The movDif of " + movDif + " will be adjusted within " + steps + " steps.");
} else if (objectX < 0) {
let movDif = -maxMovement - objectX;
if (objectX < -maxMovement) {
objectX = -maxMovement;
}
objectX += (movDif / steps);
console.log((movDif / steps) + " was added. The movDif of " + movDif + " will be adjusted within " + steps + " steps.");
}
}
if ((objectX == maxMovement && (objectZ != maxMovement || objectZ != -maxMovement)) || (objectX == -maxMovement && (objectZ != maxMovement || objectZ != -maxMovement))) {
if (objectZ > 0) {
let movDif = maxMovement - objectZ;
if (objectZ > maxMovement) {
objectZ = maxMovement;
}
objectZ += (movDif / steps);
console.log((movDif / steps) + " was added. The movDif of " + movDif + " will be adjusted within " + steps + " steps.");
} else if (objectZ < 0) {
let movDif = -maxMovement - objectZ;
if (objectZ > maxMovement) {
objectZ = -maxMovement;
}
objectZ += (movDif / steps);
console.log((movDif / steps) + " was added. The movDif of " + movDif + " will be adjusted within " + steps + " steps.");
}
}
}
else if ((rotation >= 45 && rotation < 90) || (rotation <= -45 && rotation > -90)) {
// Move forward along both X and Z as the rotation is between 45 and 90 degrees
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 90 && rotation < 135) || (rotation <= -90 && rotation > -135)) {
// Move more along the X axis as rotation moves towards 90 degrees
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 135 && rotation < 180) || (rotation <= -135 && rotation > -180)) {
// Move backward along the Z axis, with negative direction on X
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 180 && rotation < 225) || (rotation <= -180 && rotation > -225)) {
// Move backward along both X and Z
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 225 && rotation < 270) || (rotation <= -225 && rotation > -270)) {
// Move backward along the X axis
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 270 && rotation < 315) || (rotation <= -270 && rotation > -315)) {
// Move more towards Z axis (move back)
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
else if ((rotation >= 315 && rotation < 360) || (rotation <= -315 && rotation > -360)) {
// Move backward along the X axis, with a little contribution to Z for the final angle range
deltaX = Math.sin(radians) * speed;
deltaZ = Math.cos(radians) * speed;
}
// Handle rotation
if (e.key === "ArrowLeft" && e.ctrlKey) {
rotation -= 9;
console.log("Rotation is moved left by 9");
}
if (e.key === "ArrowRight" && e.ctrlKey) {
rotation += 9;
console.log("Rotation is moved right by 9");
}

// Handle movement based on key press
if (e.key === "ArrowLeft" && !e.ctrlKey) {
objectX -= deltaX; // Move left along X axis
floorX += floorSpeed;
console.log("Left arrow was pressed. Delta X: " + deltaX);
}
if (e.key === "ArrowRight" && !e.ctrlKey) {
objectX += deltaX; // Move right along X axis
floorX -= floorSpeed;
console.log("Right arrow was pressed. Delta X: " + deltaX);
}
if (e.key === "ArrowUp") {
if (rotation == 0 || rotation == 180 || rotation == -180) {
if (rotation == 180 || rotation == -180) {
deltaZ = (deltaZ * -1);
}
deltaX = 0;
}
if (rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270) {
deltaZ = 0; // No movement in the Z direction when rotated 90°
floorSpeed = 0; // keep the floor from moving sideways underneath the object
}
objectX += deltaX; // Move forward along X axis
objectZ -= deltaZ; // Move forward along Z axis
floorY += floorSpeed;
console.log("Up arrow was pressed. Delta X: " + deltaX + ", Delta Z: " + deltaZ);
}
if (e.key === "ArrowDown") {
if (rotation == 0 || rotation == 180 || rotation == -180) {
deltaX = 0;
}
if (rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270) {
deltaZ = 0; // No movement in the Z direction when rotated 90°
floorSpeed = 0; // keep the floor from moving sideways underneath the object
}
objectX -= deltaX; // Move backward along X axis
objectZ += deltaZ; // Move backward along Z axis
floorY -= floorSpeed;
console.log("Down arrow was pressed. Delta X: " + deltaX + ", Delta Z: " + deltaZ);
}

// Constrain movement within the max range

if (objectX > maxMovement) {
objectX = maxMovement;
} else if (objectX < -maxMovement) {
objectX = -maxMovement;
}

if (objectZ > maxMovement) {
objectZ = maxMovement;
} else if (objectZ < -maxMovement) {
objectZ = -maxMovement;
}

console.log("Object positioning is x:" + objectX + ", z:" + objectZ);
console.log("The floor positioning is x: " + floorX + ", y: " + floorY + " rotation: " + rotation);
updatePosition();
});
}

However, this is where things got tricky. I wanted to allow the cube to rotate along with the scene, maintaining its movement in the direction of the arrow key pressed. This required complex math and logic to handle both the rotational movement and the translation of the cube, which resulted in functions that were long and complicated.

Trial and Error:

During the process, I spent a lot of time troubleshooting the code and collaborating with ChatGPT to solve various issues. Although the back-and-forth discussions were helpful, they also made some of the "fluid motion" problems more complicated, as ChatGPT couldn’t visually see the issues in the same way I could. After several rounds of adjustments, I realized that the complexity of adding rotation to the scene—and keeping the cube’s movement smooth—was leading to a less-than-optimal user experience.

Realization & Decision

Eventually, I came to a realization: no matter how cool the interactive element would be, it would ultimately be user-unfriendly. The rotational movement introduced a level of complexity that made the interaction feel clunky and unnatural. Given the amount of trial and error involved and the realization that this level of interactivity could hurt the user experience, I decided to halt the rotational movement functionality.

While interactive websites are exciting, usability should always come first. I felt that this particular feature was more of a novelty than a practical enhancement for the user experience, and so I shifted focus to more intuitive design.

Final Thoughts

A User Controlled Cube was a fascinating experiment in creating interactive 3D environments, and it taught me valuable lessons about balancing creativity with user experience. Although I ultimately scaled back the rotational functionality, the project helped me understand the importance of keeping interactive elements intuitive and accessible.

As I continue to experiment with interactive 3D elements, I’ll be more mindful of how complex features can impact the user experience. This project, while challenging, helped shape my understanding of user-centered design in web development.

While working on the user controlled cube, and before I had determined that maybe all the time I've been devoting to 3D CSS could possibly be better spent. I was working on another 3D CSS project, 3D small house

Need Help Shaping Your Corner of the Internet?

I'd love to work with you.

Off to the Contact Page!