<canvas id="screen">HTML5 CANVAS 3D Drawing demo</canvas>
html {
  overflow: hidden;
  -ms-touch-action: none;
  -ms-content-zooming: none;
}
body {
  position: absolute;
  margin: 0;
  padding: 0;
  background: #000;
  width: 100%;
  height: 100%;
}
#screen {
  position: absolute;
  width: 100%;
  height: 100%;
  cursor: pointer;
}
/* =======================================================
 *  ---- HTML5 CANVAS 3D drawing ----
 * script: Gerard Ferrandez - 7 February 2013
 * Released under the MIT license
 * http://www.dhteumeuleu.com/LICENSE.html
 * ======================================================= */
 
"use strict";

(function () {
  // ==== private variables =====
  var scr, ctx, pointer;
  var shapes = [];
  var sparks = [];
  var sparkId = 0;
  var fov = 650;
  var globalZ = 0;
  var xm = 0;
  var ym = 0;
  var auto = true;
  var currentShape;
  var start = true;
  // ==== spark object ====
  var Spark = function (x, y) {
    this.x = x;
    this.y = y;
    this.sx = Math.random() - 0.5;
    this.sy = 5 + Math.random() * 10;
  }
  // ==== draw sparks ====
  Spark.prototype.draw = function () {
    if (this.y < scr.height) {
      this.x += this.sx;
      this.y += this.sy;
      ctx.moveTo(this.x, this.y - 2);
      ctx.lineTo(this.x, this.y);
    }
  }
  // ==== shape object ====
  var Shape = function () {
    this.points = [];
    this.length = 0;
    this.filled = false;
    this.color = "red";
    this.angle = 0;
    this.fov = fov;
    return this;
  }
  // ==== add point ====
  Shape.prototype.addPoint = function (x, y, z) {
    this.points.push(
      new Point(Math.round(x), Math.round(y), Math.round(z))
    );
    this.length++;
    if (Math.random() > 0.5) {
      sparks[sparkId++] = new Spark(x + scr.width * 0.5, y + scr.height * 0.5);
      if (sparkId == 100) sparkId = 0;
    }
  }
  // ==== rotate shape ====
  Shape.prototype.rotate = function () {
    // ---- increment angle ----
    this.angle += Math.PI / 180;
    var ax = Math.cos(this.angle);
    var ay = Math.sin(this.angle);
    // ---- points rotation ----
    for (var i = 0; i < this.length; i++) {
      this.points[i].rotate(ax, ay);
    }
  }
  // ==== draw shape ====
  Shape.prototype.draw = function () {
    // ---- 3D to 2D points projection ----
    for (var i = 0; i < this.length; i++) {
      this.points[i].project(this.fov);
    }
    // ---- draw smooth curve through N points ----
    var p0 = this.points[0];
    var lf = scr.width * 0.5;
    var tp = scr.height * 0.5;
    ctx.beginPath();
    ctx.moveTo(Math.random() * 3 - 1.5 + p0.xp + lf, Math.random() * 3 - 1.5 + p0.yp + tp);
    for (var i = 1, l = this.points.length; i < l; i++) {
      var p1 = this.points[i];
      var xc = Math.random() * 3 - 1.5 + (p0.xp + p1.xp) / 2;
      var yc = Math.random() * 3 - 1.5 + (p0.yp + p1.yp) / 2;
      ctx.quadraticCurveTo(p0.xp + lf, p0.yp + tp, xc + lf, yc + tp);
      p0 = p1;
    }
    // ---- paint ----
    ctx.strokeStyle = '#' + (function co(lor){   return (lor += [0,1,2,3,4,5,6,7,8,9,'a','b','c','d','e','f'][Math.floor(Math.random()*16)])
  && (lor.length == 6) ?  lor : co(lor); })('');
   
    
    ctx.lineWidth = 5;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    if (this.filled) {
      ctx.closePath();
      ctx.fillStyle = this.color;
      ctx.fill();
    }
    ctx.stroke();
  }
  // ==== point object ====
  var Point = function (x, y, z) {
    this.x  = x;
    this.y  = y;
    this.z  = z;
    this.x0 = x;
    this.z0 = z;
    this.xp = 0;
    this.yp = 0;
    this.zp = 0;
  }
  // ==== 3D to 2D point projection ====
  Point.prototype.project = function (sfov) {
    this.zp = sfov / (sfov + this.z);
    this.xp = this.x * this.zp;
    this.yp = this.y * this.zp;
  }
  // ==== rotate point ====
  Point.prototype.rotate = function (ax, ay) {
    this.x = Math.round(this.x0 *  ax + this.z0 * ay);
    this.z = Math.round(this.x0 * -ay + this.z0 * ax);
  }
  // ==== painting pointer ====
  var movePointer = function () {
    if (pointer.isDown) {
      var dx = xm - pointer.X;
      var dy = ym - pointer.Y;
      var d = Math.sqrt(dx * dx + dy * dy);
      if (d > 10) {
        if (!currentShape) {
          if (start) {
            start = false;
            shapes.length = 0;
          }
          shapes.push(
            currentShape = new Shape()
          );
        }
        var z = fov / (fov + globalZ);
        currentShape.addPoint(
          (pointer.X - scr.width  * 0.5) / z, 
          (pointer.Y - scr.height * 0.5) / z, 
          globalZ
        );
        xm = pointer.X;
        ym = pointer.Y;
        // ---- closing shape ----
        currentShape.filled = false;
        currentShape.color = "";
        var first = currentShape.points[0];
        var last  = currentShape.points[currentShape.length - 1];
        var dx = last.x - first.x;
        var dy = last.y - first.y;
        var dz = last.z - first.z;
        var d = Math.sqrt(dx * dx + dy * dy + dz * dz);
        if (d < 15) {
          if (currentShape.length > 4) {
            currentShape.color = 'hsla(' + Math.round(Math.random() * 360) + ', 90%, 60%, 0.2)';
            currentShape.filled = true;
          }
        }
      }
    } else {
      // ---- up ----
      if (currentShape) {
        currentShape = false;
      }
      // ---- rotate ----
      if (auto) {
        var i = 0, s;
        while ( s = shapes[i++]) s.rotate();
      }
    }
  }
  // ==== save drawing ====
  var save = function (id) {
    // ---- clean up ----
    var array = shapes.slice(0);
    for (var i = 0; i < array.length; i++) {
      delete array[i].angle;
      var pts = array[i].points;
      for (var j = 0; j < pts.length; j++) {
        var p = pts[j];
        for (var k in p) {
          if (k.length != 1) delete p[k];
        }
      }
    }
    // ---- save json to local storage ----
    var a = JSON.stringify(array);
    window.localStorage.setItem(id, a);
    // ---- re-load ----
    load(id);
  }
  // ==== load drawing ====
  var load = function (id) {
    // ---- clear all ----
    shapes.length = 0;
    // ---- load ----
    var array = JSON.parse(window.localStorage.getItem(id));
    // ---- rebuild objects ----
    build(array);
  }
  // ==== inject data ====
  var build = function(array) {
    if (array) {
      for (var i = 0; i < array.length; i++) {
        shapes.push(
          currentShape = new Shape()
        );
        var p = array[i].points;
        for (var j = 0; j < p.length; j++) {
          currentShape.points.push(
            new Point(p[j].x, p[j].y, p[j].z)
          );
        }
        currentShape.length = array[i].length;
        currentShape.filled = array[i].filled;
        currentShape.color  = array[i].color;
      }
    }
  }
  // ==== init script ====
  var init = function (json) {
    // ---- screen ----
    scr = new ge1doot.Screen({
      container: "screen",
      resize: function () {
        fov = Math.round(scr.width * 0.5);
      }
    });
    scr.resize();
    ctx = scr.ctx;
    // ---- pointer events ----
    pointer = new ge1doot.Pointer({ });
    // ---- some key events ----
    document.body.onkeydown = function (e) {
      // ---- storage detection ----
      var storage = typeof window.localStorage == 'object';
      // ---- hold/release rotation [SPACE] ----
      if (e.keyCode == 32) {
        auto = !auto;
      }
      // ---- undo shapes [DEL] ----
      if (e.keyCode == 46) {
        if (shapes.length > 0) {
          shapes.length--;
        }
      }
      // ---- switch global Z [Z] ----
      if (e.keyCode == 90) {
        if (globalZ == 0) globalZ = fov * 0.35; else globalZ = 0;
      }
      // ---- save/load [S/L]----
      if (e.keyCode == 83 && storage) save("circumscrible");
      if (e.keyCode == 76 && storage) load("circumscrible");
      return false;
    }
    // ---- intro drawing ----
    build(json);
    // ---- engine start ----
    run();
  }
  // ======== main loop ========
  var run = function () {
    ctx.clearRect(0, 0, scr.width, scr.height);
    movePointer();
    // ---- draw shapes ----
    var i = 0, s;
    while ( s = shapes[i++]) {
      s.draw();
    }
    // ---- sparks ----
    ctx.beginPath();
    var i = 0, s;
    while ( s = sparks[i++]) {
      s.draw();
    }
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#fff";
    ctx.stroke();
    // ---- animation loop ----
    requestAnimFrame(run);
  }
  return {
    // ---- onload event ----
    load : function (json) {
      window.addEventListener('load', function () {
        init(json);
      }, false);
    }
  }
})().load([{"points":[{"x":10,"y":-18,"z":-80},{"x":10,"y":-8,"z":-82},{"x":10,"y":2,"z":-83},{"x":10,"y":12,"z":-85},{"x":11,"y":22,"z":-86},{"x":11,"y":33,"z":-87}],"length":6,"filled":false,"color":"","fov":506},{"points":[{"x":10,"y":-21,"z":-81},{"x":9,"y":-23,"z":-71},{"x":7,"y":-22,"z":-61},{"x":6,"y":-15,"z":-52},{"x":6,"y":-5,"z":-47},{"x":6,"y":6,"z":-47},{"x":6,"y":17,"z":-49},{"x":7,"y":26,"z":-57},{"x":8,"y":28,"z":-67},{"x":9,"y":29,"z":-76},{"x":11,"y":29,"z":-87}],"length":11,"filled":false,"color":"","fov":506},{"points":[{"x":4,"y":-30,"z":-29},{"x":4,"y":-18,"z":-29},{"x":4,"y":-8,"z":-30},{"x":4,"y":5,"z":-30},{"x":4,"y":16,"z":-30},{"x":4,"y":27,"z":-30}],"length":6,"filled":false,"color":"","fov":506},{"points":[{"x":4,"y":-30,"z":-31},{"x":2,"y":-33,"z":-20},{"x":1,"y":-32,"z":-9},{"x":0,"y":-25,"z":0},{"x":0,"y":-15,"z":1},{"x":1,"y":-8,"z":-9},{"x":3,"y":-8,"z":-22},{"x":2,"y":3,"z":-17},{"x":1,"y":13,"z":-10},{"x":0,"y":22,"z":-3},{"x":0,"y":32,"z":1}],"length":11,"filled":false,"color":"","fov":506},{"points":[{"x":-4,"y":-29,"z":29},{"x":-3,"y":-18,"z":25},{"x":-2,"y":-7,"z":20},{"x":-2,"y":3,"z":16},{"x":-1,"y":13,"z":12},{"x":-1,"y":23,"z":10}],"length":6,"filled":false,"color":"","fov":506},{"points":[{"x":-4,"y":-34,"z":29},{"x":-4,"y":-22,"z":33},{"x":-4,"y":-10,"z":36},{"x":-5,"y":2,"z":38},{"x":-5,"y":13,"z":41},{"x":-5,"y":23,"z":43},{"x":-6,"y":33,"z":46}],"length":7,"filled":false,"color":"","fov":506},{"points":[{"x":-2,"y":2,"z":17},{"x":-3,"y":2,"z":28}],"length":2,"filled":false,"color":"","fov":506},{"points":[{"x":-2,"y":5,"z":13},{"x":-3,"y":3,"z":24},{"x":-4,"y":3,"z":35}],"length":3,"filled":false,"color":"","fov":506},{"points":[{"x":-6,"y":-31,"z":49},{"x":-6,"y":-20,"z":52},{"x":-7,"y":-10,"z":55},{"x":-7,"y":1,"z":59},{"x":-8,"y":12,"z":62},{"x":-8,"y":22,"z":66},{"x":-9,"y":12,"z":73},{"x":-10,"y":2,"z":77},{"x":-11,"y":7,"z":86},{"x":-11,"y":18,"z":91},{"x":-12,"y":28,"z":95},{"x":-13,"y":15,"z":104},{"x":-13,"y":5,"z":108},{"x":-14,"y":-12,"z":115},{"x":-15,"y":-22,"z":119},{"x":-15,"y":-33,"z":124}],"length":16,"filled":false,"color":"","fov":506}]);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. http://www.dhteumeuleu.com/library/ge1doot.js