UNPKG

isomer

Version:

A simple isometric graphics library for HTML5 canvas

981 lines (776 loc) 23.4 kB
/*! * Isomer v0.2.6 * http://jdan.github.io/isomer/ * * Copyright 2014 Jordan Scales * Released under the MIT license * http://jdan.github.io/isomer/license.txt * * Date: 2016-05-07 */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define(factory); else if(typeof exports === 'object') exports["Isomer"] = factory(); else root["Isomer"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { /** * Entry point for the Isomer API */ module.exports = __webpack_require__(5); /***/ }, /* 1 */ /***/ function(module, exports, __webpack_require__) { function Point(x, y, z) { if (this instanceof Point) { this.x = (typeof x === 'number') ? x : 0; this.y = (typeof y === 'number') ? y : 0; this.z = (typeof z === 'number') ? z : 0; } else { return new Point(x, y, z); } } Point.ORIGIN = new Point(0, 0, 0); /** * Translate a point from a given dx, dy, and dz */ Point.prototype.translate = function(dx, dy, dz) { dx = (typeof dx === 'number') ? dx : 0; dy = (typeof dy === 'number') ? dy : 0; dz = (typeof dz === 'number') ? dz : 0; return new Point( this.x + dx, this.y + dy, this.z + dz); }; /** * Scale a point about a given origin */ Point.prototype.scale = function(origin, dx, dy, dz) { var p = this.translate(-origin.x, -origin.y, -origin.z); if (dy === undefined && dz === undefined) { /* If both dy and dz are left out, scale all coordinates equally */ dy = dz = dx; /* If just dz is missing, set it equal to 1 */ } else { dz = (typeof dz === 'number') ? dz : 1; } p.x *= dx; p.y *= dy; p.z *= dz; return p.translate(origin.x, origin.y, origin.z); }; /** * Rotate about origin on the X axis */ Point.prototype.rotateX = function(origin, angle) { var p = this.translate(-origin.x, -origin.y, -origin.z); var z = p.z * Math.cos(angle) - p.y * Math.sin(angle); var y = p.z * Math.sin(angle) + p.y * Math.cos(angle); p.z = z; p.y = y; return p.translate(origin.x, origin.y, origin.z); }; /** * Rotate about origin on the Y axis */ Point.prototype.rotateY = function(origin, angle) { var p = this.translate(-origin.x, -origin.y, -origin.z); var x = p.x * Math.cos(angle) - p.z * Math.sin(angle); var z = p.x * Math.sin(angle) + p.z * Math.cos(angle); p.x = x; p.z = z; return p.translate(origin.x, origin.y, origin.z); }; /** * Rotate about origin on the Z axis */ Point.prototype.rotateZ = function(origin, angle) { var p = this.translate(-origin.x, -origin.y, -origin.z); var x = p.x * Math.cos(angle) - p.y * Math.sin(angle); var y = p.x * Math.sin(angle) + p.y * Math.cos(angle); p.x = x; p.y = y; return p.translate(origin.x, origin.y, origin.z); }; /** * The depth of a point in the isometric plane */ Point.prototype.depth = function() { /* z is weighted slightly to accomodate |_ arrangements */ return this.x + this.y - 2 * this.z; }; /** * Distance between two points */ Point.distance = function(p1, p2) { var dx = p2.x - p1.x; var dy = p2.y - p1.y; var dz = p2.z - p1.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); }; module.exports = Point; /***/ }, /* 2 */ /***/ function(module, exports, __webpack_require__) { var Point = __webpack_require__(1); /** * Path utility class * * An Isomer.Path consists of a list of Isomer.Point's */ function Path(points) { if (Object.prototype.toString.call(points) === '[object Array]') { this.points = points; } else { this.points = Array.prototype.slice.call(arguments); } } /** * Pushes a point onto the end of the path */ Path.prototype.push = function(point) { this.points.push(point); }; /** * Returns a new path with the points in reverse order */ Path.prototype.reverse = function() { var points = Array.prototype.slice.call(this.points); return new Path(points.reverse()); }; /** * Translates a given path * * Simply a forward to Point#translate */ Path.prototype.translate = function() { var args = arguments; return new Path(this.points.map(function(point) { return point.translate.apply(point, args); })); }; /** * Returns a new path rotated along the X axis by a given origin * * Simply a forward to Point#rotateX */ Path.prototype.rotateX = function() { var args = arguments; return new Path(this.points.map(function(point) { return point.rotateX.apply(point, args); })); }; /** * Returns a new path rotated along the Y axis by a given origin * * Simply a forward to Point#rotateY */ Path.prototype.rotateY = function() { var args = arguments; return new Path(this.points.map(function(point) { return point.rotateY.apply(point, args); })); }; /** * Returns a new path rotated along the Z axis by a given origin * * Simply a forward to Point#rotateZ */ Path.prototype.rotateZ = function() { var args = arguments; return new Path(this.points.map(function(point) { return point.rotateZ.apply(point, args); })); }; /** * Scales a path about a given origin * * Simply a forward to Point#scale */ Path.prototype.scale = function() { var args = arguments; return new Path(this.points.map(function(point) { return point.scale.apply(point, args); })); }; /** * The estimated depth of a path as defined by the average depth * of its points */ Path.prototype.depth = function() { var i, total = 0; for (i = 0; i < this.points.length; i++) { total += this.points[i].depth(); } return total / (this.points.length || 1); }; /** * Some paths to play with */ /** * A rectangle with the bottom-left corner in the origin */ Path.Rectangle = function(origin, width, height) { if (width === undefined) width = 1; if (height === undefined) height = 1; var path = new Path([ origin, new Point(origin.x + width, origin.y, origin.z), new Point(origin.x + width, origin.y + height, origin.z), new Point(origin.x, origin.y + height, origin.z) ]); return path; }; /** * A circle centered at origin with a given radius and number of vertices */ Path.Circle = function(origin, radius, vertices) { vertices = vertices || 20; var i, path = new Path(); for (i = 0; i < vertices; i++) { path.push(new Point( radius * Math.cos(i * 2 * Math.PI / vertices), radius * Math.sin(i * 2 * Math.PI / vertices), 0)); } return path.translate(origin.x, origin.y, origin.z); }; /** * A star centered at origin with a given outer radius, inner * radius, and number of points * * Buggy - concave polygons are difficult to draw with our method */ Path.Star = function(origin, outerRadius, innerRadius, points) { var i, r, path = new Path(); for (i = 0; i < points * 2; i++) { r = (i % 2 === 0) ? outerRadius : innerRadius; path.push(new Point( r * Math.cos(i * Math.PI / points), r * Math.sin(i * Math.PI / points), 0)); } return path.translate(origin.x, origin.y, origin.z); }; /* Expose the Path constructor */ module.exports = Path; /***/ }, /* 3 */ /***/ function(module, exports, __webpack_require__) { function Canvas(elem) { this.elem = elem; this.ctx = this.elem.getContext('2d'); this.width = elem.width; this.height = elem.height; } Canvas.prototype.clear = function() { this.ctx.clearRect(0, 0, this.width, this.height); }; Canvas.prototype.path = function(points, color) { this.ctx.beginPath(); this.ctx.moveTo(points[0].x, points[0].y); for (var i = 1; i < points.length; i++) { this.ctx.lineTo(points[i].x, points[i].y); } this.ctx.closePath(); /* Set the strokeStyle and fillStyle */ this.ctx.save(); this.ctx.globalAlpha = color.a; this.ctx.fillStyle = this.ctx.strokeStyle = color.toHex(); this.ctx.stroke(); this.ctx.fill(); this.ctx.restore(); }; module.exports = Canvas; /***/ }, /* 4 */ /***/ function(module, exports, __webpack_require__) { /** * A color instantiated with RGB between 0-255 * * Also holds HSL values */ function Color(r, g, b, a) { this.r = parseInt(r || 0); this.g = parseInt(g || 0); this.b = parseInt(b || 0); this.a = parseFloat((Math.round(a * 100) / 100 || 1)); this.loadHSL(); }; Color.prototype.toHex = function() { // Pad with 0s var hex = (this.r * 256 * 256 + this.g * 256 + this.b).toString(16); if (hex.length < 6) { hex = new Array(6 - hex.length + 1).join('0') + hex; } return '#' + hex; }; /** * Returns a lightened color based on a given percentage and an optional * light color */ Color.prototype.lighten = function(percentage, lightColor) { lightColor = lightColor || new Color(255, 255, 255); var newColor = new Color( (lightColor.r / 255) * this.r, (lightColor.g / 255) * this.g, (lightColor.b / 255) * this.b, this.a ); newColor.l = Math.min(newColor.l + percentage, 1); newColor.loadRGB(); return newColor; }; /** * Loads HSL values using the current RGB values * Converted from: * http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c */ Color.prototype.loadHSL = function() { var r = this.r / 255; var g = this.g / 255; var b = this.b / 255; var max = Math.max(r, g, b); var min = Math.min(r, g, b); var h, s, l = (max + min) / 2; if (max === min) { h = s = 0; // achromatic } else { var d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } this.h = h; this.s = s; this.l = l; }; /** * Reloads RGB using HSL values * Converted from: * http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c */ Color.prototype.loadRGB = function() { var r, g, b; var h = this.h; var s = this.s; var l = this.l; if (s === 0) { r = g = b = l; // achromatic } else { var q = l < 0.5 ? l * (1 + s) : l + s - l * s; var p = 2 * l - q; r = this._hue2rgb(p, q, h + 1 / 3); g = this._hue2rgb(p, q, h); b = this._hue2rgb(p, q, h - 1 / 3); } this.r = parseInt(r * 255); this.g = parseInt(g * 255); this.b = parseInt(b * 255); }; /** * Helper function to convert hue to rgb * Taken from: * http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c */ Color.prototype._hue2rgb = function(p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; module.exports = Color; /***/ }, /* 5 */ /***/ function(module, exports, __webpack_require__) { var Canvas = __webpack_require__(3); var Color = __webpack_require__(4); var Path = __webpack_require__(2); var Point = __webpack_require__(1); var Shape = __webpack_require__(6); var Vector = __webpack_require__(7); /** * The Isomer class * * This file contains the Isomer base definition */ function Isomer(canvasId, options) { options = options || {}; this.canvas = new Canvas(canvasId); this.angle = Math.PI / 6; this.scale = options.scale || 70; this._calculateTransformation(); this.originX = options.originX || this.canvas.width / 2; this.originY = options.originY || this.canvas.height * 0.9; /** * Light source as defined as the angle from * the object to the source. * * We'll define somewhat arbitrarily for now. */ this.lightPosition = options.lightPosition || new Vector(2, -1, 3); this.lightAngle = this.lightPosition.normalize(); /** * The maximum color difference from shading */ this.colorDifference = 0.20; this.lightColor = options.lightColor || new Color(255, 255, 255); } /** * Sets the light position for drawing. */ Isomer.prototype.setLightPosition = function(x, y, z) { this.lightPosition = new Vector(x, y, z); this.lightAngle = this.lightPosition.normalize(); }; Isomer.prototype._translatePoint = function(point) { /** * X rides along the angle extended from the origin * Y rides perpendicular to this angle (in isometric view: PI - angle) * Z affects the y coordinate of the drawn point */ var xMap = new Point(point.x * this.transformation[0][0], point.x * this.transformation[0][1]); var yMap = new Point(point.y * this.transformation[1][0], point.y * this.transformation[1][1]); var x = this.originX + xMap.x + yMap.x; var y = this.originY - xMap.y - yMap.y - (point.z * this.scale); return new Point(x, y); }; /** * Adds a shape or path to the scene * * This method also accepts arrays */ Isomer.prototype.add = function(item, baseColor) { if (Object.prototype.toString.call(item) == '[object Array]') { for (var i = 0; i < item.length; i++) { this.add(item[i], baseColor); } } else if (item instanceof Path) { this._addPath(item, baseColor); } else if (item instanceof Shape) { /* Fetch paths ordered by distance to prevent overlaps */ var paths = item.orderedPaths(); for (var j = 0; j < paths.length; j++) { this._addPath(paths[j], baseColor); } } }; /** * Adds a path to the scene */ Isomer.prototype._addPath = function(path, baseColor) { /* Default baseColor */ baseColor = baseColor || new Color(120, 120, 120); /* Compute color */ var v1 = Vector.fromTwoPoints(path.points[1], path.points[0]); var v2 = Vector.fromTwoPoints(path.points[2], path.points[1]); var normal = Vector.crossProduct(v1, v2).normalize(); /** * Brightness is between -1 and 1 and is computed based * on the dot product between the light source vector and normal. */ var brightness = Vector.dotProduct(normal, this.lightAngle); var color = baseColor.lighten(brightness * this.colorDifference, this.lightColor); this.canvas.path(path.points.map(this._translatePoint.bind(this)), color); }; /** * Precalculates transformation values based on the current angle and scale * which in theory reduces costly cos and sin calls */ Isomer.prototype._calculateTransformation = function() { this.transformation = [ [ this.scale * Math.cos(this.angle), this.scale * Math.sin(this.angle) ], [ this.scale * Math.cos(Math.PI - this.angle), this.scale * Math.sin(Math.PI - this.angle) ] ]; }; /* Namespace our primitives */ Isomer.Canvas = Canvas; Isomer.Color = Color; Isomer.Path = Path; Isomer.Point = Point; Isomer.Shape = Shape; Isomer.Vector = Vector; /* Expose Isomer API */ module.exports = Isomer; /***/ }, /* 6 */ /***/ function(module, exports, __webpack_require__) { var Path = __webpack_require__(2); var Point = __webpack_require__(1); /** * Shape utility class * * An Isomer.Shape consists of a list of Isomer.Path's */ function Shape(paths) { if (Object.prototype.toString.call(paths) === '[object Array]') { this.paths = paths; } else { this.paths = Array.prototype.slice.call(arguments); } } /** * Pushes a path onto the end of the Shape */ Shape.prototype.push = function(path) { this.paths.push(path); }; /** * Translates a given shape * * Simply a forward to Path#translate */ Shape.prototype.translate = function() { var args = arguments; return new Shape(this.paths.map(function(path) { return path.translate.apply(path, args); })); }; /** * Rotates a given shape along the X axis around a given origin * * Simply a forward to Path#rotateX */ Shape.prototype.rotateX = function() { var args = arguments; return new Shape(this.paths.map(function(path) { return path.rotateX.apply(path, args); })); }; /** * Rotates a given shape along the Y axis around a given origin * * Simply a forward to Path#rotateY */ Shape.prototype.rotateY = function() { var args = arguments; return new Shape(this.paths.map(function(path) { return path.rotateY.apply(path, args); })); }; /** * Rotates a given shape along the Z axis around a given origin * * Simply a forward to Path#rotateZ */ Shape.prototype.rotateZ = function() { var args = arguments; return new Shape(this.paths.map(function(path) { return path.rotateZ.apply(path, args); })); }; /** * Scales a path about a given origin * * Simply a forward to Point#scale */ Shape.prototype.scale = function() { var args = arguments; return new Shape(this.paths.map(function(path) { return path.scale.apply(path, args); })); }; /** * Produces a list of the shape's paths ordered by distance to * prevent overlaps when drawing */ Shape.prototype.orderedPaths = function() { var paths = this.paths.slice(); /** * Sort the list of faces by distance then map the entries, returning * only the path and not the added "further point" from earlier. */ return paths.sort(function(pathA, pathB) { return pathB.depth() - pathA.depth(); }); }; /** * Utility function to create a 3D object by raising a 2D path * along the z-axis */ Shape.extrude = function(path, height) { height = (typeof height === 'number') ? height : 1; var i, topPath = path.translate(0, 0, height); var shape = new Shape(); /* Push the top and bottom faces, top face must be oriented correctly */ shape.push(path.reverse()); shape.push(topPath); /* Push each side face */ for (i = 0; i < path.points.length; i++) { shape.push(new Path([ topPath.points[i], path.points[i], path.points[(i + 1) % path.points.length], topPath.points[(i + 1) % topPath.points.length] ])); } return shape; }; /** * Some shapes to play with */ /** * A prism located at origin with dimensions dx, dy, dz */ Shape.Prism = function(origin, dx, dy, dz) { dx = (typeof dx === 'number') ? dx : 1; dy = (typeof dy === 'number') ? dy : 1; dz = (typeof dz === 'number') ? dz : 1; /* The shape we will return */ var prism = new Shape(); /* Squares parallel to the x-axis */ var face1 = new Path([ origin, new Point(origin.x + dx, origin.y, origin.z), new Point(origin.x + dx, origin.y, origin.z + dz), new Point(origin.x, origin.y, origin.z + dz) ]); /* Push this face and its opposite */ prism.push(face1); prism.push(face1.reverse().translate(0, dy, 0)); /* Square parallel to the y-axis */ var face2 = new Path([ origin, new Point(origin.x, origin.y, origin.z + dz), new Point(origin.x, origin.y + dy, origin.z + dz), new Point(origin.x, origin.y + dy, origin.z) ]); prism.push(face2); prism.push(face2.reverse().translate(dx, 0, 0)); /* Square parallel to the xy-plane */ var face3 = new Path([ origin, new Point(origin.x + dx, origin.y, origin.z), new Point(origin.x + dx, origin.y + dy, origin.z), new Point(origin.x, origin.y + dy, origin.z) ]); /* This surface is oriented backwards, so we need to reverse the points */ prism.push(face3.reverse()); prism.push(face3.translate(0, 0, dz)); return prism; }; Shape.Pyramid = function(origin, dx, dy, dz) { dx = (typeof dx === 'number') ? dx : 1; dy = (typeof dy === 'number') ? dy : 1; dz = (typeof dz === 'number') ? dz : 1; var pyramid = new Shape(); /* Path parallel to the x-axis */ var face1 = new Path([ origin, new Point(origin.x + dx, origin.y, origin.z), new Point(origin.x + dx / 2, origin.y + dy / 2, origin.z + dz) ]); /* Push the face, and its opposite face, by rotating around the Z-axis */ pyramid.push(face1); pyramid.push(face1.rotateZ(origin.translate(dx / 2, dy / 2), Math.PI)); /* Path parallel to the y-axis */ var face2 = new Path([ origin, new Point(origin.x + dx / 2, origin.y + dy / 2, origin.z + dz), new Point(origin.x, origin.y + dy, origin.z) ]); pyramid.push(face2); pyramid.push(face2.rotateZ(origin.translate(dx / 2, dy / 2), Math.PI)); return pyramid; }; Shape.Cylinder = function(origin, radius, vertices, height) { radius = (typeof radius === 'number') ? radius : 1; var circle = Path.Circle(origin, radius, vertices); var cylinder = Shape.extrude(circle, height); return cylinder; }; module.exports = Shape; /***/ }, /* 7 */ /***/ function(module, exports, __webpack_require__) { function Vector (i, j, k) { this.i = (typeof i === 'number') ? i : 0; this.j = (typeof j === 'number') ? j : 0; this.k = (typeof k === 'number') ? k : 0; } /** * Alternate constructor */ Vector.fromTwoPoints = function(p1, p2) { return new Vector(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z); }; Vector.crossProduct = function(v1, v2) { var i = v1.j * v2.k - v2.j * v1.k; var j = -1 * (v1.i * v2.k - v2.i * v1.k); var k = v1.i * v2.j - v2.i * v1.j; return new Vector(i, j, k); }; Vector.dotProduct = function(v1, v2) { return v1.i * v2.i + v1.j * v2.j + v1.k * v2.k; }; Vector.prototype.magnitude = function() { return Math.sqrt(this.i * this.i + this.j * this.j + this.k * this.k); }; Vector.prototype.normalize = function() { var magnitude = this.magnitude(); /** * If the magnitude is 0 then return the zero vector instead of dividing by 0 */ if (magnitude === 0) { return new Vector(0, 0, 0); } return new Vector(this.i / magnitude, this.j / magnitude, this.k / magnitude); }; module.exports = Vector; /***/ } /******/ ]) }); ;