An optimized, SVG only version of identicon.js
Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greasyfork.org/scripts/490509/1347118/svg-identicon.js
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global)); })(this, (exports => { class Svg { constructor({ size, shape, margin, border, foreground, background }={}) { this.size = size; this.shape = shape; this.border = border ? ({}).toString.call(border) === '[object String]' ? { width: 1, color: border } : border : border; this.margin = margin; this.foreground = this.color(...foreground); this.background = this.color(...background); this.rectangles = []; const baseMargin = Math.floor(size * this.margin); this.pixel = Math.floor((size - (baseMargin * 2)) / 5); } color(r, g, b, a=1) { return [r, g, b, a].reduce((acc,channel,idx) => { if (idx === 3) { if (channel < 1) { acc += Math.round(channel * 255).toString(16).padStart(2, '0'); } } else { acc += channel.toString(16).padStart(2, '0'); } return acc }, '#') } getDump() { const [fg, bg, stroke, pixel] = [ this.foreground, this.background, this.size * 0.005, this.pixel ]; // https://github.com/ygoe/qrcode-generator/blob/5bb2d93e10/js/qrcode.js#L531-L662 const pointEquals = function (a, b) { return a[0] === b[0] && a[1] === b[1]; }; // Mark all four edges of each square in clockwise drawing direction const edges = []; this.rectangles.forEach(({color, x: x0, y: y0}) => { if (color !== bg) { const x1 = x0 + this.pixel; const y1 = y0 + this.pixel; edges.push([[x0, y0], [x1, y0]]); // top edge (to right) edges.push([[x1, y0], [x1, y1]]); // right edge (down) edges.push([[x1, y1], [x0, y1]]); // bottom edge (to left) edges.push([[x0, y1], [x0, y0]]); // left edge (up) } }); // Edges that exist in both directions cancel each other (connecting the rectangles) for (let i = edges.length - 1; i >= 0; i--) { for (let j = i - 1; j >= 0; j--) { if (pointEquals(edges[i][0], edges[j][1]) && pointEquals(edges[i][1], edges[j][0])) { // First remove index i, it's greater than j edges.splice(i, 1); edges.splice(j, 1); i--; break; } } } let polygons = []; while (edges.length > 0) { // Pick a random edge and follow its connected edges to form a path (remove used edges) // If there are multiple connected edges, pick the first // Stop when the starting point of this path is reached let polygon = []; polygons.push(polygon); let edge = edges.splice(0, 1)[0]; polygon.push(edge[0]); polygon.push(edge[1]); do { let foundEdge = false; for (let i = 0; i < edges.length; i++) { if (pointEquals(edges[i][0], edge[1])) { // Found an edge that starts at the last edge's end foundEdge = true; edge = edges.splice(i, 1)[0]; let p1 = polygon[polygon.length - 2]; // polygon's second-last point let p2 = polygon[polygon.length - 1]; // polygon's current end let p3 = edge[1]; // new point // Extend polygon end if it's continuing in the same direction if (p1[0] === p2[0] && // polygon ends vertical p2[0] === p3[0]) { // new point is vertical, too polygon[polygon.length - 1][1] = p3[1]; } else if (p1[1] === p2[1] && // polygon ends horizontal p2[1] === p3[1]) { // new point is horizontal, too polygon[polygon.length - 1][0] = p3[0]; } else { polygon.push(p3); // new direction } break; } } if (!foundEdge) throw new Error("no next edge found at", edge[1]); } while (!pointEquals(polygon[polygon.length - 1], polygon[0])); // Move polygon's start and end point into a corner if (polygon[0][0] === polygon[1][0] && polygon[polygon.length - 2][0] === polygon[polygon.length - 1][0]) { // start/end is along a vertical line polygon.length--; polygon[0][1] = polygon[polygon.length - 1][1]; } else if (polygon[0][1] === polygon[1][1] && polygon[polygon.length - 2][1] === polygon[polygon.length - 1][1]) { // start/end is along a horizontal line polygon.length--; polygon[0][0] = polygon[polygon.length - 1][0]; } } // Repeat until there are no more unused edges // If two paths touch in at least one point, pick such a point and include one path in the other's sequence of points for (let i = 0; i < polygons.length; i++) { const polygon = polygons[i]; for (let j = 0; j < polygon.length; j++) { const point = polygon[j]; for (let k = i + 1; k < polygons.length; k++) { const polygon2 = polygons[k]; for (let l = 0; l < polygon2.length - 1; l++) { // exclude end point (same as start) const point2 = polygon2[l]; if (pointEquals(point, point2)) { // Embed polygon2 into polygon if (l > 0) { // Touching point is not other polygon's start/end polygon.splice.apply(polygon, [j + 1, 0].concat( polygon2.slice(1, l + 1))); } polygon.splice.apply(polygon, [j + 1, 0].concat( polygon2.slice(l + 1))); polygons.splice(k, 1); k--; break; } } } } } // Generate SVG path data let d = ""; for (let i = 0; i < polygons.length; i++) { const polygon = polygons[i]; d += "M" + polygon[0][0] + "," + polygon[0][1]; for (let j = 1; j < polygon.length; j++) { if (polygon[j][0] === polygon[j - 1][0]) d += "v" + (polygon[j][1] - polygon[j - 1][1]); else d += "h" + (polygon[j][0] - polygon[j - 1][0]); } d += "z"; } let base; switch (this.shape) { case 'rect': { const borderWidth = (this.border ? this.border.width : 0); const origin = this.border ? borderWidth / 2 : 0; const width = this.size - borderWidth; base = `<path fill='${bg}' d='M${origin} ${origin}h${width}v${width}H${origin}z'${this.border ? ` stroke-width='${this.border.width}' stroke='${this.border.color}'` : ''}/>`; break; } case 'circle': { const borderWidth = (this.border ? this.border.width : 0); const width = (this.size / 2); base = `<circle cx='${width}' cy='${width}' r='${width - borderWidth}' fill='${bg}'${this.border ? ` stroke-width='${this.border.width}' stroke='${this.border.color}'` : ''}/>`; break; } default: { throw new Error(`shape must be rect or circle. ${this.shape} is not allowed`); } } return `<svg xmlns='http://www.w3.org/2000/svg' width='${this.size}' height='${this.size}'>${base}<path d='${d}' fill='${fg}' stroke='${fg}' stroke-width='${stroke}' width='${pixel}' height='${pixel}'/></svg>` } getBase64() { return btoa(this.getDump()); } } class Identicon { constructor(hash, options) { if (typeof (hash) !== 'string' || hash.length < 15) { throw 'A hash of at least 15 characters is required.'; } this.defaults = { background: [240, 240, 240, 1], margin: 0.08, size: 64, saturation: 0.7, brightness: 0.5, shape: 'rect', border: false }; this.options = typeof (options) === 'object' ? options : this.defaults; // backward compatibility with old constructor (hash, size, margin) if (typeof (arguments[1]) === 'number') { this.options.size = arguments[1]; } if (arguments[2]) { this.options.margin = arguments[2]; } this.hash = hash; this.background = this.options.background || this.defaults.background; this.size = this.options.size || this.defaults.size; this.shape = this.options.shape || this.defaults.shape; this.border = this.options.border !== undefined ? this.options.border : this.defaults.border; this.margin = this.options.margin !== undefined ? this.options.margin : this.defaults.margin; // foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness const hue = parseInt(this.hash.substring(this.hash.length - 7), 16) / 0xfffffff; const saturation = this.options.saturation || this.defaults.saturation; const brightness = this.options.brightness || this.defaults.brightness; this.foreground = this.options.foreground || this.hsl2rgb(hue, saturation, brightness).map(Math.round); } image() { return new Svg({ size: this.size, shape: this.shape, margin: this.margin, border: this.border, foreground: this.foreground, background: this.background }) } render() { const image = this.image(); const size = this.size; const pixel = image.pixel; const margin = Math.floor((size - pixel * 5) / 2); const bg = image.color.apply(image, this.background); const fg = image.color.apply(image, this.foreground); // the first 15 characters of the hash control the pixels (even/odd) // they are drawn down the middle first, then mirrored outwards for (let i = 0; i < 15; i++) { const color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg; if (i < 5) { this.rectangle({ x: 2 * pixel + margin, y: i * pixel + margin, w: pixel, h: pixel, color, image }); } else if (i < 10) { const y = (i - 5) * pixel + margin; this.rectangle({ x: 1 * pixel + margin, y, w: pixel, h: pixel, color, image }); this.rectangle({ x: 3 * pixel + margin, y, w: pixel, h: pixel, color, image }); } else if (i < 15) { const y = (i - 10) * pixel + margin; this.rectangle({ x: 0 * pixel + margin, y, w: pixel, h: pixel, color, image }); this.rectangle({ x: 4 * pixel + margin, y, w: pixel, h: pixel, color, image }); } } return image; } rectangle({ x, y, w, h, color, image }={}) { image.rectangles.push({ x, y, w, h, color }); } // adapted from: https://gist.github.com/aemkei/1325937 hsl2rgb(h, s, b) { h *= 6; s = [ b += s *= b < .5 ? b : 1 - b, b - h % 1 * s * 2, b -= s *= 2, b, b + h % 1 * s, b + s ]; return [ s[~~h % 6] * 255, // red s[(h | 16) % 6] * 255, // green s[(h | 8) % 6] * 255 // blue ]; } toURI() { return `data:image/svg+xml;base64,${this.render().getBase64()}` } toString(raw) { if (raw) return this.render().getDump(); else return this.render().getBase64(); } } exports.Identicon = Identicon; }));