Sources
Main html file: | dhtml/draw3D-auto.html |
ge1doot mini library: | /library/ge1doot.js |
CSS
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;
}
HTML
<canvas id="screen">HTML5 CANVAS 3D Drawing demo</canvas>
JS
/* =======================================================
* ---- 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 = "";
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 = "#fff";
ctx.lineWidth = 10;
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}]);