Greasy Fork is available in English.

svg-identicon

An optimized, SVG only version of identicon.js

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @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;
}));