MemoryWeaverV2 / index.html
gp01002's picture
Update index.html
16756bd verified
Raw History Blame Contribute Delete
43.4 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>記憶的編織 - 水墨投影系統</title>
<style>
/* CSS Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif; /* 使用 Inter 字體 */
/* 純黑白介面: 從深灰到淺灰的漸變背景 */
background: linear-gradient(135deg, #333333 0%, #1a1a1a 100%);
color: #e0e0e0; /* 淺灰色文字 */
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden; /* 防止滾動條 */
}
/* Main Container for the Application */
.container {
display: flex;
/* 調整容器尺寸,讓整個介面框框更小 */
width: 85%;
max-width: 1200px; /* 限制最大寬度 */
height: 85vh; /* 限制最大高度 */
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6); /* 更深的陰影 */
background: rgba(0, 0, 0, 0.4); /* 半透明黑色 */
}
/* Left and Right Panels Styling */
.left-panel, .right-panel {
flex: 1; /* 初始兩側面板等寬 */
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
background: rgba(0, 0, 0, 0.3); /* 更深的半透明背景 */
backdrop-filter: blur(5px);
margin: 8px; /* 容器內部內容的邊距 */
border-radius: 12px;
}
/* Title Styling */
.title {
text-align: center;
font-size: 1.8rem;
font-weight: 800;
color: #ffffff; /* 純白色標題 */
margin-bottom: 15px;
}
/* Status Panel Styling */
.status-panel, .info-panel {
background: rgba(255, 255, 255, 0.05); /* 淺色透明灰,更暗 */
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
color: #cccccc; /* 淺灰色文字 */
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
}
.status-item:last-child {
margin-bottom: 0; /* 最後一個項目沒有邊距 */
}
/* Controls Button Styling */
.controls {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
.btn {
flex: 1;
padding: 10px 15px; /* 稍大的內邊距以改善觸控目標 */
border: none;
border-radius: 8px; /* 更圓的角 */
color: #ffffff; /* 按鈕文字為白色 */
font-weight: bold;
font-size: 0.9rem; /* 稍大的字體 */
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease; /* 添加陰影過渡 */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); /* 更深的陰影 */
}
.btn:hover {
transform: translateY(-2px); /* 更明顯的抬升效果 */
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4); /* 懸停時更深的陰影 */
}
/* 純黑白按鈕顏色 */
.btn-primary { background: #444444; } /* 深灰 */
.btn-secondary { background: #666666; } /* 中灰 */
.btn-tertiary { background: #888888; } /* 淺灰 */
.btn-special { background: #aaaaaa; } /* 更淺的灰 */
/* Canvas Container */
.canvas-container {
flex: 1; /* 讓容器佔滿可用空間 */
background: #000; /* 純黑色背景 */
border-radius: 8px;
border: 2px solid #555555; /* 深灰色邊框 */
overflow: hidden;
position: relative;
display: flex; /* 使用 flex 來居中 canvas 內容 */
align-items: center;
justify-content: center;
min-height: 300px; /* 確保 canvas 區域有最小高度 */
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
background-color: #f0f0f0; /* 淺灰色畫布背景,確保黑色筆觸可見 */
}
.overlay {
position: absolute;
top: 10px; /* 調整位置 */
left: 10px;
background: rgba(0, 0, 0, 0.8); /* 更深的半透明黑色 */
padding: 6px 10px; /* 稍大的內邊距 */
border-radius: 5px; /* 更圓的角 */
font-size: 0.8rem;
color: #fff;
}
/* AI Area Styling */
.ai-area {
flex: 4; /* 在右側面板佔據更多空間 */
background: rgba(255, 255, 255, 0.05); /* 淺色透明灰,更暗 */
border-radius: 8px;
padding: 15px; /* 增加內邊距 */
border: 2px solid #888888; /* 中灰色邊框 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 350px; /* 確保有足夠的高度 */
position: relative; /* 用於載入覆蓋 */
}
.ai-area h3 {
color: #ffffff; /* 純白色標題 */
margin-bottom: 10px;
font-size: 1.2rem; /* 較大的字體 */
}
/* Hidden by default, will be replaced by canvas */
.ai-area img {
display: none;
}
#aiStatus {
font-size: 1.1rem; /* 較大的字體 */
color: #ffffff; /* 純白色文字 */
text-align: center;
padding: 20px;
position: absolute; /* 在 ai-area 內居中 */
background: rgba(0, 0, 0, 0.8); /* 更深的半透明黑色 */
border-radius: 8px;
width: calc(100% - 30px); /* 調整寬度以考慮內邊距 */
height: calc(100% - 30px); /* 調整高度 */
display: flex;
align-items: center;
justify-content: center;
top: 15px;
left: 15px;
}
#aiCanvas {
display: none; /* 預設隱藏,生成後顯示 */
border-radius: 8px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
width: 100%;
height: 100%;
}
/* Info Panel within Right Panel */
.info-panel h4 {
color: #ffffff; /* 純白色文字 */
margin-bottom: 8px;
font-size: 1.05rem;
}
.prompt-display {
background: rgba(0, 0, 0, 0.6); /* 更深的半透明黑色 */
padding: 10px;
border-radius: 6px;
font-size: 0.85rem;
word-break: break-all;
min-height: 60px; /* 確保較長提示詞的可見性 */
display: flex;
align-items: center;
color: #eeeeee; /* 淺灰色文字 */
}
/* Message Box */
.message {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 6px;
color: white;
font-weight: bold;
z-index: 1000;
transform: translateX(120%);
transition: transform 0.3s ease, opacity 0.3s ease;
opacity: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.message.show {
transform: translateX(0);
opacity: 1;
}
/* 純黑白訊息框顏色 */
.message.success { background: #444444; } /* 深灰 */
.message.error { background: #888888; } /* 中灰 */
.message.info { background: #666666; } /* 淺灰 */
/* Loading Spinner */
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3); /* 淺色半透明邊框 */
border-top: 4px solid #ffffff; /* 純白色旋轉部分 */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive adjustments */
@media (max-width: 900px) {
.container {
flex-direction: column;
height: auto;
min-height: 95vh;
}
.left-panel, .right-panel {
width: calc(100% - 16px); /* 寬度佔滿減去邊距 */
margin-bottom: 16px;
}
.controls {
flex-wrap: wrap; /* 讓按鈕換行 */
}
.btn {
flex: none;
width: 48%; /* 每行兩個按鈕 */
}
.ai-area {
min-height: 250px; /* 調整小螢幕的最小高度 */
}
}
</style>
</head>
<body>
<div class="container">
<div class="left-panel">
<h1 class="title">記憶的編織</h1>
<div class="status-panel">
<div class="status-item">
<span>系統狀態:</span>
<span id="systemStatus">待機</span>
</div>
<div class="status-item">
<span>FPS:</span>
<span id="fpsDisplay">0</span>
</div>
<div class="status-item">
<span>漣漪數:</span>
<span id="rippleCount">0</span>
</div>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="startSystem()">啟動</button>
<button class="btn btn-secondary" onclick="resetSystem()">重置</button>
<button class="btn btn-tertiary" onclick="togglePause()">暫停</button>
<button class="btn btn-special" onclick="generateCanvasArt()">生成</button>
</div>
<div class="canvas-container">
<canvas id="mainCanvas"></canvas>
<div class="overlay">水墨投影 - 移動滑鼠體驗</div>
</div>
</div>
<div class="right-panel">
<h1 class="title">程式化生成</h1>
<div class="info-panel">
<h4>狀態信息</h4>
<div class="status-item">
<span>滑鼠速度:</span>
<span id="mouseSpeed">0</span>
</div>
<div class="status-item">
<span>生成狀態:</span>
<span id="aiGenStatus">待機</span>
</div>
</div>
<div class="info-panel">
<h4>生成描述</h4>
<div class="prompt-display" id="promptText">
點擊「生成」按鈕以產生程式化圖案...
</div>
</div>
<div class="ai-area">
<h3>生成結果</h3>
<div id="aiStatus">
<div class="spinner" style="display:none;"></div>
點擊生成按鈕開始創作
</div>
<canvas id="aiCanvas" style="display: none;"></canvas>
<div class="action-buttons" style="display: flex; gap: 10px; margin-top: 15px;">
<button class="btn btn-secondary" onclick="copyAICanvasImage()">複製圖片</button>
<button class="btn btn-primary" onclick="downloadAICanvasImage()">下載圖片</button>
</div>
</div>
</div>
</div>
<div class="message" id="messageBox"></div>
<script>
console.log('🎨 啟動水墨系統...');
// 全域變數
let canvas, ctx; // 主水墨畫布
let aiCanvas, aiCtx; // AI 生成畫布
let isRunning = false;
let isPaused = false;
let mouseX = 0, mouseY = 0, mouseSpeed = 0;
let time = 0;
let nodes = [];
let ripples = [];
let frameCount = 0;
let lastFpsTime = Date.now();
// 追蹤當前 AI 生成圖案模式
let currentGenerationMode = 0; // 0 for connected dots (weaving), 1 for flow field
/**
* 初始化主畫布和 AI 畫布,並設置事件監聽器。
* 此函數在 DOM 完全載入後調用。
*/
function init() {
console.log('初始化系統...');
canvas = document.getElementById('mainCanvas');
aiCanvas = document.getElementById('aiCanvas');
if (!canvas || !aiCanvas) {
console.error('找不到畫布元素');
return;
}
ctx = canvas.getContext('2d');
aiCtx = aiCanvas.getContext('2d');
setupCanvas(); // 設置主畫布尺寸
setupAICanvas(); // 設置 AI 畫布尺寸
setupEvents(); // 設置事件監聽
createNodes(); // 初始化水墨節點
showMessage('系統初始化完成!', 'success');
console.log('✅ 初始化完成');
}
/**
* 設置主畫布尺寸以匹配其父容器,並初始化滑鼠座標。
*/
function setupCanvas() {
const container = canvas.parentElement;
const rect = container.getBoundingClientRect();
// 設置畫布尺寸以匹配容器,確保最小尺寸
canvas.width = rect.width; // 讓畫布完全填滿容器
canvas.height = rect.height; // 讓畫布完全填滿容器
// 初始將滑鼠置於中心
mouseX = canvas.width / 2;
mouseY = canvas.height / 2;
console.log(`主畫布: ${canvas.width}x${canvas.height}`);
// 在畫布上繪製初始訊息
ctx.fillStyle = '#f0f0f0'; /* 淺色背景以呈現水墨效果,與 CSS 中的畫布背景同步 */
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#333333'; /* 深灰色文字 */
ctx.font = '16px Inter';
ctx.textAlign = 'center';
ctx.fillText('系統準備就緒', canvas.width/2, canvas.height/2);
}
/**
* 設置 AI 畫布尺寸以匹配其父容器。
*/
function setupAICanvas() {
const aiArea = document.querySelector('.ai-area');
aiCanvas.width = aiArea.clientWidth;
aiCanvas.height = aiArea.clientHeight;
console.log(`AI 畫布: ${aiCanvas.width}x${aiCanvas.height}`);
// 如果 AI 畫布在 resize 後需要立即重繪其內容,可以在這裡調用 drawAIAbstract()
}
/**
* 設置滑鼠移動和點擊事件監聽器。
*/
function setupEvents() {
// 滑鼠移動處理器
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
// 計算比例因子以獲取高 DPI 螢幕上的精確滑鼠座標
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const newX = (e.clientX - rect.left) * scaleX;
const newY = (e.clientY - rect.top) * scaleY;
// 計算滑鼠速度以實現動態漣漪生成
mouseSpeed = Math.sqrt((newX - mouseX)**2 + (newY - mouseY)**2);
mouseX = newX;
mouseY = newY;
// 更新滑鼠速度的 UI 顯示
document.getElementById('mouseSpeed').textContent = Math.round(mouseSpeed);
// 根據滑鼠速度創建漣漪
// 僅當速度超過閾值且漣漪數量未過多時才創建新漣漪
if (mouseSpeed > 3 && ripples.length < 20) {
createRipple(mouseX, mouseY, mouseSpeed);
}
});
// 點擊處理器以創建強烈漣漪並觸發圖案生成
canvas.addEventListener('click', () => {
createRipple(mouseX, mouseY, 20); // 點擊時產生更強的漣漪
generateCanvasArt(); // 觸發畫布圖案生成
});
// 處理視窗大小調整以保持畫布響應性
window.addEventListener('resize', () => {
setupCanvas(); // 調整主畫布大小
setupAICanvas(); // 調整 AI 畫布大小
});
console.log('事件設置完成');
}
/**
* 填充 `nodes` 陣列,它構成水墨效果的基礎。
* 節點以一種更稀疏且均勻的方式分佈。
*/
function createNodes() {
nodes = [];
const nodeCount = 80; // 增加節點數量以獲得更明顯的編織密度
// 將節點初始分佈在一個較大的中心區域,模擬參考圖案的分散感
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const initialSpreadRadius = Math.min(canvas.width, canvas.height) * 0.4; // 調整初始散佈半徑
for (let i = 0; i < nodeCount; i++) {
const angle = (i / nodeCount) * Math.PI * 2; // 均勻分佈初始角度
const radius = initialSpreadRadius + (Math.random() - 0.5) * 100; // 在基礎半徑上隨機偏移,增加散佈
// 使用角度和半徑計算初始位置,增加點的規律性
let x = centerX + Math.cos(angle) * radius;
let y = centerY + Math.sin(angle) * radius;
// 確保節點仍然在畫布範圍內
x = Math.max(5, Math.min(canvas.width - 5, x));
y = Math.max(5, Math.min(canvas.height - 5, y));
nodes.push({
x: x,
y: y,
originalX: x, // 存儲原始位置以實現彈回效果
originalY: y,
vx: 0, // X 速度
vy: 0, // Y 速度
phase: Math.random() * Math.PI * 2 // 隨機相位以實現獨立運動
});
}
console.log(`創建了 ${nodes.length} 個節點`);
}
/**
* 向 `ripples` 陣列添加新的漣漪效果。
* @param {number} x - 漣漪中心的 X 座標。
* @param {number} y - 漣漪中心的 Y 座標。
* @param {number} intensity - 漣漪的初始強度。
*/
function createRipple(x, y, intensity) {
ripples.push({
x: x,
y: y,
radius: 0, // 漣漪當前半徑
maxRadius: 60 + intensity * 2, // 顯著增加最大半徑,使漣漪更顯著
life: 1, // 生命週期因子,從 1(完整)到 0(消失)
intensity: Math.min(intensity * 0.1, 1.0) // 視覺效果的限定強度
});
document.getElementById('rippleCount').textContent = ripples.length;
}
/**
* 啟動水墨模擬系統。
*/
function startSystem() {
console.log('啟動系統');
isRunning = true;
isPaused = false;
document.getElementById('systemStatus').textContent = '運行中';
showMessage('水墨系統已啟動!', 'success');
animate(); // 啟動動畫循環
}
/**
* 將水墨模擬重置為其初始狀態。
*/
function resetSystem() {
console.log('重置系統');
time = 0;
ripples = [];
createNodes(); // 重新創建節點
document.getElementById('rippleCount').textContent = '0';
showMessage('系統已重置', 'info');
// 重置 AI 畫布區域
document.getElementById('aiCanvas').style.display = 'none';
document.getElementById('aiStatus').style.display = 'flex'; // 顯示預設狀態
document.getElementById('aiStatus').innerHTML = '點擊生成按鈕開始創作';
document.getElementById('aiGenStatus').textContent = '待機';
document.getElementById('promptText').textContent = '點擊「生成」按鈕以產生程式化圖案...';
}
/**
* 切換模擬的暫停狀態。
*/
function togglePause() {
isPaused = !isPaused;
const status = isPaused ? '暫停' : '繼續';
document.getElementById('systemStatus').textContent = status;
showMessage(`系統已${status}`, 'info');
}
/**
* 更新節點和漣漪的物理屬性。
* 包括基本波浪運動、滑鼠互動和漣漪力。
*/
function updatePhysics() {
// 更新節點位置和速度
nodes.forEach(node => {
// 節點的基本流體運動
const waveX = Math.sin(time * 1.5 + node.phase) * 2;
const waveY = Math.cos(time * 1.2 + node.phase) * 2;
const targetX = node.originalX + waveX;
const targetY = node.originalY + waveY;
// 類似彈簧的力將節點拉回目標位置,增加彈性
node.vx += (targetX - node.x) * 0.03;
node.vy += (targetY - node.y) * 0.03;
// 滑鼠對節點的影響 - 更強的效果
const distToMouse = Math.sqrt((node.x - mouseX)**2 + (node.y - mouseY)**2);
if (distToMouse < 200) { // 更大的影響半徑
const influence = (1 - distToMouse / 200) * 1.5; // 更強的影響力
const mouseAngle = Math.atan2(node.y - mouseY, node.x - mouseX);
node.vx += Math.cos(mouseAngle) * influence * mouseSpeed * 0.03; // 影響力與滑鼠速度相關
node.vy += Math.sin(mouseAngle) * influence * mouseSpeed * 0.03;
}
// 漣漪對節點的影響 (水紋影響織布的核心)
ripples.forEach(ripple => {
const distToRipple = Math.sqrt((node.x - ripple.x)**2 + (node.y - ripple.y)**2);
// 檢查節點是否在漣漪擴展邊緣附近
if (Math.abs(distToRipple - ripple.radius) < 40 && distToRipple > 0) { // 更寬的活躍帶,影響更廣
const rippleForce = ripple.intensity * ripple.life * 2.0; // 更強的漣漪力
const rippleAngle = Math.atan2(node.y - ripple.y, node.x - ripple.x);
node.vx += Math.cos(rippleAngle) * rippleForce;
node.vy += Math.sin(rippleAngle) * rippleForce;
}
});
// 施加摩擦並更新位置
node.vx *= 0.7; // 更多阻尼
node.vy *= 0.7;
node.x += node.vx;
node.y += node.vy;
// 邊界碰撞(簡單的反彈)
if (node.x < 0 || node.x > canvas.width) node.vx *= -0.8;
if (node.y < 0 || node.y > canvas.height) node.vy *= -0.8;
});
// 更新漣漪:擴展和衰減
ripples = ripples.filter(ripple => {
ripple.radius += 3.5; // 更快的擴展
ripple.life -= 0.01; // 較慢的衰減,讓漣漪存在時間長一點
return ripple.life > 0 && ripple.radius < ripple.maxRadius;
});
document.getElementById('rippleCount').textContent = ripples.length;
}
/**
* 在畫布上渲染水墨效果。
*/
function render() {
if (!ctx) return;
// 用淺色背景清除畫布以呈現水墨效果
ctx.fillStyle = '#f0f0f0'; /* 畫布背景色與 CSS 中的 #f0f0f0 同步 */
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 應用混合模式以實現墨水般的透明度
ctx.globalCompositeOperation = 'multiply';
// 渲染漣漪
ripples.forEach(ripple => {
const opacity = ripple.life * ripple.intensity;
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 1.0})`; /* 漣漪完全不透明,更明顯 */
ctx.lineWidth = 3.5; /* 顯著增加漣漪線條粗細 */
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
ctx.stroke();
// 內部漣漪以實現分層效果
if (ripple.radius > 10) {
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.6})`; /* 內部漣漪維持較淡但仍可見 */
ctx.lineWidth = 2.0;
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius * 0.7, 0, Math.PI * 2);
ctx.stroke();
}
});
// 渲染節點連接(線條)- 織布效果的核心
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const nodeA = nodes[i];
const nodeB = nodes[j];
const distance = Math.sqrt((nodeA.x - nodeB.x)**2 + (nodeA.y - nodeB.y)**2);
const maxConnectDistance = 250; // 增加最大連接距離,讓更廣泛的連接可見
if (distance < maxConnectDistance) {
// 線性衰減不透明度,並提高基礎值,讓織布感更明顯且不過黑
// 將不透明度基礎值顯著提高,以確保線條可見
const opacity = (1 - distance / maxConnectDistance) * 0.9; /* 調整為線性衰減,最高不透明度 0.9 */
if (opacity > 0.1) { // 設定最小可見度閾值,確保微弱線條也顯示
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity})`;
ctx.lineWidth = 3.0; /* 顯著增加線條粗細,強化織布感 */
ctx.beginPath();
ctx.moveTo(nodeA.x, nodeA.y);
ctx.lineTo(nodeB.x, nodeB.y);
ctx.stroke();
}
}
}
}
/* // 移除節點圓點的繪製
nodes.forEach(node => {
const opacity = 0.9 + Math.sin(time * 2 + node.phase) * 0.1;
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
ctx.beginPath();
ctx.arc(node.x, node.y, 4.5, 0, Math.PI * 2);
ctx.fill();
});
*/
/* // 移除滑鼠指示器圓點
ctx.fillStyle = 'rgba(0, 0, 0, 1.0)';
ctx.beginPath();
ctx.arc(mouseX, mouseY, 7, 0, Math.PI * 2);
ctx.fill();
*/
/* // 移除滑鼠脈衝效果
const pulseSize = 9 + Math.sin(time * 6) * 3;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.9)';
ctx.lineWidth = 3.0;
ctx.beginPath();
ctx.arc(mouseX, mouseY, pulseSize, 0, Math.PI * 2);
ctx.stroke();
*/
// 重置混合模式和濾鏡
ctx.filter = 'none';
ctx.globalCompositeOperation = 'source-over';
}
/**
* 主要動畫循環。
*/
function animate() {
if (!isRunning) return;
if (!isPaused) {
time += 0.02; // 時間前進
updatePhysics(); // 更新節點和漣漪物理
render(); // 渲染畫布
// 計算並顯示 FPS
frameCount++;
const now = Date.now();
if (now - lastFpsTime >= 1000) {
document.getElementById('fpsDisplay').textContent = frameCount;
frameCount = 0;
lastFpsTime = now;
}
}
requestAnimationFrame(animate); // 請求下一幀
}
/**
* 將 AI 生成的畫布內容複製到剪貼簿。
*/
async function copyAICanvasImage() {
try {
// 將 canvas 內容轉換為 Blob
aiCanvas.toBlob(async (blob) => {
// 檢查瀏覽器是否支援 ClipboardItem 和 navigator.clipboard.write
if (navigator.clipboard && navigator.clipboard.write) {
try {
const item = new ClipboardItem({ "image/png": blob });
await navigator.clipboard.write([item]);
showMessage('圖片已複製到剪貼簿!', 'success');
} catch (err) {
console.error('複製到剪貼簿失敗 (Clipboard API):', err);
// Fallback for older browsers or restricted environments
fallbackCopyImage();
}
} else {
// Fallback for browsers that don't support ClipboardItem
fallbackCopyImage();
}
}, 'image/png');
} catch (error) {
console.error('複製圖片失敗:', error);
showMessage('複製圖片失敗!', 'error');
}
}
/**
* 舊版瀏覽器或受限環境的複製圖片備用方案。
* 注意:此方法可能因瀏覽器安全限制而失敗。
*/
function fallbackCopyImage() {
try {
// 創建一個臨時的 img 元素
const img = document.createElement('img');
img.src = aiCanvas.toDataURL('image/png');
document.body.appendChild(img); // 必須添加到 DOM 才能選中
const range = document.createRange();
range.selectNode(img);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand('copy');
window.getSelection().removeAllRanges();
document.body.removeChild(img); // 移除臨時元素
showMessage('圖片已複製到剪貼簿 (可能需要手動貼上)!', 'info');
} catch (err) {
console.error('備用複製方法失敗:', err);
showMessage('複製圖片失敗,請嘗試下載!', 'error');
}
}
/**
* 下載 AI 生成的畫布圖片。
*/
function downloadAICanvasImage() {
try {
const imageUrl = aiCanvas.toDataURL('image/png');
const a = document.createElement('a');
a.href = imageUrl;
a.download = 'generated_art.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
showMessage('圖片已下載!', 'success');
} catch (error) {
console.error('下載圖片失敗:', error);
showMessage('下載圖片失敗!', 'error');
}
}
/**
* 繪製抽象的程式化圖案到 AI 畫布上。
* 這個圖案是基於畫布的現有狀態(節點和漣漪)生成的。
*/
function drawAIAbstract() {
if (!aiCtx) return;
aiCtx.clearRect(0, 0, aiCanvas.width, aiCanvas.height);
aiCtx.fillStyle = '#f0f0f0'; /* 淺色背景,與主畫布同步 */
aiCtx.fillRect(0, 0, aiCanvas.width, aiCanvas.height);
// 將主畫布的節點和漣漪資訊映射到 AI 畫布上
const scaleX = aiCanvas.width / canvas.width;
const scaleY = aiCanvas.height / canvas.height;
if (currentGenerationMode === 0) {
// 模式 0: 連接點的「織布」風格 - 複製主畫布的編織感
document.getElementById('promptText').textContent = '基於水墨狀態的程式化網狀結構。';
// 繪製基於節點的連接線
aiCtx.strokeStyle = 'rgba(0, 0, 0, 0.9)'; /* 更深的不透明度 */
aiCtx.lineWidth = 3.0; // 匹配主畫布的線寬
nodes.forEach(nodeA => {
nodes.forEach(nodeB => {
if (nodeA !== nodeB) {
const dist = Math.sqrt(
(nodeA.x - nodeB.x) ** 2 + (nodeA.y - nodeB.y) ** 2
);
const aiMaxConnectDistance = 250; // 匹配主畫布的連接距離
if (dist < aiMaxConnectDistance) {
const opacity = (1 - dist / aiMaxConnectDistance) * 0.9; // 匹配主畫布的透明度衰減
if (opacity > 0.1) {
aiCtx.strokeStyle = `rgba(0, 0, 0, ${opacity})`;
aiCtx.beginPath();
aiCtx.moveTo(nodeA.x * scaleX, nodeA.y * scaleY);
aiCtx.lineTo(nodeB.x * scaleX, nodeB.y * scaleY);
aiCtx.stroke();
}
}
}
});
});
// 漣漪 (在模式0中以圓圈呈現)
ripples.forEach(ripple => {
const opacity = ripple.life * 1.0; /* 漣漪完全不透明 */
aiCtx.strokeStyle = `rgba(50, 50, 50, ${opacity})`;
aiCtx.lineWidth = 3.5; // 匹配主畫布的線寬
aiCtx.beginPath();
aiCtx.arc(ripple.x * scaleX, ripple.y * scaleY, ripple.radius * scaleX * 0.6, 0, Math.PI * 2);
aiCtx.stroke();
});
/* // 移除數據點 (節點) 的繪製
aiCtx.fillStyle = 'rgba(0, 0, 0, 0.8)';
nodes.forEach(node => {
aiCtx.beginPath();
aiCtx.arc(node.x * scaleX, node.y * scaleY, 4.0, 0, Math.PI * 2);
aiCtx.fill();
});
*/
} else {
// 模式 1: 粒子流動場 (全新畫面) - 提升複雜度和可見度
document.getElementById('promptText').textContent = '隨機粒子流動的抽象演算法圖案。';
const particles = [];
const numParticles = 1500; // 大幅增加粒子數量以獲得更豐富的流動
const particleSpeed = 5 + mouseSpeed * 0.12; // 粒子速度受滑鼠速度影響更明顯
const particleLife = 400; // 粒子生命週期增加,軌跡更長
// 根據漣漪位置初始化一些粒子,或隨機初始化
if (ripples.length > 0) {
ripples.forEach(ripple => {
for(let i=0; i<numParticles/ripples.length; i++) {
particles.push({
x: ripple.x * scaleX + (Math.random() - 0.5) * 200, // 從漣漪周圍開始
y: ripple.y * scaleY + (Math.random() - 0.5) * 200,
vx: (Math.random() - 0.5) * particleSpeed,
vy: (Math.random() - 0.5) * particleSpeed,
life: particleLife,
originalColor: `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})` // 基礎透明度大幅提高
});
}
});
} else {
for (let i = 0; i < numParticles; i++) {
particles.push({
x: Math.random() * aiCanvas.width,
y: Math.random() * aiCanvas.height,
vx: (Math.random() - 0.5) * particleSpeed,
vy: (Math.random() - 0.5) * particleSpeed,
life: particleLife,
originalColor: `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})`
});
}
}
// 繪製粒子軌跡
for (let step = 0; step < 150; step++) { // 增加繪製步進,生成更密集的軌跡
aiCtx.globalAlpha = 0.3; // 提高全局透明度,讓軌跡更明顯
aiCtx.strokeStyle = 'rgba(0, 0, 0, 0.2)'; // 軌跡顏色更深
aiCtx.lineWidth = 4.0; // 軌跡線條更粗
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const currentOpacity = (p.life / particleLife) * (parseFloat(p.originalColor.split(',')[3]) || 0.8); // 調整透明度計算
aiCtx.strokeStyle = `rgba(0, 0, 0, ${currentOpacity})`;
aiCtx.beginPath();
aiCtx.moveTo(p.x, p.y);
// 簡單的流場:基於位置和時間影響速度
p.vx += Math.cos(p.y * 0.01 + time * 0.5) * 0.4; // 增加流場影響
p.vy += Math.sin(p.x * 0.01 + time * 0.5) * 0.4;
// 摩擦力
p.vx *= 0.90; // 略微減少摩擦
p.vy *= 0.90;
p.x += p.vx;
p.y += p.vy;
p.life--;
aiCtx.lineTo(p.x, p.y);
aiCtx.stroke();
// 粒子邊界反彈
if (p.x < 0 || p.x > aiCanvas.width) p.vx *= -1;
if (p.y < 0 || p.y > aiCanvas.height) p.vy *= -1;
// 如果粒子生命週期結束,重新初始化
if (p.life <= 0) {
p.x = Math.random() * aiCanvas.width;
p.y = Math.random() * aiCanvas.height;
p.vx = (Math.random() - 0.5) * particleSpeed;
p.vy = (Math.random() - 0.5) * particleSpeed;
p.life = particleLife;
p.originalColor = `rgba(0, 0, 0, ${0.5 + Math.random() * 0.5})`;
}
}
}
aiCtx.globalAlpha = 1; // 重置全局透明度
}
}
/**
* 觸發本地畫布圖案生成。
* 不再調用外部 AI API。
*/
async function generateCanvasArt() {
console.log('正在生成本地畫布圖案...');
const aiStatusElement = document.getElementById('aiStatus');
const aiGenStatusDisplay = document.getElementById('aiGenStatus');
const spinnerElement = aiStatusElement.querySelector('.spinner');
// 顯示載入狀態
aiGenStatusDisplay.textContent = '生成中...';
aiCanvas.style.display = 'none'; // 隱藏 AI 畫布,顯示狀態覆蓋
aiStatusElement.style.display = 'flex';
aiStatusElement.innerHTML = `<div class="spinner"></div><p>正在生成圖案,請稍候...</p>`;
// Show action buttons container
document.querySelector('.action-buttons').style.display = 'none';
if (spinnerElement) {
spinnerElement.style.display = 'block';
}
// 模擬生成延遲
await new Promise(resolve => setTimeout(resolve, 500));
try {
// 在兩種模式間切換
currentGenerationMode = (currentGenerationMode + 1) % 2;
drawAIAbstract(); // 繪製抽象圖案到 AI 畫布上
aiCanvas.style.display = 'block'; // 顯示 AI 畫布
aiStatusElement.style.display = 'none'; // 隱藏載入狀態
document.querySelector('.action-buttons').style.display = 'flex'; // Show action buttons
aiGenStatusDisplay.textContent = '完成';
showMessage('本地圖案生成完成!', 'success');
} catch (error) {
console.error('生成本地圖案失敗:', error);
aiStatusElement.style.display = 'flex';
aiStatusElement.innerHTML = `生成失敗: ${error.message}`;
aiCanvas.style.display = 'none';
document.querySelector('.action-buttons').style.display = 'none'; // Hide action buttons on error
aiGenStatusDisplay.textContent = '失敗';
showMessage(`生成失敗: ${error.message}`, 'error');
} finally {
if (spinnerElement) {
spinnerElement.style.display = 'none';
}
}
}
/**
* 向使用者顯示臨時訊息框。
* @param {string} text - 訊息文本。
* @param {string} type - 訊息類型 ('success', 'error', 'info')。
*/
function showMessage(text, type) {
const msg = document.getElementById('messageBox');
msg.textContent = text;
msg.className = `message show ${type}`;
// 訊息在 4 秒後淡出
setTimeout(() => {
msg.classList.remove('show');
}, 4000);
}
// 當 DOM 內容完全載入時初始化系統
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM 載入完成');
init();
});
</script>
</body>
</html>