videorayegan / index.html
Elias207's picture
Update index.html
49025e5 verified
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Video Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--app-font: 'Vazirmatn', sans-serif;
--app-bg: #F8F9FC;
--panel-bg: #FFFFFF;
--panel-border: #EAEFF7;
--input-bg: #F6F8FB;
--input-border: #E1E7EF;
--text-primary: #1A202C;
--text-secondary: #626F86;
--text-tertiary: #8A94A6;
--accent-primary: #4A6CFA;
--accent-primary-hover: #3553D6;
--accent-primary-glow: rgba(74, 108, 250, 0.25);
--accent-secondary: #0FD4A8;
--success-color: #38A169;
--danger-color: #e53e3e;
--danger-color-hover: #c53030;
--shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
--shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
--radius-card: 24px;
--radius-btn: 14px;
--radius-input: 12px;
--transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
/* -- متغیرهای جدید برای استایل خطای GPU -- */
--igadlm-alpha-color-primary: #5A67D8;
--igadlm-alpha-color-secondary: #805AD5;
--igadlm-alpha-color-primary-darker: #4C51BF;
--igadlm-alpha-color-secondary-darker: #6B46C1;
--igadlm-alpha-color-text-light: #4A5568;
--igadlm-alpha-color-warning-bg: #FFFBEB;
--igadlm-alpha-color-warning-border: #FBBF24;
--igadlm-alpha-color-warning-text: #B45309;
--igadlm-alpha-border-radius-md: 8px;
--igadlm-alpha-border-radius-lg: 12px;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes core-pulse { 0%, 100% { transform: scale(0.95); box-shadow: 0 0 25px var(--accent-primary-glow); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(74, 108, 250, 0.5); } }
@keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
@keyframes igadlmAlphaV2fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; }
.container { max-width: 820px; width: 100%; }
header { position: relative; text-align: center; margin-bottom: 2.5rem; padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards; overflow: hidden; }
#neural-network-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.header-content { position: relative; z-index: 2; }
.ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
.orb-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(74, 108, 250, 0.15) 0%, transparent 70%), linear-gradient(45deg, rgba(15, 212, 168, 0.05), rgba(74, 108, 250, 0.05)); border-radius: 50%; animation: background-pan 12s ease-in-out infinite; background-size: 200% 200%; }
.orb-core { width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #394A7A 0%, #1A202C 70%); box-shadow: 0 0 25px var(--accent-primary-glow); display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; animation: core-pulse 4s ease-in-out infinite; border: 1px solid rgba(74, 108, 250, 0.3); }
.play-icon { width: 24px; height: 24px; color: var(--accent-secondary); filter: drop-shadow(0 0 8px rgba(15, 212, 168, 0.7)); transition: var(--transition-smooth); }
.orb-core:hover .play-icon { transform: scale(1.1); color: #fff; filter: drop-shadow(0 0 12px rgba(15, 212, 168, 1)); }
.orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; mix-blend-mode: screen; }
.orb-ring.one { width: 100px; height: 100px; margin-top: -50px; margin-left: -50px; border-width: 2px; border-top-color: var(--accent-primary); border-right-color: var(--accent-primary); animation: ring-rotate 8s linear infinite; filter: blur(1px); }
.orb-ring.two { width: 120px; height: 120px; margin-top: -60px; margin-left: -60px; border-width: 1px; border-bottom-color: var(--accent-secondary); animation: ring-rotate 10s linear infinite reverse; }
.orb-ring.three { width: 140px; height: 140px; margin-top: -70px; margin-left: -70px; border-width: 2px; border-left-color: rgba(255, 255, 255, 0.5); border-right-color: rgba(255, 255, 255, 0.5); animation: ring-rotate 12s linear infinite; filter: blur(1px); opacity: 0.5; }
h1 { font-size: 2.8rem; font-weight: 800; margin: 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
.subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; }
.instruction { font-size: 0.95rem; color: var(--text-tertiary); margin-top: 0.75rem; font-weight: 500; }
main { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards; }
.form-group { margin-bottom: 2.5rem; }
.form-group:last-child { margin-bottom: 0; }
.form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; }
.form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
#image-drop-zone { position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 2.5rem; text-align: center; cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; }
#image-drop-zone.drag-over, #image-drop-zone:hover:not(.has-image) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); }
#image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; }
.upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; }
#image-drop-zone p { margin: 0; color: var(--text-secondary); font-weight: 500; }
#imagePreview { display: none; width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; }
#image-drop-zone.has-image .upload-content { display: none; }
#image-drop-zone.has-image #imagePreview { display: block; }
textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
#generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px rgba(15, 212, 168, 0.25); margin-top: 1.5rem; }
#generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
#generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px rgba(15, 212, 168, 0.3); }
#generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
#result-container { min-height: 350px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
#result-container.active { border-style: solid; border-color: var(--panel-border); }
#statusSection, #outputSection, #criticalErrorSection { display: none; width: 100%; }
#statusSection.active, #outputSection.active, #criticalErrorSection.active { display: block; animation: fadeIn 0.5s; }
#statusMessages { max-height: 150px; overflow-y: auto; width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
.status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); }
.status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
.status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
.status-icon { width: 18px; height: 18px; }
#aiLoader { align-items: center; justify-content: center; }
.generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
@keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
.noise-layer, .sketch-layer, .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.noise-layer { background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; }
@keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
.sketch-layer { filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; }
@keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
.building-layer { filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; }
@keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } }
.pixel-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient(0deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px), repeating-linear-gradient(90deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px); opacity: 1; animation: dissolve-grid 7s infinite ease-in-out; }
@keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } }
.particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(56, 189, 248, 0.2) 0%, transparent 50%); animation: flow-particles 7s infinite cubic-bezier(0.4, 0, 0.6, 1); }
@keyframes flow-particles { 0% { transform: translate(0, 0) scale(1); opacity: 0.5; } 50% { transform: translate(10px, -15px) scale(1.05); opacity: 0.8; } 100% { transform: translate(0, 0) scale(1); opacity: 0.5; } }
.text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center;}
@keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
.progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.4s ease; }
#outputVideo { width: 100%; max-width: 500px; border-radius: var(--radius-input); margin: 1rem auto; display: block; background-color: #000; box-shadow: var(--shadow-md); }
#finalSeed { text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-top: 1rem; }
.video-controls { display: flex; justify-content: center; gap: 1rem; margin-top: 1.5rem; }
.video-button { padding: 0.7rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1rem; transition: var(--transition-smooth); display: flex; align-items: center; gap: 0.5rem; }
.video-button.primary { background-color: var(--accent-primary); color: white; }
.video-button.primary:hover { background-color: var(--accent-primary-hover); transform: translateY(-2px); }
.video-button:not(.primary) { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); }
.video-button:not(.primary):hover { background-color: var(--panel-border); color: var(--text-primary); }
.gpu-error-message-container { text-align: right; background-color: var(--igadlm-alpha-color-warning-bg); padding: 25px; border-radius: var(--igadlm-alpha-border-radius-lg); box-shadow: var(--shadow-lg); border: 2px solid var(--igadlm-alpha-color-warning-border); animation: igadlmAlphaV2fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) both; }
.gpu-error-message-container h2 { font-size: 1.2em; color: var(--igadlm-alpha-color-warning-text); margin-bottom: 15px; font-weight: 700; display: flex; align-items: center; border-bottom: 1px solid var(--igadlm-alpha-color-warning-border); padding-bottom: 12px; }
.gpu-error-message-container h2 .icon { margin-left: 10px; font-size: 1.3em; }
.gpu-error-message-container .error-content { font-size: 0.9rem; color: var(--igadlm-alpha-color-text-light); line-height: 1.7; word-break: break-word; white-space: pre-line; }
.gpu-error-message-container .error-content strong.error-message-title { font-weight: 600; color: var(--igadlm-alpha-color-warning-text); display: block; margin-bottom: 8px;}
.gpu-error-message-container .error-content ul { list-style-type: "▫️ "; padding-right: 20px; margin-top: 10px; }
.gpu-error-message-container .error-content li { margin-bottom: 6px; }
.gpu-error-actions { display: flex; gap: 15px; margin-top: 25px; }
.gpu-error-actions .action-button { padding: 12px 20px; border: none; border-radius: var(--igadlm-alpha-border-radius-md); font-size: 0.95em; font-weight: 600; cursor: pointer; font-family: inherit; width: 100%; box-shadow: var(--shadow-md); transition: var(--transition-smooth); text-transform: none; letter-spacing: normal; }
.gpu-error-actions .action-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: var(--shadow-lg); }
.gpu-error-actions .action-button.back-button { background-color: var(--panel-bg); color: var(--text-tertiary); border: 1px solid var(--panel-border); flex-grow: 0.45; }
.gpu-error-actions .action-button.back-button:hover:not(:disabled) { background-color: var(--input-bg); }
.gpu-error-actions .action-button.retry-button { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary) 0%, var(--igadlm-alpha-color-secondary) 100%); color: white; flex-grow: 1; }
.gpu-error-actions .action-button.retry-button:hover:not(:disabled) { background: linear-gradient(60deg, var(--igadlm-alpha-color-primary-darker) 0%, var(--igadlm-alpha-color-secondary-darker) 100%); }
@media (max-width: 768px) {
main { padding: 1.5rem; } h1 { font-size: 2.2rem; }
.generator-container { height: 250px; }
.text-overlay { font-size: 18px; }
}
</style>
</head>
<body>
<div class="container">
<header>
<canvas id="neural-network-canvas"></canvas>
<div class="header-content">
<div class="ai-orb-container">
<div class="orb-background"></div>
<div class="orb-core">
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5v14l11-7z"></path>
</svg>
</div>
<div class="orb-ring one"></div>
<div class="orb-ring two"></div>
<div class="orb-ring three"></div>
</div>
<h1>استودیو ویدیو هوش مصنوعی</h1>
<p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
<p class="instruction">ابتدا تصویر خود را با فلاکس پرو تولید کنید و بعد تصویر خود را اینجا آپلود کرده و ویدیوی خود را بسازید.</p>
</div>
</header>
<main>
<div class="form-group">
<div class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>
۱. تصویر خود را انتخاب کنید
</div>
<label id="image-drop-zone" for="imageFile">
<div class="upload-content">
<div class="upload-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg></div>
<p>فایل تصویر را اینجا بکشید یا برای انتخاب کلیک کنید</p>
</div>
<img id="imagePreview" src="" alt="Preview">
</label>
<input type="file" id="imageFile" accept="image/jpeg, image/png, image/webp" hidden>
</div>
<div class="form-group">
<label for="prompt" class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
۲. دستور ساخت ویدیو را بنویسید
</label>
<textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگ‌ها در باد می‌رقصند و نور خورشید از بین درختان می‌تابد"></textarea>
<button id="generateButton">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
<span>ساخت ویدیو جادویی</span>
</button>
</div>
<div class="form-group">
<div class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/><path d="M21 14l-1.5 3-3-1.5 3-3 1.5 3z"/><path d="M19.5 2.5l-3 1.5 1.5 3 3-1.5-1.5-3z"/></svg>
۳. نتیجه را ببینید
</div>
<div id="result-container">
<div id="statusSection">
<div id="statusMessages"></div>
<div id="aiLoader" style="display: none;">
<div class="generator-container">
<div class="noise-layer"></div>
<div class="sketch-layer"></div>
<div class="building-layer"></div>
<div class="pixel-grid"></div>
<div class="particles"></div>
<div class="text-overlay">در حال پردازش ویدیو...</div>
<div class="progress-bar"></div>
</div>
</div>
</div>
<div id="outputSection">
<video id="outputVideo" controls preload="metadata" playsinline></video>
<p id="finalSeed"></p>
<div class="video-controls">
<button id="btnDownloadVideo" class="video-button primary" style="display: none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
دانلود ویدیو
</button>
<button id="btnRestart" class="video-button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
ساخت ویدیو جدید
</button>
</div>
</div>
<div id="criticalErrorSection">
<!-- این بخش به صورت داینامیک توسط جاوااسکریپت پر می‌شود -->
</div>
</div>
</div>
</main>
</div>
<script>
// Main Application Logic
(function() {
// DOM Elements
const resultContainer = document.getElementById('result-container');
const imageFileInput = document.getElementById('imageFile');
const imagePreview = document.getElementById('imagePreview');
const imageDropZone = document.getElementById('image-drop-zone');
const promptInput = document.getElementById('prompt');
const generateButton = document.getElementById('generateButton');
const outputVideo = document.getElementById('outputVideo');
const finalSeedElement = document.getElementById('finalSeed');
const criticalErrorSection = document.getElementById('criticalErrorSection');
const statusSection = document.getElementById('statusSection');
const statusMessagesDiv = document.getElementById('statusMessages');
const aiLoader = document.getElementById('aiLoader');
const loaderProgressBar = document.querySelector('#aiLoader .progress-bar');
const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
const outputSection = document.getElementById('outputSection');
const btnRestart = document.getElementById('btnRestart');
const btnDownloadVideo = document.getElementById('btnDownloadVideo');
// Constants
const VIDEO_SPACE_URL_BASE = "https://lightricks-ltx-video-distilled.hf.space/gradio_api";
const TRANSLATOR_SPACE_URL_BASE = "https://hamed744-translate-tts-aloha.hf.space/gradio_api";
const DEFAULT_TTS_VOICE_TRANSLATOR = "انگلیسی (آمریکا) - جنی (زن)";
const FN_INDEX_TRANSLATE_SPEAK = 1;
const FN_INDEX_VIDEO_GENERATION = 5;
const DEFAULT_NEGATIVE_PROMPT = "worst quality, low quality, normal quality, inconsistent motion, blurry, jittery, distorted, watermark, text, signature, ugly, deformed";
const DEFAULT_PROMPT_TEXT = "";
const TRANSPARENT_PIXEL_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const DEFAULT_VIDEO_DURATION = 8.5;
const PREDEFINED_ASPECT_RATIOS = [{ name: "16:9", value: 16 / 9 }, { name: "1:1", value: 1 / 1 }, { name: "9:16", value: 9 / 16 }, { name: "4:3", value: 4 / 3 }, { name: "3:2", value: 3 / 2 }, { name: "21:9", value: 21 / 9 }, { name: "2:3", value: 2 / 3 }, { name: "4:5", value: 4 / 5 }];
// Variables
let currentVideoSessionHash = '';
let uploadedImageInfo = null;
let lastAttemptedVideoPayload = null;
let lastErrorWasGpuQuota = false;
let autoSelectedAspectRatio = PREDEFINED_ASPECT_RATIOS[0].name;
let originalImageWidth = null;
let originalImageHeight = null;
// Helper Functions
function generateRandomHash(length = 11) {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15).substring(0, length - 7);
}
// ===================================================================
// START: CORRECTED RETRY FUNCTION
// ===================================================================
async function retryLastAttempt() {
// Ensure we have the necessary data to retry
if (!lastAttemptedVideoPayload || !lastAttemptedVideoPayload.imageSourceFile || !lastAttemptedVideoPayload.persianPrompt) {
showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد. لطفاً از نو شروع کنید.");
return;
}
// 1. Reset the UI for a new attempt
hideCriticalError();
clearStatusMessages();
generateButton.disabled = true;
resultContainer.classList.add('active');
statusSection.classList.add('active');
aiLoader.style.display = 'block';
if(loaderProgressBar) loaderProgressBar.style.animation = '';
outputSection.classList.remove('active');
addStatusMessage('شروع تلاش مجدد...', 'info', true);
// 2. Reuse the stored image and prompt
const { persianPrompt, imageSourceFile, targetWidth, targetHeight } = lastAttemptedVideoPayload;
try {
// 3. Re-run the entire generation process from scratch
// This is crucial: we re-translate and re-upload to ensure everything is fresh and valid.
// Re-translate the prompt
const englishPromptForVideo = await translatePersianToEnglish(persianPrompt);
addStatusMessage(`ترجمه مجدد: ${englishPromptForVideo}`, 'info');
// Generate a NEW session hash for the new attempt
currentVideoSessionHash = generateRandomHash();
// Re-upload the original image file
const imageForVideoProcessing = await uploadImageToServerForVideoOriginal(imageSourceFile);
if (!imageForVideoProcessing) throw new Error("آماده‌سازی مجدد تصویر ناموفق بود.");
// 4. Submit the new, fresh request to the queue
const videoGenerationPayloadData = [englishPromptForVideo, DEFAULT_NEGATIVE_PROMPT, imageForVideoProcessing, null, targetHeight, targetWidth, "image-to-video", DEFAULT_VIDEO_DURATION, 9, Math.floor(Math.random() * (2 ** 32 - 1)), true, 1.0, true];
const videoGenerationPayload = { fn_index: FN_INDEX_VIDEO_GENERATION, data: videoGenerationPayloadData, session_hash: currentVideoSessionHash };
const joinResponse = await submitToVideoQueue(videoGenerationPayload);
if (joinResponse?.event_id) {
listenForVideoResults(joinResponse.event_id);
} else {
throw new Error(joinResponse?.error || 'ارسال مجدد درخواست به صف ناموفق بود.');
}
} catch (errorCaught) {
const isTranslationErr = errorCaught.message.toLowerCase().includes("مترجم");
showCriticalError(`خطا در تلاش مجدد: ${errorCaught.message}`, isTranslationErr);
}
}
// ===================================================================
// END: CORRECTED RETRY FUNCTION
// ===================================================================
function showCriticalError(message, isTranslationError = false) {
lastErrorWasGpuQuota = false;
const lowerCaseMessage = (message || "").toLowerCase();
if (lowerCaseMessage.includes("gpu") || lowerCaseMessage.includes("quota") || lowerCaseMessage.includes("exceeded") || lowerCaseMessage.includes("limit") || lowerCaseMessage.includes("capacity")) {
lastErrorWasGpuQuota = true;
const detailedGpuErrorHtml = `
<div class="gpu-error-message-container">
<h2><span class="icon">💡</span> راهنمای رفع محدودیت ویدیو</h2>
<div class="error-content">
<strong class="error-message-title">ارور محدودیت استفاده از GPU</strong>
<p>مدل‌های هوش مصنوعی برای اجرا به کارت‌های گرافیک بسیار قدرتمند نیاز دارند که این امر گاهی محدودیت‌هایی جی پی یو را در استفاده از آن‌ها ایجاد می‌کند. با این حال، خبر خوب این است که این محدودیت‌ها با استفاده از یک ترفند ساده قابل رفع هستند.</p>
<p><strong>برای رفع محدودیت هر بار اینگونه عمل کنید:</strong></p>
<ul>
<li>اگر از اینترنت سیم‌کارت استفاده می‌کنید، فیلترشکن خود را حتما خاموش کنید.</li>
<li>به مدت ۱۰ تا ۱۵ ثانیه، گوشی خود را در حالت هواپیما قرار دهید و سپس آن را غیرفعال کنید. این محدودیت با همین روش برداشته میشود</li>
<li>اگر از وای فای استفاده می‌کنید، مودم خود را یک بار خاموش و روشن کنید (بهتر است برای استفاده نامحدود از برنامه از اینترنت سیم کارت استفاده کنید).</li>
</ul>
<p>خلاصه: هربار این صفحه اومد اینگونه عمل کنید از اینترنت سیم کارت استفاده کنید، فیلتر شکن خاموش باشه، یک یا دوبار حالت هواپیما رو روشن و خاموش کنید. تلاش مجدداً بزنید این ارور بر طرف میشه، با این روش میشه بصورت نامحدود با این برنامه تصاویر ساخت☘️</p>
</div>
<div class="gpu-error-actions">
<button class="action-button back-button" id="gpuErrorGoBackBtn">بازگشت</button>
<button class="action-button retry-button" id="gpuErrorRetryBtn">تلاش مجدد</button>
</div>
</div>`;
criticalErrorSection.innerHTML = detailedGpuErrorHtml;
document.getElementById('gpuErrorGoBackBtn').addEventListener('click', hideCriticalError);
document.getElementById('gpuErrorRetryBtn').addEventListener('click', retryLastAttempt);
} else {
criticalErrorSection.innerHTML = `
<div id="criticalErrorMessage" style="color: var(--danger-color); font-weight: 500; margin-bottom: 1.5rem; line-height: 1.6;"><p>${message}</p></div>
<div class="video-controls">
<button id="simpleErrorGoBackBtn" class="video-button">بازگشت</button>
<button id="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button>
</div>`;
document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError);
document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt);
}
resultContainer.classList.add('active');
criticalErrorSection.classList.add('active');
statusSection.classList.remove('active');
outputSection.classList.remove('active');
aiLoader.style.display = 'none';
generateButton.disabled = false;
}
function hideCriticalError() {
criticalErrorSection.classList.remove('active');
criticalErrorSection.innerHTML = '';
}
function setAiLoaderText(text) {
if (loaderTextOverlay) loaderTextOverlay.textContent = text;
}
function addStatusMessage(message, type = 'info', isPersistent = false) {
if (criticalErrorSection.classList.contains('active') && type === 'error') {
return;
}
if (type === 'error' && !isPersistent) isPersistent = true;
resultContainer.classList.add('active');
statusSection.classList.add('active');
const messageDiv = document.createElement('div');
messageDiv.className = `status-message ${type}`;
const iconSvg = type === 'success'
? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>'
: type === 'error'
? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>'
: '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`;
statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild);
statusMessagesDiv.scrollTop = 0;
}
function clearStatusMessages() {
statusMessagesDiv.innerHTML = '';
if(loaderProgressBar) loaderProgressBar.style.width = '0%';
aiLoader.style.display = 'none';
if (outputVideo.src && outputVideo.src.startsWith('blob:')) {
URL.revokeObjectURL(outputVideo.src);
}
outputVideo.src = '';
finalSeedElement.textContent = '';
btnDownloadVideo.style.display = 'none';
}
function autoSelectAspectRatioBasedOnImage(imageActualRatio) {
let closestRatio = PREDEFINED_ASPECT_RATIOS[0];
let minDiff = Math.abs(imageActualRatio - closestRatio.value);
for (let i = 1; i < PREDEFINED_ASPECT_RATIOS.length; i++) {
const diff = Math.abs(imageActualRatio - PREDEFINED_ASPECT_RATIOS[i].value);
if (diff < minDiff) {
minDiff = diff;
closestRatio = PREDEFINED_ASPECT_RATIOS[i];
}
}
autoSelectedAspectRatio = closestRatio.name;
}
function initializeForm() {
hideCriticalError();
resultContainer.classList.remove('active');
statusSection.classList.remove('active');
outputSection.classList.remove('active');
clearStatusMessages();
lastAttemptedVideoPayload = null;
lastErrorWasGpuQuota = false;
imageFileInput.value = '';
imagePreview.src = TRANSPARENT_PIXEL_SRC;
imageDropZone.classList.remove('has-image');
originalImageWidth = null;
originalImageHeight = null;
promptInput.value = DEFAULT_PROMPT_TEXT;
generateButton.disabled = false;
uploadedImageInfo = null;
autoSelectedAspectRatio = PREDEFINED_ASPECT_RATIOS[0].name;
}
async function translatePersianToEnglish(persianText) {
let currentTranslatorSessionHash = generateRandomHash();
const payload = {
data: [persianText, DEFAULT_TTS_VOICE_TRANSLATOR, 0, 0, 0],
event_data: null,
fn_index: FN_INDEX_TRANSLATE_SPEAK,
session_hash: currentTranslatorSessionHash
};
if (aiLoader.style.display !== 'none') {
setAiLoaderText("در حال ترجمه دستور شما...");
}
try {
const joinResponse = await fetch(`${TRANSLATOR_SPACE_URL_BASE}/queue/join?`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!joinResponse.ok) throw new Error(`خطای اتصال به سرور مترجم: ${joinResponse.status}`);
const joinData = await joinResponse.json();
if (!joinData.event_id) throw new Error("پاسخ نامعتبر از سرور مترجم.");
return new Promise((resolve, reject) => {
const eventSource = new EventSource(`${TRANSLATOR_SPACE_URL_BASE}/queue/data?session_hash=${currentTranslatorSessionHash}`);
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.msg === "process_completed") {
eventSource.close();
if (data.success && data.output?.data?.[0]) {
resolve(data.output.data[0]);
} else {
reject(new Error(data.output?.error || "ترجمه ناموفق بود."));
}
} else if (data.msg === "process_starts") {
if (aiLoader.style.display !== 'none') setAiLoaderText("ترجمه در سرور آغاز شد...");
}
};
eventSource.onerror = (err) => {
eventSource.close();
reject(new Error("خطا در ارتباط با سرور مترجم."));
};
});
} catch (error) {
console.error("خطای کلی در ترجمه:", error);
throw error;
}
}
async function uploadImageToServerForVideoOriginal(file) {
addStatusMessage('آماده‌سازی تصویر برای آپلود...', 'info', true);
setAiLoaderText("در حال آماده سازی تصویر...");
aiLoader.style.display = 'block';
const uploadId = generateRandomHash(12);
const formData = new FormData();
formData.append('files', file);
try {
const response = await fetch(`${VIDEO_SPACE_URL_BASE}/upload?upload_id=${uploadId}`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error(`خطا در آپلود تصویر (${response.status})`);
const result = await response.json();
if (result?.[0]) {
addStatusMessage('تصویر با موفقیت آپلود شد.', 'info');
return {
path: result[0],
url: `${VIDEO_SPACE_URL_BASE}/file=${result[0]}`,
orig_name: file.name,
size: file.size,
mime_type: file.type || 'application/octet-stream',
meta: { "_type": "gradio.FileData" }
};
}
throw new Error('پاسخ آپلود تصویر نامعتبر است.');
} catch (error) {
console.error("Upload original image error:", error);
throw error;
}
}
async function submitToVideoQueue(payload) {
addStatusMessage('ارسال درخواست ساخت ویدیو...', 'info', true);
setAiLoaderText("در حال ارسال درخواست ساخت ویدیو...");
aiLoader.style.display = 'block';
try {
const response = await fetch(`${VIDEO_SPACE_URL_BASE}/queue/join`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(`خطا در ارسال به صف ویدیو (${response.status})`);
return await response.json();
} catch (error) {
console.error("Submit to video queue error:", error);
throw error;
}
}
function listenForVideoResults(eventId) {
addStatusMessage('در حال ساخت ویدیو...', 'info', true);
setAiLoaderText("هوش مصنوعی در حال پردازش...");
aiLoader.style.display = 'block';
hideCriticalError();
statusSection.classList.add('active');
outputSection.classList.remove('active');
const eventSource = new EventSource(`${VIDEO_SPACE_URL_BASE}/queue/data?session_hash=${currentVideoSessionHash}`);
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.event_id !== eventId && !["progress", "process_starts", "process_completed", "queue_full"].includes(data.msg)) return;
switch (data.msg) {
case "process_starts":
addStatusMessage('پردازش در سرور آغاز شد.', 'info');
if(loaderProgressBar) loaderProgressBar.style.width = '0%';
break;
case "progress":
if (data.progress_data?.[0]) {
const progress = Math.round((data.progress_data[0].progress || 0) * 100);
if(loaderProgressBar) {
loaderProgressBar.style.animation = 'none';
loaderProgressBar.style.width = `${progress}%`;
}
}
break;
case "process_completed":
eventSource.close();
aiLoader.style.display = 'none';
if(loaderProgressBar) loaderProgressBar.style.width = '100%';
if (data.success && data.output?.data?.[0]?.video?.url) {
addStatusMessage('ویدیو آماده شد! 🎉', 'success', true);
lastErrorWasGpuQuota = false;
const ltxVideoUrl = data.output.data[0].video.url;
setTimeout(() => {
statusSection.classList.remove('active');
outputSection.classList.add('active');
outputVideo.src = ltxVideoUrl;
outputVideo.load();
finalSeedElement.textContent = data.output.data[1] ? `Seed: ${data.output.data[1]}` : '';
prepareCustomDownloadLink(ltxVideoUrl);
}, 300);
} else {
showCriticalError(data.output?.error || 'تولید ویدیو ناموفق بود.');
}
generateButton.disabled = false;
break;
case "queue_full":
eventSource.close();
showCriticalError('ظرفیت سرور تکمیل است.');
break;
}
};
eventSource.onerror = () => {
eventSource.close();
showCriticalError('خطا در ارتباط با سرور.');
};
}
function prepareCustomDownloadLink(ltxVideoUrl) {
btnDownloadVideo.style.display = 'flex';
btnDownloadVideo.onclick = () => {
if (window.self !== window.top) {
const message = {
type: 'DOWNLOAD_REQUEST',
url: ltxVideoUrl
};
window.parent.postMessage(message, '*');
} else {
console.warn("در iframe اجرا نشده است. لینک مستقیم باز می‌شود.");
window.open(ltxVideoUrl, '_blank');
}
};
}
// Event Listeners
document.addEventListener('DOMContentLoaded', initializeForm);
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { imageDropZone.addEventListener(eventName, ev => { ev.preventDefault(); ev.stopPropagation(); }); });
['dragenter', 'dragover'].forEach(eventName => { imageDropZone.addEventListener(eventName, () => { if (!imageDropZone.classList.contains('has-image')) { imageDropZone.classList.add('drag-over'); } }); });
['dragleave', 'drop'].forEach(eventName => { imageDropZone.addEventListener(eventName, () => { imageDropZone.classList.remove('drag-over'); }); });
imageDropZone.addEventListener('drop', e => {
if (!imageDropZone.classList.contains('has-image') && e.dataTransfer.files.length > 0) {
imageFileInput.files = e.dataTransfer.files;
const changeEvent = new Event('change', { bubbles: true });
imageFileInput.dispatchEvent(changeEvent);
}
});
btnRestart.addEventListener('click', initializeForm);
imageFileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
hideCriticalError();
if (file) {
if (!file.type.startsWith('image/')) {
addStatusMessage('فرمت نامعتبر', 'error', true);
imageFileInput.value = '';
imagePreview.src = TRANSPARENT_PIXEL_SRC;
imageDropZone.classList.remove('has-image');
uploadedImageInfo = null;
} else {
const reader = new FileReader();
reader.onload = (e) => {
imagePreview.src = e.target.result;
imageDropZone.classList.add('has-image');
const img = new Image();
img.onload = function() {
originalImageWidth = this.width;
originalImageHeight = this.height;
if (originalImageWidth && originalImageHeight) {
const imageActualRatio = originalImageWidth / originalImageHeight;
autoSelectAspectRatioBasedOnImage(imageActualRatio);
}
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
uploadedImageInfo = null;
}
} else {
imagePreview.src = TRANSPARENT_PIXEL_SRC;
imageDropZone.classList.remove('has-image');
autoSelectedAspectRatio = PREDEFINED_ASPECT_RATIOS[0].name;
}
});
generateButton.addEventListener('click', async () => {
const persianPromptForVideo = promptInput.value.trim();
if (!persianPromptForVideo) {
addStatusMessage('لطفاً توضیحات را وارد کنید.', 'error', true);
promptInput.focus();
return;
}
const imageFile = imageFileInput.files[0];
if (!imageFile) {
addStatusMessage('لطفاً تصویر انتخاب کنید.', 'error', true);
return;
}
clearStatusMessages();
hideCriticalError();
generateButton.disabled = true;
resultContainer.classList.add('active');
statusSection.classList.add('active');
aiLoader.style.display = 'block';
if(loaderProgressBar) loaderProgressBar.style.animation = '';
outputSection.classList.remove('active');
const [ratioW_str, ratioH_str] = autoSelectedAspectRatio.split(':');
const ratioW = parseFloat(ratioW_str);
const ratioH = parseFloat(ratioH_str);
const baseShortEdge = 512, baseLongEdge = 768, multiple = 16;
let targetWidth, targetHeight;
if (ratioW === ratioH) { targetWidth = baseShortEdge; targetHeight = baseShortEdge; }
else if (ratioW > ratioH) { targetWidth = baseLongEdge; targetHeight = Math.round((targetWidth * ratioH) / ratioW); }
else { targetHeight = baseLongEdge; targetWidth = Math.round((targetHeight * ratioW) / ratioH); }
targetWidth = Math.max(256, Math.min(1024, Math.round(targetWidth / multiple) * multiple));
targetHeight = Math.max(256, Math.min(1024, Math.round(targetHeight / multiple) * multiple));
// Save the original inputs for a potential retry
lastAttemptedVideoPayload = {
persianPrompt: persianPromptForVideo,
imageSourceFile: imageFile,
targetWidth,
targetHeight
};
try {
const englishPromptForVideo = await translatePersianToEnglish(persianPromptForVideo);
addStatusMessage(`ترجمه: ${englishPromptForVideo}`, 'info');
currentVideoSessionHash = generateRandomHash();
const imageForVideoProcessing = await uploadImageToServerForVideoOriginal(imageFile);
if (!imageForVideoProcessing) throw new Error("آماده‌سازی تصویر ناموفق.");
const videoGenerationPayloadData = [englishPromptForVideo, DEFAULT_NEGATIVE_PROMPT, imageForVideoProcessing, null, targetHeight, targetWidth, "image-to-video", DEFAULT_VIDEO_DURATION, 9, Math.floor(Math.random() * (2 ** 32 - 1)), true, 1.0, true];
const videoGenerationPayload = { fn_index: FN_INDEX_VIDEO_GENERATION, data: videoGenerationPayloadData, session_hash: currentVideoSessionHash };
const joinResponse = await submitToVideoQueue(videoGenerationPayload);
if (joinResponse?.event_id) {
listenForVideoResults(joinResponse.event_id);
} else {
throw new Error(joinResponse?.error || 'ارسال درخواست ناموفق.');
}
} catch (errorCaught) {
const isTranslationErr = errorCaught.message.toLowerCase().includes("مترجم");
showCriticalError(`خطا: ${errorCaught.message}`, isTranslationErr);
}
});
})();
</script>
<script>
// --- Header Animation Script ---
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('neural-network-canvas');
if (!canvas) return;
const header = canvas.parentElement;
const ctx = canvas.getContext('2d');
let particles = [];
const particleCount = 20; const maxDistance = 100;
const computedStyles = getComputedStyle(document.documentElement);
const particleColor = computedStyles.getPropertyValue('--accent-primary').trim();
const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim();
function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = 1.2; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = particleColor; ctx.fill(); } }
function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
function connectParticles() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 0.2; ctx.globalAlpha = 1 - distance / maxDistance; ctx.stroke(); } } } ctx.globalAlpha = 1; }
function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
});
</script>
</body>
</html>