Files
SpineViewer/tests/pma-color-rendering.html
T
2026-09-11 09:49:42 +08:00

51 lines
3.4 KiB
HTML

<!doctype html><meta charset="utf-8"><title>PMA and vertex color regression</title>
<pre id="result">RUNNING</pre>
<script>var module={exports:{}};</script>
<script src="../js/spine-webgl-rasterizer.js"></script>
<script>
(async()=>{try{
const api=module.exports;
function check(ok,message){if(!ok)throw Error(message)}
function canvas(){const c=document.createElement('canvas');c.width=c.height=32;return c.getContext('2d')}
// Produce a PNG whose low-alpha RGB values can be represented exactly by
// Canvas's 8-bit storage; PMA consumers must preserve those encoded values.
const encoded=canvas(),pixels=encoded.createImageData(32,32);
for(let i=0;i<pixels.data.length;i+=4)pixels.data.set([16,8,0,32],i);
encoded.putImageData(pixels,0,0);
const blob=await new Promise(resolve=>encoded.canvas.toBlob(resolve));
const image=new Image();image.src=URL.createObjectURL(blob);await image.decode();
await api.prepareTexture(image,blob);
// Read the encoded PNG samples via a non-premultiplied ImageBitmap in WebGL.
const rawCanvas=document.createElement('canvas'),gl=rawCanvas.getContext('webgl');
const t=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,t);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,await createImageBitmap(blob,{premultiplyAlpha:'none'}));
const fb=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,fb);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);
const samples=new Uint8Array(4);gl.readPixels(0,0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,samples);
gl.deleteFramebuffer(fb);gl.deleteTexture(t);
const target=canvas(),raster=api.createRasterizer(target);check(raster,'WebGL unavailable');
const modes=['source-over','lighter','multiply','screen'];
const light=[0.8,0.5,0.25,0.6],dark=[0.1,0.2,0.3,0];
for(const pma of [true,false,true])for(const mode of modes){
target.setTransform(1,0,0,1,0,0);target.globalCompositeOperation='source-over';target.fillStyle='rgb(50,60,70)';target.fillRect(0,0,32,32);
check(raster.begin(pma),'begin');target.globalCompositeOperation=mode;
const colors=[...light,...light,...light],darks=[...dark,...dark,...dark];
raster.triangle(image,0,0,0,0,32,0,1,0,32,32,1,1,colors,darks);
raster.triangle(image,0,0,0,0,32,32,1,1,0,32,0,1,colors,darks);raster.end();
const actual=target.getImageData(16,16,1,1).data,alpha=samples[3]/255*light[3];
for(let c=0;c<3;c++){
const tex=samples[c]*(pma?1:samples[3]/255),s=(tex*light[c]+(samples[3]-tex)*dark[c])*light[3],d=[50,60,70][c];
const expected=mode==='lighter'?s+d:mode==='multiply'?s*d/255+d*(1-alpha):mode==='screen'?s+d*(1-s/255):s+d*(1-alpha);
check(Math.abs(actual[c]-expected)<2.5,JSON.stringify({pma,mode,c,actual:actual[c],expected}));
}
}
raster.dispose();URL.revokeObjectURL(image.src);
// Different per-vertex colors in the same batch must interpolate separately.
const white=canvas();white.fillStyle='white';white.fillRect(0,0,32,32);
const gradient=canvas(),g=api.createRasterizer(gradient);g.begin();
g.triangle(white.canvas,0,0,0,0,32,0,1,0,0,32,0,1,[1,0,0,1,0,1,0,1,0,0,1,1]);g.end();
const pixel=gradient.getImageData(8,8,1,1).data;
check(pixel[0]>100&&pixel[1]>50&&pixel[2]>50,'per-vertex color interpolation');g.dispose();
document.getElementById('result').textContent='PASS: direct PMA samples, straight/PMA toggles, light/dark tint, four blend modes and per-vertex colors';
}catch(e){document.getElementById('result').textContent='FAIL '+e.stack}})();
</script>