UNPKG

gun-avatar

Version:

Avatar generator for GUN public keys

1,048 lines (897 loc) 34.3 kB
import { ref } from 'vue'; import extract from 'png-chunks-extract'; import encode from 'png-chunks-encode'; import text from 'png-chunk-text'; /** * MIT License - Copyright (c) 2021 Kaiido * * A monkey-patch for Safari's drawImage. * * This browser doesn't handle well using the cropping abilities of drawImage * with out-of-bounds values. * (see https://stackoverflow.com/questions/35500999/cropping-with-drawimage-not-working-in-safari) * This script takes care of detecting when the monkey-patch is needed, * and does redefine the cropping parameters so they fall inside the source's boundaries. * **/ var patchSafari = () => { if (typeof window == 'undefined' || typeof window.CanvasRenderingContext2D == 'undefined') return if (!needPoly()) return const proto = CanvasRenderingContext2D.prototype; const original = proto.drawImage; if (!original) { console.error('This script requires a basic implementation of drawImage'); return } proto.drawImage = function drawImage(source, x, y) { // length: 3 const will_crop = arguments.length === 9; if (!will_crop) { return original.apply(this, [...arguments]) } const safe_rect = getSafeRect(...arguments); if (isEmptyRect(safe_rect)) { return } return original.apply(this, safe_rect) }; function needPoly() { const ctx = document.createElement('canvas').getContext('2d'); ctx.fillRect(0, 0, 40, 40); ctx.drawImage(ctx.canvas, -40, -40, 80, 80, 50, 50, 20, 20); const img = ctx.getImageData(50, 50, 30, 30); // 10px around expected square const data = new Uint32Array(img.data.buffer); const colorAt = (x, y) => data[y * img.width + x]; const transparents = [ [9, 9], [20, 9], [9, 20], [20, 20], ]; const blacks = [ [10, 10], [19, 10], [10, 19], [19, 19], ]; return ( transparents.some(([x, y]) => colorAt(x, y) !== 0x00000000) || blacks.some(([x, y]) => colorAt(x, y) === 0x00000000) ) } function getSafeRect(image, sx, sy, sw, sh, dx, dy, dw, dh) { const { width, height } = getSourceDimensions(image); if (sw < 0) { sx += sw; sw = Math.abs(sw); } if (sh < 0) { sy += sh; sh = Math.abs(sh); } if (dw < 0) { dx += dw; dw = Math.abs(dw); } if (dh < 0) { dy += dh; dh = Math.abs(dh); } const x1 = Math.max(sx, 0); const x2 = Math.min(sx + sw, width); const y1 = Math.max(sy, 0); const y2 = Math.min(sy + sh, height); const w_ratio = dw / sw; const h_ratio = dh / sh; return [ image, x1, y1, x2 - x1, y2 - y1, sx < 0 ? dx - sx * w_ratio : dx, sy < 0 ? dy - sy * h_ratio : dy, (x2 - x1) * w_ratio, (y2 - y1) * h_ratio, ] } function isEmptyRect(args) { // sw, sh, dw, dh return [3, 4, 7, 8].some((index) => !args[index]) } function getSourceDimensions(source) { const sourceIs = (type) => { const constructor = globalThis[type]; return constructor && source instanceof constructor }; if (sourceIs('HTMLImageElement')) { return { width: source.naturalWidth, height: source.naturalHeight } } else if (sourceIs('HTMLVideoElement')) { return { width: source.videoWidth, height: source.videoHeight } } else if (sourceIs('SVGImageElement')) { throw new TypeError( "SVGImageElement isn't yet supported as source image.", 'UnsupportedError', ) } else if (sourceIs('HTMLCanvasElement') || sourceIs('ImageBitmap')) { return source } } }; // https://datatracker.ietf.org/doc/html/rfc4648#section-5 const symbols = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; function fromB64(x) { return x.split("").reduce((s, v) => s * 64 + symbols.indexOf(v), 0) } function decodeUrlSafeBase64(st) { const symbolArray = symbols.split(""); let arr = []; let i = 0; for (let letter of st) { arr[i++] = symbolArray.indexOf(letter) / 64; } return arr; } function validatePub(pub) { return pub && typeof pub === 'string' && pub.length === 87 && pub.split('.').length === 2; } function parsePub(pub) { const split = pub.split("."); const decoded = split.map(single => decodeUrlSafeBase64(single)); const finals = decoded.map(d => d[42]); const averages = decoded.map(e => e.reduce((acc, d) => acc + d) / e.length); const angles = split.map(part => fromB64(part) % 360); const colors = split.map((s, i) => `hsl(${angles[i]} ${finals[i] * 100}% ${averages[i] * 100}%)`); return { finals, decoded, angles, averages, colors } } function chunkIt(list, chunkSize = 3) { return [...Array(Math.ceil(list.length / chunkSize))].map(() => list.splice(0, chunkSize) ); } const error = ref(null); function canvasToBuffer(canvas) { try { const base64 = canvas.toDataURL('image/png').split(',')[1]; const bytes = atob(base64); const buffer = new Uint8Array(bytes.length); for (let i = 0; i < bytes.length; i++) { buffer[i] = bytes.charCodeAt(i); } return buffer } catch (e) { error.value = 'Failed to convert canvas to buffer: ' + e.message; return null } } function embedInSvg(svgString, data) { try { const metadata = `<metadata> <gun-data>${JSON.stringify(data)}</gun-data> </metadata>`; return svgString.replace('</svg>', `${metadata}</svg>`) } catch (e) { error.value = 'Failed to embed data in SVG: ' + e.message; return null } } function extractFromSvg(svgString) { console.log(svgString); try { const metadataMatch = svgString.match(/<metadata>\s*<gun-data>(.*?)<\/gun-data>\s*<\/metadata>/s); if (!metadataMatch || !metadataMatch[1]) { error.value = 'No embedded data found in SVG'; return null } return JSON.parse(metadataMatch[1]) } catch (e) { error.value = 'Failed to extract data from SVG: ' + e.message; return null } } function embedInPNG(canvas, data) { try { const buffer = canvasToBuffer(canvas); if (!buffer) return null const chunks = extract(buffer); chunks.splice(-1, 0, text.encode('message', JSON.stringify(data))); return encode(chunks) } catch (e) { error.value = 'Failed to embed data: ' + e.message; return null } } function extractFromBuffer(buffer, type = 'image/png') { if (type === 'image/svg+xml') { const text = new TextDecoder().decode(buffer); return extractFromSvg(text) } // Default PNG handling try { const chunks = extract(buffer); const textChunks = chunks .filter(chunk => chunk.name === 'tEXt') .map(chunk => text.decode(chunk.data)); const messageChunk = textChunks.find(chunk => chunk.keyword === 'message'); return messageChunk ? JSON.parse(messageChunk.text) : null } catch (e) { error.value = e.message; return null } } async function extractFromFile(file) { try { if (!(file instanceof File)) { throw new Error('Input must be a File object') } const arrayBuffer = await file.arrayBuffer(); const buffer = new Uint8Array(arrayBuffer); return await extractFromBuffer(buffer, file.type) } catch (e) { error.value = e.message; return null } } function renderCanvasAvatar({ pub, size, dark, draw, reflect, round, embed, p3 }) { const canvas = document.createElement("canvas"); canvas.width = canvas.height = size; const ctx = canvas.getContext("2d"); const { decoded, finals } = parsePub(pub); drawGradient({ ctx, top: finals[0], bottom: finals[1], size, dark }); if (draw == "squares") { ctx.filter = "blur(20px)"; drawSquares(decoded[0], ctx, size, p3); ctx.filter = "blur(0px)"; ctx.globalCompositeOperation = "color-burn"; drawSquares(decoded[1], ctx, size, p3); } else { drawCircles(decoded[0], ctx, size, 0.42 * size, p3); ctx.globalCompositeOperation = "multiply"; drawCircles(decoded[1], ctx, size, 0.125 * size, p3); } if (reflect) { ctx.globalCompositeOperation = "source-over"; ctx.scale(-1, 1); ctx.translate(-size / 2, 0); ctx.drawImage(canvas, size / 2, 0, size, size, 0, 0, size, size); // Reset transformation matrix after reflection ctx.setTransform(1, 0, 0, 1, 0, 0); } if (round) { // Store the current canvas content const imageData = ctx.getImageData(0, 0, size, size); ctx.clearRect(0, 0, size, size); // Fill with background color first ctx.fillStyle = dark ? '#cccccc' : '#ffffff'; ctx.fillRect(0, 0, size, size); // Draw original image back ctx.putImageData(imageData, 0, 0); // Create circular mask ctx.globalCompositeOperation = 'destination-in'; ctx.beginPath(); ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); ctx.closePath(); ctx.fill(); } let image = canvas.toDataURL("image/png"); if (embed) { const embedData = { pub, content: embed }; const embedBuffer = embedInPNG(canvas, embedData); if (embedBuffer) { const blob = new Blob([embedBuffer], { type: 'image/png' }); image = URL.createObjectURL(blob); } } return image; } function drawGradient({ ctx, top = 0, bottom = 150, size = 200, dark = false }) { const gradient = ctx.createLinearGradient(0, 0, 0, size); const offset = dark ? 0 : 70; gradient.addColorStop(0, `hsl(0,0%,${offset + top * 30}%)`); gradient.addColorStop(1, `hsl(0,0%,${offset + bottom * 30}%)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, size, size); } function drawSquares(data, ctx, size, p3) { chunkIt(data, 14).forEach(chunk => { if (chunk.length === 14) { let [x, y, rRaw, h1, s1, l1, a1, x1, h2, s2, l2, a2, x2, angle] = chunk; let r = size / 8 + rRaw * size * (7 / 8); const gradient = ctx.createLinearGradient( x * size + r * x1, 0, x * size + r * x2, size ); gradient.addColorStop(0, p3 ? `color(display-p3 ${h1} ${s1} ${l1} / ${a1})` : `rgba(${h1 * 255}, ${s1 * 255}, ${l1 * 255}, ${a1})`); gradient.addColorStop(1, p3 ? `color(display-p3 ${h2} ${s2} ${l2} / ${a2})` : `rgba(${h2 * 255}, ${s2 * 255}, ${l2 * 255}, ${a2})`); ctx.fillStyle = gradient; ctx.translate(x * size, y * size); ctx.rotate(angle * Math.PI); ctx.fillRect(-r / 2, -r / 2, r, r); ctx.setTransform(1, 0, 0, 1, 0, 0); } }); } function drawCircles(data, ctx, size, radius, p3) { chunkIt(data, 7).forEach(chunk => { if (chunk.length === 7) { let [x, y, r, h, s, l, a] = chunk; ctx.beginPath(); ctx.arc( size / 2 + (x * size) / 2, y * size, r * radius, 0, 2 * Math.PI ); ctx.fillStyle = p3 ? `color(display-p3 ${h} ${s} ${l} / ${a})` : `rgba(${h * 255}, ${s * 255}, ${l * 255}, ${a})`; ctx.closePath(); ctx.fill(); } }); } function interactiveScriptGen({ size = 300, reflect = true, follow = true, finals = [0.5, 0.5], averages = [0.5, 0.5], breathVert = 0.03 } = {}) { return ` <script type="text/javascript"><![CDATA[ // Compact state & precomputed constants const s = { e: [], // elements p: { x: null, y: null, tx: null, ty: null, a: false, lt: 0, lit: 0 }, // pointer state r: null, // rect a: false, // pulse active pt: 0, // pulse time pv: 0, // pulse value t: 0 // time }, c = { sz: ${size}, cx: ${size / 2}, cy: ${size / 2}, pz: ${size} * 1.2, bz: ${size} * 0.4 * ${finals[1]}, pr: ${size} * (0.5 + ${finals[0]}/4), mv: (0.1 + 0.05*${averages[0]}) * Math.max(0.8, Math.min(4.4, 200/${size})), bs: 0.1 * Math.max(0.8, Math.min(1.4, 200/${size})), bv: ${breathVert} * Math.max(0.8, Math.min(1.4, 200/${size})), bd: 4500 + ${averages[0]}*1000, pa: 0.6 * Math.max(0.8, Math.min(1.4, 200/${size})), pr: 200, pf: 700, rf: ${follow}, d: 900, td: 1500000, pp: 0.5+ 0.5*${averages[1]} }; // Init using IIFE (() => { const svg = document.currentScript?.closest('svg'); if (!svg) return; // Update rect and get element collections const ur = () => s.r = svg.getBoundingClientRect(); ur(); // Cache elements for better performance s.e = [ ...Array.from(svg.querySelectorAll('.interactive-circle')).map(el => { const x = +el.getAttribute('data-cx'); const y = +el.getAttribute('data-cy'); const r = +el.getAttribute('r'); const o = +el.getAttribute('data-opacity'); const m = Math.pow(r / (c.sz * 0.05), 0.7) * o; const mr = el.nextElementSibling && el.nextElementSibling.getAttribute('cx') === (c.sz - x).toString() ? el.nextElementSibling : null; return { el, t: 'c', x, y, r, cx: x, cy: y, m, mr, o: Math.random() * Math.PI * 2 }; }), ...Array.from(svg.querySelectorAll('.interactive-square')).map(el => { const x = +el.getAttribute('data-cx'); const y = +el.getAttribute('data-cy'); const r = +el.getAttribute('data-r'); const o = +el.getAttribute('data-opacity'); const a = +el.getAttribute('data-angle') + (Math.random() * 20 - 10); return { el, t: 's', x, y, z: c.bz, r: a, cx: x, cy: y, cz: c.bz, cr: a, cs: 1, m: Math.pow((r / c.sz), 1.5) * o, o: Math.random() * Math.PI * 2 }; }) ]; // Initial transform for squares s.e.filter(el => el.t === 's').forEach(setTransform); // Helper functions for event handling const gc = e => ({ x: (e.touches?.[0] || e.changedTouches?.[0] || e).clientX, y: (e.touches?.[0] || e.changedTouches?.[0] || e).clientY }); const inBounds = (x, y) => { const { left, top, width, height } = s.r || {}; return x >= left && x <= left + width && y >= top && y <= top + height; }; // Event handlers const up = e => { if (!s.r) ur(); const { left, top, width, height } = s.r; const { x, y } = gc(e); if (!inBounds(x, y) && !e.type.startsWith('touch')) { if (s.p.a) pl(false); return; } // Calculate relative position const tx = ((x - left) / width) * c.sz; const ty = ((y - top) / height) * c.sz; // Init/update target position if (s.p.tx === null) { s.p.tx = tx; s.p.ty = ty; } else { s.p.tx = tx; s.p.ty = ty; // Handle reappearance after inactivity if (!s.p.a && performance.now() - s.p.lt > 100) { s.p.x = tx; s.p.y = ty; } } // Initialize actual position if first interaction if (s.p.x === null) { s.p.x = s.p.tx; s.p.y = s.p.ty; } s.p.a = true; s.p.lt = performance.now(); }; const pd = e => { up(e); s.a = true; s.pt = performance.now(); if (e.type === 'touchstart') e.preventDefault(); }; const pu = e => { s.a = false; s.pt = performance.now(); if (e.type === 'touchend' || e.type === 'touchcancel') { pl(true); } }; const pl = (immediate = false) => { s.p.a = false; s.p.lit = performance.now(); s.a = false; s.pt = s.p.lit; if (immediate) { s.p.tx = s.p.x = null; s.p.ty = s.p.y = null; } }; // Event listeners window.addEventListener('resize', ur, { passive: true }); svg.addEventListener('pointermove', up, { passive: true }); svg.addEventListener('pointerenter', up, { passive: true }); svg.addEventListener('pointerleave', pl, { passive: true }); svg.addEventListener('pointerdown', pd, { passive: false }); window.addEventListener('pointerup', pu, { passive: true }); window.addEventListener('pointercancel', pu, { passive: true }); // Start animation requestAnimationFrame(animate); // Setup auto-cleanup if (document.currentScript?.parentNode) { const cleanup = () => { window.removeEventListener('resize', ur); svg.removeEventListener('pointermove', up); svg.removeEventListener('pointerenter', up); svg.removeEventListener('pointerleave', pl); svg.removeEventListener('pointerdown', pd); window.removeEventListener('pointerup', pu); window.removeEventListener('pointercancel', pu); s.e = []; }; new MutationObserver(mutations => { mutations.forEach(m => { if (m.removedNodes) { m.removedNodes.forEach(n => { if (n === document.currentScript) { cleanup(); observer.disconnect(); } }); } }); }).observe(document.currentScript.parentNode, { childList: true }); } })(); // Apply 3D transform to square elements function setTransform(el) { const ps = c.pz / (c.pz + el.cz); const px = c.cx + (el.cx - c.cx) * ps; const py = c.cy + (el.cy - c.cy) * ps; el.el.setAttribute('transform', 'translate('+ px+' '+ py+') rotate('+ el.cr+ ') scale(' + ps * el.cs + ')' ); } // Calculate effects based on time and pulse function fx(el, t, pv) { const θ = (t % c.bd) / c.bd * Math.PI * 2 + el.o; const w = Math.sin(θ); const mf = 1 - Math.min(0.8, el.m); return { s: (1 + w * c.bs * mf) * (1 + pv * c.pa * Math.pow(mf, 2)), y: w * c.sz * c.bv * mf, p: pv * c.pa * Math.pow(mf, 2) }; } // Animation loop function animate(time) { const dt = s.t ? time - s.t : 0; s.t = time; // Handle pointer inactivity if (!s.p.a && s.p.x !== null && time - s.p.lit > c.td) { s.p.x = s.p.y = s.p.tx = s.p.ty = null; s.e.forEach(el => { if (el.ractive) el.ractive = false; }); } // Smooth pointer movement if (s.p.a && s.p.tx !== null && s.p.x !== null) { const dx = s.p.tx - s.p.x; const dy = s.p.ty - s.p.y; const dist = Math.sqrt(dx*dx + dy*dy); const factor = dist > c.sz * 0.3 ? 0.2 : c.pp; s.p.x += dx * factor; s.p.y += dy * factor; } // Update pulse s.pv = s.a ? s.pv + (1 - s.pv) * Math.min(1, (time - s.pt) / c.pr) : s.pv * Math.max(0, 1 - (time - s.pt) / c.pf); if (s.pv < 0.001) s.pv = 0; // Process elements const ps = Math.max(0.6, Math.min(1, c.sz / 150)); const active = s.p.x !== null; s.e.forEach(el => { const sm = Math.min(dt / c.d, 1) * (el.t === 's' ? 0.3 : 0.1); const effect = fx(el, time, s.pv); if (el.t === 'c') { // Circle let tx = el.x; let ty = el.y + effect.y; if (active) { const dx = s.p.x - el.x; const dy = s.p.y - el.y; const d = Math.sqrt(dx * dx + dy * dy); const mv = c.sz * c.mv * (1 - Math.min(0.8, el.m)); const m = Math.min(d, mv) * ps; if (d > 0.1) { tx += m * (dx / d); ty += m * (dy / d); } } // Smooth interpolation const cx_diff = tx - el.cx; const cy_diff = ty - el.cy; const c_dist = Math.sqrt(cx_diff*cx_diff + cy_diff*cy_diff); const c_factor = c_dist > 20 ? 0.15 : 0.1; el.cx += cx_diff * c_factor; el.cy += cy_diff * c_factor; // Apply changes el.el.setAttribute('cx', el.cx); el.el.setAttribute('cy', el.cy); el.el.setAttribute('r', el.r * effect.s); // Handle reflection if (el.mr && ${reflect}) { // Initialize reflection properties if needed const reflectX = c.sz - el.x; if (!el.rcx) { el.rcx = reflectX; el.rcy = el.y; el.rtx = reflectX; el.rty = el.y; el.ractive = false; el.rest = { x: reflectX, y: el.y }; } // Update reflection activity state if (active && !el.ractive && s.p.lt - s.p.lit > 100) { el.ractive = true; } else if (!active && el.ractive && s.p.lit - s.p.lt > 50) { el.ractive = false; } // Set target position based on active state and mode if (!active || !${follow}) { el.rtx = c.sz - el.cx; el.rty = el.cy; } else { const rdx = s.p.x - reflectX; const rdy = s.p.y - el.y; const rd = Math.sqrt(rdx * rdx + rdy * rdy); const rmv = c.sz * c.mv * (1 - Math.min(0.8, el.m)); const rm = Math.min(rd, rmv) * ps; if (rd > 0.1) { el.rtx = reflectX + rm * (rdx / rd); el.rty = el.y + effect.y + rm * (rdy / rd); } else { el.rtx = reflectX; el.rty = el.y + effect.y; } } // Return to rest position when inactive if (!active && !s.p.x) { el.rtx = el.rest.x; el.rty = el.rest.y + effect.y; } // Smooth interpolation for reflection const rcx_diff = el.rtx - el.rcx; const rcy_diff = el.rty - el.rcy; const rc_dist = Math.sqrt(rcx_diff*rcx_diff + rcy_diff*rcy_diff); const rc_factor = rc_dist > 30 ? 0.05 : (rc_dist > 10 ? 0.07 : 0.09); el.rcx += rcx_diff * rc_factor; el.rcy += rcy_diff * rc_factor; // Apply reflection position el.mr.setAttribute('cx', el.rcx); el.mr.setAttribute('cy', el.rcy); el.mr.setAttribute('r', el.r * effect.s); } } else if (el.t === 's') { // Square let tx = el.x, ty = el.y + effect.y, tz = c.bz, tr = el.r, ts = 1; if (active) { const dx = s.p.x - el.x; const dy = s.p.y - el.y; const d = Math.sqrt(dx * dx + dy * dy); const mf = 1 - Math.min(0.8, el.m); const mv = c.sz * 0.1 * mf; const m = Math.min(d, mv) * ps; if (d > 0.1) { tx += m * (dx / d); ty += m * (dy / d); } // Apply depth & scale changes const n = Math.min(d / c.pr, 1); const zf = 1 - n * n; tz -= c.bz * zf * mf; ts += zf * 0.3 * mf; tr += Math.sin(d / 100) * 5 * mf; } else { // Idle rotation tr += Math.sin(time / 1500 + el.o) * 3 * (1 - Math.min(0.8, el.m)); } // Apply pulse to Z depth tz -= effect.p * c.bz * 0.7 * (1 - Math.min(0.8, el.m)); ts *= effect.s; // Smoothing based on distance const sx_diff = tx - el.cx; const sy_diff = ty - el.cy; const s_dist = Math.sqrt(sx_diff*sx_diff + sy_diff*sy_diff); const s_factor = Math.min(1, s_dist > 20 ? sm * 1.5 : sm); // Apply interpolation el.cx += sx_diff * s_factor; el.cy += sy_diff * s_factor; el.cz += (tz - el.cz) * sm; el.cr += (tr - el.cr) * sm; el.cs += (ts - el.cs) * sm; setTransform(el); } }); requestAnimationFrame(animate); } ]]></script> `; } function renderSVGAvatar({ pub, size = 200, dark = false, draw = "circles", reflect = true, round = true, embed = true, svg } = {}) { const { decoded, finals, averages } = parsePub(pub); // Create gradient background const bgGradient = ` <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[0] * 30}%)"/> <stop offset="100%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[1] * 30}%)"/> </linearGradient> `; // Create squares for both layers (non-interactive) const createSquares = (data, isSecond = false) => { return chunkIt(data, 14).map(chunk => { if (chunk.length !== 14) return ''; const [x, y, rRaw, h1, s1, l1, a1, x1, h2, s2, l2, a2, x2, angle] = chunk; const r = size / 8 + rRaw * size * (7 / 8); const gradientId = `gradient-${x}-${y}-${isSecond ? '2' : '1'}`; const centerX = x * size; const centerY = y * size; const squareAttrs = svg === 'interactive' ? `class="interactive-square" data-cx="${centerX}" data-cy="${centerY}" data-r="${r}" data-angle="${angle * 180}" data-opacity="${(a1 + a2) / 2}"` : ''; return ` <defs> <linearGradient id="${gradientId}" x1="${x1}" y1="0" x2="${x2}" y2="1"> <stop offset="0%" stop-color="color(display-p3 ${h1} ${s1} ${l1} / ${a1})"/> <stop offset="100%" stop-color="color(display-p3 ${h2} ${s2} ${l2} / ${a2})"/> </linearGradient> </defs> <g ${squareAttrs} transform="translate(${centerX} ${centerY}) rotate(${angle * 180})"> <rect x="${-r / 2}" y="${-r / 2}" width="${r}" height="${r}" fill="url(#${gradientId})" style="${isSecond ? 'mix-blend-mode:color-burn;' : 'filter:blur(20px);'}" /> </g> `; }).join(''); }; // Generate circles for both layers with interactive attributes const createCircles = (data, radius, isSecond = false) => { return chunkIt(data, 7).map(chunk => { if (chunk.length !== 7) return ''; const [x, y, radi, r, g, b, a] = chunk; const cx = size / 2 + (x * size) / 2; const cy = y * size; const rad = radi * radius; const circleAttrs = svg === 'interactive' ? `class="interactive-circle" data-cx="${cx}" data-cy="${cy}" data-opacity="${a}"` : ''; return ` <circle ${circleAttrs} cx="${cx}" cy="${cy}" r="${rad}" fill="color(display-p3 ${r} ${g} ${b} / ${a})" style="${isSecond ? 'mix-blend-mode:multiply;' : ''}" /> ${reflect ? ` <circle cx="${size - cx}" cy="${cy}" r="${rad}" fill="color(display-p3 ${r} ${g} ${b} / ${a})" style="${isSecond ? 'mix-blend-mode:multiply;' : ''}" />` : ''} `; }).join(''); }; const clipPath = round ? ` <defs> <clipPath id="circle-mask"> <circle cx="${size / 2}" cy="${size / 2}" r="${size / 2}" /> </clipPath> </defs> ` : ''; // Interactive mode script for mouse tracking const interactiveScript = svg === 'interactive' ? interactiveScriptGen({ size, reflect, finals, averages }) : ''; let svg_content = ` <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg" style="overflow: visible;" > <defs>${bgGradient}</defs> ${clipPath} <g ${round ? 'clip-path="url(#circle-mask)"' : ''}> <rect x="${-size}" width="${3 * size}" y="${-size}" height="${3 * size}" fill="url(#bg)"/> ${draw === "squares" ? `${createSquares(decoded[0], false)} ${createSquares(decoded[1], true)}` : `${createCircles(decoded[0], 0.42 * size)} ${createCircles(decoded[1], 0.125 * size, true)}` } </g> ${interactiveScript} </svg> `; if (svg === 'interactive') { // For interactive mode, return encoded SVG without base64 return `data:image/svg+xml,${encodeURIComponent(svg_content.trim())}`; } if (embed) { const embedData = { pub }; if (embed) { embedData.content = embed; } svg_content = embedInSvg(svg_content, embedData); } const svgBase64 = typeof btoa === 'function' ? btoa(svg_content) : Buffer.from(svg_content).toString('base64'); let finalData = `data:image/svg+xml;base64,${svgBase64}`; return finalData } const cache = {}; function gunAvatar({ pub, // public key of a user size = 200, // square pixel dimensions dark = false, // Light mode, enable to have more dim backgrounds draw = "circles", // useful for people and agents avatars. Also "squares" - for backgrounds and document covers. reflect = true, // used for avatars symmetry. Disable for squares. round = true, // Cut the image with round transparency mask. Disable to get raw square image. embed = true, // Embed the "pub" key into the image (both PNG and SVG). You can put any serializable content here - an encrypted keypair is a nice example of use for this svg = true, // Scalabe vector graphics format. Disable to get Canvas PNG render available only in browsers. p3 = true, // Extended P3 color palette utilizes full capacity of modern displays. Disable to have more backwards compatible RGBA color palette. } = {}) { const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; if (!validatePub(pub)) return ''; if (svg || !isBrowser) return renderSVGAvatar({ pub, size, dark, draw, reflect, round, embed, svg}); const key = JSON.stringify(arguments[0]); if (cache?.[key]) return cache[key] const image = renderCanvasAvatar({ pub, size, dark, draw, reflect, round, embed, p3 }); cache[key] = image; return image; } function mountClass(elClass = "gun-avatar") { document.addEventListener("DOMContentLoaded", () => { let avatars = document.getElementsByClassName(elClass); for (let i in avatars) { const img = avatars[i]; if (img.dataset.round !== "false") { img.style.borderRadius = "100%"; } let embed = img.dataset.embed; if (img.dataset.embed !== 'true') { try { embed = JSON.parse(img.dataset.embed); } catch (e) { console.warn('Invalid content JSON in data-embed attribute'); } } img.src = gunAvatar({ pub: img.dataset.pub, size: Number(img.dataset.size), dark: Boolean(img.dataset.dark), draw: img.dataset.draw, reflect: img.dataset.reflect !== "false", svg: img.dataset.svg, round: Boolean(img.dataset.round), embed: embed === "false" ? false : embed || true }); } }); } function mountElement(elName = "gun-avatar") { let initiated = false; if (initiated) return; class Avatar extends HTMLElement { constructor() { super(); this.attachShadow({ mode: "open" }); /** @type {HTMLImageElement} */ this.img = document.createElement("img"); this.shadowRoot.append(this.img); } render() { this.pub = this.getAttribute("pub") || "1234123455Ute2tFhdjDQgzR-1234lfSlZxgEZKuquI.2F-j1234434U1234Asj-5lxnECG5TDyuPD8gEiuI123"; this.size = this.hasAttribute("size") ? Number(this.getAttribute("size")) : 400; this.draw = this.getAttribute("draw") || "circles"; this.reflect = this.hasAttribute("reflect") ? this.getAttribute("reflect") !== "false" : true; this.round = this.hasAttribute("round") || this.getAttribute("round") === ""; this.dark = this.hasAttribute("dark") ? this.getAttribute("dark") != "" : false; this.embed = this.hasAttribute("embed") ? this.getAttribute("embed") !== "false" : true; this.svg = this.hasAttribute("dark") && this.getAttribute("dark"); this.p3 = this.hasAttribute("p3") ? this.getAttribute("p3") !== "false" : true; let embed = this.getAttribute("embed"); if (this.getAttribute("embed")) { try { embed = JSON.parse(this.getAttribute("embed")); } catch (e) { console.warn('Invalid content JSON in embed attribute'); } } this.img.style.borderRadius = this.round ? "100%" : "0%"; this.img.src = gunAvatar({ pub: this.pub, size: this.size, dark: this.dark, draw: this.draw, reflect: this.reflect, round: this.round, svg: this.svg, embed: embed === "false" ? false : embed || true, p3: this.p3 }); } connectedCallback() { this.render(); } static get observedAttributes() { return ["pub", "round", "size", "dark", "draw", "reflect", "embed", "p3"]; } attributeChangedCallback() { this.render(); } } customElements.define(elName, Avatar); initiated = true; } patchSafari(); export { error, extractFromFile, gunAvatar, mountClass, mountElement, parsePub };