feat: ship minecraft theme updates and platform workflow improvements
这个提交包含在:
74
frontend/src/lib/pixel-avatar.ts
普通文件
74
frontend/src/lib/pixel-avatar.ts
普通文件
@@ -0,0 +1,74 @@
|
||||
function hashSeed(seed: string): number {
|
||||
let h = 2166136261 >>> 0;
|
||||
for (let i = 0; i < seed.length; i += 1) {
|
||||
h ^= seed.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619) >>> 0;
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function mulberry32(seed: number): () => number {
|
||||
let t = seed >>> 0;
|
||||
return () => {
|
||||
t += 0x6d2b79f5;
|
||||
let x = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
x ^= x + Math.imul(x ^ (x >>> 7), x | 61);
|
||||
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const SKIN_PALETTE = ["#f6d3b3", "#e6be95", "#d7a980", "#f8dec6", "#c99264"];
|
||||
const HAIR_PALETTE = ["#2f1b12", "#4a2f1d", "#6d4c41", "#212121", "#4e342e", "#1b5e20"];
|
||||
const ACCENT_PALETTE = ["#7cb342", "#00b0d6", "#ffb300", "#8d6e63", "#ab47bc", "#ef5350"];
|
||||
|
||||
export function buildAvatarSeed(...parts: Array<string | number | null | undefined>): string {
|
||||
const cleaned = parts
|
||||
.map((part) => String(part ?? "").trim())
|
||||
.filter((part) => part.length > 0);
|
||||
return cleaned.length > 0 ? cleaned.join("|") : "guest|pixel";
|
||||
}
|
||||
|
||||
export function generatePixelAvatarDataUri(seedInput: string, pixelSize = 10): string {
|
||||
const seed = buildAvatarSeed(seedInput);
|
||||
const rng = mulberry32(hashSeed(seed));
|
||||
const skin = SKIN_PALETTE[Math.floor(rng() * SKIN_PALETTE.length)] ?? SKIN_PALETTE[0];
|
||||
const hair = HAIR_PALETTE[Math.floor(rng() * HAIR_PALETTE.length)] ?? HAIR_PALETTE[0];
|
||||
const accent = ACCENT_PALETTE[Math.floor(rng() * ACCENT_PALETTE.length)] ?? ACCENT_PALETTE[0];
|
||||
const bg = rng() > 0.5 ? "#1f1f1f" : "#2b2b2b";
|
||||
const border = rng() > 0.5 ? "#000000" : "#3e2723";
|
||||
|
||||
const width = 8 * pixelSize;
|
||||
const height = 8 * pixelSize;
|
||||
const rects: string[] = [
|
||||
`<rect width="${width}" height="${height}" fill="${bg}"/>`,
|
||||
`<rect x="0" y="0" width="${width}" height="${pixelSize}" fill="${accent}" opacity="0.4"/>`,
|
||||
];
|
||||
|
||||
for (let y = 0; y < 8; y += 1) {
|
||||
for (let x = 0; x < 4; x += 1) {
|
||||
const fillFace = y >= 1 && y <= 6 && x >= 1;
|
||||
const threshold = fillFace ? 0.2 : 0.52;
|
||||
if (rng() < threshold) continue;
|
||||
|
||||
const color = y <= 1 ? hair : y >= 6 ? accent : skin;
|
||||
const leftX = x * pixelSize;
|
||||
const rightX = (7 - x) * pixelSize;
|
||||
const yPos = y * pixelSize;
|
||||
rects.push(`<rect x="${leftX}" y="${yPos}" width="${pixelSize}" height="${pixelSize}" fill="${color}"/>`);
|
||||
if (rightX !== leftX) {
|
||||
rects.push(`<rect x="${rightX}" y="${yPos}" width="${pixelSize}" height="${pixelSize}" fill="${color}"/>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eyeY = 3 * pixelSize;
|
||||
rects.push(`<rect x="${2 * pixelSize}" y="${eyeY}" width="${pixelSize}" height="${pixelSize}" fill="#111"/>`);
|
||||
rects.push(`<rect x="${5 * pixelSize}" y="${eyeY}" width="${pixelSize}" height="${pixelSize}" fill="#111"/>`);
|
||||
|
||||
const mouthY = 5 * pixelSize;
|
||||
rects.push(`<rect x="${3 * pixelSize}" y="${mouthY}" width="${2 * pixelSize}" height="${pixelSize}" fill="${hair}" opacity="0.8"/>`);
|
||||
rects.push(`<rect x="${0}" y="${0}" width="${width}" height="${height}" fill="none" stroke="${border}" stroke-width="${Math.max(2, Math.floor(pixelSize / 4))}"/>`);
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">${rects.join("")}</svg>`;
|
||||
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户