mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
feat(scroll): show which scroll speeds are smooth on this panel (#710)
* feat(scroll): show which scroll speeds are smooth on this panel The Vegas Scroll Speed slider now says what the panel will do with the chosen speed and offers the nearest smooth ones to click. Backed by scroll_config.speed_advice() and GET /api/v3/config/scroll-speed-advice, which uses the refresh the display measured rather than the cap. Also stops the default 50 px/s snapping to a stepped 48 px/s (2px every 5 refreshes, 24fps) on a 120Hz panel: the low-fps penalty in solve_crisp() now loses to 60 or 40 px/s. 100Hz panels are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(scroll): hint threw before its timer variables existed; count 25-30fps as stepped The Vegas speed hint called refreshScrollSpeedHint() before the let declarations it uses, so it never rendered (found on ledpi). And the solver's low-fps penalty stopped at 25fps, which let a measured 125.7Hz panel keep a 25.1fps 2px-every-5-refreshes scroll. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * test: add the scroll-speed-advice route to the /api/v3 URL map snapshot Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
4ddc3a3620
commit
16b566e14f
@@ -84,6 +84,49 @@ def get_main_config():
|
||||
# any of it; /api/v3/auth/* manages it.
|
||||
return jsonify({'status': 'success',
|
||||
'data': _redact_credentials(strip_auth_section(config))})
|
||||
def _panel_refresh_hz(config):
|
||||
"""(hz, source): the rate the panel really refreshes at, else the cap.
|
||||
|
||||
The display service writes what it measured to the frame-stats file. The
|
||||
configured limit_refresh_rate_hz is only a cap (a 120Hz cap refreshes at
|
||||
~126Hz on one rig, ~95Hz on a long chain), and the speeds that look smooth
|
||||
are fractions of the real rate, so advice built on the cap can be wrong.
|
||||
"""
|
||||
from src.common import frame_timing, scroll_config
|
||||
cap = scroll_config.refresh_hz_from_config(config)
|
||||
try:
|
||||
with open(frame_timing.default_stats_path(), encoding='utf-8') as fh:
|
||||
measured = float(json.load(fh).get('measured_refresh_hz') or 0)
|
||||
except (OSError, ValueError, TypeError, AttributeError):
|
||||
measured = 0.0
|
||||
# Reject a stale file from a previous hardware config: a measurement far
|
||||
# off the cap says the config changed since it was written.
|
||||
if measured > 0 and 0.5 * cap <= measured <= 1.5 * cap:
|
||||
return measured, 'measured'
|
||||
return cap, 'configured'
|
||||
|
||||
|
||||
@api_v3.route('/config/scroll-speed-advice', methods=['GET'])
|
||||
def get_scroll_speed_advice():
|
||||
"""What this panel does with a requested scroll speed, and smooth options.
|
||||
|
||||
Backs the hint under the Vegas Scroll Speed slider.
|
||||
"""
|
||||
from src.common import scroll_config
|
||||
try:
|
||||
speed = float(request.args.get('speed', ''))
|
||||
lo = float(request.args.get('min', 10))
|
||||
hi = float(request.args.get('max', 200))
|
||||
except ValueError:
|
||||
return jsonify({'status': 'error', 'message': 'speed must be a number'}), 400
|
||||
if not api_v3.config_manager:
|
||||
return jsonify({'status': 'error', 'message': 'Config manager not initialized'}), 500
|
||||
hz, source = _panel_refresh_hz(api_v3.config_manager.load_config())
|
||||
advice = scroll_config.speed_advice(speed, hz, lo, hi)
|
||||
advice['refresh_source'] = source
|
||||
return jsonify({'status': 'success', 'data': advice})
|
||||
|
||||
|
||||
@api_v3.route('/config/schedule', methods=['GET'])
|
||||
def get_schedule_config():
|
||||
"""Get current schedule configuration"""
|
||||
|
||||
@@ -457,7 +457,7 @@
|
||||
<div id="vegas_scroll_settings" class="space-y-4" style="{% if not main_config.display.get('vegas_scroll', {}).get('enabled', false) %}display: none;{% endif %}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-vegas_scroll_speed" data-setting-key="display.vegas_scroll.scroll_speed">
|
||||
<label for="vegas_scroll_speed" class="block text-sm font-medium text-gray-700">Scroll Speed (pixels/second){{ ui.help_tip('How fast the Vegas ticker scrolls (10–200 px/s).\nDefault: 50. Higher is faster but harder to read.', 'Scroll Speed') }}</label>
|
||||
<label for="vegas_scroll_speed" class="block text-sm font-medium text-gray-700">Scroll Speed (pixels/second){{ ui.help_tip('How fast the Vegas ticker scrolls (10–200 px/s).\nDefault: 50. Higher is faster but harder to read. Only some speeds look perfectly smooth on a given panel; the note below the slider shows which.', 'Scroll Speed') }}</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="range"
|
||||
id="vegas_scroll_speed"
|
||||
@@ -465,10 +465,11 @@
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('scroll_speed', 50) }}"
|
||||
min="10"
|
||||
max="200"
|
||||
step="5"
|
||||
step="1"
|
||||
class="flex-1">
|
||||
<span id="vegas_scroll_speed_value" class="text-sm font-medium w-12">{{ main_config.display.get('vegas_scroll', {}).get('scroll_speed', 50) }}</span>
|
||||
</div>
|
||||
<p id="vegas_scroll_speed_hint" class="mt-1 text-xs text-gray-600" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_separator_width" data-setting-key="display.vegas_scroll.separator_width">
|
||||
@@ -915,6 +916,10 @@ document.getElementById('brightness').addEventListener('input', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Declared before first use: let is not hoisted usably.
|
||||
let scrollHintTimer = null;
|
||||
let scrollHintSeq = 0;
|
||||
|
||||
// Update scroll speed display
|
||||
const scrollSpeedSlider = document.getElementById('vegas_scroll_speed');
|
||||
const scrollSpeedValue = document.getElementById('vegas_scroll_speed_value');
|
||||
@@ -922,6 +927,62 @@ document.getElementById('brightness').addEventListener('input', function() {
|
||||
if (scrollSpeedSlider && scrollSpeedValue) {
|
||||
scrollSpeedSlider.addEventListener('input', function() {
|
||||
scrollSpeedValue.textContent = this.value;
|
||||
refreshScrollSpeedHint();
|
||||
});
|
||||
refreshScrollSpeedHint();
|
||||
}
|
||||
|
||||
// Tell the user what the panel will really do with this speed. Only
|
||||
// speeds that advance a whole number of pixels per refresh look smooth,
|
||||
// and which those are depends on the panel, so the server works it out.
|
||||
function refreshScrollSpeedHint() {
|
||||
clearTimeout(scrollHintTimer);
|
||||
scrollHintTimer = setTimeout(function() {
|
||||
const hint = document.getElementById('vegas_scroll_speed_hint');
|
||||
if (!hint) return;
|
||||
const seq = ++scrollHintSeq;
|
||||
const q = new URLSearchParams({
|
||||
speed: scrollSpeedSlider.value,
|
||||
min: scrollSpeedSlider.min,
|
||||
max: scrollSpeedSlider.max
|
||||
});
|
||||
fetch('/api/v3/config/scroll-speed-advice?' + q)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(body) {
|
||||
if (seq !== scrollHintSeq || body.status !== 'success') return;
|
||||
renderScrollSpeedHint(hint, body.data);
|
||||
})
|
||||
.catch(function() { hint.textContent = ''; });
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function renderScrollSpeedHint(hint, a) {
|
||||
const ap = a.applied;
|
||||
const motion = ap.pixels_per_frame + ' px every ' + ap.frame_hold +
|
||||
' refresh' + (ap.frame_hold === 1 ? '' : 'es');
|
||||
hint.textContent = '';
|
||||
hint.className = 'mt-1 text-xs ' + (a.smooth && a.exact ? 'text-green-700' : 'text-amber-700');
|
||||
const line = document.createElement('span');
|
||||
if (a.smooth && a.exact) {
|
||||
line.textContent = 'Smooth on this panel (' + motion + ', ' + a.refresh_hz + ' Hz).';
|
||||
} else {
|
||||
line.textContent = a.requested + ' px/s will run as ' + ap.pixels_per_second +
|
||||
' px/s (' + motion + ', ' + ap.steppiness + ') on this ' + a.refresh_hz +
|
||||
' Hz panel.' + (a.alternatives.length ? ' Smooth speeds: ' : '');
|
||||
}
|
||||
hint.appendChild(line);
|
||||
a.alternatives.forEach(function(alt, i) {
|
||||
if (i > 0) hint.appendChild(document.createTextNode(' '));
|
||||
const value = Math.round(alt.pixels_per_second);
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'underline font-medium';
|
||||
btn.textContent = value + ' px/s';
|
||||
btn.addEventListener('click', function() {
|
||||
scrollSpeedSlider.value = value;
|
||||
scrollSpeedSlider.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
});
|
||||
hint.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user