11660ac63d
the play/pause button had no fixed width, so swapping the single-character play glyph for the two-character pause glyph (with the fonts normal spacing between them) visibly widened the button. now a fixed 2.4ch width with tightened tracking on the pause state.\n\nmute only ever toggled — there was no way to set a level. added a small click/drag bar next to it (same interaction as the scrub bar), synced with the video elements own volume/muted state via the volumechange event so it stays correct regardless of how its changed.
95 lines
4.2 KiB
JavaScript
95 lines
4.2 KiB
JavaScript
/* minimal terminal-style video player — custom controls in our own font.
|
|
re-inits after pjax navigation. */
|
|
(function () {
|
|
"use strict";
|
|
function fmt(t) {
|
|
t = Math.max(0, t | 0);
|
|
var m = (t / 60) | 0, s = t % 60;
|
|
return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
|
|
}
|
|
function init(root) {
|
|
var v = root.querySelector("video");
|
|
var play = root.querySelector(".player__play");
|
|
var big = root.querySelector(".player__big");
|
|
var scrub = root.querySelector(".player__scrub");
|
|
var fill = root.querySelector(".player__fill");
|
|
var time = root.querySelector(".player__time");
|
|
var mute = root.querySelector(".player__mute");
|
|
var vol = root.querySelector(".player__vol");
|
|
var volFill = root.querySelector(".player__volfill");
|
|
var full = root.querySelector(".player__full");
|
|
if (!v) return;
|
|
|
|
function toggle() { if (v.paused) v.play(); else v.pause(); }
|
|
if (play) play.addEventListener("click", toggle);
|
|
if (big) big.addEventListener("click", toggle);
|
|
v.addEventListener("click", toggle);
|
|
|
|
// drive the scrub bar off rAF instead of `timeupdate` — timeupdate only
|
|
// fires a handful of times a second (browser-dependent), which made the
|
|
// fill visibly step instead of glide; rAF ties it to the display's own
|
|
// refresh rate, only running while actually playing.
|
|
var raf = null;
|
|
function sync() {
|
|
var p = v.duration ? v.currentTime / v.duration : 0;
|
|
if (fill) fill.style.width = (p * 100) + "%";
|
|
if (time) time.textContent = fmt(v.currentTime) + " / " + fmt(v.duration || 0);
|
|
}
|
|
function tick() { sync(); raf = requestAnimationFrame(tick); }
|
|
|
|
v.addEventListener("play", function () {
|
|
root.classList.add("is-playing"); if (play) play.textContent = "❚❚";
|
|
if (!raf) raf = requestAnimationFrame(tick);
|
|
});
|
|
v.addEventListener("pause", function () {
|
|
root.classList.remove("is-playing"); if (play) play.textContent = "▶";
|
|
if (raf) { cancelAnimationFrame(raf); raf = null; }
|
|
sync(); // one last sync so the bar lands exactly on the paused frame
|
|
});
|
|
v.addEventListener("ended", function () { if (raf) { cancelAnimationFrame(raf); raf = null; } });
|
|
v.addEventListener("loadedmetadata", function () { if (time) time.textContent = "00:00 / " + fmt(v.duration); });
|
|
v.addEventListener("seeked", sync); // scrubbing while paused still needs to reflect immediately
|
|
if (scrub) scrub.addEventListener("click", function (e) {
|
|
var r = scrub.getBoundingClientRect();
|
|
if (v.duration) v.currentTime = ((e.clientX - r.left) / r.width) * v.duration;
|
|
});
|
|
// volume level, not just mute — click or drag the little bar next to
|
|
// the mute button, same interaction as the scrub bar
|
|
function syncVol() {
|
|
var level = v.muted ? 0 : v.volume;
|
|
if (volFill) volFill.style.width = (level * 100) + "%";
|
|
if (mute) mute.textContent = (v.muted || v.volume === 0) ? "muted" : "vol";
|
|
}
|
|
function setVolFromEvent(e) {
|
|
var r = vol.getBoundingClientRect();
|
|
var level = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
|
|
v.volume = level;
|
|
v.muted = level <= 0;
|
|
}
|
|
if (vol) {
|
|
vol.addEventListener("click", setVolFromEvent);
|
|
vol.addEventListener("mousedown", function (e) {
|
|
setVolFromEvent(e);
|
|
function onMove(ev) { setVolFromEvent(ev); }
|
|
function onUp() { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); }
|
|
document.addEventListener("mousemove", onMove);
|
|
document.addEventListener("mouseup", onUp);
|
|
});
|
|
}
|
|
v.addEventListener("volumechange", syncVol);
|
|
syncVol();
|
|
|
|
if (mute) mute.addEventListener("click", function () { v.muted = !v.muted; syncVol(); });
|
|
if (full) full.addEventListener("click", function () {
|
|
if (document.fullscreenElement) document.exitFullscreen();
|
|
else if (root.requestFullscreen) root.requestFullscreen();
|
|
});
|
|
}
|
|
function initAll() {
|
|
var ps = document.querySelectorAll(".player");
|
|
for (var i = 0; i < ps.length; i++) if (!ps[i].__init) { ps[i].__init = 1; init(ps[i]); }
|
|
}
|
|
initAll();
|
|
document.addEventListener("pjax:load", initAll);
|
|
})();
|