15 lines
3.8 KiB
TypeScript
15 lines
3.8 KiB
TypeScript
import {useId} from 'react';
|
||
import type {Layout,Parameters} from '../lib/layout-parameters';
|
||
export type LayerLayout=Omit<Layout,'elements'>&{source:string;background:string;tutorialUrl:string;sourceSha256:string;elements:(Layout['elements'][number]&{raster:string;shape?:string})[]};
|
||
export function LayoutCanvas({layout,params,guides=false,imageSources={}}:{layout:LayerLayout;params:Parameters;guides?:boolean;imageSources?:Record<string,string>}){
|
||
const uid=useId().replace(/[^a-z0-9]/gi,''),W=layout.width,H=layout.height,unchanged=!params.paper&&!Object.keys(params.texts).length&&!Object.keys(params.images).length&&!Object.keys(params.decorations).length;
|
||
return <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} ${H}`} width={W*6} height={H*6} role="img" aria-label={layout.id+' '+layout.name} data-layout-id={layout.id} style={{width:'100%',height:'auto',display:'block'}}><title>{layout.id+' · '+layout.name}</title><desc>对应原图的文字、图位和装饰。未替换构件保留原始低分辨率图层;替换文字为 SVG 文字。</desc>{unchanged?<image href={layout.source} width={W} height={H}/>:<><rect width={W} height={H} fill={params.paper||'#f4f2eb'}/><image href={layout.background} width={W} height={H}/>{params.paper&&<><defs><mask id={uid+'paper'}><image href={layout.background} width={W} height={H}/></mask></defs><rect width={W} height={H} fill={params.paper} mask={'url(#'+uid+'paper)'}/></>}{['image','decoration','text'].flatMap(kind=>layout.elements.filter(s=>s.kind===kind).map(s=>{
|
||
const [x,y,w,h]=s.box,id=uid+s.id,t=params.texts[s.id],im=params.images[s.id],d=params.decorations[s.id];
|
||
if(kind==='text'&&t){const lines=t.value.split('\n'),len=(v:string)=>Array.from(v).reduce((a,c)=>a+(/[\u3400-\u9fff]/.test(c)?1:.6),0),vertical=s.direction==='vertical',size=Math.min(vertical?w*.9:h/(lines.length*1.2),vertical?h/Math.max(1,Array.from(t.value).length):w/Math.max(1,...lines.map(len)))*(t.scale||1),align=t.align||'start';return <g key={s.id} data-slot={s.id} fill={t.color||'#26362f'} fontFamily="Arial,'Noto Sans CJK SC','PingFang SC',sans-serif"><defs><clipPath id={id+'clip'}><rect x={x} y={y} width={w} height={h}/></clipPath></defs><g clipPath={`url(#${id}clip)`}>{vertical?Array.from(t.value.replaceAll('\n','')).map((c,i)=><text key={i} x={x+w/2} y={y+size*(i+.85)} textAnchor="middle" fontSize={size}>{c}</text>):s.direction==='curved'?<><defs><path id={id+'path'} d={`M${x} ${y+h*.2} Q${x+w/2} ${y+h*1.7} ${x+w} ${y+h*.2}`}/></defs><text fontSize={size}><textPath href={'#'+id+'path'}>{t.value}</textPath></text></>:lines.map((l,i)=><text key={i} x={x+(align==='middle'?w/2:align==='end'?w:0)} y={y+size*(.88+i*1.2)} textAnchor={align} fontSize={size}>{l}</text>)}</g></g>}
|
||
if(kind==='decoration'&&d?.hidden)return null;
|
||
const fill=kind==='image'?im?.color:d?.color;
|
||
if(fill||im?.assetId)return <g key={s.id} data-slot={s.id}><defs><mask id={id} maskUnits="userSpaceOnUse" x={x} y={y} width={w} height={h} style={{maskType:'alpha'}}><image href={s.shape||s.raster} x={x} y={y} width={w} height={h}/></mask></defs><g mask={'url(#'+id+')'}>{fill&&<rect x={x} y={y} width={w} height={h} fill={fill}/>} {im?.assetId&&<image href={imageSources[im.assetId]||'/api/preview?'+new URLSearchParams({source:'/api/media/'+im.assetId,width:'960'})} x={x} y={y} width={w} height={h} preserveAspectRatio={im.fit==='contain'?'xMidYMid meet':'xMidYMid slice'}/>}</g></g>;
|
||
return <image key={s.id} data-slot={s.id} href={s.raster} x={x} y={y} width={w} height={h}/>;
|
||
}))}</>}{guides&&layout.elements.map(s=>{const[x,y,w,h]=s.box,color=s.kind==='text'?'#2765da':s.kind==='image'?'#d86b27':'#08875d';return <g key={s.id} aria-hidden="true"><rect x={x} y={y} width={w} height={h} fill="none" stroke={color} strokeWidth=".35"/><text x={x} y={y+3} fontSize="3" fill={color}>{s.id}</text></g>})}</svg>;
|
||
}
|