mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
#238: the dashboard preview of a 90/270 display was sideways while the panel on the wall was right — the split that makes a preview useless, because a designer checking portrait content can no longer tell a real fault from an artefact of the tool. A portrait panel is a landscape framebuffer that the player rotates content INSIDE (+90), hung turned the other way (-90); the two cancel and the viewer sees upright portrait. The dashboard modelled only the first half. It iframed the player into a box it had already given the finished 9/16 shape, so the player rotated a second time inside a box that was pretending to be the finished picture, and nothing anywhere stood in for the mount. Screenshots had the opposite half missing: they are the raw framebuffer, shown untouched, so every portrait screen looked wrong on the cards and in Now Playing too. So each surface now has a stage (the panel's face) and a frame (its framebuffer), with the frame turned by the INVERSE of the player's angle. Turning it the same way is the tempting mistake and the worst kind of wrong: 90+90 lands upside-down, which reads as nearly-right. The dimension swap is not cosmetic either — composing into the real framebuffer shape is what makes the player lay content out in the same portrait box the panel uses; hand it a portrait viewport instead and every zone and object-fit decision is computed for a canvas no panel has. The geometry is the players' own rule (server/lib/orientation-style.js), served to the dashboard rather than re-derived, since a second copy of a rotation rule is exactly how the two came to disagree. Covers the device preview modal, the playlist preview's portrait toggle (same fault), Now Playing and the device cards. The Remote canvas stays raw on purpose: taps are sent as fractions of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
136 lines
6.4 KiB
JavaScript
136 lines
6.4 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* The CSS needed to rotate a full-screen player container, and (below) the CSS needed to show that
|
|
* rotated output back to a human in the dashboard.
|
|
*
|
|
* This looks trivial and is not, because rotating a box does NOT move it. The web player set
|
|
* `width:100vh; height:100vw` and `rotate(90deg)` on a container pinned `inset: 0`, which leaves
|
|
* the box in the top-left corner and spins it about ITS OWN centre — not the viewport's. On a
|
|
* 1920x1080 panel that put the content 420px off-screen to the left and 420px off the bottom:
|
|
* portrait content, correctly rotated, in the wrong place and cropped on two edges.
|
|
*
|
|
* The box has to be centred on the viewport BEFORE it is turned. Tizen already does this
|
|
* (`top:50%; left:50%; translate(-50%,-50%) rotate()`), and Android does the equivalent with
|
|
* translationX/Y offsets of (w-h)/2 — so the web player was the odd one out.
|
|
*
|
|
* Returned as plain style values so the same rule can be asserted in a test without a browser,
|
|
* and shared rather than re-derived per player. Every property the rotated state sets is also
|
|
* cleared by the landscape state: a half-reset leaves a container stuck at 100vh wide.
|
|
*/
|
|
|
|
const ROTATION_DEG = {
|
|
'landscape': 0,
|
|
'portrait': 90,
|
|
'landscape-flipped': 180,
|
|
'portrait-flipped': 270,
|
|
};
|
|
|
|
/**
|
|
* @param {string} orientation landscape | portrait | landscape-flipped | portrait-flipped
|
|
* @returns {{transform:string,width:string,height:string,top:string,left:string,transformOrigin:string}}
|
|
* Values to assign directly onto element.style. Empty string means "clear it".
|
|
*/
|
|
function orientationStyle(orientation) {
|
|
const deg = ROTATION_DEG[orientation];
|
|
|
|
// Unknown orientation falls back to landscape rather than throwing: a bad value from the server
|
|
// should leave a readable screen, not a blank or sideways one.
|
|
if (!deg) {
|
|
return { transform: '', width: '', height: '', top: '', left: '', transformOrigin: '' };
|
|
}
|
|
|
|
// 180 needs no dimension swap — the box already matches the viewport, it just turns over. Giving
|
|
// it the portrait treatment would swap width and height for no reason and letterbox it.
|
|
const swap = deg === 90 || deg === 270;
|
|
if (!swap) {
|
|
return {
|
|
transform: 'rotate(180deg)',
|
|
width: '', height: '', top: '', left: '',
|
|
transformOrigin: 'center center',
|
|
};
|
|
}
|
|
|
|
return {
|
|
// translate BEFORE rotate: transforms apply right-to-left, so the box is turned about its own
|
|
// centre and then that centre is moved onto the viewport's. Reversing them rotates the offset
|
|
// as well and puts the content back off-screen.
|
|
transform: 'translate(-50%, -50%) rotate(' + deg + 'deg)',
|
|
width: '100vh',
|
|
height: '100vw',
|
|
top: '50%',
|
|
left: '50%',
|
|
transformOrigin: 'center center',
|
|
};
|
|
}
|
|
|
|
/** Does this orientation put the panel's long edge vertical? 90 and 270 swap the axes; 0/180 don't. */
|
|
function swapsAxes(orientation) {
|
|
const deg = ROTATION_DEG[orientation];
|
|
return deg === 90 || deg === 270;
|
|
}
|
|
|
|
/**
|
|
* The CSS needed to show a rotated display's OUTPUT — its framebuffer — inside a fixed dashboard
|
|
* box, as a person standing in front of the panel sees it.
|
|
*
|
|
* #238: the dashboard preview of a 90/270 device was sideways while the panel was right, and the
|
|
* reason is that the dashboard only did half the job. A portrait panel is a landscape framebuffer
|
|
* that the player rotates content INSIDE (+90), hung on the wall turned the other way (-90); the
|
|
* two cancel and the viewer sees upright portrait. The dashboard iframed the player into a box it
|
|
* had already made portrait-shaped, so the player rotated content a second time inside a box that
|
|
* was pretending to be the finished picture — one rotation applied, the mount's never modelled.
|
|
* Designers checking their work on a portrait screen saw sideways content and could not tell a
|
|
* real fault from a preview artefact, so every portrait anomaly became a support question.
|
|
*
|
|
* So the frame stands in for the physical mount and rotates by the INVERSE of the player's angle.
|
|
* Rotating it the SAME way instead is the tempting mistake and the worst kind of wrong: 90+90
|
|
* lands upside-down, which reads as "nearly right" and gets shipped.
|
|
*
|
|
* The dimension swap matters as much as the angle. Composing into a box the shape of the real
|
|
* FRAMEBUFFER (stage axes swapped) and turning that is not a no-op round trip — it is what makes
|
|
* the player lay the content out in the same portrait box the panel uses. Feed the player a
|
|
* portrait-shaped viewport instead and every zone, aspect and object-fit decision is computed for
|
|
* the wrong canvas.
|
|
*
|
|
* @param {string} orientation landscape | portrait | landscape-flipped | portrait-flipped
|
|
* @param {{width:number,height:number}} box the on-screen stage, in px, AS THE VIEWER SEES IT
|
|
* @returns {{transform:string,width:string,height:string,top:string,left:string,transformOrigin:string}}
|
|
* Values to assign directly onto element.style. Empty string means "clear it" — the
|
|
* landscape state must clear every property the rotated state sets, or a device switched
|
|
* back to landscape keeps a stale swapped size and looks broken until a reload.
|
|
*/
|
|
function previewFrameStyle(orientation, box) {
|
|
const deg = ROTATION_DEG[orientation];
|
|
const w = box && box.width, h = box && box.height;
|
|
|
|
// Unknown/landscape, or a stage that has not been laid out yet (a hidden tab measures 0x0):
|
|
// clear back to the base CSS rather than pinning a 0px frame nobody can see.
|
|
if (!deg || !(w > 0) || !(h > 0)) {
|
|
return { transform: '', width: '', height: '', top: '', left: '', transformOrigin: '' };
|
|
}
|
|
|
|
const swap = swapsAxes(orientation);
|
|
return {
|
|
transform: 'translate(-50%, -50%) rotate(' + ((360 - deg) % 360) + 'deg)',
|
|
width: (swap ? h : w) + 'px',
|
|
height: (swap ? w : h) + 'px',
|
|
top: '50%',
|
|
left: '50%',
|
|
transformOrigin: 'center center',
|
|
};
|
|
}
|
|
|
|
/** Stage aspect for a device, as the viewer sees it: '9 / 16' for a portrait-hung 16:9 panel. */
|
|
function previewAspectRatio(orientation, panelW, panelH) {
|
|
const w = panelW || 16, h = panelH || 9;
|
|
return swapsAxes(orientation) ? (h + ' / ' + w) : (w + ' / ' + h);
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = { orientationStyle, previewFrameStyle, previewAspectRatio, swapsAxes, ROTATION_DEG };
|
|
}
|
|
if (typeof window !== 'undefined') {
|
|
window.OrientationStyle = { orientationStyle, previewFrameStyle, previewAspectRatio, swapsAxes, ROTATION_DEG };
|
|
}
|