diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a7fae..93e0ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed: HTML display pages now fit to width, top-aligned + +The HTML display pages (`/rotation` for Visionect and the zero-config root `/`) +default to fitting the image to the viewport **width**, keeping the aspect ratio, +pinning the top of the page to the top of the viewport, and clipping whatever +runs past the bottom. This keeps the masthead and lead story large and legible on +panels whose aspect ratio differs from the paper's, instead of shrinking the whole +page to a letterboxed thumbnail. + +- New `?fit=` values on the HTML pages: default (width, top-aligned), + `?fit=contain` (the previous behavior — whole page, letterboxed), and + `?fit=cover` (fill, center-crop). Pair with `?margin=0` to pin the image flush + to the top-left corner. `fit=width` is client-side CSS only; the raw-image + endpoint (`/rotation.png`) still frames with `contain`/`cover` server-side. +- The URL builder's Fit control offers all three for the Visionect target + (defaulting to width) and `contain`/`cover` for the image/TRMNL targets. +- **Behavior change:** existing HTML displays render fit-to-width on upgrade. + Add `?fit=contain` to the page URL to restore the previous letterboxed layout. + ### Added: content-aware cropping Served pages are now trimmed to their content bounds before framing diff --git a/cmd/broadsheet-server/main.go b/cmd/broadsheet-server/main.go index b518f7a..65fded8 100644 --- a/cmd/broadsheet-server/main.go +++ b/cmd/broadsheet-server/main.go @@ -200,9 +200,14 @@ const displayHTML = ` @@ -213,12 +218,15 @@ const displayHTML = ` var q = new URLSearchParams(window.location.search); // Framing is done here in CSS, driven by the page URL: - // ?margin= -> padding, in vmin (default 3) - // ?fit=cover -> object-fit: cover (crop to fill) instead of contain + // ?margin= -> padding, in vmin (default 3) + // default -> fit to width, top-aligned, clipping the bottom overflow + // ?fit=contain -> scale the whole page to fit inside, letterboxed + // ?fit=cover -> fill the viewport, center-cropping the overflow if (q.has('margin')) { document.documentElement.style.setProperty('--pad', (parseFloat(q.get('margin')) || 0) + 'vmin'); } - if (q.get('fit') === 'cover') document.body.classList.add('cover'); + var fit = q.get('fit'); + if (fit === 'contain' || fit === 'cover') document.body.classList.add(fit); // Fetch the image exactly once per load — each fetch advances this device's // rotation cursor server-side, so one fetch == one advance. We ask only for a diff --git a/cmd/broadsheet-server/rotation.go b/cmd/broadsheet-server/rotation.go index b4dc9e5..41edc26 100644 --- a/cmd/broadsheet-server/rotation.go +++ b/cmd/broadsheet-server/rotation.go @@ -160,9 +160,14 @@ var rotationTmpl = template.Must(template.New("rotation").Parse(` @@ -180,7 +185,9 @@ var rotationTmpl = template.Must(template.New("rotation").Parse(` if (margin !== null) { document.documentElement.style.setProperty('--pad', (parseFloat(margin) || 0) + 'vmin'); } - if (qparam('fit') === 'cover') document.body.classList.add('cover'); + // Default is fit-to-width, top-aligned (CSS above); contain/cover opt out. + var fit = qparam('fit'); + if (fit === 'contain' || fit === 'cover') document.body.classList.add(fit); var I = {{.IntervalSec}}; // dwell seconds var phase = {{.Phase}}; diff --git a/cmd/broadsheet-server/web/templates/builder.html b/cmd/broadsheet-server/web/templates/builder.html index 8cee2d5..4bf8d9c 100644 --- a/cmd/broadsheet-server/web/templates/builder.html +++ b/cmd/broadsheet-server/web/templates/builder.html @@ -18,8 +18,8 @@

URL builder

- +

@@ -41,8 +41,12 @@

Preview

if (!forPage) { if ($('w').value) ps.push('w=' + $('w').value); if ($('h').value) ps.push('h=' + $('h').value); - if ($('fit').value) ps.push('fit=' + $('fit').value); } else if ($('sleep').value) ps.push('sleep=' + $('sleep').value); + // Fit applies to the HTML page (client-side CSS) and the raw image + // (server-side framing); emit it only when it differs from the target's + // default (width for the HTML page, contain for the image). + var fitDefault = $('target').value === 'visionect' ? 'width' : 'contain'; + if ($('fit').value && $('fit').value !== fitDefault) ps.push('fit=' + $('fit').value); return ps.length ? '?' + ps.join('&') : ''; } var HINTS = { @@ -62,8 +66,27 @@

Preview

ps.push('w=420'); return '?' + ps.join('&'); } + // The HTML page defaults to fit=width (fill width, top-align) and also offers + // contain/cover; the raw image and TRMNL envelope go through the server + // renderer, which frames with contain/cover alone. Reset to the target's + // default only when the target itself changes, so edits elsewhere on the form + // don't clobber a deliberate fit choice. + var lastTarget = null; + function setFitOptions(t) { + var sel = $('fit'); + var opts = [['contain', 'contain'], ['cover', 'cover']]; + if (t === 'visionect') opts.push(['width', 'width (top-align)']); + var def = t === 'visionect' ? 'width' : 'contain'; + var want = t === lastTarget ? sel.value : def; + lastTarget = t; + sel.innerHTML = opts.map(function (o) { + return ''; + }).join(''); + sel.value = opts.some(function (o) { return o[0] === want; }) ? want : def; + } function update() { var t = $('target').value, url; + setFitOptions(t); if (t === 'visionect') url = base + '/rotation' + params(true); else if (t === 'png') url = base + '/rotation.png' + params(false); else url = base + '/api/display' + params(false);