← back to CelebritySignatures

public/assets/wallpaper.js

164 lines

import {DEFAULTS,normalize,drawTile,geometry} from './wallpaper-pattern.js';
const $=id=>document.getElementById(id), KEY='celebsig-wallpaper-v1';
let design=normalize(), signatures=[], selected=null, mask=null, loadId=0, frame=0;
const masks=new Map(), tile=document.createElement('canvas');
let storageAvailable=true;
try {design=normalize(JSON.parse(localStorage.getItem(KEY)||'{}'));} catch {storageAvailable=false;}
const requested=new URLSearchParams(location.search).get('qid');
if(requested && /^Q\d+$/.test(requested)) design.qid=requested;
const fields=['stripeWidth','gapWidth','rowGap','scale','stripeColor','gapColor','inkColor','zoom'];
const inches=n=>`${Number(n.toFixed(2))} in`;
function notice(message='') {$('notice').textContent=message;$('notice').hidden=!message;}
function persist() {
  try {localStorage.setItem(KEY,JSON.stringify(design));storageAvailable=true;}
  catch {storageAvailable=false;}
  $('saveStatus').textContent=storageAvailable?'Saved in this browser':'Use “Save design settings” to keep your work';
}
function controls() {
  for(const field of fields) {
    $(field).value=design[field];
    $(field+'Value').textContent=field.endsWith('Color')?design[field]:['scale','zoom'].includes(field)?design[field]+'%':inches(design[field]);
  }
  document.querySelectorAll('[name=repeat]').forEach(r=>r.checked=r.value===design.repeat);
}
function schedule() {if(!frame) frame=requestAnimationFrame(()=>{frame=0;render();});}
function render() {
  if(!mask) return;
  const g=drawTile(tile,mask,design), canvas=$('wallpaperPreview');
  const {width,height}=$('previewFrame').getBoundingClientRect();
  const dpr=Math.min(devicePixelRatio||1,2);
  canvas.width=Math.round(width*dpr);canvas.height=Math.round(height*dpr);
  const ctx=canvas.getContext('2d');
  const scale=dpr*design.zoom/100*0.55;
  ctx.scale(scale,scale);ctx.fillStyle=ctx.createPattern(tile,'repeat');
  ctx.fillRect(0,0,canvas.width/scale,canvas.height/scale);
  canvas.dataset.signature=design.qid;canvas.dataset.repeat=design.repeat;
  canvas.setAttribute('aria-label',`${selected.full_name} wallpaper, ${design.repeat==='half-drop'?'half-drop':'straight'} repeat`);
  $('tileSummary').textContent=`Repeat tile: ${inches(g.width/g.ppi)} × ${inches(g.height/g.ppi)} · ${design.repeat==='half-drop'?'Half-drop':'Straight'} repeat`;
  $('previewLoading').hidden=true;$('downloadTile').disabled=false;
}
function signatureMask(img) {
  const ratio=Math.min(1,1600/Math.max(img.naturalWidth,img.naturalHeight));
  const source=document.createElement('canvas');source.width=Math.max(1,Math.round(img.naturalWidth*ratio));source.height=Math.max(1,Math.round(img.naturalHeight*ratio));
  const ctx=source.getContext('2d',{willReadFrequently:true});ctx.drawImage(img,0,0,source.width,source.height);
  const pixels=ctx.getImageData(0,0,source.width,source.height),d=pixels.data;
  let minX=source.width,minY=source.height,maxX=-1,maxY=-1;
  for(let y=0;y<source.height;y++) for(let x=0;x<source.width;x++) {
    const i=(y*source.width+x)*4;
    // Keep the dark handwriting and remove white paper, including opaque JPG grounds.
    const alpha=Math.round((255-Math.min(d[i],d[i+1],d[i+2]))*d[i+3]/255);
    d[i]=d[i+1]=d[i+2]=0;d[i+3]=alpha;
    if(alpha>20){minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x);maxY=Math.max(maxY,y);}
  }
  if(maxX<minX || maxY<minY) throw new Error('No signature ink found');
  ctx.putImageData(pixels,0,0);
  const result=document.createElement('canvas');result.width=maxX-minX+1;result.height=maxY-minY+1;
  result.getContext('2d').drawImage(source,minX,minY,result.width,result.height,0,0,result.width,result.height);
  return result;
}
function loadImage(url) {
  return new Promise((resolve,reject)=>{
    const image=new Image();image.crossOrigin='anonymous';
    const timer=setTimeout(()=>{image.onload=image.onerror=null;reject(new Error('Image timed out'));},18000);
    image.onload=()=>{clearTimeout(timer);resolve(image);};image.onerror=()=>{clearTimeout(timer);reject(new Error('Image unavailable'));};image.src=url;
  });
}
function updateURL() {
  const url=new URL(location.href);url.searchParams.set('qid',design.qid);url.searchParams.delete('name');
  history.replaceState(null,'',url.pathname+url.search+url.hash);
}
async function choose(qid) {
  const sig=signatures.find(s=>s.qid===qid);if(!sig)return;
  selected=sig;design.qid=qid;const token=++loadId;mask=null;
  $('selectedName').textContent=$('previewName').textContent=sig.full_name;
  $('selectedCategory').textContent=sig.category||'';
  $('signatureThumb').src=sig.signature_image_url;$('signatureThumb').alt=`Signature of ${sig.full_name}`;$('signatureThumb').hidden=false;
  $('wearSignature').href='/wear?qid='+encodeURIComponent(qid);
  $('signatureSelect').value=qid;$('downloadTile').disabled=true;$('downloadDesign').disabled=false;
  $('previewLoading').textContent='Loading your signature…';$('previewLoading').hidden=false;$('retry').hidden=true;notice();
  updateURL();persist();
  try {
    const source=new URL(sig.signature_image_url,location.href);
    if(source.protocol!=='https:' && source.origin!==location.origin)throw new Error('Unsupported image URL');
    const next=masks.get(qid)||signatureMask(await loadImage(source.href));
    if(token!==loadId)return;
    masks.set(qid,next);mask=next;render();
  } catch {
    if(token!==loadId)return;
    $('previewLoading').textContent='This signature image could not load.';
    notice('The archive image is unavailable or cannot be used for a downloadable preview. Try again or choose another name. Your colors and spacing are kept.');
    $('retry').hidden=false;
  }
}
function filter() {
  const q=$('signatureSearch').value.trim().toLocaleLowerCase();
  const matches=signatures.filter(s=>s.full_name.toLocaleLowerCase().includes(q));
  $('signatureSelect').replaceChildren();
  for(const sig of matches.slice(0,80)) {
    const option=document.createElement('option');option.value=sig.qid;option.textContent=sig.full_name;
    option.selected=sig.qid===design.qid;$('signatureSelect').append(option);
  }
  // Searching never silently replaces the signature being designed.
  if(!matches.slice(0,80).some(s=>s.qid===design.qid)) $('signatureSelect').selectedIndex=-1;
  $('searchCount').textContent=matches.length?`${matches.length.toLocaleString()} names${matches.length>80?' · type to narrow the list':''}`:'No names match. Try another spelling.';
}
for(const field of fields) $(field).addEventListener('input',()=>{
  design=normalize({...design,[field]:$(field).value});controls();persist();schedule();
});
document.querySelectorAll('[name=repeat]').forEach(r=>r.addEventListener('change',()=>{if(r.checked){design.repeat=r.value;persist();schedule();}}));
$('signatureSearch').addEventListener('input',filter);
$('signatureSearch').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();const first=$('signatureSelect').options[0];if(first)choose(first.value);}});
$('signatureSelect').addEventListener('change',()=>choose($('signatureSelect').value));
$('retry').addEventListener('click',()=>signatures.length?choose(design.qid):start());
$('signatureThumb').addEventListener('error',()=>{$('signatureThumb').hidden=true;});
$('reset').addEventListener('click',()=>{design=normalize({...DEFAULTS,qid:design.qid});controls();persist();schedule();notice();});
function download(blob,name) {
  const url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download=name;document.body.append(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);
}
const filename=()=>`${(selected?.full_name||'signature').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')}-wallpaper`;
$('downloadTile').addEventListener('click',async()=>{
  if(!mask)return;
  try {
    const name=filename(),out=document.createElement('canvas'),g=drawTile(out,mask,design,150);
    const blob=await new Promise(resolve=>out.toBlob(resolve,'image/png'));
    if(!blob)throw new Error('Export unavailable');
    download(blob,name+'-repeat.png');
    notice(`Repeat tile downloaded: ${g.width} × ${g.height} pixels. Save the design settings for its intended dimensions.`);
  } catch {notice('The image could not be exported. Try loading the signature again, then download.');$('retry').hidden=false;}
});
$('downloadDesign').addEventListener('click',()=>{
  const g=mask?geometry(design,mask.width/mask.height,150):null;
  download(new Blob([JSON.stringify({kind:'celebrity-signature-wallpaper',version:1,design,signatureName:selected?.full_name,source:selected?.signature_image_url,
    tile:g?{widthPixels:g.width,heightPixels:g.height,widthInches:g.width/g.ppi,heightInches:g.height/g.ppi,pixelsPerInch:g.ppi}:null},null,2)],{type:'application/json'}),filename()+'.json');
});
$('importDesign').addEventListener('change',async()=>{
  try {
    const file=$('importDesign').files[0];if(!file)return;
    if(file.size>100000)throw new Error('Too large');
    const saved=JSON.parse(await file.text());
    if(saved.kind!=='celebrity-signature-wallpaper'||saved.version!==1||!saved.design||!signatures.some(s=>s.qid===saved.design.qid))throw new Error('Unknown design');
    design=normalize(saved.design);$('signatureSearch').value='';controls();filter();await choose(design.qid);
  } catch {notice('That file is not a supported saved wallpaper design. Your current design has been kept.');}
  finally {$('importDesign').value='';}
});
async function start() {
  $('retry').hidden=true;
  try {
    const response=await fetch('/api/wear/signatures',{signal:AbortSignal.timeout(20000)});
    if(!response.ok)throw new Error('Catalog unavailable');
    const result=await response.json();
    signatures=(result.signatures||[]).filter(s=>/^Q\d+$/.test(s.qid)&&s.full_name&&s.signature_image_url).sort((a,b)=>a.full_name.localeCompare(b.full_name));
    if(!signatures.length)throw new Error('Empty catalog');
    let missing=false;
    if(!signatures.some(s=>s.qid===design.qid)){missing=true;design.qid=signatures.some(s=>s.qid==='Q91')?'Q91':signatures[0].qid;}
    $('signatureSearch').disabled=$('signatureSelect').disabled=false;controls();filter();await choose(design.qid);
    if(missing)notice('That signature is not available in the wallpaper catalog. A different name is selected; you can search for another.');
  } catch {
    $('selectedName').textContent=$('previewName').textContent='Choose a signature';
    $('searchCount').textContent='The signature catalog is unavailable.';
    $('previewLoading').textContent='The signature catalog could not load.';
    notice('Please try loading the catalog again. Your saved design settings are kept.');$('retry').hidden=false;
  }
}
controls();new ResizeObserver(schedule).observe($('previewFrame'));start();