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; }