Custom Code Experiment •
Music: Hakim Bey
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Hakim Bey Music Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
height: 100vh;
width: 100vw;
position: fixed;
}
#canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* Track Selection Interface - More Compact */
#trackInterface {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 400px;
max-height: 70vh;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(15px);
border-radius: 15px;
padding: 15px;
z-index: 10;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
#trackInterface.hidden {
opacity: 0;
visibility: hidden;
transform: translateX(-50%) translateY(-10px);
}
.header {
text-align: center;
margin-bottom: 20px;
position: relative;
}
.header h1 {
font-size: 22px;
margin-bottom: 6px;
color: #fff;
font-weight: 600;
letter-spacing: -0.5px;
}
.header p {
color: #888;
font-size: 13px;
}
.tracks-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.track-item {
background: rgba(255, 255, 255, 0.08);
border-radius: 10px;
padding: 12px 15px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
position: relative;
}
.track-item:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateY(-1px);
}
.track-item.active {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.track-item.playing {
background: rgba(0, 102, 255, 0.2);
border-color: rgba(0, 102, 255, 0.5);
}
.track-number {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: #fff;
font-size: 13px;
flex-shrink: 0;
}
.track-info {
flex: 1;
min-width: 0;
}
.track-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #fff;
}
.track-artist {
font-size: 12px;
color: #888;
}
.play-icon {
width: 24px;
height: 24px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
}
.play-icon svg {
width: 12px;
height: 12px;
fill: #fff;
margin-left: 1px;
}
/* Player Controls - More Compact */
#playerControls {
position: fixed;
bottom: 20px;
left: 0;
width: 100%;
padding: 0 20px;
z-index: 5;
display: flex;
justify-content: center;
align-items: center;
}
.controls-container {
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(15px);
border-radius: 40px;
padding: 12px 20px;
width: 100%;
max-width: 400px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.control-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.control-btn.play-pause {
background: #fff;
width: 48px;
height: 48px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.control-btn.play-pause:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5);
}
.control-btn svg {
width: 20px;
height: 20px;
fill: #000;
}
.control-btn:not(.play-pause) svg {
fill: #fff;
}
.control-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.control-btn:disabled:hover {
transform: none;
box-shadow: none;
}
.track-display {
flex: 1;
min-width: 0;
overflow: hidden;
}
.current-track {
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
color: #fff;
font-weight: 500;
}
.track-time {
font-size: 11px;
color: #888;
letter-spacing: 0.5px;
}
.progress-container {
position: absolute;
top: -6px;
left: 0;
width: 100%;
}
.progress-bar {
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
border-radius: 1.5px;
overflow: hidden;
cursor: pointer;
}
.progress {
width: 0%;
height: 100%;
background: #fff;
transition: width 0.1s linear;
}
/* Track List Toggle */
#trackListToggle {
position: fixed;
top: 20px;
right: 20px;
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(15px);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 20;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
transition: all 0.2s ease;
}
#trackListToggle:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5);
}
#trackListToggle svg {
width: 20px;
height: 20px;
fill: #fff;
}
/* Loading Spinner */
.spinner {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
z-index: 100;
display: none;
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
/* Overlay for mobile */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
z-index: 9;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.overlay.show {
opacity: 1;
visibility: visible;
}
/* AudioContext Resume Overlay */
#audioResumeOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 20px;
}
.resume-content {
max-width: 300px;
}
.resume-content h2 {
font-size: 24px;
margin-bottom: 15px;
color: #fff;
}
.resume-content p {
color: #888;
margin-bottom: 25px;
line-height: 1.5;
}
.resume-btn {
background: #fff;
color: #000;
border: none;
padding: 14px 28px;
border-radius: 25px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.resume-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(255, 255, 255, 0.2);
}
/* Mobile Optimizations */
@media (max-width: 768px) {
#trackInterface {
top: 15px;
width: 95%;
max-height: 75vh;
padding: 12px;
border-radius: 12px;
}
.header h1 {
font-size: 20px;
}
.track-item {
padding: 10px 12px;
}
.track-title {
font-size: 13px;
}
.controls-container {
padding: 10px 15px;
gap: 12px;
border-radius: 35px;
}
.control-btn {
width: 36px;
height: 36px;
}
.control-btn.play-pause {
width: 42px;
height: 42px;
}
.current-track {
font-size: 12px;
}
.track-time {
font-size: 10px;
}
#trackListToggle {
top: 15px;
right: 15px;
width: 36px;
height: 36px;
}
}
@media (max-width: 480px) {
.track-item {
padding: 8px 10px;
gap: 10px;
}
.track-number {
width: 24px;
height: 24px;
font-size: 12px;
}
.controls-container {
padding: 8px 12px;
gap: 10px;
}
.control-btn {
width: 32px;
height: 32px;
}
.control-btn.play-pause {
width: 38px;
height: 38px;
}
.control-btn svg {
width: 16px;
height: 16px;
}
}
/* Hide scrollbar but keep functionality */
#trackInterface::-webkit-scrollbar {
width: 4px;
}
#trackInterface::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 2px;
}
#trackInterface::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
#trackInterface::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
</style>
</head>
<body>
<!-- Canvas for Circular Spectrum Visualization -->
<canvas id="canvas"></canvas>
<!-- AudioContext Resume Overlay -->
<div id="audioResumeOverlay">
<div class="resume-content">
<h2>Start Music Player</h2>
<p>Click the button below to start the audio engine and enable music visualisation.</p>
<button class="resume-btn" id="resumeBtn">Start Player</button>
</div>
</div>
<!-- Overlay for closing track interface on mobile -->
<div class="overlay" id="overlay"></div>
<!-- Track Selection Interface -->
<div id="trackInterface" class="hidden">
<div class="header">
<h1>Hakim Bey</h1>
<p>Select a track to begin</p>
</div>
<div class="tracks-container" id="tracksList">
<!-- Tracks will be populated by JavaScript -->
</div>
</div>
<!-- Player Controls -->
<div id="playerControls">
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress" id="progress"></div>
</div>
</div>
<div class="controls-container">
<button class="control-btn prev" id="prevBtn" disabled>
<svg viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
</svg>
</button>
<button class="control-btn play-pause" id="playPauseBtn">
<svg viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
<button class="control-btn next" id="nextBtn" disabled>
<svg viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
</svg>
</button>
<div class="track-display">
<div class="current-track" id="currentTrack">Select a track</div>
<div class="track-time" id="trackTime">0:00 / 0:00</div>
</div>
</div>
</div>
<!-- Track List Toggle -->
<div id="trackListToggle">
<svg viewBox="0 0 24 24">
<path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/>
</svg>
</div>
<!-- Loading Spinner -->
<div class="spinner" id="loadingSpinner"></div>
<!-- Audio Element (hidden) -->
<audio id="audioElement" crossorigin="anonymous"></audio>
<script>
class MusicPlayer {
constructor() {
// Initialize track list from GitHub
this.tracks = [
{ id: 1, title: "Hakim Bey 1", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-1.mp3" },
{ id: 2, title: "Hakim Bey 2", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-2.mp3" },
{ id: 3, title: "Hakim Bey 3", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-3.mp3" },
{ id: 4, title: "Hakim Bey 4", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-4.mp3" },
{ id: 5, title: "Hakim Bey 5", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-5.mp3" },
{ id: 6, title: "Hakim Bey 6", artist: "Mike Higgins", url: "https://cdn.jsdelivr.net/gh/alaric-visi/music@main/hakim-bey/hakim-bey-part-6.mp3" }
];
this.currentTrackIndex = 0;
this.isPlaying = false;
this.isLoading = false;
this.audioContextStarted = false;
// DOM Elements
this.canvas = document.getElementById('canvas');
this.ctx = this.canvas.getContext('2d');
this.audio = document.getElementById('audioElement');
this.trackInterface = document.getElementById('trackInterface');
this.tracksList = document.getElementById('tracksList');
this.playerControls = document.getElementById('playerControls');
this.trackListToggle = document.getElementById('trackListToggle');
this.loadingSpinner = document.getElementById('loadingSpinner');
this.playPauseBtn = document.getElementById('playPauseBtn');
this.prevBtn = document.getElementById('prevBtn');
this.nextBtn = document.getElementById('nextBtn');
this.progress = document.getElementById('progress');
this.progressBar = document.getElementById('progressBar');
this.currentTrackDisplay = document.getElementById('currentTrack');
this.trackTimeDisplay = document.getElementById('trackTime');
this.overlay = document.getElementById('overlay');
this.audioResumeOverlay = document.getElementById('audioResumeOverlay');
this.resumeBtn = document.getElementById('resumeBtn');
// Audio Context for visualization - created after user gesture
this.audioContext = null;
this.analyser = null;
this.source = null;
// Visualization settings
this.bufferLength = 256;
this.dataArray = new Uint8Array(this.bufferLength);
// Initialize
this.init();
}
async init() {
this.setupCanvas();
this.renderTrackList();
this.setupEventListeners();
this.animate();
}
setupCanvas() {
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
resizeCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
async initAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 512;
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
// Create gain node for volume control
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = 0.8;
// Connect nodes
this.analyser.connect(this.gainNode);
this.gainNode.connect(this.audioContext.destination);
}
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
this.audioContextStarted = true;
this.audioResumeOverlay.style.display = 'none';
}
setupEventListeners() {
// Resume AudioContext
this.resumeBtn.addEventListener('click', () => this.initAudioContext());
// Play/Pause
this.playPauseBtn.addEventListener('click', () => this.togglePlay());
// Previous/Next
this.prevBtn.addEventListener('click', () => this.previousTrack());
this.nextBtn.addEventListener('click', () => this.nextTrack());
// Track list toggle
this.trackListToggle.addEventListener('click', () => this.toggleTrackInterface());
// Overlay click
this.overlay.addEventListener('click', () => this.toggleTrackInterface());
// Audio events
this.audio.addEventListener('timeupdate', () => this.updateProgress());
this.audio.addEventListener('loadeddata', () => this.onTrackLoaded());
this.audio.addEventListener('ended', () => this.nextTrack());
this.audio.addEventListener('error', (e) => this.onAudioError(e));
// Progress bar click
this.progressBar.addEventListener('click', (e) => this.seekTrack(e));
this.progressBar.addEventListener('touchstart', (e) => {
e.preventDefault();
this.seekTrack(e.touches[0]);
});
// Close track interface when clicking outside on mobile
if (window.innerWidth <= 768) {
document.addEventListener('click', (e) => {
if (!this.trackInterface.contains(e.target) &&
!this.trackListToggle.contains(e.target) &&
!this.trackInterface.classList.contains('hidden')) {
this.toggleTrackInterface();
}
});
}
}
renderTrackList() {
this.tracksList.innerHTML = '';
this.tracks.forEach((track, index) => {
const trackElement = document.createElement('div');
trackElement.className = `track-item ${index === this.currentTrackIndex ? 'active' : ''}`;
trackElement.innerHTML = `
<div class="track-number">${track.id}</div>
<div class="track-info">
<div class="track-title">${track.title}</div>
<div class="track-artist">${track.artist}</div>
</div>
<div class="play-icon">
<svg viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</div>
`;
trackElement.addEventListener('click', () => this.selectTrack(index));
this.tracksList.appendChild(trackElement);
});
}
async selectTrack(index) {
if (!this.audioContextStarted) {
await this.initAudioContext();
}
this.currentTrackIndex = index;
const track = this.tracks[index];
this.showLoading();
this.renderTrackList();
// Update current track display
this.currentTrackDisplay.textContent = `${track.title} - ${track.artist}`;
// Reset audio
this.audio.pause();
this.isPlaying = false;
this.updatePlayButton();
// Load new track
try {
this.audio.src = track.url;
await this.audio.load();
// Connect audio source to analyser
if (!this.source) {
this.source = this.audioContext.createMediaElementSource(this.audio);
this.source.connect(this.analyser);
// Also connect to destination for audio output
this.source.connect(this.audioContext.destination);
}
// Enable controls
this.prevBtn.disabled = index === 0;
this.nextBtn.disabled = index === this.tracks.length - 1;
// Auto-play
await this.audio.play();
this.isPlaying = true;
this.updatePlayButton();
// Close track interface on mobile
if (window.innerWidth <= 768) {
this.toggleTrackInterface();
}
} catch (error) {
console.error('Error loading track:', error);
alert('Error loading track. Please try again.');
} finally {
this.hideLoading();
}
}
onTrackLoaded() {
this.updateProgress();
}
onAudioError(e) {
console.error('Audio error:', e);
this.hideLoading();
alert('Error playing audio. Please try another track.');
}
async togglePlay() {
if (!this.audioContextStarted) {
await this.initAudioContext();
return;
}
if (!this.audio.src) {
this.selectTrack(0);
return;
}
if (this.isPlaying) {
this.pause();
} else {
this.play();
}
}
async play() {
try {
await this.audio.play();
this.isPlaying = true;
this.updatePlayButton();
} catch (error) {
console.error('Play error:', error);
// If there's a play error, try resuming audio context
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
await this.audio.play();
this.isPlaying = true;
this.updatePlayButton();
}
}
}
pause() {
this.audio.pause();
this.isPlaying = false;
this.updatePlayButton();
}
previousTrack() {
if (this.currentTrackIndex > 0) {
this.selectTrack(this.currentTrackIndex - 1);
}
}
nextTrack() {
if (this.currentTrackIndex < this.tracks.length - 1) {
this.selectTrack(this.currentTrackIndex + 1);
}
}
updatePlayButton() {
const svgPath = this.playPauseBtn.querySelector('svg path');
if (this.isPlaying) {
svgPath.setAttribute('d', 'M6 19h4V5H6v14zm8-14v14h4V5h-4z');
} else {
svgPath.setAttribute('d', 'M8 5v14l11-7z');
}
}
updateProgress() {
if (!this.audio.duration || isNaN(this.audio.duration)) return;
const currentTime = this.audio.currentTime;
const duration = this.audio.duration;
const progressPercent = (currentTime / duration) * 100;
this.progress.style.width = `${progressPercent}%`;
// Update time display
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
this.trackTimeDisplay.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
}
seekTrack(e) {
if (!this.audio.duration) return;
const progressBar = e.currentTarget || e.target.closest('.progress-bar');
const rect = progressBar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
this.audio.currentTime = percentage * this.audio.duration;
this.updateProgress();
}
toggleTrackInterface() {
this.trackInterface.classList.toggle('hidden');
this.overlay.classList.toggle('show');
// Hide overlay on desktop
if (window.innerWidth > 768) {
this.overlay.classList.remove('show');
}
}
showLoading() {
this.isLoading = true;
this.loadingSpinner.style.display = 'block';
}
hideLoading() {
this.isLoading = false;
this.loadingSpinner.style.display = 'none';
}
// Circular Spectrum Visualization
drawCircularSpectrum() {
if (this.analyser && this.audioContextStarted) {
this.analyser.getByteFrequencyData(this.dataArray);
}
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
const radius = Math.min(centerX, centerY) * 0.35;
// Clear with fade effect for motion blur
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw circular spectrum
const bars = 180;
const angleStep = (Math.PI * 2) / bars;
const barWidth = (Math.PI * 2) / bars * radius * 0.5;
for (let i = 0; i < bars; i++) {
const angle = i * angleStep;
let value;
if (this.analyser && this.audioContextStarted) {
const dataIndex = Math.floor(i * this.bufferLength / bars);
value = this.dataArray[dataIndex] || 0;
} else {
// Demo animation when no audio is playing
const time = Date.now() * 0.001;
value = Math.sin(time * 2 + i * 0.1) * 64 + 64;
}
const barHeight = (value / 255) * radius * 0.7;
// Create gradient based on frequency
const x1 = centerX + Math.cos(angle) * radius;
const y1 = centerY + Math.sin(angle) * radius;
const x2 = centerX + Math.cos(angle) * (radius + barHeight);
const y2 = centerY + Math.sin(angle) * (radius + barHeight);
const gradient = this.ctx.createLinearGradient(x1, y1, x2, y2);
// White theme with subtle color accents
const intensity = value / 255;
if (i < bars * 0.3) { // Bass - cool white
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + intensity * 0.3})`);
gradient.addColorStop(1, `rgba(200, 230, 255, ${0.6 + intensity * 0.4})`);
} else if (i < bars * 0.7) { // Mid - pure white
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.4 + intensity * 0.4})`);
gradient.addColorStop(1, `rgba(255, 255, 255, ${0.8 + intensity * 0.2})`);
} else { // Treble - warm white
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + intensity * 0.3})`);
gradient.addColorStop(1, `rgba(255, 240, 220, ${0.6 + intensity * 0.4})`);
}
this.ctx.save();
this.ctx.translate(centerX, centerY);
this.ctx.rotate(angle);
// Draw bar
this.ctx.beginPath();
this.ctx.rect(radius, -barWidth / 2, barHeight, barWidth);
this.ctx.fillStyle = gradient;
this.ctx.fill();
// Add glow effect for strong frequencies
if (value > 180) {
this.ctx.shadowColor = 'rgba(255, 255, 255, 0.6)';
this.ctx.shadowBlur = 12;
this.ctx.fill();
this.ctx.shadowBlur = 0;
}
this.ctx.restore();
}
// Draw center circle
const centerGradient = this.ctx.createRadialGradient(
centerX, centerY, radius * 0.1,
centerX, centerY, radius * 0.4
);
centerGradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
centerGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)');
centerGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius * 0.4, 0, Math.PI * 2);
this.ctx.fillStyle = centerGradient;
this.ctx.fill();
// Draw inner ring
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, radius * 0.15, 0, Math.PI * 2);
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
this.ctx.lineWidth = 1.5;
this.ctx.stroke();
}
animate() {
requestAnimationFrame(() => this.animate());
this.drawCircularSpectrum();
}
}
// Initialize player when page loads
window.addEventListener('load', () => {
const player = new MusicPlayer();
// Make player globally available for debugging
window.player = player;
// Add touch-friendly interactions for mobile
if ('ontouchstart' in window) {
// Make buttons more touch-friendly
const buttons = document.querySelectorAll('button, .track-item');
buttons.forEach(btn => {
btn.addEventListener('touchstart', () => {
btn.style.transform = 'scale(0.95)';
});
btn.addEventListener('touchend', () => {
btn.style.transform = '';
});
});
}
});
// Handle iframe specific requirements
if (window.self !== window.top) {
document.body.style.overflow = 'hidden';
}
</script>
</body>
</html>