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 }
|
2022-10-28 20:42:11 +00:00
|
|
|
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'
|
2022-10-28 20:42:11 +00:00
|
|
|
] as const
|
|
|
|
|
|
|
|
type Color = typeof COLORS[number]
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
class AudioAnalyser {
|
2022-10-28 20:42:11 +00:00
|
|
|
bandCount: number
|
2022-04-15 14:24:30 +00:00
|
|
|
smoothing: number
|
|
|
|
audio: HTMLMediaElement
|
2022-10-28 20:42:11 +00:00
|
|
|
source: MediaElementAudioSourceNode
|
|
|
|
analyser: AnalyserNode
|
2022-04-15 14:24:30 +00:00
|
|
|
bands: Uint8Array
|
2022-10-28 20:42:11 +00:00
|
|
|
onUpdate: Closure
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-10-28 20:42:11 +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
|
2022-10-28 20:42:11 +00:00
|
|
|
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
|
2022-10-28 20:42:11 +00:00
|
|
|
this.analyser.fftSize = this.bandCount * 2
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
this.bands = new Uint8Array(this.analyser.frequencyBinCount)
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
this.update()
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
update () {
|
|
|
|
requestAnimationFrame(this.update.bind(this))
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-10-28 20:42:11 +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
|
2022-10-28 20:42:11 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
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)
|
2022-10-28 20:42:11 +00:00
|
|
|
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)
|
2022-10-28 20:42:11 +00:00
|
|
|
this.energy = random(this.band / 256)
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
move () {
|
2022-04-15 14:24:30 +00:00
|
|
|
this.rotation += this.spin
|
|
|
|
this.y -= this.speed * this.level
|
|
|
|
}
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
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()
|
2022-10-28 20:42:11 +00:00
|
|
|
ctx.restore()
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-06 17:09:06 +00:00
|
|
|
export const init = (container: HTMLElement) => {
|
2022-10-28 20:42:11 +00:00
|
|
|
const particles: Particle[] = []
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
Sketch.create({
|
|
|
|
container,
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-10-28 20:42:11 +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'
|
|
|
|
|
2022-10-28 20:42:11 +00:00
|
|
|
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()
|
2022-10-28 20:42:11 +00:00
|
|
|
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
|
|
|
}
|