I program in C, C++, Python, PHP, Javascript and a number of languages that are long forgotten. This is just an example of a little "toy" that I wrote for fun. A tesseract, is to a cube, as a cube is to a square. It is a fundamental 4 dimensional piece of geometry. Then, I set it to rotate on all of the six axes that it has. It's interesting, I think, because while it appears to be a cube within a cube, all of the sides are the same length! They appear to grow and shrink as they get closer to you, just like a cube does, but it looks inside out.

If you are interested, I have placed the code for it below. I didn't use WebGl, because I was playing around with writing my own

// A simple tesseract
class Tesseract {
    constructor (win) {
        this.win = win;
        // The vertices of a tesseract
        this.tess = [];

        // Go ahead and fill those vertices
        for (let i = 0; i < 16; i++) {
            this.tess.push(this.bits_to_coords(i));
        }
    }

    // create the coordinates for a tesseract 
    // from the bits while we count
    bits_to_coords(n) {
        let x = (n & 1) ? 1 : -1;
        let y = (n & 2) ? 1 : -1;
        let z = (n & 4) ? 1 : -1;
        let w = (n & 8) ? 1 : -1;
        return new Array(x, y, z, w);
    }

    // Every frame, we'll run through this
    doTesseract () {
        // clear the screen
        win_context.clearRect(0, 0, win.width, win.height);

        // Make a rotation matrix for each of
        // the SIX axis of rotation that a 4D object has
        let rotxy = this.getRotXY(angle);
        let rotxz = this.getRotXZ(angle);
        let rotxw = this.getRotXW(angle);
        let rotyz = this.getRotYZ(angle);
        let rotyw = this.getRotYW(angle);
        let rotzw = this.getRotZW(angle);

        let projected = [];
        let that = this;

        this.tess.forEach(function (i) {
            i = mat_mul_vert(rotxy, i);             // Rotate it on the XY (that's the Z axis)
            i = mat_mul_vert(rotxz, i);             // Rotate it on the XZ (that's the Y axis)
            i = mat_mul_vert(rotxw, i);             // Rotate it in the XW (I have no idea what it's called)
            i = mat_mul_vert(rotyz, i);             // Rotate it on the YZ (that's the X axis)
            i = mat_mul_vert(rotyw, i);             // Rotate it on the YW (no idea how to name it)
            i = mat_mul_vert(rotzw, i);             // Rotate it on the ZW (and another that I can't name)
			
            i = mat_mul_vert(that.persp4D(i), i);
            i = mat_mul_vert(that.persp3D(i), i);
            i = vert_scale_mult(i, 300);            // Scale all of the vertices up
            i[0] += that.win.width / 2;             // Translate them over on the x
            i[1] += that.win.height / 2;            // Translate them over on the y
            projected.push(i);                      // And add them to the list of projected vertices
        });

        for (let p = 0; p < projected.length; p++) {
            drawVertices(projected[p][0], projected[p][1], [255, 255, 255]);
        }

        this.draw4Dlines(projected);
        this.draw4Dfaces(projected);
        angle += 1;

        if (angle >= 360) {
            angle = 0;
        }
    }

    // Build a simple rotation matrix for the XY axis
    getRotXY(ang) {
        // The angle has to be in radians
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([a, -b, 0, 0]);
        rot.push([b,  a, 0, 0]);
        rot.push([0,  0, 1, 0]);
        rot.push([0,  0, 0, 1]);
        return rot;
    }

    // And the XZ axis
    getRotXZ(ang) {
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([ a, 0, b, 0]);
        rot.push([ 0, 1, 0, 0]);
        rot.push([-b, 0, a, 0]);
        rot.push([0,  0, 0, 1]);
        return rot;
    }

    // And the YZ axis
    getRotYZ(ang) {
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([1, 0, 0, 0]);
        rot.push([0, a,-b, 0]);
        rot.push([0, b, a, 0]);
        rot.push([0, 0, 0, 1]);
        return rot;
    }

    // And the XW axis
    getRotXW(ang) {
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([a, 0, 0,-b]);
        rot.push([0, 1, 0, 0]);
        rot.push([0, 0, 1, 0]);
        rot.push([b, 0, 0, a]);
        return rot;
    }

    // And the YW axis
    getRotYW(ang) {
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([1, 0, 0, 0]);
        rot.push([0, a, 0,-b]);
        rot.push([0, 0, 1, 0]);
        rot.push([0, b, 0, a]);
        return rot;
    }

    // And the ZW axis
    getRotZW(ang) {
        let a = Math.cos(toRadians(ang));
        let b = Math.sin(toRadians(ang));
        let rot = [];
        rot.push([1, 0, 0, 0]);
        rot.push([0, 1, 0, 0]);
        rot.push([0, 0, a,-b]);
        rot.push([0, 0, b, a]);
        return rot;
    }

    // Get the perspective transform of a 4D vertex
    persp4D (rotated) {
        let distance = 2.5;
        let w = 1 / (distance - rotated[3]);
        let matrix = [];
        matrix.push([w, 0, 0, 0]);
        matrix.push([0, w, 0, 0]);
        matrix.push([0, 0, w, 0]);
        matrix.push([0, 0, 0, 0]);
        return matrix;
    }

    // Get the perspective transform of a 3D vertex
    persp3D (rotated) {
        let distance = 2.5;
        let z = 1 / (distance - rotated[2]);
        let matrix = [];
        matrix.push([z, 0, 0, 0]);
        matrix.push([0, z, 0, 0]);
        matrix.push([0, 0, z, 0]);
        matrix.push([0, 0, 0, 0]);
        return matrix;
    }

    // Draw the edges of a 4D tesseract
    draw4Dlines (p) {
        // Using this, we can draw all of the edges from one to the next 
        // without repeating and without drawing over any one twice 
        // (we can't do that with a cube)
        let disp = [0, 1, 3, 2, 6, 14, 10, 8, 9, 11, 3, 7, 15, 14, \
        12, 13, 9, 1, 5, 7, 6, 4, 12, 8, 0, 4, 5, 13, 15, 11, 10, 2, 0];
		
        // Start a new drawing
        let s = [0, 0];
        let e = [0, 0];
        let start = 0;
        disp.forEach(function (line) {
            if (start == 1) {
                s[0] = e[0];
                s[1] = e[1];
                e[0] = p[line][0];
                e[1] = p[line][1];
                drawLine(s[0], s[1], e[0], e[1], [0, 0, 0, 0]);
            } else {
                start = 1;
                e[0] = p[line][0];
                e[1] = p[line][1];
            }
        });
    }

    // Draw the faces of a 4D tesseract
    draw4Dfaces (p) {
        // Get the order of the vertices in the quads
        let disp = [
            [ 0,  1,  3,  2],   // quad inner back
            [ 2,  6, 14, 10],   // quad connect bottom of the left side
            [10,  8,  9, 11],   // quad inner front
            [11,  3,  7, 15],   // quad connect bottom of the right side
            [15, 14, 12, 13],   // quad near outer side 
            [13,  9,  1,  5],   // quad connect top of the right side
            [ 5,  7,  6,  4],   // quad far outer side
            [ 4, 12,  8,  0],   // quad connect top of the left side 
            [ 0,  1,  5,  4],   // quad connect top of the back
            [ 1,  3,  7,  5],   // quad connect right rear
            [ 3,  2,  6,  7],   // quad connect bottom rear
            [ 0,  2,  6,  4],   // quad connect left rear
            [ 8, 10, 14, 12],   // quad connect front left
            [ 9, 13, 12,  8],   // quad connect front top
            [ 9, 11, 15, 13],   // quad connect front right
            [10, 14, 15, 11],   // quad connect the front bottom 
            [ 1,  0,  8,  9],   // quad top of the inner cube
            [ 3,  1,  9, 11],   // quad right side of the inner cube
            [ 2, 10, 11,  3],   // quad bottom of the inner cube
            [ 0,  2, 10,  8],   // quad left side of the inner cube
            [14, 12,  4,  6],   // quad left side of the outer cube
            [13,  5,  4, 12],   // quad top of the outer cube
            [ 7, 15, 13,  5],   // quad right side outer cube
            [ 6, 14, 15,  7]    // quad for the bottom of the outer cube
        ];
        let a, b, c, d;
        a = b = c = d = [];
        disp.forEach (function (face, index) {
            let colour = [];
            if (index < 4) {
                // First four faces are coloured red
                colour = [200, 0, 0, 60];
            } else {
                if (index < 8) {
                    // next 4 faces are coloured yellow
                    colour = [200, 200, 0, 60];
                } else {
                    if (index < 11) {
                        // Next four faces are coloured green
                        colour = [0, 200, 0, 60];
                    } else {
                        if (index < 16) {
                            // Next four faces are coloured cyan
                            colour = [0, 200, 200, 60];
                        } else {
                            if (index < 20) {
                                // The next four faces are coloured blue
                                colour = [0, 0, 200, 60];
                            } else {
                                // And all the rest are coloured purple
                                colour = [200, 0, 200, 60];
                            }
                        }
                    }
                }
            }
            a = [p[face[0]][0], p[face[0]][1]];
            b = [p[face[1]][0], p[face[1]][1]];
            c = [p[face[2]][0], p[face[2]][1]];
            d = [p[face[3]][0], p[face[3]][1]];
            drawFilledQuad(a[0], a[1], b[0], b[1], c[0], c[1], d[0], d[1], colour);
        });
    }
};

// There isn't a Math function for this already, with javascript
// Convert radians to degrees
function toRadians(degrees) {
    return degrees * (Math.PI / 180.0); 
}

// A few global variables
let win_context;
let win;
let angle = 30;

// Draws a little circle around the vertex
function drawVertices (x, y, colour) {
    let colourString = "#" + colour[0].toString().padStart(2, "0");
    colourString += colour[1].toString(16).padStart(2, "0"); 
    colourString += colour[2].toString(16).padStart(2, "0");
    win_context.beginPath();
    win_context.arc(x, y, 2, 2 * Math.PI, false);
    win_context.lineWidth = '1';
    win_context.strokeStyle = colourString;
    win_context.stroke();
}

// This is the start of our line drawing function
function drawLine (x1, y1, x2, y2, colour) {
    let colourString = "#" + colour[0].toString(16).padStart(2, "0");
    colourString += colour[1].toString(16).padStart(2, "0"); 
    colourString += colour[2].toString(16).padStart(2, "0");
    win_context.beginPath();
    win_context.lineWidth = '1';
    win_context.strokeStyle = colourString;
    win_context.moveTo(x1, y1);
    win_context.lineTo(x2, y2);
    win_context.stroke();
}

// And this is the fully javascript version of the face drawing function
function drawFilledQuad (x1, y1, x2, y2, x3, y3, x4, y4, colour) {
    let colourString = "#" + colour[0].toString(16).padStart(2, "0"); 
    colourString += colour[1].toString(16).padStart(2, "0"); 
    colourString += colour[2].toString(16).padStart(2, "0");
    colourString += colour[3].toString(16).padStart(2, "0");
    win_context.beginPath();
    win_context.fillStyle = colourString; 
    win_context.moveTo(x1, y1);
    win_context.lineTo(x2, y2);
    win_context.lineTo(x3, y3);
    win_context.lineTo(x4, y4);
    win_context.fill();
}

// Multiply a vertex with a scale factor
function vert_scale_mult (v, sf) {
    for (let i = 0; i < v.length; i++) {
        v[i] *= sf;
    }
    return v;
}

// Turn a vertex sideways to be a matrix
// We need to do this to multiply them
// A "vertex" looks like this:
// vertex[x, y, z, w]
// but we want it to be:
// matrix [
//  [x],
//  [y],
//  [z],
//  [w]
// ]
function vert_to_matrix(vert) {
    let m = [];
    for (let i = 0; i < vert.length; i++) {
        m.push([vert[i]]);
    }
    return m;
}

// Turn a matrix back into a vertex
function matrix_to_vert(m) {
    let v = [];
    m.forEach (function (val) {
        v.push(val[0]);
    }); 
    return v;
}

// Multiply a matrix with a vertex
// returns a 1 x n array (a vertex)
function mat_mul_vert (mat_a, vert_b) {
    return matrix_to_vert(mat_mul(mat_a, vert_to_matrix(vert_b)));
}

// mutiply matrix a by matrix b for a new resulting matrix
// take in two matrices, and returns a third matrix
function mat_mul (a, b) {
    rowA = a.length;
    rowB = b.length;
    colA = a[0].length;
    colB = b[0].length;

    let result = [];
    for (let t = 0; t < 4; t++) {
        result.push([0, 0, 0, 0]);
    }

    for (let i = 0; i < rowA; i++) {
        for (let j = 0; j < colB; j++) {
            for (k = 0; k < rowB; k++) {
                result[i][j] += a[i][k] * b[k][j];
            }
        }
    }
    return result;
}

// And a function that starts everything going
function whenReady () {
    // First, lets start with some js specific code,
    // that will address the html file that we put this into
    win = document.getElementById("canvas3d");  // win is the window of the canvas
    win_context = win.getContext("2d");         // an object that gets us information about the win

    // Create a new tesseract object
    tess = new Tesseract(win);

    // And animate it rotating in place
    setInterval(function () {
        tess.doTesseract();
    }, 30, tess);
}