import fs from 'node:fs'; import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import {createCanvas} from '@napi-rs/canvas'; import {pathToFileURL} from 'node:url'; import path from 'node:path'; const repo=process.cwd(); const task=path.resolve('research-reproduction'); const out=task;fs.mkdirSync(out,{recursive:true});fs.mkdirSync(task+'/work',{recursive:true}); globalThis.document={createElement:n=>{assert.equal(n,'canvas');return createCanvas(1,1)}}; const {makeLayer,layerSurface,composite,validateDoc}=await import(pathToFileURL(path.join(repo,'src/document.ts')).href); const {makeAdjustment,compileAdjustment}=await import(pathToFileURL(path.join(repo,'src/adjustments.ts')).href); const {toneResponse}=await import(pathToFileURL(path.join(repo,'src/tone-response.ts')).href); const base=makeLayer({width:100,height:100,strokes:[{points:[{x:50,y:50}],size:200,color:'#0072b2',erase:false},{points:[{x:15,y:20},{x:85,y:80}],size:18,color:'#e69f00',erase:false}]}); const original=JSON.stringify(base),mask={width:100,height:100,enabled:true,inverted:false,strokes:[{points:[{x:50,y:50}],size:40,color:'#ffffff',erase:true}]}; const source=await layerSurface(base),masked=await layerSurface({...base,mask}),disabled=await layerSurface({...base,mask:{...mask,enabled:false}}); const pixels=c=>c.getContext('2d').getImageData(0,0,100,100).data; assert.equal(JSON.stringify(base),original); assert.deepEqual(pixels(source),pixels(disabled)); assert.equal(pixels(masked)[(50*100+50)*4+3],0); const restored=await layerSurface({...base,mask:{...mask,strokes:[...mask.strokes,{...mask.strokes[0],size:10,erase:false}]}}); assert.equal(pixels(restored)[(50*100+50)*4+3],255); const doc={format:'layer-studio',version:1,name:'mask diagnostic',width:100,height:100,layers:[{...base,mask}]}; const reopened=await composite(validateDoc(JSON.parse(JSON.stringify(doc)))); assert.deepEqual(pixels(reopened),pixels(masked)); // Deliberately destructive alternative, constructed for this article. Not a historical build. const control=createCanvas(100,100),ctx=control.getContext('2d');ctx.drawImage(source,0,0);ctx.globalCompositeOperation='destination-out';ctx.beginPath();ctx.arc(50,50,20,0,2*Math.PI);ctx.fill();ctx.globalCompositeOperation='source-over'; for(const [name,c]of Object.entries({source,masked,disabled,control}))fs.writeFileSync(out+'/mask-'+name+'.png',c.toBuffer('image/png')); const maskData={kind:'new controlled diagnostic',sourceRevision:'19f45ed6dd99800f910c0587baa53e73d46ab4fb',date:new Date().toISOString(),size:[100,100],checks:{sourceDocumentUnchanged:true,disabledMatchesSourceRGBA:true,maskHidesCenter:true,restoreStrokeRestoresCenter:true,JSONRoundtripMatchesRGBA:true},centerAlpha:{source:255,masked:0,destructiveControlAfterDisable:0,documentMaskDisabled:255},alphaProfile:Array.from({length:100},(_,x)=>({x,source:pixels(source)[(50*100+x)*4+3],masked:pixels(masked)[(50*100+x)*4+3],disabled:pixels(disabled)[(50*100+x)*4+3],destructiveControl:pixels(control)[(50*100+x)*4+3]})),controlNote:'Constructed destructive control; not an archived Photoshot failure. New source-level test, not a browser UI or Photoshop parity test.'}; fs.writeFileSync(out+'/mask-diagnostic.json',JSON.stringify(maskData,null,2)); const archived=JSON.parse(fs.readFileSync('brightness-results.json','utf8')); const recorded=Object.fromEntries(['brightness','hold_brightness','hold_brightness_dark'].map(n=>[n,{gray:archived.gray.filter(r=>r.case===n)}])); recorded.hold_brightness.mean=.215;recorded.hold_brightness.max=1;recorded.hold_brightness_dark.mean=.229;recorded.hold_brightness_dark.max=1; // Table equality was checked against a separate archived calibration file in the author's run. // This public replay checks the retained output points, not that unpublished archive. let gray=[];let curves=[]; for(const [name,b,c]of [['brightness',30,20],['hold_brightness',17,13],['hold_brightness_dark',-65,-23]]){ const a=makeAdjustment('brightness');Object.assign(a.values,{brightness:b,contrast:c});const f=compileAdjustment(a); for(const row of recorded[name].gray){const rgb=[0,0,0];f(row.input,row.input,row.input,rgb);const actual=new Uint8ClampedArray(rgb.map(v=>v*255))[0];assert.equal(actual,row.ours);gray.push({case:name,brightness:b,contrast:c,...row,current:actual,errorToArchivedPS:Math.abs(actual-row.ps)});} if(name==='brightness')for(let x=0;x<=255;x++){const rgb=[0,0,0];f(x,x,x,rgb);curves.push({input:x,current:new Uint8ClampedArray(rgb.map(v=>v*255))[0]});} } const brightness={kind:'archived full-chart results and new source-level replay',sourceRevision:'19f45ed6dd99800f910c0587baa53e73d46ab4fb',archivedDate:'2026-09-25',rerunAt:new Date().toISOString(),chart:{width:256,height:32,rgbSamples:24576,settings:{brightness:30,contrast:20},reference:'Photoshop 27.3.1 normal mode'},archivedComparison:[{implementation:'Intermediate approximation',mean:11.505,max:25},{implementation:'Response B → C',mean:0.2412109375,max:1},{implementation:'Response C → B',mean:1.1497395833333333,max:4}],archivedAdditionalCases:[{brightness:17,contrast:13,mean:recorded.hold_brightness.mean,max:recorded.hold_brightness.max},{brightness:-65,contrast:-23,mean:recorded.hold_brightness_dark.mean,max:recorded.hold_brightness_dark.max}],tablesMatch:null,replayedPoints:gray.length,gray,curves,note:'The full historical PNG chart is unavailable here. Historical full-chart errors were not remeasured today. The 11.505 result belongs to the intermediate September 25 approximation, not the first September 18 legacy formula. No independent fresh Photoshop validation or performance benchmark is claimed.'}; fs.writeFileSync(out+'/brightness-results.json',JSON.stringify(brightness,null,2)); fs.writeFileSync(out+'/brightness-replay.csv','case,brightness,contrast,input,archived_ours,archived_ps,current,absolute_error_to_archived_ps\n'+gray.map(r=>[r.case,r.brightness,r.contrast,r.input,r.ours,r.ps,r.current,r.errorToArchivedPS].join(',')).join('\n')+'\n'); fs.writeFileSync(out+'/mask-profile.csv','x,source_alpha,masked_alpha,disabled_alpha,destructive_control_alpha\n'+maskData.alphaProfile.map(r=>[r.x,r.source,r.masked,r.disabled,r.destructiveControl].join(',')).join('\n')+'\n'); fs.writeFileSync(task+'/work/measure-result.json',JSON.stringify({mask:maskData.checks,brightnessReplay:gray.length,tablesMatch:null,sourceHashes:Object.fromEntries(['document.ts','stage.tsx','adjustments.ts','tone-response.ts'].map(n=>[n,crypto.createHash('sha256').update(fs.readFileSync(path.join(repo,'src',n))).digest('hex')]))},null,2)); console.log(JSON.stringify({maskChecks:Object.keys(maskData.checks).length,brightnessReplay:gray.length,tablesMatch:null}));