预览一致性修复

This commit is contained in:
tianmo committed 2026-09-11 00:33:43 +08:00
1 parent 11b1c95d98
commit f0c7e27552
7 files changed
+189 -36

No files matched your search

+13
View File
@@ -12,6 +12,7 @@ const frames=versions.map(version=>{
html+=script(fs.readFileSync(path.join(root,'lib',version,'spine-canvas.js'),'utf8'));
html+=script(`(async()=>{try{
const image=new Image();image.src='data:image/png;base64,${fs.readFileSync(path.join(dir,'spineboy.png')).toString('base64')}';await image.decode();
await modules['./spine-webgl-rasterizer.js'].prepareTexture(image,await (await fetch(image.src)).blob());
const text=${JSON.stringify(fs.readFileSync(path.join(dir,'spineboy.atlas'),'utf8'))};
const texture=new (spine.CanvasTexture||spine.canvas.CanvasTexture)(image);let atlas;
if(spine.TextureAtlas.length<=1){atlas=new spine.TextureAtlas(text);for(const page of atlas.pages){if(page.setTexture)page.setTexture(texture);else page.texture=texture}}
@@ -33,6 +34,18 @@ const frames=versions.map(version=>{
renderer.draw(skeleton);const pixels=ctx.getImageData(0,0,400,400).data;
if(mode===0)normal=pixels;else{let changed=0;for(let i=0;i<pixels.length;i+=4)if(Math.abs(pixels[i]-normal[i])>3)changed++;if(changed<100)throw Error('Slot multiply did not change image');}
}
let black;
for(const tint of [[0,0,0],[0.25,0.5,0.75]]){
ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#1e1e1e';ctx.fillRect(0,0,400,400);
const scale=320/Math.max(size.x,size.y);ctx.translate(200,200);ctx.scale(scale,-scale);ctx.translate(-offset.x-size.x/2,-offset.y-size.y/2);
modules['./spine-background.js'].drawCheckerboard(ctx,400,400);
for(const slot of skeleton.slots)slot.data.blendMode=0;
[skeleton.color.r,skeleton.color.g,skeleton.color.b]=tint;
renderer.draw(skeleton);const pixels=ctx.getImageData(0,0,400,400).data;
if(tint[0]===0)black=pixels;
else{let checked=0,wrong=0;for(let i=0;i<pixels.length;i+=4)for(let c=0;c<3;c++)if(normal[i+c]-black[i+c]>30){checked++;const expected=black[i+c]+(normal[i+c]-black[i+c])*tint[c];if(Math.abs(pixels[i+c]-expected)>5)wrong++;}
if(checked<100||wrong>checked*0.01)throw Error('Vertex tint mismatch: '+wrong+'/'+checked);}
}
renderer.premultipliedAlpha=true;renderer.draw(skeleton);renderer.dispose();
parent.postMessage({version,result:'PASS'},'*');
}catch(error){parent.postMessage({version,error:error.stack},'*')}})();`);
+44
View File
@@ -0,0 +1,44 @@
'use strict';
// Local visual diagnostic. The private project/export remain outside version control.
const fs = require('fs'), path = require('path');
const root = path.resolve(__dirname, '..');
const library = '/Users/tianmokeji/Desktop/特效/特效.library/images';
const source = process.argv[2] || path.join(library, 'MTV7M6B96MAP6.info/x10.skel');
const atlasPath = process.argv[3] || path.join(library, 'MTV7M6B96RPMA.info/x10.atlas');
const texturePath = process.argv[4] || path.join(library, 'MTV7M6B9BJ9MS.info/x10.png');
const output = process.argv[5] || '/tmp/spine-x10-comparison.html';
const baseline = process.env.SPINE_COMPARE_BASELINE === '1';
const script = s => '<script>' + s.replace(/<\/script/gi, '<\\/script') + '</script>';
let html = '<!doctype html><meta charset="utf-8"><style>body{margin:0;background:#555;color:white;font:16px sans-serif}canvas{width:486.5px;height:278.5px}section{display:inline-block}pre{white-space:pre-wrap}</style><pre id="result">RUNNING</pre>';
html += script('var module={exports:{}};const modules={};function require(n){return modules[n]}');
for (const name of ['spine-webgl-rasterizer', 'spine-canvas-clipping'])
html += script('module={exports:{}};\n' + (baseline ? require('child_process').execFileSync('git',['show','HEAD:js/'+name+'.js'],{cwd:root,encoding:'utf8'}) : fs.readFileSync(path.join(root, 'js', name+'.js'), 'utf8')) + `\nmodules['./${name}.js']=module.exports;`);
html += script(fs.readFileSync(path.join(root, 'lib/4.2/spine-canvas.js'), 'utf8'));
html += script(`(async()=>{try{
const raw=Uint8Array.from(atob('${fs.readFileSync(texturePath).toString('base64')}'),c=>c.charCodeAt(0));
const image=new Image();image.src='data:image/png;base64,${fs.readFileSync(texturePath).toString('base64')}';await image.decode();
if(modules['./spine-webgl-rasterizer.js'].prepareTexture) await modules['./spine-webgl-rasterizer.js'].prepareTexture(image,new Blob([raw],{type:'image/png'}));
const atlas=new spine.TextureAtlas(${JSON.stringify(fs.readFileSync(atlasPath,'utf8'))});for(const page of atlas.pages)page.setTexture(new spine.CanvasTexture(image));
const data=new spine.SkeletonBinary(new spine.AtlasAttachmentLoader(atlas)).readSkeletonData(Uint8Array.from(atob('${fs.readFileSync(source).toString('base64')}'),c=>c.charCodeAt(0)));
const reference=new Image();reference.src='data:image/png;base64,${fs.readFileSync(path.join(__dirname,'对比测试.png')).toString('base64')}';await reference.decode();
const reports=[];
function panel(name){const e=document.createElement('section');e.innerHTML='<div>'+name+'</div>';const c=document.createElement('canvas');c.width=973;c.height=557;e.appendChild(c);document.body.appendChild(e);return c;}
const rc=panel('Spine editor export'),rx=rc.getContext('2d');rx.fillStyle='#555';rx.fillRect(0,0,973,557);rx.drawImage(reference,0,0);const expected=rx.getImageData(0,0,973,557).data;
function redBounds(pixels){let x0=973,y0=557,x1=0,y1=0;for(let y=0;y<557;y++){const xs=[];for(let x=0;x<973;x++){const i=(y*973+x)*4;if(pixels[i]>100&&pixels[i]>pixels[i+1]*2&&pixels[i]>pixels[i+2]*2)xs.push(x)}if(xs.length>24){x0=Math.min(x0,...xs);y0=Math.min(y0,y);x1=Math.max(x1,...xs);y1=Math.max(y1,y)}}return [x0,y0,x1,y1];}
const rb=redBounds(expected);
for(const frame of [7,8]){
const sk=new spine.Skeleton(data),state=new spine.AnimationState(new spine.AnimationStateData(data));state.setAnimation(0,'word_fly',false);state.update(frame/30);state.apply(sk);sk.updateWorldTransform(spine.Physics.update);
const off=new spine.Vector2(),size=new spine.Vector2();sk.getBounds(off,size,[]);
const c=panel('Plugin frame '+frame),ctx=c.getContext('2d');ctx.fillStyle='#555';ctx.fillRect(0,0,973,557);
// Editor exports tight bounds plus its export padding. Record bounds for registration.
const scale=Math.min(973/size.x,557/size.y);ctx.translate(973/2,557/2);ctx.scale(scale,-scale);ctx.translate(-off.x-size.x/2,-off.y-size.y/2);
const renderer=modules['./spine-canvas-clipping.js'].createRenderer(spine,ctx,'4.2');renderer.premultipliedAlpha=true;renderer.draw(sk);
const ab=redBounds(ctx.getImageData(0,0,973,557).data),ratio=(rb[2]-rb[0])/(ab[2]-ab[0]);
const transform=ctx.getTransform();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#555';ctx.fillRect(0,0,973,557);
ctx.translate((rb[0]+rb[2])/2-ratio*(ab[0]+ab[2])/2,(rb[1]+rb[3])/2-ratio*(ab[1]+ab[3])/2);ctx.scale(ratio,ratio);ctx.transform(transform.a,transform.b,transform.c,transform.d,transform.e,transform.f);renderer.draw(sk);renderer.dispose();
const actual=ctx.getImageData(0,0,973,557).data;let sum=0;for(let i=0;i<actual.length;i++)if(i%4!==3)sum+=Math.abs(actual[i]-expected[i]);
reports.push({frame,scale:scale*ratio,mae:sum/(973*557*3)});
}
document.getElementById('result').textContent=JSON.stringify(reports);
}catch(e){document.getElementById('result').textContent='FAIL '+e.stack}})();`);
fs.writeFileSync(output,html);console.log(output);
+50
View File
@@ -0,0 +1,50 @@
<!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([64,32,16,8],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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB