|
|
class AudioVisualizer extends HTMLElement { |
|
|
constructor() { |
|
|
super(); |
|
|
this.analyser = null; |
|
|
this.animationId = null; |
|
|
this.dataArray = null; |
|
|
this.type = 'bars'; |
|
|
this.canvas = document.createElement('canvas'); |
|
|
this.ctx = this.canvas.getContext('2d'); |
|
|
} |
|
|
static get observedAttributes() { |
|
|
return ['type', 'static']; |
|
|
} |
|
|
attributeChangedCallback(name, oldValue, newValue) { |
|
|
if (name === 'type') { |
|
|
this.type = newValue; |
|
|
if (this.animationId) { |
|
|
cancelAnimationFrame(this.animationId); |
|
|
this.startVisualization(); |
|
|
} |
|
|
} |
|
|
} |
|
|
connectedCallback() { |
|
|
this.attachShadow({ mode: 'open' }); |
|
|
|
|
|
|
|
|
this.canvas.width = this.clientWidth; |
|
|
this.canvas.height = this.clientHeight; |
|
|
|
|
|
this.shadowRoot.appendChild(this.canvas); |
|
|
|
|
|
|
|
|
if (this.analyser) { |
|
|
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.hasAttribute('static')) { |
|
|
this.dataArray = new Uint8Array(128); |
|
|
this.generateDemoData(); |
|
|
this.startVisualization(); |
|
|
} |
|
|
} |
|
|
|
|
|
generateDemoData() { |
|
|
const now = Date.now() / 1000; |
|
|
for (let i = 0; i < this.dataArray.length; i++) { |
|
|
|
|
|
const pos = i / this.dataArray.length; |
|
|
const timeFactor = Math.sin(now + pos * Math.PI * 2) * 0.5 + 0.5; |
|
|
const freqFactor = Math.pow(1 - pos, 1.5) + 0.1 * Math.random(); |
|
|
this.dataArray[i] = 64 + Math.round(150 * timeFactor * freqFactor); |
|
|
} |
|
|
return this.dataArray; |
|
|
} |
|
|
setAnalyser(analyser) { |
|
|
this.analyser = analyser; |
|
|
if (this.analyser) { |
|
|
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount); |
|
|
} |
|
|
} |
|
|
|
|
|
startVisualization() { |
|
|
if (!this.analyser) return; |
|
|
|
|
|
const draw = () => { |
|
|
this.analyser.getByteFrequencyData(this.dataArray); |
|
|
this.clearCanvas(); |
|
|
|
|
|
switch(this.type) { |
|
|
case 'bars': |
|
|
this.drawBars(); |
|
|
break; |
|
|
case 'wave': |
|
|
this.drawWave(); |
|
|
break; |
|
|
case 'circle': |
|
|
this.drawCircle(); |
|
|
break; |
|
|
case 'matrix': |
|
|
this.drawMatrix(); |
|
|
break; |
|
|
case 'particles': |
|
|
this.drawParticles(); |
|
|
break; |
|
|
case 'lissajous': |
|
|
this.drawLissajous(); |
|
|
break; |
|
|
case 'radial': |
|
|
this.drawRadial(); |
|
|
break; |
|
|
case 'polar': |
|
|
this.drawPolar(); |
|
|
break; |
|
|
case 'vortex': |
|
|
this.drawVortex(); |
|
|
break; |
|
|
default: |
|
|
this.drawBars(); |
|
|
} |
|
|
|
|
|
this.animationId = requestAnimationFrame(draw); |
|
|
}; |
|
|
|
|
|
draw(); |
|
|
} |
|
|
|
|
|
stopVisualization() { |
|
|
if (this.animationId) { |
|
|
cancelAnimationFrame(this.animationId); |
|
|
this.animationId = null; |
|
|
this.clearCanvas(); |
|
|
} |
|
|
} |
|
|
|
|
|
clearCanvas() { |
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); |
|
|
} |
|
|
|
|
|
drawBars() { |
|
|
const width = this.canvas.width; |
|
|
const height = this.canvas.height; |
|
|
const barWidth = width / this.dataArray.length; |
|
|
const gradient = this.ctx.createLinearGradient(0, 0, 0, height); |
|
|
gradient.addColorStop(0, '#4f46e5'); |
|
|
gradient.addColorStop(1, '#c026d3'); |
|
|
|
|
|
this.ctx.fillStyle = gradient; |
|
|
|
|
|
for (let i = 0; i < this.dataArray.length; i++) { |
|
|
const barHeight = (this.dataArray[i] / 255) * height; |
|
|
const x = i * barWidth; |
|
|
const y = height - barHeight; |
|
|
|
|
|
|
|
|
this.ctx.beginPath(); |
|
|
this.ctx.moveTo(x + barWidth / 2, y); |
|
|
this.ctx.arcTo(x + barWidth, y, x + barWidth, y + barHeight, 2); |
|
|
this.ctx.arcTo(x + barWidth, y + barHeight, x, y + barHeight, 2); |
|
|
this.ctx.arcTo(x, y + barHeight, x, y, 2); |
|
|
this.ctx.arcTo(x, y, x + barWidth / 2, y, 2); |
|
|
this.ctx.closePath(); |
|
|
this.ctx.fill(); |
|
|
} |
|
|
} |
|
|
|
|
|
drawWave() { |
|
|
const width = this.canvas.width; |
|
|
const height = this.canvas.height; |
|
|
const sliceWidth = width / this.dataArray.length; |
|
|
|
|
|
this.ctx.lineWidth = 2; |
|
|
this.ctx.strokeStyle = '#818cf8'; |
|
|
this.ctx.beginPath(); |
|
|
|
|
|
let x = 0; |
|
|
for (let i = 0; i < this.dataArray.length; i++) { |
|
|
const v = this.dataArray[i] / 255; |
|
|
const y = v * height / 2; |
|
|
|
|
|
if (i === 0) { |
|
|
this.ctx.moveTo(x, y); |
|
|
} else { |
|
|
this.ctx.lineTo(x, y); |
|
|
} |
|
|
|
|
|
x += sliceWidth; |
|
|
} |
|
|
|
|
|
this.ctx.lineTo(width, height / 2); |
|
|
this.ctx.stroke(); |
|
|
} |
|
|
drawCircle() { |
|
|
const centerX = this.canvas.width / 2; |
|
|
const centerY = this.canvas.height / 2; |
|
|
const radius = Math.min(this.canvas.width, this.canvas.height) * 0.4; |
|
|
|
|
|
this.ctx.lineWidth = 2; |
|
|
|
|
|
for (let i = 0; i < this.dataArray.length; i++) { |
|
|
const amplitude = this.dataArray[i] / 255; |
|
|
const angle = (i / this.dataArray.length) * Math.PI * 2; |
|
|
const pointRadius = radius + (amplitude * radius * 0.5); |
|
|
|
|
|
const x = centerX + Math.cos(angle) * pointRadius; |
|
|
const y = centerY + Math.sin(angle) * pointRadius; |
|
|
|
|
|
|
|
|
const gradient = this.ctx.createRadialGradient(x, y, 0, x, y, 5); |
|
|
gradient.addColorStop(0, '#8b5cf6'); |
|
|
gradient.addColorStop(1, '#3b82f6'); |
|
|
|
|
|
this.ctx.beginPath(); |
|
|
this.ctx.arc(x, y, 3, 0, Math.PI * 2); |
|
|
this.ctx.fillStyle = gradient; |
|
|
this.ctx.fill(); |
|
|
|
|
|
|
|
|
if (i > 0) { |
|
|
const prevAngle = ((i - 1) / this.dataArray.length) * Math.PI * 2; |
|
|
const prevAmplitude = this.dataArray[i - 1] / 255; |
|
|
const prevPointRadius = radius + (prevAmplitude * radius * 0.5); |
|
|
const prevX = centerX + Math.cos(prevAngle) * prevPointRadius; |
|
|
const prevY = centerY + Math.sin(prevAngle) * prevPointRadius; |
|
|
|
|
|
this.ctx.beginPath(); |
|
|
this.ctx.moveTo(prevX, prevY); |
|
|
this.ctx.lineTo(x, y); |
|
|
this.ctx.strokeStyle = `rgba(139, 92, 246, ${0.5 + amplitude * 0.5})`; |
|
|
this.ctx.stroke(); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
customElements.define('audio-visualizer', AudioVisualizer); |