atprotocol stickers
Something went wrong. Try again.
5.3 kB · 260 lines
Svelte
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261<script lang="ts"> import { getBlobUrl } from '$lib/types.js';
interface Props { shortname: string; imageCid: string; did: string; pds: string; maskPath?: string; imageWidth?: number; imageHeight?: number; borderColor?: string; borderThickness?: number; showDelete?: boolean; ondelete?: () => void; }
let { shortname, imageCid, did, pds, maskPath, imageWidth, imageHeight, borderColor = '', borderThickness = 0, showDelete = false, ondelete }: Props = $props();
let imgError = $state(false); let imgLoaded = $state(false);
const blobUrl = $derived(getBlobUrl(pds, did, imageCid));
// A unique ID for the SVG clipPath element (CIDs are alphanumeric base32) const clipId = $derived(`clip-${imageCid.replace(/[^a-zA-Z0-9]/g, '')}`);
// Compute display dimensions fitting within 160×160px, min 40px const MAX = 160; const MIN = 40; const svgW = $derived( imageWidth && imageHeight ? Math.max(MIN, Math.round(imageWidth * Math.min(MAX / imageWidth, MAX / imageHeight, 1))) : 0 ); const svgH = $derived( imageWidth && imageHeight ? Math.max(MIN, Math.round(imageHeight * Math.min(MAX / imageWidth, MAX / imageHeight, 1))) : 0 );
const isSvgSticker = $derived(!!(maskPath && imageWidth && imageHeight));</script>
<div class="sticker-wrap"> {#if isSvgSticker} <!-- SVG sticker: arbitrary clip shape, no white rectangle --> {#if !imgError} <svg class="sticker-svg" class:loaded={imgLoaded} width={svgW} height={svgH} viewBox="0 0 {imageWidth} {imageHeight}" style="overflow: visible; display: block;" > <defs> <clipPath id={clipId}> <path d={maskPath} /> </clipPath> </defs> {#if borderColor && borderThickness} <!-- Border drawn before image so inner half is covered by the clipped image --> <path d={maskPath} fill="none" stroke={borderColor} stroke-width={borderThickness * 2} stroke-linejoin="round" stroke-linecap="round" /> {/if} <image href={blobUrl} x="0" y="0" width={imageWidth} height={imageHeight} clip-path="url(#{clipId})" preserveAspectRatio="none" onload={() => imgLoaded = true} onerror={() => imgError = true} /> </svg> {:else} <div class="img-error">🖼</div> {/if} {:else} <!-- Legacy PNG sticker with white backing --> <div class="sticker" class:loaded={imgLoaded}> {#if !imgError} <img src={blobUrl} alt={shortname} onload={() => imgLoaded = true} onerror={() => imgError = true} draggable="false" /> {:else} <div class="img-error">🖼</div> {/if} </div> {/if}
<div class="sticker-label">{shortname}</div> {#if showDelete} <button class="delete-btn" onclick={ondelete} title="delete sticker">✕</button> {/if}</div>
<style> .sticker-wrap { position: relative; display: inline-flex; flex-direction: column; align-items: center; user-select: none; cursor: grab; }
.sticker-wrap:active { cursor: grabbing; }
/* SVG sticker */ .sticker-svg { opacity: 0; transition: opacity 0.2s ease, filter 0.2s ease; filter: drop-shadow(2px 3px 6px rgba(0,0,0,0.18)); }
.sticker-svg.loaded { opacity: 1; }
.sticker-wrap:hover .sticker-svg { filter: drop-shadow(3px 8px 18px rgba(0,0,0,0.28)); }
/* Legacy PNG sticker */ .sticker { background: white; box-shadow: 2px 3px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.1); max-width: 160px; max-height: 160px; min-width: 80px; min-height: 80px; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 1px; opacity: 0; position: relative; transition: opacity 0.2s ease, box-shadow 0.2s ease; }
.sticker.loaded { opacity: 1; }
/* Folded corner peel for legacy PNG stickers */ .sticker::after { content: ''; position: absolute; bottom: 0; right: 0; width: 0; height: 0; background: linear-gradient( 225deg, #f5f2ee 0%, #e8e4de 40%, rgba(0, 0, 0, 0.08) 44%, transparent 48% ); transition: width 0.22s ease, height 0.22s ease, box-shadow 0.22s ease; pointer-events: none; z-index: 1; }
.sticker-wrap:hover .sticker::after { width: 30px; height: 30px; box-shadow: -2px -2px 6px rgba(0, 0, 0, 0.12); }
.sticker-wrap:hover .sticker { box-shadow: 3px 6px 16px rgba(0, 0, 0, 0.22), 0 1px 3px rgba(0, 0, 0, 0.1); }
.sticker img { width: 100%; height: 100%; object-fit: contain; display: block; max-width: 160px; max-height: 160px; }
.img-error { font-size: 2rem; padding: 1rem; opacity: 0.4; }
.sticker-label { font-family: 'Caveat', cursive; font-size: 0.85rem; color: #666; margin-top: 4px; white-space: nowrap; max-width: 160px; overflow: hidden; text-overflow: ellipsis; text-align: center; }
.delete-btn { position: absolute; top: -4px; right: -4px; width: 18px; height: 18px; background: #ff8888; border: none; border-radius: 50%; color: white; font-size: 0.65rem; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; line-height: 1; box-shadow: 0 1px 3px rgba(0,0,0,0.2); opacity: 0; transition: opacity 0.15s; }
.sticker-wrap:hover .delete-btn { opacity: 1; }</style>