jdenticon
Version:
Javascript identicon generator
537 lines (493 loc) • 19.7 kB
JavaScript
/**
* Jdenticon 1.2.0
* http://jdenticon.com
*
* Built: 2015-07-20T07:55:01Z
*
* 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";
var undefined,
/** @const */
version = "1.2.0",
/** @const */
HASH_ATTRIBUTE = "data-jdenticon-hash";
/**
* Represents a color.
* @private
* @constructor
*/
function Color() { }
/**
* @param {number} r Red channel [0, 255]
* @param {number} g Green channel [0, 255]
* @param {number} b Blue channel [0, 255]
* @param {number=} a Alpha [0, 1]
*/
Color.rgb = function (r, g, b, a) {
var color = new Color();
color.s = "rgba(" +
(r & 0xff) + "," +
(g & 0xff) + "," +
(b & 0xff) + "," +
(a === undefined ? 1 : a) + ")";
return color;
};
/**
* @param h Hue [0, 1]
* @param s Saturation [0, 1]
* @param l Lightness [0, 1]
* @param {number=} a Alpha [0, 1]
*/
Color.hsl = function (h, s, l, a) {
var color = new Color();
color.s = "hsla(" +
((h * 360) | 0) + "," +
((s * 100) | 0) + "%," +
((l * 100) | 0) + "%," +
(a === undefined ? 1 : a) + ")";
return color;
};
// This function will correct the lightness for the "dark" hues
Color.correctedHsl = function (h, s, l) {
var correctors = [ 0.95, 1, 1, 1, 0.7, 0.8, 0.8 ];
return Color.hsl(h, s, 1 - correctors[(h * 6 + 0.5) | 0] * (1 - l));
};
Color.prototype = {
toString: function () {
return this.s;
}
};
/**
* 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.noTransform = new Transform(0, 0, 0, 0);
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 ? [right - y - (h || 0), this._y + x] :
this._rotation === 2 ? [right - x - (w || 0), bottom - y - (h || 0)] :
this._rotation === 3 ? [this._x + y, bottom - x - (w || 0)] :
[this._x + x, this._y + y];
}
};
/**
* A wrapper around a context for building paths.
* @private
* @constructor
*/
function Path(ctx, transform) {
this._ctx = ctx;
this._transform = transform || Transform.noTransform;
ctx.beginPath();
}
Path.prototype = {
/**
* Adds a polygon to the path.
* @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,
i = invert ? points.length - 2 : 0,
ctx = this._ctx;
ctx.moveTo.apply(ctx, this._transform.transformPoint(points[i], points[i + 1]));
for (i += di; i < points.length && i >= 0; i += di) {
ctx.lineTo.apply(ctx, this._transform.transformPoint(points[i], points[i + 1]));
}
ctx.closePath();
},
/**
* Adds a polygon to the path.
* 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} w The width of the ellipse.
* @param {number} h The height of the ellipse.
* @param {boolean=} invert Specifies if the ellipse will be inverted.
*/
addEllipse: function (x, y, w, h, invert) {
var ctx = this._ctx,
kappa = .5522848,
p = this._transform.transformPoint(x, y, w, h),
x = p[0],
y = p[1],
ox = (w / 2) * kappa, // control point offset horizontal
oy = (h / 2) * kappa, // control point offset vertical
xe = x + w, // x-end
ye = y + h, // y-end
xm = x + w / 2, // x-middle
ym = y + h / 2; // y-middle
if (invert) {
ye = y;
y = y + h;
oy = -oy;
}
ctx.moveTo(x, ym);
ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
ctx.closePath();
},
/**
* Adds a rectangle to the path.
* @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 path.
* @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 path.
* @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);
},
fill: function () {
this._ctx.fill();
}
};
// SHAPES
/** @const */
var CENTER_SHAPES = [
/** @param {Path} p */
function (p, cell, index) {
var k = cell * 0.42;
p.addPolygon([
0, 0,
cell, 0,
cell, cell - k * 2,
cell - k, cell,
0, cell
]);
},
/** @param {Path} p */
function (p, cell, index) {
var w = 0 | (cell * 0.5),
h = 0 | (cell * 0.8);
p.addTriangle(cell - w, 0, w, h, 2);
},
/** @param {Path} p */
function (p, cell, index) {
var s = 0 | (cell / 3);
p.addRectangle(s, s, cell - s, cell - s);
},
/** @param {Path} p */
function (p, cell, index) {
var inner = 0 | (cell * 0.1),
outer = 0 | (cell * 0.25);
p.addRectangle(outer, outer, cell - inner - outer, cell - inner - outer);
},
/** @param {Path} p */
function (p, cell, index) {
var m = 0 | (cell * 0.15),
s = 0 | (cell * 0.5);
p.addEllipse(cell - s - m, cell - s - m, s, s);
},
/** @param {Path} p */
function (p, cell, index) {
var inner = cell * 0.1,
outer = inner * 4;
p.addRectangle(0, 0, cell, cell);
p.addPolygon([
outer, outer,
cell - inner, outer,
outer + (cell - outer - inner) / 2, cell - inner
], true);
},
/** @param {Path} p */
function (p, cell, index) {
p.addPolygon([
0, 0,
cell, 0,
cell, cell * 0.7,
cell * 0.4, cell * 0.4,
cell * 0.7, cell,
0, cell
]);
},
/** @param {Path} p */
function (p, cell, index) {
p.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3);
},
/** @param {Path} p */
function (p, cell, index) {
p.addRectangle(0, 0, cell, cell / 2);
p.addRectangle(0, cell / 2, cell / 2, cell / 2);
p.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 1);
},
/** @param {Path} p */
function (p, cell, index) {
var inner = 0 | (cell * 0.14),
outer = 0 | (cell * 0.35);
p.addRectangle(0, 0, cell, cell);
p.addRectangle(outer, outer, cell - outer - inner, cell - outer - inner, true);
},
/** @param {Path} p */
function (p, cell, index) {
var inner = cell * 0.12,
outer = inner * 3;
p.addRectangle(0, 0, cell, cell);
p.addEllipse(outer, outer, cell - inner - outer, cell - inner - outer, true);
},
/** @param {Path} p */
function (p, cell, index) {
p.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3);
},
/** @param {Path} p */
function (p, cell, index) {
var m = cell * 0.25;
p.addRectangle(0, 0, cell, cell);
p.addRhombus(m, m, cell - m, cell - m, true);
},
/** @param {Path} p */
function (p, cell, index) {
var m = cell * 0.4, s = cell * 1.2;
if (!index) {
p.addEllipse(m, m, s, s);
}
}
];
/** @const */
var OUTER_SHAPES = [
/** @param {Path} p */
function (p, cell, index) {
p.addTriangle(0, 0, cell, cell, 0);
},
/** @param {Path} p */
function (p, cell, index) {
p.addTriangle(0, cell / 2, cell, cell / 2, 0);
},
/** @param {Path} p */
function (p, cell, index) {
p.addRhombus(0, 0, cell, cell);
},
/** @param {Path} p */
function (p, cell, index) {
var m = cell / 6;
p.addEllipse(m, m, cell - 2 * m, cell - 2 * m);
}
];
/**
* Updates the identicon in the speciifed canvas element.
* @param {number=} padding Optional padding in pixels. Extra padding might be added to center the rendered identicon.
*/
function update(canvas, hash, padding) {
var ctx = (canvas = typeof(canvas) === "string" ? document.querySelector(canvas) : canvas).getContext("2d"),
size = Math.min(canvas.width) * (1 - 2 * (padding === undefined ? 0.08 : padding));
ctx.save();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(
0 | ((canvas.width - size) / 2),
0 | ((canvas.height - size) / 2));
drawIcon(
ctx,
hash || canvas.getAttribute(HASH_ATTRIBUTE),
size);
ctx.restore();
}
/**
* Draws an identicon to a context.
*/
function drawIcon(ctx, hash, size) {
// 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]{10,}$/i.test(hash)) {
throw new Error("Invalid hash passed to Jdenticon.");
}
if (!ctx) {
throw new Error("No canvas specified.");
}
size = size | 0;
var cell = (0 | (size / 8)) * 2;
function renderShape(ctx, shapes, index, rotationIndex, positions) {
var r = rotationIndex ? parseInt(hash.charAt(rotationIndex), 16) : 0,
shape = shapes[parseInt(hash.charAt(index), 16) % shapes.length],
i,
path,
transform;
for (i = 0; i < positions.length; i++) {
transform = new Transform(positions[i][0] * cell, positions[i][1] * cell, cell, r++ % 4);
path = new Path(ctx, transform);
shape(path, cell, i);
path.fill();
}
}
// AVAILABLE COLORS
var hue = parseInt(hash.substr(-7), 16) / 0xfffffff,
// Available colors for this icon
availableColors = [
// Dark gray
Color.rgb(76, 76, 76),
// Mid color
Color.correctedHsl(hue, 0.5, 0.6),
// Light gray
Color.rgb(230, 230, 230),
// Light color
Color.correctedHsl(hue, 0.5, 0.8),
// Dark color
Color.hsl(hue, 0.5, 0.4)
],
// 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);
}
function selectColor(index) {
ctx.fillStyle = availableColors[selectedColorIndexes[index]].toString();
}
// ACTUAL RENDERING
ctx.clearRect(0, 0, size, size);
// Sides
selectColor(0);
renderShape(ctx, OUTER_SHAPES, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
// Corners
selectColor(1);
renderShape(ctx, OUTER_SHAPES, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
// Center
selectColor(2);
renderShape(ctx, CENTER_SHAPES, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
};
/**
* Updates all canvas elements with the data-jdenticon-hash attribute.
*/
function jdenticon() {
var hash,
canvases = "document" in global ? document.getElementsByTagName("canvas") : [];
for (var i = 0; i < canvases.length; i++) {
hash = canvases[i].getAttribute(HASH_ATTRIBUTE);
if (hash) {
update(canvases[i], hash);
}
}
}
jdenticon["drawIcon"] = drawIcon;
jdenticon["update"] = update;
jdenticon["version"] = version;
// Basic jQuery plugin
if (jQuery) {
jQuery["fn"]["jdenticon"] = function (hash, padding) {
this["each"](function (index, el) {
update(el, hash, padding);
});
return this;
};
}
if (typeof setTimeout === "function") {
setTimeout(jdenticon, 0);
}
return jdenticon;
});