加入3.5 修复水印
This commit is contained in:
1 parent
ef01cdd6ee
commit
e82a62a9ad
19 files changed
+5894
-38
No files matched your search
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
const fs=require('fs'),path=require('path'),os=require('os');
|
||||
const root=path.resolve(__dirname,'..'),fixtures=path.join(__dirname,'.fixtures');
|
||||
const script=text=>'<script>'+text.replace(/<\/script/gi,'<\\/script')+'</script>';
|
||||
const versions=['3.4','3.5','3.6','3.7','3.8','4.0','4.1','4.2','4.3'];
|
||||
const frames=versions.map(version=>{
|
||||
const dir=path.join(fixtures,version,'spineboy');
|
||||
const source=path.join(dir,['3.4','3.5'].includes(version)?'spineboy-mesh.skel':'spineboy-pro.skel');
|
||||
let html=script(`var module={exports:{}};const modules={};function require(n){return modules[n]}const version=${JSON.stringify(version)};`);
|
||||
for(const name of ['spine-webgl-rasterizer','spine-canvas-clipping','spine-background','spine-legacy-binary'])
|
||||
html+=script('module={exports:{}};\n'+fs.readFileSync(path.join(root,'js',name+'.js'),'utf8')+`\nmodules['./${name}.js']=module.exports;`);
|
||||
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();
|
||||
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}}
|
||||
else atlas=new spine.TextureAtlas(text,()=>texture);
|
||||
modules['./spine-legacy-binary.js'].install(spine,version);
|
||||
const bytes=Uint8Array.from(atob('${fs.readFileSync(source).toString('base64')}'),c=>c.charCodeAt(0));
|
||||
const data=new spine.SkeletonBinary(new spine.AtlasAttachmentLoader(atlas)).readSkeletonData(bytes),skeleton=new spine.Skeleton(data);
|
||||
if(spine.Physics)skeleton.updateWorldTransform(spine.Physics.update);else skeleton.updateWorldTransform();
|
||||
const offset=new spine.Vector2(),size=new spine.Vector2();skeleton.getBounds(offset,size,[]);
|
||||
const canvas=document.createElement('canvas');canvas.width=canvas.height=400;document.body.appendChild(canvas);
|
||||
const ctx=canvas.getContext('2d'),renderer=modules['./spine-canvas-clipping.js'].createRenderer(spine,ctx,version);
|
||||
if(renderer.rasterization!=='webgl')throw Error('WebGL not active');
|
||||
let normal;
|
||||
for(const mode of [0,2]){
|
||||
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=mode;
|
||||
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');}
|
||||
}
|
||||
renderer.premultipliedAlpha=true;renderer.draw(skeleton);renderer.dispose();
|
||||
parent.postMessage({version,result:'PASS'},'*');
|
||||
}catch(error){parent.postMessage({version,error:error.stack},'*')}})();`);
|
||||
return html;
|
||||
});
|
||||
const html='<!doctype html><meta charset="utf-8"><style>iframe{width:420px;height:420px;border:0}body{background:#222;color:#eee}</style><pre id="result">RUNNING</pre>'+script(`let results=[];window.addEventListener('message',e=>{if(e.data.version){results.push(e.data);document.getElementById('result').textContent=JSON.stringify(results)}});for(const html of ${JSON.stringify(frames)}){const frame=document.createElement('iframe');frame.srcdoc=html;document.body.appendChild(frame)}`);
|
||||
const output=path.resolve(process.argv[2]||path.join(os.tmpdir(),'spineviewer-browser-preview-test.html'));
|
||||
fs.writeFileSync(output,html);console.log(output);
|
||||
@@ -15,7 +15,7 @@ for (const name of ['notes.txt', 'payload.bytes', 'hero.atlas.txt.bak']) assert.
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-extensions-test-'));
|
||||
try {
|
||||
// Sibling directories with identical filenames must not share stale indexes.
|
||||
for (const version of ['3.4', '3.6', '3.7', '3.8', '4.0', '4.1', '4.2', '4.3']) {
|
||||
for (const version of ['3.4', '3.5', '3.6', '3.7', '3.8', '4.0', '4.1', '4.2', '4.3']) {
|
||||
const directory = path.join(root, version); fs.mkdirSync(directory);
|
||||
const skeleton = path.join(directory, 'hero.skel.bytes');
|
||||
const atlas = path.join(directory, 'hero.atlas.txt');
|
||||
|
||||
@@ -4,17 +4,18 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
const destination = path.resolve(process.argv[2] || path.join(__dirname, '.fixtures'));
|
||||
if (process.argv[3] === '3.4') {
|
||||
const revision = 'ef5013143178918ddfc7cabfbddeae7aec7cf84e';
|
||||
if (['3.4', '3.5'].includes(process.argv[3])) {
|
||||
const version = process.argv[3];
|
||||
const revision = version === '3.4' ? 'ef5013143178918ddfc7cabfbddeae7aec7cf84e' : '2cd9467d2736b7f3a0b81382f117c73823bfb44b';
|
||||
for (const name of ['spineboy', 'goblins', 'vine', 'stretchyman', 'tank', 'raptor']) {
|
||||
const directory = path.join(destination, '3.4', name);
|
||||
const directory = path.join(destination, version, name);
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
const variants = name === 'spineboy' ? [name, name + '-mesh', name + '-hover'] : name === 'goblins' ? [name, name + '-mesh'] : [name];
|
||||
for (const file of [...variants.flatMap(v => [v + '.json', v + '.skel']), name + '.atlas', name + '.png']) {
|
||||
const url = `https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${revision}/examples/${name}/export/${file}`;
|
||||
execFileSync('curl', ['--http1.1', '-LfsS', '--retry', '2', '--max-time', '60', url, '-o', path.join(directory, file)]);
|
||||
}
|
||||
console.log('3.4 / ' + name);
|
||||
console.log(version + ' / ' + name);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
// Version-pinned, complete official export sets for manual Eagle testing.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { execFile } = require('child_process');
|
||||
const root = path.resolve(process.argv[2] || path.join(__dirname, '.fixtures'));
|
||||
const revisions = {
|
||||
'3.4': 'ef5013143178918ddfc7cabfbddeae7aec7cf84e',
|
||||
'3.5': '2cd9467d2736b7f3a0b81382f117c73823bfb44b',
|
||||
'3.6': '654c20e5b0e523040b6366bbd1042510d2645134',
|
||||
'3.7': '9639bcc81722d7178fd9d1cdc1a3d55a4c91f989',
|
||||
'3.8': '8b4844bd4b193ba9e54487ed397a777993cbad56',
|
||||
'4.0': '425ce416bb218b28caeec47b317aa57cd7140375',
|
||||
'4.1': '77a5db0ec6d16331f5efbaa7662bba9355bd3424',
|
||||
'4.2': 'e7dc1435fa4a0083ab431f1b28e083c14a1f5c68',
|
||||
'4.3': 'ba17cf88b198c6531c6c51d8d4e1e4b3c3ab47dd'
|
||||
};
|
||||
const names = ['spineboy', 'goblins', 'vine', 'stretchyman', 'tank', 'coin', 'raptor'];
|
||||
const curl = (url, file) => new Promise((resolve, reject) => execFile('curl', ['--http1.1', '-LfsS', '--retry', '2', '--max-time', '60', url, '-o', file], error => error ? reject(error) : resolve()));
|
||||
function blobHash(file) {
|
||||
const bytes = fs.readFileSync(file);
|
||||
return crypto.createHash('sha1').update('blob ' + bytes.length + '\0').update(bytes).digest('hex');
|
||||
}
|
||||
async function pool(jobs) {
|
||||
let cursor = 0;
|
||||
await Promise.all(Array.from({ length: 6 }, async () => { while (cursor < jobs.length) await jobs[cursor++](); }));
|
||||
}
|
||||
(async () => {
|
||||
fs.mkdirSync(root, { recursive: true });
|
||||
const inventory = [];
|
||||
for (const [version, revision] of Object.entries(revisions)) {
|
||||
const folder = path.join(root, version); fs.mkdirSync(folder, { recursive: true });
|
||||
let previous = [];
|
||||
try { previous = JSON.parse(fs.readFileSync(path.join(folder, '.sources.json'), 'utf8')).files; } catch (_) {}
|
||||
const treeFile = path.join(folder, '.tree.json');
|
||||
await curl(`https://api.github.com/repos/EsotericSoftware/spine-runtimes/git/trees/${revision}?recursive=1`, treeFile);
|
||||
const tree = JSON.parse(fs.readFileSync(treeFile, 'utf8'));
|
||||
if (tree.truncated || !tree.tree || tree.sha !== revision) throw new Error('Incomplete source tree: ' + version);
|
||||
const license = tree.tree.find(f => f.path === 'LICENSE');
|
||||
if (license) {
|
||||
const dest = path.join(folder, 'LICENSE.txt');
|
||||
if (!fs.existsSync(dest) || blobHash(dest) !== license.sha) await curl(`https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${revision}/LICENSE`, dest);
|
||||
if (blobHash(dest) !== license.sha) throw new Error('License checksum mismatch: ' + version);
|
||||
}
|
||||
const files = tree.tree.filter(f => f.type === 'blob' && names.some(n => f.path.startsWith('examples/' + n + '/export/')) && /\.(json|skel|atlas|png)$/.test(f.path) && !/-pma/.test(f.path));
|
||||
for (const old of previous.filter(old => !files.some(f => f.path === old.path))) {
|
||||
const parts = old.path.split('/'), dest = path.join(folder, parts[1], parts.slice(3).join('/'));
|
||||
if (fs.existsSync(dest) && blobHash(dest) === old.sha) {
|
||||
fs.unlinkSync(dest);
|
||||
if (!fs.readdirSync(path.dirname(dest)).length) fs.rmdirSync(path.dirname(dest));
|
||||
}
|
||||
}
|
||||
await pool(files.map(f => async () => {
|
||||
const parts = f.path.split('/'), dest = path.join(folder, parts[1], parts.slice(3).join('/'));
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
if (fs.existsSync(dest) && blobHash(dest) === f.sha) return;
|
||||
const temp = dest + '.download';
|
||||
await curl(`https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${revision}/${f.path}`, temp);
|
||||
if (blobHash(temp) !== f.sha) throw new Error('Checksum mismatch: ' + f.path);
|
||||
fs.renameSync(temp, dest);
|
||||
}));
|
||||
for (const f of files.filter(f => f.path.endsWith('.json'))) {
|
||||
const parts = f.path.split('/');
|
||||
const data = JSON.parse(fs.readFileSync(path.join(folder, parts[1], parts.slice(3).join('/')), 'utf8'));
|
||||
if (!data.skeleton || !data.skeleton.spine.startsWith(version + '.')) throw new Error('Wrong export version: ' + f.path);
|
||||
}
|
||||
const entry = { version, revision, files: files.map(f => ({ path: f.path, sha: f.sha, size: f.size })) };
|
||||
fs.writeFileSync(path.join(folder, '.sources.json'), JSON.stringify(entry, null, 2));
|
||||
inventory.push(entry);
|
||||
console.log(version + ': ' + files.length + ' official files verified');
|
||||
}
|
||||
const rows = inventory.map(e => '| ' + e.version + ' | ' + new Set(e.files.map(f => f.path.split('/')[1])).size + ' | ' + e.files.length + ' | `' + e.revision + '` |').join('\n');
|
||||
fs.writeFileSync(path.join(root, 'README.md'), '# Spine 官方测试资源\n\n按版本和示例分目录,骨骼、Atlas 和贴图均来自官方仓库,所有文件已校验 Git blob SHA-1。这里下载的是普通 Alpha 导出,未混入同名 PMA 图集。\n\n| 版本系列 | 示例组 | 文件数 | 固定提交 |\n|---|---:|---:|---|\n' + rows + '\n\n将单个示例文件夹导入 Eagle。JSON 与 SKEL 可分别测试;测试复合后缀时请复制文件后将 `.atlas` 改为 `.atlas.txt`、`.skel` 改为 `.skel.bytes`。保留原始版本作为对照。每个版本的 `.sources.json` 记录来源文件及校验值。\n\n官方仓库:https://github.com/EsotericSoftware/spine-runtimes\n');
|
||||
console.log('Saved to ' + root);
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
+14
-10
@@ -24,30 +24,34 @@ function runtime(version) {
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '..', 'lib', version, 'spine-canvas.js'), 'utf8'), context);
|
||||
return install(context.spine, version);
|
||||
}
|
||||
const versions = process.argv[3] === '3.4' ? ['3.4'] : ['3.6', '3.7'];
|
||||
const versions = ['3.4', '3.5'].includes(process.argv[3]) ? [process.argv[3]] : ['3.6', '3.7'];
|
||||
let pairs = 0;
|
||||
for (const version of versions) {
|
||||
const spine = runtime(version);
|
||||
const oldNames = version === '3.4' || version === '3.5';
|
||||
// 3.4 JSON export truncates to two decimal places instead of rounding.
|
||||
const quantization = version === '3.4' ? 0.0102 : 0.0052;
|
||||
const quantization = oldNames ? 0.0102 : 0.0052;
|
||||
assert.equal(utils.chooseRuntimeDir(version + '.32', true), version);
|
||||
for (const name of version === '3.4' ? ['spineboy', 'spineboy-mesh', 'spineboy-hover', 'goblins', 'goblins-mesh', 'vine', 'stretchyman', 'tank', 'raptor'] : names) {
|
||||
const directory = path.join(fixtures, version, version === '3.4' ? name.split('-')[0] : name);
|
||||
const file = name + (version === '3.4' ? '' : '-pro');
|
||||
const oldExamples = ['spineboy', 'spineboy-mesh', 'spineboy-hover', 'goblins', 'goblins-mesh', 'vine', 'stretchyman', 'tank', 'raptor'];
|
||||
// The 3.5.51 tag keeps beta SKELs but newer 3.5.49 JSON for Tank/Raptor.
|
||||
// Those independent exports are smoke-tested separately, not used as parity oracles.
|
||||
for (const name of oldNames ? oldExamples.filter(n => version !== '3.5' || !['tank', 'raptor'].includes(n)) : names) {
|
||||
const directory = path.join(fixtures, version, oldNames ? name.split('-')[0] : name);
|
||||
const file = name + (oldNames ? '' : '-pro');
|
||||
const bytes = fs.readFileSync(path.join(directory, file + '.skel'));
|
||||
const json = JSON.parse(fs.readFileSync(path.join(directory, file + '.json'), 'utf8'));
|
||||
const decoded = decode(bytes, version);
|
||||
assert.equal(decoded.skeleton.hash, json.skeleton.hash);
|
||||
assert.equal(decoded.skeleton.spine, json.skeleton.spine);
|
||||
assert.equal(utils.detectVersion(path.join(directory, file + '.skel')), json.skeleton.spine);
|
||||
assert.equal(utils.detectVersion(path.join(directory, file + '.skel')), json.skeleton.spine.match(/^\d+\.\d+\.\d+/)[0]);
|
||||
// Region geometry uses a real official atlas; no native Canvas dependency
|
||||
// is needed to verify animation poses and renderer triangle output.
|
||||
const texture = { getImage: () => ({ width: 1024, height: 1024 }), setFilters() {}, setWraps() {} };
|
||||
const atlas = new spine.TextureAtlas(fs.readFileSync(path.join(directory, (version === '3.4' ? name.split('-')[0] : name) + '.atlas'), 'utf8'), () => texture);
|
||||
const atlas = new spine.TextureAtlas(fs.readFileSync(path.join(directory, (oldNames ? name.split('-')[0] : name) + '.atlas'), 'utf8'), () => texture);
|
||||
const attachmentLoader = new spine.AtlasAttachmentLoader(atlas);
|
||||
// 3.4 Goblins binary optimizes three constant segments to linear;
|
||||
// JSON keeps stepped. Their equal endpoints produce identical poses.
|
||||
if (version === '3.4' && name.startsWith('goblins')) {
|
||||
if (oldNames && name.startsWith('goblins')) {
|
||||
for (const [bone, type, index] of [['left lower leg', 'translate', 2], ['right lower leg', 'translate', 0], ['hip', 'rotate', 0]]) {
|
||||
const timeline = json.animations.walk.bones[bone][type];
|
||||
assert.equal(timeline[index].curve, 'stepped');
|
||||
@@ -92,7 +96,7 @@ for (const version of versions) {
|
||||
}
|
||||
for (const animation of data.animations) {
|
||||
const refAnimation = expected.findAnimation(animation.name);
|
||||
near(animation.duration, refAnimation.duration, version === '3.4' ? 0.00011 : 0.00006, 'duration');
|
||||
near(animation.duration, refAnimation.duration, oldNames ? 0.00011 : 0.00006, 'duration');
|
||||
assert.equal(animation.timelines.length, refAnimation.timelines.length);
|
||||
// Older JSON and binary exports order independent slot timelines differently.
|
||||
const timelineKey = t => [t.constructor.name, t.slotIndex, t.boneIndex, t.ikConstraintIndex, t.transformConstraintIndex, t.pathConstraintIndex, t.attachment && t.attachment.name].join('/');
|
||||
@@ -122,7 +126,7 @@ for (const version of versions) {
|
||||
// Quantized JSON weights/curves compound along bone chains.
|
||||
// 3.4 truncates weights/scales by up to 0.01. Bound accumulated
|
||||
// world-position error by 1% of export size; local data is checked above.
|
||||
const tolerance = version === '3.4' && field.startsWith('world')
|
||||
const tolerance = oldNames && field.startsWith('world')
|
||||
? Math.max(2.1, Math.max(json.skeleton.width, json.skeleton.height) * scale * 0.01) : 2.1;
|
||||
near(pair[0].bones[i][field], pair[1].bones[i][field], tolerance, name + '/' + animation.name + '/' + field);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
const fs=require('fs'),path=require('path'),assert=require('assert/strict');
|
||||
const render=require('../js/spine-render'),utils=require('../js/spine-utils'),clipping=require('../js/spine-canvas-clipping');
|
||||
const root=path.resolve(process.argv[2]||path.join(__dirname,'.fixtures'));
|
||||
(async()=>{
|
||||
let files=0,poses=0;
|
||||
for(const version of ['3.4','3.5','3.6','3.7','3.8','4.0','4.1','4.2','4.3']){
|
||||
const spine=render.loadRuntime(version);
|
||||
for(const name of fs.readdirSync(path.join(root,version)).filter(n=>!n.startsWith('.')&&fs.statSync(path.join(root,version,n)).isDirectory())){
|
||||
const dir=path.join(root,version,name),atlasPath=path.join(dir,name+'.atlas');
|
||||
if(!fs.existsSync(atlasPath))continue;
|
||||
const text=fs.readFileSync(atlasPath,'utf8');
|
||||
for(const page of utils.parseAtlasPages(text))assert(fs.existsSync(path.join(dir,page)),version+'/'+name+' missing '+page);
|
||||
const atlas=await render.buildTextureAtlas(spine,text,dir);
|
||||
for(const file of fs.readdirSync(dir).filter(n=>/\.(json|skel)$/.test(n))){
|
||||
const source=path.join(dir,file),detected=utils.detectVersion(source);
|
||||
assert.equal(utils.chooseRuntimeDir(detected,file.endsWith('.skel')),version,source+' version');
|
||||
const data=render.readSkeletonData(spine,source,atlas,file.endsWith('.skel'));
|
||||
assert(data.bones.length>0,source);
|
||||
const renderer=clipping.createRenderer(spine,{globalAlpha:1,globalCompositeOperation:'source-over'},version);
|
||||
let triangles=0;renderer.drawTriangle=(_image,...coords)=>{assert(coords.every(Number.isFinite),source+' geometry');triangles++};
|
||||
for(const animation of data.animations){
|
||||
const skeleton=new spine.Skeleton(data),state=new spine.AnimationState(new spine.AnimationStateData(data));
|
||||
state.setAnimation(0,animation.name,true);state.update(animation.duration*0.37);state.apply(skeleton);
|
||||
if(spine.Physics)skeleton.updateWorldTransform(spine.Physics.update);else skeleton.updateWorldTransform();
|
||||
renderer.draw(skeleton);poses++;
|
||||
}
|
||||
assert(triangles>0||data.animations.length===0,source+' empty geometry');files++;
|
||||
}
|
||||
}
|
||||
console.log('PASS '+version+' official exports');
|
||||
}
|
||||
console.log('PASS: '+files+' skeleton files, '+poses+' animation/render checks; all atlas pages present');
|
||||
})().catch(error=>{console.error(error);process.exitCode=1});
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html><meta charset="utf-8"><title>Spine preview compositing tests</title>
|
||||
<pre id="result">RUNNING</pre>
|
||||
<script>var module={exports:{}},__dirname='';function require(n){return n==='path'?{join:()=>''}:{}}</script>
|
||||
<script src="../js/spine-webgl-rasterizer.js"></script>
|
||||
<script>const rasterizer=module.exports;module={exports:{}};</script>
|
||||
<script src="../js/spine-background.js"></script>
|
||||
<script>const background=module.exports;module={exports:{}};</script>
|
||||
<script src="../js/spine-badge.js"></script>
|
||||
<script>
|
||||
try {
|
||||
const badge=module.exports;
|
||||
function check(ok,message){if(!ok)throw Error(message)}
|
||||
function canvas(w,h){const c=document.createElement('canvas');c.width=w;c.height=h;return c.getContext('2d')}
|
||||
function pixel(c,x,y){return c.getImageData(x,y,1,1).data}
|
||||
const grid=canvas(220,220);grid.translate(10,20);grid.scale(2,2);background.drawCheckerboard(grid,220,220);
|
||||
check(pixel(grid,109,70)[0]===90 && pixel(grid,110,70)[0]===80,'50-unit cells must scale to 100px and follow translation');
|
||||
const source=canvas(8,8);source.fillStyle='rgb(128,64,192)';source.fillRect(0,0,8,8);
|
||||
const destination=[80,120,160],color=[128,64,192];
|
||||
for(const mode of ['source-over','lighter','multiply','screen']) for(const alpha of [1,0.5]){
|
||||
const target=canvas(64,64);target.fillStyle='rgb(80,120,160)';target.fillRect(0,0,64,64);
|
||||
const raster=rasterizer.createRasterizer(target);check(raster&&raster.begin(),'WebGL unavailable');
|
||||
target.globalAlpha=alpha;target.globalCompositeOperation=mode;
|
||||
raster.triangle(source.canvas,8,8,0,0,56,8,1,0,56,56,1,1);
|
||||
raster.triangle(source.canvas,8,8,0,0,56,56,1,1,8,56,0,1);raster.end();
|
||||
const actual=pixel(target,32,32);
|
||||
for(let channel=0;channel<3;channel++){
|
||||
const s=color[channel]*alpha,d=destination[channel];
|
||||
const expected=mode==='lighter'?Math.min(255,s+d):mode==='multiply'?d*(s/255+1-alpha):mode==='screen'?s+d*(1-s/255):s+d*(1-alpha);
|
||||
check(Math.abs(actual[channel]-expected)<2,mode+' '+alpha+' channel '+channel+': '+actual[channel]+' vs '+expected);
|
||||
}
|
||||
check(actual[3]===255,'background alpha must remain opaque');
|
||||
target.globalAlpha=1;target.globalCompositeOperation='source-over';target.fillStyle='#ff0000';target.fillRect(0,0,64,64);
|
||||
raster.begin();raster.end();check(pixel(target,32,32)[0]===255&&pixel(target,32,32)[1]===0,'background texture must refresh each frame');
|
||||
raster.dispose();
|
||||
}
|
||||
const stamp=canvas(400,400);let text='';const fillText=stamp.fillText.bind(stamp);stamp.fillText=(s,...args)=>{text=s;fillText(s,...args)};
|
||||
badge.drawVersion(stamp,400,400,'4.3.75-beta');check(text==='4.3.75-beta','beta version watermark');
|
||||
let white=0;for(const value of stamp.getImageData(12,354,170,34).data)if(value>200)white++;
|
||||
check(white>100,'watermark pixels');
|
||||
document.getElementById('result').textContent='PASS: moving/scaling 50-unit checkerboard, four blend modes at two opacities, background refresh, 4.3 beta watermark';
|
||||
}catch(error){document.getElementById('result').textContent='FAIL: '+error.stack}
|
||||
</script>
|
||||
Reference in new issue
Block a user