svg-identicon

An optimized, SVG only version of identicon.js

目前為 2024-03-22 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/490509/1347118/svg-identicon.js

  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  3. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global));
  5. })(this, (exports => {
  6. class Svg {
  7. constructor({
  8. size,
  9. shape,
  10. margin,
  11. border,
  12. foreground,
  13. background
  14. }={}) {
  15. this.size = size;
  16. this.shape = shape;
  17. this.border = border ? ({}).toString.call(border) === '[object String]' ? {
  18. width: 1,
  19. color: border
  20. } : border : border;
  21. this.margin = margin;
  22. this.foreground = this.color(...foreground);
  23. this.background = this.color(...background);
  24. this.rectangles = [];
  25.  
  26. const baseMargin = Math.floor(size * this.margin);
  27. this.pixel = Math.floor((size - (baseMargin * 2)) / 5);
  28. }
  29. color(r, g, b, a=1) {
  30. return [r, g, b, a].reduce((acc,channel,idx) => {
  31. if (idx === 3) {
  32. if (channel < 1) {
  33. acc += Math.round(channel * 255).toString(16).padStart(2, '0');
  34. }
  35. } else {
  36. acc += channel.toString(16).padStart(2, '0');
  37. }
  38. return acc
  39. }, '#')
  40. }
  41. getDump() {
  42. const [fg, bg, stroke, pixel] = [
  43. this.foreground,
  44. this.background,
  45. this.size * 0.005,
  46. this.pixel
  47. ];
  48.  
  49. // https://github.com/ygoe/qrcode-generator/blob/5bb2d93e10/js/qrcode.js#L531-L662
  50. const pointEquals = function (a, b) {
  51. return a[0] === b[0] && a[1] === b[1];
  52. };
  53.  
  54. // Mark all four edges of each square in clockwise drawing direction
  55. const edges = [];
  56. this.rectangles.forEach(({color, x: x0, y: y0}) => {
  57. if (color !== bg) {
  58. const x1 = x0 + this.pixel;
  59. const y1 = y0 + this.pixel;
  60. edges.push([[x0, y0], [x1, y0]]); // top edge (to right)
  61. edges.push([[x1, y0], [x1, y1]]); // right edge (down)
  62. edges.push([[x1, y1], [x0, y1]]); // bottom edge (to left)
  63. edges.push([[x0, y1], [x0, y0]]); // left edge (up)
  64. }
  65. });
  66. // Edges that exist in both directions cancel each other (connecting the rectangles)
  67. for (let i = edges.length - 1; i >= 0; i--) {
  68. for (let j = i - 1; j >= 0; j--) {
  69. if (pointEquals(edges[i][0], edges[j][1]) &&
  70. pointEquals(edges[i][1], edges[j][0])) {
  71. // First remove index i, it's greater than j
  72. edges.splice(i, 1);
  73. edges.splice(j, 1);
  74. i--;
  75. break;
  76. }
  77. }
  78. }
  79.  
  80. let polygons = [];
  81. while (edges.length > 0) {
  82. // Pick a random edge and follow its connected edges to form a path (remove used edges)
  83. // If there are multiple connected edges, pick the first
  84. // Stop when the starting point of this path is reached
  85. let polygon = [];
  86. polygons.push(polygon);
  87. let edge = edges.splice(0, 1)[0];
  88. polygon.push(edge[0]);
  89. polygon.push(edge[1]);
  90. do {
  91. let foundEdge = false;
  92. for (let i = 0; i < edges.length; i++) {
  93. if (pointEquals(edges[i][0], edge[1])) {
  94. // Found an edge that starts at the last edge's end
  95. foundEdge = true;
  96. edge = edges.splice(i, 1)[0];
  97. let p1 = polygon[polygon.length - 2]; // polygon's second-last point
  98. let p2 = polygon[polygon.length - 1]; // polygon's current end
  99. let p3 = edge[1]; // new point
  100. // Extend polygon end if it's continuing in the same direction
  101. if (p1[0] === p2[0] && // polygon ends vertical
  102. p2[0] === p3[0]) { // new point is vertical, too
  103. polygon[polygon.length - 1][1] = p3[1];
  104. }
  105. else if (p1[1] === p2[1] && // polygon ends horizontal
  106. p2[1] === p3[1]) { // new point is horizontal, too
  107. polygon[polygon.length - 1][0] = p3[0];
  108. }
  109. else {
  110. polygon.push(p3); // new direction
  111. }
  112. break;
  113. }
  114. }
  115. if (!foundEdge)
  116. throw new Error("no next edge found at", edge[1]);
  117. }
  118. while (!pointEquals(polygon[polygon.length - 1], polygon[0]));
  119.  
  120. // Move polygon's start and end point into a corner
  121. if (polygon[0][0] === polygon[1][0] &&
  122. polygon[polygon.length - 2][0] === polygon[polygon.length - 1][0]) {
  123. // start/end is along a vertical line
  124. polygon.length--;
  125. polygon[0][1] = polygon[polygon.length - 1][1];
  126. }
  127. else if (polygon[0][1] === polygon[1][1] &&
  128. polygon[polygon.length - 2][1] === polygon[polygon.length - 1][1]) {
  129. // start/end is along a horizontal line
  130. polygon.length--;
  131. polygon[0][0] = polygon[polygon.length - 1][0];
  132. }
  133. }
  134. // Repeat until there are no more unused edges
  135.  
  136. // If two paths touch in at least one point, pick such a point and include one path in the other's sequence of points
  137. for (let i = 0; i < polygons.length; i++) {
  138. const polygon = polygons[i];
  139. for (let j = 0; j < polygon.length; j++) {
  140. const point = polygon[j];
  141. for (let k = i + 1; k < polygons.length; k++) {
  142. const polygon2 = polygons[k];
  143. for (let l = 0; l < polygon2.length - 1; l++) { // exclude end point (same as start)
  144. const point2 = polygon2[l];
  145. if (pointEquals(point, point2)) {
  146. // Embed polygon2 into polygon
  147. if (l > 0) {
  148. // Touching point is not other polygon's start/end
  149. polygon.splice.apply(polygon, [j + 1, 0].concat(
  150. polygon2.slice(1, l + 1)));
  151. }
  152. polygon.splice.apply(polygon, [j + 1, 0].concat(
  153. polygon2.slice(l + 1)));
  154. polygons.splice(k, 1);
  155. k--;
  156. break;
  157. }
  158. }
  159. }
  160. }
  161. }
  162.  
  163. // Generate SVG path data
  164. let d = "";
  165. for (let i = 0; i < polygons.length; i++) {
  166. const polygon = polygons[i];
  167. d += "M" + polygon[0][0] + "," + polygon[0][1];
  168. for (let j = 1; j < polygon.length; j++) {
  169. if (polygon[j][0] === polygon[j - 1][0])
  170. d += "v" + (polygon[j][1] - polygon[j - 1][1]);
  171. else
  172. d += "h" + (polygon[j][0] - polygon[j - 1][0]);
  173. }
  174. d += "z";
  175. }
  176. let base;
  177. switch (this.shape) {
  178. case 'rect': {
  179. const borderWidth = (this.border ? this.border.width : 0);
  180. const origin = this.border ? borderWidth / 2 : 0;
  181. const width = this.size - borderWidth;
  182. 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}'` : ''}/>`;
  183. break;
  184. }
  185. case 'circle': {
  186. const borderWidth = (this.border ? this.border.width : 0);
  187. const width = (this.size / 2);
  188. base = `<circle cx='${width}' cy='${width}' r='${width - borderWidth}' fill='${bg}'${this.border ? ` stroke-width='${this.border.width}' stroke='${this.border.color}'` : ''}/>`;
  189. break;
  190. }
  191. default: {
  192. throw new Error(`shape must be rect or circle. ${this.shape} is not allowed`);
  193. }
  194. }
  195.  
  196. 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>`
  197. }
  198. getBase64() {
  199. return btoa(this.getDump());
  200. }
  201. }
  202.  
  203. class Identicon {
  204. constructor(hash, options) {
  205. if (typeof (hash) !== 'string' || hash.length < 15) {
  206. throw 'A hash of at least 15 characters is required.';
  207. }
  208.  
  209. this.defaults = {
  210. background: [240, 240, 240, 1],
  211. margin: 0.08,
  212. size: 64,
  213. saturation: 0.7,
  214. brightness: 0.5,
  215. shape: 'rect',
  216. border: false
  217. };
  218.  
  219. this.options = typeof (options) === 'object' ? options : this.defaults;
  220.  
  221. // backward compatibility with old constructor (hash, size, margin)
  222. if (typeof (arguments[1]) === 'number') { this.options.size = arguments[1]; }
  223. if (arguments[2]) { this.options.margin = arguments[2]; }
  224.  
  225. this.hash = hash;
  226. this.background = this.options.background || this.defaults.background;
  227. this.size = this.options.size || this.defaults.size;
  228. this.shape = this.options.shape || this.defaults.shape;
  229. this.border = this.options.border !== undefined ? this.options.border : this.defaults.border;
  230. this.margin = this.options.margin !== undefined ? this.options.margin : this.defaults.margin;
  231.  
  232. // foreground defaults to last 7 chars as hue at 70% saturation, 50% brightness
  233. const hue = parseInt(this.hash.substring(this.hash.length - 7), 16) / 0xfffffff;
  234. const saturation = this.options.saturation || this.defaults.saturation;
  235. const brightness = this.options.brightness || this.defaults.brightness;
  236. this.foreground = this.options.foreground || this.hsl2rgb(hue, saturation, brightness).map(Math.round);
  237. }
  238. image() {
  239. return new Svg({
  240. size: this.size,
  241. shape: this.shape,
  242. margin: this.margin,
  243. border: this.border,
  244. foreground: this.foreground,
  245. background: this.background
  246. })
  247. }
  248. render() {
  249. const image = this.image();
  250. const size = this.size;
  251. const pixel = image.pixel;
  252. const margin = Math.floor((size - pixel * 5) / 2);
  253. const bg = image.color.apply(image, this.background);
  254. const fg = image.color.apply(image, this.foreground);
  255.  
  256. // the first 15 characters of the hash control the pixels (even/odd)
  257. // they are drawn down the middle first, then mirrored outwards
  258. for (let i = 0; i < 15; i++) {
  259. const color = parseInt(this.hash.charAt(i), 16) % 2 ? bg : fg;
  260. if (i < 5) {
  261. this.rectangle({
  262. x: 2 * pixel + margin,
  263. y: i * pixel + margin,
  264. w: pixel,
  265. h: pixel,
  266. color,
  267. image
  268. });
  269. } else if (i < 10) {
  270. const y = (i - 5) * pixel + margin;
  271. this.rectangle({
  272. x: 1 * pixel + margin,
  273. y,
  274. w: pixel,
  275. h: pixel,
  276. color,
  277. image
  278. });
  279. this.rectangle({
  280. x: 3 * pixel + margin,
  281. y,
  282. w: pixel,
  283. h: pixel,
  284. color,
  285. image
  286. });
  287. } else if (i < 15) {
  288. const y = (i - 10) * pixel + margin;
  289. this.rectangle({
  290. x: 0 * pixel + margin,
  291. y,
  292. w: pixel,
  293. h: pixel,
  294. color,
  295. image
  296. });
  297. this.rectangle({
  298. x: 4 * pixel + margin,
  299. y,
  300. w: pixel,
  301. h: pixel,
  302. color,
  303. image
  304. });
  305. }
  306. }
  307.  
  308. return image;
  309. }
  310. rectangle({ x, y, w, h, color, image }={}) {
  311. image.rectangles.push({ x, y, w, h, color });
  312. }
  313. // adapted from: https://gist.github.com/aemkei/1325937
  314. hsl2rgb(h, s, b) {
  315. h *= 6;
  316. s = [
  317. b += s *= b < .5 ? b : 1 - b,
  318. b - h % 1 * s * 2,
  319. b -= s *= 2,
  320. b,
  321. b + h % 1 * s,
  322. b + s
  323. ];
  324.  
  325. return [
  326. s[~~h % 6] * 255, // red
  327. s[(h | 16) % 6] * 255, // green
  328. s[(h | 8) % 6] * 255 // blue
  329. ];
  330. }
  331. toURI() {
  332. return `data:image/svg+xml;base64,${this.render().getBase64()}`
  333. }
  334. toString(raw) {
  335. if (raw) return this.render().getDump();
  336. else return this.render().getBase64();
  337. }
  338. }
  339.  
  340. exports.Identicon = Identicon;
  341. }));

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址