Rebuild source geometry as clean vectors without old glyph cutouts
studio CI and deployment / verify-and-deploy (push) Successful in 4m4s
studio CI and deployment / verify-and-deploy (push) Successful in 4m4s
This commit is contained in:
@@ -9,5 +9,5 @@ export function LayoutSelection(){
|
||||
async function download(format:'svg'|'json'){
|
||||
try{setStatus('正在生成…');const r=await fetch('/studio/api/export/layout-selection?format='+format,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({layoutId:layout.id,...current})});if(!r.ok)throw Error();const url=URL.createObjectURL(await r.blob()),a=document.createElement('a');a.href=url;a.download=layout.id+'.'+format;a.click();setTimeout(()=>URL.revokeObjectURL(url),500);setStatus('已导出当前模板。')}catch{setStatus('导出失败,请重试。')}
|
||||
}
|
||||
return <main className="template-trial"><Head><title>三种排版 · Studio 工具</title></Head><header className="trial-nav"><a href={tutorialPost}>← waterfall-wiki 使用教程</a><span>STUDIO / LAYOUT SELECTION</span></header><section className="trial-intro"><div><small>REF-01 / REF-07 / REF-26</small><h1>三个构图,各有位置。</h1><p>切换真实原图对应的模板,分别试文字与配色。图位蒙版与装饰来自教程的分层资产;当前只放占位内容。</p></div></section><div className="trial-workspace"><aside className="trial-controls"><h2>选择与编辑</h2><label>排版<select value={active} onChange={e=>{setActive(Number(e.target.value));setStatus('')}}>{selections.map((l,i)=><option key={l.id} value={i}>{l.id} · {l.name}</option>)}</select></label>{layout.slots.filter(s=>s.kind==='text').map(s=><label key={s.id}>{s.id} · {s.role}<textarea rows={current.fields[s.id]?.includes('\n')?3:1} maxLength={120} value={current.fields[s.id]||''} onChange={e=>update({fields:{...current.fields,[s.id]:e.target.value}})}/></label>)}<div className="trial-colors"><label>主图色<input type="color" value={current.color} onChange={e=>update({color:e.target.value})}/></label><label>副图色<input type="color" value={current.accent} onChange={e=>update({accent:e.target.value})}/></label></div><label className="trial-switch"><input type="checkbox" checked={guides} onChange={e=>setGuides(e.target.checked)}/>显示构件边界</label><div className="trial-actions"><button className="trial-primary" onClick={()=>void download('svg')}>下载当前 SVG</button><button onClick={()=>void download('json')}>下载参数 JSON</button></div><p className="trial-help">每张模板单独保留本页修改,刷新恢复初始值。下载 SVG 为无边界线的正式图;原始装饰为低分辨率图层。</p><p role="status">{status}</p></aside><div><div className="selection-preview"><SourceCover layout={layout} values={current} guides={guides}/></div><div className="selection-thumbs">{selections.map((l,i)=><button key={l.id} aria-pressed={active===i} onClick={()=>setActive(i)}><SourceCover layout={l} values={values[i]}/><span>{l.id}</span></button>)}</div><details className="trial-reference"><summary>原图与构件坐标</summary><div className="trial-reference-body"><img src={layout.referenceUrl} alt={layout.name+'原图'}/><div><a href={layout.tutorialUrl}>阅读这张原图的教程 ↗</a><table><thead><tr><th>ID</th><th>构件</th><th>x,y,w,h</th></tr></thead><tbody>{layout.slots.map(s=><tr key={s.id}><td>{s.id}</td><td>{s.role}</td><td>{s.box.join(', ')}</td></tr>)}</tbody></table></div></div></details></div></div></main>;
|
||||
return <main className="template-trial"><Head><title>三种排版 · Studio 工具</title></Head><header className="trial-nav"><a href={tutorialPost}>← waterfall-wiki 使用教程</a><span>STUDIO / LAYOUT SELECTION</span></header><section className="trial-intro"><div><small>REF-01 / REF-07 / REF-26</small><h1>三个构图,各有位置。</h1><p>切换真实原图对应的模板,分别试文字与配色。图位与装饰按教程构件重建为干净矢量;当前只放占位内容。</p></div></section><div className="trial-workspace"><aside className="trial-controls"><h2>选择与编辑</h2><label>排版<select value={active} onChange={e=>{setActive(Number(e.target.value));setStatus('')}}>{selections.map((l,i)=><option key={l.id} value={i}>{l.id} · {l.name}</option>)}</select></label>{layout.slots.filter(s=>s.kind==='text').map(s=><label key={s.id}>{s.id} · {s.role}<textarea rows={current.fields[s.id]?.includes('\n')?3:1} maxLength={120} value={current.fields[s.id]||''} onChange={e=>update({fields:{...current.fields,[s.id]:e.target.value}})}/></label>)}<div className="trial-colors"><label>主图色<input type="color" value={current.color} onChange={e=>update({color:e.target.value})}/></label><label>副图色<input type="color" value={current.accent} onChange={e=>update({accent:e.target.value})}/></label></div><label className="trial-switch"><input type="checkbox" checked={guides} onChange={e=>setGuides(e.target.checked)}/>显示构件边界</label><div className="trial-actions"><button className="trial-primary" onClick={()=>void download('svg')}>下载当前 SVG</button><button onClick={()=>void download('json')}>下载参数 JSON</button></div><p className="trial-help">每张模板单独保留本页修改,刷新恢复初始值。下载 SVG 为无边界线的正式图;原始低分辨率图层在 JSON 中保留作对照。</p><p role="status">{status}</p></aside><div><div className="selection-preview"><SourceCover layout={layout} values={current} guides={guides}/></div><div className="selection-thumbs">{selections.map((l,i)=><button key={l.id} aria-pressed={active===i} onClick={()=>setActive(i)}><SourceCover layout={l} values={values[i]}/><span>{l.id}</span></button>)}</div><details className="trial-reference"><summary>原图与构件坐标</summary><div className="trial-reference-body"><img src={layout.referenceUrl} alt={layout.name+'原图'}/><div><a href={layout.tutorialUrl}>阅读这张原图的教程 ↗</a><table><thead><tr><th>ID</th><th>构件</th><th>x,y,w,h</th></tr></thead><tbody>{layout.slots.map(s=><tr key={s.id}><td>{s.id}</td><td>{s.role}</td><td>{s.box.join(', ')}</td></tr>)}</tbody></table></div></div></details></div></div></main>;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@ export {selections};
|
||||
export function SourceCover({layout,values,guides=false}:{layout:SourceLayout;values:CoverValues;guides?:boolean}){
|
||||
const uid=useId().replace(/:/g,''),W=layout.width*10,H=layout.height*10;
|
||||
return <svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${W} ${H}`} width={W} height={H} role="img" aria-label={layout.id+' '+layout.name} data-layout-id={layout.id} style={{display:'block',width:'100%',height:'auto'}}>
|
||||
<title>{layout.name} · 封面试版</title><desc>文字为可编辑占位,图位使用原始形状蒙版,装饰沿用原图分层。来源 {layout.tutorialUrl}</desc><rect width={W} height={H} fill="#f4f2eb"/>
|
||||
<title>{layout.name} · 封面试版</title><desc>文字为可编辑占位,图位和装饰按原图构件重建为矢量,原始分层数据保留在模板 JSON 中。来源 {layout.tutorialUrl}</desc><rect width={W} height={H} fill="#f4f2eb"/>
|
||||
{['image','decoration','text'].flatMap(kind=>layout.slots.filter(s=>s.kind===kind).map(s=>{
|
||||
const [x,y,w,h]=s.box.map(n=>n*10),raster='raster' in s?s.raster:undefined;
|
||||
if(kind==='image')return <g data-slot={s.id} key={s.id}><defs><mask id={uid+s.id} maskUnits="userSpaceOnUse" x={x} y={y} width={w} height={h} style={{maskType:'alpha'}}><image href={raster} x={x} y={y} width={w} height={h}/></mask></defs><rect x={x} y={y} width={w} height={h} fill={s.id==='I01'?values.color:values.accent} mask={`url(#${uid+s.id})`}/></g>;
|
||||
if(kind==='decoration')return <image data-slot={s.id} key={s.id} href={raster} x={x} y={y} width={w} height={h}/>;
|
||||
const [x,y,w,h]=s.box.map(n=>n*10);
|
||||
if(kind==='image')return <g data-slot={s.id} key={s.id}>{layout.id==='ref-07'?<ellipse cx={x+w/2} cy={y+h/2} rx={w/2} ry={h/2} fill={values.color}/>:<rect x={x} y={y} width={w} height={h} fill={s.id==='I01'?values.color:values.accent}/>}</g>;
|
||||
if(kind==='decoration'){
|
||||
if(layout.id==='ref-07')return <g data-slot={s.id} key={s.id} fill="none" stroke="#b8c2b8" strokeWidth="9"><defs><clipPath id={uid+s.id}><rect x={x} y={y} width={w} height={h}/></clipPath></defs><g clipPath={'url(#'+uid+s.id+')'}>{[550,595,640,685,730,775,820].map(r=><ellipse key={r} cx="640" cy="560" rx={r} ry={r*1.02}/>)}</g></g>;
|
||||
if(layout.id==='ref-26'&&s.id==='D04')return <path data-slot={s.id} key={s.id} d={`M${x} ${y+h} C${x+w*.28} ${y+h*.93} ${x+w*.43} ${y+h*.64} ${x+w*.30} ${y+h*.56} C${x+w*.16} ${y+h*.47} ${x+w*.19} ${y+h*.89} ${x+w*.38} ${y+h*.68} S${x+w*.69} ${y+h*.52} ${x+w*.65} ${y+h*.34} C${x+w*.59} ${y+h*.15} ${x+w*.52} ${y+h*.48} ${x+w*.75} ${y+h*.37} S${x+w*.90} ${y+h*.15} ${x+w} ${y}`} fill="none" stroke="#526459" strokeWidth="4"/>;
|
||||
return <path data-slot={s.id} key={s.id} d={`M${x} ${y+h/2}H${x+w}`} stroke="#526459" strokeWidth={layout.id==='ref-26'&&s.id==='D03'?4:2}/>;
|
||||
}
|
||||
const text=values.fields[s.id]||'',lines=text.split('\n').slice(0,4),length=(str:string)=>Array.from(str).reduce((sum,c)=>sum+(/[\u3400-\u9fff]/.test(c)?1:.6),0),size=Math.min(h/(lines.length*1.25),w/Math.max(1,...lines.map(length))*.94);
|
||||
if(s.direction==='vertical'){const letters=Array.from(text.replace(/\n/g,'')),font=Math.min(w*.88,h/Math.max(1,letters.length));return <g data-slot={s.id} key={s.id} fill="#26362f" fontFamily="'PingFang SC',sans-serif">{letters.map((c,i)=><text key={i} x={x+w/2} y={y+font*(i+.85)} textAnchor="middle" fontSize={font}>{c}</text>)}</g>}
|
||||
if(s.direction==='curved')return <g data-slot={s.id} key={s.id}><defs><path id={uid+s.id} d={`M${x} ${y} Q${x+w/2} ${y+h*1.8} ${x+w} ${y}`}/></defs><text fill="#26362f" fontFamily="Arial,sans-serif" fontSize={Math.min(36,w/Math.max(1,text.length*.6))} letterSpacing="1"><textPath href={'#'+uid+s.id} startOffset="50%" textAnchor="middle">{text}</textPath></text></g>;
|
||||
|
||||
Reference in New Issue
Block a user