openusd-explorer / src /preview.ts
ZIP Space Deployer
Deploy ZIP — 2026-10-26 20:15 UTC
0dfa89f
Raw History Blame Contribute Delete
2.45 kB
import type {Asset} from './types.ts';
/** Repository file listings can be truncated. Only a failed image request,
* not absence from that listing, means a thumbnail is unavailable.
* Also upgrades catalogues generated by earlier app versions.
*/
export function previewSources(asset:Pick<Asset,'image'|'imageFallback'|'preview'>):string[]{
const prepared=asset.preview?[asset.preview]:[];
const url=new URL(asset.image);
if(!url.pathname.endsWith('.png'))return [...new Set([...prepared,asset.image,asset.imageFallback])];
const base=url.pathname.replace(/(?:_(?:transparent|light))?\.png$/,'');
const variants=['_transparent.png','_light.png','.png'].map(suffix=>{
const candidate=new URL(url);candidate.pathname=base+suffix;return candidate.href;
});
return [...new Set([...prepared,...variants,asset.imageFallback,asset.image])];
}
/** Read native alpha bounds without altering colors or transparency.
* Opaque previews retain their background; color-keying them can destroy
* dark details and leave baked shadows behind. Antialiased edges stay intact.
*/
export function alphaBounds(data:Uint8ClampedArray,width:number,height:number){
let left=width,right=-1,top=height,bottom=-1;
for(let y=0;y<height;y++)for(let x=0;x<width;x++)if(data[(y*width+x)*4+3]>0){
left=Math.min(left,x);right=Math.max(right,x);top=Math.min(top,y);bottom=Math.max(bottom,y);
}
return right<left||bottom<top?null:{left,top,width:right-left+1,height:bottom-top+1};
}
export function normalizePreview(image:HTMLImageElement):string|null{
try{
const canvas=document.createElement('canvas');canvas.width=image.naturalWidth;canvas.height=image.naturalHeight;
if(canvas.width>2048||canvas.height>2048)return null;
const context=canvas.getContext('2d',{willReadFrequently:true});if(!context)return null;context.drawImage(image,0,0);
const pixels=context.getImageData(0,0,canvas.width,canvas.height);
const bounds=alphaBounds(pixels.data,canvas.width,canvas.height);if(!bounds)return null;
const output=document.createElement('canvas');output.width=output.height=320;const out=output.getContext('2d');if(!out)return null;
out.imageSmoothingEnabled=true;out.imageSmoothingQuality='high';
const {left,top,width:w,height:h}=bounds,scale=292/Math.max(w,h);
out.drawImage(canvas,left,top,w,h,(320-w*scale)/2,(320-h*scale)/2,w*scale,h*scale);
return output.toDataURL('image/png');
}catch{return null;}
}