Player: full-screen "stage mode" with landscape lock
Add a ⛶ button (top bar) that takes the PM-1 device full-screen and locks
it to landscape — turning it into a glanceable stage display.
A single body.stage class drives the layout: top bar + load panel hidden,
OLED / beat-LEDs / transport enlarged with viewport units, plus a floating
✕ exit (the top bar is hidden in stage mode). Per platform:
- Android: requestFullscreen() + screen.orientation.lock("landscape").
- Desktop: real fullscreen; the lock harmlessly no-ops (already landscape).
- iPhone (no Fullscreen/orientation-lock API): CSS pseudo-fullscreen + a
"⟳ rotate to landscape" overlay shown when held in portrait.
Screen Wake Lock keeps the display awake during a performance (re-acquired
on visibility change). 'F' toggles; Esc / fullscreenchange tear the stage
down and unlock cleanly. All API calls are guarded so rejections on
unsupported platforms never throw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
77fdf9ee70
commit
075fbb51a7
1 changed files with 80 additions and 1 deletions
81
player.html
81
player.html
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<title>VARASYS PM‑1 — hardware player (mockup)</title>
|
<title>VARASYS PM‑1 — hardware player (mockup)</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||||
<!--
|
<!--
|
||||||
|
|
@ -144,6 +144,43 @@
|
||||||
.status.ok{ color:#5fd08a } .status.err{ color:#ff7a7a }
|
.status.ok{ color:#5fd08a } .status.err{ color:#ff7a7a }
|
||||||
.hint{ font-size:11px; color:var(--muted) }
|
.hint{ font-size:11px; color:var(--muted) }
|
||||||
code{ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:4px; padding:1px 5px; font-size:11px }
|
code{ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:4px; padding:1px 5px; font-size:11px }
|
||||||
|
|
||||||
|
/* ---- full-screen "stage" mode ---- */
|
||||||
|
.fs-exit{ display:none; position:fixed; top:14px; right:14px; z-index:80;
|
||||||
|
background:rgba(18,21,28,.7); color:#e7edf5; border:1px solid rgba(255,255,255,.28);
|
||||||
|
border-radius:50%; width:40px; height:40px; font-size:17px; line-height:1; cursor:pointer;
|
||||||
|
backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px) }
|
||||||
|
.fs-exit:hover{ background:rgba(40,48,62,.9) }
|
||||||
|
.rotate-hint{ display:none }
|
||||||
|
|
||||||
|
body.stage{ position:fixed; inset:0; padding:2.5vmin; gap:0; justify-content:center; overflow:hidden }
|
||||||
|
body.stage .topbar, body.stage .panel{ display:none }
|
||||||
|
body.stage .fs-exit{ display:block }
|
||||||
|
body.stage .device{ max-width:none; width:min(96vw, 168vh); margin:auto; border-radius:min(3vmin,22px) }
|
||||||
|
/* enlarge the glanceable bits with viewport-relative units */
|
||||||
|
body.stage .screen{ padding:2vh 3vw }
|
||||||
|
body.stage .scr-top{ font-size:2.8vh }
|
||||||
|
body.stage .scr-top .tempo{ font-size:3.4vh }
|
||||||
|
body.stage .scr-top .tempo b{ font-size:8.5vh }
|
||||||
|
body.stage .scr-name{ font-size:6vh; margin:1.4vh 0 1.2vh }
|
||||||
|
body.stage .scr-bot{ font-size:2.8vh }
|
||||||
|
body.stage .leds{ gap:1.6vmin; margin:2.6vh 0 1vh }
|
||||||
|
body.stage .led{ width:4.4vmin; height:4.4vmin }
|
||||||
|
body.stage .controls{ gap:1.6vmin; margin-top:2.2vh }
|
||||||
|
body.stage .controls .btn{ font-size:2.6vh; padding:1.5vh 1.8vw; min-width:7vw }
|
||||||
|
body.stage .controls .btn.play{ min-width:11vw; font-size:3.4vh }
|
||||||
|
body.stage .controls .btn small{ font-size:1.3vh }
|
||||||
|
body.stage .brandrow{ margin-bottom:2vh }
|
||||||
|
body.stage .grille{ display:none }
|
||||||
|
|
||||||
|
/* portrait while staged (mainly iPhone, which can't lock) → prompt to rotate */
|
||||||
|
@media (orientation: portrait){
|
||||||
|
body.stage .device{ filter:blur(3px) brightness(.5); pointer-events:none }
|
||||||
|
body.stage .rotate-hint{ display:flex; position:fixed; inset:0; z-index:90;
|
||||||
|
flex-direction:column; align-items:center; justify-content:center; gap:18px;
|
||||||
|
background:var(--bg2); color:var(--txt); font-size:20px; text-align:center; padding:24px }
|
||||||
|
body.stage .rotate-hint .rh-icon{ font-size:64px; line-height:1; color:var(--cyan) }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -151,6 +188,7 @@
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<span><b>VARASYS PM‑1</b> · hardware player (mockup)</span>
|
<span><b>VARASYS PM‑1</b> · hardware player (mockup)</span>
|
||||||
<span class="topbar-right">
|
<span class="topbar-right">
|
||||||
|
<button id="fsBtn" class="tbtn" title="Full screen (landscape)">⛶</button>
|
||||||
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
<button id="themeBtn" class="tbtn" title="toggle light / dark theme">☀</button>
|
||||||
<a href="/index.html">Open editor ↗</a>
|
<a href="/index.html">Open editor ↗</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -203,6 +241,13 @@
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- stage-mode overlays (only visible in full-screen "stage" mode) -->
|
||||||
|
<button id="fsExit" class="fs-exit" title="Exit full screen (Esc)" aria-label="Exit full screen">✕</button>
|
||||||
|
<div id="rotateHint" class="rotate-hint">
|
||||||
|
<span class="rh-icon">⟳</span>
|
||||||
|
<span>Rotate your device to landscape</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
@ -379,6 +424,39 @@ $("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%T
|
||||||
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
|
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
|
||||||
applyTheme(themePref());
|
applyTheme(themePref());
|
||||||
|
|
||||||
|
/* full-screen "stage" mode — real fullscreen + landscape lock where supported (Android/desktop),
|
||||||
|
CSS pseudo-fullscreen + a rotate hint where not (iPhone). body.stage drives the layout. */
|
||||||
|
const docEl = document.documentElement;
|
||||||
|
const reqFS = docEl.requestFullscreen || docEl.webkitRequestFullscreen;
|
||||||
|
const exitFS = document.exitFullscreen || document.webkitExitFullscreen;
|
||||||
|
const fsEl = () => document.fullscreenElement || document.webkitFullscreenElement;
|
||||||
|
let wakeLock = null;
|
||||||
|
async function requestWake(){ try{ if(navigator.wakeLock) wakeLock = await navigator.wakeLock.request("screen"); }catch(e){} }
|
||||||
|
function releaseWake(){ try{ wakeLock && wakeLock.release(); }catch(e){} wakeLock = null; }
|
||||||
|
function isStage(){ return document.body.classList.contains("stage"); }
|
||||||
|
function syncFsBtn(){ $("fsBtn").title = isStage() ? "Exit full screen" : "Full screen (landscape)"; }
|
||||||
|
async function enterStage(){
|
||||||
|
document.body.classList.add("stage");
|
||||||
|
if(reqFS){
|
||||||
|
try{ await reqFS.call(docEl); }catch(e){}
|
||||||
|
try{ await screen.orientation.lock("landscape"); }catch(e){} // Android only; rejects on desktop/iOS — harmless
|
||||||
|
}
|
||||||
|
requestWake(); syncFsBtn();
|
||||||
|
}
|
||||||
|
function exitStage(){
|
||||||
|
try{ if(screen.orientation && screen.orientation.unlock) screen.orientation.unlock(); }catch(e){}
|
||||||
|
if(fsEl() && exitFS){ try{ exitFS.call(document); }catch(e){} }
|
||||||
|
document.body.classList.remove("stage");
|
||||||
|
releaseWake(); syncFsBtn();
|
||||||
|
}
|
||||||
|
function toggleStage(){ isStage() ? exitStage() : enterStage(); }
|
||||||
|
$("fsBtn").onclick = toggleStage;
|
||||||
|
$("fsExit").onclick = exitStage;
|
||||||
|
function onFsChange(){ if(reqFS && !fsEl() && isStage()){ document.body.classList.remove("stage"); releaseWake(); syncFsBtn(); } }
|
||||||
|
document.addEventListener("fullscreenchange", onFsChange);
|
||||||
|
document.addEventListener("webkitfullscreenchange", onFsChange);
|
||||||
|
document.addEventListener("visibilitychange", ()=>{ if(document.visibilityState==="visible" && isStage()) requestWake(); });
|
||||||
|
|
||||||
addEventListener("keydown",(e)=>{
|
addEventListener("keydown",(e)=>{
|
||||||
const t=e.target, tag=t?t.tagName:""; if(tag==="TEXTAREA"||tag==="INPUT"||tag==="SELECT") return;
|
const t=e.target, tag=t?t.tagName:""; if(tag==="TEXTAREA"||tag==="INPUT"||tag==="SELECT") return;
|
||||||
const k=e.key;
|
const k=e.key;
|
||||||
|
|
@ -388,6 +466,7 @@ addEventListener("keydown",(e)=>{
|
||||||
else if(k==="ArrowUp"){ e.preventDefault(); nudge(e.shiftKey?10:1); }
|
else if(k==="ArrowUp"){ e.preventDefault(); nudge(e.shiftKey?10:1); }
|
||||||
else if(k==="ArrowDown"){ e.preventDefault(); nudge(e.shiftKey?-10:-1); }
|
else if(k==="ArrowDown"){ e.preventDefault(); nudge(e.shiftKey?-10:-1); }
|
||||||
else if(k==="t"||k==="T") tapTempo();
|
else if(k==="t"||k==="T") tapTempo();
|
||||||
|
else if(k==="f"||k==="F"){ e.preventDefault(); toggleStage(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ========================= INIT ============================================== */
|
/* ========================= INIT ============================================== */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue