Add minimised player option, remove transition on about image due to transition bug, make about image bigger
Build and Deploy to Web Server / deploy (push) Successful in 6m21s
Build and Deploy to Web Server / deploy (push) Successful in 6m21s
This commit is contained in:
3 files changed
+394
-71
No files matched your search
+392
-67
@@ -39,20 +39,24 @@ const playerArtworkBlur = "10px";
|
|||||||
const playerArtworkOpacity = 0.4;
|
const playerArtworkOpacity = 0.4;
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- Morphing player shell: bar <-> circle, layers crossfade inside -->
|
||||||
<div
|
<div
|
||||||
id="player"
|
id="player-shell"
|
||||||
class:list={[
|
class:list={[
|
||||||
"fixed right-0 bottom-0 left-0 z-100 overflow-hidden bg-black shadow-lg/75 md:right-3 md:bottom-3 md:left-3 md:rounded",
|
"group fixed z-100 overflow-hidden bg-black shadow-lg/75",
|
||||||
height
|
height
|
||||||
]}
|
]}
|
||||||
transition:persist=""
|
transition:persist=""
|
||||||
transition:name="player"
|
transition:name="player"
|
||||||
transition:animate="none"
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
|
<!-- Full-bar layer -->
|
||||||
|
<div class="player-full absolute inset-0">
|
||||||
|
<!-- Blurred artwork backdrop (static: animations restart on view transitions) -->
|
||||||
<div class="absolute inset-0 -z-90 overflow-hidden">
|
<div class="absolute inset-0 -z-90 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
id="player:artwork:background"
|
id="player:artwork:background"
|
||||||
class="animate-bg-perlin absolute top-0 right-0 bottom-0 left-0 -z-90"
|
class="absolute top-0 right-0 bottom-0 left-0 -z-90"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,11 +81,24 @@ const playerArtworkOpacity = 0.4;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 pt-2 pb-2 text-white md:px-3 md:pb-3">
|
<div class="px-2 pt-2 pb-2 text-white md:px-3 md:pb-3">
|
||||||
<div class="grid grid-cols-2 text-xs">
|
<div class="grid grid-cols-[1fr_auto_1fr] items-center text-xs">
|
||||||
<span id="player:times:current" class="col-span-1 text-left">0:00</span>
|
<span id="player:times:current" class="col-span-1 text-left">0:00</span>
|
||||||
<span id="player:times:duration" class="col-span-1 text-right">0:00</span>
|
<button
|
||||||
|
id="player:controls:minimise"
|
||||||
|
type="button"
|
||||||
|
title="Minimise player"
|
||||||
|
aria-label="Minimise player"
|
||||||
|
class="col-span-1 rounded-full px-3 py-1 text-gray-400 hover:cursor-pointer hover:text-white"
|
||||||
|
>
|
||||||
|
<Icon name="fa7-solid:chevron-down" class="pointer-events-none" />
|
||||||
|
</button>
|
||||||
|
<span id="player:times:duration" class="col-span-1 text-right"
|
||||||
|
>0:00</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-full flex-row items-center justify-center align-middle">
|
<div
|
||||||
|
class="flex h-full flex-row items-center justify-center align-middle"
|
||||||
|
>
|
||||||
<div class="m-0 flex grow-0 flex-row space-x-2 p-0 text-3xl">
|
<div class="m-0 flex grow-0 flex-row space-x-2 p-0 text-3xl">
|
||||||
<span>
|
<span>
|
||||||
<Icon name="fa7-solid:backward" id="player:controls:previous" />
|
<Icon name="fa7-solid:backward" id="player:controls:previous" />
|
||||||
@@ -99,15 +116,173 @@ const playerArtworkOpacity = 0.4;
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-grow flex-col text-sm">
|
<div class="flex flex-grow flex-col text-sm">
|
||||||
<span id="player:metadata:title" class="w-full text-right font-semibold"
|
<span
|
||||||
></span>
|
id="player:metadata:title"
|
||||||
|
class="w-full text-right font-semibold"></span>
|
||||||
<span id="player:metadata:subtitle" class="w-full text-right"></span>
|
<span id="player:metadata:subtitle" class="w-full text-right"></span>
|
||||||
<span id="player:metadata:extra" class="w-full text-right"></span>
|
<span id="player:metadata:extra" class="w-full text-right"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class:list={[height, "bg-gray-900"]}></div>
|
<!-- Circle face: artwork + progress ring -->
|
||||||
|
<div class="player-face absolute inset-0" aria-hidden="true">
|
||||||
|
<div class="absolute inset-0 rounded-full bg-black ring-1 ring-white/10">
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
id="player:face:artwork"
|
||||||
|
alt=""
|
||||||
|
class="absolute inset-[5px] h-[calc(100%-10px)] w-[calc(100%-10px)] rounded-full bg-gray-900 object-cover"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="player:face:fallback"
|
||||||
|
class="absolute inset-[5px] flex items-center justify-center rounded-full bg-gray-900 text-xl text-gray-400"
|
||||||
|
>
|
||||||
|
<Icon name="fa7-solid:music" class="pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="absolute inset-0 h-full w-full -rotate-90"
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
r="29"
|
||||||
|
stroke="rgba(255,255,255,0.15)"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<circle
|
||||||
|
id="player:face:progress:buffered"
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
r="29"
|
||||||
|
stroke="rgba(255,255,255,0.35)"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-dasharray="182.2"
|
||||||
|
stroke-dashoffset="182.2"></circle>
|
||||||
|
<circle
|
||||||
|
id="player:face:progress:played"
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
r="29"
|
||||||
|
stroke="#ff2d00"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-dasharray="182.2"
|
||||||
|
stroke-dashoffset="182.2"></circle>
|
||||||
|
</svg>
|
||||||
|
<button
|
||||||
|
id="player:face:controls:toggle"
|
||||||
|
type="button"
|
||||||
|
title="Play / pause"
|
||||||
|
aria-label="Play / pause"
|
||||||
|
class="absolute top-1/2 left-1/2 flex h-8 w-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-black/55 text-white opacity-100 backdrop-blur-sm hover:cursor-pointer hover:bg-black/70 md:h-9 md:w-9 md:opacity-0 md:group-focus-within:opacity-100 md:group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="fa7-solid:play"
|
||||||
|
id="player:face:controls:play"
|
||||||
|
class="pointer-events-none text-sm drop-shadow"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="fa7-solid:pause"
|
||||||
|
id="player:face:controls:pause"
|
||||||
|
class="pointer-events-none hidden text-sm drop-shadow"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="player:spacer" class:list={[height, "bg-gray-900"]}></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#player-shell {
|
||||||
|
--ease-slide: cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
|
--ease-round: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
transition:
|
||||||
|
left 0.55s var(--ease-slide),
|
||||||
|
width 0.55s var(--ease-slide),
|
||||||
|
bottom 0.55s var(--ease-slide),
|
||||||
|
height 0.55s var(--ease-slide),
|
||||||
|
border-radius 0.6s var(--ease-round);
|
||||||
|
}
|
||||||
|
#player-shell.player--minimised {
|
||||||
|
left: calc(100% - 9rem);
|
||||||
|
width: 8rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
height: 8rem;
|
||||||
|
border-radius: 4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
#player-shell {
|
||||||
|
left: 0.75rem;
|
||||||
|
width: calc(100% - 1.5rem);
|
||||||
|
bottom: 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
#player-shell.player--minimised {
|
||||||
|
left: calc(100% - 9.5rem);
|
||||||
|
width: 8rem;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
height: 8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#player\:spacer.player-spacer--hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outgoing layer fades fast, incoming fades in late */
|
||||||
|
.player-full {
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease 0.3s,
|
||||||
|
visibility 0s linear 0s;
|
||||||
|
}
|
||||||
|
#player-shell.player--minimised .player-full {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
opacity 0.15s ease 0s,
|
||||||
|
visibility 0s linear 0.15s;
|
||||||
|
}
|
||||||
|
.player-face {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
opacity 0.15s ease 0s,
|
||||||
|
visibility 0s linear 0.15s;
|
||||||
|
}
|
||||||
|
#player-shell.player--minimised .player-face {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease 0.3s,
|
||||||
|
visibility 0s linear 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#player-shell,
|
||||||
|
.player-full,
|
||||||
|
.player-face {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No morph on first paint */
|
||||||
|
#player-shell.player--no-motion,
|
||||||
|
#player-shell.player--no-motion .player-full,
|
||||||
|
#player-shell.player--no-motion .player-face {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script is:inline src="/js/howler.min.js"></script>
|
<script is:inline src="/js/howler.min.js"></script>
|
||||||
<script
|
<script
|
||||||
@@ -119,34 +294,57 @@ const playerArtworkOpacity = 0.4;
|
|||||||
* Includes all methods for playing, skipping, updating the display, etc.
|
* Includes all methods for playing, skipping, updating the display, etc.
|
||||||
* @param {Array} playlist Array of objects with playlist song details ({title, file, howl}).
|
* @param {Array} playlist Array of objects with playlist song details ({title, file, howl}).
|
||||||
*/
|
*/
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
class Player {
|
class Player {
|
||||||
constructor(playlist) {
|
constructor(playlist) {
|
||||||
this.playlist = window.player?.playlist ?? playlist;
|
this.playlist = window.player?.playlist ?? playlist;
|
||||||
this.index = window.player?.index ?? 0;
|
this.index = window.player?.index ?? 0;
|
||||||
|
this.minimised = window.player?.minimised ?? false;
|
||||||
|
|
||||||
|
this.RING_C = 182.2; // 2 * PI * 29, must match dasharray in markup
|
||||||
|
|
||||||
this.UI = {
|
this.UI = {
|
||||||
|
root: $("player-shell"),
|
||||||
|
spacer: $("player:spacer"),
|
||||||
|
full: document.querySelector("#player-shell .player-full"),
|
||||||
|
face: document.querySelector("#player-shell .player-face"),
|
||||||
controls: {
|
controls: {
|
||||||
play: document.getElementById("player:controls:play"),
|
play: $("player:controls:play"),
|
||||||
pause: document.getElementById("player:controls:pause"),
|
pause: $("player:controls:pause"),
|
||||||
previous: document.getElementById("player:controls:previous"),
|
previous: $("player:controls:previous"),
|
||||||
next: document.getElementById("player:controls:next")
|
next: $("player:controls:next"),
|
||||||
|
minimise: $("player:controls:minimise")
|
||||||
},
|
},
|
||||||
times: {
|
times: {
|
||||||
current: document.getElementById("player:times:current"),
|
current: $("player:times:current"),
|
||||||
duration: document.getElementById("player:times:duration")
|
duration: $("player:times:duration")
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
title: document.getElementById("player:metadata:title"),
|
title: $("player:metadata:title"),
|
||||||
subtitle: document.getElementById("player:metadata:subtitle"),
|
subtitle: $("player:metadata:subtitle"),
|
||||||
extra: document.getElementById("player:metadata:extra")
|
extra: $("player:metadata:extra")
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
bar: document.getElementById("player:time:progress"),
|
bar: $("player:time:progress"),
|
||||||
buffered: document.getElementById("player:time:progress:buffered"),
|
buffered: $("player:time:progress:buffered"),
|
||||||
played: document.getElementById("player:time:progress:played")
|
played: $("player:time:progress:played")
|
||||||
},
|
},
|
||||||
artwork: {
|
artwork: {
|
||||||
background: document.getElementById("player:artwork:background")
|
background: $("player:artwork:background")
|
||||||
|
},
|
||||||
|
faceArtwork: {
|
||||||
|
img: $("player:face:artwork"),
|
||||||
|
fallback: $("player:face:fallback")
|
||||||
|
},
|
||||||
|
faceProgress: {
|
||||||
|
buffered: $("player:face:progress:buffered"),
|
||||||
|
played: $("player:face:progress:played")
|
||||||
|
},
|
||||||
|
faceControls: {
|
||||||
|
toggle: $("player:face:controls:toggle"),
|
||||||
|
play: $("player:face:controls:play"),
|
||||||
|
pause: $("player:face:controls:pause")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,38 +356,161 @@ const playerArtworkOpacity = 0.4;
|
|||||||
Math.round(playlist[this.index].duration)
|
Math.round(playlist[this.index].duration)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (playlist[this.index].metadata.artwork) {
|
this.updateArtwork(playlist[this.index]);
|
||||||
this.UI.artwork.background.style.backgroundImage = `url('${playlist[this.index].metadata.artwork.src}')`;
|
this.syncPlayPauseUI(false);
|
||||||
this.UI.artwork.background.style.backgroundSize = "cover";
|
|
||||||
this.UI.artwork.background.style.backgroundPosition = "center";
|
|
||||||
this.UI.artwork.background.style.filter = `blur(${playerArtworkBlur})`;
|
|
||||||
this.UI.artwork.background.style.opacity = playerArtworkOpacity;
|
|
||||||
} else {
|
|
||||||
this.UI.artwork.background.style.backgroundImage = "";
|
|
||||||
this.UI.artwork.background.style.filter = "";
|
|
||||||
this.UI.artwork.background.style.opacity = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.UI.controls.play.addEventListener("click", function () {
|
try {
|
||||||
|
const stored = localStorage.getItem("player-minimised");
|
||||||
|
if (stored !== null && window.player === undefined) {
|
||||||
|
this.minimised = stored === "1";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
// No morph on first paint.
|
||||||
|
this.UI.root.classList.add("player--no-motion");
|
||||||
|
this.applyminimised();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.UI.root?.classList.remove("player--no-motion");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.UI.controls.play.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
window.player.play();
|
window.player.play();
|
||||||
});
|
});
|
||||||
this.UI.controls.pause.addEventListener("click", function () {
|
this.UI.controls.pause.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
window.player.pause();
|
window.player.pause();
|
||||||
});
|
});
|
||||||
this.UI.controls.previous.addEventListener("click", function () {
|
this.UI.controls.previous.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
window.player.skip("prev");
|
window.player.skip("prev");
|
||||||
});
|
});
|
||||||
this.UI.controls.next.addEventListener("click", function () {
|
this.UI.controls.next.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
window.player.skip("next");
|
window.player.skip("next");
|
||||||
});
|
});
|
||||||
|
this.UI.controls.minimise.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.player.setminimised(true);
|
||||||
|
});
|
||||||
|
|
||||||
this.UI.progress.bar.addEventListener("click", function (event) {
|
this.UI.progress.bar.addEventListener("click", function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
window.player.seek(event.clientX / window.innerWidth);
|
window.player.seek(event.clientX / window.innerWidth);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tap the circle to expand (not the play button).
|
||||||
|
const expand = (e) => {
|
||||||
|
if (!window.player.minimised) return;
|
||||||
|
if (
|
||||||
|
e.target instanceof Element &&
|
||||||
|
this.UI.faceControls.toggle.contains(e.target)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.type === "keydown" && e.key !== "Enter" && e.key !== " ") return;
|
||||||
|
e.preventDefault();
|
||||||
|
window.player.setminimised(false);
|
||||||
|
};
|
||||||
|
this.UI.root.addEventListener("click", expand);
|
||||||
|
this.UI.root.addEventListener("keydown", expand);
|
||||||
|
this.UI.faceControls.toggle.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const sound = window.player.playlist[window.player.index].howl;
|
||||||
|
if (sound && sound.playing()) {
|
||||||
|
window.player.pause();
|
||||||
|
} else {
|
||||||
|
window.player.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.playerIsInitialised = true;
|
window.playerIsInitialised = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setminimised(minimised) {
|
||||||
|
this.minimised = minimised;
|
||||||
|
try {
|
||||||
|
localStorage.setItem("player-minimised", minimised ? "1" : "0");
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
this.applyminimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One class drives the morph; re-applying is a no-op.
|
||||||
|
applyminimised() {
|
||||||
|
if (!this.UI.root) return;
|
||||||
|
if (this.minimised) {
|
||||||
|
this.UI.root.classList.add("player--minimised");
|
||||||
|
this.UI.root.setAttribute("role", "button");
|
||||||
|
this.UI.root.setAttribute("tabindex", "0");
|
||||||
|
this.UI.root.setAttribute(
|
||||||
|
"title",
|
||||||
|
"Expand player — click to restore full size"
|
||||||
|
);
|
||||||
|
this.UI.root.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
"Expand player — click to restore full size"
|
||||||
|
);
|
||||||
|
this.UI.spacer?.classList.add("player-spacer--hidden");
|
||||||
|
this.UI.full?.setAttribute("aria-hidden", "true");
|
||||||
|
this.UI.face?.setAttribute("aria-hidden", "false");
|
||||||
|
} else {
|
||||||
|
this.UI.root.classList.remove("player--minimised");
|
||||||
|
this.UI.root.removeAttribute("role");
|
||||||
|
this.UI.root.removeAttribute("tabindex");
|
||||||
|
this.UI.root.removeAttribute("title");
|
||||||
|
this.UI.root.removeAttribute("aria-label");
|
||||||
|
this.UI.spacer?.classList.remove("player-spacer--hidden");
|
||||||
|
this.UI.full?.removeAttribute("aria-hidden");
|
||||||
|
this.UI.face?.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArtwork(data) {
|
||||||
|
const artworkSrc = data.metadata.artwork?.src;
|
||||||
|
const bg = this.UI.artwork.background;
|
||||||
|
const img = this.UI.faceArtwork.img;
|
||||||
|
const fallback = this.UI.faceArtwork.fallback;
|
||||||
|
|
||||||
|
bg.style.backgroundImage = artworkSrc ? `url('${artworkSrc}')` : "";
|
||||||
|
bg.style.backgroundSize = "cover";
|
||||||
|
bg.style.backgroundPosition = "center";
|
||||||
|
bg.style.filter = artworkSrc ? `blur(${playerArtworkBlur})` : "";
|
||||||
|
bg.style.opacity = artworkSrc ? playerArtworkOpacity : "0";
|
||||||
|
|
||||||
|
if (!img) return;
|
||||||
|
if (artworkSrc) {
|
||||||
|
const alt =
|
||||||
|
data.metadata.artwork?.alt || data.metadata.title || "Track artwork";
|
||||||
|
img.src = artworkSrc;
|
||||||
|
img.style.display = "";
|
||||||
|
img.alt = typeof alt === "string" ? alt : "";
|
||||||
|
if (fallback) fallback.style.display = "none";
|
||||||
|
} else {
|
||||||
|
img.removeAttribute("src");
|
||||||
|
img.style.display = "none";
|
||||||
|
img.alt = "";
|
||||||
|
if (fallback) fallback.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncPlayPauseUI(isPlaying) {
|
||||||
|
for (const set of [this.UI.controls, this.UI.faceControls]) {
|
||||||
|
set.pause.classList.toggle("hidden", !isPlaying);
|
||||||
|
set.play.classList.toggle("hidden", isPlaying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRingProgress(el, per) {
|
||||||
|
if (!el) return;
|
||||||
|
const clamped = Math.min(1, Math.max(0, per || 0));
|
||||||
|
el.style.strokeDashoffset = String(this.RING_C * (1 - clamped));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a song in the playlist.
|
* Play a song in the playlist.
|
||||||
* @param {Number} index Index of the song in the playlist (leave empty to play the first or current).
|
* @param {Number} index Index of the song in the playlist (leave empty to play the first or current).
|
||||||
@@ -217,21 +538,14 @@ const playerArtworkOpacity = 0.4;
|
|||||||
// Start updating the progress of the track.
|
// Start updating the progress of the track.
|
||||||
requestAnimationFrame(this.step.bind(this));
|
requestAnimationFrame(this.step.bind(this));
|
||||||
|
|
||||||
this.UI.controls.pause.classList.remove("hidden");
|
this.syncPlayPauseUI(true);
|
||||||
this.UI.controls.play.classList.add("hidden");
|
|
||||||
},
|
},
|
||||||
// onload: () => {
|
|
||||||
// loading.style.display = "none";
|
|
||||||
// },
|
|
||||||
onend: () => {
|
onend: () => {
|
||||||
this.skip("next");
|
this.skip("next");
|
||||||
},
|
},
|
||||||
onpause: () => {
|
onpause: () => {
|
||||||
this.UI.controls.pause.classList.add("hidden");
|
this.syncPlayPauseUI(false);
|
||||||
this.UI.controls.play.classList.remove("hidden");
|
|
||||||
},
|
},
|
||||||
// onstop: () => {
|
|
||||||
// },
|
|
||||||
onseek: () => {
|
onseek: () => {
|
||||||
// Start updating the progress of the track.
|
// Start updating the progress of the track.
|
||||||
requestAnimationFrame(this.step.bind(this));
|
requestAnimationFrame(this.step.bind(this));
|
||||||
@@ -250,18 +564,10 @@ const playerArtworkOpacity = 0.4;
|
|||||||
Math.round(data.duration)
|
Math.round(data.duration)
|
||||||
);
|
);
|
||||||
this.UI.progress.buffered.style.width = "0%";
|
this.UI.progress.buffered.style.width = "0%";
|
||||||
|
this.setRingProgress(this.UI.faceProgress.buffered, 0);
|
||||||
|
this.setRingProgress(this.UI.faceProgress.played, 0);
|
||||||
|
|
||||||
if (data.metadata.artwork) {
|
this.updateArtwork(data);
|
||||||
this.UI.artwork.background.style.backgroundImage = `url('${data.metadata.artwork.src}')`;
|
|
||||||
this.UI.artwork.background.style.backgroundSize = "cover";
|
|
||||||
this.UI.artwork.background.style.backgroundPosition = "center";
|
|
||||||
this.UI.artwork.background.style.filter = `blur(${playerArtworkBlur})`;
|
|
||||||
this.UI.artwork.background.style.opacity = playerArtworkOpacity;
|
|
||||||
} else {
|
|
||||||
this.UI.artwork.background.style.backgroundImage = "";
|
|
||||||
this.UI.artwork.background.style.filter = "";
|
|
||||||
this.UI.artwork.background.style.opacity = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep track of the index we are currently playing.
|
// Keep track of the index we are currently playing.
|
||||||
this.index = index;
|
this.index = index;
|
||||||
@@ -276,8 +582,7 @@ const playerArtworkOpacity = 0.4;
|
|||||||
|
|
||||||
sound.pause();
|
sound.pause();
|
||||||
|
|
||||||
this.UI.controls.pause.classList.add("hidden");
|
this.syncPlayPauseUI(false);
|
||||||
this.UI.controls.play.classList.remove("hidden");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,6 +621,7 @@ const playerArtworkOpacity = 0.4;
|
|||||||
|
|
||||||
// Reset progress.
|
// Reset progress.
|
||||||
this.UI.progress.played.style.width = "0%";
|
this.UI.progress.played.style.width = "0%";
|
||||||
|
this.setRingProgress(this.UI.faceProgress.played, 0);
|
||||||
this.UI.times.current.textContent = this.formatTime(0);
|
this.UI.times.current.textContent = this.formatTime(0);
|
||||||
|
|
||||||
// Play the new track.
|
// Play the new track.
|
||||||
@@ -345,10 +651,16 @@ const playerArtworkOpacity = 0.4;
|
|||||||
|
|
||||||
var seek = sound.seek() || 0;
|
var seek = sound.seek() || 0;
|
||||||
this.UI.times.current.textContent = this.formatTime(Math.round(seek));
|
this.UI.times.current.textContent = this.formatTime(Math.round(seek));
|
||||||
this.UI.progress.played.style.width =
|
const playedPer = seek / sound.duration() || 0;
|
||||||
((seek / sound.duration()) * 100 || 0) + "%";
|
this.UI.progress.played.style.width = playedPer * 100 + "%";
|
||||||
|
this.setRingProgress(this.UI.faceProgress.played, playedPer);
|
||||||
|
|
||||||
this.UI.progress.buffered.style.width = `${this.getBufferedAmount(sound).bufferedPercent}%`;
|
const bufferedPercent = this.getBufferedAmount(sound).bufferedPercent;
|
||||||
|
this.UI.progress.buffered.style.width = `${bufferedPercent}%`;
|
||||||
|
this.setRingProgress(
|
||||||
|
this.UI.faceProgress.buffered,
|
||||||
|
bufferedPercent / 100
|
||||||
|
);
|
||||||
|
|
||||||
// If the sound is still playing, continue stepping.
|
// If the sound is still playing, continue stepping.
|
||||||
if (sound.playing()) {
|
if (sound.playing()) {
|
||||||
@@ -378,13 +690,14 @@ const playerArtworkOpacity = 0.4;
|
|||||||
|
|
||||||
getBufferedAmount(sound) {
|
getBufferedAmount(sound) {
|
||||||
if (sound._sounds.length > 0) {
|
if (sound._sounds.length > 0) {
|
||||||
const audioNode = sound._sounds[0]._node; // The underlying HTML5 Audio element
|
const audioNode = sound._sounds[0]._node; // Underlying HTML5 Audio element
|
||||||
if (audioNode.buffered.length > 0) {
|
if (audioNode.buffered.length > 0) {
|
||||||
const bufferedEnd = audioNode.buffered.end(0); // Time (seconds) where buffering ends
|
const bufferedEnd = audioNode.buffered.end(0);
|
||||||
const duration = audioNode.duration;
|
const duration = audioNode.duration;
|
||||||
const bufferedSeconds = bufferedEnd;
|
return {
|
||||||
const bufferedPercent = duration ? (bufferedEnd / duration) * 100 : 0;
|
bufferedSeconds: bufferedEnd,
|
||||||
return { bufferedSeconds, bufferedPercent };
|
bufferedPercent: duration ? (bufferedEnd / duration) * 100 : 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { bufferedSeconds: 0, bufferedPercent: 0 };
|
return { bufferedSeconds: 0, bufferedPercent: 0 };
|
||||||
@@ -411,5 +724,17 @@ const playerArtworkOpacity = 0.4;
|
|||||||
|
|
||||||
if (!window.playerIsInitialised) {
|
if (!window.playerIsInitialised) {
|
||||||
window.player = new Player(initialQueue);
|
window.player = new Player(initialQueue);
|
||||||
|
} else {
|
||||||
|
// View-transition restore: re-grab nodes and re-apply.
|
||||||
|
try {
|
||||||
|
const p = window.player;
|
||||||
|
p.UI.root = $("player-shell");
|
||||||
|
p.UI.spacer = $("player:spacer");
|
||||||
|
p.UI.full = document.querySelector("#player-shell .player-full");
|
||||||
|
p.UI.face = document.querySelector("#player-shell .player-face");
|
||||||
|
p.applyminimised();
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -19,9 +19,8 @@ import { Image } from "astro:assets";
|
|||||||
<Image
|
<Image
|
||||||
src={aboutImage}
|
src={aboutImage}
|
||||||
alt="A photo of Nathan conducting the Woodville Concert Band"
|
alt="A photo of Nathan conducting the Woodville Concert Band"
|
||||||
class="mx-auto mb-8 size-96 rounded-full object-cover shadow-lg/75 transition ease-in-out md:hover:scale-150"
|
class="mx-auto mb-8 size-125 rounded-full object-cover shadow-lg/75 transition ease-in-out md:hover:scale-150"
|
||||||
transition:name="aboutImage"
|
height={750}
|
||||||
height={576}
|
|
||||||
/>
|
/>
|
||||||
<SectionTitle class="py-8">Dr Nathan Cummins</SectionTitle>
|
<SectionTitle class="py-8">Dr Nathan Cummins</SectionTitle>
|
||||||
<H2>{site.tagline}</H2>
|
<H2>{site.tagline}</H2>
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ const tracks = (
|
|||||||
src={aboutImage}
|
src={aboutImage}
|
||||||
alt="Nathan conducting the Woodville Concert Band"
|
alt="Nathan conducting the Woodville Concert Band"
|
||||||
class="mx-auto mt-8 size-96 rounded-full object-cover shadow-lg/75 transition ease-in-out md:hover:scale-150"
|
class="mx-auto mt-8 size-96 rounded-full object-cover shadow-lg/75 transition ease-in-out md:hover:scale-150"
|
||||||
transition:name="aboutImage"
|
|
||||||
height={576}
|
height={576}
|
||||||
/>
|
/>
|
||||||
<span class="block w-full text-center"
|
<span class="block w-full text-center"
|
||||||
|
|||||||
Reference in new issue
Block a user