Download index.html from gp01002/RegenerativeAILearningModels: direct link, hf CLI and curl.
- Browser
- Download file 20.8 kB
-
https://huggingface.co/spaces/gp01002/RegenerativeAILearningModels/resolve/main/index.html
- Command line
-
hf download hf://spaces/gp01002/RegenerativeAILearningModels/index.html
-
curl -L -o index.html https://huggingface.co/spaces/gp01002/RegenerativeAILearningModels/resolve/main/index.html
20.8 kB
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>再生式AI學習-立體創作|3D DNA 版</title> | |
| <!-- Import Map:定義模組路徑 --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <style> | |
| :root{ | |
| --prime:#06ffa5; | |
| --ink:#e5e7eb; | |
| --panel:#0b1020cc; | |
| --panel2:#121a33ea; | |
| --accent:#3a86ff; | |
| --warn:#ff6b6b; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100%} | |
| body{ | |
| font-family: "Microsoft JhengHei",system-ui,Segoe UI,Arial; | |
| background: radial-gradient(1200px 600px at 50% 40%, #0c1230 0%, #0a0f25 60%, #070b18 100%); | |
| color:#fff; overflow:hidden; | |
| } | |
| /* 星空背景 */ | |
| canvas#stars{position:fixed;inset:0;z-index:-2} | |
| /* 3D 區域 */ | |
| #stage{ | |
| position:fixed; inset:0; | |
| display:grid; place-items:center; | |
| z-index:0; | |
| } | |
| #three-container{ | |
| width:min(1100px, 90vw); | |
| height:min(640px, 62vh); | |
| border-radius:24px; | |
| border:1px solid #0f1a3b; | |
| box-shadow: 0 20px 80px rgba(0,0,0,.45), inset 0 0 0 1px #1b2d57; | |
| background: radial-gradient(400px 300px at 50% 40%, rgba(16,28,56,.65), rgba(9,15,30,.65)); | |
| position:relative; | |
| } | |
| /* 四階段浮標 */ | |
| .loop-tag{ | |
| position:absolute; padding:8px 12px; border-radius:12px; | |
| background:var(--panel); border:1px solid rgba(255,255,255,.12); | |
| backdrop-filter: blur(6px); font-size:.9rem; line-height:1.2; | |
| box-shadow:0 6px 24px rgba(0,0,0,.3); | |
| user-select:none; pointer-events:none; | |
| } | |
| #tag1{ top:12px; left:12px } /* 具身感知 */ | |
| #tag2{ top:12px; right:12px } /* 感知編碼 */ | |
| #tag3{ bottom:12px; right:12px } /* 數位解碼 */ | |
| #tag4{ bottom:12px; left:12px } /* 具身再詮釋 */ | |
| /* 標題 */ | |
| header{ | |
| position:fixed; top:22px; left:50%; transform:translateX(-50%); | |
| text-align:center; z-index:10; | |
| } | |
| header h1{ | |
| font-size: clamp(28px, 3.6vw, 44px); | |
| letter-spacing:.02em; font-weight:800; | |
| background:linear-gradient(90deg,#66ffe0,#9ae6ff,#b3a6ff); | |
| -webkit-background-clip:text; background-clip:text; color:transparent; | |
| text-shadow: 0 0 40px rgba(6,255,165,.15); | |
| } | |
| header p{ opacity:.72; margin-top:6px; font-size:.95rem } | |
| /* 左上:詠唱台 */ | |
| .panel{ | |
| position:fixed; left:18px; top:18px; width:280px; | |
| background:var(--panel2); border:1px solid rgba(255,255,255,.12); | |
| border-radius:16px; padding:14px; z-index:12; | |
| box-shadow:0 12px 40px rgba(0,0,0,.35); | |
| } | |
| .panel h3{font-size:.95rem; color:#b7c9ff; margin-bottom:8px} | |
| textarea{ | |
| width:100%; min-height:90px; resize:vertical; | |
| background:transparent; color:#fff; border:1px solid #30406e; border-radius:10px; | |
| padding:10px; font-size:.95rem; | |
| } | |
| .hint{ margin-top:8px; font-size:.85rem; color:#bcd6ff } | |
| .select-row{ margin-top:10px; display:flex; gap:8px; align-items:center } | |
| select{ | |
| flex:1; padding:8px 10px; border-radius:10px; background:#0e1633; color:#fff; | |
| border:1px solid #30406e; | |
| } | |
| /* 左下:畫布 */ | |
| #canvasBox{ | |
| position:fixed; | |
| left: 18px; | |
| bottom: 24px; | |
| z-index: 12; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .canvas-label{ | |
| background:#ff6b6bcc; color:#fff; border:1px solid #ff9a9a; | |
| border-radius:14px; padding:10px 12px; font-weight:700; font-size:.92rem | |
| } | |
| #draw{ width:220px; height:220px; background:#fff; border-radius:18px; border:3px solid #ff8a8a; cursor:crosshair } | |
| /* 右側:進化紀錄 */ | |
| #log{ | |
| position:fixed; right:10px; top:0; bottom:0; width:180px; z-index:11; | |
| background: linear-gradient(180deg, rgba(5,10,23,.86), rgba(5,10,23,.6)); | |
| border-left:1px solid rgba(255,255,255,.08); padding:70px 10px 14px; overflow-y:auto; | |
| } | |
| #log h4{ position:fixed; right:14px; top:14px; color:#b7fbe8; } | |
| .log-item{ | |
| margin-bottom:16px; text-align:center; | |
| } | |
| .thumb{ | |
| width:120px; height:120px; border-radius:999px; overflow:hidden; | |
| border:3px solid var(--prime); margin:6px auto; display:grid;place-items:center; background:#0d152f; | |
| cursor:pointer; transition:.2s transform; | |
| } | |
| .thumb:hover{ transform:scale(1.05) } | |
| .gen{ color:var(--prime); font-weight:700; font-size:.9rem } | |
| /* 主按鈕 */ | |
| #mutate{ | |
| position:fixed; | |
| bottom: 320px; | |
| left: 80px; | |
| width:86px; height:86px; border-radius:999px; | |
| background: radial-gradient(120px 120px at 30% 30%, #19ffbf 0%, #14b8ff 60%, #0b79ff 100%); | |
| border:none; color:#001018; font-size:36px; font-weight:900; cursor:pointer; z-index:12; | |
| box-shadow:0 10px 40px rgba(20,184,255,.4), inset 0 0 0 2px #012e3b; transition:.15s transform; | |
| } | |
| #mutate:hover{ transform:scale(1.06) } | |
| #mutate:disabled{ opacity:.5; cursor:not-allowed; filter:grayscale(1) } | |
| /* 作品彈窗 */ | |
| #viewer{ | |
| position:fixed; inset:0; display:none; place-items:center; z-index:20; | |
| background:rgba(0,0,0,.6); | |
| } | |
| .card{ | |
| width:min(92vw,520px); background:#050b20f2; border:1px solid rgba(255,255,255,.16); | |
| border-radius:22px; padding:22px; box-shadow:0 30px 80px rgba(0,0,0,.6); | |
| } | |
| .imgbox{ | |
| width:100%; aspect-ratio:1/1; border-radius:16px; overflow:hidden; border:1px solid #1a2a57; | |
| background:#0b1431; display:grid; place-items:center; margin-bottom:12px; | |
| } | |
| .traits{ | |
| white-space:pre-wrap; background:#0b1431; border:1px solid #22346a; padding:10px 12px; border-radius:12px; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:.9rem; color:#bfeaff; | |
| } | |
| .close{ margin-top:12px; display:inline-block; padding:8px 14px; border-radius:999px; border:1px solid #2b3f75; color:#cfeaff; cursor:pointer } | |
| /* 錯誤訊息 */ | |
| #err{ | |
| position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); | |
| display:none; background:#ff7777; color:#fff; padding:14px 18px; border-radius:14px; z-index:30; | |
| } | |
| /* 手機簡化 */ | |
| @media (max-width: 900px){ | |
| #log{ display:none } /* 紀錄在手機上隱藏以簡化介面 */ | |
| /* 調整頂部元素 */ | |
| header { | |
| top: 12px; | |
| } | |
| header h1 { | |
| font-size: clamp(20px, 5vw, 24px); | |
| } | |
| header p { | |
| display: none; | |
| } | |
| /* 詠唱台調整 */ | |
| .panel{ | |
| width: 90vw; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| top: 55px; /* 移到標題下方 */ | |
| bottom: auto; | |
| } | |
| textarea { | |
| min-height: 45px; | |
| font-size: 0.9rem; | |
| } | |
| .hint, .select-row { | |
| margin-top: 8px; | |
| font-size: 0.8rem; | |
| } | |
| select { | |
| padding: 6px 8px; | |
| } | |
| /* 調整 3D 畫布,為下方控制項留出空間 */ | |
| #three-container{ | |
| width: 95vw; | |
| height: calc(100vh - 280px); /* 計算剩餘高度 */ | |
| top: 170px; /* 放在詠唱台下方 */ | |
| margin-top: 0; | |
| } | |
| /* 調整創作畫布 */ | |
| #canvasBox { | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 8px; | |
| left: 15px; | |
| bottom: 15px; | |
| transform: none; /* 移除縮放,避免模糊 */ | |
| } | |
| #draw { | |
| width: 140px; /* 縮小畫布尺寸 */ | |
| height: 140px; | |
| } | |
| .canvas-label { | |
| padding: 8px 10px; | |
| font-size: 0.85rem; | |
| } | |
| /* 調整主按鈕 */ | |
| #mutate{ | |
| width: 72px; /* 縮小按鈕尺寸 */ | |
| height: 72px; | |
| font-size: 32px; | |
| right: 15px; | |
| bottom: 15px; | |
| left: auto; | |
| transform: none; /* 移除縮放 */ | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <canvas id="stars"></canvas> | |
| <header> | |
| <h1>再生式AI學習-立體創作</h1> | |
| <p>人機共創的生物進化實驗室 · 免費無限創作</p> | |
| </header> | |
| <!-- 左上:詠唱台 --> | |
| <section class="panel" aria-label="詠唱台"> | |
| <h3>詠唱台(輸入關鍵詞)</h3> | |
| <textarea id="prompt" placeholder="例:水晶洞穴中的森林,賽博龐克,霧氣光束,沉靜調性"></textarea> | |
| <div class="hint">創意挑戰:<span id="challenge"></span></div> | |
| <div class="select-row"> | |
| <span style="font-size:.9rem;color:#9adfd2">AI引擎:</span> | |
| <select id="api"> | |
| <option value="pollinations">Pollinations(推薦)</option> | |
| <option value="unsplash">Unsplash(搜圖)</option> | |
| <option value="picsum">Picsum(隨機)</option> | |
| </select> | |
| </div> | |
| </section> | |
| <!-- 左下:畫布 --> | |
| <section id="canvasBox"> | |
| <div class="canvas-label">🎨 創作畫布(雙擊清除)</div> | |
| <canvas id="draw" width="220" height="220"></canvas> | |
| </section> | |
| <!-- 右側:進化紀錄 --> | |
| <aside id="log" aria-label="進化紀錄"> | |
| <h4>🧬 進化紀錄</h4> | |
| <div class="log-item" id="seedLog"> | |
| <div class="gen">第0代</div> | |
| <div class="thumb" style="border-color:#2cf6c5">📝</div> | |
| </div> | |
| </aside> | |
| <!-- 中央 3D 舞台 --> | |
| <main id="stage" aria-label="3D DNA"> | |
| <div id="three-container"> | |
| <div id="tag1" class="loop-tag">① 具身感知<br><small>Embodied Sensing</small></div> | |
| <div id="tag2" class="loop-tag">② 感知編碼<br><small>Sensory Encoding</small></div> | |
| <div id="tag3" class="loop-tag">③ 數位解碼<br><small>Digital Decoding</small></div> | |
| <div id="tag4" class="loop-tag">④ 具身再詮釋<br><small>Embodied Reinterpretation</small></div> | |
| </div> | |
| </main> | |
| <button id="mutate" title="觸發突變">🧬</button> | |
| <!-- 作品檢視 --> | |
| <div id="viewer"> | |
| <div class="card"> | |
| <div class="imgbox" id="bigImg">🖼️</div> | |
| <div id="spec" class="traits"></div> | |
| <div class="close" id="close">關閉</div> | |
| </div> | |
| </div> | |
| <div id="err"></div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
| /* ========= 星空背景 ========= */ | |
| (function stars(){ | |
| const cvs=document.getElementById('stars'), ctx=cvs.getContext('2d'); | |
| function size(){ cvs.width=innerWidth; cvs.height=innerHeight } | |
| size(); addEventListener('resize',size); | |
| const N=180, s=[]; | |
| for(let i=0;i<N;i++){ | |
| s.push({x:Math.random()*cvs.width, y:Math.random()*cvs.height, | |
| r:Math.random()*1.4+0.3, a:Math.random(), v:(Math.random()*0.5+0.2)}); | |
| } | |
| function tick(){ | |
| ctx.clearRect(0,0,cvs.width,cvs.height); | |
| for(const p of s){ | |
| p.a += (Math.random()-0.5)*0.03; | |
| p.a = Math.min(1,Math.max(0.2,p.a)); | |
| ctx.globalAlpha=p.a; | |
| ctx.fillStyle='#ffffff'; | |
| ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); | |
| } | |
| requestAnimationFrame(tick); | |
| } | |
| tick(); | |
| })(); | |
| /* ========= 3D DNA 建構 ========= */ | |
| let scene, camera, renderer, controls, dnaGroup, autoRotate=true; | |
| function init3D(){ | |
| const wrap = document.getElementById('three-container'); | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera(38, wrap.clientWidth / wrap.clientHeight, 0.1, 2000); | |
| camera.position.set(0, 30, 170); | |
| renderer = new THREE.WebGLRenderer({ antialias:true, alpha:true }); | |
| renderer.setPixelRatio(devicePixelRatio); | |
| renderer.setSize(wrap.clientWidth, wrap.clientHeight); | |
| wrap.appendChild(renderer.domElement); | |
| // 柔和燈光 | |
| scene.add(new THREE.AmbientLight(0x88c2ff, 0.5)); | |
| const key = new THREE.DirectionalLight(0x90ffe0, 1.0); key.position.set(80,120,60); scene.add(key); | |
| const rim = new THREE.PointLight(0x5cb3ff, 1.2, 500, 2); rim.position.set(-120,-30,140); scene.add(rim); | |
| // 控制器(滑鼠/觸控) | |
| controls = new OrbitControls(camera, renderer.domElement); | |
| controls.enableDamping = true; controls.dampingFactor = 0.06; | |
| controls.rotateSpeed = 0.6; controls.enablePan=false; controls.minDistance=90; controls.maxDistance=260; | |
| buildDNA(); | |
| // 光暈背景圓 | |
| const geo = new THREE.RingGeometry(70, 72, 64); | |
| const mat = new THREE.MeshBasicMaterial({ color:0x0dfdc1, transparent:true, opacity:0.06, side:THREE.DoubleSide}); | |
| const ring = new THREE.Mesh(geo,mat); ring.rotation.x = -Math.PI/2; ring.position.y = -60; scene.add(ring); | |
| // 自動旋轉(手機/閒置) | |
| let idleTimer; function idle(){ autoRotate=true } | |
| function active(){ autoRotate=false; clearTimeout(idleTimer); idleTimer=setTimeout(idle,2500) } | |
| renderer.domElement.addEventListener('pointerdown',active); renderer.domElement.addEventListener('wheel',active); | |
| idleTimer=setTimeout(idle, 1800); | |
| // 尺寸變更 | |
| addEventListener('resize', ()=>{ | |
| const w=wrap.clientWidth, h=wrap.clientHeight; | |
| camera.aspect = w/h; camera.updateProjectionMatrix(); | |
| renderer.setSize(w,h); | |
| }); | |
| // 動畫迴圈 | |
| (function animate(){ | |
| requestAnimationFrame(animate); | |
| if (dnaGroup){ | |
| if(autoRotate) dnaGroup.rotation.y += 0.0045; | |
| dnaGroup.children.forEach(n=>{ | |
| if(n.userData && n.userData.type==='node'){ n.rotation.y += 0.01 } | |
| }); | |
| } | |
| controls.update(); | |
| renderer.render(scene, camera); | |
| })(); | |
| } | |
| function buildDNA(){ | |
| if(dnaGroup){ scene.remove(dnaGroup) } | |
| dnaGroup = new THREE.Group(); | |
| const radius = 36, height = 160, turns = 6, steps = 240; | |
| const strandMat1 = new THREE.MeshPhongMaterial({ color:0xff6b6b, emissive:0x300000, shininess:60 }); | |
| const strandMat2 = new THREE.MeshPhongMaterial({ color:0x6b7bff, emissive:0x000030, shininess:60 }); | |
| const rungMat = new THREE.MeshPhongMaterial({ color:0xb9e5ff, emissive:0x001a22, shininess:40, transparent:true, opacity:0.72 }); | |
| const sphereGeo = new THREE.SphereGeometry(1.8, 16, 16); | |
| const cylGeo = new THREE.CylinderGeometry(0.65, 0.65, radius*2, 16); | |
| for (let i=0;i<=steps;i++){ | |
| const t = i/steps, y = (t-0.5)*height; | |
| const ang = t*turns*Math.PI*2; | |
| // strand A | |
| const ax = Math.cos(ang)*radius, az = Math.sin(ang)*radius; | |
| const a = new THREE.Mesh(sphereGeo, strandMat1); a.position.set(ax,y,az); | |
| a.userData.type='node'; dnaGroup.add(a); | |
| // strand B (相位差 pi) | |
| const bx = Math.cos(ang+Math.PI)*radius, bz = Math.sin(ang+Math.PI)*radius; | |
| const b = new THREE.Mesh(sphereGeo, strandMat2); b.position.set(bx,y,bz); | |
| b.userData.type='node'; dnaGroup.add(b); | |
| // 每隔幾步放一個「梯級」 | |
| if(i%10===0){ | |
| const rung = new THREE.Mesh(cylGeo, rungMat); | |
| // 將圓柱擺到 A 與 B 中間,並旋向 AB 連線 | |
| const mid = new THREE.Vector3().addVectors(new THREE.Vector3(ax,y,az), new THREE.Vector3(bx,y,bz)).multiplyScalar(0.5); | |
| rung.position.copy(mid); | |
| // 計算朝向 | |
| rung.lookAt(ax,y,az); | |
| rung.rotateX(Math.PI/2); | |
| dnaGroup.add(rung); | |
| } | |
| } | |
| // 柔和外殼(體積暈光) | |
| const shell = new THREE.Mesh( | |
| new THREE.CylinderGeometry(radius*1.18, radius*1.18, height*0.94, 48, 1, true), | |
| new THREE.MeshPhongMaterial({ color:0x0dfdc1, transparent:true, opacity:0.06, side:THREE.DoubleSide }) | |
| ); | |
| shell.rotation.z = Math.PI/2; | |
| dnaGroup.add(shell); | |
| scene.add(dnaGroup); | |
| } | |
| /* ========= UI/業務邏輯 ========= */ | |
| const challenges = [ | |
| "水晶洞穴中的森林","會唱歌的蘑菇","漂浮在雲中的鯨魚", | |
| "由星塵構成的城市","機械蝴蝶","時間旅行者的懷錶", | |
| "熔岩燈裡的宇宙","一本有生命的魔法書" | |
| ]; | |
| const challengeEl = document.getElementById('challenge'); | |
| function newChallenge(){ challengeEl.textContent = challenges[Math.floor(Math.random()*challenges.length)] } | |
| newChallenge(); | |
| let historyList = []; // {id, imageUrl, traits} | |
| let generation = 0; | |
| function $(id){ return document.getElementById(id) } | |
| const err = $('err'); | |
| function showErr(msg){ | |
| err.textContent = msg; err.style.display='block'; | |
| setTimeout(()=>err.style.display='none', 2800); | |
| } | |
| function isCanvasDirty(c){ | |
| const ctx=c.getContext('2d'); | |
| const buf = new Uint32Array(ctx.getImageData(0,0,c.width,c.height).data.buffer); | |
| return buf.some(v=>v!==0); | |
| } | |
| /* 畫布 */ | |
| (function initCanvas(){ | |
| const c=$('draw'), ctx=c.getContext('2d'); | |
| ctx.strokeStyle='#111'; ctx.lineWidth=5; ctx.lineCap='round'; | |
| let drawing=false; | |
| function pos(e){ | |
| const r=c.getBoundingClientRect(), sx=c.width/r.width, sy=c.height/r.height; | |
| const x=(e.touches?e.touches[0].clientX:e.clientX)-r.left; | |
| const y=(e.touches?e.touches[0].clientY:e.clientY)-r.top; | |
| return {x:x*sx, y:y*sy}; | |
| } | |
| function down(e){ drawing=true; const p=pos(e); ctx.beginPath(); ctx.moveTo(p.x,p.y) } | |
| function move(e){ if(!drawing) return; e.preventDefault(); const p=pos(e); ctx.lineTo(p.x,p.y); ctx.stroke() } | |
| function up(){ drawing=false } | |
| c.addEventListener('mousedown',down); c.addEventListener('mousemove',move); | |
| addEventListener('mouseup',up); | |
| c.addEventListener('touchstart',down,{passive:false}); c.addEventListener('touchmove',move,{passive:false}); | |
| c.addEventListener('touchend',up); | |
| c.addEventListener('dblclick',()=>ctx.clearRect(0,0,c.width,c.height)); | |
| })(); | |
| /* 生成圖片 */ | |
| async function generateImage(prompt, provider){ | |
| const p = encodeURIComponent(prompt); | |
| let url; | |
| if(provider==='pollinations'){ | |
| const seed = Math.floor(Math.random()*100000); | |
| url = `https://image.pollinations.ai/prompt/${p}?seed=${seed}&width=640&height=640`; | |
| }else if(provider==='unsplash'){ | |
| url = `https://source.unsplash.com/640x640/?${p}`; | |
| }else{ | |
| url = `https://picsum.photos/640/640?random=${Math.random()}`; | |
| } | |
| const res = await fetch(url, { cache:'no-store' }); | |
| if(!res.ok) throw new Error(`API ${provider} 回應 ${res.status}`); | |
| return res.url; // 最終 redirect 後的實際 URL | |
| } | |
| /* 突變流程 */ | |
| $('mutate').addEventListener('click', async ()=>{ | |
| const c = $('draw'), prompt = $('prompt').value.trim(), api=$('api').value; | |
| if(!prompt && !isCanvasDirty(c)) { showErr('請先在「詠唱台」輸入關鍵詞或在畫布上創作'); return } | |
| $('mutate').disabled = true; $('mutate').textContent = '⏳'; | |
| // 若畫布有內容,幫 prompt 加上線描語意(與論文中的「感知編碼」呼應) | |
| let finalPrompt = prompt; | |
| if(isCanvasDirty(c)) finalPrompt = `a simple line art sketch of ${finalPrompt || 'abstract form'}, high contrast, minimal, vector-like`; | |
| finalPrompt += ", beautiful, intricate details, high quality"; | |
| try{ | |
| const url = await generateImage(finalPrompt, api); | |
| addHistory(url, finalPrompt); | |
| openViewer(url, finalPrompt); | |
| newChallenge(); | |
| }catch(e){ | |
| console.error(e); showErr('生成失敗:'+e.message); | |
| }finally{ | |
| $('mutate').disabled=false; $('mutate').textContent='🧬'; | |
| } | |
| }); | |
| function addHistory(imageUrl, traits){ | |
| generation = (historyList[0]?.id || 0) + 1; | |
| const data = { id:generation, imageUrl, traits }; | |
| historyList.unshift(data); | |
| saveHistory(); | |
| // 建立 UI 縮圖 | |
| const log = $('log'); $('seedLog')?.remove(); | |
| const item = document.createElement('div'); item.className='log-item'; | |
| const gen = document.createElement('div'); gen.className='gen'; gen.textContent=`第${data.id}代`; | |
| const th = document.createElement('div'); th.className='thumb'; th.innerHTML=`<img src="${imageUrl}" alt="gen ${data.id}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentElement.textContent='🖼️'">`; | |
| th.onclick = ()=>openViewer(imageUrl, traits); | |
| item.appendChild(gen); item.appendChild(th); | |
| log.appendChild(item); | |
| } | |
| /* 作品檢視 */ | |
| function openViewer(url, traits){ | |
| $('viewer').style.display='grid'; | |
| $('bigImg').innerHTML = `<img src="${url}" alt="生成作品" style="width:100%;height:100%;object-fit:cover" onerror="this.parentElement.textContent='🖼️'">`; | |
| $('spec').textContent = `基因序列(Prompt):\n${traits}\n\n思考題:此結果如何反映你的初始感知?下一步如何再詮釋為個人風格?`; | |
| } | |
| $('close').onclick = ()=>$('viewer').style.display='none'; | |
| /* 歷史保存 */ | |
| function saveHistory(){ | |
| try{ localStorage.setItem('evoHistory', JSON.stringify(historyList)) }catch(e){} | |
| } | |
| (function loadHistory(){ | |
| try{ | |
| const raw = localStorage.getItem('evoHistory'); | |
| if(!raw) return; | |
| historyList = JSON.parse(raw) || []; | |
| if(historyList.length){ | |
| $('seedLog')?.remove(); | |
| historyList.slice().reverse().forEach(d=>{ | |
| const item = document.createElement('div'); item.className='log-item'; | |
| const gen = document.createElement('div'); gen.className='gen'; gen.textContent=`第${d.id}代`; | |
| const th = document.createElement('div'); th.className='thumb'; th.style.borderColor='var(--prime)'; | |
| th.innerHTML=`<img src="${d.imageUrl}" alt="gen ${d.id}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentElement.textContent='🖼️'">`; | |
| th.onclick = ()=>openViewer(d.imageUrl, d.traits); | |
| item.appendChild(gen); item.appendChild(th); | |
| $('log').appendChild(item); | |
| }); | |
| generation = historyList[0].id; | |
| } | |
| }catch(e){ historyList=[] } | |
| })(); | |
| /* 啟動 3D */ | |
| init3D(); | |
| </script> | |
| </body> | |
| </html> | |