v© £üaÍïêjj&@Ú:ø'À_Ϭ cordQ text/html Mê<!doctype html><html><head></head><body><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced BSV Ordinals Audio Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
overflow: hidden;
background: #000;
display: flex;
justify-content: center;
align-items: center;
}
.wrapper {
position: relative;
width: 100vw;
height: 100vh;
}
#particle-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
#frequency-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
z-index: 2;
mix-blend-mode: screen;
cursor: crosshair;
}
.controls {
position: absolute;
top: 20px;
right: 20px;
z-index: 3;
display: flex;
gap: 10px;
}
.control-button {
background: rgba(0, 20, 40, 0.8);
border: 1px solid rgba(0, 247, 255, 0.5);
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
padding: 0;
backdrop-filter: blur(10px);
}
.control-button:hover {
opacity: 1;
border-color: rgba(0, 247, 255, 0.8);
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 247, 255, 0.3);
}
.control-button .icon::before {
font-size: 24px;
}
.ordinals-button .icon::before {
content: "<µ";
}
.ordinals-button.playing .icon::before {
content: "ø";
}
.ordinals-button.muted .icon::before {
opacity: 0.4;
}
.loading {
border: 2px solid rgba(0, 247, 255, 0.2);
border-top: 2px solid rgba(0, 247, 255, 0.8);
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.audio-info {
position: absolute;
bottom: 20px;
left: 20px;
color: rgba(0, 247, 255, 0.8);
font-size: 12px;
z-index: 3;
backdrop-filter: blur(10px);
background: rgba(0, 20, 40, 0.6);
padding: 10px 15px;
border-radius: 8px;
border: 1px solid rgba(0, 247, 255, 0.3);
}
@media (max-width: 768px) {
.controls {
top: 15px;
right: 15px;
}
.control-button {
width: 45px;
height: 45px;
}
.control-button .icon::before {
font-size: 20px;
}
.audio-info {
bottom: 15px;
left: 15px;
font-size: 11px;
}
}
</style>
</head>
<body>
<div class="wrapper">
<canvas id="particle-canvas"></canvas>
<canvas id="frequency-canvas"></canvas>
<div class="controls">
<button id="ordinalsToggle" class="control-button ordinals-button muted" aria-label="Toggle ordinals audio">
<span class="icon"></span>
</button>
</div>
<div class="audio-info" id="audioInfo">
<div>BSV Ordinals Audio Visualizer</div>
<div>Frequency: <span id="dominantFreq">-</span> Hz</div>
<div>Volume: <span id="volumeLevel">-</span>%</div>
</div>
</div>
<script>
class OrdinalsAudioPlayer {
constructor() {
this.audio = null;
this.audioContext = null;
this.analyser = null;
this.isLoaded = false;
this.isPlaying = false;
this.isLoading = false;
this.ordinalsUrl = 'https://ordinals.gorillapool.io/content/1bf39556a2e958b4c449b36acd549099b65fa8067c07df13459c18efbba71fbd_0';
}
async initialize() {
if (this.isLoaded || this.isLoading) return;
this.isLoading = true;
try {
this.audio = new Audio();
this.audio.crossOrigin = "anonymous";
this.audio.loop = true;
this.audio.volume = 0.7;
this.audio.preload = 'metadata';
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error('Audio loading timeout (15s)')), 15000);
const resolveOnce = () => { clearTimeout(timeoutId); resolve(); };
this.audio.addEventListener('canplaythrough', resolveOnce, { once: true });
this.audio.addEventListener('error', (e) => {
clearTimeout(timeoutId);
reject(new Error(`Audio loading error: ${this.audio.error?.message || 'Unknown error'}`));
}, { once: true });
this.audio.src = this.ordinalsUrl;
this.audio.load();
});
this.isLoaded = true;
} catch (error) {
console.error('Error loading ordinals audio:', error);
this.isLoaded = false;
throw error;
} finally {
this.isLoading = false;
}
}
setupAudioContext() {
if (this.audioContext) return;
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
const source = this.audioContext.createMediaElementSource(this.audio);
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 1024; // Increased for better frequency resolution
this.analyser.smoothingTimeConstant = 0.3; // Reduced for more responsive visuals
source.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
}
async play() {
if (!this.isLoaded && !this.isLoading) await this.initialize();
if (this.audio && this.isLoaded && !this.isPlaying) {
this.setupAudioContext();
if (this.audioContext.state === 'suspended') await this.audioContext.resume();
await this.audio.play();
this.isPlaying = true;
}
}
pause() { if (this.audio && this.isPlaying) { this.audio.pause(); this.isPlaying = false; } }
stop() { if (this.audio) { this.audio.pause(); this.audio.currentTime = 0; this.isPlaying = false; } }
getLoadingState() { return this.isLoading; }
getAnalyser() { return this.analyser; }
getAudioContext() { return this.audioContext; }
}
class EnhancedParticle {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.reset();
this.trail = [];
this.maxTrailLength = 8;
}
reset() {
this.x = Math.random() * this.canvas.width;
this.y = Math.random() * this.canvas.height;
this.size = Math.random() * 3 + 1;
this.baseSize = this.size;
this.speedX = (Math.random() - 0.5) * 2;
this.speedY = (Math.random() - 0.5) * 2;
this.baseSpeedX = this.speedX;
this.baseSpeedY = this.speedY;
this.hue = 180 + Math.random() * 60;
this.saturation = 70 + Math.random() * 30;
this.brightness = 80 + Math.random() * 20;
this.alpha = Math.random() * 0.6 + 0.4;
this.baseAlpha = this.alpha;
this.frequencyBand = Math.floor(Math.random() * 16); // Assign to frequency band
this.pulsePhase = Math.random() * Math.PI * 2;
this.trail = [];
}
update(frequencyData, bassLevel, trebbleLevel, volume) {
// Frequency-specific response
const myFrequency = frequencyData[this.frequencyBand] || 0;
const frequencyResponse = myFrequency / 255;
// Bass response affects movement
const bassResponse = bassLevel * 3;
this.speedX = this.baseSpeedX * (1 + bassResponse);
this.speedY = this.baseSpeedY * (1 + bassResponse);
// Trebble affects size and color
const trebbleResponse = trebbleLevel * 2;
this.size = this.baseSize * (1 + frequencyResponse * 4 + trebbleResponse);
// Volume affects alpha and hue shift
this.alpha = Math.min(1, this.baseAlpha * (0.5 + volume * 0.8));
this.hue = (180 + Math.random() * 60 + trebbleResponse * 30) % 360;
// Pulse effect based on frequency
this.pulsePhase += 0.1 + frequencyResponse * 0.3;
const pulse = Math.sin(this.pulsePhase) * frequencyResponse * 0.5;
this.size += pulse;
// Movement
this.x += this.speedX;
this.y += this.speedY;
// Trail effect
this.trail.push({ x: this.x, y: this.y, alpha: this.alpha });
if (this.trail.length > this.maxTrailLength) {
this.trail.shift();
}
// Boundary collision with energy transfer
if (this.x < 0 || this.x > this.canvas.width) {
this.speedX *= -0.8;
this.x = Math.max(0, Math.min(this.canvas.width, this.x));
}
if (this.y < 0 || this.y > this.canvas.height) {
this.speedY *= -0.8;
this.y = Math.max(0, Math.min(this.canvas.height, this.y));
}
}
draw() {
// Draw trail
this.trail.forEach((point, index) => {
const trailAlpha = (index / this.trail.length) * point.alpha * 0.3;
this.ctx.beginPath();
this.ctx.globalAlpha = trailAlpha;
this.ctx.fillStyle = `hsla(${this.hue}, ${this.saturation}%, ${this.brightness}%, 1)`;
this.ctx.arc(point.x, point.y, this.size * 0.3, 0, Math.PI * 2);
this.ctx.fill();
});
// Draw main particle
this.ctx.beginPath();
this.ctx.globalAlpha = this.alpha;
const gradient = this.ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size * 3);
gradient.addColorStop(0, `hsla(${this.hue}, ${this.saturation}%, ${this.brightness}%, 1)`);
gradient.addColorStop(0.3, `hsla(${this.hue}, ${this.saturation}%, ${this.brightness}%, 0.6)`);
gradient.addColorStop(1, `hsla(${this.hue}, ${this.saturation}%, ${this.brightness}%, 0)`);
this.ctx.fillStyle = gradient;
this.ctx.arc(this.x, this.y, this.size * 3, 0, Math.PI * 2);
this.ctx.fill();
// Inner glow
this.ctx.beginPath();
this.ctx.globalAlpha = this.alpha * 0.8;
this.ctx.fillStyle = `hsla(${this.hue + 20}, 90%, 90%, 1)`;
this.ctx.arc(this.x, this.y, this.size * 0.8, 0, Math.PI * 2);
this.ctx.fill();
}
}
class FrequencyVisualizer {
constructor(canvas, player) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.player = player;
this.ripples = []; // Store ripple effects
this.lastBassTime = 0;
}
drawFrequencyBars(frequencyData, mouse = { x: -1000, y: -1000 }) {
const totalBars = Math.min(frequencyData.length, 64);
const barSpacing = this.canvas.width / totalBars;
const barWidth = barSpacing * 0.7;
const centerY = this.canvas.height / 2;
const maxBarHeight = this.canvas.height * 0.4;
// Enhanced audio analysis
const avgVolume = frequencyData.reduce((sum, val) => sum + val, 0) / frequencyData.length / 255;
const bassRange = Math.floor(totalBars * 0.2); // 20% for bass
const midRange = Math.floor(totalBars * 0.6); // 60% for mids
const trebbleRange = totalBars - bassRange - midRange; // 20% for treble
const bassLevel = frequencyData.slice(0, bassRange).reduce((sum, val) => sum + val, 0) / bassRange / 255;
const midLevel = frequencyData.slice(bassRange, bassRange + midRange).reduce((sum, val) => sum + val, 0) / midRange / 255;
const trebbleLevel = frequencyData.slice(bassRange + midRange).reduce((sum, val) => sum + val, 0) / trebbleRange / 255;
// Dynamic background color based on frequency balance
const bgGradient = this.ctx.createLinearGradient(0, 0, 0, this.canvas.height);
const bgHue = 200 + bassLevel * 30 - trebbleLevel * 20;
const bgSaturation = 25 + midLevel * 15;
bgGradient.addColorStop(0, `hsla(${bgHue}, ${bgSaturation}%, ${4 + avgVolume * 8}%, 0.12)`);
bgGradient.addColorStop(0.5, `hsla(${bgHue + 15}, ${bgSaturation + 5}%, ${3 + avgVolume * 6}%, 0.08)`);
bgGradient.addColorStop(1, `hsla(${bgHue + 30}, ${bgSaturation + 10}%, ${2 + avgVolume * 4}%, 0.06)`);
this.ctx.fillStyle = bgGradient;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Enhanced star field
this.drawStarField(avgVolume, trebbleLevel);
// Bass ripple effects
if (bassLevel > 0.6) {
this.createBassRipples(bassLevel);
}
this.drawRipples();
// High frequency sparkle effects
if (trebbleLevel > 0.6) {
this.drawSparkles(trebbleLevel);
}
for (let i = 0; i < totalBars; i++) {
const barHeight = (frequencyData[i] / 255) * maxBarHeight;
const x = i * barSpacing + (barSpacing - barWidth) / 2;
// Dynamic bar width based on frequency content
let dynamicBarWidth = barWidth;
// Bass bars get wider during heavy bass
if (i < bassRange) {
const bassIntensity = (frequencyData[i] / 255) * bassLevel;
dynamicBarWidth = barWidth * (1 + bassIntensity * 0.4); // Up to 40% wider
}
// Mid frequency bars get slight width boost during active mids
else if (i < bassRange + midRange) {
const midIntensity = (frequencyData[i] / 255) * midLevel;
dynamicBarWidth = barWidth * (1 + midIntensity * 0.15); // Up to 15% wider
}
// Treble bars stay normal width for clean high frequency representation
// Smooth width animation to prevent jarring changes
const targetWidth = dynamicBarWidth;
const currentWidth = this.barWidths && this.barWidths[i] ? this.barWidths[i] : barWidth;
const smoothWidth = currentWidth + (targetWidth - currentWidth) * 0.12;
// Store for next frame
if (!this.barWidths) this.barWidths = new Array(totalBars).fill(barWidth);
this.barWidths[i] = smoothWidth;
// Adjust x position to keep bars centered as they expand
const widthDifference = smoothWidth - barWidth;
const adjustedX = x - widthDifference / 2;
// Mouse interaction - calculate distance from mouse to bar center
const barCenterX = adjustedX + smoothWidth / 2;
const barCenterY = centerY;
const mouseDistance = Math.sqrt(
Math.pow(mouse.x - barCenterX, 2) + Math.pow(mouse.y - barCenterY, 2)
);
const maxInteractionDistance = 80;
const mouseInfluence = Math.max(0, 1 - (mouseDistance / maxInteractionDistance));
// Mouse hover effects
const hoverBrightness = mouseInfluence * 25; // Brighten bars near mouse
const hoverSize = 1 + mouseInfluence * 0.3; // Slightly enlarge bars near mouse
const hoverGlow = mouseInfluence * 0.5; // Extra glow near mouse
// Frequency-based color zones
let hue, saturation, baseLightness;
const normalizedPos = i / totalBars;
if (i < bassRange) {
// Bass: Warm colors (red-orange)
hue = 15 + (i / bassRange) * 45; // 15° to 60° (red to orange)
saturation = 70 + bassLevel * 20;
baseLightness = 45 + bassLevel * 25;
} else if (i < bassRange + midRange) {
// Mids: Green-cyan colors
const midPos = (i - bassRange) / midRange;
hue = 120 + midPos * 60; // 120° to 180° (green to cyan)
saturation = 65 + midLevel * 25;
baseLightness = 50 + midLevel * 20;
} else {
// Treble: Blue-purple colors
const trebblePos = (i - bassRange - midRange) / trebbleRange;
hue = 220 + trebblePos * 60; // 220° to 280° (blue to purple)
saturation = 75 + trebbleLevel * 20;
baseLightness = 55 + trebbleLevel * 25;
}
// Smooth bar animation
const targetHeight = barHeight;
const currentHeight = this.barHeights && this.barHeights[i] ? this.barHeights[i] : 0;
const smoothHeight = currentHeight + (targetHeight - currentHeight) * 0.18;
if (!this.barHeights) this.barHeights = new Array(totalBars).fill(0);
this.barHeights[i] = smoothHeight;
// Dynamic saturation based on volume and mouse interaction
const volumeBoost = (frequencyData[i] / 255);
saturation = Math.min(95, saturation + volumeBoost * 15 + hoverBrightness * 0.5);
const lightness = Math.min(85, baseLightness + volumeBoost * 20 + hoverBrightness);
// Apply mouse hover size effect
const finalHeight = smoothHeight * hoverSize;
const finalWidth = smoothWidth * hoverSize;
const sizeAdjustX = adjustedX - (finalWidth - smoothWidth) / 2;
// Enhanced vibration effect with more natural movement
const vibrationX = bassLevel > 0.65 ? Math.sin(Date.now() * 0.015 + i * 0.3) * bassLevel * 1.5 : 0;
const vibrationY = bassLevel > 0.65 ? Math.cos(Date.now() * 0.012 + i * 0.4) * bassLevel * 0.8 : 0;
const vibratedX = sizeAdjustX + vibrationX;
const vibratedY = centerY + vibrationY;
// Enhanced glow effect with frequency-specific colors and mouse interaction
const glowIntensity = volumeBoost * (0.7 + avgVolume * 0.5) + hoverGlow;
if (glowIntensity > 0.15) {
this.ctx.shadowColor = `hsl(${hue}, ${Math.min(saturation + 10, 100)}%, ${Math.min(lightness + 15, 90)}%)`;
this.ctx.shadowBlur = 12 + glowIntensity * 35;
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
}
// Enhanced 5-stop gradient
const gradient = this.ctx.createLinearGradient(vibratedX, vibratedY - finalHeight/2, vibratedX, vibratedY + finalHeight/2);
gradient.addColorStop(0, `hsl(${hue + 5}, ${Math.min(saturation + 15, 100)}%, ${Math.min(lightness + 30, 90)}%)`);
gradient.addColorStop(0.2, `hsl(${hue}, ${saturation + 10}%, ${lightness + 15}%)`);
gradient.addColorStop(0.5, `hsl(${hue}, ${saturation}%, ${lightness}%)`);
gradient.addColorStop(0.8, `hsl(${hue - 5}, ${Math.max(saturation - 10, 40)}%, ${Math.max(lightness - 15, 25)}%)`);
gradient.addColorStop(1, `hsl(${hue - 10}, ${Math.max(saturation - 20, 30)}%, ${Math.max(lightness - 25, 15)}%)`);
// Draw main bar with dynamic width and mouse effects
this.ctx.fillStyle = gradient;
this.ctx.beginPath();
const radius = Math.min(finalWidth * 0.12, 4); // Scale radius with width
const topY = vibratedY - finalHeight / 2;
const bottomY = vibratedY + finalHeight / 2;
this.ctx.moveTo(vibratedX + radius, topY);
this.ctx.lineTo(vibratedX + finalWidth - radius, topY);
this.ctx.quadraticCurveTo(vibratedX + finalWidth, topY, vibratedX + finalWidth, topY + radius);
this.ctx.lineTo(vibratedX + finalWidth, bottomY - radius);
this.ctx.quadraticCurveTo(vibratedX + finalWidth, bottomY, vibratedX + finalWidth - radius, bottomY);
this.ctx.lineTo(vibratedX + radius, bottomY);
this.ctx.quadraticCurveTo(vibratedX, bottomY, vibratedX, bottomY - radius);
this.ctx.lineTo(vibratedX, topY + radius);
this.ctx.quadraticCurveTo(vibratedX, topY, vibratedX + radius, topY);
this.ctx.closePath();
this.ctx.fill();
// Reset shadow for other elements
this.ctx.shadowBlur = 0;
// Enhanced reflection with frequency-specific colors
if (finalHeight > 8) {
const reflectionHeight = finalHeight * 0.65;
const reflectionY = vibratedY + finalHeight / 2 + 3;
const reflectionGradient = this.ctx.createLinearGradient(
vibratedX, reflectionY,
vibratedX, reflectionY + reflectionHeight
);
reflectionGradient.addColorStop(0, `hsla(${hue}, ${saturation}%, ${lightness}%, 0.35)`);
reflectionGradient.addColorStop(0.3, `hsla(${hue}, ${saturation}%, ${lightness}%, 0.2)`);
reflectionGradient.addColorStop(0.7, `hsla(${hue}, ${saturation}%, ${lightness}%, 0.1)`);
reflectionGradient.addColorStop(1, `hsla(${hue}, ${saturation}%, ${lightness}%, 0)`);
this.ctx.fillStyle = reflectionGradient;
this.ctx.beginPath();
this.ctx.moveTo(vibratedX + radius, reflectionY + reflectionHeight);
this.ctx.lineTo(vibratedX + finalWidth - radius, reflectionY + reflectionHeight);
this.ctx.quadraticCurveTo(vibratedX + finalWidth, reflectionY + reflectionHeight, vibratedX + finalWidth, reflectionY + reflectionHeight - radius);
this.ctx.lineTo(vibratedX + finalWidth, reflectionY + radius);
this.ctx.quadraticCurveTo(vibratedX + finalWidth, reflectionY, vibratedX + finalWidth - radius, reflectionY);
this.ctx.lineTo(vibratedX + radius, reflectionY);
this.ctx.quadraticCurveTo(vibratedX, reflectionY, vibratedX, reflectionY + radius);
this.ctx.lineTo(vibratedX, reflectionY + reflectionHeight - radius);
this.ctx.quadraticCurveTo(vibratedX, reflectionY + reflectionHeight, vibratedX + radius, reflectionY + reflectionHeight);
this.ctx.closePath();
this.ctx.fill();
}
// Enhanced inner highlight with frequency colors and dynamic width
if (finalHeight > 12) {
this.ctx.fillStyle = `hsla(${hue + 10}, ${Math.min(saturation + 20, 100)}%, 90%, 0.45)`;
this.ctx.fillRect(vibratedX + 1, vibratedY - finalHeight/2 + 1, Math.max(finalWidth - 2, 1), Math.max(finalHeight/3.5, 1));
}
// High frequency particle burst
if (i >= bassRange + midRange && volumeBoost > 0.75 && trebbleLevel > 0.7) {
this.drawParticleBurst(vibratedX + finalWidth/2, topY, hue, volumeBoost);
}
}
}
drawStarField(intensity, trebbleLevel) {
const baseStarCount = 40;
const starCount = Math.floor(baseStarCount + intensity * 35 + trebbleLevel * 20);
const time = Date.now() * 0.001;
for (let i = 0; i < starCount; i++) {
const x = (i * 1234.5) % this.canvas.width;
const y = (i * 2345.6) % this.canvas.height;
const baseSize = 0.4 + (i % 4) * 0.3;
const size = Math.max(0.1, baseSize); // Ensure positive size
const twinkleSpeed = 0.003 + (i % 10) * 0.0005;
const twinkle = Math.sin(time * twinkleSpeed + i) * 0.5 + 0.5;
// Color variation based on position
const starHue = 180 + (i % 60) + trebbleLevel * 30;
const alpha = Math.max(0, (0.08 + intensity * twinkle * 0.4) * (0.7 + trebbleLevel * 0.5));
this.ctx.fillStyle = `hsla(${starHue}, 70%, 85%, ${alpha})`;
this.ctx.beginPath();
this.ctx.arc(x, y, size, 0, Math.PI * 2);
this.ctx.fill();
}
}
createBassRipples(bassLevel) {
const currentTime = Date.now();
// Create new ripple every 200ms minimum to avoid overwhelming
if (currentTime - this.lastBassTime > 200 && bassLevel > 0.65) {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
// Add some randomness to ripple origin for variety
const offsetX = (Math.random() - 0.5) * 100;
const offsetY = (Math.random() - 0.5) * 60;
this.ripples.push({
x: centerX + offsetX,
y: centerY + offsetY,
radius: 0,
maxRadius: Math.min(this.canvas.width, this.canvas.height) * 0.8,
intensity: bassLevel,
life: 1,
speed: 2 + bassLevel * 3, // Faster expansion for stronger bass
color: {
hue: 15 + Math.random() * 30, // Warm bass colors
saturation: 70 + bassLevel * 20,
lightness: 40 + bassLevel * 20
}
});
this.lastBassTime = currentTime;
}
}
drawRipples() {
for (let i = this.ripples.length - 1; i >= 0; i--) {
const ripple = this.ripples[i];
// Update ripple
ripple.radius += ripple.speed;
ripple.life -= 0.008; // Slower fade for more visible effect
// Remove dead ripples
if (ripple.life <= 0 || ripple.radius > ripple.maxRadius) {
this.ripples.splice(i, 1);
continue;
}
// Draw multiple concentric rings for richer effect
for (let ring = 0; ring < 3; ring++) {
const ringRadius = Math.max(0.1, ripple.radius - ring * 15);
const ringAlpha = Math.max(0, ripple.life * ripple.intensity * (0.3 - ring * 0.08));
if (ringRadius > 0 && ringAlpha > 0) {
this.ctx.strokeStyle = `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness}%, ${ringAlpha})`;
this.ctx.lineWidth = 2 + ring * 0.5;
this.ctx.beginPath();
this.ctx.arc(ripple.x, ripple.y, ringRadius, 0, Math.PI * 2);
this.ctx.stroke();
}
}
// Add inner glow effect
if (ripple.radius < 50) {
const glowAlpha = Math.max(0, ripple.life * ripple.intensity * 0.15);
if (glowAlpha > 0) {
const glowGradient = this.ctx.createRadialGradient(
ripple.x, ripple.y, 0,
ripple.x, ripple.y, ripple.radius + 20
);
glowGradient.addColorStop(0, `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness + 20}%, ${glowAlpha})`);
glowGradient.addColorStop(1, `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness + 20}%, 0)`);
this.ctx.fillStyle = glowGradient;
this.ctx.beginPath();
this.ctx.arc(ripple.x, ripple.y, ripple.radius + 20, 0, Math.PI * 2);
this.ctx.fill();
}
}
}
}
drawSparkles(trebbleLevel) {
const sparkleCount = Math.floor(trebbleLevel * 15);
const time = Date.now() * 0.002;
for (let i = 0; i < sparkleCount; i++) {
const x = (Math.sin(time + i * 2.5) * 0.5 + 0.5) * this.canvas.width;
const y = (Math.cos(time + i * 3.1) * 0.3 + 0.2) * this.canvas.height;
const baseSize = 1 + Math.sin(time * 2 + i) * 1.5;
const size = Math.max(0.1, baseSize); // Ensure positive size
const brightness = trebbleLevel * (0.7 + Math.sin(time * 3 + i) * 0.3);
this.ctx.fillStyle = `hsla(${45 + i * 20}, 90%, 90%, ${brightness})`;
this.ctx.beginPath();
this.ctx.arc(x, y, size, 0, Math.PI * 2);
this.ctx.fill();
// Cross sparkle effect with safe line length
const lineLength = Math.max(1, size * 2);
this.ctx.strokeStyle = `hsla(${45 + i * 20}, 90%, 95%, ${brightness * 0.8})`;
this.ctx.lineWidth = 0.5;
this.ctx.beginPath();
this.ctx.moveTo(x - lineLength, y);
this.ctx.lineTo(x + lineLength, y);
this.ctx.moveTo(x, y - lineLength);
this.ctx.lineTo(x, y + lineLength);
this.ctx.stroke();
}
}
drawParticleBurst(x, y, hue, intensity) {
if (!this.particles) this.particles = [];
// Create new particles
if (Math.random() < intensity * 0.3) {
for (let i = 0; i < 3; i++) {
this.particles.push({
x: x + (Math.random() - 0.5) * 10,
y: y + (Math.random() - 0.5) * 5,
vx: (Math.random() - 0.5) * 4,
vy: -Math.random() * 3 - 1,
life: 1,
maxLife: 0.8 + Math.random() * 0.4,
hue: hue + (Math.random() - 0.5) * 30,
size: 1 + Math.random() * 2
});
}
}
// Update and draw existing particles
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.1; // gravity
p.life -= 0.02;
if (p.life <= 0) {
this.particles.splice(i, 1);
continue;
}
const alpha = p.life / p.maxLife;
const radius = Math.max(0.1, p.size * alpha); // Ensure positive radius
this.ctx.fillStyle = `hsla(${p.hue}, 90%, 80%, ${alpha})`;
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
this.ctx.fill();
}
}
drawWaveform(frequencyData) {
const sliceWidth = this.canvas.width / frequencyData.length;
let x = 0;
this.ctx.beginPath();
this.ctx.lineWidth = 3;
this.ctx.strokeStyle = 'rgba(0, 247, 255, 0.8)';
for (let i = 0; i < frequencyData.length; i++) {
const v = frequencyData[i] / 128.0;
const y = v * this.canvas.height / 2;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.stroke();
}
drawCircularSpectrum(frequencyData) {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
const radius = Math.min(centerX, centerY) * 0.3;
const angleStep = (Math.PI * 2) / frequencyData.length;
for (let i = 0; i < frequencyData.length; i++) {
const angle = i * angleStep;
const barHeight = (frequencyData[i] / 255) * 150;
const hue = (i / frequencyData.length) * 360;
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);
this.ctx.beginPath();
this.ctx.strokeStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
this.ctx.lineWidth = 3;
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
}
}
class EnhancedParticleSystem {
constructor(particleCanvasId, frequencyCanvasId, player) {
this.particleCanvas = document.getElementById(particleCanvasId);
this.frequencyCanvas = document.getElementById(frequencyCanvasId);
this.particleCtx = this.particleCanvas.getContext('2d');
this.frequencyCtx = this.frequencyCanvas.getContext('2d');
this.player = player;
this.particles = [];
this.particleCount = 100;
this.visualMode = 1; // Always use frequency bars
this.frequencyVisualizer = new FrequencyVisualizer(this.frequencyCanvas, player);
this.mouse = { x: 0, y: 0, isPressed: false };
this.clickRipples = [];
this.init();
}
init() {
window.addEventListener('resize', () => this.resize(), false);
this.resize();
for (let i = 0; i < this.particleCount; i++) {
this.particles.push(new EnhancedParticle(this.particleCanvas));
}
// Mouse event listeners
this.frequencyCanvas.addEventListener('mousemove', (e) => {
const rect = this.frequencyCanvas.getBoundingClientRect();
this.mouse.x = e.clientX - rect.left;
this.mouse.y = e.clientY - rect.top;
});
this.frequencyCanvas.addEventListener('mousedown', (e) => {
this.mouse.isPressed = true;
this.createClickRipple(this.mouse.x, this.mouse.y);
});
this.frequencyCanvas.addEventListener('mouseup', () => {
this.mouse.isPressed = false;
});
this.frequencyCanvas.addEventListener('mouseleave', () => {
this.mouse.x = -1000; // Move mouse off-screen
this.mouse.y = -1000;
this.mouse.isPressed = false;
});
requestAnimationFrame(() => this.animate());
}
resize() {
this.particleCanvas.width = window.innerWidth;
this.particleCanvas.height = window.innerHeight;
this.frequencyCanvas.width = window.innerWidth;
this.frequencyCanvas.height = window.innerHeight;
}
toggleMode() {
this.visualMode = (this.visualMode + 1) % 4;
const modes = ['Particles', 'Frequency Bars', 'Waveform', 'Circular Spectrum'];
document.getElementById('audioInfo').querySelector('div').textContent = `Mode: ${modes[this.visualMode]}`;
}
animate() {
// Clear canvases
this.particleCtx.clearRect(0, 0, this.particleCanvas.width, this.particleCanvas.height);
this.frequencyCtx.clearRect(0, 0, this.frequencyCanvas.width, this.frequencyCanvas.height);
const analyser = this.player.getAnalyser();
let audioData = {
frequencyData: new Uint8Array(128),
bassLevel: 0,
trebbleLevel: 0,
volume: 0,
dominantFreq: 0
};
if (analyser) {
const bufferLength = analyser.frequencyBinCount;
const frequencyData = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(frequencyData);
// Downsample for performance
const downsampledData = new Uint8Array(128);
const step = Math.floor(bufferLength / 128);
for (let i = 0; i < 128; i++) {
downsampledData[i] = frequencyData[i * step] || 0;
}
// Calculate audio metrics
const bassEnd = Math.floor(128 * 0.15);
const trebbleStart = Math.floor(128 * 0.7);
audioData.frequencyData = downsampledData;
audioData.bassLevel = this.getAverageLevel(downsampledData, 0, bassEnd) / 255;
audioData.trebbleLevel = this.getAverageLevel(downsampledData, trebbleStart, 128) / 255;
audioData.volume = this.getAverageLevel(downsampledData, 0, 128) / 255;
audioData.dominantFreq = this.getDominantFrequency(downsampledData);
// Update UI
document.getElementById('volumeLevel').textContent = Math.round(audioData.volume * 100);
document.getElementById('dominantFreq').textContent = Math.round(audioData.dominantFreq);
}
// Always render frequency bars with mouse interaction
this.frequencyVisualizer.drawFrequencyBars(audioData.frequencyData, this.mouse);
// Draw click ripples
this.drawClickRipples();
requestAnimationFrame(() => this.animate());
}
getAverageLevel(data, start, end) {
let sum = 0;
for (let i = start; i < end; i++) {
sum += data[i];
}
return sum / (end - start);
}
getDominantFrequency(data) {
let maxIndex = 0;
let maxValue = 0;
for (let i = 0; i < data.length; i++) {
if (data[i] > maxValue) {
maxValue = data[i];
maxIndex = i;
}
}
// Convert bin to frequency (approximate)
const nyquist = 22050; // Assuming 44.1kHz sample rate
return (maxIndex / data.length) * nyquist;
}
createClickRipple(x, y) {
this.clickRipples.push({
x: x,
y: y,
radius: 0,
maxRadius: 200,
life: 1,
speed: 4,
color: {
hue: 200 + Math.random() * 60,
saturation: 80,
lightness: 70
}
});
}
drawClickRipples() {
for (let i = this.clickRipples.length - 1; i >= 0; i--) {
const ripple = this.clickRipples[i];
ripple.radius += ripple.speed;
ripple.life -= 0.015;
if (ripple.life <= 0 || ripple.radius > ripple.maxRadius) {
this.clickRipples.splice(i, 1);
continue;
}
// Draw ripple
const alpha = ripple.life * 0.6;
this.frequencyCtx.strokeStyle = `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness}%, ${alpha})`;
this.frequencyCtx.lineWidth = 2;
this.frequencyCtx.beginPath();
this.frequencyCtx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
this.frequencyCtx.stroke();
// Inner glow
if (ripple.radius < 50) {
const glowAlpha = ripple.life * 0.3;
const glowGradient = this.frequencyCtx.createRadialGradient(
ripple.x, ripple.y, 0,
ripple.x, ripple.y, ripple.radius + 15
);
glowGradient.addColorStop(0, `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness + 10}%, ${glowAlpha})`);
glowGradient.addColorStop(1, `hsla(${ripple.color.hue}, ${ripple.color.saturation}%, ${ripple.color.lightness + 10}%, 0)`);
this.frequencyCtx.fillStyle = glowGradient;
this.frequencyCtx.beginPath();
this.frequencyCtx.arc(ripple.x, ripple.y, ripple.radius + 15, 0, Math.PI * 2);
this.frequencyCtx.fill();
}
}
}
}
document.addEventListener('DOMContentLoaded', () => {
const ordinalsPlayer = new OrdinalsAudioPlayer();
const particleSystem = new EnhancedParticleSystem('particle-canvas', 'frequency-canvas', ordinalsPlayer);
const ordinalsToggle = document.getElementById('ordinalsToggle');
ordinalsToggle.addEventListener('click', async () => {
if (ordinalsPlayer.getLoadingState()) return;
const isMuted = ordinalsToggle.classList.toggle('muted');
ordinalsToggle.classList.toggle('playing', !isMuted);
if (isMuted) {
ordinalsPlayer.stop();
} else {
ordinalsToggle.classList.add('loading');
try {
await ordinalsPlayer.play();
} catch (error) {
console.error('Failed to play audio:', error);
ordinalsToggle.classList.remove('playing');
ordinalsToggle.classList.add('muted');
} finally {
ordinalsToggle.classList.remove('loading');
}
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden' && ordinalsPlayer.isPlaying) {
ordinalsPlayer.pause();
ordinalsToggle.classList.remove('playing');
ordinalsToggle.classList.add('muted');
}
});
});
</script>
</body>
</html></body><html>hj"1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5SETapp3D Orditypeordname
nezumi bar royaltiesLX[{"type":"address","destination":"1AwjKNmoFkZJQbyAUSW85jvFoM8XWPDNcA","percentage":0.2}]
https://whatsonchain.com/tx/undefined