39 lines
3.2 KiB
JavaScript
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'}));
|