← back to CelebritySignatures
public/assets/signature-preview.js
109 lines
import {imageSource} from './signature-ink.js';
const cache=new Map(), tokens=new WeakMap(), queue=[];
// "clean" = colorized ink on white (the requirement); "fallback" = CORS-denied
// source shown uncolored (the escape hatch). Expose the tally so "ALL signatures
// are color on white" is a measurable number, not a claim — read
// window.signatureRenderStats in the console after browsing.
export const renderStats=(typeof window!=='undefined'?(window.signatureRenderStats={clean:0,fallback:0,error:0}):{clean:0,fallback:0,error:0});
function tallyRender(mode){ if(mode in renderStats)renderStats[mode]++; }
const resolvedSources=new Map();
let running=0;
function schedule(job,priority) {
return new Promise((resolve,reject)=>{const item={job,resolve,reject};priority?queue.unshift(item):queue.push(item);pump();});
}
function pump() {
while(running<4 && queue.length){const {job,resolve,reject}=queue.shift();running++;Promise.resolve().then(job).then(resolve,reject).finally(()=>{running--;pump();});}
}
function load(url,cors=true) {
return new Promise((resolve,reject)=>{
const img=new Image();if(cors)img.crossOrigin='anonymous';img.referrerPolicy='no-referrer';
const timer=setTimeout(()=>finish(new Error('Image timed out')),12000);
function finish(error){clearTimeout(timer);img.onload=img.onerror=null;error?reject(error):resolve(img);}
img.onload=()=>finish();img.onerror=()=>finish(new Error('Image unavailable'));img.src=url;
});
}
async function resolveSource(url) {
const u=new URL(url),match=u.pathname.match(/^\/wiki\/(?:Special:FilePath\/|File:)(.+)$/i);
if(u.hostname!=='commons.wikimedia.org'||!match)return url;
const title='File:'+decodeURIComponent(match[1]);
if(!resolvedSources.has(title)){
const request=(async()=>{
const params=new URLSearchParams({action:'query',format:'json',origin:'*',prop:'imageinfo',iiprop:'url',iiurlwidth:'2000',titles:title});
const response=await fetch('https://commons.wikimedia.org/w/api.php?'+params,{signal:AbortSignal.timeout(10000)});
if(!response.ok)throw new Error('Source lookup unavailable');
const data=await response.json(),info=Object.values(data.query?.pages||{})[0]?.imageinfo?.[0];
// High-res: serve the ORIGINAL file — an SVG renders as VECTOR (crisp at any
// size), a raster at its full native resolution — not a downsampled thumb.
// Signature files are small, so full-res is cheap; the thumb is a fallback.
const direct=imageSource(info?.url||info?.thumburl);
if(!direct||!['upload.wikimedia.org','thumb.wikimedia.org'].includes(new URL(direct).hostname))throw new Error('No source image');
return direct;
})();
resolvedSources.set(title,request);request.catch(()=>resolvedSources.delete(title));
if(resolvedSources.size>192)resolvedSources.delete(resolvedSources.keys().next().value);
}
return resolvedSources.get(title);
}
async function convert(source) {
let url=source;try{url=await resolveSource(source);}catch{}
// Show the ORIGINAL signature art directly — crisp (an SVG stays vector at any
// size) and in its TRUE source colors, on the white ground. No canvas
// recolor/binarize: that rasterized (blurred) the art and flattened every
// signature to one invented color. A plain <img> needs no CORS, so more
// sources load cleanly too.
try { await load(url,false); return {url,mode:'clean'}; }
catch { return {url,mode:'fallback'}; }
}
function preview(url,priority,retry) {
const key=url;
if(retry)cache.delete(key);
if(cache.has(key))return cache.get(key);
const p=schedule(()=>convert(url),priority);cache.set(key,p);
p.catch(()=>{if(cache.get(key)===p)cache.delete(key);});
// Bound retained pixel previews, including long gallery browsing sessions.
if(cache.size>96)cache.delete(cache.keys().next().value);
return p;
}
export async function renderInto(target,source,{priority=false,retry=false,label='Signature',tintKey=null}={}) {
const token={};tokens.set(target,token);
target.dataset.phase='loading';target.replaceChildren();
const waiting=document.createElement('span');waiting.className='signature-message';waiting.textContent='Loading signature…';target.append(waiting);
const url=imageSource(source,location.href);
try {
if(!url)throw new Error('Unsupported image source');
const result=await preview(url,priority,retry);
if(tokens.get(target)!==token || !target.isConnected)return;
const img=new Image();img.alt=label;img.className='signature-image';img.src=result.url;
await img.decode();
if(tokens.get(target)!==token || !target.isConnected)return;
target.replaceChildren(img);target.dataset.phase=result.mode;target.dataset.source=url;
if(result.mode==='fallback'){
img.classList.add('signature-fallback');
target.title='High-contrast source preview. Automatic cleanup is unavailable for this source.';
} else target.removeAttribute('title');
tallyRender(result.mode);
target.dispatchEvent(new CustomEvent('signaturepreviewchange',{bubbles:true}));return result.mode;
} catch {
if(tokens.get(target)!==token || !target.isConnected)return;
tallyRender('error');
target.dataset.phase='error';target.removeAttribute('data-source');
const box=document.createElement('span');box.className='signature-message';box.textContent='Preview unavailable. ';
// Do not nest buttons inside the variant selector buttons.
if(!target.closest('button')){
const button=document.createElement('button');button.type='button';button.textContent='Try again';
button.onclick=e=>{e.stopPropagation();renderInto(target,source,{priority:true,retry:true,label,tintKey:target.dataset.signatureTintKey});};box.append(button);
} else box.append('Select to retry.');
target.replaceChildren(box);target.dispatchEvent(new CustomEvent('signaturepreviewchange',{bubbles:true}));return 'error';
}
}
const observer=new IntersectionObserver(entries=>{
for(const e of entries)if(e.isIntersecting){observer.unobserve(e.target);if(e.target.isConnected)renderInto(e.target,e.target.dataset.signatureSrc,{label:e.target.dataset.signatureLabel,tintKey:e.target.dataset.signatureTintKey});}
},{rootMargin:'120px'});
export function watch(root=document) {
for(const el of root.querySelectorAll('[data-signature-src]:not([data-preview-watched])')){el.dataset.previewWatched='true';observer.observe(el);}
}
watch();
new MutationObserver(records=>{for(const r of records)for(const node of r.addedNodes)if(node.nodeType===1){if(node.matches('[data-signature-src]')){node.dataset.previewWatched='true';observer.observe(node);}watch(node);}
for(const r of records)for(const node of r.removedNodes)if(node.nodeType===1){observer.unobserve(node);node.querySelectorAll('[data-signature-src]').forEach(el=>observer.unobserve(el));}
}).observe(document.body,{childList:true,subtree:true});