Skip to content

WebGL - Interactive Cube

Building on the previous example, this chapter shows how to make the 3D cube interactive, allowing rotation based on mouse movement.

We’ll add event listeners to the canvas to track mouse down, move, and up events. When the mouse button is held down and moved, we’ll calculate the change in X and Y position and use that to update rotation angles for the cube.

The following code extends the rotating cube example with mouse controls.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebGL Interactive Cube</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.3.0/gl-matrix-min.js"></script>
<style>canvas { border: 1px solid black; cursor: grab; } canvas:active { cursor: grabbing; }</style>
</head>
<body>
<canvas width="570" height="570" id="my_Canvas"></canvas>
<script>
const { mat4, vec3 } = glMatrix;
/*============= Creating a canvas ======================*/
const canvas = document.getElementById('my_Canvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert('WebGL not supported!');
throw new Error('WebGL not supported');
}
/*========== Defining and storing the geometry ==========*/
// (Geometry and Color definitions - Same as rotating cube example)
const vertices = [
// Front face
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
// Back face
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
// Top face
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
// Bottom face
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
// Right face
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
// Left face
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0,
];
const faceColors = [
[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0],
[1.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]
];
let colors = [];
faceColors.forEach(c => { colors = colors.concat(c, c, c, c); });
const indices = [
0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11,
12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23
];
// --- Create Buffers (Same as rotating cube example) ---
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
/*=================== SHADERS =================== */
// (Shader sources, compilation, linking - Same as rotating cube example)
const vsSource = ` attribute vec4 aVertexPosition; attribute vec3 aVertexColor; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; varying lowp vec3 vColor; void main(void) { gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vColor = aVertexColor; }`;
const fsSource = ` varying lowp vec3 vColor; void main(void) { gl_FragColor = vec4(vColor, 1.0); }`;
function loadShader(gl, type, source) { /* ... (same as before) ... */
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null;
} return shader;
}
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('Shader link error:', gl.getProgramInfoLog(shaderProgram)); throw new Error('Shader link failed');
}
const programInfo = {
program: shaderProgram,
attribLocations: { /* ... (same as before) ... */
vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
vertexColor: gl.getAttribLocation(shaderProgram, 'aVertexColor'),
},
uniformLocations: { /* ... (same as before) ... */
projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
},
};
/*======== Associating attributes & buffers =====*/
// (Attribute setup - Same as rotating cube example)
{
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(programInfo.attribLocations.vertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}
{
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.vertexAttribPointer(programInfo.attribLocations.vertexColor, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(programInfo.attribLocations.vertexColor);
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.useProgram(programInfo.program);
/*==================== MATRIX Setup ====================== */
// (Projection Matrix setup - Same as rotating cube example)
const fieldOfView = 45 * Math.PI / 180;
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
const zNear = 0.1; const zFar = 100.0;
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar);
// Model-View Matrix setup (initial position)
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -6.0]); // Move camera back
// Variables for tracking rotation based on mouse drag
let currentRotation = {
x: 0,
y: 0
};
/*================= Mouse events ======================*/
let drag = false;
let lastMouseX = -1, lastMouseY = -1;
function mouseDown(event) {
drag = true;
lastMouseX = event.clientX;
lastMouseY = event.clientY;
event.preventDefault(); // Prevent text selection
}
function mouseUp(event) {
drag = false;
}
function mouseMove(event) {
if (!drag) return;
const dx = event.clientX - lastMouseX;
const dy = event.clientY - lastMouseY;
// Update rotation angles (adjust sensitivity as needed)
currentRotation.y += dx * 0.01;
currentRotation.x += dy * 0.01;
// Clamp X rotation to avoid flipping upside down
currentRotation.x = Math.max(Math.min(currentRotation.x, Math.PI / 2), -Math.PI / 2);
lastMouseX = event.clientX;
lastMouseY = event.clientY;
}
canvas.addEventListener('mousedown', mouseDown, false);
canvas.addEventListener('mouseup', mouseUp, false);
canvas.addEventListener('mouseout', mouseUp, false); // Stop dragging if mouse leaves canvas
canvas.addEventListener('mousemove', mouseMove, false);
/*=================== Drawing =================== */
function render() {
gl.clearColor(0.5, 0.5, 0.5, 0.9);
gl.clearDepth(1.0);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Reset modelViewMatrix to base translation each frame
mat4.identity(modelViewMatrix);
mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -6.0]);
// Apply mouse-controlled rotation
mat4.rotate(modelViewMatrix, modelViewMatrix, currentRotation.x, [1, 0, 0]); // Rotate around X axis
mat4.rotate(modelViewMatrix, modelViewMatrix, currentRotation.y, [0, 1, 0]); // Rotate around Y axis
// Set shader uniforms
gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, projectionMatrix);
gl.uniformMatrix4fv(programInfo.uniformLocations.modelViewMatrix, false, modelViewMatrix);
// Draw the cube
{
const vertexCount = 36;
const type = gl.UNSIGNED_SHORT;
const offset = 0;
gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>

Expected Result:

This code renders a colored cube. Clicking and dragging the mouse on the canvas will rotate the cube according to the mouse movement.