Song Player
I've seen a lot of people asking for a music player widget, but the main one people were using has
been broken for a while. On top of that the music widgets I've seen haven't been customizeable using
just HTML/CSS. So I made this one! I'm not sure what instructional text I should put here!
Copy the HTML/CSS/JS below to get started!
Technically you don't need to copy the CSS, but it's what makes
the first example!
Use Inspect Element to see how the other examples are styled! >:D Mwahahahaha! (The laugh of someone who likes making people learn things)
<div class="StarrPlayer"> <div class="SP-track-info"> <div class="SP-track"><span class="SP-track-name">Really Cool Song</span></div> <div class="SP-artist">By: <span class="SP-artist-name">Really Cool Artist</span></div> <div class="SP-duration"> <span class="SP-duration-current"> 0:00 </span> <span class="SP-duration-total"> X:XX </span> </div> <div class="SP-progress-bar"> <div class="SP-progress-bar-fill"> <div class="SP-progress-bar-knob"></div> </div> </div> </div> <div class="SP-button-row"> <div class="SP-prev SP-button"><<</div> <div class="SP-play SP-button">Play</div> <div class="SP-pause SP-button">Pause</div> <div class="SP-stop SP-button">Stop</div> <div class="SP-next SP-button">>></div> </div> </div>
.SP-track-name, .SP-artist-name { display: inline-block; text-wrap: nowrap; overflow: hidden; background-color: #00000085; width: 99.5%; } .StarrPlayer { display: flex; justify-content: center; align-items: center; flex-direction: column; background-color: #00000085; padding: 1rem; color: white; width: 330px; } .SP-track-info { display: flex; justify-content: center; align-items: flex-start; flex-direction: column; background-color: #00000085; padding: 0; width: 100%; position: relative; } .SP-track { font-size: 1.5rem; font-weight: bold; width: 100%; background-color: #00000085; } .SP-artist { font-size: 1.25rem; font-style: italic; display: flex; justify-content: flex-start; flex-direction: row; flex-wrap: nowrap; width: 100%; overflow: hidden; background-color: #00000085; } .SP-duration { display: flex; justify-content: space-between; align-items: center; flex-direction: row; gap: 1rem; background-color: #00000085; width: 100%; } .SP-progress-bar { width: 100%; height: 0.2rem; background-color: #ffffff63; margin: 0.5rem 0; } .SP-progress-bar-fill { height: 100%; background-color: #8f41fb; position: relative; } .SP-progress-bar-knob { position: absolute; top: 50%; right: 0%; transform: translate(50%, -50%); width: 0.75rem; height: 0.75rem; background-color: #d093ff; border-radius: 50%; } .SP-button-row { display: flex; justify-content: center; align-items: center; flex-direction: row; background-color: #00000085; width: 100%; gap: 0.5rem; } .SP-button { background-color: #ffffff63; color: white; padding: 0.25rem 0.75rem; border: none; cursor: pointer; user-select: none; } .SP-button:hover { background-color: #a8a8a863; }
// This component is where the magic happens! // You shouldn't need to modify this unless you are adding new features! >🐢 class MusicPlayer { constructor(element, song_data) { this.parent_element = element; this.audio = null; this.current_track_index = -1; this.track_element = this.parent_element.querySelector(".SP-track-name"); this.track_scrolling_left = true; this.artist_element = this.parent_element.querySelector(".SP-artist-name"); this.artist_scrolling_left = true; this.song_data = song_data; this.isDragging = false; this.start(); } start = () => { this.initializeButtons(); this.initializeStatefulRenderingInterval(); this.initializeProgressBar(); } initializeStatefulRenderingInterval = () => { setInterval(() => { this.scrollTrackText(); this.scrollArtistText(); this.updateProgressBar(this.getSongProgressPercent(this.audio) * 100) this.updateProgressText(this.audio); this.updateDurationText(this.audio); this.renderPauseOrPlayButton(); }, 50); } initializeButtons = () => { let buttons = this.parent_element.querySelectorAll('.SP-button'); buttons.forEach((button) => { button.addEventListener('click', (event) => { switch (true) { case event.target.classList.contains("SP-play"): this.play_song(); break; case event.target.classList.contains("SP-pause"): this.pause_song(); break; case event.target.classList.contains("SP-next"): this.next_song(); break; case event.target.classList.contains("SP-prev"): this.prev_song(); break; case event.target.classList.contains("SP-stop"): this.stop_song(); break; default: break; } }); }); } initializeProgressBar = () => { let progressKnob = this.parent_element.querySelector('.SP-progress-bar-knob'); let progressBar = this.parent_element.querySelector('.SP-progress-bar'); progressKnob.addEventListener('pointerdown', (event) => { this.isDragging = true; event.preventDefault(); }); this.parent_element.addEventListener('pointermove', (event) => { if (!this.isDragging || !this.audio) return; const boundingRect = progressBar.getBoundingClientRect(); let percent = (event.clientX - boundingRect.left) / boundingRect.width; percent = Math.max(0, Math.min(1, percent)); this.audio.currentTime = this.audio.duration * percent; this.updateProgressBar(percent * 100); }); document.addEventListener('pointerup', () => { this.isDragging = false; }); progressBar.addEventListener('click', (event) => { if (this.audio == null || this.isDragging) return; // Skip if dragging or audio is not initialized const bounding = progressBar.getBoundingClientRect(); const percent = (event.clientX - bounding.left) / bounding.width; this.audio.currentTime = this.audio.duration * percent; this.updateProgressBar(percent * 100); }); } play_song = (track_number) => { // If the audio is paused and the track number is the same as the current track, just resume playing if (this.audio && this.audio.paused && (track_number === this.current_track_index || track_number == null)) { this.audio.play(); return; // If the audio is playing, but the play button is pressed, pause the audio } else if (this.audio && track_number == null) { this.audio.pause(); return; } // Loop the song list if the track number is out of bounds if (!track_number && this.current_track_index === -1) { track_number = 0; } else if (track_number >= this.song_data.length) { track_number = 0; } else if (track_number < 0) { track_number = this.song_data.length - 1; } // Stop the current song if it is playing if (this.audio && !this.audio.paused) { this.stop_song(); } // Play the song let song_url = this.song_data[track_number].url; this.parent_element.querySelector(".SP-track-name").innerHTML = this.song_data[track_number].name; this.parent_element.querySelector(".SP-artist-name").innerHTML = this.song_data[track_number].artist; this.audio = new Audio(song_url); this.audio.addEventListener('ended', () => { this.audio.currentTime = 0; this.next_song(); }); this.audio.play(); this.current_track_index = track_number; } pause_song = () => { this.audio.pause(); } next_song = () => { let next_track_number = this.current_track_index + 1; if (next_track_number >= this.song_data.length) { next_track_number = 0; } this.play_song(next_track_number); } prev_song = () => { let prev_track_number = this.current_track_index - 1; if (prev_track_number < 0) { prev_track_number = this.song_data.length - 1; } this.play_song(prev_track_number); } stop_song = () => { this.audio.pause(); this.audio = null; this.current_track_index = -1; } getSongProgressString = (audio_object) => { if (!audio_object) { return "0:00"; } const current_time = audio_object.currentTime || 0; return `${Math.floor(current_time / 60)}:${Math.floor(current_time % 60).toString().padStart(2, '0')}`; } getSongDurationString = (audio_object) => { if (!audio_object) { return "0:00"; } const duration = audio_object.duration || 0; return `${Math.floor(duration / 60)}:${Math.floor(duration % 60).toString().padStart(2, '0')}` } updateDurationText = (audio_object) => { let progressText = this.parent_element.querySelector('.SP-duration-total'); progressText.innerHTML = this.getSongDurationString(audio_object); } updateProgressText = (audio_object) => { let progressText = this.parent_element.querySelector('.SP-duration-current'); progressText.innerHTML = this.getSongProgressString(audio_object); } getSongProgressPercent = (audio_object) => { if (audio_object == null) return 0; const currentTime = audio_object.currentTime; const duration = audio_object.duration; return currentTime / duration; } updateProgressBar = (progressFloat) => { let progressTruncated = progressFloat.toFixed(2); let progressBar = this.parent_element.querySelector('.SP-progress-bar-fill'); progressBar.style.width = progressTruncated + "%"; } // This function scrolls the track text in the player when it is too long to fit in the container scrollTrackText = () => { if (this.track_element.scrollWidth > this.track_element.clientWidth + this.track_element.scrollLeft && this.track_scrolling_left) { this.track_element.scrollBy(1, 0); } else { this.track_scrolling_left = false; } if (!this.track_scrolling_left) { this.track_element.scrollBy(-0.5, 0); if (this.track_element.scrollLeft === 0) { this.track_scrolling_left = true; } } } // This function scrolls the artist text in the player when it is too long to fit in the container scrollArtistText = () => { if (this.artist_element.scrollWidth > this.artist_element.clientWidth + this.artist_element.scrollLeft && this.artist_scrolling_left) { this.artist_element.scrollBy(1, 0); } else { this.artist_scrolling_left = false; } if (!this.artist_scrolling_left) { this.artist_element.scrollBy(-0.5, 0); if (this.artist_element.scrollLeft === 0) { this.artist_scrolling_left = true; } } } // This hides the play button when the audio is playing and hides the pause button when the audio is paused renderPauseOrPlayButton = () => { let play_button = this.parent_element.querySelector('.SP-play'); let pause_button = this.parent_element.querySelector('.SP-pause'); if (this.audio && !this.audio.paused) { play_button.style.display = "none"; pause_button.style.display = "block"; } else { play_button.style.display = "block"; pause_button.style.display = "none"; } } } // Song files hosted on github >🐢 let song_data_absolute = [ { "name": "Guy Gets Promoted / End Title", "artist": "Tom Hiel", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/01%20Track%2001.mp3" }, { "name": "The Singing Sea", "artist": "Tulivu-Donna Cumberbatch", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/02%20Track%2002.mp3" }, { "name": "La Valse d'Amelie", "artist": "Yann Tiersen", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/03%20Track%2003.mp3" }, { "name": "Come Undone", "artist": "Duran Duran", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/04%20Track%2004.mp3" }, { "name": "This Is a Lie", "artist": "The Cure", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/05%20Track%2005.mp3" }, { "name": "Millennia", "artist": "Hotel de Ville", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/06%20Track%2006.mp3" }, { "name": "Trust", "artist": "The Cure", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/07%20Track%2007.mp3" }, { "name": "Perfect Disguise", "artist": "Modest Mouse", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/08%20Track%2008.mp3" }, { "name": "The Last Beat of My Heart", "artist": "Siouxsie and the Banshees", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/09%20Track%2009.mp3" }, { "name": "The World Has Turned and Left Me Here", "artist": "Christopher John", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/10%20Track%2010.mp3" }, { "name": "Moonlight Sonata", "artist": "Depeche Mode", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/11%20Track%2011.mp3" }, { "name": "Fear of the South", "artist": "Tin Hat Trio", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/12%20Track%2012.mp3" }, { "name": "One More Time", "artist": "The Cure", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/13%20Track%2013.mp3" }, { "name": "Max Payne 2 Theme", "artist": "Kärtsy Hatakka & Kimmo Kajasto", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/14%20Track%2014.mp3" }, { "name": "If Only Tonight We Could Sleep", "artist": "The Cure", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/15%20Track%2015.mp3" }, { "name": "This Side of the Blue", "artist": "Joanna Newsom", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/16%20Track%2016.mp3" }, { "name": "Playground Love", "artist": "Air", "url": "https://github.com/alexbatesdev/neocity-website/raw/master/Sleep%20CD/17%20Track%2017.mp3" } ]; // Song files hosted on nekoweb >🐢 let song_data_relative = [ { "name": "Guy Gets Promoted / End Title", "artist": "Tom Hiel", "url": "../Sleep CD/01 Track 01.mp3" }, { "name": "The Singing Sea", "artist": "Tulivu-Donna Cumberbatch", "url": "../Sleep CD/02 Track 02.mp3" }, { "name": "La Valse d'Amelie", "artist": "Yann Tiersen", "url": "../Sleep CD/03 Track 03.mp3" }, { "name": "Come Undone", "artist": "Duran Duran", "url": "../Sleep CD/04 Track 04.mp3" }, { "name": "This Is a Lie", "artist": "The Cure", "url": "../Sleep CD/05 Track 05.mp3" }, { "name": "Millennia", "artist": "Hotel de Ville", "url": "../Sleep CD/06 Track 06.mp3" }, { "name": "Trust", "artist": "The Cure", "url": "../Sleep CD/07 Track 07.mp3" }, { "name": "Perfect Disguise", "artist": "Modest Mouse", "url": "../Sleep CD/08 Track 08.mp3" }, { "name": "The Last Beat of My Heart", "artist": "Siouxsie and the Banshees", "url": "../Sleep CD/09 Track 09.mp3" }, { "name": "The World Has Turned and Left Me Here", "artist": "Christopher John", "url": "../Sleep CD/10 Track 10.mp3" }, { "name": "Moonlight Sonata", "artist": "Depeche Mode", "url": "../Sleep CD/11 Track 11.mp3" }, { "name": "Fear of the South", "artist": "Tin Hat Trio", "url": "../Sleep CD/12 Track 12.mp3" }, { "name": "One More Time", "artist": "The Cure", "url": "../Sleep CD/13 Track 13.mp3" }, { "name": "Max Payne 2 Theme", "artist": "Kärtsy Hatakka & Kimmo Kajasto", "url": "../Sleep CD/14 Track 14.mp3" }, { "name": "If Only Tonight We Could Sleep", "artist": "The Cure", "url": "../Sleep CD/15 Track 15.mp3" }, { "name": "This Side of the Blue", "artist": "Joanna Newsom", "url": "../Sleep CD/16 Track 16.mp3" }, { "name": "Playground Love", "artist": "Air", "url": "../Sleep CD/17 Track 17.mp3" } ]; // Automatically initialize all song players on the page with the same data >🐢 // let song_player_elements = document.querySelectorAll(".StarrPlayer"); // let audio_players = []; // song_player_elements.forEach((element) => { // let audio_player = new MusicPlayer(element, song_data_relative); // audio_players.push(audio_player); // }); // Load the song players manually >🐢 let song_player_basic = document.querySelector(".StarrPlayer"); let basic_player = new MusicPlayer(song_player_basic, song_data_relative); // This one loads from github via an absolute path >🐢 let song_player_stone = document.querySelector("#SP-stone.StarrPlayer"); let stone_player = new MusicPlayer(song_player_stone, song_data_absolute); // This one loads from nekoweb via a relative path >🐢 let song_player_tv = document.querySelector("#SP-tv.StarrPlayer"); let tv_player = new MusicPlayer(song_player_tv, song_data_relative);