index.html 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Anim Studio · 动画资源库</title>
  7. <style>
  8. :root{
  9. --bg:#1b1430; --panel:#241a3d; --card:#2c2150; --line:#3d2f63;
  10. --accent:#ff7eb6; --accent2:#7ee8ff; --text:#f3eefb; --muted:#a99ccb;
  11. }
  12. *{box-sizing:border-box}
  13. body{margin:0;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
  14. background:linear-gradient(160deg,#1b1430,#2a1b45);color:var(--text);min-height:100vh}
  15. header{padding:18px 24px;border-bottom:1px solid var(--line);display:flex;
  16. align-items:center;gap:14px}
  17. header h1{font-size:20px;margin:0}
  18. header .sub{color:var(--muted);font-size:13px}
  19. .wrap{max-width:1180px;margin:0 auto;padding:20px 24px 60px}
  20. details.gen{background:var(--panel);border:1px solid var(--line);border-radius:14px;
  21. padding:0 18px;margin-bottom:22px}
  22. details.gen summary{cursor:pointer;padding:16px 0;font-weight:600;font-size:15px;
  23. list-style:none}
  24. details.gen summary::-webkit-details-marker{display:none}
  25. .gen-grid{display:grid;grid-template-columns:320px 1fr;gap:18px;padding:0 0 18px}
  26. .workflow-grid{display:grid;grid-template-columns:repeat(4,minmax(150px,1fr));gap:12px;padding:0 0 18px}
  27. .workflow-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:0 0 18px}
  28. .workflow-note{color:var(--muted);font-size:12px;line-height:1.5}
  29. .field{margin-bottom:10px}
  30. .field label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}
  31. .field input,.field select,textarea{width:100%;background:#160f29;color:var(--text);
  32. border:1px solid var(--line);border-radius:8px;padding:9px 10px;font-size:13px}
  33. .field input[type="checkbox"]{width:auto;margin-right:6px;vertical-align:-1px}
  34. textarea{font-family:ui-monospace,Menlo,monospace;min-height:300px;resize:vertical;line-height:1.45}
  35. button{background:linear-gradient(90deg,var(--accent),#ff9d6e);color:#2a0f23;border:none;
  36. border-radius:9px;padding:10px 18px;font-weight:700;cursor:pointer;font-size:14px}
  37. button.ghost{background:#2c2150;color:var(--text);border:1px solid var(--line);font-weight:600}
  38. button:disabled{opacity:.5;cursor:default}
  39. .log{white-space:pre-wrap;background:#0f0a1e;border:1px solid var(--line);border-radius:8px;
  40. padding:10px;font-family:ui-monospace,monospace;font-size:12px;color:#bfe;max-height:160px;
  41. overflow:auto;margin-top:10px}
  42. .tabs{display:flex;gap:8px;margin:6px 0 18px}
  43. .tabs button{background:#241a3d;color:var(--muted);border:1px solid var(--line)}
  44. .tabs button.active{background:var(--accent);color:#2a0f23}
  45. .toolbar{display:flex;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap}
  46. .toolbar select{background:#160f29;color:var(--text);border:1px solid var(--line);
  47. border-radius:8px;padding:7px 10px;font-size:13px}
  48. .count{color:var(--muted);font-size:13px}
  49. .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:16px}
  50. .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px;
  51. display:flex;flex-direction:column;gap:10px}
  52. .stage{height:200px;border-radius:10px;background:
  53. repeating-conic-gradient(#241a3d 0 25%, #2c2150 0 50%) 0/22px 22px;
  54. display:flex;align-items:flex-end;justify-content:center;overflow:hidden;position:relative}
  55. .stage img{max-height:88%;max-width:88%;transform-origin:50% 100%;will-change:transform;
  56. filter:drop-shadow(0 6px 10px rgba(0,0,0,.35))}
  57. .stage canvas{position:absolute;inset:0;width:100%;height:100%}
  58. .name{font-weight:700;font-size:15px}
  59. .meta{color:var(--muted);font-size:12px;line-height:1.5}
  60. .row{display:flex;gap:8px;align-items:center}
  61. .row select{flex:1;background:#160f29;color:var(--text);border:1px solid var(--line);
  62. border-radius:7px;padding:6px 8px;font-size:12px}
  63. .pill{display:inline-block;background:#160f29;border:1px solid var(--line);border-radius:20px;
  64. padding:2px 10px;font-size:11px;color:var(--accent2)}
  65. .art-img{max-width:100%;max-height:180px;object-fit:contain}
  66. .demo-box{width:84px;height:84px;border-radius:14px;
  67. background:linear-gradient(135deg,var(--accent),var(--accent2));margin:auto;
  68. display:flex;align-items:center;justify-content:center;font-weight:800;color:#241a3d;font-size:22px}
  69. .empty{color:var(--muted);text-align:center;padding:50px;border:1px dashed var(--line);border-radius:14px}
  70. code{background:#160f29;padding:1px 6px;border-radius:5px;font-size:12px}
  71. a{color:var(--accent2)}
  72. </style>
  73. </head>
  74. <body>
  75. <header>
  76. <h1>🍬 Anim Studio</h1>
  77. <span class="sub">动画资源库 · 角色 / 特效 / 动效 可视化预览</span>
  78. </header>
  79. <div class="wrap">
  80. <details class="gen" id="workflowPanel" open>
  81. <summary>▸ AI 游戏方案(创意 + 风格 + 玩法 → manifest)</summary>
  82. <div class="field"><label>创意简报(可填一句话,也可以写完整需求)</label>
  83. <textarea id="creativeBrief" style="min-height:92px" placeholder="例:做一个竖屏海盗金币 slot,参考糖果传奇那种明亮可爱的反馈,但主题是热带宝藏。核心钩子是金币 Hold & Win,整体要轻松、亮、适合移动端。"></textarea></div>
  84. <div class="workflow-grid">
  85. <div class="field"><label>参考图 / 网址链接(一行一个)</label>
  86. <textarea id="creativeRefs" style="min-height:92px" placeholder="https://...\n也可以写:本地截图文件名、参考页面链接、竞品链接"></textarea></div>
  87. <div class="field"><label>上传参考图 / 截图(最多 4 张)</label>
  88. <input id="creativeImageFiles" type="file" accept="image/*" multiple>
  89. <div class="workflow-note" id="creativeImageMsg">上传图会先由视觉模型提炼风格,再生成 manifest。</div></div>
  90. <div class="field"><label>希望参考的风格点</label>
  91. <textarea id="creativeStyleNotes" style="min-height:92px" placeholder="按钮质感、颜色、角色比例、背景气氛、UI 密度等"></textarea></div>
  92. <div class="field"><label>必须避免</label>
  93. <textarea id="creativeAvoidNotes" style="min-height:92px" placeholder="不要照抄 logo / IP / 角色;不要暗黑;不要复杂文字"></textarea></div>
  94. <div>
  95. <div class="field"><label>文字模型</label>
  96. <input id="textModel" value="gpt-4o-mini"></div>
  97. <button class="ghost" id="aiWorkflowBtn">AI 生成完整游戏方案</button>
  98. <div class="workflow-note" id="aiWorkflowMsg">先生成统一方案,再微调玩法和图片资源。</div>
  99. </div>
  100. </div>
  101. <div class="log" id="gamePlanView" style="display:none;max-height:260px"></div>
  102. <div class="workflow-grid">
  103. <div class="field"><label>游戏代号</label>
  104. <input id="wfGameId" value="jelly-candy-slot"></div>
  105. <div class="field"><label>游戏标题</label>
  106. <input id="wfTitle" value="Jelly Candy Slot"></div>
  107. <div class="field"><label>主题</label>
  108. <select id="wfTheme">
  109. <option value="jelly">果冻糖果</option>
  110. <option value="fruit">水果乐园</option>
  111. <option value="egypt">埃及宝藏</option>
  112. <option value="pirate">海盗金币</option>
  113. <option value="pirate_jelly">海盗糖果</option>
  114. <option value="cyber">霓虹赛博</option>
  115. </select></div>
  116. <div class="field"><label>基础玩法</label>
  117. <select id="wfReelMode">
  118. <option value="ways">Ways 5×3</option>
  119. <option value="paylines">固定赔线 5×3</option>
  120. <option value="megaways">Megaways 6轴</option>
  121. <option value="cluster">Cluster Pays 6×5</option>
  122. </select></div>
  123. <div class="field"><label>波动风格</label>
  124. <select id="wfVolatility">
  125. <option value="medium">中波动</option>
  126. <option value="low">低波动</option>
  127. <option value="high">高波动</option>
  128. </select></div>
  129. <div class="field"><label>目标 RTP(%)</label>
  130. <input id="wfTargetRtp" type="number" value="96" min="50" max="99.5" step="0.1"></div>
  131. <div class="field"><label><input type="checkbox" id="wfEnableMathModel" checked> 生成赔率和触发概率数学模型</label></div>
  132. <div class="field"><label>角色/符号数量</label>
  133. <select id="wfCharacterCount">
  134. <option>10</option><option>8</option><option>12</option><option>6</option>
  135. </select></div>
  136. <div class="field"><label>UI 资产范围</label>
  137. <select id="wfUiCompleteness">
  138. <option value="full">完整</option>
  139. <option value="basic">基础</option>
  140. </select></div>
  141. <div class="field"><label>反馈强度</label>
  142. <select id="wfFeedbackIntensity">
  143. <option value="standard">标准</option>
  144. <option value="quiet">克制</option>
  145. <option value="loud">夸张</option>
  146. </select></div>
  147. <div class="field"><label>初始金币</label>
  148. <input id="wfStartingBalance" type="number" value="5000" min="100" step="100"></div>
  149. <div class="field"><label>默认下注</label>
  150. <input id="wfDefaultBet" type="number" value="50" min="1" step="10"></div>
  151. <div class="field"><label>免费旋转次数</label>
  152. <input id="wfFreeSpinCount" type="number" value="8" min="3" max="30"></div>
  153. <div class="field"><label>最大连锁次数</label>
  154. <input id="wfMaxCascades" type="number" value="6" min="1" max="20"></div>
  155. <div class="field"><label><input type="checkbox" id="wfCascades" checked> Cascades 连锁下落</label></div>
  156. <div class="field"><label><input type="checkbox" id="wfFreeSpins" checked> Scatter Free Spins</label></div>
  157. <div class="field"><label><input type="checkbox" id="wfWilds" checked> Wild 符号</label></div>
  158. <div class="field"><label><input type="checkbox" id="wfHoldWin"> Hold & Win</label></div>
  159. <div class="field"><label><input type="checkbox" id="wfMultipliers"> 连锁倍数</label></div>
  160. </div>
  161. <div class="workflow-actions">
  162. <button id="buildWorkflowBtn">生成玩法配置和 manifest</button>
  163. <button class="ghost" id="openGenBtn">打开生成面板</button>
  164. <span class="workflow-note" id="workflowMsg">先生成 manifest,再点下方“开始生成”。</span>
  165. </div>
  166. </details>
  167. <details class="gen" id="genPanel" open>
  168. <summary>▸ 生成图片资源(填 key,点开始)</summary>
  169. <div class="gen-grid">
  170. <div>
  171. <div class="field"><label>接口协议(厂商)</label>
  172. <select id="provider"><option>OpenAI 兼容接口</option></select></div>
  173. <div class="field"><label>API Key</label>
  174. <input id="apiKey" type="password" placeholder="已内置,留空即可;临时换 key 时再填写"></div>
  175. <div class="field"><label>Base URL</label>
  176. <input id="baseUrl" value="https://x.long.bid/v1"></div>
  177. <div class="field"><label>模型名(真正发给 API 的,按你的填)</label>
  178. <input id="model" value="gpt-image-2"></div>
  179. <div class="field"><label>尺寸</label>
  180. <select id="size"><option>1024x1024</option><option>1024x1536</option><option>1536x1024</option></select></div>
  181. <div class="field"><label><input type="checkbox" id="removeBg"> 生图后去背景(较慢)</label></div>
  182. <button id="startBtn">▶ 开始生成</button>
  183. <div class="log" id="log" style="display:none"></div>
  184. </div>
  185. <div class="field"><label>animation_manifest.json</label>
  186. <textarea id="manifest"></textarea></div>
  187. </div>
  188. </details>
  189. <div class="toolbar">
  190. <span class="meta">资源库(game):</span>
  191. <select id="gameSel"></select>
  192. <button class="ghost" id="reloadBtn">↻ 刷新</button>
  193. <button id="exportBtn">📦 导出 Cocos 整合包</button>
  194. <button class="ghost" id="deleteBtn">🗑 删除该资源库</button>
  195. <span class="count" id="opMsg"></span>
  196. </div>
  197. <div class="tabs">
  198. <button data-tab="chars" class="active">角色库</button>
  199. <button data-tab="art">UI 美术</button>
  200. <button data-tab="vfx">特效库</button>
  201. <button data-tab="ui">动效库</button>
  202. </div>
  203. <div id="view"></div>
  204. </div>
  205. <script>
  206. const $ = s => document.querySelector(s);
  207. let LIB = {characters:[],vfx:[],ui:[]}, ASSET="", TAB="chars";
  208. const animTargets = []; // {el, anim, start}
  209. // ---------- 拉取默认 manifest & 资源库 ----------
  210. async function loadManifest(){
  211. try{ const t = await (await fetch('/api/manifest')).text(); $('#manifest').value = t; }catch(e){}
  212. }
  213. async function loadLibrary(game){
  214. const url = '/api/library' + (game?('?game='+encodeURIComponent(game)):'');
  215. const lib = await (await fetch(url)).json();
  216. LIB = lib; ASSET = lib.assetBase || "";
  217. const sel = $('#gameSel'); sel.innerHTML='';
  218. (lib.games||[]).forEach(g=>{ const o=document.createElement('option');
  219. o.value=g;o.textContent=g; if(g===lib.game)o.selected=true; sel.appendChild(o); });
  220. if(!(lib.games||[]).length){ const o=document.createElement('option');o.textContent='(暂无)';sel.appendChild(o); }
  221. render();
  222. }
  223. // ---------- 关键帧采样(与后端 spine 数据一致)----------
  224. const lerp=(a,b,t)=>a+(b-a)*t;
  225. function sample(keys,t,field){
  226. if(!keys||!keys.length) return field==='value'?0:1;
  227. if(t<=keys[0].time) return keys[0][field];
  228. for(let i=0;i<keys.length-1;i++){
  229. if(t>=keys[i].time && t<=keys[i+1].time){
  230. const f=(t-keys[i].time)/((keys[i+1].time-keys[i].time)||1);
  231. return lerp(keys[i][field],keys[i+1][field],f);
  232. }
  233. }
  234. return keys[keys.length-1][field];
  235. }
  236. function tick(now){
  237. for(const t of animTargets){
  238. if(!t.el.isConnected) continue;
  239. const a=t.anim; if(!a){continue;}
  240. const dur=a.duration||1; const time=((now-t.start)/1000)%dur;
  241. const sx=sample(a.scale,time,'x'), sy=sample(a.scale,time,'y');
  242. const rot=sample(a.rotate,time,'value');
  243. t.el.style.transform=`scaleX(${sx}) scaleY(${sy}) rotate(${(-rot).toFixed(2)}deg)`;
  244. }
  245. requestAnimationFrame(tick);
  246. }
  247. requestAnimationFrame(tick);
  248. // ---------- 渲染 ----------
  249. function render(){
  250. const v=$('#view'); v.innerHTML=''; animTargets.length=0;
  251. if(TAB==='chars') renderChars(v);
  252. if(TAB==='art') renderArt(v);
  253. if(TAB==='vfx') renderVfx(v);
  254. if(TAB==='ui') renderUi(v);
  255. }
  256. function renderChars(v){
  257. const list=LIB.characters||[];
  258. if(!list.length){ v.innerHTML='<div class="empty">还没有角色。填 key 在生成面板点「开始」即可生成。</div>'; return; }
  259. const grid=document.createElement('div'); grid.className='grid';
  260. list.forEach(c=>{
  261. const card=document.createElement('div'); card.className='card';
  262. const anims=Object.keys(c.animations||{});
  263. card.innerHTML=`
  264. <div class="stage"><img src="${ASSET+c.png}" alt="${c.id}"></div>
  265. <div class="name">${c.id}</div>
  266. <div class="row">
  267. <select>${anims.map(a=>`<option>${a}</option>`).join('')}</select>
  268. </div>
  269. <div class="meta">${c.w}×${c.h}px · ${(c.files||[]).length} 个文件<br>
  270. <span class="pill">spine</span> 动画: ${anims.join(', ')||'idle'}</div>`;
  271. const img=card.querySelector('img'), selA=card.querySelector('select');
  272. const tgt={el:img, anim:c.animations[anims[0]], start:performance.now()};
  273. animTargets.push(tgt);
  274. selA.onchange=()=>{ tgt.anim=c.animations[selA.value]; tgt.start=performance.now(); };
  275. grid.appendChild(card);
  276. });
  277. v.appendChild(grid);
  278. }
  279. function renderArt(v){
  280. const list=LIB.ui_art||[];
  281. if(!list.length){ v.innerHTML='<div class="empty">还没有 UI 美术。用工作流生成 manifest 后点「开始生成」。</div>'; return; }
  282. const grid=document.createElement('div'); grid.className='grid';
  283. list.forEach(a=>{
  284. const card=document.createElement('div'); card.className='card';
  285. card.innerHTML=`
  286. <div class="stage"><img class="art-img" src="${ASSET+a.file}" alt="${a.id}"></div>
  287. <div class="name">${a.id}</div>
  288. <div class="meta">${a.w}×${a.h}px · ${a.transparent?'透明素材':'整图背景'}<br>
  289. <span class="pill">ui_art</span> ${a.file}</div>`;
  290. grid.appendChild(card);
  291. });
  292. v.appendChild(grid);
  293. }
  294. function renderVfx(v){
  295. const list=LIB.vfx||[];
  296. if(!list.length){ v.innerHTML='<div class="empty">还没有特效。</div>'; return; }
  297. const grid=document.createElement('div'); grid.className='grid';
  298. list.forEach(x=>{
  299. const card=document.createElement('div'); card.className='card';
  300. card.innerHTML=`
  301. <div class="stage"><canvas></canvas></div>
  302. <div class="name">${x.id}</div>
  303. <div class="meta"><span class="pill">particle</span> 模板: ${x.template} ·
  304. 发射率 ${x.config.emissionRate}/s · 寿命 ${x.config.life}s</div>`;
  305. grid.appendChild(card);
  306. startParticle(card.querySelector('canvas'), x.config);
  307. });
  308. v.appendChild(grid);
  309. }
  310. function startParticle(canvas, cfg){
  311. const dpr=window.devicePixelRatio||1;
  312. function resize(){ canvas.width=canvas.clientWidth*dpr; canvas.height=canvas.clientHeight*dpr; }
  313. setTimeout(resize,0); window.addEventListener('resize',resize);
  314. const ctx=canvas.getContext('2d');
  315. const ps=[]; let acc=0, last=performance.now();
  316. const sc=(cfg.startColor||[255,255,255,255]), ec=(cfg.endColor||[255,255,255,0]);
  317. const rand=(v)=>(Math.random()*2-1)*(v||0);
  318. function spawn(){
  319. const W=canvas.width,H=canvas.height;
  320. const cx=W/2+rand(cfg.posVarX)*dpr, cy=H*0.5+rand(cfg.posVarY)*dpr;
  321. const ang=((cfg.angle||90)+rand(cfg.angleVar))*Math.PI/180;
  322. const sp=((cfg.speed||100)+rand(cfg.speedVar))*dpr*0.5;
  323. ps.push({x:cx,y:cy,vx:Math.cos(ang)*sp,vy:-Math.sin(ang)*sp,
  324. life:(cfg.life||1)+rand(cfg.lifeVar),age:0,
  325. ss:(cfg.startSize||20),es:(cfg.endSize!=null?cfg.endSize:cfg.startSize||20)});
  326. }
  327. function loop(now){
  328. if(!canvas.isConnected) return;
  329. const dt=Math.min(0.05,(now-last)/1000); last=now;
  330. const W=canvas.width,H=canvas.height;
  331. acc+=(cfg.emissionRate||60)*dt;
  332. while(acc>=1 && ps.length<260){ acc-=1; spawn(); }
  333. ctx.clearRect(0,0,W,H); ctx.globalCompositeOperation='lighter';
  334. for(let i=ps.length-1;i>=0;i--){
  335. const p=ps[i]; p.age+=dt;
  336. if(p.age>=p.life){ ps.splice(i,1); continue; }
  337. p.vy+=(-(cfg.gravityY||0))*dpr*dt; p.vx+=(cfg.gravityX||0)*dpr*dt;
  338. p.x+=p.vx*dt; p.y+=p.vy*dt;
  339. const f=p.age/p.life;
  340. const r=Math.max(0.5,(p.ss+(p.es-p.ss)*f))*dpr/2;
  341. const cr=Math.round(lerp(sc[0],ec[0],f)),cg=Math.round(lerp(sc[1],ec[1],f)),cb=Math.round(lerp(sc[2],ec[2],f));
  342. const a=lerp((sc[3]??255),(ec[3]??0),f)/255;
  343. ctx.beginPath();ctx.fillStyle=`rgba(${cr},${cg},${cb},${a})`;
  344. ctx.arc(p.x,p.y,r,0,7);ctx.fill();
  345. }
  346. requestAnimationFrame(loop);
  347. }
  348. requestAnimationFrame(loop);
  349. }
  350. // ---------- UI tween 预览(JS 复刻,仅用于看效果)----------
  351. const EASE={
  352. cubicOut:t=>1-Math.pow(1-t,3),
  353. backOut:t=>{const c=1.7;return 1+(c+1)*Math.pow(t-1,3)+c*Math.pow(t-1,2);},
  354. elasticOut:t=>t===0?0:t===1?1:Math.pow(2,-10*t)*Math.sin((t*10-0.75)*(2*Math.PI/3))+1,
  355. sineInOut:t=>-(Math.cos(Math.PI*t)-1)/2,
  356. linear:t=>t,
  357. };
  358. function animate(dur,ease,fn){
  359. const s=performance.now();
  360. function step(now){const t=Math.min(1,(now-s)/(dur*1000));fn(EASE[ease](t));if(t<1)requestAnimationFrame(step);}
  361. requestAnimationFrame(step);
  362. }
  363. const TWEEN_DEMO={
  364. scale_bounce:(el)=>{animate(0.09,'linear',t=>el.style.transform=`scale(${1-0.1*t})`);
  365. setTimeout(()=>animate(0.12,'backOut',t=>el.style.transform=`scale(${0.9+0.15*t})`),90);
  366. setTimeout(()=>animate(0.08,'linear',t=>el.style.transform=`scale(${1.05-0.05*t})`),210);},
  367. elastic_in:(el)=>{el.style.opacity=1;animate(0.45,'elasticOut',t=>el.style.transform=`scale(${t})`);},
  368. fade_slide_in:(el)=>{animate(0.3,'cubicOut',t=>{el.style.opacity=t;el.style.transform=`translateY(${40*(1-t)}px)`;});},
  369. number_roll:(el)=>{el.dataset.role='num';animate(0.8,'cubicOut',t=>el.textContent=Math.floor(8888*t));},
  370. pulse:(el)=>{let on=true;el._pi&&clearInterval(el._pi);
  371. el._pi=setInterval(()=>{animate(0.6,'sineInOut',t=>el.style.transform=`scale(${on?1+0.06*t:1.06-0.06*t})`);on=!on;},620);},
  372. };
  373. function renderUi(v){
  374. const list=LIB.ui||[];
  375. if(!list.length){ v.innerHTML='<div class="empty">还没有 UI 动效。已生成的会编译进 <code>ui/TweenPresets.ts</code>。</div>'; return; }
  376. const grid=document.createElement('div'); grid.className='grid';
  377. list.forEach(u=>{
  378. const card=document.createElement('div'); card.className='card';
  379. card.innerHTML=`<div class="stage"><div class="demo-box">${u.preset==='number_roll'?'0':'UI'}</div></div>
  380. <div class="name">${u.id}</div>
  381. <div class="meta"><span class="pill">tween</span> 预设: ${u.preset}</div>
  382. <button class="ghost">▶ 播放</button>`;
  383. const box=card.querySelector('.demo-box');
  384. card.querySelector('button').onclick=()=>{
  385. box.style.opacity=1;box.style.transform='';
  386. (TWEEN_DEMO[u.preset]||(()=>{}))(box);
  387. };
  388. grid.appendChild(card);
  389. });
  390. v.appendChild(grid);
  391. }
  392. // ---------- 事件 ----------
  393. document.querySelectorAll('.tabs button').forEach(b=>b.onclick=()=>{
  394. document.querySelectorAll('.tabs button').forEach(x=>x.classList.remove('active'));
  395. b.classList.add('active'); TAB=b.dataset.tab; render();
  396. });
  397. $('#gameSel').onchange=e=>loadLibrary(e.target.value);
  398. $('#reloadBtn').onclick=()=>loadLibrary($('#gameSel').value);
  399. function opMsg(t,ok=true){ const m=$('#opMsg'); m.textContent=t; m.style.color=ok?'#7ee8ff':'#ff9d9d'; }
  400. function workflowMsg(t,ok=true){ const m=$('#workflowMsg'); m.textContent=t; m.style.color=ok?'#a99ccb':'#ff9d9d'; }
  401. function renderGamePlan(plan, source){
  402. const box=$('#gamePlanView');
  403. if(!plan){ box.style.display='none'; box.textContent=''; return; }
  404. const gd=plan.gameDesign||{}, creative=plan.creative||{}, art=gd.artDirection||{};
  405. const lines=[
  406. `AI 游戏方案 · ${source||plan.source||''}`,
  407. `标题:${gd.title||''}`,
  408. `核心钩子:${gd.coreHook||''}`,
  409. `玩法:${gd.reelExperience||''} / ${gd.volatility||''}`,
  410. `差异点:${(gd.differentiators||[]).join('、')||'未生成'}`,
  411. `美术主题:${art.theme||''}`,
  412. `美术风格:${art.style||''}`,
  413. `参考:${(creative.references||[]).join(',')||'无'};上传图 ${creative.uploadedReferenceImages||0} 张`,
  414. `视觉分析:${creative.visionStyleAnalysis&&Object.keys(creative.visionStyleAnalysis).length?JSON.stringify(creative.visionStyleAnalysis,null,2):(creative.visionStyleAnalysisError||'无')}`
  415. ];
  416. box.textContent=lines.join('\n');
  417. box.style.display='block';
  418. }
  419. function workflowPayload(){
  420. const features=[];
  421. if($('#wfCascades').checked) features.push('cascades');
  422. if($('#wfFreeSpins').checked) features.push('free_spins');
  423. if($('#wfWilds').checked) features.push('wilds');
  424. if($('#wfHoldWin').checked) features.push('hold_win');
  425. if($('#wfMultipliers').checked) features.push('multipliers');
  426. return {
  427. gameId: $('#wfGameId').value,
  428. title: $('#wfTitle').value,
  429. theme: $('#wfTheme').value,
  430. reelMode: $('#wfReelMode').value,
  431. volatility: $('#wfVolatility').value,
  432. targetRtp: $('#wfTargetRtp').value,
  433. enableMathModel: $('#wfEnableMathModel').checked,
  434. characterCount: $('#wfCharacterCount').value,
  435. uiCompleteness: $('#wfUiCompleteness').value,
  436. feedbackIntensity: $('#wfFeedbackIntensity').value,
  437. startingBalance: $('#wfStartingBalance').value,
  438. defaultBet: $('#wfDefaultBet').value,
  439. freeSpinCount: $('#wfFreeSpinCount').value,
  440. maxCascades: $('#wfMaxCascades').value,
  441. creative: {
  442. brief: $('#creativeBrief').value,
  443. references: $('#creativeRefs').value.split('\n').map(x=>x.trim()).filter(Boolean),
  444. uploadedReferenceImages: ($('#creativeImageFiles').files||[]).length,
  445. styleNotes: $('#creativeStyleNotes').value,
  446. avoidNotes: $('#creativeAvoidNotes').value
  447. },
  448. gameDesign: {
  449. title: $('#wfTitle').value,
  450. artDirection: { theme: $('#wfTheme').value, styleNotes: $('#creativeStyleNotes').value },
  451. reelExperience: $('#wfReelMode').value,
  452. volatility: $('#wfVolatility').value
  453. },
  454. features
  455. };
  456. }
  457. $('#buildWorkflowBtn').onclick=async()=>{
  458. const btn=$('#buildWorkflowBtn'); btn.disabled=true; workflowMsg('正在生成玩法配置和 manifest…');
  459. try{
  460. const r=await fetch('/api/slot-workflow',{method:'POST',headers:{'Content-Type':'application/json'},
  461. body:JSON.stringify(workflowPayload())});
  462. const d=await r.json();
  463. if(!d.ok){ workflowMsg('生成失败: '+(d.error||'未知错误'),false); }
  464. else{
  465. $('#manifest').value=JSON.stringify(d.manifest,null,2);
  466. const cfg=d.slot_config||d.manifest.slot_config||{};
  467. const reels=cfg.reels?`${cfg.reels.columns}×${cfg.reels.rows}`:'';
  468. const minMatch=cfg.winRules?cfg.winRules.minMatch:'';
  469. const sim=cfg.mathModel&&cfg.mathModel.simulation?cfg.mathModel.simulation:{};
  470. if(cfg.mathModel&&cfg.mathModel.status==='disabled_by_user'){
  471. workflowMsg(`已生成 ${d.manifest.game}:${reels},数学模型未生成,最小中奖 ${minMatch} 连。`);
  472. }else{
  473. workflowMsg(`已生成 ${d.manifest.game}:${reels},RTP ${((sim.estimatedRtp||0)*100).toFixed(2)}%,命中率 ${((sim.hitFrequency||0)*100).toFixed(2)}%,最小中奖 ${minMatch} 连。`);
  474. }
  475. $('#genPanel').open=true;
  476. }
  477. }catch(e){ workflowMsg('请求失败: '+e,false); }
  478. btn.disabled=false;
  479. };
  480. async function readReferenceImages(){
  481. const input=$('#creativeImageFiles');
  482. const files=Array.from(input.files||[]).slice(0,4);
  483. if(!files.length) return [];
  484. $('#creativeImageMsg').textContent=`已选择 ${files.length} 张,准备上传给视觉模型分析…`;
  485. const readers=files.map(file=>new Promise((resolve,reject)=>{
  486. if(file.size>6*1024*1024){ reject(new Error(file.name+' 超过 6MB')); return; }
  487. const r=new FileReader();
  488. r.onload=()=>resolve(r.result);
  489. r.onerror=()=>reject(r.error||new Error('读取失败'));
  490. r.readAsDataURL(file);
  491. }));
  492. return Promise.all(readers);
  493. }
  494. $('#aiWorkflowBtn').onclick=async()=>{
  495. const btn=$('#aiWorkflowBtn'); btn.disabled=true; const msg=$('#aiWorkflowMsg');
  496. msg.textContent='AI 正在理解创意和参考链接…'; msg.style.color='#a99ccb';
  497. try{
  498. const referenceImages=await readReferenceImages();
  499. if(referenceImages.length) msg.textContent='视觉模型正在分析上传参考图…';
  500. const r=await fetch('/api/creative-manifest',{method:'POST',headers:{'Content-Type':'application/json'},
  501. body:JSON.stringify({
  502. api_key:$('#apiKey').value,
  503. base_url:$('#baseUrl').value,
  504. text_model:$('#textModel').value,
  505. gameId:$('#wfGameId').value,
  506. title:$('#wfTitle').value,
  507. brief:$('#creativeBrief').value,
  508. references:$('#creativeRefs').value,
  509. reference_images:referenceImages,
  510. styleNotes:$('#creativeStyleNotes').value,
  511. avoidNotes:$('#creativeAvoidNotes').value,
  512. targetRtp:$('#wfTargetRtp').value,
  513. enableMathModel:$('#wfEnableMathModel').checked
  514. })});
  515. const d=await r.json();
  516. if(!d.ok){ msg.textContent='AI 生成失败: '+(d.error||'未知错误'); msg.style.color='#ff9d9d'; }
  517. else{
  518. $('#manifest').value=JSON.stringify(d.manifest,null,2);
  519. const req=d.creative_request||{};
  520. $('#wfGameId').value=req.gameId||$('#wfGameId').value;
  521. $('#wfTitle').value=req.title||$('#wfTitle').value;
  522. if(req.theme) $('#wfTheme').value=req.theme;
  523. if(req.reelMode) $('#wfReelMode').value=req.reelMode;
  524. if(req.volatility) $('#wfVolatility').value=req.volatility;
  525. if(req.characterCount) $('#wfCharacterCount').value=String(req.characterCount);
  526. if(req.uiCompleteness) $('#wfUiCompleteness').value=req.uiCompleteness;
  527. if(req.feedbackIntensity) $('#wfFeedbackIntensity').value=req.feedbackIntensity;
  528. const fs=new Set(req.features||[]);
  529. $('#wfCascades').checked=fs.has('cascades');
  530. $('#wfFreeSpins').checked=fs.has('free_spins');
  531. $('#wfWilds').checked=fs.has('wilds');
  532. $('#wfHoldWin').checked=fs.has('hold_win');
  533. $('#wfMultipliers').checked=fs.has('multipliers');
  534. renderGamePlan(d.game_plan, d.source);
  535. msg.textContent=`已生成完整游戏方案和 manifest(来源:${d.source}),可微调后开始生成图片资源。`;
  536. $('#genPanel').open=true;
  537. }
  538. }catch(e){ msg.textContent='请求失败: '+e; msg.style.color='#ff9d9d'; }
  539. btn.disabled=false;
  540. };
  541. $('#openGenBtn').onclick=()=>{ $('#genPanel').open=true; $('#genPanel').scrollIntoView({behavior:'smooth',block:'start'}); };
  542. $('#exportBtn').onclick=async()=>{
  543. const game=$('#gameSel').value;
  544. if(!game||game==='(暂无)'){ opMsg('没有可导出的资源库',false); return; }
  545. const btn=$('#exportBtn'); btn.disabled=true; opMsg('打包中…');
  546. try{
  547. const r=await fetch('/api/export',{method:'POST',headers:{'Content-Type':'application/json'},
  548. body:JSON.stringify({game})});
  549. const d=await r.json();
  550. if(d.ok) opMsg('✅ 已导出到 '+d.pack);
  551. else opMsg('❌ '+(d.error||'导出失败'),false);
  552. }catch(e){ opMsg('请求失败: '+e,false); }
  553. btn.disabled=false;
  554. };
  555. $('#deleteBtn').onclick=async()=>{
  556. const game=$('#gameSel').value;
  557. if(!game||game==='(暂无)'){ opMsg('没有可删除的资源库',false); return; }
  558. if(!confirm(`确定删除资源库「${game}」?此操作会从磁盘删掉 out/${game} 整个文件夹,不可恢复。`)) return;
  559. const btn=$('#deleteBtn'); btn.disabled=true; opMsg('删除中…');
  560. try{
  561. const r=await fetch('/api/delete',{method:'POST',headers:{'Content-Type':'application/json'},
  562. body:JSON.stringify({game})});
  563. const d=await r.json();
  564. if(d.ok){ opMsg('✅ 已删除 '+d.deleted); await loadLibrary(); }
  565. else opMsg('❌ '+(d.error||'删除失败'),false);
  566. }catch(e){ opMsg('请求失败: '+e,false); }
  567. btn.disabled=false;
  568. };
  569. $('#startBtn').onclick=async()=>{
  570. const btn=$('#startBtn'); const log=$('#log');
  571. btn.disabled=true; log.style.display='block'; log.textContent='任务创建中…';
  572. try{
  573. const r=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},
  574. body:JSON.stringify({
  575. provider:$('#provider').value, api_key:$('#apiKey').value,
  576. base_url:$('#baseUrl').value, model:$('#model').value, size:$('#size').value,
  577. remove_bg:$('#removeBg').checked,
  578. manifest:$('#manifest').value, async:true })});
  579. const d=await r.json();
  580. if(!d.ok || !d.jobId){
  581. log.textContent=(d.logs||[]).join('\n')+(d.error?('\n❌ '+d.error):'');
  582. btn.disabled=false;
  583. return;
  584. }
  585. await pollJob(d.jobId, log, btn);
  586. }catch(e){ log.textContent='请求失败: '+e; }
  587. };
  588. async function pollJob(jobId, log, btn){
  589. let lastText='';
  590. while(true){
  591. let d;
  592. try{
  593. d=await (await fetch('/api/job?id='+encodeURIComponent(jobId))).json();
  594. }catch(e){
  595. log.textContent=lastText+'\n轮询失败: '+e;
  596. btn.disabled=false;
  597. return;
  598. }
  599. const lines=d.logs||[];
  600. const head=`任务 ${jobId.slice(0,8)} · ${d.status||'running'} · ${(d.game||'')}`;
  601. lastText=head+'\n'+lines.join('\n')+(d.error?('\n❌ '+d.error):'');
  602. log.textContent=lastText;
  603. log.scrollTop=log.scrollHeight;
  604. if(d.status==='done'){
  605. await loadLibrary(d.game);
  606. btn.disabled=false;
  607. return;
  608. }
  609. if(d.status==='error'){
  610. btn.disabled=false;
  611. return;
  612. }
  613. await new Promise(r=>setTimeout(r,1200));
  614. }
  615. }
  616. loadManifest(); loadLibrary();
  617. </script>
  618. </body>
  619. </html>