/* 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); } // the pause icon is drawn in CSS (two bars via ::before/::after on // .is-playing), not a "❚❚" glyph — a monospace font's letter-spacing // between two heavy-bar characters left a gap no amount of // letter-spacing closed cleanly. the ▶ span just hides/shows. v.addEventListener("play", function () { root.classList.add("is-playing"); if (!raf) raf = requestAnimationFrame(tick); }); v.addEventListener("pause", function () { root.classList.remove("is-playing"); 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); })();