gun-avatar
Version:
Avatar generator for GUN public keys
1,048 lines (897 loc) • 34.3 kB
TypeScript
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 };