koel/resources/assets/js/visualizers/default.ts

184 lines
4.4 KiB
TypeScript
Raw Normal View History

2022-04-15 14:24:30 +00:00
import Sketch from 'sketch-js'
2022-04-24 08:50:45 +00:00
import { audioService } from '@/services'
2022-04-15 14:24:30 +00:00
import { random, sample } from 'lodash'
2022-11-06 17:09:06 +00:00
import { noop } from '@/utils'
2022-04-15 14:24:30 +00:00
const NUM_PARTICLES = 128
const NUM_BANDS = 128
const SMOOTHING = 0.5
const SCALE = { MIN: 5.0, MAX: 80.0 }
const SPEED = { MIN: 0.2, MAX: 1.0 }
const ALPHA = { MIN: 0.8, MAX: 0.9 }
const SPIN = { MIN: 0.001, MAX: 0.005 }
const SIZE = { MIN: 0.5, MAX: 1.25 }
const TWO_PI = Math.PI * 2
2022-04-15 14:24:30 +00:00
const COLORS = [
'#69D2E7',
'#1B676B',
'#BEF202',
'#EBE54D',
'#00CDAC',
'#1693A5',
'#F9D423',
'#FF4E50',
'#E7204E',
'#0CCABA',
'#FF006F'
] as const
type Color = typeof COLORS[number]
2022-04-15 14:24:30 +00:00
class AudioAnalyser {
bandCount: number
2022-04-15 14:24:30 +00:00
smoothing: number
audio: HTMLMediaElement
source: MediaElementAudioSourceNode
analyser: AnalyserNode
2022-04-15 14:24:30 +00:00
bands: Uint8Array
onUpdate: Closure
2022-04-15 14:24:30 +00:00
constructor (bandCount: number, smoothing: number, onUpdate: (bands: Uint8Array) => void) {
this.bandCount = bandCount
2022-04-15 14:24:30 +00:00
this.smoothing = smoothing
this.onUpdate = onUpdate
2022-04-15 14:24:30 +00:00
2022-11-02 19:25:22 +00:00
this.audio = audioService.element
this.source = audioService.source
2022-04-15 14:24:30 +00:00
2022-11-06 17:09:06 +00:00
this.analyser = audioService.analyzer
2022-04-15 14:24:30 +00:00
this.analyser.smoothingTimeConstant = this.smoothing
this.analyser.fftSize = this.bandCount * 2
2022-04-15 14:24:30 +00:00
this.bands = new Uint8Array(this.analyser.frequencyBinCount)
this.update()
}
2022-04-15 14:24:30 +00:00
update () {
requestAnimationFrame(this.update.bind(this))
2022-04-15 14:24:30 +00:00
if (!this.audio.paused) {
2022-04-15 14:24:30 +00:00
this.analyser.getByteFrequencyData(this.bands)
2022-10-28 22:29:31 +00:00
this.onUpdate(this.bands)
2022-04-15 14:24:30 +00:00
}
}
}
class Particle {
x: number
y: number
level = 0
scale = 0
alpha = 0
speed = 0
color: Color = COLORS[0]
size = 0
spin = 0
band = 0
smoothedScale = 0
smoothedAlpha = 0
decayScale = 0
decayAlpha = 0
rotation = 0
energy = 0
constructor (x: number, y: number) {
2022-04-15 14:24:30 +00:00
this.x = x
this.y = y
this.reset()
}
reset () {
2022-04-15 14:24:30 +00:00
this.level = 1 + Math.floor(random(4))
this.scale = random(SCALE.MIN, SCALE.MAX)
this.alpha = random(ALPHA.MIN, ALPHA.MAX)
this.speed = random(SPEED.MIN, SPEED.MAX)
this.color = sample(COLORS)!
2022-04-15 14:24:30 +00:00
this.size = random(SIZE.MIN, SIZE.MAX)
this.spin = random(SPIN.MAX, SPIN.MAX)
this.band = Math.floor(random(NUM_BANDS))
if (Math.random() < 0.5) {
this.spin = -this.spin
}
this.smoothedScale = 0.0
this.smoothedAlpha = 0.0
this.decayScale = 0.0
this.decayAlpha = 0.0
this.rotation = random(TWO_PI)
this.energy = random(this.band / 256)
2022-04-15 14:24:30 +00:00
}
move () {
2022-04-15 14:24:30 +00:00
this.rotation += this.spin
this.y -= this.speed * this.level
}
draw (ctx: CanvasRenderingContext2D) {
2022-04-15 14:24:30 +00:00
const power = Math.exp(this.energy)
const scale = this.scale * power
const alpha = this.alpha * this.energy * 2
this.decayScale = Math.max(this.decayScale, scale)
this.decayAlpha = Math.max(this.decayAlpha, alpha)
this.smoothedScale += (this.decayScale - this.smoothedScale) * 0.3
this.smoothedAlpha += (this.decayAlpha - this.smoothedAlpha) * 0.3
this.decayScale *= 0.985
this.decayAlpha *= 0.975
ctx.save()
ctx.beginPath()
ctx.translate(this.x + Math.cos(this.rotation * this.speed) * 250, this.y)
ctx.rotate(this.rotation)
ctx.scale(this.smoothedScale * this.level, this.smoothedScale * this.level)
ctx.moveTo(this.size * 0.5, 0)
ctx.lineTo(this.size * -0.5, 0)
ctx.lineWidth = 1
ctx.lineCap = 'round'
ctx.globalAlpha = this.smoothedAlpha / this.level
ctx.strokeStyle = this.color
ctx.stroke()
ctx.restore()
2022-04-15 14:24:30 +00:00
}
}
2022-11-06 17:09:06 +00:00
export const init = (container: HTMLElement) => {
const particles: Particle[] = []
2022-04-15 14:24:30 +00:00
Sketch.create({
container,
setup () {
for (let i = 0; i < NUM_PARTICLES; ++i) {
particles.push(new Particle(random(this.width), random(this.height)))
2022-04-15 14:24:30 +00:00
}
new AudioAnalyser(NUM_BANDS, SMOOTHING, bands => {
// update particles based on fft transformed audio frequencies
particles.forEach(particle => (particle.energy = bands[particle.band] / 256))
2022-04-15 14:24:30 +00:00
})
},
draw () {
this.globalCompositeOperation = 'lighter'
particles.map(particle => {
2022-04-15 14:24:30 +00:00
if (particle.y < (-particle.size * particle.level * particle.scale * 2)) {
particle.reset()
particle.x = random(this.width)
particle.y = this.height + (particle.size * particle.scale * particle.level * 2)
}
particle.move()
particle.draw(this as CanvasRenderingContext2D)
2022-04-15 14:24:30 +00:00
})
}
})
2022-11-06 17:09:06 +00:00
return noop
2022-04-15 14:24:30 +00:00
}