| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>Subtitle Studio Pro</title> |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700;900&display=swap" rel="stylesheet"> |
| <link href="https://fonts.googleapis.com/css2?family=Lalezar&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 { --bg: #0f0f0f; --surface: #1e1e1e; --primary: #7C4DFF; --text: #fff; --highlight: #2a2a2a; --panel-bg: #151515; } |
| * { box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; outline: none; } |
| |
| body { margin: 0; background: var(--bg); color: var(--text); font-family: 'Vazirmatn'; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; min-height: 100vh; padding-bottom: 50px;} |
| |
| |
| .top-bar { position: sticky; top: 0; height: 60px; background: rgba(30,30,30,0.95); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; z-index: 100; border-bottom: 1px solid #333; box-shadow: 0 4px 10px rgba(0,0,0,0.3); } |
| |
| .new-proj-btn { background: #333; color: #ccc; border: 1px solid #444; padding: 8px 15px; border-radius: 12px; font-size: 0.9rem; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 8px; font-family: inherit; } |
| .new-proj-btn:hover { background: #444; color: #fff; border-color: #666; } |
| |
| |
| .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(5px); z-index: 3000; display: none; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s; } |
| .modal-overlay.show { opacity: 1; } |
| .modal-box { background: #222; border: 1px solid #444; width: 90%; max-width: 350px; border-radius: 20px; padding: 25px; text-align: center; transform: scale(0.8); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: 0 10px 40px rgba(0,0,0,0.5); } |
| .modal-overlay.show .modal-box { transform: scale(1); } |
| .modal-title { font-size: 1.2rem; font-weight: bold; margin-bottom: 10px; color: #fff; } |
| .modal-btn { flex: 1; padding: 12px; border-radius: 12px; border: none; font-family: inherit; font-weight: bold; cursor: pointer; transition: 0.2s; } |
| .btn-confirm { background: var(--primary); color: #fff; } |
| |
| |
| #workspace { width: 100%; display: flex; justify-content: center; background: #000; padding: 20px 0; min-height: 300px; flex-shrink: 0; } |
| #scaler { transform-origin: top center; transition: transform 0.1s ease-out; } |
| #videoContainer { position: relative; background: #000; overflow: hidden; cursor: pointer; box-shadow: 0 0 30px rgba(124, 77, 255, 0.1); border-radius: 8px;} |
| video { width: 100%; height: 100%; object-fit: contain; display: block; } |
| #playOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: rgba(0,0,0,0.2); z-index: 10; transition: opacity 0.2s; } |
| #playOverlay.playing { opacity: 0; pointer-events: none; } |
| #playOverlay i { font-size: 4rem; color: rgba(255,255,255,0.8); pointer-events: none; } |
| |
| #activeText { |
| position: absolute; white-space: pre; pointer-events: auto; cursor: move; |
| padding: 10px 20px; line-height: 1.5; font-weight: bold; |
| transition: background-color 0.2s, text-shadow 0.2s, -webkit-text-stroke 0.2s, border-radius 0.2s; |
| transform-origin: center center; text-align: center; left: 50%; transform: translateX(-50%); |
| paint-order: stroke fill; |
| } |
| |
| |
| .controls-bar { display: flex; justify-content: center; gap: 10px; padding: 10px 10px; background: var(--bg); border-bottom: 1px solid #222; position: sticky; top: 115px; z-index: 90; flex-wrap: wrap; } |
| .tool-btn { display: flex; flex-direction: column; align-items: center; gap: 5px; background: var(--surface); border: 1px solid #333; color: #aaa; cursor: pointer; transition: 0.2s; padding: 8px 10px; border-radius: 12px; min-width: 70px; flex: 1; max-width: 100px; } |
| .tool-btn:hover, .tool-btn.active-tool { color: #fff; background: var(--primary); border-color: var(--primary); } |
| |
| #toolsContainer { width: 100%; background: var(--panel-bg); overflow: hidden; min-height: 0; transition: none; border-bottom: 1px solid #333; } |
| .tool-section { display: none; padding: 20px; animation: fadeIn 0.3s ease; max-width: 800px; margin: 0 auto; } |
| .tool-section.active-section { display: block; } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } |
| |
| .btn-exp { background: var(--primary); color: #fff; border: none; padding: 10px 25px; border-radius: 12px; font-weight: bold; cursor: pointer; transition: 0.2s; font-family: inherit; } |
| |
| .btn-save-manual { |
| width: 100%; background: #00C853; color: white; border: none; |
| padding: 12px; border-radius: 10px; font-weight: bold; font-family: inherit; |
| margin-top: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; |
| } |
| |
| .row { margin-bottom: 20px; } |
| label { display: block; margin-bottom: 8px; color: #ccc; font-size: 0.9rem; } |
| input[type=range] { width: 100%; accent-color: var(--primary); height: 6px; cursor: pointer; border-radius:3px; } |
| |
| .style-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px; } |
| .style-card { background: #333; border: 2px solid transparent; border-radius: 10px; padding: 15px; cursor: pointer; display: flex; flex-direction:column; align-items: center; gap: 8px; justify-content: center; min-height: 110px; position: relative; transition: 0.2s; } |
| .style-card.selected { border-color: var(--primary); background: #2a2a2a; } |
| |
| .delete-style-btn { |
| position: absolute; top: 5px; left: 5px; |
| background: rgba(255, 50, 50, 0.2); color: #ff5555; |
| border: none; border-radius: 5px; width: 25px; height: 25px; |
| display: flex; align-items: center; justify-content: center; cursor: pointer; |
| } |
| |
| .mode-btn { flex: 1; padding: 12px; border-radius: 8px; background: #333; color: #aaa; border: 2px solid transparent; cursor: pointer; } |
| .mode-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); } |
| |
| .font-btn { padding: 15px; background: #333; border-radius: 8px; cursor: pointer; border: 2px solid transparent; position: relative; } |
| .font-btn.ticked { border-color: var(--primary); } |
| |
| |
| .sub-card { background: #222; border-right: 4px solid #444; border-radius: 8px; margin-bottom: 10px; padding: 15px; display: flex; flex-direction: column; gap: 10px; transition: 0.2s; cursor: pointer; } |
| .sub-card.active { background: #2b2535; border-right-color: var(--primary); } |
| .sub-time { font-size: 0.75rem; color: #666; font-family: monospace; } |
| .sub-text-content { font-size: 1rem; color: #ddd; } |
| .sub-card.active .sub-text-content { color: #fff; font-weight: bold; } |
| .mini-btn { width: 36px; height: 36px; border-radius: 10px; border: none; background: rgba(255,255,255,0.1); color: #fff; cursor: pointer; margin-left: 5px;} |
| .mini-btn:hover { background: var(--primary); } |
| |
| #uploadScreen, #loader, #resultScreen { position: fixed; top:0; left:0; width:100%; height:100%; background: var(--bg); z-index: 2000; display: flex; flex-direction: column; justify-content: center; align-items: center; } |
| #loader, #resultScreen { display: none; } |
| .spinner { width: 50px; height: 50px; border: 5px solid #444; border-top-color: var(--primary); border-radius: 50%; animation: s 0.8s linear infinite; } |
| @keyframes s { to { transform: rotate(360deg); } } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="exitModal" class="modal-overlay"> |
| <div class="modal-box"> |
| <div class="modal-title">شروع پروژه جدید؟</div> |
| <div style="margin-bottom:20px; color:#aaa;">تغییرات فعلی از بین میروند.</div> |
| <div style="display:flex; gap:10px;"> |
| <button class="modal-btn btn-confirm" onclick="confirmExit()">بله</button> |
| <button class="modal-btn" style="background:#333; color:#fff;" onclick="closeExitModal()">لغو</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="uploadScreen"> |
| <h2 style="margin-bottom: 30px; font-size:1.8rem;">استودیو زیرنویس</h2> |
| <input type="file" id="fileIn" hidden accept="video/*" onchange="startUpload()"> |
| <button class="btn-exp" style="padding: 15px 40px; font-size: 1.1rem; border-radius: 30px;" onclick="document.getElementById('fileIn').click()"> |
| <i class="fa-solid fa-folder-open"></i> انتخاب ویدیو |
| </button> |
| </div> |
|
|
| <div id="loader"><div class="spinner"></div><p style="margin-top:20px; color:#fff;">در حال پردازش...</p></div> |
|
|
| <div id="resultScreen"> |
| <h3 style="color:#fff; margin-bottom: 15px;">ویدیو آماده شد!</h3> |
| <div style="width: 90%; max-width: 400px; height: 50vh; background: #111; border-radius: 15px; overflow: hidden; margin-bottom: 25px; display: flex; align-items: center; justify-content: center; border: 1px solid #333;"> |
| <video id="resultVideo" controls style="max-width:100%; max-height:100%; width:100%; height:100%;"></video> |
| </div> |
| <div style="display: flex; gap: 15px;"> |
| <a id="downloadBtn" href="#" download class="btn-exp" style="background: #00C853; text-decoration: none;">دانلود</a> |
| <button class="btn-exp" style="background: #444;" onclick="closeResult()">ویرایش</button> |
| </div> |
| </div> |
|
|
| <div class="top-bar"> |
| <button class="new-proj-btn" onclick="showExitModal()">پروژه جدید</button> |
| <button class="btn-exp" onclick="render()" style="padding: 8px 20px;">خروجی نهایی</button> |
| </div> |
|
|
| <div id="workspace"> |
| <div id="scaler"> |
| <div id="videoContainer" onclick="togglePlay()"> |
| <video id="vid" playsinline webkit-playsinline></video> |
| <div id="playOverlay"><i class="fa-solid fa-play"></i></div> |
| <div id="subtitleLayer"><div id="activeText"></div></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="controls-bar"> |
| <button class="tool-btn" onclick="toggleTool('style')" id="btn-style"><i class="fa-solid fa-layer-group"></i><span>استایل</span></button> |
| <button class="tool-btn" onclick="toggleTool('appearance')" id="btn-appearance"><i class="fa-solid fa-palette"></i><span>ظاهر</span></button> |
| <button class="tool-btn" onclick="toggleTool('font')" id="btn-font"><i class="fa-solid fa-font"></i><span>فونت</span></button> |
| <button class="tool-btn" onclick="toggleTool('text')" id="btn-text"><i class="fa-solid fa-pen-to-square"></i><span>متن</span></button> |
| </div> |
|
|
| <div id="toolsContainer"> |
| <div id="section-style" class="tool-section"> |
| <div style="margin-bottom: 10px; color: #888;">استایلهای ذخیره شده:</div> |
| <div class="style-grid" id="savedStylesGrid"></div> |
| <div id="noStylesMsg" style="text-align: center; padding: 30px; color: #666;"> |
| هنوز استایلی ندارید. از دکمه "ذخیره این سبک" در تب ظاهر استفاده کنید. |
| </div> |
| </div> |
|
|
| <div id="section-appearance" class="tool-section"> |
| <div class="row"><label>سایز متن</label><input type="range" id="fz" min="10" max="300" value="65" oninput="upd()"></div> |
| <div class="row"><label>موقعیت عمودی</label><input type="range" id="pos" min="0" max="1500" value="150" oninput="upd()"></div> |
| <div class="row" id="radiusRow" style="display:none;"><label>گردی کادر (فقط در پیشنمایش)</label><input type="range" id="radius" min="0" max="50" value="12" oninput="upd()"></div> |
|
|
| <div style="display:flex; gap:15px; margin-bottom:20px;"> |
| <div style="flex:1;"><label>رنگ متن</label><input type="color" id="col" value="#ffffff" style="width:100%; height:45px; border:none; border-radius:8px;" oninput="upd()"></div> |
| <div style="flex:1;"><label>رنگ حاشیه/باکس</label><input type="color" id="bgCol" value="#000000" style="width:100%; height:45px; border:none; border-radius:8px;" oninput="upd()"></div> |
| </div> |
| <div class="row" style="display:flex; gap:10px;"> |
| <button class="btn-exp mode-btn active" onclick="setMode('solid')">کادر پر</button> |
| <button class="btn-exp mode-btn" onclick="setMode('transparent')">شیشهای</button> |
| <button class="btn-exp mode-btn" onclick="setMode('outline')">فقط دورخط</button> |
| </div> |
| <button class="btn-save-manual" onclick="saveCurrentStyle()"><i class="fa-solid fa-floppy-disk"></i> ذخیره این سبک</button> |
| </div> |
|
|
| <div id="section-font" class="tool-section"> |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> |
| <button class="font-btn ticked" style="font-family:Lalezar;" onclick="setFont('lalezar', this)">لاله زار</button> |
| <button class="font-btn" style="font-family:Vazirmatn; font-weight:900;" onclick="setFont('vazir', this)">وزیر ضخیم</button> |
| <button class="font-btn" style="font-family:Arial;" onclick="setFont('roboto', this)">ساده (Arial)</button> |
| <button class="font-btn" style="font-family:Impact;" onclick="setFont('bangers', this)">بولد (Impact)</button> |
| </div> |
| </div> |
|
|
| <div id="section-text" class="tool-section"> |
| <div id="subCardsContainer"></div> |
| </div> |
| </div> |
|
|
| <script> |
| let state = { |
| id: null, w: 1080, h: 1920, segs: [], |
| st: { f: 'lalezar', fz: 65, col: '#ffffff', bg: '#000000', type: 'solid', y: 150, x: 0, radius: 12, name: 'default' } |
| }; |
| let curSeg = -1; let manualOverride = false; let stopAtTime = null; |
| const v = document.getElementById('vid'); const tEl = document.getElementById('activeText'); |
| const toolsContainer = document.getElementById('toolsContainer'); |
| |
| window.onload = function() { renderSavedStyles(); fit(); }; |
| |
| function hexToRgba(hex, alpha) { |
| let c = hex.substring(1).split(''); |
| if(c.length==3) c = [c[0], c[0], c[1], c[1], c[2], c[2]]; |
| c = '0x'+c.join(''); |
| return 'rgba('+[(c>>16)&255, (c>>8)&255, c&255].join(',')+','+alpha+')'; |
| } |
| |
| function getSavedStyles() { try { return JSON.parse(localStorage.getItem('aiStyles') || '[]'); } catch { return []; } } |
| function saveStyleToStorage(styleData) { const styles = getSavedStyles(); styles.unshift(styleData); localStorage.setItem('aiStyles', JSON.stringify(styles)); renderSavedStyles(); } |
| function deleteStyle(e, id) { e.stopPropagation(); if(confirm('حذف شود؟')) { const styles = getSavedStyles().filter(s => s.id !== id); localStorage.setItem('aiStyles', JSON.stringify(styles)); renderSavedStyles(); } } |
| |
| function saveCurrentStyle() { |
| const name = prompt("نام استایل:", "استایل شخصی"); |
| if(!name) return; |
| saveStyleToStorage({ |
| id: Date.now(), name: name, |
| primaryColor: state.st.col, outlineColor: state.st.bg, |
| backType: state.st.type, font: state.st.f, fontSize: state.st.fz |
| }); |
| alert("ذخیره شد."); |
| } |
| |
| function applySavedStyle(id) { |
| const found = getSavedStyles().find(s => s.id === id); |
| if(found) { |
| state.st.col = found.primaryColor; state.st.bg = found.outlineColor; |
| state.st.type = found.backType; state.st.f = found.font; state.st.fz = found.fontSize; |
| state.st.name = 'custom_' + id; |
| document.getElementById('col').value = state.st.col; |
| document.getElementById('bgCol').value = state.st.bg; |
| document.getElementById('fz').value = state.st.fz; |
| syncModeButtons(); |
| document.querySelectorAll('.font-btn').forEach(b => b.classList.remove('ticked')); |
| let fIdx = 0; |
| if(state.st.f === 'vazir') fIdx=1; else if(state.st.f === 'roboto') fIdx=2; else if(state.st.f === 'bangers') fIdx=3; |
| document.querySelectorAll('.font-btn')[fIdx].classList.add('ticked'); |
| upd(); |
| } |
| } |
| |
| function renderSavedStyles() { |
| const grid = document.getElementById('savedStylesGrid'); |
| const msg = document.getElementById('noStylesMsg'); |
| const styles = getSavedStyles(); |
| grid.innerHTML = ''; |
| if(styles.length === 0) { msg.style.display = 'block'; return; } |
| msg.style.display = 'none'; |
| styles.forEach(s => { |
| const el = document.createElement('div'); |
| el.className = 'style-card'; |
| let previewStyle = `color: ${s.primaryColor};`; |
| if(s.backType === 'solid') previewStyle += `background: ${s.outlineColor};`; |
| else if(s.backType === 'outline') previewStyle += `-webkit-text-stroke: 2px ${s.outlineColor}; background: transparent;`; |
| else previewStyle += `background: rgba(0,0,0,0.5);`; |
| el.innerHTML = `<button class="delete-style-btn" onclick="deleteStyle(event, ${s.id})"><i class="fa-solid fa-trash"></i></button><div style="font-size:1.2rem; padding:5px; border-radius:6px; ${previewStyle}">نمونه</div><div style="font-size:0.8rem; color:#aaa;">${s.name}</div>`; |
| el.onclick = () => applySavedStyle(s.id); |
| grid.appendChild(el); |
| }); |
| } |
| |
| function showExitModal() { document.getElementById('exitModal').style.display = 'flex'; setTimeout(() => document.getElementById('exitModal').classList.add('show'), 10); } |
| function closeExitModal() { document.getElementById('exitModal').classList.remove('show'); setTimeout(() => document.getElementById('exitModal').style.display = 'none', 300); } |
| function confirmExit() { window.location.reload(); } |
| |
| function fit() { |
| if(!state.w) return; |
| const ws = document.getElementById('workspace'); |
| const scale = Math.min((ws.clientWidth - 40) / state.w, (window.innerHeight * 0.6) / state.h); |
| const c = document.getElementById('videoContainer'); |
| c.style.width = state.w + 'px'; c.style.height = state.h + 'px'; |
| document.getElementById('scaler').style.transform = `scale(${scale})`; |
| ws.style.height = (state.h * scale + 40) + 'px'; |
| } |
| window.onresize = fit; |
| |
| function toggleTool(toolName) { |
| document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active-tool')); |
| document.getElementById('btn-'+toolName)?.classList.add('active-tool'); |
| document.querySelectorAll('.tool-section').forEach(s => s.style.display = 'none'); |
| document.getElementById('section-'+toolName).style.display = 'block'; |
| toolsContainer.classList.add('open'); |
| } |
| |
| async function startUpload() { |
| const f = document.getElementById('fileIn').files[0]; if(!f) return; |
| document.getElementById('loader').style.display = 'flex'; |
| const fd = new FormData(); fd.append('file', f); |
| try { |
| const r = await fetch('/api/upload', {method:'POST', body:fd}); |
| const d = await r.json(); |
| state.id = d.file_id; state.w = d.width; state.h = d.height; |
| state.segs = d.segments.map(s => ({...s, isHidden: false})); |
| v.src = d.url; |
| renderSegList(); |
| document.getElementById('uploadScreen').style.display='none'; |
| fit(); upd(); v.play(); togglePlayIcon(true); |
| } catch(e) { alert("خطا در آپلود"); } finally { document.getElementById('loader').style.display='none'; } |
| } |
| |
| function renderSegList() { |
| const container = document.getElementById('subCardsContainer'); container.innerHTML = ''; |
| state.segs.forEach((s, i) => { |
| const card = document.createElement('div'); card.className = 'sub-card'; card.id = 'seg-card-' + i; |
| card.innerHTML = `<div style="display:flex; justify-content:space-between; align-items:center;"> |
| <span class="sub-time">${formatTimeSimple(s.start)}</span> |
| <div class="sub-text-content">${s.text}</div> |
| <div><button class="mini-btn" onclick="playSeg(event, ${i})"><i class="fa-solid fa-play"></i></button></div> |
| </div>`; |
| card.onclick = () => { manualOverride = true; selectSegment(i); v.currentTime = s.start + 0.01; if(v.paused) showTextOverlay(i); else v.pause(); }; |
| container.appendChild(card); |
| }); |
| } |
| |
| function playSeg(e, i) { e.stopPropagation(); v.currentTime = state.segs[i].start; stopAtTime = state.segs[i].end; v.play(); togglePlayIcon(true); } |
| |
| function formatTimeSimple(sec) { let m = Math.floor(sec/60), s = Math.floor(sec%60); return `${m}:${s<10?'0'+s:s}`; } |
| function selectSegment(i) { document.querySelectorAll('.sub-card').forEach(el => el.classList.remove('active')); document.getElementById('seg-card-'+i)?.classList.add('active'); curSeg = i; } |
| function showTextOverlay(i) { if(i===-1) tEl.style.opacity=0; else { tEl.innerText=state.segs[i].text; tEl.style.opacity=1; } } |
| |
| v.ontimeupdate = () => { |
| if(stopAtTime && v.currentTime >= stopAtTime) { v.pause(); stopAtTime = null; togglePlayIcon(false); return; } |
| if(manualOverride && v.paused) return; if(!v.paused) manualOverride=false; |
| const idx = state.segs.findIndex(s => v.currentTime >= s.start && v.currentTime <= s.end); |
| if(idx !== -1) { if(curSeg!==idx) selectSegment(idx); tEl.style.opacity=1; tEl.innerText=state.segs[idx].text; } |
| else { if(!manualOverride) { curSeg=-1; document.querySelectorAll('.sub-card').forEach(el=>el.classList.remove('active')); tEl.style.opacity=0; } } |
| }; |
| |
| function upd() { |
| state.st.fz = parseInt(document.getElementById('fz').value); |
| state.st.y = parseInt(document.getElementById('pos').value); |
| state.st.col = document.getElementById('col').value; |
| state.st.bg = document.getElementById('bgCol').value; |
| state.st.radius = parseInt(document.getElementById('radius').value); |
| document.getElementById('radiusRow').style.display = (state.st.type !== 'outline') ? 'flex' : 'none'; |
| |
| let font = 'Vazirmatn'; |
| if(state.st.f === 'lalezar') font = 'Lalezar'; |
| if(state.st.f === 'bangers') font = 'Impact'; |
| if(state.st.f === 'roboto') font = 'Arial'; |
| tEl.style.fontFamily = font; |
| tEl.style.fontSize = state.st.fz + 'px'; |
| tEl.style.bottom = state.st.y + 'px'; |
| tEl.style.color = state.st.col; |
| |
| tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; tEl.style.backgroundColor = 'transparent'; tEl.style.borderRadius = '0px'; |
| |
| if(state.st.type === 'solid') { |
| tEl.style.backgroundColor = state.st.bg; tEl.style.borderRadius = state.st.radius + 'px'; |
| } else if (state.st.type === 'transparent') { |
| tEl.style.backgroundColor = hexToRgba(state.st.bg, 0.5); tEl.style.borderRadius = state.st.radius + 'px'; |
| } else if (state.st.type === 'outline') { |
| const s = Math.max(1, state.st.fz * 0.04); |
| tEl.style.webkitTextStroke = `${s}px ${state.st.bg}`; tEl.style.textShadow = `0 0 ${s}px ${state.st.bg}`; |
| } |
| } |
| |
| function syncModeButtons() { document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); if (state.st.type !== 'none') { document.querySelector(`.mode-btn[onclick="setMode('${state.st.type}')"]`)?.classList.add('active'); } } |
| function togglePlay() { if(v.paused) { stopAtTime=null; v.play(); togglePlayIcon(true); } else { v.pause(); togglePlayIcon(false); } } |
| function togglePlayIcon(isPlaying) { document.getElementById('playOverlay').className = isPlaying ? 'playing' : ''; } |
| function setFont(f, el) { document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); el.classList.add('ticked'); state.st.f = f; upd(); } |
| function setMode(m) { state.st.type = m; syncModeButtons(); upd(); } |
| |
| async function render() { |
| |
| state.st.fz = parseInt(document.getElementById('fz').value); |
| state.st.y = parseInt(document.getElementById('pos').value); |
| state.st.col = document.getElementById('col').value; |
| state.st.bg = document.getElementById('bgCol').value; |
| |
| v.pause(); togglePlayIcon(false); document.getElementById('loader').style.display='flex'; |
| const activeSegments = state.segs.filter(s => !s.isHidden); |
| const pl = { file_id: state.id, segments: activeSegments, video_width: state.w, video_height: state.h, style: { font: state.st.f, fontSize: state.st.fz, primaryColor: state.st.col, outlineColor: state.st.bg, backType: state.st.type, marginV: state.st.y, x: state.st.x, name: state.st.name } }; |
| try { |
| const r = await fetch('/api/render', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(pl)}); |
| const d = await r.json(); |
| const resVid = document.getElementById('resultVideo'); |
| resVid.src = d.url + "?t=" + new Date().getTime(); |
| document.getElementById('downloadBtn').href = d.url; |
| document.getElementById('resultScreen').style.display='flex'; |
| resVid.play(); |
| } catch(e) { alert('خطا در ساخت ویدیو'); } finally { document.getElementById('loader').style.display='none'; } |
| } |
| |
| function closeResult() { document.getElementById('resultScreen').style.display='none'; const rv = document.getElementById('resultVideo'); rv.pause(); rv.src = ""; } |
| |
| |
| let initialY=0, initialX=0, initialBottom=0, initialXState=0, initialDist=0, initialFontSize=0, touchMode=null; |
| tEl.addEventListener('touchstart', (e) => { if(e.touches.length === 1) { touchMode = 'drag'; initialY = e.touches[0].clientY; initialX = e.touches[0].clientX; initialBottom = state.st.y; initialXState = state.st.x; } else if (e.touches.length === 2) { touchMode = 'pinch'; initialDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); initialFontSize = state.st.fz; } }, {passive: false}); |
| tEl.addEventListener('touchmove', (e) => { if (!touchMode) return; e.preventDefault(); if (touchMode === 'drag' && e.touches.length === 1) { let newBottom = initialBottom + ((initialY - e.touches[0].clientY) * 2); state.st.y = Math.round(newBottom); document.getElementById('pos').value = state.st.y; let diffX = (e.touches[0].clientX - initialX) * 1.5; state.st.x = Math.round(initialXState + diffX); upd(); } else if (touchMode === 'pinch' && e.touches.length === 2) { let dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); let newSize = initialFontSize * (dist / initialDist); state.st.fz = Math.round(Math.max(5, newSize)); document.getElementById('fz').value = state.st.fz; upd(); } }, {passive: false}); |
| tEl.addEventListener('touchend', () => touchMode = null); |
| </script> |
| </body> |
| </html> |