<div id="header">
            <h1 id="header-title">jSphere</h1>
            <h2 id="header-subtitle">Just to see what I could do.</h2>
        </div>
        <div id="controls">
            <h3 id="controls-title">Controls</h3>
            <div id="controls-body">
                <p id="c-1">Rotate:  Click and drag</p>
                <p id="c-2">Pan:     Hold shift, click and drag</p>
                <p id="c-3">Zoom:    Hold ctrl, click and drag</p>
            </div>
        </div>
        <div id="content">
            <canvas id="canvas" width="800" height="700"></canvas>
        </div>
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 100;
  src: local('Lato Hairline'), local('Lato-Hairline'), url(https://themes.googleusercontent.com/static/fonts/lato/v6/boeCNmOCCh-EWFLSfVffDg.woff) format('woff');
  /* */
}
@font-face {
  font-family: 'Lato';
  font-style: normal;
  font-weight: 400;
  src: local('Lato Regular'), local('Lato-Regular'), url(https://themes.googleusercontent.com/static/fonts/lato/v6/9k-RPmcnxYEPm8CNFsH2gg.woff) format('woff');
  /* */
}
@font-face {
  font-family: 'Lato';
  font-style: italic;
  font-weight: 300;
  src: local('Lato Light Italic'), local('Lato-LightItalic'), url(https://themes.googleusercontent.com/static/fonts/lato/v6/2HG_tEPiQ4Z6795cGfdivD8E0i7KZn-EPnyo3HZu7kw.woff) format('woff');
}

body {
	background-color: #333;
	font-family: Lato;
	color: #DDD;	
}

h1 {
        font-size: 64px;
}

h1,h2,h3,h4 {
	font-weight: 100;
	position: relative;
	margin: 30px auto;
	text-align: center;
}

p {
	line-height: 30px;
}

#header {
	position: relative;
	width: 100%;
}

#header-title, #header-subtitle {
	width: 500px;
}
#header-subtitle {
	font-style: italic;
}


#content {
	padding: 0px;
	width: 810px;
	height: 900px;
	margin: 0px auto;
	position: relative;
	top: -50px;
}

#canvas {
	border: 2px solid black;
	z-index: 2;
}

#controls {
	width: 810;
	height: 200px;
	margin-left: auto;
	margin-right: auto;
	margin-top: 60px;
	margin-bottom: -50px;
	z-index:1;
	position: relative;
}

#controls-title {
	width: 75px;
	margin-bottom: 15px;
}

#controls-body {
	width: 100%;
	position: relative;
	margin: 0px;
}

#c-1 { /* Rotate */
	position: absolute;
	left: 0px;
	top: 0px;
	margin: 0px;
}

#c-2 { /* Pan */
	position: relative;
	width: 200px; 
	text-align: center;
	margin: 0px auto;
	right: 10px;
}

#c-3 { /* Zoom */
	position: absolute;
	right: 10px;
	top: 0px;
	margin: 0px;
}

#psst {
	font-style: italic;
}
/**
 * Class Dot
 * =========
 * 
 * Members:
 * --------
 * 
 * x, y, z:     The cartesian coordinates of the dot in the space of the sphere.
 * fg:          A flag indicating whether the dot is in the foreground.
 * fgcolor:     The color to use if fg == true.
 * bgcolor:     The color to use if fg == false.
 * 
 * 
 * About:
 * ------
 * 
 * The Dot class represents one point on the surface of sphere. 
 * Each Dot is displayed as a smaller sphere attached to its point.
 * 
 * When drawn to the canvas, each dot is considered to be either in
 * the foreground or the background. This is determined by the Sphere
 * object, but basically, if its Z value is less than that of the
 * sphere's origin (center) point, it is in the background. Otherwise,
 * it is in the foreground.
 *
 * If in the foreground, the dot has a 10 pixel radius and is drawn with
 * a dark color. If in the background, the dot has a 5 pixel radius and
 * is drawn with a very light color, with those furthest back being almost
 * invisible. This gives the illusion of depth.
 * 
 */
function Dot(x,y,z,fg,fgColor,bgColor) {
    // Default Values:  { x: 0, y: 0, z: 0, fg: true, fgcolor: #7EE37E, bgcolor: "#787878" }
    this.x = typeof x !== 'undefined' ? x : 0;
    this.y = typeof y !== 'undefined' ? y : 0;
    this.z = typeof z !== 'undefined' ? z : 0;
    this.fg = typeof fg !== 'undefined' ? fg : true;
    this.fgColor = typeof fgColor !== 'undefined' ? fgColor : "#7EE37E";
    this.bgColor = typeof bgColor !== 'undefined' ? bgColor : "#787878";
    
    this.Draw = function(ctx) {
        // Store the context's fillStyle
        var tmpStyle = ctx.fillStyle;
        // Set the fillStyle for the dot
        ctx.fillStyle = (this.fg ? this.fgColor : this.bgColor);
        // Draw the dot
        ctx.fillCircle(x,y,this.fg?10:5);
        // Restore the previous fillStyle
        ctx.fillStyle = tmpStyle;
    }
}


/**
 * Class Sphere
 * =========
 * 
 * Members:
 * --------
 * 
 * x, y, z:     Cartesian coordinates of the origin (center) point of the sphere.
 * r:           Radius of the sphere.
 * ctx:         Canvas context with which to draw.
 * drawSpheres: Flag indicating whether surface points (Dots) should be drawn as spheres or points.
 * circleSize:  Radius of the dots to be drawn.
 * points:      Array of surface points to keep track of.
 * 
 * 
 * About:
 * ------
 * 
 * The Spehre class represents the sphere object itself. (Go figure.)
 * It is basically comprised of a set of points on its surface and various
 * numbers to keep track of how it should be drawn next.
 * 
 * When drawn to the canvas, the dot list is dynamically sorted by Z-value 
 * and drawn back-to-front, so that the foremost dots are consistently drawn 
 * on top of those behind them. The further back a dot is (i.e. the lower its
 * z-value) the lighter it is colored. This way, those in back blend into the 
 * background, becoming almost invisible.
 * 
 */
function Sphere(x,y,z,r,ctx,drawSpheres) {
    // Default Values:  { x: 200, y: 200, z: 0, r: 90, ctx: null, drawSpheres: false }
    this.x = (typeof x !== 'undefined') ? x : 200;
    this.y = (typeof y !== 'undefined') ? y : 200;
    this.z = (typeof z !== 'undefined') ? z : 0;
    this.r = (typeof r !== 'undefined') ? r : 90;
    this.ctx = (typeof ctx !== 'undefined') ? ctx : null;
    this.drawSpheres = (typeof drawSpheres !== 'undefined') ? drawSpheres : false;
    
    // 10 pixel dots
    this.circleSize = 10;
    this.points = new Array();
    
    // The angle delta to use when calculating the surface point positions;
    // a larger angstep means fewer points on the surface
    var angstep = Math.PI/10;
    var i = 0;
    // Loop from 0 to 2*pi, creating one row of points at each step
    for (var angxy=0; angxy<2*Math.PI; angxy+=angstep){
        this.points[i] = new Array();
        var j=0;
        for (var angyz=0; angyz<2*Math.PI; angyz+=angstep) {
            // Loop from 0 to 2*pi, creating one point at each step
            var px  = r * Math.cos(angxy) * Math.sin(angyz) + x,
                py  = r * Math.sin(angxy) * Math.sin(angyz) + y,
                pz  = r * Math.cos(angyz) + z,
                pfg = pz > z;
            this.points[i][j] = new Dot(px,py,pz,pfg);
            j++;
        }
        i++;
    }

    // Draw to the canvas
    this.Draw = function() {
        // Store the context's fillStyle
        var tmpStyle = this.ctx.fillStyle;


        // Clear the canvas
        canvas.width -= 1;
        canvas.width += 1;
        
        // Set the canvas background color
        this.ctx.fillStyle = '#FFF';
        // Fill the canvas with the selected background color
        this.ctx.fillRect (0, 0, canvas.width, canvas.height);
        
        // Sort the points by z-value
        
        // Empty array
        var z_sorted = new Array();
        // Add each surface point first
        for (var i=0; i<this.points.length; i++) {
            for (var j=0; j<this.points[i].length; j++) {
                z_sorted[this.points.length*i+j] = this.points[i][j];
            }
        }
        // Add the origin point of the sphere
        z_sorted[z_sorted.length] = new Dot(this.x, this.y, this.z);
        z_sorted.sort(function(a,b){return (b.z-a.z)});
        
        
        for (var p=0; p<z_sorted.length; p++) {
            var color;
            // If drawing the origin point, draw it blue
            if (z_sorted[p].x == this.x && z_sorted[p].y == this.y && z_sorted[p].z == this.z) {
                color = "#27F";
            } 
            // Else, draw the point a shade of gray relative to the z-value,
            // with darker pixels in the front and lighter pixels farther back
            else {
                var n = Math.round(((z_sorted[p].z + this.r)/(this.r*2)) * 250);
            
                color = "rgb(" + n + "," + n + "," + n + ")";
            }
            if (this.drawSpheres) {
                this.ctx.fillCircleGradient(z_sorted[p].x, z_sorted[p].y, this.circleSize, "#FFFFFF", color);
            } else {
                this.ctx.fillCircle(z_sorted[p].x, z_sorted[p].y, this.circleSize, color);
            }
        }
        
        // Restore the context's fillStyle
        this.ctx.fillStyle = tmpStyle;
    }
    
    // Zoom in or out (ctrl drag)
    this.Zoom = function(x,y) {
        var length = Math.round(Math.sqrt(x*x + y*y)),
            scale = (r + (x>0?length:-length))/r;
                    
                
        this.x *= scale;
        this.y *= scale;
        this.z *= scale;

        this.r *= scale;
        this.circleSize *= scale;
        
        // Scale each point
        for (var i in this.points) {
            for (var j in this.points[i]) {
                this.points[i][j].x *= scale;
                this.points[i][j].y *= scale;
                this.points[i][j].z *= scale;
            }
        }
        // Redraw in new positions
        this.Draw();
    }
    
    // Split pan!  (Hidden function, alt+shift drag)
    this.HiddenFun1 = function(x,y) {
        this.r += Math.round(Math.sqrt(x*x + y*y));
        // Scale each point
        for (var i in this.points) {
            for (var j in this.points[i]) {
                this.points[i][j].x += (this.points[i][j].x > this.x ? x : -x);
                this.points[i][j].y += (this.points[i][j].y > this.y ? y : -y);
            }
        }
        // Redraw in new positions
        this.Draw();
    }
    
    // Cigar Zoom!  (Hidden function, alt+ctrl drag)
    this.HiddenFun2 = function(x,y) {
        var length = Math.round(Math.sqrt(x*x + y*y)),
            scale = (r + (x>0?length:-length))/r;
        
        this.r += length;
        
        this.x *= scale;
        this.y *= scale;
        //this.z *= scale;

        
        // Scale each point
        for (var i in this.points) {
            for (var j in this.points[i]) {
                this.points[i][j].x *= scale;
                this.points[i][j].y *= scale;
            }
        }
        // Redraw in new positions
        this.Draw();
    }
        
    
    // Pan  (shift drag)
    this.Pan = function(x,y) {
        // Move each point
        for (var i in this.points) {
            for (var j in this.points[i]) {
                this.points[i][j].x += x;
                this.points[i][j].y += y;
            }
        }
        this.x += x;
        this.y += y;
        // Redraw in new positions
        this.Draw();
    }
    
    // Rotate the sphere (drag with no modifier keys)
    this.Rotate = function(x,y) {
        // Vertical rotation
        this.rotateXZ(((Math.PI/2)/this.r)*x);
        // Horizontal rotation
        this.rotateYZ(((Math.PI/2)/this.r)*y);
        
        // Redraw in new positions
        this.Draw();
    }
    
    // Rotate around the z-axis
    this.rotateXY = function(ang) {
        //console.log("XY test: ",ang);
        for (var i in this.points) {
            for (var j in this.points[i]) {
                var px = this.points[i][j].x - this.x,
                    py = this.points[i][j].y - this.y;
                
                var newx = px*Math.cos(ang)-py*Math.sin(ang),
                    newy = px*Math.sin(ang)+py*Math.cos(ang);
                    
                this.points[i][j].x = newx+this.x;
                this.points[i][j].y = newy+this.y;
            }
        }
    };
    
    // Rotate around the y-axis
    this.rotateXZ = function(ang) {
        //console.log("XZ test: ",ang);
        for (var i in this.points) {
            for (var j in this.points[i]) {
                var px = this.points[i][j].x - this.x,
                    pz = this.points[i][j].z - this.z;
                
                var newx = px*Math.cos(ang)-pz*Math.sin(ang),
                    newz = px*Math.sin(ang)+pz*Math.cos(ang);
                    
                this.points[i][j].x = newx+this.x;
                this.points[i][j].z = newz+this.z;
            }
        }
    };
    
    // Rotate around the x-axis
    this.rotateYZ = function(ang) {
        //console.log("YZ test: ",ang);
        
        for (var i in this.points) {
            for (var j in this.points[i]) {
                //console.log("old y: ", this.points[i][j].y,"\nold z: ", this.points[i][j].z);
                var py = this.points[i][j].y - this.y,
                    pz = this.points[i][j].z - this.z;
                
                var newy = py*Math.cos(ang)-pz*Math.sin(ang),
                    newz = py*Math.sin(ang)+pz*Math.cos(ang);
                    
                this.points[i][j].y = newy+this.y;
                this.points[i][j].z = newz+this.z;
                //console.log("new y: ", this.points[i][j].y,"\nnew z: ", this.points[i][j].z);
            }
        }
    };
}


function ctxFillCircle(x,y,r,color) {
    var oldFS = this.fillStyle;
    this.fillStyle = color;
    this.beginPath();
    this.arc(x, y, r, 0, 2 * Math.PI, false);
    this.closePath();
    this.fill();
    this.fillStyle = oldFS;
}
function ctxStrokeCircle(x,y,r) {
    this.beginPath();
    this.arc(x, y, r, 0, 2 * Math.PI, false);
    this.closePath();
    this.stroke();
}
function ctxFillCircleGradient(x,y,r,color1,color2) {
    var oldFS = this.fillStyle;
    this.beginPath();
    this.arc(x, y, r, 0, 2 * Math.PI, false);
    this.closePath();
    // create radial gradient
    var grd = this.createRadialGradient(x-r/5, y-r/5, 0, x-r/5, y-r/5, r*0.8);
    // light blue
    grd.addColorStop(0, color1);
    // dark blue
    grd.addColorStop(1, color2);
    this.fillStyle = grd;
    this.fill();
    this.fillStyle = oldFS;
}

var sphere;

$(document).ready(function(){
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    
    Object.getPrototypeOf(ctx).fillCircle = ctxFillCircle;
    Object.getPrototypeOf(ctx).strokeCircle = ctxStrokeCircle;
    Object.getPrototypeOf(ctx).fillCircleGradient = ctxFillCircleGradient;
    
    sphere = new Sphere(parseInt($("#canvas").css("width"))/2,parseInt($("#canvas").css("height"))/2,0,275,ctx,true);
    sphere.Draw();
    
    
    var startX, startY;
    $("#canvas")
    .drag("init", function(e){
        startX = e.clientX;
        startY = e.clientY;
    })
    .drag(function(e){
        if (e.ctrlKey) {
            if (e.altKey) sphere.HiddenFun2(e.clientX-startX,e.clientY-startY);
            else sphere.Zoom(e.clientX-startX, e.clientY-startY, -1);
        } else if (e.shiftKey) {
            if (e.altKey) sphere.HiddenFun1(e.clientX-startX,e.clientY-startY);
            else sphere.Pan(e.clientX-startX,e.clientY-startY);
        } else {        
            sphere.Rotate(e.clientX-startX,e.clientY-startY);
        }
        startX = e.clientX;
        startY = e.clientY;
    });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/6pac-slickgrid/2.4.4/lib/jquery.event.drag-2.3.0.js