Files
probably-rendering-one-more…/scripts/layout-shadow-covers.mjs

39 lines
3.2 KiB
JavaScript

/** Precise layout authorized by the user; all photographic/puppet/paper content comes from original photos or imagegen. */
import {createRequire} from 'node:module';
import {writeFile} from 'node:fs/promises';
import {resolve,join} from 'node:path';
const require=createRequire(new URL('../apps/waterfall-wiki/package.json',import.meta.url)),sharp=require('sharp');
const root=resolve(process.argv[2]||'../output/shadow-puppet-covers'),W=1536,H=2048,HALF=1024;
const specs=[
{key:'lotus',draft:'lotus-shadow-puppet.png',seam:724},
{key:'pavilion',draft:'pavilion-shadow-puppet.png',seam:727},
{key:'crane',draft:'crane-draft.png',seam:648,cutoutHeight:880},
{key:'lion',draft:'lion-draft.png',seam:724,cutoutHeight:880,generatedPhoto:true},
{key:'peach',draft:'peach-draft.png',seam:645,cutoutHeight:900}
];
const checks=[];
for(const spec of specs){
const path=join(root,spec.draft),meta=await sharp(path).metadata();let lower;
if(spec.cutoutHeight){
const cutout=join(root,spec.key+'-cutout.png'),alpha=await sharp(cutout).stats();
if(!alpha.channels[3]||alpha.channels[3].min!==0)throw Error('Cutout lacks real transparent background: '+spec.key);
const resizedSubject=await sharp(cutout).trim({background:'#00000000',threshold:1}).resize({width:1050,height:spec.cutoutHeight,fit:'inside'}).png().toBuffer();
const sized=await sharp(resizedSubject).metadata();
// Control rods continue beyond the frame rather than terminating on the paper.
const subject=await sharp(resizedSubject).extract({left:0,top:0,width:sized.width,height:sized.height-36}).png().toBuffer();
const subjectInfo=await sharp(subject).metadata();
const paper=await sharp(join(root,'paper-stage.png')).resize(W,HALF,{fit:'cover'}).png().toBuffer();
lower=await sharp(paper).composite([{input:subject,left:Math.round((W-subjectInfo.width)/2),top:HALF-subjectInfo.height}]).png().toBuffer();
}else{
lower=await sharp(path).extract({left:0,top:spec.seam,width:meta.width,height:meta.height-spec.seam}).resize(W,HALF,{fit:'cover'}).png().toBuffer();
}
const upper=spec.generatedPhoto?await sharp(path).extract({left:0,top:0,width:meta.width,height:spec.seam}).resize(W,HALF,{fit:'cover'}).png().toBuffer():await sharp(join(root,'sources',spec.key+'.jpg')).resize(W,HALF,{fit:'cover',position:'centre'}).png().toBuffer();
const output=join(root,spec.key+'-cover.png');
const composed=await sharp({create:{width:W,height:H,channels:3,background:'#f3d39c'}}).composite([{input:upper,left:0,top:0},{input:lower,left:0,top:HALF}]).png().toBuffer();
await sharp(composed).flatten({background:'#f3d39c'}).removeAlpha().png().toFile(output);
const final=await sharp(output).metadata();if(final.width*4!==final.height*3||final.height%2||final.hasAlpha)throw Error('Invalid cover geometry: '+spec.key);
checks.push({key:spec.key,path:output,width:final.width,height:final.height,photographRows:[0,HALF-1],puppetRows:[HALF,H-1],sourcePanelSeam:spec.seam,sourcePhoto:spec.generatedPhoto?'imagegen photographic outpaint of original':'original photograph',cutout:!!spec.cutoutHeight});
}
await writeFile(join(root,'layout-checks.json'),JSON.stringify(checks,null,2));
console.log(JSON.stringify({covers:checks.length,width:W,height:H,seam:HALF,checks:'exact 3:4, equal halves, opaque'}));