| <!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"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" 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; |
| --accent-premium: #FFC107; |
| --accent-premium-glow: rgba(255, 193, 7, 0.3); |
| --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); |
| } |
| |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } |
| @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 badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } |
| @keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes fadeOutShrink { to { opacity: 0; transform: scale(0.8); height: 0; padding: 0; margin: 0; overflow: hidden; border: none; box-shadow: none; } } |
| |
| .fade-out-shrink { animation: fadeOutShrink 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; } |
| |
| @keyframes shake { |
| 0%, 100% { transform: translateX(0); } |
| 20%, 60% { transform: translateX(-6px); } |
| 40%, 80% { transform: translateX(6px); } |
| } |
| .shake-animation { |
| animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both !important; |
| background: linear-gradient(95deg, #e53e3e 0%, #c53030 100%) !important; |
| box-shadow: 0 6px 12px -3px rgba(229, 62, 62, 0.4) !important; |
| } |
| |
| @keyframes modal-fade-in { from { opacity: 0; } to { opacity: 1; } } |
| @keyframes modal-zoom-in { from { transform: translate(-50%, -50%) scale(0.9); } to { transform: translate(-50%, -50%) scale(1); } } |
| |
| .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(26, 32, 44, 0.5); backdrop-filter: blur(8px); z-index: 1000; animation: modal-fade-in 0.3s ease-out; } |
| .modal-overlay.active { display: block; } |
| .modal-content { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); width: 90%; max-width: 450px; text-align: center; animation: modal-zoom-in 0.3s cubic-bezier(0.4, 0, 0.2, 1); } |
| .modal-icon { width: 60px; height: 60px; margin: 0 auto 1.5rem; background: linear-gradient(45deg, var(--accent-primary-glow), rgba(15, 212, 168, 0.2)); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--accent-primary); } |
| .modal-icon svg { width: 32px; height: 32px; } |
| .modal-title { font-size: 1.3rem; font-weight: 700; color: var(--text-primary); margin: 0 0 1rem 0; } |
| .modal-text { font-size: 0.95rem; color: var(--text-secondary); line-height: 1.7; margin: 0 0 2rem 0; } |
| .modal-buttons { display: flex; gap: 1rem; width: 100%; } |
| .modal-button { flex: 1; padding: 0.8rem 1.5rem; background: var(--accent-primary); color: white; border: none; border-radius: var(--radius-btn); font-family: var(--app-font); font-weight: 600; font-size: 1rem; cursor: pointer; transition: var(--transition-smooth); } |
| .modal-button:hover { background: var(--accent-primary-hover); transform: translateY(-2px); } |
| .modal-button.secondary { background: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); } |
| .modal-button.secondary:hover { background-color: var(--panel-border); color: var(--text-primary); } |
| |
| #confirm-reset-modal .modal-icon, #delete-video-modal .modal-icon, #clear-history-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); } |
| #confirm-reset-modal .modal-button:not(.secondary), #delete-video-modal .modal-button:not(.secondary), #clear-history-modal .modal-button:not(.secondary) { background-color: var(--danger-color); } |
| #confirm-reset-modal .modal-button:not(.secondary):hover, #delete-video-modal .modal-button:not(.secondary):hover, #clear-history-modal .modal-button:not(.secondary):hover { background-color: var(--danger-color-hover); } |
| |
| #report-video-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); } |
| .report-options-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 1.5rem; } |
| .report-option-btn { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); padding: 8px 14px; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.2s ease; } |
| .report-option-btn:hover { background-color: var(--panel-border); color: var(--text-primary); } |
| #custom-report-section { display: none; width: 100%; } |
| #report-video-modal textarea { min-height: 80px; margin-top: 1rem; } |
| #emoji-container { margin-top: 10px; } .emoji { cursor: pointer; font-size: 24px; margin: 0 5px; } |
| #report-success-message { color: var(--success-color); font-weight: 600; display: none; margin-top: 1rem; } |
| |
| body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem 7.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; position: relative; } |
| .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; } |
| |
| #remove-image-btn { position: absolute; top: 12px; left: 12px; width: 32px; height: 32px; background-color: rgba(26, 32, 44, 0.7); color: white; border: none; border-radius: 50%; font-size: 22px; font-weight: bold; line-height: 32px; text-align: center; cursor: pointer; opacity: 0; visibility: hidden; transform: scale(0.8); transition: all 0.25s ease-out; z-index: 10; } |
| #image-drop-zone.has-image #remove-image-btn { opacity: 1; visibility: visible; transform: scale(1); } |
| #remove-image-btn:hover { background-color: var(--danger-color); transform: scale(1.1) rotate(90deg); } |
| |
| 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); } |
| textarea:disabled { background-color: var(--input-border); color: var(--text-tertiary); } |
| |
| .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; } |
| .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } |
| .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; display: flex; align-items: center; gap: 8px; } |
| .info-icon { width: 20px; height: 20px; background-color: var(--input-border); color: var(--text-tertiary); border-radius: 50%; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } |
| .info-icon:hover { background-color: var(--accent-primary); color: white; transform: scale(1.1); } |
| .slider-value { font-weight: 700; font-size: 1rem; color: var(--accent-primary); background-color: var(--input-bg); padding: 0.25rem 0.75rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 80px; text-align: center; } |
| .slider-wrapper { display: flex; align-items: center; gap: 1rem; } |
| .slider-icon { width: 22px; height: 22px; color: var(--text-tertiary); flex-shrink: 0; opacity: 0.8; } |
| input[type="range"] { -webkit-appearance: none; appearance: none; flex-grow: 1; width: 100%; height: 12px; background: var(--input-bg); border-radius: var(--radius-input); outline: none; cursor: pointer; direction: rtl; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: var(--transition-smooth); } |
| input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; margin-top: -6px; } |
| input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; } |
| input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); } |
| input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); } |
| input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); } |
| |
| #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); margin-top: 1rem; } |
| #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); } |
| #generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; } |
| |
| #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; } |
| #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); } |
| #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); } |
| #credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; } |
| #upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; } |
| |
| |
| #results-container { min-height: 180px; 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; } |
| #statusSection { display: none; width: 100%; } |
| #statusSection.active { display: block; animation: fadeIn 0.5s; } |
| #criticalErrorSection { display: none; width: 100%; } |
| #criticalErrorSection.active { display: flex; flex-direction: column; align-items: center; justify-content: center; 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; } |
| |
| |
| .history-section { |
| margin-top: 1rem; |
| animation: fadeIn 0.5s ease-out; |
| } |
| .history-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 2rem; |
| flex-wrap: wrap; |
| gap: 1rem; |
| } |
| .history-title { |
| font-size: 1.3rem; |
| font-weight: 800; |
| color: var(--text-primary); |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| } |
| .history-title i { |
| font-size: 1.5rem; |
| color: var(--accent-primary); |
| } |
| .btn-clear-history { |
| background: linear-gradient(135deg, rgba(229, 62, 62, 0.08) 0%, rgba(229, 62, 62, 0.03) 100%); |
| color: var(--danger-color); |
| border: 1px solid rgba(229, 62, 62, 0.2); |
| padding: 0.6rem 1.2rem; |
| border-radius: 12px; |
| font-family: var(--app-font); |
| font-weight: 600; |
| font-size: 0.95rem; |
| cursor: pointer; |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| display: inline-flex; |
| align-items: center; |
| gap: 0.5rem; |
| } |
| .btn-clear-history:hover { |
| background: var(--danger-color); |
| color: white; |
| box-shadow: 0 6px 15px rgba(229, 62, 62, 0.3); |
| transform: translateY(-2px); |
| border-color: var(--danger-color); |
| } |
| |
| .history-grid { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; } |
| |
| .video-result-card { |
| position: relative; |
| background: var(--panel-bg); |
| border-radius: 20px; |
| padding: 0.75rem 1rem !important; |
| border: 1px solid var(--panel-border); |
| box-shadow: var(--shadow-sm); |
| transition: var(--transition-smooth); |
| overflow: hidden; |
| display: flex; |
| flex-direction: column; |
| cursor: pointer; |
| } |
| .video-result-card:hover { |
| border-color: rgba(74, 108, 250, 0.2); |
| box-shadow: var(--shadow-md); |
| } |
| |
| .card-header-layout { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| width: 100%; |
| user-select: none; |
| } |
| .card-header-left { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| min-width: 0; |
| flex: 1; |
| } |
| |
| .card-thumbnail-container { |
| width: 48px; |
| height: 48px; |
| border-radius: 50%; |
| overflow: hidden; |
| position: relative; |
| border: 2px solid #fff; |
| box-shadow: var(--shadow-sm); |
| flex-shrink: 0; |
| cursor: zoom-in; |
| background-color: var(--input-bg); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| .card-thumbnail-container img { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| transition: transform 0.3s ease; |
| } |
| .card-thumbnail-container:hover img { |
| transform: scale(1.1); |
| } |
| .card-thumbnail-zoom-overlay { |
| position: absolute; |
| inset: 0; |
| background: rgba(0,0,0,0.4); |
| opacity: 0; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| font-size: 0.75rem; |
| transition: var(--transition-smooth); |
| } |
| .card-thumbnail-container:hover .card-thumbnail-zoom-overlay { |
| opacity: 1; |
| } |
| |
| .card-header-titles { |
| display: flex; |
| flex-direction: column; |
| gap: 2px; |
| min-width: 0; |
| } |
| .card-header-title-text { |
| font-size: 0.9rem; |
| font-weight: 800; |
| color: var(--text-primary); |
| line-height: 1.2; |
| } |
| .card-header-meta { |
| font-size: 0.75rem; |
| color: var(--text-tertiary); |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| flex-wrap: wrap; |
| } |
| |
| .card-status-badge { |
| font-size: 0.7rem; |
| font-weight: 700; |
| padding: 1px 6px; |
| border-radius: 6px; |
| display: inline-block; |
| } |
| .status-badge-completed { background-color: rgba(56, 161, 105, 0.08); color: var(--success-color); border: 1px solid rgba(56, 161, 105, 0.1); } |
| .status-badge-processing { background-color: rgba(217, 119, 6, 0.08); color: #d97706; border: 1px solid rgba(217, 119, 6, 0.1); } |
| .status-badge-failed { background-color: rgba(229, 62, 62, 0.08); color: var(--danger-color); border: 1px solid rgba(229, 62, 62, 0.1); } |
| |
| .card-header-actions { |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| flex-shrink: 0; |
| } |
| .chevron-arrow { |
| color: var(--text-tertiary); |
| font-size: 0.85rem; |
| transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| order: 1; |
| } |
| .video-result-card.expanded .chevron-arrow { |
| transform: rotate(-90deg); |
| color: var(--accent-primary); |
| } |
| |
| .card-delete-btn { |
| width: 32px; |
| height: 32px; |
| border-radius: 50%; |
| background-color: var(--input-bg); |
| border: 1px solid var(--input-border); |
| color: var(--text-secondary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: var(--transition-smooth); |
| order: 2; |
| } |
| .card-delete-btn:hover { |
| background-color: rgba(229, 62, 62, 0.08); |
| color: var(--danger-color); |
| border-color: rgba(229, 62, 62, 0.15); |
| } |
| |
| .card-expanded-body { |
| max-height: 0; |
| overflow: hidden; |
| opacity: 0; |
| transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; |
| } |
| .video-result-card.expanded .card-expanded-body { |
| max-height: 1500px; |
| opacity: 1; |
| margin-top: 1.25rem; |
| padding-top: 1.25rem; |
| border-top: 1px dashed var(--panel-border); |
| } |
| |
| .video-result-card video { |
| width: 100%; |
| max-width: 100%; |
| height: auto; |
| aspect-ratio: auto; |
| border-radius: 18px; |
| margin: 0 auto 1.25rem; |
| display: block; |
| background-color: #000; |
| box-shadow: var(--shadow-lg); |
| } |
| |
| .video-info-container { width: 100%; text-align: right; margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; } |
| .video-prompt-lbl { font-size: 0.95rem; font-weight: 600; color: var(--text-primary); line-height: 1.6; } |
| .video-meta-lbl { font-size: 0.8rem; color: var(--text-tertiary); display: flex; gap: 12px; align-items: center; } |
| |
| .video-controls { display: flex; justify-content: center; width: 100%; gap: 10px; } |
| |
| .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: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none;} |
| .video-button.primary { width: 100%; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); color: white; box-shadow: 0 4px 10px rgba(74, 108, 250, 0.2); } |
| .video-button.primary:hover:not(:disabled) { box-shadow: 0 6px 15px rgba(74, 108, 250, 0.4); transform: translateY(-2px); } |
| .video-button:not(.primary):hover:not(:disabled) { background-color: var(--panel-border); color: var(--text-primary); } |
| .video-button:disabled { cursor: not-allowed; opacity: 0.6; } |
| |
| #image-lightbox-modal { |
| display: none; |
| position: fixed; |
| top: 0; left: 0; width: 100%; height: 100%; |
| background-color: rgba(15, 23, 42, 0.9); |
| backdrop-filter: blur(10px); |
| z-index: 2000; |
| justify-content: center; |
| align-items: center; |
| } |
| #image-lightbox-modal.active { display: flex; } |
| .lightbox-img { |
| max-width: 90%; |
| max-height: 80%; |
| border-radius: 20px; |
| box-shadow: 0 10px 40px rgba(0,0,0,0.6); |
| object-fit: contain; |
| animation: zoomIn 0.35s cubic-bezier(0.4, 0, 0.2, 1); |
| border: 2px solid rgba(255, 255, 255, 0.1); |
| } |
| .lightbox-close { |
| position: absolute; |
| top: 24px; |
| right: 32px; |
| color: #fff; |
| font-size: 40px; |
| font-weight: bold; |
| cursor: pointer; |
| transition: var(--transition-smooth); |
| } |
| .lightbox-close:hover { color: var(--danger-color); transform: scale(1.1); } |
| @keyframes zoomIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } |
| |
| .processing-card-overlay { |
| position: relative; |
| width: 100%; |
| aspect-ratio: 16 / 9; |
| border-radius: 18px; |
| overflow: hidden; |
| background-color: #0d1117; |
| margin-bottom: 1rem; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| border: 1px solid var(--panel-border); |
| } |
| .processing-bg-image { |
| position: absolute; |
| top: 0; left: 0; width: 100%; height: 100%; |
| object-fit: cover; |
| opacity: 0.3; |
| filter: blur(4px); |
| } |
| .processing-card-content { |
| position: relative; |
| z-index: 5; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 12px; |
| width: 80%; |
| text-align: center; |
| } |
| .processing-spinner { |
| width: 32px; height: 32px; |
| border: 3px solid rgba(74, 108, 250, 0.2); |
| border-top-color: var(--accent-primary); |
| border-radius: 50%; |
| animation: ring-rotate 1s linear infinite; |
| } |
| .processing-progress-bg { |
| width: 100%; height: 6px; |
| background-color: rgba(255,255,255,0.1); |
| border-radius: 10px; |
| overflow: hidden; |
| } |
| .processing-progress-bar { |
| height: 100%; |
| width: 0%; |
| background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary)); |
| border-radius: 10px; |
| transition: width 0.5s ease-out; |
| } |
| .processing-status-lbl { |
| font-size: 0.85rem; |
| font-weight: 700; |
| color: #fff; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.8); |
| } |
| |
| .failed-card-overlay { |
| position: relative; |
| width: 100%; |
| aspect-ratio: 16 / 9; |
| border-radius: 18px; |
| overflow: hidden; |
| background-color: #1a0f0f; |
| margin-bottom: 1rem; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| } |
| .failed-card-content { |
| z-index: 5; |
| color: #feb2b2; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 10px; |
| padding: 1rem; |
| text-align: center; |
| } |
| .failed-card-content i { font-size: 2rem; } |
| .failed-card-content p { font-size: 0.8rem; margin: 0; line-height: 1.6; } |
| |
| .bottom-nav-container { |
| position: fixed; |
| bottom: 24px; |
| left: 0; |
| right: 0; |
| z-index: 999; |
| display: flex; |
| justify-content: center; |
| pointer-events: none; |
| padding: 0 1.5rem; |
| } |
| .bottom-nav-inner { |
| background: rgba(255, 255, 255, 0.95); |
| backdrop-filter: blur(15px); |
| border: 1px solid var(--panel-border); |
| border-radius: 40px; |
| padding: 6px; |
| display: flex; |
| gap: 8px; |
| box-shadow: 0 10px 40px -10px rgba(0,0,0,0.12); |
| pointer-events: auto; |
| width: 100%; |
| max-width: 360px; |
| height: 72px; |
| box-sizing: border-box; |
| align-items: center; |
| } |
| .nav-tab { |
| flex: 1; |
| background: transparent; |
| border: none; |
| height: 100%; |
| border-radius: 30px; |
| cursor: pointer; |
| font-family: var(--app-font); |
| font-weight: 700; |
| font-size: 0.85rem; |
| color: var(--text-secondary); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| gap: 4px; |
| transition: var(--transition-smooth); |
| position: relative; |
| outline: none; |
| } |
| .nav-tab.active { |
| color: var(--accent-primary); |
| background: linear-gradient(to top, rgba(74, 108, 250, 0.05), transparent); |
| } |
| .nav-tab.active i { |
| color: var(--accent-primary); |
| transform: translateY(-2px); |
| filter: drop-shadow(0 2px 8px rgba(74, 108, 250, 0.3)); |
| } |
| .nav-tab.active::after { |
| content: ''; |
| position: absolute; |
| bottom: 4px; |
| width: 5px; |
| height: 5px; |
| border-radius: 50%; |
| background-color: var(--accent-primary); |
| } |
| .nav-tab i { |
| font-size: 1.3rem; |
| transition: var(--transition-smooth); |
| } |
| |
| .nav-badge { |
| position: absolute; |
| top: 6px; |
| right: 28%; |
| background: var(--danger-color); |
| color: white; |
| font-size: 0.7rem; |
| font-weight: 800; |
| min-width: 16px; |
| height: 16px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| box-shadow: 0 0 10px rgba(229, 62, 62, 0.4); |
| border: 2px solid #fff; |
| animation: badge-fade-in 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| @keyframes nav-bounce { |
| 0%, 100% { transform: scale(1); } |
| 50% { transform: scale(1.2) translateY(-6px); } |
| } |
| .bounce-nav-tab i { |
| animation: nav-bounce 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| } |
| |
| .fly-orb { |
| position: fixed; |
| pointer-events: none; |
| z-index: 2000; |
| transition: all 0.85s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| display: none; |
| align-items: center; |
| justify-content: center; |
| width: 76px; |
| height: 76px; |
| border-radius: 22px; |
| background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); |
| box-shadow: 0 10px 35px rgba(74, 108, 250, 0.5); |
| color: white; |
| font-size: 1.8rem; |
| will-change: top, left, transform, opacity; |
| } |
| |
| #aiLoader { display: none; 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 { position: absolute; top: 0; left: 0; width: 100%; height: 100%; 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 { position: absolute; top: 0; left: 0; width: 100%; height: 100%; 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 { position: absolute; top: 0; left: 0; width: 100%; height: 100%; 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.3s linear; } |
| |
| .report-flag-icon { display: inline-block; width: 16px; height: 16px; margin: 0 8px; vertical-align: middle; cursor: pointer; transition: all 0.4s ease; color: var(--text-tertiary); } |
| .report-flag-icon:hover { color: var(--danger-color); } |
| |
| @media (max-width: 768px) { |
| main { padding: 3rem 1.5rem; } |
| h1 { font-size: 2.2rem; } |
| .generator-container { height: 250px; } |
| .text-overlay { font-size: 18px; } |
| .history-header { flex-direction: column; align-items: flex-start; } |
| .btn-clear-history { width: 100%; justify-content: center; } |
| |
| .history-section { |
| margin-left: -1.5rem; |
| margin-right: -1.5rem; |
| padding-left: 0.75rem; |
| padding-right: 0.75rem; |
| } |
| } |
| </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" id="main-subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p> |
| <p class="instruction" id="main-instruction">ابتدا تصویر خود را تولید کرده و اینجا آپلود کنید تا ویدیوی خود را بسازید.</p> |
| <div id="subscription-status-badge" style="display:inline-block;"></div> |
| </div> |
| </header> |
|
|
| <div id="fly-orb" class="fly-orb"> |
| <i class="fas fa-wand-magic-sparkles"></i> |
| </div> |
|
|
| <main> |
| <div id="tab-home-content"> |
| <div id="main-controls"> |
| <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=""> |
| <button id="remove-image-btn" type="button" title="شروع پروژه جدید (حذف تصویر)">×</button> |
| </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> |
| |
| <div class="slider-group"> |
| <div class="slider-label-container"> |
| <label for="durationSlider" class="slider-label" id="duration-label">مدت زمان<span class="info-icon" id="duration-info-icon">!</span></label> |
| <span id="durationValue" class="slider-value">4.0 ثانیه</span> |
| </div> |
| <div class="slider-wrapper"> |
| <svg class="slider-icon" 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 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg> |
| <input type="range" id="durationSlider" min="1.0" max="5.0" value="4.0" step="1.0"> |
| <svg class="slider-icon" 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"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> |
| </div> |
| </div> |
| |
| <div id="credit-info-section"></div> |
| <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button> |
| |
| <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 id="prompt-warning" style="color: var(--danger-color); font-size: 0.95rem; font-weight: 700; text-align: center; margin-top: 12px; display: none;">لطفاً توضیحات را وارد کنید</div> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="tab-history-content" style="display: none;"> |
| <div class="history-section"> |
| <div class="history-header"> |
| <div class="history-title"> |
| <i class="fas fa-history"></i> |
| لیست پروژههای ویدیویی شما |
| </div> |
| <button id="clear-history-btn" class="btn-clear-history"> |
| <i class="fas fa-trash-can"></i> |
| حذف کل تاریخچه |
| </button> |
| </div> |
| |
| <div id="history-grid" class="history-grid"></div> |
| </div> |
| </div> |
|
|
| <div id="processing-container" class="form-group" style="display: none; margin-top: 2.5rem;"> |
| <div id="results-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" id="tv-progress-bar"></div> |
| </div> |
| </div> |
| </div> |
| <div id="criticalErrorSection"></div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <div class="bottom-nav-container"> |
| <div class="bottom-nav-inner"> |
| <button id="tab-home-btn" class="nav-tab active"> |
| <i class="fas fa-home"></i> |
| <span>خانه</span> |
| </button> |
| <button id="tab-history-btn" class="nav-tab"> |
| <i class="fas fa-clock-rotate-left"></i> |
| <span>تاریخچه</span> |
| <span id="active-badge" class="nav-badge" style="display: none;">0</span> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="image-lightbox-modal"> |
| <span class="lightbox-close" id="close-lightbox-btn">×</span> |
| <img class="lightbox-img" id="lightbox-image-element" src="" alt="Input Image Preview"> |
| </div> |
|
|
| <div class="modal-overlay" id="limit-reached-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon" style="color: var(--accent-premium); background: rgba(255, 193, 7, 0.12)"> |
| <i class="fas fa-triangle-exclamation" style="font-size: 1.8rem;"></i> |
| </div> |
| <h3 class="modal-title">تکمیل ظرفیت پردازش همزمان</h3> |
| <p class="modal-text">شما در حال حاضر دارای ۵ پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.</p> |
| <div class="modal-buttons"> |
| <button class="modal-button" id="close-limit-modal">متوجه شدم</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="duration-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><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"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></div> |
| <h3 class="modal-title">راهنمای مدت زمان</h3> |
| <p class="modal-text">گاهی به دلیل شلوغی سرور، ساخت ویدیوهای طولانی با محدودیت مواجه میشود.<br><br>اگر با خطا مواجه شدید، لطفا مدت زمان را کاهش داده و دوباره امتحان کنید.</p> |
| <div class="modal-buttons"><button class="modal-button" id="close-duration-modal">متوجه شدم</button></div> |
| </div> |
| </div> |
| |
| <div class="modal-overlay" id="report-video-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><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="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg></div> |
| <h3 class="modal-title">گزارش ویدیو</h3> |
| <div id="report-form-content"> |
| <p class="modal-text">لطفا دلیل گزارش خود را انتخاب یا وارد کنید.</p> |
| <div class="report-options-container"> |
| <button class="report-option-btn" data-reason="محتوای نامناسب">محتوای نامناسب</button> |
| <button class="report-option-btn" data-reason="محتوای نادرست">محتوای نادرست</button> |
| <button class="report-option-btn" id="report-other-btn">دیگر</button> |
| </div> |
| <div id="custom-report-section"> |
| <textarea id="custom-report-text" placeholder="توضیحات بیشتر..."></textarea> |
| <div id="emoji-container"> |
| <span class="emoji">😊</span><span class="emoji">😡</span><span class="emoji">😢</span> |
| </div> |
| <div class="modal-buttons" style="margin-top: 1.5rem;"> |
| <button class="modal-button" id="send-report-btn">ارسال گزارش</button> |
| </div> |
| </div> |
| </div> |
| <div id="report-success-message"> |
| <p>گزارش شما با موفقیت ثبت شد. از همکاری شما متشکریم.</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="modal-overlay" id="confirm-reset-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></div> |
| <h3 class="modal-title">آیا مطمئن هستید؟</h3> |
| <p class="modal-text">با این کار تمام نتایج فعلی حذف شده و پروژه از ابتدا شروع میشود.</p> |
| <div class="modal-buttons"> |
| <button class="modal-button secondary" id="toast-cancel-btn">انصراف</button> |
| <button class="modal-button" id="toast-confirm-btn">تایید و حذف</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="delete-video-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></div> |
| <h3 class="modal-title">حذف ویدیو</h3> |
| <p class="modal-text">آیا از حذف این ویدیو مطمئن هستید؟ این عملیات قابل بازگشت نیست.</p> |
| <div class="modal-buttons"> |
| <button class="modal-button secondary" id="cancel-delete-btn">انصراف</button> |
| <button class="modal-button" id="confirm-delete-btn">تایید و حذف</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="clear-history-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path></svg></div> |
| <h3 class="modal-title">حذف کل تاریخچه</h3> |
| <p class="modal-text">آیا مطمئن هستید که میخواهید تمام ویدیوهای ذخیره شده را حذف کنید؟ این عمل غیرقابل بازگشت است.</p> |
| <div class="modal-buttons"> |
| <button class="modal-button secondary" id="cancel-clear-history-btn">انصراف</button> |
| <button class="modal-button" id="confirm-clear-history-btn">تایید و حذف</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const MAX_VIDEOS = 20; |
| |
| const projectDB = { |
| db: null, |
| init() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open('AIStudioDB_LTX_V3', 2); |
| request.onupgradeneeded = event => { |
| const db = event.target.result; |
| if (!db.objectStoreNames.contains('projects')) { |
| db.createObjectStore('projects', { keyPath: 'id' }); |
| } |
| }; |
| request.onsuccess = event => { |
| this.db = event.target.result; |
| resolve(); |
| }; |
| request.onerror = event => { |
| console.error('IndexedDB error:', event.target.errorCode); |
| reject(event.target.errorCode); |
| }; |
| }); |
| }, |
| saveProject(project) { |
| return new Promise((resolve, reject) => { |
| const transaction = this.db.transaction(['projects'], 'readwrite'); |
| const store = transaction.objectStore('projects'); |
| const request = store.put(project); |
| request.onsuccess = () => resolve(); |
| request.onerror = e => reject(e.target.error); |
| }); |
| }, |
| getAllProjects() { |
| return new Promise((resolve, reject) => { |
| const transaction = this.db.transaction(['projects'], 'readonly'); |
| const store = transaction.objectStore('projects'); |
| const request = store.getAll(); |
| request.onsuccess = () => resolve(request.result || []); |
| request.onerror = e => reject(e.target.error); |
| }); |
| }, |
| deleteProject(id) { |
| return new Promise((resolve, reject) => { |
| const transaction = this.db.transaction(['projects'], 'readwrite'); |
| const store = transaction.objectStore('projects'); |
| const request = store.delete(id); |
| request.onsuccess = () => resolve(); |
| request.onerror = e => reject(e.target.error); |
| }); |
| }, |
| clearAllProjects() { |
| return new Promise((resolve, reject) => { |
| const transaction = this.db.transaction(['projects'], 'readwrite'); |
| const store = transaction.objectStore('projects'); |
| const request = store.clear(); |
| request.onsuccess = () => resolve(); |
| request.onerror = e => reject(e.target.error); |
| }); |
| } |
| }; |
| |
| let userSubscriptionStatus = 'free', userFingerprint = null, countdownInterval = null; const PREMIUM_PAGE_ID = '1149636'; |
| async function getBrowserFingerprint() { const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset(), "canvas-test"]; const fingerprintString = components.join('---'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i); hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); } |
| function isUserPaid(userObject) { return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID); } |
| function updateUIForSubscriptionStatus(status) { |
| userSubscriptionStatus = status; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); const subscriptionBadge = document.getElementById('subscription-status-badge'); |
| if (status === 'paid') { |
| subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; creditInfoDiv.style.display = 'none'; upgradeBtn.style.display = 'none'; genBtn.disabled = false; if(countdownInterval) clearInterval(countdownInterval); |
| } else { |
| subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; checkFreeUserCredit(); |
| } |
| subscriptionBadge.style.display = 'inline-block'; |
| } |
| async function checkFreeUserCredit() { |
| if (!userFingerprint) return; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); |
| if (userSubscriptionStatus !== 'paid') { creditInfoDiv.style.display = 'block'; } |
| |
| const lastGen = localStorage.getItem('ltx_last_gen_time'); |
| if (lastGen && userSubscriptionStatus !== 'paid') { |
| const lastGenTime = parseInt(lastGen, 10); |
| const now = Date.now(); |
| const oneDayMs = 24 * 60 * 60 * 1000; |
| const timeLeft = lastGenTime + oneDayMs - now; |
| |
| if (timeLeft > 0) { |
| genBtn.disabled = true; |
| upgradeBtn.style.display = 'block'; |
| startCountdown(Math.floor((lastGenTime + oneDayMs) / 1000)); |
| return; |
| } |
| } |
| |
| genBtn.disabled = false; |
| upgradeBtn.style.display = 'none'; |
| if(countdownInterval) clearInterval(countdownInterval); |
| creditInfoDiv.textContent = `شما ۱ اعتبار ساخت ویدیوی رایگان روزانه دارید.`; |
| } |
| function startCountdown(resetTimestamp) { |
| if (countdownInterval) clearInterval(countdownInterval); const creditInfoDiv = document.getElementById('credit-info-section'); |
| const updateTimer = () => { |
| const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000)); |
| if (timeLeft === 0) { clearInterval(countdownInterval); creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...'; setTimeout(checkFreeUserCredit, 2000); return; } |
| const d = Math.floor(timeLeft / 86400); const h = Math.floor((timeLeft % 86400) / 3600); const m = Math.floor((timeLeft % 3600) / 60); |
| creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقیمانده: ${d} روز و ${h} ساعت و ${m} دقیقه`; |
| }; |
| updateTimer(); countdownInterval = setInterval(updateTimer, 60000); |
| } |
| window.addEventListener('message', (event) => { |
| if (event.data?.type === 'USER_DATA_RESPONSE') { |
| if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; } |
| try { const userObject = JSON.parse(event.data.payload); updateUIForSubscriptionStatus(isUserPaid(userObject) ? 'paid' : 'free'); } |
| catch (e) { updateUIForSubscriptionStatus('free'); } |
| } |
| }); |
| document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); }); |
| |
| |
| const mainControls = document.getElementById('main-controls'); |
| const processingContainer = document.getElementById('processing-container'); |
| const historySection = document.getElementById('tab-history-content'); |
| const historyGrid = document.getElementById('history-grid'); |
| const imageFileInput = document.getElementById('imageFile'); |
| const imagePreview = document.getElementById('imagePreview'); |
| const imageDropZone = document.getElementById('image-drop-zone'); |
| const removeImageBtn = document.getElementById('remove-image-btn'); |
| const promptInput = document.getElementById('prompt'); |
| const generateButton = document.getElementById('generateButton'); |
| const generateButtonText = generateButton.querySelector('span'); |
| const criticalErrorSection = document.getElementById('criticalErrorSection'); |
| const statusSection = document.getElementById('statusSection'); |
| const statusMessagesDiv = document.getElementById('statusMessages'); |
| const aiLoader = document.getElementById('aiLoader'); |
| const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay'); |
| const tvProgressBar = document.getElementById('tv-progress-bar'); |
| const durationSlider = document.getElementById('durationSlider'); |
| const durationValueDisplay = document.getElementById('durationValue'); |
| const durationInfoIcon = document.getElementById('duration-info-icon'); |
| const durationModal = document.getElementById('duration-modal'); |
| const closeDurationModalBtn = document.getElementById('close-duration-modal'); |
| const confirmResetModal = document.getElementById('confirm-reset-modal'); |
| const toastConfirmBtn = document.getElementById('toast-confirm-btn'); |
| const toastCancelBtn = document.getElementById('toast-cancel-btn'); |
| const reportVideoModal = document.getElementById('report-video-modal'); |
| const reportFormContent = document.getElementById('report-form-content'); |
| const customReportSection = document.getElementById('custom-report-section'); |
| const reportSuccessMessage = document.getElementById('report-success-message'); |
| |
| const deleteVideoModal = document.getElementById('delete-video-modal'); |
| const clearHistoryModal = document.getElementById('clear-history-modal'); |
| const limitReachedModal = document.getElementById('limit-reached-modal'); |
| const closeLimitModalBtn = document.getElementById('close-limit-modal'); |
| |
| const tabHomeBtn = document.getElementById('tab-home-btn'); |
| const tabHistoryBtn = document.getElementById('tab-history-btn'); |
| const tabHomeContent = document.getElementById('tab-home-content'); |
| const tabHistoryContent = document.getElementById('tab-history-content'); |
| const activeBadge = document.getElementById('active-badge'); |
| const flyOrb = document.getElementById('fly-orb'); |
| |
| const lightboxModal = document.getElementById('image-lightbox-modal'); |
| const lightboxImg = document.getElementById('lightbox-image-element'); |
| const closeLightboxBtn = document.getElementById('close-lightbox-btn'); |
| |
| let videoToDeleteId = null; |
| let currentImageFile = null; |
| let currentImageObjectURL = null; |
| let globalPollInterval = null; |
| let currentReportingVideoId = null; |
| let expandedCardId = null; |
| let pollTimer = null; |
| |
| |
| |
| |
| async function uploadFileToDocker(file, runId, suffix) { |
| const formData = new FormData(); |
| const ext = 'png'; |
| formData.append('run_id', `${runId}_${suffix}`); |
| formData.append('ext', ext); |
| formData.append('file', file); |
| |
| const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData }); |
| if (!response.ok) throw new Error("آپلود تصویر با خطا مواجه شد."); |
| return ext; |
| } |
| |
| |
| function updateSliderStyle(slider) { |
| const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100; |
| const colorStops = `var(--accent-primary), var(--accent-secondary)`; |
| slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`; |
| } |
| |
| function setGenerateButtonState(text, disabled) { |
| generateButtonText.textContent = text; |
| generateButton.disabled = disabled; |
| } |
| |
| function setUiLock(locked) { |
| generateButton.disabled = locked; |
| imageFileInput.disabled = locked; |
| } |
| |
| function switchTab(target) { |
| if (target === 'home') { |
| tabHomeBtn.classList.add('active'); |
| tabHistoryBtn.classList.remove('active'); |
| tabHomeContent.style.display = 'block'; |
| tabHistoryContent.style.display = 'none'; |
| } else { |
| tabHomeBtn.classList.remove('active'); |
| tabHistoryBtn.classList.add('active'); |
| tabHomeContent.style.display = 'none'; |
| tabHistoryContent.style.display = 'block'; |
| renderHistoryGrid(); |
| } |
| } |
| |
| tabHomeBtn.addEventListener('click', () => switchTab('home')); |
| tabHistoryBtn.addEventListener('click', () => switchTab('history')); |
| |
| function openLightbox(url) { |
| lightboxImg.src = url; |
| lightboxModal.classList.add('active'); |
| } |
| function closeLightbox() { |
| lightboxModal.classList.remove('active'); |
| lightboxImg.src = ''; |
| } |
| closeLightboxBtn.addEventListener('click', closeLightbox); |
| lightboxModal.addEventListener('click', (e) => { |
| if (e.target === lightboxModal) closeLightbox(); |
| }); |
| |
| async function getActiveProjectsCount() { |
| const projects = await projectDB.getAllProjects(); |
| return projects.filter(p => p.status === 'started' || p.status === 'processing').length; |
| } |
| |
| function updateActiveBadge() { |
| projectDB.getAllProjects().then(projects => { |
| const activeCount = projects.filter(p => p.status === 'started' || p.status === 'processing').length; |
| if (activeCount > 0) { |
| activeBadge.textContent = activeCount; |
| activeBadge.style.display = 'flex'; |
| } else { |
| activeBadge.style.display = 'none'; |
| } |
| }); |
| } |
| |
| function triggerFlyToHistoryAnimation() { |
| const rectBtn = generateButton.getBoundingClientRect(); |
| const rectTarget = tabHistoryBtn.getBoundingClientRect(); |
| |
| const startX = rectBtn.left + rectBtn.width / 2; |
| const startY = rectBtn.top + rectBtn.height / 2; |
| const endX = rectTarget.left + rectTarget.width / 2; |
| const endY = rectTarget.top + rectTarget.height / 2; |
| |
| flyOrb.style.display = 'flex'; |
| flyOrb.style.top = `${startY}px`; |
| flyOrb.style.left = `${startX}px`; |
| flyOrb.style.transform = 'translate(-50%, -50%) scale(1.5)'; |
| flyOrb.style.opacity = '1'; |
| |
| setTimeout(() => { |
| flyOrb.style.top = `${endY}px`; |
| flyOrb.style.left = `${endX}px`; |
| flyOrb.style.transform = 'translate(-50%, -50%) scale(0.1) rotate(-180deg)'; |
| flyOrb.style.opacity = '0.1'; |
| }, 50); |
| |
| setTimeout(() => { |
| flyOrb.style.display = 'none'; |
| tabHistoryBtn.classList.add('bounce-nav-tab'); |
| setTimeout(() => tabHistoryBtn.classList.remove('bounce-nav-tab'), 800); |
| }, 900); |
| } |
| |
| function startGlobalStatusPolling() { |
| if (globalPollInterval) return; |
| |
| globalPollInterval = setInterval(async () => { |
| const projects = await projectDB.getAllProjects(); |
| const activeProjects = projects.filter(p => p.status === 'started' || p.status === 'processing'); |
| |
| if (activeProjects.length === 0) { |
| clearInterval(globalPollInterval); |
| globalPollInterval = null; |
| updateActiveBadge(); |
| return; |
| } |
| |
| updateActiveBadge(); |
| |
| for (let proj of activeProjects) { |
| try { |
| |
| let isProcessingInBackend = false; |
| try { |
| const procRes = await fetch(`/static/images/${proj.id}_processing.txt`); |
| if (procRes.ok) { |
| const procText = await procRes.text(); |
| if (procText.trim() === "processing") { |
| isProcessingInBackend = true; |
| } |
| } |
| } catch (e) {} |
| |
| const statusRes = await fetch(`/api/status/${proj.id}`); |
| const statusData = await statusRes.json(); |
| |
| |
| if (statusData.status === 'ready' && statusData.url && (statusData.url.endsWith('.mp4') || statusData.url.endsWith('.webm'))) { |
| isProcessingInBackend = false; |
| } |
| |
| if (isProcessingInBackend) { |
| |
| proj.status = 'processing'; |
| let currentProgress = proj.progress || 10; |
| if (currentProgress < 98) { |
| currentProgress += (Math.random() * 0.4) + 0.3; |
| } |
| proj.progress = Math.min(Math.round(currentProgress), 98); |
| await projectDB.saveProject(proj); |
| |
| const progressElement = document.getElementById(`prog-text-${proj.id}`); |
| const progressBarElement = document.getElementById(`prog-bar-${proj.id}`); |
| if (progressElement) progressElement.textContent = `در حال پردازش (مرحله دوم): ${proj.progress}%`; |
| if (progressBarElement) progressBarElement.style.width = `${proj.progress}%`; |
| continue; |
| } |
| |
| let currentProgress = proj.progress || 10; |
| if (currentProgress < 98) { |
| currentProgress += (Math.random() * 1.0) + 1.5; |
| } |
| proj.progress = Math.min(Math.round(currentProgress), 98); |
| |
| if (statusData.status === 'ready') { |
| |
| proj.status = 'completed'; |
| proj.progress = 100; |
| await projectDB.saveProject(proj); |
| renderHistoryGrid(); |
| |
| try { |
| const vidRes = await fetch(statusData.url); |
| const videoBlob = await vidRes.blob(); |
| proj.videoBlob = videoBlob; |
| proj.remoteUrl = window.location.origin + statusData.url; |
| |
| await projectDB.saveProject(proj); |
| renderHistoryGrid(); |
| } catch (downloadErr) { |
| console.error("Error downloading video file:", downloadErr); |
| } |
| |
| if (userSubscriptionStatus === 'free') { |
| localStorage.setItem('ltx_last_gen_time', Date.now().toString()); |
| checkFreeUserCredit(); |
| } |
| |
| } else if (statusData.status === 'failed' || statusData.status === 'error') { |
| proj.status = 'failed'; |
| proj.progress = 0; |
| proj.error = statusData.message || "خطا در سرور پردازش موازی"; |
| |
| await projectDB.saveProject(proj); |
| renderHistoryGrid(); |
| } else { |
| proj.status = 'processing'; |
| await projectDB.saveProject(proj); |
| |
| const progressElement = document.getElementById(`prog-text-${proj.id}`); |
| const progressBarElement = document.getElementById(`prog-bar-${proj.id}`); |
| if (progressElement) progressElement.textContent = `در حال پردازش: ${proj.progress}%`; |
| if (progressBarElement) progressBarElement.style.width = `${proj.progress}%`; |
| } |
| } catch (pollErr) { |
| console.error(`Polling check failure for ID ${proj.id}:`, pollErr); |
| } |
| } |
| }, 4000); |
| } |
| |
| async function renderHistoryGrid() { |
| const projects = await projectDB.getAllProjects(); |
| projects.sort((a, b) => b.timestamp - a.timestamp); |
| |
| historyGrid.innerHTML = ''; |
| |
| if (projects.length === 0) { |
| historyGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 4rem 1rem; color: var(--text-tertiary);"><i class="fas fa-video-slash" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i><p style="font-weight: 500; font-size: 1.1rem; margin: 0;">هنوز هیچ پروژه ویدیویی ثبت نشده است</p></div>`; |
| return; |
| } |
| |
| const totalCount = projects.length; |
| |
| projects.forEach((proj, index) => { |
| const videoNumber = totalCount - index; |
| |
| const card = document.createElement('div'); |
| card.className = `video-result-card ${expandedCardId === proj.id ? 'expanded' : ''}`; |
| card.id = proj.id; |
| |
| const headerLayout = document.createElement('div'); |
| headerLayout.className = 'card-header-layout'; |
| |
| const headerLeft = document.createElement('div'); |
| headerLeft.className = 'card-header-left'; |
| |
| const thumbContainer = document.createElement('div'); |
| thumbContainer.className = 'card-thumbnail-container'; |
| thumbContainer.title = 'کلیک برای بزرگنمایی تصویر ورودی'; |
| |
| let imgUrl = ''; |
| if (proj.imageBlob) { |
| imgUrl = URL.createObjectURL(proj.imageBlob); |
| const thumbImg = document.createElement('img'); |
| thumbImg.src = imgUrl; |
| thumbContainer.appendChild(thumbImg); |
| } else { |
| thumbContainer.innerHTML = `<i class="fas fa-image" style="font-size: 1.2rem; color: var(--text-tertiary);"></i>`; |
| } |
| |
| const zoomOverlay = document.createElement('div'); |
| zoomOverlay.className = 'card-thumbnail-zoom-overlay'; |
| zoomOverlay.innerHTML = `<i class="fas fa-search-plus"></i>`; |
| thumbContainer.appendChild(zoomOverlay); |
| |
| thumbContainer.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| if (imgUrl) { |
| openLightbox(imgUrl); |
| } |
| }); |
| headerLeft.appendChild(thumbContainer); |
| |
| const titlesDiv = document.createElement('div'); |
| titlesDiv.className = 'card-header-titles'; |
| |
| const titleText = document.createElement('div'); |
| titleText.className = 'card-header-title-text'; |
| titleText.textContent = `ویدیو شماره ${videoNumber}`; |
| |
| const metaDiv = document.createElement('div'); |
| metaDiv.className = 'card-header-meta'; |
| |
| const fDate = new Date(proj.timestamp).toLocaleDateString('fa-IR'); |
| const dateSpan = document.createElement('span'); |
| dateSpan.innerHTML = `<i class="far fa-calendar" style="margin-left: 4px;"></i>${fDate}`; |
| metaDiv.appendChild(dateSpan); |
| |
| const statusBadge = document.createElement('span'); |
| statusBadge.className = 'card-status-badge'; |
| if (proj.status === 'completed') { |
| statusBadge.classList.add('status-badge-completed'); |
| statusBadge.textContent = 'تکمیل شده'; |
| } else if (proj.status === 'failed') { |
| statusBadge.classList.add('status-badge-failed'); |
| statusBadge.textContent = 'خطا'; |
| } else { |
| statusBadge.classList.add('status-badge-processing'); |
| statusBadge.textContent = 'در حال ساخت'; |
| } |
| metaDiv.appendChild(statusBadge); |
| |
| titlesDiv.appendChild(titleText); |
| titlesDiv.appendChild(metaDiv); |
| headerLeft.appendChild(titlesDiv); |
| headerLayout.appendChild(headerLeft); |
| |
| const headerActions = document.createElement('div'); |
| headerActions.className = 'card-header-actions'; |
| |
| const btnDelete = document.createElement('button'); |
| btnDelete.className = 'card-delete-btn'; |
| btnDelete.title = 'حذف ویدیو'; |
| btnDelete.innerHTML = `<i class="fas fa-trash-alt"></i>`; |
| btnDelete.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| videoToDeleteId = proj.id; |
| deleteVideoModal.classList.add('active'); |
| }); |
| |
| const chevron = document.createElement('i'); |
| chevron.className = 'fas fa-chevron-left chevron-arrow'; |
| |
| headerActions.appendChild(btnDelete); |
| headerActions.appendChild(chevron); |
| headerLayout.appendChild(headerActions); |
| card.appendChild(headerLayout); |
| |
| const expandedBody = document.createElement('div'); |
| expandedBody.className = 'card-expanded-body'; |
| |
| expandedBody.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| }); |
| |
| if (proj.status === 'started' || proj.status === 'processing') { |
| const overlayDiv = document.createElement('div'); |
| overlayDiv.className = 'processing-card-overlay'; |
| |
| if (proj.imageBlob) { |
| const bgImg = document.createElement('img'); |
| bgImg.className = 'processing-bg-image'; |
| bgImg.src = imgUrl; |
| overlayDiv.appendChild(bgImg); |
| } |
| |
| overlayDiv.innerHTML += ` |
| <div class="processing-card-content"> |
| <div class="processing-spinner"></div> |
| <span class="processing-status-lbl" id="prog-text-${proj.id}">در حال پردازش: ${proj.progress || 10}%</span> |
| <div class="processing-progress-bg"> |
| <div class="processing-progress-bar" id="prog-bar-${proj.id}" style="width: ${proj.progress || 10}%"></div> |
| </div> |
| </div> |
| `; |
| expandedBody.appendChild(overlayDiv); |
| |
| } else if (proj.status === 'failed') { |
| const failedOverlay = document.createElement('div'); |
| failedOverlay.className = 'failed-card-overlay'; |
| failedOverlay.innerHTML = ` |
| <div class="failed-card-content"> |
| <i class="fas fa-circle-exclamation"></i> |
| <p>ساخت ویدیو با شکست مواجه شد.</p> |
| <span style="font-size: 0.75rem; opacity: 0.8; max-width: 90%; display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${proj.error || 'خطای اتصال به سرور'}</span> |
| </div> |
| `; |
| expandedBody.appendChild(failedOverlay); |
| |
| } else { |
| const video = document.createElement('video'); |
| video.src = URL.createObjectURL(proj.videoBlob); |
| video.controls = true; |
| video.playsInline = true; |
| video.loop = true; |
| video.autoplay = false; |
| video.muted = false; |
| expandedBody.appendChild(video); |
| } |
| |
| const infoContainer = document.createElement('div'); |
| infoContainer.className = 'video-info-container'; |
| |
| const promptLbl = document.createElement('div'); |
| promptLbl.className = 'video-prompt-lbl'; |
| const originalPrompt = proj.prompt || 'تصویر بدون توضیحات متنی'; |
| const isLongPrompt = originalPrompt.length > 60; |
| |
| if (isLongPrompt) { |
| promptLbl.innerHTML = ` |
| <strong>دستور ساخت:</strong> |
| <span class="prompt-text" style="display: inline-block; max-width: 70%; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${originalPrompt}</span> |
| <button class="toggle-prompt-btn" style="display: inline-block; vertical-align: middle; background: none; border: none; color: var(--accent-primary); cursor: pointer; font-family: var(--app-font); font-size: 0.85rem; font-weight: 700; padding: 0 4px; margin-right: 4px; outline: none;">نمایش بیشتر</button> |
| `; |
| const btn = promptLbl.querySelector('.toggle-prompt-btn'); |
| const txt = promptLbl.querySelector('.prompt-text'); |
| btn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| if (txt.style.whiteSpace === 'nowrap') { |
| txt.style.whiteSpace = 'normal'; |
| txt.style.maxWidth = '100%'; |
| btn.textContent = 'نمایش کمتر'; |
| } else { |
| txt.style.whiteSpace = 'nowrap'; |
| txt.style.maxWidth = '70%'; |
| btn.textContent = 'نمایش بیشتر'; |
| } |
| }); |
| } else { |
| promptLbl.innerHTML = `<strong>دستور ساخت:</strong> <span class="prompt-text">${originalPrompt}</span>`; |
| } |
| infoContainer.appendChild(promptLbl); |
| |
| const metaLbl = document.createElement('div'); |
| metaLbl.className = 'video-meta-lbl'; |
| metaLbl.innerHTML = ` |
| <span><i class="far fa-calendar" style="margin-left: 4px;"></i>تاریخ ثبت: ${fDate}</span> |
| <span><i class="far fa-clock" style="margin-left: 4px;"></i>مدت زمان: ${proj.duration} ثانیه</span> |
| ${proj.status === 'completed' ? `<i class="far fa-flag report-flag-icon" data-video-id="${proj.id}" title="گزارش خطای رندر" onclick="handleReportClick(event)"></i>` : ''} |
| `; |
| infoContainer.appendChild(metaLbl); |
| expandedBody.appendChild(infoContainer); |
| |
| if (proj.status === 'completed') { |
| const controlsDiv = document.createElement('div'); |
| controlsDiv.className = 'video-controls'; |
| |
| const btnDownload = document.createElement('button'); |
| btnDownload.className = 'video-button primary'; |
| btnDownload.innerHTML = `<i class="fas fa-download"></i> دانلود ویدیو`; |
| btnDownload.addEventListener('click', (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| const targetUrl = proj.remoteUrl || URL.createObjectURL(proj.videoBlob); |
| parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: targetUrl }, '*'); |
| }); |
| |
| controlsDiv.appendChild(btnDownload); |
| expandedBody.appendChild(controlsDiv); |
| } else if (proj.status === 'failed') { |
| const retryControls = document.createElement('div'); |
| retryControls.className = 'video-controls'; |
| const btnRetry = document.createElement('button'); |
| btnRetry.className = 'video-button primary'; |
| btnRetry.style.background = 'linear-gradient(135deg, #e53e3e, #c53030)'; |
| btnRetry.innerHTML = `<i class="fas fa-rotate-right"></i> تلاش مجدد`; |
| btnRetry.addEventListener('click', async (e) => { |
| e.stopPropagation(); |
| card.remove(); |
| await projectDB.deleteProject(proj.id); |
| promptInput.value = proj.prompt; |
| durationSlider.value = proj.duration; |
| currentImageFile = proj.imageFile; |
| |
| const dataTransfer = new DataTransfer(); |
| dataTransfer.items.add(proj.imageFile); |
| imageFileInput.files = dataTransfer.files; |
| imageFileInput.dispatchEvent(new Event('change')); |
| |
| switchTab('home'); |
| }); |
| retryControls.appendChild(btnRetry); |
| expandedBody.appendChild(retryControls); |
| } |
| |
| card.appendChild(expandedBody); |
| |
| card.addEventListener('click', () => { |
| const isExpanded = card.classList.contains('expanded'); |
| |
| document.querySelectorAll('.video-result-card video').forEach(vid => { |
| vid.pause(); |
| }); |
| |
| document.querySelectorAll('.video-result-card').forEach(c => { |
| c.classList.remove('expanded'); |
| }); |
| |
| document.querySelectorAll('.video-result-card').forEach(c => { |
| const txt = c.querySelector('.prompt-text'); |
| const btn = c.querySelector('.toggle-prompt-btn'); |
| if (txt && btn) { |
| txt.style.whiteSpace = 'nowrap'; |
| txt.style.maxWidth = '70%'; |
| btn.textContent = 'نمایش بیشتر'; |
| } |
| }); |
| |
| if (isExpanded) { |
| card.classList.remove('expanded'); |
| expandedCardId = null; |
| } else { |
| card.classList.add('expanded'); |
| expandedCardId = proj.id; |
| } |
| }); |
| |
| historyGrid.appendChild(card); |
| }); |
| } |
| |
| async function startGenerationProcess() { |
| let promptToUse = promptInput.value.trim(); |
| const warningEl = document.getElementById('prompt-warning'); |
| |
| |
| if (!promptToUse) { |
| if (warningEl) { |
| warningEl.style.display = 'block'; |
| } |
| setUiLock(false); |
| return; |
| } else { |
| if (warningEl) { |
| warningEl.style.display = 'none'; |
| } |
| } |
| |
| const promptToUseLower = promptToUse.toLowerCase(); |
| const forbiddenWords = ["sex", "sexy", "porn", "nude", "erotic", "سکس", "سکسی", "پورن", "شهوانی", "برهنه", "لخت"]; |
| |
| if (forbiddenWords.some(word => promptToUseLower.includes(word))) { |
| alert("متن ورودی شما حاوی کلمات نامناسب است. لطفاً آن را اصلاح کرده و مجدداً تلاش کنید."); |
| setUiLock(false); |
| return; |
| } |
| |
| if (!currentImageFile) { |
| alert('لطفاً یک تصویر برای شروع پروژه انتخاب کنید.'); |
| setUiLock(false); |
| return; |
| } |
| |
| const activeCount = await getActiveProjectsCount(); |
| const maxAllowedActive = (userSubscriptionStatus === 'paid') ? 5 : 2; |
| if (activeCount >= maxAllowedActive) { |
| const modalText = limitReachedModal.querySelector('.modal-text'); |
| if (modalText) { |
| modalText.innerHTML = `شما در حال حاضر دارای ${maxAllowedActive} پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.`; |
| } |
| limitReachedModal.classList.add('active'); |
| setUiLock(false); |
| return; |
| } |
| |
| if (userSubscriptionStatus !== 'paid') { |
| const lastGen = localStorage.getItem('ltx_last_gen_time'); |
| if (lastGen) { |
| const lastGenTime = parseInt(lastGen, 10); |
| const now = Date.now(); |
| const oneDayMs = 24 * 60 * 60 * 1000; |
| if (lastGenTime + oneDayMs > now) { |
| alert("محدودیت ساخت ویدیوی رایگان شما برای امروز به پایان رسیده است."); |
| setUiLock(false); |
| return; |
| } |
| } |
| } |
| |
| |
| setUiLock(true); |
| setGenerateButtonState("در حال آپلود تصویر...", true); |
| |
| const newProjectId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); |
| return v.toString(16); |
| }); |
| |
| try { |
| |
| const uploadExt = await uploadFileToDocker(currentImageFile, newProjectId, 'img'); |
| |
| const newProject = { |
| id: newProjectId, |
| prompt: promptToUse, |
| duration: parseFloat(durationSlider.value), |
| status: 'started', |
| progress: 10, |
| timestamp: Date.now(), |
| imageFile: currentImageFile, |
| imageBlob: currentImageFile |
| }; |
| |
| try { |
| |
| await projectDB.saveProject(newProject); |
| } catch (dbErr) { |
| console.warn("ذخیره اولیه به دلیل محدودیت مرورگر با خطا مواجه شد. در حال ذخیره متنی...", dbErr); |
| |
| delete newProject.imageFile; |
| delete newProject.imageBlob; |
| await projectDB.saveProject(newProject); |
| } |
| |
| triggerFlyToHistoryAnimation(); |
| updateActiveBadge(); |
| switchTab('history'); |
| |
| |
| promptInput.value = ''; |
| imageFileInput.value = ''; |
| if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); } |
| currentImageObjectURL = null; |
| imagePreview.src = ''; |
| imageDropZone.classList.remove('has-image'); |
| currentImageFile = null; |
| durationSlider.value = "4.0"; |
| durationValueDisplay.textContent = "4.0 ثانیه"; |
| updateSliderStyle(durationSlider); |
| setUiLock(false); |
| setGenerateButtonState("ساخت ویدیو جادویی", false); |
| |
| |
| proceedWithBackendGeneration(newProject); |
| |
| } catch (err) { |
| alert(`خطا در آپلود فایل: ${err.message}`); |
| setUiLock(false); |
| setGenerateButtonState("ساخت ویدیو جادویی", false); |
| } |
| } |
| |
| async function proceedWithBackendGeneration(projectObj) { |
| try { |
| const imageUrl = window.location.origin + `/static/images/${projectObj.id}_img.png`; |
| |
| const ltxPayload = { |
| image_path: imageUrl, |
| prompt: projectObj.prompt, |
| seconds: projectObj.duration < 5.0 ? projectObj.duration + 0.1 : projectObj.duration, |
| seed: -1 |
| }; |
| |
| const base64Payload = btoa(unescape(encodeURIComponent(JSON.stringify(ltxPayload)))); |
| const finalPrompt = `VOICECONFIG_LTX_${base64Payload}`; |
| |
| const genRes = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| prompt: finalPrompt, |
| action_name: "ltxvideo" |
| }) |
| }); |
| |
| const data = await genRes.json(); |
| |
| if (data.status === 'success') { |
| const actualRunId = data.run_id; |
| const oldId = projectObj.id; |
| |
| projectObj.id = actualRunId; |
| projectObj.status = 'processing'; |
| projectObj.progress = 20; |
| |
| try { |
| |
| await projectDB.saveProject(projectObj); |
| } catch (dbErr) { |
| console.warn("ذخیره فایل خام در این مرورگر پشتیبانی نمیشود. ذخیره بدون فایل انجام میشود...", dbErr); |
| |
| delete projectObj.imageFile; |
| delete projectObj.imageBlob; |
| await projectDB.saveProject(projectObj); |
| } |
| |
| |
| await projectDB.deleteProject(oldId); |
| |
| renderHistoryGrid(); |
| startGlobalStatusPolling(); |
| } else { |
| throw new Error(data.message || 'خطا در ثبت نوبت صف.'); |
| } |
| } catch (error) { |
| projectObj.status = 'failed'; |
| projectObj.error = error.message; |
| |
| try { |
| |
| await projectDB.saveProject(projectObj); |
| } catch (dbErr) { |
| delete projectObj.imageFile; |
| delete projectObj.imageBlob; |
| await projectDB.saveProject(projectObj); |
| } |
| |
| renderHistoryGrid(); |
| } |
| } |
| |
| generateButton.addEventListener('click', async () => { |
| if (generateButton.disabled) return; |
| await startGenerationProcess(); |
| }); |
| |
| async function initializeForm() { |
| if(pollTimer) clearInterval(pollTimer); |
| |
| const warningEl = document.getElementById('prompt-warning'); |
| if (warningEl) warningEl.style.display = 'none'; |
| |
| |
| promptInput.value = ''; |
| imageFileInput.value = ''; |
| if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); } |
| currentImageObjectURL = null; |
| imagePreview.src = ''; |
| imageDropZone.classList.remove('has-image'); |
| currentImageFile = null; |
| durationSlider.value = "4.0"; |
| durationValueDisplay.textContent = "4.0 ثانیه"; |
| |
| setUiLock(false); |
| setGenerateButtonState("ساخت ویدیو جادویی", false); |
| updateSliderStyle(durationSlider); |
| } |
| |
| document.getElementById('confirm-delete-btn').addEventListener('click', async () => { |
| if (videoToDeleteId) { |
| deleteVideoModal.classList.remove('active'); |
| const card = document.getElementById(videoToDeleteId); |
| if (card) { |
| card.classList.add('fade-out-shrink'); |
| setTimeout(() => card.remove(), 500); |
| } |
| await projectDB.deleteProject(videoToDeleteId); |
| videoToDeleteId = null; |
| updateActiveBadge(); |
| setTimeout(() => { |
| projectDB.getAllProjects().then(projs => { |
| if (projs.length === 0) renderHistoryGrid(); |
| }); |
| }, 600); |
| } |
| }); |
| |
| document.getElementById('cancel-delete-btn').addEventListener('click', () => { deleteVideoModal.classList.remove('active'); videoToDeleteId = null; }); |
| |
| document.getElementById('clear-history-btn').addEventListener('click', () => { |
| clearHistoryModal.classList.add('active'); |
| }); |
| |
| document.getElementById('cancel-clear-history-btn').addEventListener('click', () => clearHistoryModal.classList.remove('active')); |
| closeLimitModalBtn.addEventListener('click', () => limitReachedModal.classList.remove('active')); |
| |
| document.getElementById('confirm-clear-history-btn').addEventListener('click', async () => { |
| clearHistoryModal.classList.remove('active'); |
| await projectDB.clearAllProjects(); |
| renderHistoryGrid(); |
| updateActiveBadge(); |
| }); |
| |
| toastConfirmBtn.addEventListener('click', async () => { |
| confirmResetModal.classList.remove('active'); |
| await initializeForm(); |
| }); |
| toastCancelBtn.addEventListener('click', () => confirmResetModal.classList.remove('active')); |
| |
| removeImageBtn.addEventListener('click', async (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| await initializeForm(); |
| }); |
| |
| imageFileInput.addEventListener('change', async function(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| generateButton.disabled = true; |
| |
| if (!file.type.startsWith('image/')) { |
| alert('فرمت فایل نامعتبر است. لطفاً یک تصویر انتخاب کنید.'); |
| currentImageFile = null; imageFileInput.value = ''; generateButton.disabled = false; |
| return; |
| } |
| |
| currentImageFile = file; |
| if (currentImageObjectURL) URL.revokeObjectURL(currentImageObjectURL); |
| currentImageObjectURL = URL.createObjectURL(file); |
| |
| imagePreview.onload = () => { |
| imageDropZone.classList.add('has-image'); |
| setUiLock(false); |
| }; |
| imagePreview.src = currentImageObjectURL; |
| }); |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| updateSliderStyle(durationSlider); |
| userFingerprint = await getBrowserFingerprint(); |
| parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*'); |
| |
| try { |
| await projectDB.init(); |
| await renderHistoryGrid(); |
| updateActiveBadge(); |
| startGlobalStatusPolling(); |
| } |
| catch (error) { |
| console.error("Failed to init state database:", error); |
| } |
| }); |
| |
| durationSlider.addEventListener('input', () => { |
| const value = parseFloat(durationSlider.value); |
| durationValueDisplay.textContent = `${value.toFixed(1)} ثانیه`; |
| updateSliderStyle(durationSlider); |
| }); |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault())); |
| ['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); })); |
| ['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over'))); |
| imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } }); |
| durationInfoIcon.addEventListener('click', () => durationModal.classList.add('active')); |
| closeDurationModalBtn.addEventListener('click', () => durationModal.classList.remove('active')); |
| |
| function handleReportClick(event) { |
| currentReportingVideoId = event.currentTarget.dataset.videoId; |
| reportFormContent.style.display = 'block'; |
| reportSuccessMessage.style.display = 'none'; |
| customReportSection.style.display = 'none'; |
| reportVideoModal.querySelector('#custom-report-text').value = ''; |
| reportVideoModal.classList.add('active'); |
| } |
| |
| reportVideoModal.querySelectorAll('.report-option-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| if (btn.id === 'report-other-btn') { customReportSection.style.display = 'block'; return; } |
| reportFormContent.style.display = 'none'; |
| reportSuccessMessage.style.display = 'block'; |
| setTimeout(() => reportVideoModal.classList.remove('active'), 2000); |
| }); |
| }); |
| |
| reportVideoModal.querySelector('#send-report-btn').addEventListener('click', () => { |
| const customText = reportVideoModal.querySelector('#custom-report-text').value.trim(); |
| if (customText && currentReportingVideoId) { |
| reportFormContent.style.display = 'none'; |
| reportSuccessMessage.style.display = 'block'; |
| setTimeout(() => reportVideoModal.classList.remove('active'), 2000); |
| } |
| }); |
| reportVideoModal.addEventListener('click', (e) => { if(e.target === reportVideoModal) reportVideoModal.classList.remove('active'); }); |
| reportVideoModal.querySelectorAll('.emoji').forEach(emoji => { emoji.addEventListener('click', () => { reportVideoModal.querySelector('#custom-report-text').value += emoji.textContent; }); }); |
| |
| 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() || '#4A6CFA'; |
| const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim() || '#8A94A6'; |
| 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()); } } |
| const connectParticles = function() { 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> |