xxtomm
max
hugh
diana
solo
<script>
import FallbackAvatar from '$registry/spelte/fallback-avatar.svelte';
</script>
<div class="flex items-center gap-3">
{#each ['xxtomm', 'max', 'hugh', 'diana', 'solo'] as name}
<div class="flex flex-col items-center gap-1">
<FallbackAvatar class="border" {name} size={32} />
<span class="text-[11px] text-muted-foreground">{name}</span>
</div>
{/each}
</div>Installation
pnpm dlx shadcn-svelte@latest add https://spelte.dev/r/fallback-avatar.json<script lang="ts">
import { cn } from '$lib/utils';
interface Props {
name: string;
size?: number;
animated?: boolean;
class?: string;
}
let { name, size = 32, animated = true, class: className }: Props = $props();
function hashString(str: string): [number, number] {
let h1 = 0xdeadbeef, h2 = 0x41c6ce57;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return [h1 >>> 0, h2 >>> 0];
}
function mulberry32(seed: number) {
return () => {
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function deriveHue(hash: [number, number]): number {
const bytes: number[] = [];
for (let i = 0; i < 4; i++) {
bytes.push((hash[0] >> (i * 8)) & 0xff);
bytes.push((hash[1] >> (i * 8)) & 0xff);
}
return bytes.reduce((a, b) => a + b, 0) % 360;
}
function oklchToRgb(L: number, C: number, H: number): [number, number, number] {
const hRad = (H * Math.PI) / 180;
const a = C * Math.cos(hRad), b = C * Math.sin(hRad);
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
const l = l_ * l_ * l_, m = m_ * m_ * m_, s = s_ * s_ * s_;
let R = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
let G = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
let B = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;
const gamma = (v: number) => v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
return [
Math.round(Math.max(0, Math.min(1, gamma(R))) * 255),
Math.round(Math.max(0, Math.min(1, gamma(G))) * 255),
Math.round(Math.max(0, Math.min(1, gamma(B))) * 255)
];
}
interface Uniforms {
S: number; H: number;
P: [number, number, number, number];
Q: [number, number, number, number];
C1: [number, number, number];
C2: [number, number, number];
}
function computeUniforms(n: string): Uniforms {
const hash = hashString(n);
const rng = mulberry32(hash[0]);
const hue = deriveHue(hash);
const c1 = oklchToRgb(0.8, 0.18, hue);
const c2 = oklchToRgb(0.45, 0.18, hue);
const p: number[] = [];
for (let i = 0; i < 8; i++) p.push(rng());
return {
S: hash[0] / 4294967296,
H: hue / 360,
P: [p[0], p[1], p[2], p[3]],
Q: [p[4], p[5], p[6], p[7]],
C1: [c1[0] / 255, c1[1] / 255, c1[2] / 255],
C2: [c2[0] / 255, c2[1] / 255, c2[2] / 255]
};
}
const VERT_SRC = 'attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}';
const FRAG_SRC = `precision mediump float;
uniform vec2 R;uniform float S,H;uniform vec4 P,Q;uniform vec3 C1,C2;uniform float T;
#define UV (gl_FragCoord.xy/R)
void main(){
vec2 uv=UV;vec2 b1=vec2(P.x,P.y);vec2 b2=vec2(P.z,P.w);vec2 b3=vec2(Q.x,Q.y);
vec2 c1=b1+vec2(sin(T*.7+b1.x*6.)*.08,cos(T*.9+b1.y*6.)*.08);
vec2 c2=b2+vec2(sin(T*.6+2.1)*.1,cos(T*.8+1.3)*.07);
vec2 c3=b3+vec2(sin(T*.5+4.2)*.07,cos(T*1.1+3.7)*.09);
float breath=1.+sin(T*1.3)*.06;
float d1=(1.-length(uv-c1)*1.5)*breath;
float d2=(1.-length(uv-c2)*1.5)*(1.+sin(T*1.7+1.)*.05);
float d3=(1.-length(uv-c3)*1.5)*(1.+sin(T*1.1+2.)*.05);
vec3 col=vec3(0);
col=1.-(1.-col)*(1.-C1*max(d1,0.));col=1.-(1.-col)*(1.-C2*max(d2,0.));
vec3 c3col=mix(C1,C2,.5+sin(T*.4)*.15);col=1.-(1.-col)*(1.-c3col*max(d3,0.));
col=clamp(col,0.,1.);gl_FragColor=vec4(col,1);
}`;
let _gl: WebGLRenderingContext | null = null;
let _glCanvas: HTMLCanvasElement | null = null;
let _qBuf: WebGLBuffer | null = null;
let _prog: WebGLProgram | null = null;
function ensureGL() {
if (_gl) return;
_glCanvas = document.createElement('canvas');
_glCanvas.width = 64; _glCanvas.height = 64;
_gl = _glCanvas.getContext('webgl', { antialias: false, depth: false, preserveDrawingBuffer: true });
if (!_gl) return;
const vs = _gl.createShader(_gl.VERTEX_SHADER)!;
_gl.shaderSource(vs, VERT_SRC); _gl.compileShader(vs);
const fs = _gl.createShader(_gl.FRAGMENT_SHADER)!;
_gl.shaderSource(fs, FRAG_SRC); _gl.compileShader(fs);
_qBuf = _gl.createBuffer();
_gl.bindBuffer(_gl.ARRAY_BUFFER, _qBuf);
_gl.bufferData(_gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), _gl.STATIC_DRAW);
_prog = _gl.createProgram()!;
_gl.attachShader(_prog, vs); _gl.attachShader(_prog, fs); _gl.linkProgram(_prog);
}
function renderToCanvas(target: HTMLCanvasElement, uniforms: Uniforms, time: number) {
ensureGL();
if (!_gl || !_glCanvas || !_prog || !_qBuf) return;
const pxW = target.width, pxH = target.height;
if (_glCanvas.width !== pxW || _glCanvas.height !== pxH) { _glCanvas.width = pxW; _glCanvas.height = pxH; }
_gl.viewport(0, 0, pxW, pxH);
_gl.useProgram(_prog);
_gl.bindBuffer(_gl.ARRAY_BUFFER, _qBuf);
const pos = _gl.getAttribLocation(_prog, 'a');
_gl.enableVertexAttribArray(pos); _gl.vertexAttribPointer(pos, 2, _gl.FLOAT, false, 0, 0);
_gl.uniform2f(_gl.getUniformLocation(_prog, 'R'), pxW, pxH);
_gl.uniform1f(_gl.getUniformLocation(_prog, 'S'), uniforms.S);
_gl.uniform1f(_gl.getUniformLocation(_prog, 'H'), uniforms.H);
_gl.uniform4fv(_gl.getUniformLocation(_prog, 'P'), uniforms.P);
_gl.uniform4fv(_gl.getUniformLocation(_prog, 'Q'), uniforms.Q);
_gl.uniform3fv(_gl.getUniformLocation(_prog, 'C1'), uniforms.C1);
_gl.uniform3fv(_gl.getUniformLocation(_prog, 'C2'), uniforms.C2);
_gl.uniform1f(_gl.getUniformLocation(_prog, 'T'), time);
_gl.drawArrays(_gl.TRIANGLES, 0, 6);
const ctx2d = target.getContext('2d');
if (ctx2d) { ctx2d.clearRect(0, 0, pxW, pxH); ctx2d.drawImage(_glCanvas, 0, 0); }
}
let canvasEl = $state<HTMLCanvasElement | null>(null);
let animId = 0;
let isHovering = false;
let timeVal = 0;
let uniformsCache: Uniforms | null = null;
function getUniforms() {
if (!uniformsCache) uniformsCache = computeUniforms(name);
return uniformsCache;
}
$effect(() => {
uniformsCache = null;
});
$effect(() => {
const canvas = canvasEl;
if (!canvas) return;
renderToCanvas(canvas, getUniforms(), 0);
});
$effect(() => {
if (!animated) return;
const canvas = canvasEl;
if (!canvas) return;
let startTime: number | null = null;
function animLoop(timestamp: number) {
if (!isHovering) return;
if (startTime === null) startTime = timestamp - timeVal * 1000;
const elapsed = (timestamp - startTime) / 1000;
timeVal = elapsed;
renderToCanvas(canvas!, getUniforms(), elapsed);
animId = requestAnimationFrame(animLoop);
}
function onEnter() {
isHovering = true;
startTime = null;
animId = requestAnimationFrame(animLoop);
}
function onLeave() {
isHovering = false;
cancelAnimationFrame(animId);
renderToCanvas(canvas!, getUniforms(), timeVal);
}
canvas.addEventListener('mouseenter', onEnter);
canvas.addEventListener('mouseleave', onLeave);
return () => {
canvas.removeEventListener('mouseenter', onEnter);
canvas.removeEventListener('mouseleave', onLeave);
cancelAnimationFrame(animId);
};
});
const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1;
</script>
<canvas
bind:this={canvasEl}
width={size * dpr}
height={size * dpr}
class={cn('rounded-full', className)}
style="width: {size}px; height: {size}px;"
></canvas>
Examples
Sizes
<script lang="ts">
import FallbackAvatar from '$registry/spelte/fallback-avatar.svelte';
<\/script>
<div class="flex items-center gap-4">
<FallbackAvatar name="max" size={28} />
<FallbackAvatar name="hugh" size={36} />
<FallbackAvatar name="diana" size={44} />
</div>Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | String used to derive unique colors and blob positions |
size | number | 32 | Pixel size of the avatar |
animated | boolean | true | Blobs orbit and breathe on hover |
class | string | — | Additional CSS classes |