/** * Jdenticon 1.3.2 * http://jdenticon.com * * Built: 2015-10-10T11:55:57.451Z * * Copyright (c) 2014-2015 Daniel Mester Pirttijärvi * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. * */ /*jslint bitwise: true */ (function (global, name, factory) { var jQuery = global["jQuery"], jdenticon = factory(global, jQuery); // Node.js if (typeof module !== "undefined" && "exports" in module) { module["exports"] = jdenticon; } // RequireJS else if (typeof define === "function" && define["amd"]) { define([], function () { return jdenticon; }); } // No module loader else { global[name] = jdenticon; } })(this, "jdenticon", function (global, jQuery) { "use strict"; /** * Represents a point. * @private * @constructor */ function Point(x, y) { this.x = x; this.y = y; }; /** * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. * @param {number} x The x-coordinate of the upper left corner of the transformed rectangle. * @param {number} y The y-coordinate of the upper left corner of the transformed rectangle. * @param {number} size The size of the transformed rectangle. * @param {number} rotation Rotation specified as 0 = 0 rad, 1 = 0.5π rad, 2 = π rad, 3 = 1.5π rad * @private * @constructor */ function Transform(x, y, size, rotation) { this._x = x; this._y = y; this._size = size; this._rotation = rotation; } Transform.prototype = { /** * Transforms the specified point based on the translation and rotation specification for this Transform. * @param {number} x x-coordinate * @param {number} y y-coordinate * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. */ transformPoint: function (x, y, w, h) { var right = this._x + this._size, bottom = this._y + this._size; return this._rotation === 1 ? new Point(right - y - (h || 0), this._y + x) : this._rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : this._rotation === 3 ? new Point(this._x + y, bottom - x - (w || 0)) : new Point(this._x + x, this._y + y); } }; Transform.noTransform = new Transform(0, 0, 0, 0); /** * Provides helper functions for rendering common basic shapes. * @private * @constructor */ function Graphics(renderer) { this._renderer = renderer; this._transform = Transform.noTransform; } Graphics.prototype = { /** * Adds a polygon to the underlying renderer. * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] * @param {boolean=} invert Specifies if the polygon will be inverted. */ addPolygon: function (points, invert) { var di = invert ? -2 : 2, transform = this._transform, transformedPoints = [], i; for (i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { transformedPoints.push(transform.transformPoint(points[i], points[i + 1])); } this._renderer.addPolygon(transformedPoints); }, /** * Adds a polygon to the underlying renderer. * Source: http://stackoverflow.com/a/2173084 * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. * @param {number} size The size of the ellipse. * @param {boolean=} invert Specifies if the ellipse will be inverted. */ addCircle: function (x, y, size, invert) { var p = this._transform.transformPoint(x, y, size, size); this._renderer.addCircle(p, size, invert); }, /** * Adds a rectangle to the underlying renderer. * @param {number} x The x-coordinate of the upper left corner of the rectangle. * @param {number} y The y-coordinate of the upper left corner of the rectangle. * @param {number} w The width of the rectangle. * @param {number} h The height of the rectangle. * @param {boolean=} invert Specifies if the rectangle will be inverted. */ addRectangle: function (x, y, w, h, invert) { this.addPolygon([ x, y, x + w, y, x + w, y + h, x, y + h ], invert); }, /** * Adds a right triangle to the underlying renderer. * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. * @param {number} w The width of the triangle. * @param {number} h The height of the triangle. * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. * @param {boolean=} invert Specifies if the triangle will be inverted. */ addTriangle: function (x, y, w, h, r, invert) { var points = [ x + w, y, x + w, y + h, x, y + h, x, y ]; points.splice(((r || 0) % 4) * 2, 2); this.addPolygon(points, invert); }, /** * Adds a rhombus to the underlying renderer. * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. * @param {number} w The width of the rhombus. * @param {number} h The height of the rhombus. * @param {boolean=} invert Specifies if the rhombus will be inverted. */ addRhombus: function (x, y, w, h, invert) { this.addPolygon([ x + w / 2, y, x + w, y + h / 2, x + w / 2, y + h, x, y + h / 2 ], invert); } }; var shapes = { center: [ /** @param {Graphics} g */ function (g, cell, index) { var k = cell * 0.42; g.addPolygon([ 0, 0, cell, 0, cell, cell - k * 2, cell - k, cell, 0, cell ]); }, /** @param {Graphics} g */ function (g, cell, index) { var w = 0 | (cell * 0.5), h = 0 | (cell * 0.8); g.addTriangle(cell - w, 0, w, h, 2); }, /** @param {Graphics} g */ function (g, cell, index) { var s = 0 | (cell / 3); g.addRectangle(s, s, cell - s, cell - s); }, /** @param {Graphics} g */ function (g, cell, index) { var inner = 0 | (cell * 0.1), outer = 0 | (cell * 0.25); g.addRectangle(outer, outer, cell - inner - outer, cell - inner - outer); }, /** @param {Graphics} g */ function (g, cell, index) { var m = 0 | (cell * 0.15), s = 0 | (cell * 0.5); g.addCircle(cell - s - m, cell - s - m, s); }, /** @param {Graphics} g */ function (g, cell, index) { var inner = cell * 0.1, outer = inner * 4; g.addRectangle(0, 0, cell, cell); g.addPolygon([ outer, outer, cell - inner, outer, outer + (cell - outer - inner) / 2, cell - inner ], true); }, /** @param {Graphics} g */ function (g, cell, index) { g.addPolygon([ 0, 0, cell, 0, cell, cell * 0.7, cell * 0.4, cell * 0.4, cell * 0.7, cell, 0, cell ]); }, /** @param {Graphics} g */ function (g, cell, index) { g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3); }, /** @param {Graphics} g */ function (g, cell, index) { g.addRectangle(0, 0, cell, cell / 2); g.addRectangle(0, cell / 2, cell / 2, cell / 2); g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 1); }, /** @param {Graphics} g */ function (g, cell, index) { var inner = 0 | (cell * 0.14), outer = 0 | (cell * 0.35); g.addRectangle(0, 0, cell, cell); g.addRectangle(outer, outer, cell - outer - inner, cell - outer - inner, true); }, /** @param {Graphics} g */ function (g, cell, index) { var inner = cell * 0.12, outer = inner * 3; g.addRectangle(0, 0, cell, cell); g.addCircle(outer, outer, cell - inner - outer, true); }, /** @param {Graphics} g */ function (g, cell, index) { g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3); }, /** @param {Graphics} g */ function (g, cell, index) { var m = cell * 0.25; g.addRectangle(0, 0, cell, cell); g.addRhombus(m, m, cell - m, cell - m, true); }, /** @param {Graphics} g */ function (g, cell, index) { var m = cell * 0.4, s = cell * 1.2; if (!index) { g.addCircle(m, m, s); } } ], outer: [ /** @param {Graphics} g */ function (g, cell, index) { g.addTriangle(0, 0, cell, cell, 0); }, /** @param {Graphics} g */ function (g, cell, index) { g.addTriangle(0, cell / 2, cell, cell / 2, 0); }, /** @param {Graphics} g */ function (g, cell, index) { g.addRhombus(0, 0, cell, cell); }, /** @param {Graphics} g */ function (g, cell, index) { var m = cell / 6; g.addCircle(m, m, cell - 2 * m); } ] }; function decToHex(v) { v |= 0; // Ensure integer value return v < 0 ? "00" : v < 16 ? "0" + v.toString(16) : v < 256 ? v.toString(16) : "ff"; } function hueToRgb(m1, m2, h) { h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; return decToHex(255 * ( h < 1 ? m1 + (m2 - m1) * h : h < 3 ? m2 : h < 4 ? m1 + (m2 - m1) * (4 - h) : m1)); } /** * Functions for converting colors to hex-rgb representations. * @private */ var color = { /** * @param {number} r Red channel [0, 255] * @param {number} g Green channel [0, 255] * @param {number} b Blue channel [0, 255] */ rgb: function (r, g, b) { return "#" + decToHex(r) + decToHex(g) + decToHex(b); }, /** * @param h Hue [0, 1] * @param s Saturation [0, 1] * @param l Lightness [0, 1] */ hsl: function (h, s, l) { // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color if (s == 0) { var partialHex = decToHex(l * 255); return "#" + partialHex + partialHex + partialHex; } else { var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s, m1 = l * 2 - m2; return "#" + hueToRgb(m1, m2, h * 6 + 2) + hueToRgb(m1, m2, h * 6) + hueToRgb(m1, m2, h * 6 - 2); } }, // This function will correct the lightness for the "dark" hues correctedHsl: function (h, s, l) { // The corrector specifies the perceived middle lightnesses for each hue var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], corrector = correctors[(h * 6 + 0.5) | 0]; // Adjust the input lightness relative to the corrector l = l < 0.5 ? l * corrector * 2 : corrector + (l - 0.5) * (1 - corrector) * 2; return color.hsl(h, s, l); } }; /** * Gets a set of identicon color candidates for a specified hue and config. */ function colorTheme(hue, config) { return [ // Dark gray color.hsl(0, 0, config.grayscaleLightness(0)), // Mid color color.correctedHsl(hue, config.saturation, config.colorLightness(0.5)), // Light gray color.hsl(0, 0, config.grayscaleLightness(1)), // Light color color.correctedHsl(hue, config.saturation, config.colorLightness(1)), // Dark color color.correctedHsl(hue, config.saturation, config.colorLightness(0)) ]; } /** * Draws an identicon to a specified renderer. */ function iconGenerator(renderer, hash, x, y, size, padding, config) { var undefined; // Calculate padding padding = (size * (padding === undefined ? 0.08 : padding)) | 0; size -= padding * 2; // Sizes smaller than 30 px are not supported. If really needed, apply a scaling transformation // to the context before passing it to this function. if (size < 30) { throw new Error("Jdenticon cannot render identicons smaller than 30 pixels."); } if (!/^[0-9a-f]{11,}$/i.test(hash)) { throw new Error("Invalid hash passed to Jdenticon."); } var graphics = new Graphics(renderer); // Calculate cell size and ensure it is an integer var cell = 0 | (size / 4); // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon x += 0 | (padding + size / 2 - cell * 2); y += 0 | (padding + size / 2 - cell * 2); function renderShape(colorIndex, shapes, index, rotationIndex, positions) { var r = rotationIndex ? parseInt(hash.charAt(rotationIndex), 16) : 0, shape = shapes[parseInt(hash.charAt(index), 16) % shapes.length], i; renderer.beginShape(availableColors[selectedColorIndexes[colorIndex]]); for (i = 0; i < positions.length; i++) { graphics._transform = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); shape(graphics, cell, i); } renderer.endShape(); } // AVAILABLE COLORS var hue = parseInt(hash.substr(-7), 16) / 0xfffffff, // Available colors for this icon availableColors = colorTheme(hue, config), // The index of the selected colors selectedColorIndexes = [], index; function isDuplicate(values) { if (values.indexOf(index) >= 0) { for (var i = 0; i < values.length; i++) { if (selectedColorIndexes.indexOf(values[i]) >= 0) { return true; } } } } for (var i = 0; i < 3; i++) { index = parseInt(hash.charAt(8 + i), 16) % availableColors.length; if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo isDuplicate([2, 3])) { // Disallow light gray and light color combo index = 1; } selectedColorIndexes.push(index); } // ACTUAL RENDERING // Sides renderShape(0, shapes.outer, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); // Corners renderShape(1, shapes.outer, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); // Center renderShape(2, shapes.center, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); }; /** * Represents an SVG path element. * @private * @constructor */ function SvgPath() { /** * This property holds the data string (path.d) of the SVG path. */ this.dataString = ""; } SvgPath.prototype = { /** * Adds a polygon with the current fill color to the SVG path. * @param points An array of Point objects. */ addPolygon: function (points) { var dataString = "M" + points[0].x + " " + points[0].y; for (var i = 1; i < points.length; i++) { dataString += "L" + points[i].x + " " + points[i].y; } this.dataString += dataString + "Z"; }, /** * Adds a circle with the current fill color to the SVG path. * @param {Point} point The upper left corner of the circle bounding box. * @param {number} diameter The diameter of the circle. * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). */ addCircle: function (point, diameter, counterClockwise) { var sweepFlag = counterClockwise ? 0 : 1, radius = diameter / 2; this.dataString += "M" + (point.x) + " " + (point.y + radius) + "a" + radius + "," + radius + " 0 1," + sweepFlag + " " + diameter + ",0" + "a" + radius + "," + radius + " 0 1," + sweepFlag + " " + (-diameter) + ",0"; } }; /** * Renderer producing SVG output. * @private * @constructor */ function SvgRenderer(width, height) { this._pathsByColor = { }; this._size = { w: width, h: height }; } SvgRenderer.prototype = { /** * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. * @param {string} color Fill color on format #xxxxxx. */ beginShape: function (color) { this._path = this._pathsByColor[color] || (this._pathsByColor[color] = new SvgPath()); }, /** * Marks the end of the currently drawn shape. */ endShape: function () { }, /** * Adds a polygon with the current fill color to the SVG. * @param points An array of Point objects. */ addPolygon: function (points) { this._path.addPolygon(points); }, /** * Adds a circle with the current fill color to the SVG. * @param {Point} point The upper left corner of the circle bounding box. * @param {number} diameter The diameter of the circle. * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). */ addCircle: function (point, diameter, counterClockwise) { this._path.addCircle(point, diameter, counterClockwise); }, /** * Gets the rendered image as an SVG string. * @param {boolean=} fragment If true, the container svg element is not included in the result. */ toSvg: function (fragment) { var svg = fragment ? '' : ''; for (var color in this._pathsByColor) { svg += ''; } return fragment ? svg : svg + ''; } }; /** * Renderer redirecting drawing commands to a canvas context. * @private * @constructor */ function CanvasRenderer(ctx, width, height) { this._ctx = ctx; ctx.clearRect(0, 0, width, height); } CanvasRenderer.prototype = { /** * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. * @param {string} color Fill color on format #xxxxxx. */ beginShape: function (color) { this._ctx.fillStyle = color; this._ctx.beginPath(); }, /** * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. */ endShape: function () { this._ctx.fill(); }, /** * Adds a polygon to the rendering queue. * @param points An array of Point objects. */ addPolygon: function (points) { var ctx = this._ctx, i; ctx.moveTo(points[0].x, points[0].y); for (i = 1; i < points.length; i++) { ctx.lineTo(points[i].x, points[i].y); } ctx.closePath(); }, /** * Adds a circle to the rendering queue. * @param {Point} point The upper left corner of the circle bounding box. * @param {number} diameter The diameter of the circle. * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). */ addCircle: function (point, diameter, counterClockwise) { var ctx = this._ctx, radius = diameter / 2; ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); ctx.closePath(); } }; var /** @const */ HASH_ATTRIBUTE = "data-jdenticon-hash", supportsQuerySelectorAll = "document" in global && "querySelectorAll" in document; /** * Gets the normalized current Jdenticon color configuration. Missing fields have default values. */ function getCurrentConfig() { var configObject = jdenticon["config"] || global["jdenticon_config"] || { }, lightnessConfig = configObject["lightness"] || { }, saturation = configObject["saturation"]; /** * Creates a lightness range. */ function lightness(configName, defaultMin, defaultMax) { var range = lightnessConfig[configName] instanceof Array ? lightnessConfig[configName] : [defaultMin, defaultMax]; /** * Gets a lightness relative the specified value in the specified lightness range. */ return function (value) { value = range[0] + value * (range[1] - range[0]); return value < 0 ? 0 : value > 1 ? 1 : value; }; } return { saturation: typeof saturation == "number" ? saturation : 0.5, colorLightness: lightness("color", 0.4, 0.8), grayscaleLightness: lightness("grayscale", 0.3, 0.9) } } /** * Updates the identicon in the specified canvas or svg elements. * @param {string=} hash Optional hash to be rendered. If not specified, the hash specified by the data-jdenticon-hash is used. * @param {number=} padding Optional padding in percents. Extra padding might be added to center the rendered identicon. */ function update(el, hash, padding) { if (typeof(el) === "string") { if (supportsQuerySelectorAll) { var elements = document.querySelectorAll(el); for (var i = 0; i < elements.length; i++) { update(elements[i], hash, padding); } } return; } if (!el || !el["tagName"]) { // No element found return; } hash = hash || el.getAttribute(HASH_ATTRIBUTE); if (!hash) { // No hash specified return; } var isSvg = el["tagName"].toLowerCase() == "svg", isCanvas = el["tagName"].toLowerCase() == "canvas"; // Ensure we have a supported element if (!isSvg && !(isCanvas && "getContext" in el)) { return; } var width = Number(el.getAttribute("width")) || el.clientWidth || 0, height = Number(el.getAttribute("height")) || el.clientHeight || 0, renderer = isSvg ? new SvgRenderer(width, height) : new CanvasRenderer(el.getContext("2d"), width, height), size = Math.min(width, height); // Draw icon iconGenerator(renderer, hash, 0, 0, size, padding, getCurrentConfig()); // SVG needs postprocessing if (isSvg) { // Parse svg to a temporary span element. // Simply using innerHTML does unfortunately not work on IE. var wrapper = document.createElement("span"); wrapper.innerHTML = renderer.toSvg(false); // Then replace the content of the target element with the parsed svg. while (el.firstChild) { el.removeChild(el.firstChild); } var newNodes = wrapper.firstChild.childNodes; while (newNodes.length) { el.appendChild(newNodes[0]); } // Set viewBox attribute to ensure the svg scales nicely. el.setAttribute("viewBox", "0 0 " + width + " " + height); } } /** * Draws an identicon to a context. */ function drawIcon(ctx, hash, size) { if (!ctx) { throw new Error("No canvas specified."); } var renderer = new CanvasRenderer(ctx, size, size); iconGenerator(renderer, hash, 0, 0, size, 0, getCurrentConfig()); } /** * Draws an identicon to a context. * @param {number=} padding Optional padding in percents. Extra padding might be added to center the rendered identicon. */ function toSvg(hash, size, padding) { var renderer = new SvgRenderer(size, size); iconGenerator(renderer, hash, 0, 0, size, padding, getCurrentConfig()); return renderer.toSvg(); } /** * Updates all canvas elements with the data-jdenticon-hash attribute. */ function jdenticon() { if (supportsQuerySelectorAll) { update("svg[" + HASH_ATTRIBUTE + "],canvas[" + HASH_ATTRIBUTE + "]"); } } // Public API jdenticon["drawIcon"] = drawIcon; jdenticon["toSvg"] = toSvg; jdenticon["update"] = update; jdenticon["version"] = "1.3.2"; // Basic jQuery plugin if (jQuery) { jQuery["fn"]["jdenticon"] = function (hash, padding) { this["each"](function (index, el) { update(el, hash, padding); }); return this; }; } // Schedule to render all identicons on the page once it has been loaded. if (typeof setTimeout === "function") { setTimeout(jdenticon, 0); } return jdenticon; });