Spaces:
Sleeping
Sleeping
| // منطق اصلی ادیتور + Color Picker + Trimmer Logic | |
| // ========================== | |
| // --- Editor Core Logic --- | |
| // ========================== | |
| // متغیرهای پیکر رنگ | |
| let pickerState = { h: 0, s: 0, v: 100, a: 100 }; | |
| let currentTarget = null; | |
| let savedColors = []; | |
| let isDraggingSpectrum = false; | |
| let currentDeleteIndex = -1; | |
| let toastTimeout; | |
| // متغیرهای Trimmer (ویرایش زمان دقیق) | |
| let trimMinLimit = 0; // پایان کلمه قبل | |
| let trimMaxLimit = 0; // شروع کلمه بعد | |
| let trimViewDuration = 0; | |
| let activeDragHandle = null; | |
| // آپدیت گرافیکی اسلایدرها | |
| function updateRange(input, labelId, unit = '%') { | |
| const label = document.getElementById(labelId); | |
| if (label) label.innerText = input.value + unit; | |
| const min = parseFloat(input.min); | |
| const max = parseFloat(input.max); | |
| const val = parseFloat(input.value); | |
| const percentage = ((val - min) / (max - min)) * 100; | |
| input.style.backgroundSize = percentage + '% 100%'; | |
| } | |
| function syncUIWithState() { | |
| let fzPercent = ((state.st.fz - 10) / 140) * 100; | |
| const fzInput = document.getElementById('fz'); | |
| if(fzInput) { fzInput.value = Math.round(Math.max(0, Math.min(100, fzPercent))); updateRange(fzInput, 'lbl-size'); } | |
| let yPercent = (state.st.y / 1200) * 100; | |
| const posInput = document.getElementById('pos'); | |
| if(posInput) { posInput.value = Math.round(Math.max(0, Math.min(100, yPercent))); updateRange(posInput, 'lbl-y'); } | |
| let currentX = state.st.x || 0; | |
| let xPercent = (currentX / 500) * 100; | |
| const posXInput = document.getElementById('posX'); | |
| if(posXInput) { posXInput.value = Math.round(Math.max(-100, Math.min(100, xPercent))); updateRange(posXInput, 'lbl-x'); } | |
| const radiusInput = document.getElementById('radius'); | |
| if(radiusInput) { radiusInput.value = state.st.radius || 16; updateRange(radiusInput, 'lbl-radius', 'px'); } | |
| const paddingXInput = document.getElementById('paddingX'); | |
| if(paddingXInput) { paddingXInput.value = state.st.paddingX || 20; updateRange(paddingXInput, 'lbl-paddingX', 'px'); } | |
| const paddingYInput = document.getElementById('paddingY'); | |
| if(paddingYInput) { paddingYInput.value = state.st.paddingY || 10; updateRange(paddingYInput, 'lbl-paddingY', 'px'); } | |
| } | |
| function openProject(projectId) { | |
| currentProjectId = projectId; | |
| const tx = db.transaction("projects", "readonly"); | |
| const store = tx.objectStore("projects"); | |
| const req = store.get(projectId); | |
| req.onsuccess = (e) => { | |
| const p = e.target.result; | |
| if (!p) { alert('پروژه یافت نشد'); loadHome(); return; } | |
| state = p.state; | |
| if (state.st.radius === undefined) state.st.radius = 16; | |
| if (state.st.paddingX === undefined) state.st.paddingX = 20; | |
| if (state.st.paddingY === undefined) state.st.paddingY = 10; | |
| document.getElementById('currentProjectTitle').innerText = p.name; | |
| try { | |
| const videoURL = URL.createObjectURL(p.videoBlob); | |
| v.src = videoURL; | |
| } catch(err) { | |
| console.error("Error creating video URL:", err); | |
| alert("خطا در بارگذاری ویدیو."); | |
| return; | |
| } | |
| updateColorPreviewButtons(); | |
| syncUIWithState(); | |
| document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); | |
| if(state.st.f === 'vazir') document.querySelectorAll('.font-btn')[1].classList.add('ticked'); | |
| else if(state.st.f === 'lalezar') document.querySelectorAll('.font-btn')[0].classList.add('ticked'); | |
| document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); | |
| if(state.st.name === 'karaoke_static') { | |
| const cards = document.querySelectorAll('.style-card'); | |
| cards.forEach(c => { | |
| if(c.textContent.includes('ثابت') || (c.dataset.styleName === 'karaoke_static')) c.classList.add('selected'); | |
| }); | |
| } | |
| document.getElementById('homeScreen').style.display = 'none'; | |
| document.getElementById('editorScreen').style.display = 'flex'; | |
| initSavedColors(); | |
| const palette = generateGridPalette(); | |
| renderGridWithData(palette); | |
| renderSegList(); | |
| if (v.readyState >= 1) { fit(); upd(); } | |
| else { | |
| v.onloadeddata = () => { | |
| fit(); upd(); | |
| if(!p.thumbnail && v.duration > 1) { | |
| v.currentTime = 1.0; | |
| let captured = false; | |
| v.addEventListener('seeked', function cap() { | |
| if(captured) return; captured = true; saveProjectToDB(); v.currentTime = 0; | |
| }, {once:true}); | |
| } else if (!p.thumbnail) { saveProjectToDB(); } | |
| }; | |
| } | |
| }; | |
| req.onerror = (e) => { console.error("Error opening project:", e); alert("خطا در باز کردن پروژه"); loadHome(); }; | |
| } | |
| function goHome() { if(v) v.pause(); saveProjectToDB(); loadHome(); } | |
| function fit() { | |
| // اصلاح: دریافت ابعاد از ویدیو اگر در استیت موجود نبود (حل مشکل نمایش و خروجی) | |
| const vw = (state.w && state.w > 0) ? state.w : v.videoWidth; | |
| const vh = (state.h && state.h > 0) ? state.h : v.videoHeight; | |
| if(!vw || !v) return; | |
| // ذخیره ابعاد اصلاح شده در استیت برای جلوگیری از ارسال عدد 0 به سرور | |
| if(!state.w || state.w === 0) { state.w = vw; state.h = vh; } | |
| const ws = document.getElementById('workspace'); | |
| const availableHeight = window.innerHeight * 0.6; | |
| const scale = Math.min((ws.clientWidth - 40) / vw, availableHeight / vh); | |
| const c = document.getElementById('videoContainer'); | |
| c.style.width = vw + 'px'; | |
| c.style.height = vh + 'px'; | |
| document.getElementById('scaler').style.transform = `scale(${scale})`; | |
| ws.style.height = (vh * scale + 40) + 'px'; | |
| } | |
| function activateCustomStyle() { | |
| if(['karaoke_static', 'auto_director', 'plain_white', 'white_outline'].includes(state.st.name)) { | |
| state.st.name = 'classic'; | |
| document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); | |
| const classicCard = document.querySelector('.custom-style-container .style-card:first-child'); | |
| if(classicCard) classicCard.classList.add('selected'); | |
| } | |
| } | |
| function renderSegList() { | |
| saveProjectToDB(); | |
| const timeline = document.getElementById('timelineScroll'); | |
| const spacers = timeline.querySelectorAll('.spacer'); | |
| timeline.innerHTML = ''; | |
| timeline.appendChild(spacers[0]); | |
| if(!state.segs) state.segs = []; | |
| state.segs.forEach((seg, sIdx) => { | |
| if (!seg.words || seg.words.length === 0) { | |
| const wordsArr = seg.text.trim().split(/\s+/).filter(w => w.length > 0); | |
| const duration = seg.end - seg.start; | |
| const timePerWord = duration / Math.max(1, wordsArr.length); | |
| let wStart = seg.start; | |
| seg.words = wordsArr.map((wStr, i) => { | |
| let wEnd = wStart + timePerWord; | |
| if (i === wordsArr.length - 1) wEnd = seg.end; | |
| let obj = { word: wStr, start: parseFloat(wStart.toFixed(2)), end: parseFloat(wEnd.toFixed(2)) }; | |
| wStart = wEnd; | |
| return obj; | |
| }); | |
| } | |
| seg.words.forEach((w, wIdx) => { | |
| const el = document.createElement('div'); | |
| el.className = 'word-chip'; | |
| el.innerText = w.word; | |
| const uid = `${sIdx}-${wIdx}`; | |
| el.id = `w-${uid}`; | |
| if (activeWordId === uid) el.classList.add('active'); | |
| el.onclick = (e) => { e.stopPropagation(); highlightWord(sIdx, wIdx, true); }; | |
| timeline.appendChild(el); | |
| }); | |
| if (sIdx < state.segs.length - 1) { | |
| const nl = document.createElement('div'); | |
| nl.className = 'newline-indicator'; | |
| nl.innerHTML = '<i class="fa-solid fa-arrow-turn-down" style="transform: rotate(90deg) scaleX(-1);"></i>'; | |
| timeline.appendChild(nl); | |
| } | |
| }); | |
| timeline.appendChild(spacers[1]); | |
| updateSplitButton(); | |
| } | |
| function highlightWord(sIdx, wIdx, showToolbar) { | |
| activeWordId = `${sIdx}-${wIdx}`; | |
| v.pause(); | |
| togglePlayIcon(false); | |
| if(!state.segs[sIdx] || !state.segs[sIdx].words[wIdx]) return; | |
| const word = state.segs[sIdx].words[wIdx]; | |
| v.currentTime = word.start; | |
| manualOverride = true; | |
| updateOverlayContent(v.currentTime); | |
| document.querySelectorAll('.word-chip').forEach(c => c.classList.remove('active')); | |
| const el = document.getElementById(`w-${activeWordId}`); | |
| if(el) { | |
| el.classList.add('active'); | |
| el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' }); | |
| } | |
| if(showToolbar) document.getElementById('toolbar').classList.add('show'); | |
| updateSplitButton(); | |
| } | |
| // ============================================ | |
| // --- LOGIC: Trimmer & Precise Time Playback --- | |
| // ============================================ | |
| function initTrimmerUI(startT, endT, minLimit, maxLimit) { | |
| // تنظیم محدودیتهای سراسری | |
| trimMinLimit = minLimit; | |
| trimMaxLimit = maxLimit; | |
| trimViewDuration = trimMaxLimit - trimMinLimit; | |
| // جلوگیری از خطای تقسیم بر صفر | |
| if(trimViewDuration <= 0.001) trimViewDuration = 1; | |
| // اعمال مقادیر اولیه به UI | |
| updateTrimmerVisuals(); | |
| // مخفی کردن نوار نورانی وسط (طبق درخواست) | |
| const track = document.getElementById('trackActive'); | |
| if(track) track.style.display = 'none'; | |
| // هندلرهای درگ | |
| const handleL = document.getElementById('handleLeft'); | |
| const handleR = document.getElementById('handleRight'); | |
| const startDrag = (e, type) => { | |
| if(e.cancelable) e.preventDefault(); | |
| e.stopPropagation(); | |
| activeDragHandle = type; | |
| // بزرگنمایی دکمه هنگام لمس | |
| e.target.style.transform = "scale(1.5)"; | |
| e.target.style.transition = "transform 0.1s"; | |
| e.target.style.zIndex = "100"; | |
| document.addEventListener('mousemove', onDrag); | |
| document.addEventListener('mouseup', endDrag); | |
| document.addEventListener('touchmove', onDrag, {passive: false}); | |
| document.addEventListener('touchend', endDrag); | |
| }; | |
| if(handleL) { | |
| handleL.onmousedown = (e) => startDrag(e, 'left'); | |
| handleL.ontouchstart = (e) => startDrag(e, 'left'); | |
| // استایل اولیه برای جلوگیری از تداخل | |
| handleL.style.position = 'absolute'; | |
| } | |
| if(handleR) { | |
| handleR.onmousedown = (e) => startDrag(e, 'right'); | |
| handleR.ontouchstart = (e) => startDrag(e, 'right'); | |
| handleR.style.position = 'absolute'; | |
| } | |
| } | |
| function onDrag(e) { | |
| if (!activeDragHandle) return; | |
| if(e.cancelable) e.preventDefault(); | |
| e.stopPropagation(); | |
| const strip = document.getElementById('timelineStrip'); | |
| if (!strip) return; | |
| const rect = strip.getBoundingClientRect(); | |
| // دریافت موقعیت X | |
| let clientX; | |
| if (e.touches && e.touches.length > 0) { | |
| clientX = e.touches[0].clientX; | |
| } else { | |
| clientX = e.clientX; | |
| } | |
| // محاسبه درصد از سمت چپ | |
| let percentFromLeft = (clientX - rect.left) / rect.width; | |
| percentFromLeft = Math.max(0, Math.min(1, percentFromLeft)); | |
| // تبدیل به منطق RTL (راست به چپ) | |
| // در این حالت: سمت راست (100% چپ) = شروع بازه (Min) | |
| // سمت چپ (0% چپ) = پایان بازه (Max) | |
| // فرمول معکوس: | |
| let percentRTL = 1 - percentFromLeft; | |
| // محاسبه زمان بر اساس درصد RTL | |
| let newTime = trimMinLimit + (percentRTL * trimViewDuration); | |
| newTime = Math.round(newTime * 100) / 100; | |
| if (activeDragHandle === 'right') { | |
| // دکمه سمت راست = شروع زمان (Start Time) | |
| // نباید کمتر از حد مجاز (Min) باشد | |
| if (newTime < trimMinLimit) newTime = trimMinLimit; | |
| // نباید جلوتر از پایان فعلی برود | |
| if (newTime >= tempEndTime - 0.05) newTime = tempEndTime - 0.05; | |
| tempStartTime = newTime; | |
| } else { | |
| // دکمه سمت چپ = پایان زمان (End Time) | |
| // نباید بیشتر از حد مجاز (Max) باشد | |
| if (newTime > trimMaxLimit) newTime = trimMaxLimit; | |
| // نباید عقبتر از شروع فعلی بیاید | |
| if (newTime <= tempStartTime + 0.05) newTime = tempStartTime + 0.05; | |
| tempEndTime = newTime; | |
| } | |
| updateTrimmerVisuals(); | |
| } | |
| function endDrag(e) { | |
| if(!activeDragHandle) return; | |
| // بازگشت سایز دکمه به حالت عادی | |
| const handleL = document.getElementById('handleLeft'); | |
| const handleR = document.getElementById('handleRight'); | |
| if(handleL) { handleL.style.transform = "scale(1)"; handleL.style.zIndex = "10"; } | |
| if(handleR) { handleR.style.transform = "scale(1)"; handleR.style.zIndex = "10"; } | |
| activeDragHandle = null; | |
| document.removeEventListener('mousemove', onDrag); | |
| document.removeEventListener('mouseup', endDrag); | |
| document.removeEventListener('touchmove', onDrag); | |
| document.removeEventListener('touchend', endDrag); | |
| } | |
| function updateTrimmerVisuals() { | |
| // نمایش متنی | |
| const tStartDisp = document.getElementById('trimStartDisp'); | |
| const tEndDisp = document.getElementById('trimEndDisp'); | |
| if(tStartDisp) tStartDisp.innerText = formatTimeMs(tempStartTime); | |
| if(tEndDisp) tEndDisp.innerText = formatTimeMs(tempEndTime); | |
| // محاسبه موقعیت دکمهها برای حالت RTL | |
| // دکمه راست (Start): فاصله از لبه راست کادر | |
| const percentStart = ((tempStartTime - trimMinLimit) / trimViewDuration) * 100; | |
| // دکمه چپ (End): فاصله از لبه چپ کادر | |
| // چون محور معکوس است: End Time به Max نزدیکتر است که سمت چپ قرار دارد | |
| const percentEnd = ((trimMaxLimit - tempEndTime) / trimViewDuration) * 100; | |
| const handleL = document.getElementById('handleLeft'); | |
| const handleR = document.getElementById('handleRight'); | |
| // اعمال پوزیشن | |
| if(handleR) { | |
| handleR.style.right = `${percentStart}%`; | |
| handleR.style.left = 'auto'; // حذف تداخل | |
| } | |
| if(handleL) { | |
| handleL.style.left = `${percentEnd}%`; | |
| handleL.style.right = 'auto'; // حذف تداخل | |
| } | |
| } | |
| function formatTimeMs(t) { | |
| let m = Math.floor(t / 60); | |
| let s = Math.floor(t % 60); | |
| let ms = Math.round((t - Math.floor(t)) * 100); | |
| return `${m < 10 ? '0'+m : m}:${s < 10 ? '0'+s : s}.${ms < 10 ? '0'+ms : ms}`; | |
| } | |
| // تابع پخش دقیق | |
| function playPreviewWord() { | |
| if ((tempStartTime === undefined) || (tempEndTime === undefined)) return; | |
| if(previewInterval) clearInterval(previewInterval); | |
| v.pause(); | |
| v.currentTime = tempStartTime; | |
| const icon1 = document.getElementById('btnPreviewPlay')?.querySelector('i'); | |
| if(icon1) icon1.className = "fa-solid fa-pause"; | |
| const icon2 = document.getElementById('btnTimeSheetPlayIcon'); | |
| if(icon2) icon2.className = "fa-solid fa-pause"; | |
| v.play().then(() => { | |
| previewInterval = setInterval(() => { | |
| if(v.currentTime >= tempEndTime) { | |
| v.pause(); | |
| clearInterval(previewInterval); | |
| if(icon1) icon1.className = "fa-solid fa-play"; | |
| if(icon2) icon2.className = "fa-solid fa-play"; | |
| v.currentTime = tempEndTime; | |
| } | |
| }, 15); | |
| }).catch(e => console.log("Play interrupted", e)); | |
| } | |
| function saveText() { | |
| if (!activeWordId) return; | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| const val = document.getElementById('textInput').value.trim(); | |
| if (val) { | |
| state.segs[sIdx].words[wIdx].word = val; | |
| state.segs[sIdx].text = state.segs[sIdx].words.map(w => w.word).join(' '); | |
| renderSegList(); | |
| highlightWord(sIdx, wIdx, true); | |
| } | |
| closeAllSheets(); | |
| } | |
| function adjustTime(type, am) { | |
| // اصلاح منطق دکمههای مثبت و منفی برای حالت RTL | |
| if(type === 'start') { | |
| let v = parseFloat((tempStartTime + am).toFixed(2)); | |
| if(v < trimMinLimit) v = trimMinLimit; | |
| if(v >= tempEndTime - 0.05) v = parseFloat((tempEndTime - 0.05).toFixed(2)); | |
| tempStartTime = v; | |
| } else { | |
| let v = parseFloat((tempEndTime + am).toFixed(2)); | |
| if(v <= tempStartTime + 0.05) v = parseFloat((tempStartTime + 0.05).toFixed(2)); | |
| if(v > trimMaxLimit) v = trimMaxLimit; | |
| tempEndTime = v; | |
| } | |
| updateTrimmerVisuals(); | |
| } | |
| function confirmTimeChanges() { | |
| if (!activeWordId) return; | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| const seg = state.segs[sIdx]; | |
| seg.words[wIdx].start = tempStartTime; | |
| seg.words[wIdx].end = tempEndTime; | |
| if (wIdx === 0 && tempStartTime < seg.start) seg.start = tempStartTime; | |
| if (wIdx === seg.words.length - 1 && tempEndTime > seg.end) seg.end = tempEndTime; | |
| closeAllSheets(); | |
| renderSegList(); | |
| highlightWord(sIdx, wIdx, true); | |
| } | |
| function cancelTimeChanges() { closeAllSheets(); } | |
| function confirmDeleteWord() { | |
| if (!activeWordId) return; | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| const seg = state.segs[sIdx]; | |
| seg.words.splice(wIdx, 1); | |
| if (seg.words.length === 0) { state.segs.splice(sIdx, 1); activeWordId = null; document.getElementById('toolbar').classList.remove('show'); } | |
| else { seg.text = seg.words.map(w => w.word).join(' '); activeWordId = null; document.getElementById('toolbar').classList.remove('show'); } | |
| closeAllSheets(); renderSegList(); | |
| } | |
| function toggleSplit() { | |
| if (!activeWordId) return; | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| if (wIdx === 0 && sIdx > 0) { | |
| const prevSeg = state.segs[sIdx - 1]; const currSeg = state.segs[sIdx]; | |
| prevSeg.words = prevSeg.words.concat(currSeg.words); prevSeg.end = currSeg.end; | |
| prevSeg.text = prevSeg.words.map(w => w.word).join(' '); state.segs.splice(sIdx, 1); | |
| const newWIdx = prevSeg.words.length - currSeg.words.length; | |
| renderSegList(); highlightWord(sIdx - 1, newWIdx, true); | |
| } else { | |
| const seg = state.segs[sIdx]; const wordsFirstHalf = seg.words.slice(0, wIdx); const wordsSecondHalf = seg.words.slice(wIdx); | |
| seg.words = wordsFirstHalf; seg.text = seg.words.map(w => w.word).join(' '); seg.end = wordsFirstHalf[wordsFirstHalf.length-1].end; | |
| const newSeg = { id: Date.now().toString() + "_" + Math.floor(Math.random() * 1000), text: wordsSecondHalf.map(w => w.word).join(' '), start: wordsSecondHalf[0].start, end: wordsSecondHalf[wordsSecondHalf.length-1].end, words: wordsSecondHalf, isHidden: false }; | |
| state.segs.splice(sIdx + 1, 0, newSeg); | |
| renderSegList(); highlightWord(sIdx + 1, 0, true); | |
| } | |
| } | |
| function updateSplitButton() { | |
| const btn = document.getElementById('btnSplit'); if(!activeWordId) { btn.classList.remove('active-state'); return; } | |
| const [sIdx, wIdx] = activeWordId.split('-').map(Number); | |
| if (wIdx === 0 && sIdx > 0) { btn.classList.add('active-state'); btn.style.transform = "rotate(180deg)"; } else { btn.classList.remove('active-state'); btn.style.transform = ""; } | |
| } | |
| function togglePlay() { if(v.paused) { v.play(); togglePlayIcon(true); } else { v.pause(); togglePlayIcon(false); } } | |
| function togglePlayIcon(isPlaying) { const overlay = document.getElementById('playOverlay'); overlay.className = isPlaying ? 'playing' : ''; } | |
| function updateOverlayContent(currentTime) { | |
| if (!state.segs) return; | |
| const idx = state.segs.findIndex(s => currentTime >= s.start && currentTime <= s.end); | |
| if(idx !== -1) { | |
| const seg = state.segs[idx]; | |
| if(seg.isHidden) { tEl.style.opacity = 0; } | |
| else { | |
| tEl.style.opacity = 1; | |
| const karaokeStyles = ['auto_director', 'karaoke_static']; | |
| if(karaokeStyles.includes(state.st.name) && seg.words) { | |
| let html = ""; | |
| // فاصله استاندارد بین کلمات (ثابت) | |
| const GAP = 5; | |
| seg.words.forEach((w, i) => { | |
| let isActive = (currentTime >= w.start && currentTime <= w.end); | |
| let boxColor = (state.st.name === 'auto_director') | |
| ? (i % 2 === 0 ? '#00D7FF' : '#FF0080') | |
| : state.st.col; | |
| let textColor = '#ffffff'; | |
| if (w.color) textColor = w.color; | |
| // استایل پایه: اینلاین-بلاک برای اعمال پدینگ/مارجین، و تراز عمودی وسط | |
| let baseStyle = "display: inline-block; vertical-align: middle; transition: all 0.1s; font-family: inherit;"; | |
| if(isActive) { | |
| // تکنیک حاشیه منفی: | |
| // 1. padding: کادر را بزرگ میکند. | |
| // 2. margin منفی: اثر padding را خنثی میکند تا بقیه کلمات هل داده نشوند. | |
| // فرمول افقی: فاصله استاندارد (GAP) منهای پدینگ تنظیمی | |
| // فرمول عمودی: منفی پدینگ تنظیمی (برای جلوگیری از پریدن خط بالا/پایین) | |
| let py = state.st.paddingY; | |
| let px = state.st.paddingX; | |
| html += `<span style="${baseStyle} | |
| background-color: ${boxColor}; | |
| color: ${textColor} !important; | |
| border-radius: ${state.st.radius}px; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.3); | |
| padding: ${py}px ${px}px; | |
| margin: -${py}px calc(${GAP}px - ${px}px); | |
| position: relative; | |
| z-index: 100; | |
| white-space: nowrap;">${w.word}</span>`; | |
| } else { | |
| // کلمات غیرفعال: فقط فاصله استاندارد را دارند | |
| html += `<span style="${baseStyle} | |
| color: ${textColor} !important; | |
| text-shadow: none; | |
| margin: 0 ${GAP}px; | |
| padding: 0; | |
| position: relative; | |
| z-index: 1;">${w.word}</span>`; | |
| } | |
| // شکستن خطوط | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { | |
| html += "<br><br>"; | |
| } | |
| }); | |
| tEl.innerHTML = html; | |
| } | |
| else if (state.st.name === 'progressive_write' && seg.words) { | |
| let html = ""; | |
| seg.words.forEach((w, i) => { | |
| let styleExtra = ""; | |
| if (w.color) styleExtra = `color:${w.color} !important;`; | |
| // تغییر ۱: اضافه کردن دیسپلی اینلاین-بلاک و مارجین برای جلوگیری از چسبیدن | |
| let baseStyle = "display: inline-block; vertical-align: middle; margin: 0px 3px;"; | |
| if(currentTime >= w.start) html += `<span style="${baseStyle} opacity:1; ${styleExtra}">${w.word}</span> `; | |
| else html += `<span style="${baseStyle} opacity:0; ${styleExtra}">${w.word}</span> `; | |
| // تغییر ۲: استفاده از دو عدد اینتر (<br><br>) برای ایجاد فاصله بین خطوط بالا و پایین | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { | |
| html += "<br><br>"; | |
| } | |
| }); | |
| tEl.innerHTML = html.trim(); | |
| } | |
| else { | |
| // For static blocks without word-level highlights in data, mostly fallback or manual override | |
| // We'll rely on text segmentation. If words array exists, use it to break lines | |
| if (seg.words && seg.words.length > 0) { | |
| let html = ""; | |
| seg.words.forEach((w, i) => { | |
| let styleExtra = ""; | |
| if (w.color) styleExtra = `color:${w.color} !important;`; | |
| html += `<span style="${styleExtra}">${w.word}</span> `; | |
| if ((i + 1) % 5 === 0 && i !== seg.words.length - 1) { | |
| html += "<br><br>"; | |
| } | |
| }); | |
| tEl.innerHTML = html; | |
| } else { | |
| tEl.innerText = seg.text; | |
| } | |
| } | |
| } | |
| } else { tEl.style.opacity = 0; } | |
| } | |
| function upd() { | |
| saveProjectToDB(); | |
| state.st.fz = Math.round(10 + (parseFloat(document.getElementById('fz').value) / 100) * 140); | |
| state.st.y = Math.round((parseFloat(document.getElementById('pos').value) / 100) * 1200); | |
| state.st.x = Math.round((parseFloat(document.getElementById('posX').value) / 100) * 500); | |
| state.st.radius = parseInt(document.getElementById('radius').value, 10); | |
| state.st.paddingX = parseInt(document.getElementById('paddingX').value, 10); | |
| state.st.paddingY = parseInt(document.getElementById('paddingY').value, 10); | |
| updateColorPreviewButtons(); | |
| 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.lineHeight = '0.8'; tEl.style.bottom = state.st.y + 'px'; | |
| tEl.style.textAlign = 'center'; tEl.style.left = '50%'; tEl.style.transform = `translateX(calc(-50% + ${state.st.x}px))`; | |
| tEl.style.paintOrder = 'normal'; tEl.style.webkitPaintOrder = 'normal'; | |
| // Remove fixed padding here because we might handle it per word in karaoke | |
| tEl.style.padding = '0'; | |
| tEl.style.borderRadius = '0px'; | |
| const karaokeStyles = ['karaoke_static', 'auto_director']; | |
| if(karaokeStyles.includes(state.st.name)) { | |
| tEl.style.backgroundColor = 'transparent'; tEl.style.color = '#FFFFFF'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; | |
| } else if (state.st.name === 'plain_white' || state.st.name === 'white_outline') { | |
| tEl.style.color = '#FFFFFF'; tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = (state.st.name === 'white_outline') ? `${Math.max(3, state.st.fz / 4.5)}px #000000` : '0px'; tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; tEl.style.textShadow = 'none'; | |
| } else { | |
| if(!state.st.col) state.st.col = '#FFFFFF'; if(!state.st.bg) state.st.bg = '#000000'; | |
| tEl.style.color = state.st.col; | |
| // Apply box style to the container | |
| tEl.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`; | |
| tEl.style.borderRadius = `${state.st.radius}px`; | |
| if(state.st.type === 'solid' || state.st.type === 'transparent') { | |
| let bgColor = state.st.bg; | |
| if (state.st.type === 'transparent') { | |
| if(bgColor.startsWith('#') && bgColor.length === 7) { | |
| let r = parseInt(bgColor.substring(1, 3), 16); let g = parseInt(bgColor.substring(3, 5), 16); let b = parseInt(bgColor.substring(5, 7), 16); | |
| bgColor = `rgba(${r},${g},${b},0.6)`; | |
| } else if(bgColor.startsWith('rgba')) { | |
| bgColor = bgColor.replace(/[\d.]+\)$/, '0.6)'); | |
| } | |
| } | |
| tEl.style.backgroundColor = bgColor; | |
| tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; | |
| } else if (state.st.type === 'outline') { | |
| tEl.style.backgroundColor = 'transparent'; | |
| const s = Math.max(3, state.st.fz / 4.5); tEl.style.webkitTextStroke = `${s}px ${state.st.bg}`; | |
| tEl.style.paintOrder = 'stroke fill'; tEl.style.webkitPaintOrder = 'stroke fill'; | |
| tEl.style.textShadow = 'none'; | |
| } else { | |
| tEl.style.backgroundColor = 'transparent'; tEl.style.webkitTextStroke = '0px'; tEl.style.textShadow = 'none'; | |
| } | |
| } | |
| const boxPreview = document.getElementById('boxPreview'); | |
| const boxControlsPanel = document.getElementById('boxControlsPanel'); | |
| if (boxControlsPanel) { | |
| const isBoxStyleActive = ['solid', 'transparent'].includes(state.st.type) || ['karaoke_static', 'auto_director'].includes(state.st.name); | |
| boxControlsPanel.style.display = isBoxStyleActive ? 'block' : 'none'; | |
| if (isBoxStyleActive && boxPreview) { | |
| boxPreview.style.borderRadius = `${state.st.radius}px`; | |
| boxPreview.style.padding = `${state.st.paddingY}px ${state.st.paddingX}px`; | |
| let previewColor = state.st.name === 'karaoke_static' ? state.st.col : state.st.bg; | |
| boxPreview.style.backgroundColor = previewColor; | |
| } | |
| } | |
| } | |
| function resegmentByWordCount(count) { | |
| let allWords = []; | |
| state.segs.forEach(s => { | |
| if(s.words) allWords.push(...s.words); | |
| }); | |
| if(allWords.length === 0) return; | |
| let newSegs = []; | |
| for (let i = 0; i < allWords.length; i += count) { | |
| let chunk = allWords.slice(i, i + count); | |
| let start = chunk[0].start; | |
| let end = chunk[chunk.length - 1].end; | |
| let text = chunk.map(w => w.word).join(' '); | |
| newSegs.push({ | |
| id: Date.now() + i, | |
| start: start, | |
| end: end, | |
| text: text, | |
| words: chunk, | |
| isHidden: false | |
| }); | |
| } | |
| state.segs = newSegs; | |
| renderSegList(); | |
| upd(); | |
| saveProjectToDB(); | |
| closeAllSheets(); | |
| showToast(`هر جمله شامل ${count} کلمه شد`, "fa-solid fa-list-ol"); | |
| } | |
| function updateColorPreviewButtons() { | |
| const mainBtn = document.getElementById('preview-main-btn'); | |
| if(mainBtn && state.st.col) mainBtn.style.backgroundColor = state.st.col; | |
| const bgBtn = document.getElementById('preview-bg-btn'); | |
| if(bgBtn && state.st.bg) bgBtn.style.backgroundColor = state.st.bg; | |
| } | |
| function setStylePreset(name, el, skipModeSet = false) { | |
| state.st.name = name; | |
| document.querySelectorAll('.style-card').forEach(c => c.classList.remove('selected')); | |
| if(el) el.classList.add('selected'); | |
| if(name === 'karaoke_static' && !skipModeSet) { | |
| const defaultPurple = '#A020F0'; state.st.col = defaultPurple; | |
| document.documentElement.style.setProperty('--static-color', defaultPurple); | |
| } | |
| if(name === 'classic' && !skipModeSet) setMode('solid'); | |
| else if (name === 'progressive_write') setMode('none'); | |
| else if (name === 'plain_white' || name === 'white_outline') { | |
| state.st.col = '#FFFFFF'; state.st.bg = '#000000'; | |
| setMode('outline'); | |
| } | |
| upd(); | |
| } | |
| function handleClassicDoubleClick(element) { | |
| event.stopPropagation(); | |
| activateCustomStyle(); | |
| setStylePreset('classic', element, true); | |
| setMode('none'); | |
| } | |
| 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(); } | |
| function openColorPicker(target, ev) { | |
| if (ev) ev.stopPropagation(); | |
| currentTarget = target; | |
| document.getElementById('pickerBackdrop').classList.add('active'); | |
| document.getElementById('colorPickerModal').classList.add('active'); | |
| let hex = '#FFFFFF'; | |
| if(target === 'main') hex = state.st.col; | |
| else if(target === 'bg') hex = state.st.bg; | |
| else if(target === 'static') hex = state.st.col; | |
| else if(target === 'word') { | |
| if(activeWordId) { | |
| const [s, w] = activeWordId.split('-').map(Number); | |
| const wordObj = state.segs[s].words[w]; | |
| hex = wordObj.color || state.st.col; | |
| } else { | |
| hex = state.st.col; | |
| } | |
| } | |
| let parsed = parseColorStringToState(hex || '#FFFFFF'); | |
| pickerState = parsed; | |
| switchTab('spectrum'); | |
| syncAllUI(); | |
| syncGridSelectedFromCurrentColor(); | |
| } | |
| function closePicker() { document.getElementById('colorPickerModal').classList.remove('active'); document.getElementById('pickerBackdrop').classList.remove('active'); } | |
| function saveAndClosePicker() { | |
| const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); | |
| let colorStr; | |
| if (pickerState.a >= 100) { colorStr = rgbToHex(rgb.r, rgb.g, rgb.b); } else { colorStr = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${pickerState.a/100})`; } | |
| if (currentTarget === 'main') state.st.col = colorStr; | |
| else if (currentTarget === 'bg') state.st.bg = colorStr; | |
| else if (currentTarget === 'static') { state.st.col = colorStr; document.documentElement.style.setProperty('--static-color', colorStr); } | |
| else if (currentTarget === 'word' && activeWordId) { | |
| const [s, w] = activeWordId.split('-').map(Number); | |
| state.segs[s].words[w].color = colorStr; | |
| updateOverlayContent(v.currentTime); // آپدیت آنی پیشنمایش | |
| } | |
| upd(); | |
| closePicker(); | |
| } | |
| const spectrumAreaP = document.getElementById('spectrumArea'); const spectrumHandleP = document.getElementById('spectrumHandle'); const slBrightP = document.getElementById('brightness-slider'); const slAlphaP = document.getElementById('alpha-slider'); const previewBoxP = document.getElementById('largeColorPreview'); const gridContP = document.getElementById('gridContainer'); | |
| function switchTab(name) { document.querySelectorAll('.picker-tab').forEach(t => t.classList.remove('active')); document.getElementById('tab-' + name).classList.add('active'); document.querySelectorAll('.view-section').forEach(v => v.classList.remove('active-view')); document.getElementById('view-' + name).classList.add('active-view'); const brRow = document.getElementById('brightness-row'); if(brRow) brRow.style.display = (name === 'manual') ? 'none' : 'flex'; syncAllUI(); if(name === 'grid') syncGridSelectedFromCurrentColor(); } | |
| function handleSpectrum(e) { if(!spectrumAreaP) return; const rect = spectrumAreaP.getBoundingClientRect(); let x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); let y = Math.max(0, Math.min(e.clientY - rect.top, rect.height)); spectrumHandleP.style.left = x + 'px'; spectrumHandleP.style.top = y + 'px'; pickerState.h = (x / rect.width) * 360; pickerState.s = 100 - ((y / rect.height) * 100); syncAllUI(); syncGridSelectedFromCurrentColor(); } | |
| if(spectrumAreaP) { spectrumAreaP.addEventListener('mousedown', e => { isDraggingSpectrum = true; handleSpectrum(e); }); window.addEventListener('mousemove', e => { if(isDraggingSpectrum) handleSpectrum(e); }); window.addEventListener('mouseup', () => isDraggingSpectrum = false); spectrumAreaP.addEventListener('touchstart', e => { isDraggingSpectrum = true; handleSpectrum(e.touches[0]); }, { passive:false }); window.addEventListener('touchmove', e => { if(isDraggingSpectrum) { e.preventDefault(); handleSpectrum(e.touches[0]); } }, { passive:false }); window.addEventListener('touchend', () => isDraggingSpectrum = false); } | |
| function updateBrightness() { pickerState.v = parseInt(slBrightP.value, 10); syncAllUI(); syncGridSelectedFromCurrentColor(); } | |
| function updateAlpha() { pickerState.a = parseInt(slAlphaP.value, 10); syncAllUI(); } | |
| function updateRGBFromSliders() { const r = parseInt(document.getElementById('slider-r').value, 10); const g = parseInt(document.getElementById('slider-g').value, 10); const b = parseInt(document.getElementById('slider-b').value, 10); updateFromRGB(r, g, b); } | |
| function updateRGBFromInputs() { const r = Math.min(255, Math.max(0, parseInt(document.getElementById('input-r').value, 10) || 0)); const g = Math.min(255, Math.max(0, parseInt(document.getElementById('input-g').value, 10) || 0)); const b = Math.min(255, Math.max(0, parseInt(document.getElementById('input-b').value, 10) || 0)); updateFromRGB(r, g, b); } | |
| function updateFromRGB(r, g, b) { const hsv = rgbToHsv(r, g, b); pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v; syncAllUI(); syncGridSelectedFromCurrentColor(); } | |
| function updateFromHexInput() { let val = document.getElementById('input-hex').value.trim(); if(val.startsWith('#')) val = val.substring(1); if(/^[0-9A-Fa-f]{6}$/.test(val) || /^[0-9A-Fa-f]{3}$/.test(val)) { const rgb = hexToRgb('#'+val); const hsv = rgbToHsv(rgb.r,rgb.g,rgb.b); pickerState.h = hsv.h; pickerState.s = hsv.s; pickerState.v = hsv.v; pickerState.a = 100; syncAllUI(); syncGridSelectedFromCurrentColor(); } } | |
| function syncAllUI() { const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); const rgba = `rgba(${rgb.r},${rgb.g},${rgb.b},${pickerState.a/100})`; const slR = document.getElementById('slider-r'), inR = document.getElementById('input-r'); const slG = document.getElementById('slider-g'), inG = document.getElementById('input-g'); const slB = document.getElementById('slider-b'), inB = document.getElementById('input-b'); const inHex = document.getElementById('input-hex'); slR.value = inR.value = rgb.r; slG.value = inG.value = rgb.g; slB.value = inB.value = rgb.b; if(document.activeElement !== inHex) inHex.value = hex.replace('#','').toUpperCase(); slR.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))`); slG.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))`); slB.style.setProperty('--track-bg', `linear-gradient(90deg, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))`); slBrightP.value = Math.round(pickerState.v); document.getElementById('disp-brightness').innerText = Math.round(pickerState.v); const brightColor = hsvToRgb(pickerState.h, pickerState.s, 100); slBrightP.style.setProperty('--track-bg', `linear-gradient(90deg, #000000, rgb(${brightColor.r},${brightColor.g},${brightColor.b}))`); slAlphaP.value = Math.round(pickerState.a); document.getElementById('disp-alpha').innerText = Math.round(pickerState.a); slAlphaP.style.setProperty('--track-bg', `linear-gradient(90deg, rgba(${rgb.r},${rgb.g},${rgb.b},0), rgba(${rgb.r},${rgb.g},${rgb.b},1))`); if(previewBoxP) { previewBoxP.style.background = `linear-gradient(0deg, ${rgba}, ${rgba}), conic-gradient(#dedede 0.25turn, #ffffff 0.25turn 0.5turn, #dedede 0.5turn 0.75turn, #ffffff 0.75turn) top left / 16px 16px`; } if(!isDraggingSpectrum && spectrumHandleP) { spectrumHandleP.style.left = (pickerState.h / 360 * 100) + '%'; spectrumHandleP.style.top = (100 - pickerState.s) + '%'; } } | |
| function rgbToHex(r,g,b) { r=Math.round(r);g=Math.round(g);b=Math.round(b); return "#" + ((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1).toUpperCase(); } | |
| function hexToRgb(hex) { hex = (hex||'#000').replace('#','').trim(); if(hex.length===3) hex=hex.split('').map(c=>c+c).join(''); if(hex.length!==6) return {r:0,g:0,b:0}; return {r:parseInt(hex.substring(0,2),16),g:parseInt(hex.substring(2,4),16),b:parseInt(hex.substring(4,6),16)}; } | |
| function hsvToRgb(h,s,v){ s/=100;v/=100; let c=v*s; let x=c*(1-Math.abs(((h/60)%2)-1)); let m=v-c; let r=0,g=0,b=0; if(h<60){r=c;g=x;}else if(h<120){r=x;g=c;}else if(h<180){g=c;b=x;}else if(h<240){g=x;b=c;}else if(h<300){r=x;b=c;}else{r=c;b=x;} return {r:Math.round((r+m)*255),g:Math.round((g+m)*255),b:Math.round((b+m)*255)}; } | |
| function rgbToHsv(r,g,b){ r/=255;g/=255;b/=255; let max=Math.max(r,g,b), min=Math.min(r,g,b), d=max-min, h=0, s=max===0?0:d/max, v=max; if(d!==0){ switch(max){ case r: h=(g-b)/d+(g<b?6:0);break; case g: h=(b-r)/d+2;break; case b: h=(r-g)/d+4;break;} h*=60;} return {h,s:s*100,v:v*100}; } | |
| function parseColorStringToState(colorStr) { colorStr = colorStr.trim(); let r=255, g=255, b=255, a=100; if(colorStr.startsWith('#')) { let rgb = hexToRgb(colorStr); r=rgb.r; g=rgb.g; b=rgb.b; } else if(colorStr.startsWith('rgb')) { let parts = colorStr.match(/[\d.]+/g); if(parts && parts.length>=3) { r = parseFloat(parts[0]); g = parseFloat(parts[1]); b = parseFloat(parts[2]); if(parts.length > 3) a = Math.round(parseFloat(parts[3]) * 100); } } const hsv = rgbToHsv(r,g,b); return { h:hsv.h, s:hsv.s, v:hsv.v, a:a }; } | |
| function initSavedColors() { const s = localStorage.getItem('mySavedColors'); if(s) { try{savedColors=JSON.parse(s)||[];}catch(e){savedColors=[];} } renderSavedColors(); } | |
| function saveCurrentColor() { const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r, rgb.g, rgb.b); savedColors.push(hex); localStorage.setItem('mySavedColors', JSON.stringify(savedColors)); renderSavedColors(); showToast("این رنگ در حافظه رنگها ذخیره شد", "fa-solid fa-save"); } | |
| function renderSavedColors(){ const c = document.getElementById('savedColorsContainer'); if(!c) return; const svBtn = c.querySelector('.btn-save-text'); while(svBtn.nextSibling) svBtn.nextSibling.remove(); savedColors.forEach((hex, idx) => { const w = document.createElement('div'); w.className='saved-color-wrapper'; w.innerHTML = `<div class="saved-circle" style="background-color:${hex}"></div><div class="mini-delete-btn"><i class="fa-solid fa-xmark"></i></div>`; w.querySelector('.saved-circle').onclick = () => { let parsed = parseColorStringToState(hex); pickerState=parsed; syncAllUI(); syncGridSelectedFromCurrentColor(); }; w.querySelector('.mini-delete-btn').onclick = (e) => { e.stopPropagation(); currentDeleteIndex=idx; document.getElementById('delColorPreview').style.backgroundColor=hex; document.getElementById('deleteModal').classList.add('active'); }; c.appendChild(w); }); } | |
| function showToast(message = "این رنگ در حافظه رنگها ذخیره شد", iconClass = "fa-solid fa-circle-check") { const t=document.getElementById('toastNotification'); if (!t) return; const span = t.querySelector('span'); const icon = t.querySelector('i'); if(span) span.innerText = message; if(icon) icon.className = iconClass; t.classList.add('show'); clearTimeout(toastTimeout); toastTimeout=setTimeout(()=>t.classList.remove('show'),2200); } | |
| function confirmDelete(){ if(currentDeleteIndex>-1) { savedColors.splice(currentDeleteIndex,1); localStorage.setItem('mySavedColors',JSON.stringify(savedColors)); renderSavedColors(); } closeDeleteModal(); } | |
| function closeDeleteModal(){ document.getElementById('deleteModal').classList.remove('active'); currentDeleteIndex=-1; } | |
| function generateGridPalette() { const colors = ['#FFFFFF','#F2F2F7','#E5E5EA','#D1D1D6','#C7C7CC','#AEAEB2','#8E8E93','#636366','#48484A','#3A3A3C','#2C2C2E','#1C1C1E','#000000','#FF3B30','#FF453A','#FF9500','#FF9F0A','#FFD60A','#FFCC00','#34C759','#30D158','#00C7BE','#64D2FF','#32ADE6','#0A84FF','#007AFF','#5E5CE6','#5856D6','#AF52DE','#BF5AF2','#FF2D55']; for(let i=0;i<=20;i++){ let v=Math.round(i/20*255); colors.push(rgbToHex(v,v,v)); } for(let h=0;h<360;h+=12){ for(let s of [92,72]) for(let v of [100,85,70]) { colors.push(rgbToHex(hsvToRgb(h,s,v).r,hsvToRgb(h,s,v).g,hsvToRgb(h,s,v).b)); } } return [...new Set(colors)]; } | |
| function renderGridWithData(colors){ if(!gridContP) return; gridContP.innerHTML=''; colors.forEach(hex => { const d = document.createElement('div'); d.className='grid-item'; d.style.backgroundColor=hex; d.setAttribute('data-hex', hex.toUpperCase()); d.onclick=()=>{ document.querySelectorAll('.grid-item').forEach(x=>x.classList.remove('selected')); d.classList.add('selected'); let parsed=parseColorStringToState(hex); pickerState=parsed; syncAllUI(); }; gridContP.appendChild(d); }); } | |
| function syncGridSelectedFromCurrentColor() { if(!gridContP) return; const rgb = hsvToRgb(pickerState.h, pickerState.s, pickerState.v); const hex = rgbToHex(rgb.r,rgb.g,rgb.b).toUpperCase(); const items = gridContP.querySelectorAll('.grid-item'); items.forEach(it => { if(it.getAttribute('data-hex')===hex) it.classList.add('selected'); else it.classList.remove('selected'); }); } |