The marquee now has the dial it needs. `ScoreboardPresenter.PixelsPerSecond` is the single owner of the range (20-400, default 120), the layer's properties expose it with a live px/s readout, and the value is persisted as Scoreboard.ScrollSpeed because it is a READABILITY choice — re-dialling it every launch would be a tax on the creator who needs the strip to be readable. Advance() still takes an optional override so tests can drive exact distances. Guard: the presenter test proves speed is real (same elapsed time, twice the speed, different window) — not a value that is stored and ignored — and the panel test pins the control, its range and the persistence key.
96 lines
3.4 KiB
C#
96 lines
3.4 KiB
C#
using System.Collections.Generic;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Services.Compositor;
|
|
|
|
/// <summary>Turns the session's tallies into the strip frame, and keeps the raster until a displayed
|
|
/// value actually moves.
|
|
///
|
|
/// <para>The cache is the point: a strip re-rasterised every tick would be the most expensive layer
|
|
/// the compositor carries, for pixels that did not change. `ScoreboardRecorder.Changed` only fires
|
|
/// on a visible change, so the presenter rebuilds only then.</para></summary>
|
|
public sealed class ScoreboardPresenter
|
|
{
|
|
private readonly ScoreboardRecorder _recorder;
|
|
private VideoFrame? _frame;
|
|
private VideoFrame? _run; // the wide scrollable run (dev preview only)
|
|
private double _offsetPx;
|
|
private string? _tilesFingerprint;
|
|
private ScoreboardStyle _style;
|
|
private ScoreboardTiles _tiles = ScoreboardTiles.None;
|
|
|
|
public ScoreboardPresenter(ScoreboardRecorder recorder, ScoreboardStyle style)
|
|
{
|
|
_recorder = recorder;
|
|
_style = style;
|
|
_recorder.Changed += Invalidate;
|
|
}
|
|
|
|
/// <summary>The strip TICKS (creator ruling 2026-10-30: "the point is, it's a ticker… so,
|
|
/// tick"). True for the product, and the dev simulation uses the same path — so what the
|
|
/// creator previews is what ships.</summary>
|
|
public bool Scroll { get; set; } = true;
|
|
|
|
/// <summary>Marquee speed in pixels per second — the creator's dial (a read-speed choice, not a
|
|
/// styling one).</summary>
|
|
public double PixelsPerSecond { get; set; } = DefaultPixelsPerSecond;
|
|
|
|
public const double DefaultPixelsPerSecond = 120;
|
|
public const double MinPixelsPerSecond = 20;
|
|
public const double MaxPixelsPerSecond = 400;
|
|
|
|
/// <summary>Advances the scroll by <paramref name="seconds"/>. 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.</summary>
|
|
public void Advance(double seconds, double? pixelsPerSecond = null)
|
|
{
|
|
if (!Scroll) return;
|
|
_offsetPx += seconds * (pixelsPerSecond ?? PixelsPerSecond);
|
|
}
|
|
|
|
/// <summary>The strip frame for the current tallies, or null when nothing is enabled/shown.</summary>
|
|
public VideoFrame? Frame
|
|
{
|
|
get
|
|
{
|
|
var tiles = _tiles.Build(_recorder.State);
|
|
var fingerprint = $"{_tiles.CacheKey}|{string.Join('|', tiles)}|{_style}";
|
|
|
|
if (Scroll)
|
|
{
|
|
if (_run is null || fingerprint != _tilesFingerprint)
|
|
{
|
|
_tilesFingerprint = fingerprint;
|
|
_run = ScoreboardRenderer.RenderRun(tiles, _style);
|
|
}
|
|
return _run is null ? null : ScoreboardRenderer.Crop(_run, (int)_offsetPx, ScoreboardRenderer.Width);
|
|
}
|
|
|
|
if (_frame != null && fingerprint == _tilesFingerprint) return _frame;
|
|
_tilesFingerprint = fingerprint;
|
|
_frame = ScoreboardRenderer.Render(tiles, _style);
|
|
return _frame;
|
|
}
|
|
}
|
|
|
|
public void SetTiles(ScoreboardTiles tiles)
|
|
{
|
|
_tiles = tiles;
|
|
Invalidate();
|
|
}
|
|
|
|
public void SetStyle(ScoreboardStyle style)
|
|
{
|
|
_style = style;
|
|
Invalidate();
|
|
}
|
|
|
|
/// <summary>Drops the cached raster so the next read rebuilds it.</summary>
|
|
public void Invalidate()
|
|
{
|
|
_frame = null;
|
|
_run = null;
|
|
_tilesFingerprint = null;
|
|
}
|
|
}
|