gp01002's picture
Update index.html
e9020b2 verified
Raw History Blame Contribute Delete
20.8 kB
<!DOCTYPE html>
<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>