AI-ART / index.html
gp01002's picture
Update index.html
9428e5b verified
Raw History Blame Contribute Delete
33.2 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; /* Using Inter font as per guidelines */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden; /* Prevent body scroll */
}
/* Main Container for the Application */
.container {
display: flex;
width: 95%; /* Responsive width */
max-width: 1400px;
height: 95vh; /* Responsive height */
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.2);
}
/* Left and Right Panels Styling */
.left-panel, .right-panel {
flex: 1; /* Equal width for both panels initially */
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(5px);
margin: 8px; /* Margin around inner content of container */
border-radius: 12px;
}
/* Title Styling */
.title {
text-align: center;
font-size: 1.8rem;
font-weight: 800;
color: #e0e0e6;
margin-bottom: 15px;
}
/* Status Panel Styling */
.status-panel, .info-panel {
background: rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
}
.status-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.9rem;
}
.status-item:last-child {
margin-bottom: 0; /* No margin for the last item */
}
/* Controls Button Styling */
.controls {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
.btn {
flex: 1;
padding: 10px 15px; /* Slightly larger padding for better touch targets */
border: none;
border-radius: 8px; /* More rounded corners */
color: white;
font-weight: bold;
font-size: 0.9rem; /* Slightly larger font */
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease; /* Add box-shadow transition */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.btn:hover {
transform: translateY(-2px); /* More pronounced lift effect */
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
.btn-primary { background: #4CAF50; } /* Green */
.btn-secondary { background: #2196F3; } /* Blue */
.btn-tertiary { background: #9C27B0; } /* Purple */
.btn-special { background: #FF6B35; } /* Orange */
/* Canvas Container */
.canvas-container {
flex: 1;
background: #000;
border-radius: 8px;
border: 2px solid rgba(255, 107, 53, 0.5);
overflow: hidden;
position: relative;
display: flex; /* Use flex to center canvas content if needed */
align-items: center;
justify-content: center;
min-height: 300px; /* Ensure a minimum height for the canvas area */
}
canvas {
width: 100%;
height: 100%;
display: block;
cursor: crosshair;
background-color: #f8f8f8; /* Ensure canvas background is light for ink effect */
}
.overlay {
position: absolute;
top: 10px; /* Adjusted position */
left: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 6px 10px; /* Slightly larger padding */
border-radius: 5px; /* More rounded */
font-size: 0.8rem;
color: #fff;
}
/* AI Area Styling */
.ai-area {
flex: 4; /* Takes more space in the right panel */
background: rgba(75, 255, 181, 0.1);
border-radius: 8px;
padding: 15px; /* Increased padding */
border: 2px solid rgba(75, 255, 181, 0.3);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 350px; /* Ensure it's tall enough */
position: relative; /* For loading overlay */
}
.ai-area h3 {
color: #4bffb5;
margin-bottom: 10px;
font-size: 1.2rem; /* Larger font */
}
.ai-area img {
width: 100%;
height: 100%;
object-fit: contain; /* Use contain to fit the image without cropping */
border-radius: 8px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
display: none; /* Hidden by default */
}
#aiStatus {
font-size: 1.1rem; /* Larger font */
color: #4bffb5;
text-align: center;
padding: 20px;
position: absolute; /* Center within ai-area */
background: rgba(0, 0, 0, 0.7);
border-radius: 8px;
width: calc(100% - 30px); /* Adjust width to account for padding */
height: calc(100% - 30px); /* Adjust height */
display: flex;
align-items: center;
justify-content: center;
top: 15px;
left: 15px;
}
/* Info Panel within Right Panel */
.info-panel h4 {
color: #b0e0e6;
margin-bottom: 8px;
font-size: 1.05rem;
}
.prompt-display {
background: rgba(0, 0, 0, 0.4);
padding: 10px;
border-radius: 6px;
font-size: 0.85rem;
word-break: break-all;
min-height: 60px; /* Ensure visibility for longer prompts */
display: flex;
align-items: center;
}
/* 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.3);
}
.message.show {
transform: translateX(0);
opacity: 1;
}
.message.success { background: #4CAF50; }
.message.error { background: #f44336; }
.message.info { background: #2196F3; }
/* Loading Spinner */
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #4bffb5;
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); /* Full width minus margin */
margin-bottom: 16px;
}
.controls {
flex-wrap: wrap; /* Allow buttons to wrap */
}
.btn {
flex: none;
width: 48%; /* Two buttons per row */
}
.ai-area {
min-height: 250px; /* Adjust min-height for smaller screens */
}
}
</style>
</head>
<body>
<div class="container">
<!-- Left Panel: Canvas and Controls -->
<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="generateAIImage()">生成</button>
</div>
<div class="canvas-container">
<canvas id="mainCanvas"></canvas>
<div class="overlay">水墨投影 - 移動滑鼠體驗</div>
</div>
</div>
<!-- Right Panel: AI Generation -->
<div class="right-panel">
<h1 class="title">AI 生成</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">
點擊「生成」按鈕以產生AI圖像...
</div>
</div>
<div class="ai-area">
<h3>🖼️ 生成結果</h3>
<div id="aiStatus">
<div class="spinner" style="display:none;"></div>
點擊生成按鈕開始創作
</div>
<img id="aiImage" alt="Generated AI Image" style="display: none;">
</div>
</div>
</div>
<div class="message" id="messageBox"></div>
<script>
console.log('🎨 啟動水墨系統...');
// Global variables for canvas and system state
let canvas, ctx;
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();
/**
* Initializes the main canvas and sets up event listeners.
* This function is called once the DOM is fully loaded.
*/
function init() {
console.log('初始化系統...');
canvas = document.getElementById('mainCanvas');
if (!canvas) {
console.error('找不到canvas元素');
return;
}
ctx = canvas.getContext('2d');
setupCanvas();
setupEvents();
createNodes(); // Initialize nodes for the ink effect
showMessage('系統初始化完成!', 'success');
console.log('✅ 初始化完成');
}
/**
* Sets the canvas dimensions to match its parent container
* and initializes mouse coordinates.
*/
function setupCanvas() {
const container = canvas.parentElement;
// Get the bounding rectangle of the parent to set canvas size
const rect = container.getBoundingClientRect();
// Set canvas dimensions based on container, ensuring minimums
canvas.width = Math.max(400, rect.width - 10);
canvas.height = Math.max(300, rect.height - 10);
// Center mouse initially
mouseX = canvas.width / 2;
mouseY = canvas.height / 2;
console.log(`畫布: ${canvas.width}x${canvas.height}`);
// Draw initial message on canvas
ctx.fillStyle = '#f5f5f5'; // Light background for ink effect
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#333';
ctx.font = '16px Inter';
ctx.textAlign = 'center';
ctx.fillText('系統準備就緒', canvas.width/2, canvas.height/2);
}
/**
* Sets up event listeners for mouse movement and clicks on the canvas.
*/
function setupEvents() {
// Mouse movement handler
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
// Calculate scale factors for accurate mouse coordinates on high-DPI screens
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;
// Calculate mouse speed for dynamic ripple generation
mouseSpeed = Math.sqrt((newX - mouseX)**2 + (newY - mouseY)**2);
mouseX = newX;
mouseY = newY;
// Update UI display for mouse speed
document.getElementById('mouseSpeed').textContent = Math.round(mouseSpeed);
// Create ripples based on mouse speed
// Only create a new ripple if speed is above a threshold and ripple count is not too high
if (mouseSpeed > 3 && ripples.length < 20) { // Increased ripple limit for more dynamism
createRipple(mouseX, mouseY, mouseSpeed);
}
});
// Click handler to create a strong ripple and trigger image generation
canvas.addEventListener('click', () => {
createRipple(mouseX, mouseY, 20); // Stronger ripple on click
generateAIImage(); // Trigger AI image generation on click
});
// Handle window resizing to keep canvas responsive
window.addEventListener('resize', setupCanvas);
console.log('事件設置完成');
}
/**
* Populates the `nodes` array, which forms the basis of the ink effect.
* Nodes are distributed in a somewhat organic pattern.
*/
function createNodes() {
nodes = [];
const nodeCount = 40; // Increased node count for denser ink effect
for (let i = 0; i < nodeCount; i++) {
const angle = (i / nodeCount) * Math.PI * 4; // Multiple rotations for spread
const radius = Math.random() * Math.min(canvas.width, canvas.height) * 0.3; // Wider distribution
// Randomize initial positions slightly
const x = canvas.width/2 + Math.cos(angle) * radius + (Math.random() - 0.5) * 100;
const y = canvas.height/2 + Math.sin(angle) * radius + (Math.random() - 0.5) * 100;
nodes.push({
x: Math.max(10, Math.min(canvas.width - 10, x)), // Keep nodes within canvas bounds
y: Math.max(10, Math.min(canvas.height - 10, y)),
originalX: x, // Store original position for spring back effect
originalY: y,
vx: 0, // Velocity X
vy: 0, // Velocity Y
phase: Math.random() * Math.PI * 2 // Random phase for independent motion
});
}
console.log(`創建了 ${nodes.length} 個節點`);
}
/**
* Adds a new ripple effect to the `ripples` array.
* @param {number} x - X coordinate of the ripple center.
* @param {number} y - Y coordinate of the ripple center.
* @param {number} intensity - The initial strength of the ripple.
*/
function createRipple(x, y, intensity) {
ripples.push({
x: x,
y: y,
radius: 0, // Current radius of the ripple
maxRadius: 30 + intensity * 2, // Max radius based on intensity
life: 1, // Life factor from 1 (full) to 0 (dead)
intensity: Math.min(intensity * 0.05, 1.0) // Clamped intensity for visual effect
});
document.getElementById('rippleCount').textContent = ripples.length;
}
/**
* Starts the ink simulation system.
*/
function startSystem() {
console.log('啟動系統');
isRunning = true;
isPaused = false;
document.getElementById('systemStatus').textContent = '運行中';
showMessage('水墨系統已啟動!', 'success');
animate(); // Start the animation loop
}
/**
* Resets the ink simulation to its initial state.
*/
function resetSystem() {
console.log('重置系統');
time = 0;
ripples = [];
createNodes(); // Recreate nodes
document.getElementById('rippleCount').textContent = '0';
showMessage('系統已重置', 'info');
// Clear the AI image area on reset
document.getElementById('aiImage').style.display = 'none';
document.getElementById('aiStatus').style.display = 'flex'; // Show default status
document.getElementById('aiStatus').innerHTML = '點擊生成按鈕開始創作';
document.getElementById('aiGenStatus').textContent = '待機';
document.getElementById('promptText').textContent = '點擊「生成」按鈕以產生AI圖像...';
}
/**
* Toggles the pause state of the simulation.
*/
function togglePause() {
isPaused = !isPaused;
const status = isPaused ? '暫停' : '繼續';
document.getElementById('systemStatus').textContent = status;
showMessage(`系統已${status}`, 'info');
if (!isPaused && isRunning) {
animate(); // Resume animation if not paused and running
}
}
/**
* Updates the physical properties of nodes and ripples.
* This includes basic wave motion, mouse interaction, and ripple forces.
*/
function updatePhysics() {
// Update node positions and velocities
nodes.forEach(node => {
// Basic fluid motion for nodes
const waveX = Math.sin(time * 1.5 + node.phase) * 2; // Increased amplitude and speed
const waveY = Math.cos(time * 1.2 + node.phase) * 2;
const targetX = node.originalX + waveX;
const targetY = node.originalY + waveY;
// Spring-like force pulling nodes back to target position
node.vx += (targetX - node.x) * 0.02;
node.vy += (targetY - node.y) * 0.02;
// Mouse influence on nodes - stronger effect
const distToMouse = Math.sqrt((node.x - mouseX)**2 + (node.y - mouseY)**2);
if (distToMouse < 100) { // Larger influence radius
const influence = (1 - distToMouse / 100) * 0.5; // Stronger influence
const mouseAngle = Math.atan2(node.y - mouseY, node.x - mouseX);
node.vx += Math.cos(mouseAngle) * influence * mouseSpeed * 0.01; // Influence scales with mouse speed
node.vy += Math.sin(mouseAngle) * influence * mouseSpeed * 0.01;
}
// Ripple influence on nodes
ripples.forEach(ripple => {
const distToRipple = Math.sqrt((node.x - ripple.x)**2 + (node.y - ripple.y)**2);
// Check if node is near the ripple's expanding edge
if (Math.abs(distToRipple - ripple.radius) < 10 && distToRipple > 0) { // Wider active band
const rippleForce = ripple.intensity * ripple.life * 0.5; // Stronger ripple force
const rippleAngle = Math.atan2(node.y - ripple.y, node.x - ripple.x);
node.vx += Math.cos(rippleAngle) * rippleForce;
node.vy += Math.sin(rippleAngle) * rippleForce;
}
});
// Apply friction and update position
node.vx *= 0.88; // More damping
node.vy *= 0.88;
node.x += node.vx;
node.y += node.vy;
// Boundary collision (simple bounce)
if (node.x < 0 || node.x > canvas.width) node.vx *= -0.8;
if (node.y < 0 || node.y > canvas.height) node.vy *= -0.8;
});
// Update ripples: expand and decay
ripples = ripples.filter(ripple => {
ripple.radius += 2.0; // Faster expansion
ripple.life -= 0.03; // Faster decay
return ripple.life > 0 && ripple.radius < ripple.maxRadius;
});
document.getElementById('rippleCount').textContent = ripples.length;
}
/**
* Renders the ink effect on the canvas.
*/
function render() {
if (!ctx) return;
// Clear canvas with a light background for ink effect
ctx.fillStyle = '#f8f8f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Apply blend mode for ink-like transparency
ctx.globalCompositeOperation = 'multiply'; // Makes darker colors multiply
// Render ripples
ripples.forEach(ripple => {
const opacity = ripple.life * ripple.intensity;
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.6})`; // Darker ink color
ctx.lineWidth = 2; // Thicker lines
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
ctx.stroke();
// Inner ripple for layered effect
if (ripple.radius > 10) {
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.3})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(ripple.x, ripple.y, ripple.radius * 0.7, 0, Math.PI * 2); // Inner ripple expands slower
ctx.stroke();
}
});
// Render node connections (lines)
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);
if (distance < 120) { // Larger connection distance
const opacity = (1 - distance / 120) * 0.4; // More opaque for closer nodes
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity})`;
ctx.lineWidth = 0.8; // Thinner lines for connections
ctx.beginPath();
ctx.moveTo(nodeA.x, nodeA.y);
ctx.lineTo(nodeB.x, nodeB.y);
ctx.stroke();
}
}
}
// Render nodes
nodes.forEach(node => {
const opacity = 0.5 + Math.sin(time * 2 + node.phase) * 0.2; // Pulsating opacity
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
ctx.beginPath();
ctx.arc(node.x, node.y, 2, 0, Math.PI * 2); // Slightly larger nodes
ctx.fill();
});
// Render mouse indicator
ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
ctx.beginPath();
ctx.arc(mouseX, mouseY, 5, 0, Math.PI * 2); // Slightly larger mouse indicator
ctx.fill();
// Mouse pulse effect
const pulseSize = 7 + Math.sin(time * 6) * 3; // Faster and more pronounced pulse
ctx.strokeStyle = 'rgba(0, 0, 0, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(mouseX, mouseY, pulseSize, 0, Math.PI * 2);
ctx.stroke();
// Reset blend mode and filters
ctx.filter = 'none';
ctx.globalCompositeOperation = 'source-over';
}
/**
* The main animation loop.
*/
function animate() {
if (!isRunning) return;
if (!isPaused) {
time += 0.02; // Advance time for physics
updatePhysics(); // Update node and ripple physics
render(); // Render the canvas
// Calculate and display FPS
frameCount++;
const now = Date.now();
if (now - lastFpsTime >= 1000) {
document.getElementById('fpsDisplay').textContent = frameCount;
frameCount = 0;
lastFpsTime = now;
}
}
requestAnimationFrame(animate); // Request next frame
}
/**
* Dynamically generates a prompt for the AI image based on the current canvas state.
* The prompt is tailored for a more "programmatic" or "digital" abstract art style.
* @returns {string} The generated prompt.
*/
function generatePromptFromCanvasState() {
let prompt = "An abstract digital art piece, visualizing interconnected data streams,";
// Describe ripples as wave-like patterns or energy pulses
if (ripples.length > 8) { // More intense ripple effect
prompt += " with complex, expanding energy rings creating intricate network patterns,";
} else if (ripples.length > 3) {
prompt += " with subtle, fluid wave-like disturbances,";
} else if (ripples.length > 0) {
prompt += " featuring soft, concentric pulses,";
}
// Describe mouse speed as density or intensity of lines/connections
if (mouseSpeed > 70) { // Very fast mouse implies high energy, dense connections
prompt += " bursting with dense, energetic nodes and thick, dynamic connections,";
} else if (mouseSpeed > 30) { // Moderate speed implies flowing, active lines
prompt += " showcasing active, flowing lines and pulsating nodes,";
} else { // Slow speed implies subtle, minimalist lines
prompt += " with delicate, subtle linear pathways and softly glowing points,";
}
prompt += " minimalist aesthetic, monochromatic with dark grey and black lines on a very light, almost white background, clean abstract forms, high detail, algorithmic art, generative design, flowing liquid metal patterns.";
return prompt;
}
/**
* Calls the Gemini Image Generation API to create an AI image.
* The prompt is dynamically generated based on the current canvas state.
*/
async function generateAIImage() {
console.log('正在生成AI圖像...');
const aiImageElement = document.getElementById('aiImage');
const aiStatusElement = document.getElementById('aiStatus');
const aiGenStatusDisplay = document.getElementById('aiGenStatus');
const promptTextDisplay = document.getElementById('promptText');
const spinnerElement = aiStatusElement.querySelector('.spinner');
// Show loading state
aiGenStatusDisplay.textContent = '生成中...';
aiImageElement.style.display = 'none';
aiStatusElement.style.display = 'flex'; // Show loading text and spinner
aiStatusElement.innerHTML = `<div class="spinner"></div><p>正在生成圖像,請稍候...</p>`;
spinnerElement.style.display = 'block'; // Ensure spinner is visible
const prompt = generatePromptFromCanvasState();
promptTextDisplay.textContent = prompt; // Display the prompt being sent
try {
// Gemini API call structure
const payload = {
instances: { prompt: prompt },
parameters: { "sampleCount": 1 }
};
// __app_id and __api_key are globally available variables in the Canvas environment
const apiKey = ""; // Leave empty, Canvas will inject it
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/imagen-3.0-generate-002:predict?key=${apiKey}`;
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`API Error: ${response.status} - ${errorData.error.message || 'Unknown error'}`);
}
const result = await response.json();
if (result.predictions && result.predictions.length > 0 && result.predictions[0].bytesBase64Encoded) {
const imageUrl = `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
aiImageElement.src = imageUrl;
aiImageElement.style.display = 'block'; // Show the image
aiStatusElement.style.display = 'none'; // Hide loading status
aiGenStatusDisplay.textContent = '完成';
showMessage('AI圖像生成完成!', 'success');
} else {
throw new Error('API response did not contain expected image data.');
}
} catch (error) {
console.error('生成AI圖像失敗:', error);
aiStatusElement.style.display = 'flex'; // Show error status
aiStatusElement.innerHTML = `生成失敗: ${error.message}`;
aiImageElement.style.display = 'none'; // Hide the image
aiGenStatusDisplay.textContent = '失敗';
showMessage(`生成失敗: ${error.message}`, 'error');
} finally {
if (spinnerElement) {
spinnerElement.style.display = 'none'; // Ensure spinner is hidden
}
}
}
/**
* Displays a temporary message box to the user.
* @param {string} text - The message text.
* @param {string} type - The type of message ('success', 'error', 'info').
*/
function showMessage(text, type) {
const msg = document.getElementById('messageBox');
msg.textContent = text;
msg.className = `message show ${type}`;
// Message fades out after 4 seconds
setTimeout(() => {
msg.classList.remove('show');
}, 4000);
}
// Initialize the system when the DOM content is fully loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM載入完成');
init();
});
</script>
</body>
</html>