From 818be1d5a014976cfbaf5fe0fe63e244abfe3a7e Mon Sep 17 00:00:00 2001 From: sasha gudulin Date: Sun, 23 Aug 2026 16:52:06 +0200 Subject: [PATCH] portrait cleanup --- .../at/[actor]/[collection]/[rkey]/page.tsx | 21 +- src/components/avatar-portrait.module.css | 4 - src/components/avatar-portrait.tsx | 11 +- src/components/player-card.module.css | 4 +- src/components/player-card.tsx | 3 +- src/components/player-grid.module.css | 12 -- src/components/player-grid.tsx | 15 +- src/components/portrait-card.tsx | 35 +--- src/lib/avatar.ts | 190 +++++++++--------- src/lib/player.ts | 48 ++--- 10 files changed, 132 insertions(+), 211 deletions(-) diff --git a/src/app/at/[actor]/[collection]/[rkey]/page.tsx b/src/app/at/[actor]/[collection]/[rkey]/page.tsx index add4ba0..de97aa8 100644 --- a/src/app/at/[actor]/[collection]/[rkey]/page.tsx +++ b/src/app/at/[actor]/[collection]/[rkey]/page.tsx @@ -26,7 +26,7 @@ import { normalizeActor, tidToDate, } from "@/lib/atproto/uri"; -import { avatarBlobUrl, buildDrawPlan, getAvatarCatalog } from "@/lib/avatar"; +import { avatarBlobUrl, buildDrawPlan } from "@/lib/avatar"; import { BADGE_AWARD, isBadgeCollection, loadBadge } from "@/lib/badge"; import { registerFolkYurei } from "@/lib/diffs-theme"; import { breadcrumbList } from "@/lib/jsonld"; @@ -155,31 +155,24 @@ function parseTryOn( ); } -async function renderPortrait( +function renderPortrait( value: Record, tryOn: Record, owner: { pds: string; did: string }, -): Promise { +): Portrait { // A composited blob has nothing to layer onto, so an explicit `?try=` falls - // through to the catalog and renders a speculative portrait instead. + // through to the trait stack and renders a speculative portrait instead. const cid = blobCid(value.avatar); if (cid && Object.keys(tryOn).length === 0) { const url = avatarBlobUrl(value.avatar, owner); if (url) return { kind: "blob", url, cid }; } - const resolved = await getAvatarCatalog(); - if (!resolved) return null; // A blob's own keys (`ref`, `size`, `mimeType`) are not slot ids. Spreading // them would silently resolve every slot to its default and pass that off as // the user's avatar, so the blob contributes nothing to the draw plan. const stored = cid ? {} : ((value.avatar ?? {}) as Record); - const layers = buildDrawPlan( - resolved.catalog, - { ...stored, ...tryOn }, - { pds: resolved.pds, did: resolved.did }, - ); - return layers.length > 0 ? { kind: "traits", layers, stored } : null; + return { kind: "traits", layers: buildDrawPlan({ ...stored, ...tryOn }) }; } export default async function RecordPage({ @@ -211,7 +204,7 @@ export default async function RecordPage({ const tryOn = parseTryOn((await searchParams).try); const portrait = collection === "lol.folk.profile" - ? await renderPortrait(record.value, tryOn, { + ? renderPortrait(record.value, tryOn, { pds: identity.pds, did: identity.did, }) @@ -289,7 +282,7 @@ export default async function RecordPage({ {portrait && ( <> portrait - + )} diff --git a/src/components/avatar-portrait.module.css b/src/components/avatar-portrait.module.css index 4832991..c00fb52 100644 --- a/src/components/avatar-portrait.module.css +++ b/src/components/avatar-portrait.module.css @@ -30,7 +30,3 @@ object-fit: cover; pointer-events: none; } - -.pixelated { - image-rendering: pixelated; -} diff --git a/src/components/avatar-portrait.tsx b/src/components/avatar-portrait.tsx index 76c98ef..340295c 100644 --- a/src/components/avatar-portrait.tsx +++ b/src/components/avatar-portrait.tsx @@ -4,18 +4,11 @@ import styles from "./avatar-portrait.module.css"; export function AvatarPortrait({ layers, size = 160, - pixelated = true, shape = "rounded", }: { layers: Layer[]; size?: number; shape?: "rounded" | "squircle"; - /** - * Catalog trait art is stacked at its native size and upscaled, so it keeps - * nearest-neighbour sampling. A composited avatar blob is downscaled into the - * frame instead, where nearest-neighbour only adds aliasing. - */ - pixelated?: boolean; }) { const background = layers.find((l) => l.kind === "color"); const images = layers.filter((l) => l.kind === "image"); @@ -39,9 +32,7 @@ export function AvatarPortrait({ // biome-ignore lint/performance/noImgElement: layers come from arbitrary PDS hosts diff --git a/src/components/player-card.module.css b/src/components/player-card.module.css index 3160019..993b922 100644 --- a/src/components/player-card.module.css +++ b/src/components/player-card.module.css @@ -9,8 +9,8 @@ /* Same footprint as the portrait, so a missing profile does not reflow. */ .faceless { - width: 200px; - height: 200px; + width: 156px; + height: 156px; background: var(--raised); -webkit-mask-image: var(--squircle); mask-image: var(--squircle); diff --git a/src/components/player-card.tsx b/src/components/player-card.tsx index 116b26a..80e32a8 100644 --- a/src/components/player-card.tsx +++ b/src/components/player-card.tsx @@ -2,7 +2,7 @@ import type { Player, PlayerBadge } from "@/lib/player"; import { AvatarPortrait } from "./avatar-portrait"; import styles from "./player-card.module.css"; -const PORTRAIT_SIZE = 200; +const PORTRAIT_SIZE = 156; const BADGE_SIZE = 56; /** A face and a name. Anything technical lives on the repo page instead. */ @@ -13,7 +13,6 @@ export function PlayerCard({ player }: { player: Player }) { ) : ( diff --git a/src/components/player-grid.module.css b/src/components/player-grid.module.css index 391ae07..6c7efc9 100644 --- a/src/components/player-grid.module.css +++ b/src/components/player-grid.module.css @@ -26,18 +26,6 @@ border-color: var(--border-soft); } -.faceless { - width: 84px; - height: 84px; - background: var(--raised); - -webkit-mask-image: var(--squircle); - mask-image: var(--squircle); - -webkit-mask-size: 100% 100%; - mask-size: 100% 100%; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; -} - .name { margin-top: 0.6rem; font-size: 0.85rem; diff --git a/src/components/player-grid.tsx b/src/components/player-grid.tsx index e20597f..4ab7953 100644 --- a/src/components/player-grid.tsx +++ b/src/components/player-grid.tsx @@ -12,16 +12,11 @@ export function PlayerGrid({ players }: { players: PlayerSummary[] }) { {players.map((player) => (
  • - {player.portrait ? ( - - ) : ( -