Make fullscreen video fill the viewport
This commit is contained in:
@@ -11456,7 +11456,7 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
stage: els.video.parentElement, video: els.video,
|
stage: els.video.parentElement, video: els.video,
|
||||||
settings: () => data.settings, persist,
|
settings: () => data.settings, persist,
|
||||||
toggle: () => Player.toggle(), seek: time => Player.seek(time),
|
toggle: () => Player.toggle(), seek: time => Player.seek(time),
|
||||||
state: () => ({ time: Player.master.currentTime, duration: Player.master.duration, paused: Player.master.paused }),
|
state: () => ({ time: Player.master.currentTime, duration: Player.master.duration, paused: Player.master.paused, title: current?.meta?.title || '' }),
|
||||||
});
|
});
|
||||||
window.playerFullscreen = fullscreen;
|
window.playerFullscreen = fullscreen;
|
||||||
els.fsBtn.addEventListener('click', () => fullscreen.enter());
|
els.fsBtn.addEventListener('click', () => fullscreen.enter());
|
||||||
|
|||||||
@@ -11,15 +11,23 @@
|
|||||||
if (angle === -90) return { x: rect.bottom - y, y: x - rect.left, width: rect.height, height: rect.width };
|
if (angle === -90) return { x: rect.bottom - y, y: x - rect.left, width: rect.height, height: rect.width };
|
||||||
return { x: x - rect.left, y: y - rect.top, width: rect.width, height: rect.height };
|
return { x: x - rect.left, y: y - rect.top, width: rect.width, height: rect.height };
|
||||||
}
|
}
|
||||||
|
function normalizeFit(value) { return value === 'fit' ? 'fit' : 'fill'; }
|
||||||
function create(options) {
|
function create(options) {
|
||||||
const { stage, settings, persist, toggle, seek, state } = options;
|
const { stage, settings, persist, toggle, seek, state } = options;
|
||||||
let active = false, custom = false, native = false, locked = false, angle = 0, timer, hideTimer;
|
let active = false, custom = false, native = false, locked = false, angle = 0, timer, hideTimer;
|
||||||
const ancestors = [];
|
const ancestors = [];
|
||||||
for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el);
|
for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el);
|
||||||
const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true;
|
const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true;
|
||||||
hud.innerHTML = '<button type="button" aria-label="Play or pause" title="Play or pause"><svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation" title="Change fullscreen orientation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V4h4M20 16v4h-4M4 4l6 6m4 4 6 6M20 8V4h-4M4 16v4h4M20 4l-6 6m-4 4-6 6"/></svg></button><button type="button" aria-label="Exit fullscreen" title="Exit fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H4v5m0-5 6 6m5 9h5v-5m0 5-6-6"/></svg></button>';
|
hud.innerHTML = '<div class="fullscreen-top"><div class="fullscreen-title" aria-live="polite"></div><button type="button" aria-label="Exit fullscreen" title="Exit fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4H4v5m0-5 6 6m5 9h5v-5m0 5-6-6"/></svg></button></div><div class="fullscreen-controls"><button type="button" aria-label="Play or pause" title="Play or pause"><svg class="fill" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg></button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation" title="Change fullscreen orientation"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8V4h4M20 16v4h-4M4 4l6 6m4 4 6 6M20 8V4h-4M4 16v4h4M20 4l-6 6m-4 4-6 6"/></svg></button><button type="button" class="fullscreen-fit" aria-label="Change video fit" title="Change video fit"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7V4h3M17 4h3v3M20 17v3h-3M7 20H4v-3M7 9h10v6H7z"/></svg></button></div>';
|
||||||
stage.append(hud);
|
stage.append(hud);
|
||||||
const [play, orientation, exit] = hud.querySelectorAll('button'), range = hud.querySelector('input');
|
const play = hud.querySelector('button[aria-label="Play or pause"]'), orientation = hud.querySelector('.fullscreen-orientation'), fitButton = hud.querySelector('.fullscreen-fit'), exit = hud.querySelector('button[aria-label="Exit fullscreen"]'), range = hud.querySelector('input');
|
||||||
|
const title = hud.querySelector('.fullscreen-title');
|
||||||
|
function setFit(value) {
|
||||||
|
const fit = normalizeFit(value); settings().fullscreenFit = fit; persist();
|
||||||
|
stage.dataset.fullscreenFit = fit;
|
||||||
|
fitButton.setAttribute('aria-label', `Video fill mode: ${fit === 'fill' ? 'Fill' : 'Fit'}`);
|
||||||
|
fitButton.title = `${fit === 'fill' ? 'Fill' : 'Fit'} video frame`;
|
||||||
|
}
|
||||||
function revealControls() {
|
function revealControls() {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
hud.classList.remove('hud-hidden'); clearTimeout(hideTimer);
|
hud.classList.remove('hud-hidden'); clearTimeout(hideTimer);
|
||||||
@@ -33,10 +41,24 @@
|
|||||||
hud.addEventListener('focusout', () => setTimeout(revealControls, 0));
|
hud.addEventListener('focusout', () => setTimeout(revealControls, 0));
|
||||||
hud.addEventListener('click', revealControls);
|
hud.addEventListener('click', revealControls);
|
||||||
play.onclick = toggle; exit.onclick = leave;
|
play.onclick = toggle; exit.onclick = leave;
|
||||||
|
fitButton.onclick = () => setFit(settings().fullscreenFit === 'fill' ? 'fit' : 'fill');
|
||||||
|
let lastTap = 0, pointers = new Set();
|
||||||
|
stage.addEventListener('pointerdown', event => {
|
||||||
|
if (!active || hud.contains(event.target)) return;
|
||||||
|
pointers.add(event.pointerId);
|
||||||
|
if (pointers.size > 1) { setFit(settings().fullscreenFit === 'fill' ? 'fit' : 'fill'); pointers.clear(); return; }
|
||||||
|
const now = performance.now(), rect = stage.getBoundingClientRect();
|
||||||
|
const center = Math.abs(event.clientX - rect.left - rect.width / 2) < rect.width * .24 && Math.abs(event.clientY - rect.top - rect.height / 2) < rect.height * .24;
|
||||||
|
if (center && now - lastTap < 330) { setFit(settings().fullscreenFit === 'fill' ? 'fit' : 'fill'); lastTap = 0; }
|
||||||
|
else lastTap = center ? now : 0;
|
||||||
|
}, { passive: true });
|
||||||
|
stage.addEventListener('pointerup', event => pointers.delete(event.pointerId), { passive: true });
|
||||||
|
stage.addEventListener('pointercancel', event => pointers.delete(event.pointerId), { passive: true });
|
||||||
function layout() {
|
function layout() {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0;
|
angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0;
|
||||||
stage.dataset.fullscreenRotation = String(angle);
|
stage.dataset.fullscreenRotation = String(angle);
|
||||||
|
title.textContent = state().title || '';
|
||||||
orientation.setAttribute('aria-label', `Change fullscreen orientation (currently ${normalize(settings().fullscreenOrientation)})`);
|
orientation.setAttribute('aria-label', `Change fullscreen orientation (currently ${normalize(settings().fullscreenOrientation)})`);
|
||||||
orientation.title = `Orientation: ${normalize(settings().fullscreenOrientation)}`;
|
orientation.title = `Orientation: ${normalize(settings().fullscreenOrientation)}`;
|
||||||
}
|
}
|
||||||
@@ -65,6 +87,7 @@
|
|||||||
ancestors.forEach(el => el.classList.remove('orientation-ancestor'));
|
ancestors.forEach(el => el.classList.remove('orientation-ancestor'));
|
||||||
hud.hidden = true; hud.classList.remove('hud-hidden'); stage.classList.remove('orientation-overlay', 'orientation-fullscreen');
|
hud.hidden = true; hud.classList.remove('hud-hidden'); stage.classList.remove('orientation-overlay', 'orientation-fullscreen');
|
||||||
delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs');
|
delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs');
|
||||||
|
delete stage.dataset.fullscreenFit;
|
||||||
if (locked) { screen.orientation?.unlock?.(); locked = false; }
|
if (locked) { screen.orientation?.unlock?.(); locked = false; }
|
||||||
}
|
}
|
||||||
function leave() {
|
function leave() {
|
||||||
@@ -74,6 +97,7 @@
|
|||||||
async function enter() {
|
async function enter() {
|
||||||
if (active) { leave(); return; }
|
if (active) { leave(); return; }
|
||||||
active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false; revealControls();
|
active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false; revealControls();
|
||||||
|
setFit(normalizeFit(settings().fullscreenFit));
|
||||||
document.body.classList.add('landscape-fs');
|
document.body.classList.add('landscape-fs');
|
||||||
try {
|
try {
|
||||||
if (!stage.requestFullscreen) throw new Error('Custom fullscreen');
|
if (!stage.requestFullscreen) throw new Error('Custom fullscreen');
|
||||||
@@ -86,6 +110,7 @@
|
|||||||
timer = setInterval(() => {
|
timer = setInterval(() => {
|
||||||
const s = state();
|
const s = state();
|
||||||
range.value = String(s.duration ? s.time / s.duration * 1000 : 0);
|
range.value = String(s.duration ? s.time / s.duration * 1000 : 0);
|
||||||
|
range.style.setProperty('--seek-progress', `${s.duration ? Math.min(100, s.time / s.duration * 100) : 0}%`);
|
||||||
play.setAttribute('aria-label', s.paused ? 'Play video' : 'Pause video');
|
play.setAttribute('aria-label', s.paused ? 'Play video' : 'Pause video');
|
||||||
play.title = s.paused ? 'Play' : 'Pause';
|
play.title = s.paused ? 'Play' : 'Pause';
|
||||||
play.innerHTML = s.paused
|
play.innerHTML = s.paused
|
||||||
@@ -98,6 +123,6 @@
|
|||||||
window.addEventListener('resize', layout);
|
window.addEventListener('resize', layout);
|
||||||
return { enter, leave, point: event => coordinates(event.clientX, event.clientY, stage.getBoundingClientRect(), angle) };
|
return { enter, leave, point: event => coordinates(event.clientX, event.clientY, stage.getBoundingClientRect(), angle) };
|
||||||
}
|
}
|
||||||
const api = { normalize, rotation, coordinates, create };
|
const api = { normalize, normalizeFit, rotation, coordinates, create };
|
||||||
if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.FullscreenOrientation = api;
|
if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.FullscreenOrientation = api;
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
const { test } = require('node:test');
|
const { test } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const { normalize, rotation, coordinates } = require('./fullscreen-orientation');
|
const { normalize, normalizeFit, rotation, coordinates } = require('./fullscreen-orientation');
|
||||||
|
test('fullscreen fit defaults safely to fill and accepts fit', () => {
|
||||||
|
assert.equal(normalizeFit(undefined), 'fill');
|
||||||
|
assert.equal(normalizeFit('fit'), 'fit');
|
||||||
|
assert.equal(normalizeFit('unexpected'), 'fill');
|
||||||
|
});
|
||||||
test('orientation defaults and only rotates mismatched viewports', () => {
|
test('orientation defaults and only rotates mismatched viewports', () => {
|
||||||
assert.equal(normalize('invalid'), 'auto');
|
assert.equal(normalize('invalid'), 'auto');
|
||||||
assert.equal(rotation('auto', 390, 844), 0);
|
assert.equal(rotation('auto', 390, 844), 0);
|
||||||
|
|||||||
BIN
plans/codex-shots-r2/05-fullscreen-portrait-fill.png
Normal file
BIN
plans/codex-shots-r2/05-fullscreen-portrait-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
plans/codex-shots-r2/06-fullscreen-portrait-fit.png
Normal file
BIN
plans/codex-shots-r2/06-fullscreen-portrait-fit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
plans/codex-shots-r2/07-fullscreen-landscape-fill.png
Normal file
BIN
plans/codex-shots-r2/07-fullscreen-landscape-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
plans/codex-shots-r2/10-fullscreen-portrait-fill.png
Normal file
BIN
plans/codex-shots-r2/10-fullscreen-portrait-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
plans/codex-shots-r2/11-fullscreen-portrait-fit.png
Normal file
BIN
plans/codex-shots-r2/11-fullscreen-portrait-fit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
plans/codex-shots-r2/12-fullscreen-landscape-fill.png
Normal file
BIN
plans/codex-shots-r2/12-fullscreen-landscape-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Reference in New Issue
Block a user