diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml
index ca0dd2d..b0cd4b5 100644
--- a/Controls/LeftPanel.xaml
+++ b/Controls/LeftPanel.xaml
@@ -479,6 +479,16 @@
ItemsSource="{Binding ScoreboardEdgeOptions}"
SelectedItem="{Binding ScoreboardEdge, Mode=TwoWay}"/>
+
+
+
+
+
+
public bool Scroll { get; set; } = true;
+ /// Marquee speed in pixels per second — the creator's dial (a read-speed choice, not a
+ /// styling one).
+ public double PixelsPerSecond { get; set; } = DefaultPixelsPerSecond;
+
+ public const double DefaultPixelsPerSecond = 120;
+ public const double MinPixelsPerSecond = 20;
+ public const double MaxPixelsPerSecond = 400;
+
/// Advances the scroll by . The strip is a marquee: one
/// measured run, cropped to a moving window, so a tick is a row-wise byte copy rather than a
/// re-rasterisation.
- public void Advance(double seconds, double pixelsPerSecond = 120)
+ public void Advance(double seconds, double? pixelsPerSecond = null)
{
if (!Scroll) return;
- _offsetPx += seconds * pixelsPerSecond;
+ _offsetPx += seconds * (pixelsPerSecond ?? PixelsPerSecond);
}
/// The strip frame for the current tallies, or null when nothing is enabled/shown.
diff --git a/Services/LayoutStore.Settings.cs b/Services/LayoutStore.Settings.cs
index 58ae368..de6a4b7 100644
--- a/Services/LayoutStore.Settings.cs
+++ b/Services/LayoutStore.Settings.cs
@@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite;
+using ytLive.Services.Compositor;
using ytLive.Models;
namespace ytLive.Services;
@@ -57,6 +58,15 @@ public partial class LayoutStore : IDisposable
public void SaveScoreboardEnabled(bool enabled) => UpsertSetting("Scoreboard.Enabled", enabled ? "1" : "0");
+ /// Scoreboard marquee speed (px/s), restored at startup.
+ public int LoadScoreboardScrollSpeed()
+ => int.TryParse(GetSetting("Scoreboard.ScrollSpeed"), out var px)
+ ? Math.Clamp(px, (int)ScoreboardPresenter.MinPixelsPerSecond, (int)ScoreboardPresenter.MaxPixelsPerSecond)
+ : (int)ScoreboardPresenter.DefaultPixelsPerSecond;
+
+ public void SaveScoreboardScrollSpeed(int pixelsPerSecond)
+ => UpsertSetting("Scoreboard.ScrollSpeed", pixelsPerSecond.ToString());
+
/// The user's chosen record output folder (TASK 18), or null to use the
/// default %APPDATA%\ytLlive\recordings. Restored at startup.
public string? LoadRecordFolder()
diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs
index f45b3e5..b6a4560 100644
--- a/ViewModels/MainViewModel.Scoreboard.cs
+++ b/ViewModels/MainViewModel.Scoreboard.cs
@@ -43,6 +43,20 @@ public partial class MainViewModel
}
public IReadOnlyList ScoreboardEdgeOptions { get; } = new[] { BarEdge.Top, BarEdge.Bottom };
+
+ /// How fast the ticker travels, in pixels per second. Persisted like the rest of the
+ /// strip's look: it is a readability choice, and re-dialling it every launch would be a tax.
+ public double ScoreboardScrollSpeed
+ {
+ get => _scoreboardPresenter?.PixelsPerSecond ?? ScoreboardPresenter.DefaultPixelsPerSecond;
+ set
+ {
+ var clamped = Math.Clamp(value, ScoreboardPresenter.MinPixelsPerSecond, ScoreboardPresenter.MaxPixelsPerSecond);
+ if (_scoreboardPresenter != null) _scoreboardPresenter.PixelsPerSecond = clamped;
+ _layoutStore.SaveScoreboardScrollSpeed((int)Math.Round(clamped));
+ OnPropertyChanged(nameof(ScoreboardScrollSpeed));
+ }
+ }
private Source? _scoreboardPreviewSource;
internal ScoreboardRecorder ScoreboardRecorder => _scoreboard;
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index cc31cda..f6620c7 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -280,6 +280,7 @@ public partial class MainViewModel : ViewModelBase
_musicPlayer.UserGain = (float)Helpers.GainDb.FromDb(TraxDefaultDb); // background by default
InitScoreboard();
ScoreboardEnabled = _layoutStore.LoadScoreboardEnabled();
+ ScoreboardScrollSpeed = _layoutStore.LoadScoreboardScrollSpeed();
try
{
var s = _layoutStore.GetSetting("ZoomShape");
diff --git a/ytLive.Tests/ScoreboardPresenterTests.cs b/ytLive.Tests/ScoreboardPresenterTests.cs
index ae43954..90304c6 100644
--- a/ytLive.Tests/ScoreboardPresenterTests.cs
+++ b/ytLive.Tests/ScoreboardPresenterTests.cs
@@ -48,6 +48,16 @@ public class ScoreboardPresenterTests
Assert.NotNull(ticked);
Assert.NotEqual(first.BgraPixels, ticked!.BgraPixels);
+ // SPEED is a real dial: the same elapsed time at twice the speed moves twice as far,
+ // so the window lands somewhere else.
+ var slow = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 60 };
+ var fast = new ScoreboardPresenter(recorder, new ScoreboardStyle()) { PixelsPerSecond = 120 };
+ slow.SetTiles(tiles);
+ fast.SetTiles(tiles);
+ slow.Advance(1.0);
+ fast.Advance(1.0);
+ Assert.NotEqual(slow.Frame!.BgraPixels, fast.Frame!.BgraPixels);
+
// A visible change re-rasterises the run.
recorder.Record(Member("Second"));
var second = presenter.Frame;
diff --git a/ytLive.Tests/ScoreboardPropertiesTests.cs b/ytLive.Tests/ScoreboardPropertiesTests.cs
index 07d124f..ce76603 100644
--- a/ytLive.Tests/ScoreboardPropertiesTests.cs
+++ b/ytLive.Tests/ScoreboardPropertiesTests.cs
@@ -29,6 +29,11 @@ public class ScoreboardPropertiesTests
Assert.Contains("Simulate (dev)", block);
Assert.Contains("Brush.Attention", block);
+ // Scroll speed: a real dial, persisted, with its range owned by the presenter.
+ Assert.Contains("ScoreboardScrollSpeed", block);
+ Assert.Contains("Minimum=\"20\" Maximum=\"400\"", block);
+ Assert.Contains("Scoreboard.ScrollSpeed", SourceAssert.Text("Services", "LayoutStore.Settings.cs"));
+
// The master switch sits in the same panel: off hides the strip and stops the polling.
Assert.Contains("ScoreboardEnabled", panel);
@@ -61,7 +66,7 @@ public class ScoreboardPropertiesTests
{
"ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity",
"ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers",
- "ScoreboardEnabled", "CanUseScoreboard",
+ "ScoreboardEnabled", "CanUseScoreboard", "ScoreboardScrollSpeed",
})
Assert.NotNull(type.GetProperty(name));
}