diff --git a/src/control.js b/src/control.js index eddcf1e..133fabb 100644 --- a/src/control.js +++ b/src/control.js @@ -222,17 +222,58 @@ function parseTime(str) { return parseFloat(str) || 0; } +// Snapshot the current Text-tab styling for a cue. +function captureTextStyle() { + return { + dir: $('#ticker-dir').value, + fx: $('#ticker-fx').value, + font: $('#ticker-font').value, + size: parseFloat($('#ticker-size').value), + weight: $('#ticker-bold').checked, + color: $('#ticker-color').value + }; +} +const FX_NAMES = { none: 'Nessuno', updown: 'Su-giù', wave: 'Onda', zoom: 'Zoom', flash: 'Flash', rotate: 'Rotazione' }; +function textStyleSummary(ts) { + if (!ts) return 'stile attuale del pannello'; + const dir = ts.dir === 'h' ? 'Orizz.' : (ts.dir === 'vup' ? 'Vert.↑' : 'Vert.↓'); + return dir + ' · ' + (FX_NAMES[ts.fx] || ts.fx) + ' · ' + ts.size + 'vh'; +} +const IMG_POS = { center: { x: 50, y: 50 }, top: { x: 50, y: 18 }, bottom: { x: 50, y: 82 }, left: { x: 20, y: 50 }, right: { x: 80, y: 50 } }; + // Apply one cue (effect optional; text and image always set so a timeline reads // predictably — empty text / null image hide them). function applyCue(c) { if (!c) return; if (c.effectIndex != null && EFFECTS.list[c.effectIndex]) applyEffect(EFFECTS.list[c.effectIndex]); + + // Text + its full styling const showText = !!(c.text && c.text.trim()); send({ type: 'tickerText', text: c.text || '' }); send({ type: 'tickerOn', on: showText }); $('#ticker-text').value = c.text || ''; $('#ticker-on').checked = showText; - send({ type: 'sceneImage', path: c.image || null }); + const ts = c.textStyle; + if (ts) { + send({ type: 'tickerDir', value: ts.dir }); + send({ type: 'tickerFx', value: ts.fx }); + send({ type: 'tickerFont', value: ts.font }); + send({ type: 'tickerSize', value: ts.size }); + send({ type: 'tickerWeight', on: ts.weight }); + send({ type: 'tickerColor', value: ts.color }); + $('#ticker-dir').value = ts.dir; $('#ticker-fx').value = ts.fx; + $('#ticker-font').value = ts.font; $('#ticker-size').value = ts.size; + $('#ticker-size-val').textContent = (+ts.size).toFixed(1); + $('#ticker-bold').checked = ts.weight; $('#ticker-color').value = ts.color; + } + + // Image + size/position + if (c.image) { + const p = IMG_POS[c.imagePos || 'center'] || IMG_POS.center; + send({ type: 'sceneImage', path: c.image, size: c.imageSize || 60, x: p.x, y: p.y }); + } else { + send({ type: 'sceneImage', path: null }); + } } function startCues(tr) { activeCues = (tr.cues || []).slice().sort((a, b) => a.time - b.time); @@ -308,6 +349,84 @@ function moveTrack(from, to) { let dragFrom = -1; +function defaultCue(time) { + return { time, effectIndex: null, effectName: '', text: '', textStyle: null, image: null, imageSize: 60, imagePos: 'center' }; +} + +// Build the per-track scene editor (timeline of cues with text + image styling). +function buildSceneEditor(tr, i, li) { + const cues = tr.cues || (tr.cues = []); + if (cues.length === 0) cues.push(defaultCue(0)); + cues.sort((a, b) => a.time - b.time); + const ed = document.createElement('li'); + ed.className = 'scene-editor'; + + const posLabels = { center: 'Centro', top: 'Alto', bottom: 'Basso', left: 'Sinistra', right: 'Destra' }; + let html = '
🎬 Scene a tempo — la cue @00:00 parte col brano, le altre scattano al loro tempo
'; + cues.forEach((c, ci) => { + html += + '
' + + '
' + + '@' + + '' + + '' + + (ci > 0 ? '' : 'base') + + '
' + + '
' + + '
🔤 Testo
' + + '' + + '
' + + '' + + '' + textStyleSummary(c.textStyle) + '' + + (c.textStyle ? '' : '') + + '
' + + '
' + + '
' + + '
🖼 Immagine
' + + '
' + + '' + + '' + + '' + (c.image ? baseName(c.image) : 'nessuna') + '' + + '
' + + (c.image ? + '
Dim
' + : '') + + '
' + + '
'; + }); + html += '
'; + ed.innerHTML = html; + + ed.querySelectorAll('.cue').forEach(cueEl => { + const ci = parseInt(cueEl.dataset.c, 10); + const c = cues[ci]; + cueEl.querySelector('.cue-text').value = c.text || ''; + cueEl.querySelector('.cue-time').addEventListener('change', (e) => { c.time = parseTime(e.target.value); savePlaylistState(); renderPlaylist(); }); + cueEl.querySelector('.cue-eff').addEventListener('click', () => { c.effectIndex = EFFECTS.list.indexOf(currentEffect); c.effectName = currentEffect.name; savePlaylistState(); renderPlaylist(); }); + cueEl.querySelector('.cue-text').addEventListener('input', (e) => { c.text = e.target.value; li.querySelector('.scene-btn').classList.toggle('active', hasScene(tr)); savePlaylistState(); }); + cueEl.querySelector('.cue-tstyle').addEventListener('click', () => { c.textStyle = captureTextStyle(); savePlaylistState(); renderPlaylist(); }); + const tsc = cueEl.querySelector('.cue-tstyle-clear'); + if (tsc) tsc.addEventListener('click', () => { c.textStyle = null; savePlaylistState(); renderPlaylist(); }); + cueEl.querySelector('.cue-img').addEventListener('click', () => { sceneImgTarget = i; sceneCueTarget = ci; $('#scene-img-input').click(); }); + cueEl.querySelector('.cue-img-clear').addEventListener('click', () => { c.image = null; savePlaylistState(); renderPlaylist(); }); + const posSel = cueEl.querySelector('.cue-img-pos'); + if (posSel) posSel.addEventListener('change', (e) => { c.imagePos = e.target.value; savePlaylistState(); }); + const sizeSl = cueEl.querySelector('.cue-img-size'); + if (sizeSl) sizeSl.addEventListener('input', (e) => { c.imageSize = parseInt(e.target.value, 10); savePlaylistState(); }); + const del = cueEl.querySelector('.cue-del'); + if (del) del.addEventListener('click', () => { cues.splice(ci, 1); savePlaylistState(); renderPlaylist(); }); + }); + ed.querySelector('.cue-add').addEventListener('click', () => { + const lastT = cues.length ? cues[cues.length - 1].time : 0; + cues.push(defaultCue(lastT + 30)); + savePlaylistState(); renderPlaylist(); + }); + ed.querySelector('.se-clear').addEventListener('click', () => { tr.cues = []; sceneEditing = -1; savePlaylistState(); renderPlaylist(); }); + return ed; +} + function renderPlaylist() { const ol = $('#playlist'); ol.innerHTML = ''; @@ -362,51 +481,7 @@ function renderPlaylist() { ol.appendChild(li); - if (sceneEditing === i) { - const cues = tr.cues || (tr.cues = []); - if (cues.length === 0) cues.push({ time: 0, effectIndex: null, effectName: '', text: '', image: null }); - cues.sort((a, b) => a.time - b.time); - const ed = document.createElement('li'); - ed.className = 'scene-editor'; - let html = '
🎬 Scene a tempo del brano
'; - cues.forEach((c, ci) => { - html += '
' + - '
@' + - '' + - '' + - '' + (c.effectName || '—') + '' + - (ci > 0 ? '' : '') + - '
' + - '' + - '
' + - '' + - '' + (c.image ? baseName(c.image) : 'nessuna') + '
' + - '
'; - }); - html += '
' + - '
' + - '
La cue @00:00 parte col brano; le altre scattano al loro tempo.
'; - ed.innerHTML = html; - ed.querySelectorAll('.cue').forEach(cueEl => { - const ci = parseInt(cueEl.dataset.c, 10); - const c = cues[ci]; - cueEl.querySelector('.cue-text').value = c.text || ''; - cueEl.querySelector('.cue-time').addEventListener('change', (e) => { c.time = parseTime(e.target.value); savePlaylistState(); renderPlaylist(); }); - cueEl.querySelector('.cue-eff').addEventListener('click', () => { c.effectIndex = EFFECTS.list.indexOf(currentEffect); c.effectName = currentEffect.name; savePlaylistState(); renderPlaylist(); }); - cueEl.querySelector('.cue-text').addEventListener('input', (e) => { c.text = e.target.value; li.querySelector('.scene-btn').classList.toggle('active', hasScene(tr)); savePlaylistState(); }); - cueEl.querySelector('.cue-img').addEventListener('click', () => { sceneImgTarget = i; sceneCueTarget = ci; $('#scene-img-input').click(); }); - cueEl.querySelector('.cue-img-clear').addEventListener('click', () => { c.image = null; savePlaylistState(); renderPlaylist(); }); - const del = cueEl.querySelector('.cue-del'); - if (del) del.addEventListener('click', () => { cues.splice(ci, 1); savePlaylistState(); renderPlaylist(); }); - }); - ed.querySelector('.cue-add').addEventListener('click', () => { - const lastT = cues.length ? cues[cues.length - 1].time : 0; - cues.push({ time: lastT + 30, effectIndex: null, effectName: '', text: '', image: null }); - savePlaylistState(); renderPlaylist(); - }); - ed.querySelector('.se-clear').addEventListener('click', () => { tr.cues = []; sceneEditing = -1; savePlaylistState(); renderPlaylist(); }); - ol.appendChild(ed); - } + if (sceneEditing === i) ol.appendChild(buildSceneEditor(tr, i, li)); }); $('#playlist-count').textContent = playlist.length + (playlist.length === 1 ? ' brano' : ' brani'); } @@ -424,7 +499,7 @@ $('#scene-img-input').addEventListener('change', (e) => { const tr = playlist[sceneImgTarget]; tr.cues = tr.cues || []; if (!tr.cues[sceneCueTarget]) sceneCueTarget = 0; - if (!tr.cues[sceneCueTarget]) tr.cues.push({ time: 0, effectIndex: null, effectName: '', text: '', image: null }); + if (!tr.cues[sceneCueTarget]) tr.cues.push(defaultCue(0)); tr.cues[sceneCueTarget].image = p; savePlaylistState(); renderPlaylist(); } diff --git a/src/output.js b/src/output.js index 5e0ad94..7b2a529 100644 --- a/src/output.js +++ b/src/output.js @@ -367,8 +367,15 @@ djv.onControl(async (m) => { case 'trackVideoFit': trackVideo.style.objectFit = m.value; break; case 'sceneImage': - if (m.path) { sceneImage.src = toFileURL(m.path); sceneImage.classList.add('show'); hideHint(); } - else { sceneImage.classList.remove('show'); sceneImage.removeAttribute('src'); } + if (m.path) { + sceneImage.src = toFileURL(m.path); + const s = m.size || 60; + sceneImage.style.maxWidth = s + 'vw'; + sceneImage.style.maxHeight = s + 'vh'; + sceneImage.style.left = (m.x != null ? m.x : 50) + '%'; + sceneImage.style.top = (m.y != null ? m.y : 50) + '%'; + sceneImage.classList.add('show'); hideHint(); + } else { sceneImage.classList.remove('show'); sceneImage.removeAttribute('src'); } break; case 'logo': setLogo(m.index, m.path ? toFileURL(m.path) : ''); break; case 'logoX': logos[m.index].style.left = m.value + '%'; break; diff --git a/src/style.css b/src/style.css index 4d35eab..a216fe3 100644 --- a/src/style.css +++ b/src/style.css @@ -353,29 +353,42 @@ textarea.textarea-lg { /* Per-track scene editor */ .scene-editor { list-style: none; - margin: -2px 0 7px; - padding: 10px; + margin: -2px 0 9px; + padding: 12px; background: rgba(110,150,255,0.10); - border: 1px solid rgba(120,150,255,0.3); - border-radius: 9px; + border: 1px solid rgba(120,150,255,0.38); + border-radius: 12px; } -.scene-editor .se-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; } -.scene-editor .se-row:last-child { margin-bottom: 0; } -.scene-editor .se-name { font-size: 10.5px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.scene-editor .se-text { margin: 0 0 6px; } -.scene-editor .se-img-clear, .scene-editor .se-clear { flex: 0 0 auto; } -.scene-editor .se-title { font-size: 11px; opacity: 0.75; margin-bottom: 8px; } +.scene-editor .se-title { font-size: 12px; font-weight: 600; opacity: 0.92; margin-bottom: 10px; } +.scene-editor .se-title small { font-weight: 400; opacity: 0.55; } + .scene-editor .cue { - border-top: 1px dashed rgba(255,255,255,0.12); - padding-top: 7px; - margin-top: 7px; + background: rgba(0,0,0,0.22); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 10px; + padding: 9px; + margin-bottom: 9px; } -.scene-editor .cue:first-of-type { border-top: none; padding-top: 0; margin-top: 0; } +.scene-editor .cue-head { display: flex; align-items: center; gap: 6px; } .scene-editor .cue-at { opacity: 0.5; flex: 0 0 auto; } -.scene-editor .cue-time { width: 58px; flex: 0 0 auto; text-align: center; padding: 6px 4px; } +.scene-editor .cue-time { width: 60px; flex: 0 0 auto; text-align: center; padding: 6px 4px; font-variant-numeric: tabular-nums; } +.scene-editor .cue-eff { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.scene-editor .cue-del { flex: 0 0 auto; color: #ff9a9a; } +.scene-editor .cue-base { flex: 0 0 auto; font-size: 9px; opacity: 0.45; padding: 0 4px; } + +.scene-editor .cue-block { background: rgba(255,255,255,0.035); border-radius: 8px; padding: 7px 8px; margin-top: 7px; } +.scene-editor .cue-bh { font-size: 9.5px; text-transform: uppercase; letter-spacing: 0.6px; opacity: 0.55; margin-bottom: 5px; } .scene-editor .cue-text { width: 100%; margin: 0 0 6px; } -.scene-editor .cue-eff, .scene-editor .cue-img, .scene-editor .cue-img-clear, -.scene-editor .cue-del, .scene-editor .cue-add { flex: 0 0 auto; } +.scene-editor .cue-row { display: flex; align-items: center; gap: 6px; } +.scene-editor .cue-row + .cue-row { margin-top: 6px; } +.scene-editor .cue-tstyle, .scene-editor .cue-img, +.scene-editor .cue-img-clear, .scene-editor .cue-tstyle-clear { flex: 0 0 auto; } +.scene-editor .cue-tinfo, .scene-editor .cue-name { flex: 1; min-width: 0; font-size: 10px; opacity: 0.72; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.scene-editor .cue-img-pos { flex: 0 0 auto; width: 92px; } +.scene-editor .cue-dim { flex: 0 0 auto; font-size: 10px; opacity: 0.55; } +.scene-editor .cue-img-size { flex: 1; } +.scene-editor .se-foot { display: flex; gap: 6px; margin-top: 2px; } +.scene-editor .se-foot .se-clear { color: #ff9a9a; } /* ============ OUTPUT WINDOW ============ */ body.output { cursor: none; background: #000; }