Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/backend/pattern/fftMirrorRipple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { IColorGetter, IColorMapper } from 'src/typings'
import { callIndexedGetter } from './mappers'
import { pixelsCount, hueToColor } from '../shared'
import { settings } from 'src/settings'
import { audioState } from '../wsAudio'

interface Ripple {
pos: number
radius: number
brightness: number
hue: number
}

let ripples: Ripple[] = []
let lastTime = Date.now()
let averages: number[] = []
let hueShift = 0
const attenuation = 0.9
const speed = 60
const shiftSpeed = 40

function spawnRipple(bin: number, mag: number) {
const hue = ((bin / audioState.bins.length) * 360 + hueShift) % 360
const pos = (bin / audioState.bins.length) * pixelsCount
const data = {
radius: 1,
brightness: Math.min(1, mag),
hue,
}
ripples.push({ pos, ...data })
ripples.push({ pos: pixelsCount - pos, ...data })
}

function update(time: number) {
const dt = (time - lastTime) * settings.effectSpeed
lastTime = time
const bins = audioState.bins
if (bins && bins.length) {
if (averages.length !== bins.length) averages = bins.slice()
for (let i = 0; i < bins.length; i++) {
const m = bins[i]
averages[i] = averages[i] * 0.9 + m * 0.1
if (m > averages[i] * 1.5) spawnRipple(i, m)
}
}
ripples.forEach(r => {
r.radius += (speed * dt) / 1000
r.brightness *= Math.pow(attenuation, dt / 16)
})
ripples = ripples.filter(r => r.brightness > 0.05)
hueShift = (hueShift + (shiftSpeed * dt) / 1000) % 360
}

export const getFftMirrorRippleColor: IColorGetter = (index, time) => {
if (index === 0) update(time)
let r = 0
let g = 0
let b = 0
for (const ripple of ripples) {
const dist = Math.abs(index - ripple.pos)
if (dist <= ripple.radius) {
const intensity = ripple.brightness * (1 - dist / ripple.radius)
const { r: rr, g: gg, b: bb } = hueToColor(ripple.hue).rgb()
r += rr * intensity
g += gg * intensity
b += bb * intensity
}
}
return [Math.min(255, Math.round(r)), Math.min(255, Math.round(g)), Math.min(255, Math.round(b))]
}

export function resetFftMirrorRipples() {
ripples = []
lastTime = Date.now()
averages = []
}

export const fftMirrorRippleMapper: IColorMapper = () => callIndexedGetter(getFftMirrorRippleColor)
81 changes: 81 additions & 0 deletions src/backend/pattern/fftRandomRipple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { IColorGetter, IColorMapper } from 'src/typings'
import { callIndexedGetter } from './mappers'
import { pixelsCount, hueToColor } from '../shared'
import { settings } from 'src/settings'
import { audioState } from '../wsAudio'

interface Ripple {
pos: number
radius: number
brightness: number
hue: number
}

let ripples: Ripple[] = []
let lastTime = Date.now()
let averages: number[] = []
let hueShift = 0
const attenuation = 0.9
const speed = 60
const shiftSpeed = 40
const offsetRange = pixelsCount * 0.25

function spawnRipple(bin: number, mag: number) {
const hue = ((bin / audioState.bins.length) * 360 + hueShift) % 360
let pos = (bin / audioState.bins.length) * pixelsCount
pos += (Math.random() - 0.5) * offsetRange
if (pos < 0) pos = 0
if (pos >= pixelsCount) pos = pixelsCount - 1
ripples.push({
pos,
radius: 1,
brightness: Math.min(1, mag),
hue,
})
}

function update(time: number) {
const dt = (time - lastTime) * settings.effectSpeed
lastTime = time
const bins = audioState.bins
if (bins && bins.length) {
if (averages.length !== bins.length) averages = bins.slice()
for (let i = 0; i < bins.length; i++) {
const m = bins[i]
averages[i] = averages[i] * 0.9 + m * 0.1
if (m > averages[i] * 1.5) spawnRipple(i, m)
}
}
ripples.forEach(r => {
r.radius += (speed * dt) / 1000
r.brightness *= Math.pow(attenuation, dt / 16)
})
ripples = ripples.filter(r => r.brightness > 0.05)
hueShift = (hueShift + (shiftSpeed * dt) / 1000) % 360
}

export const getFftRandomRippleColor: IColorGetter = (index, time) => {
if (index === 0) update(time)
let r = 0
let g = 0
let b = 0
for (const ripple of ripples) {
const dist = Math.abs(index - ripple.pos)
if (dist <= ripple.radius) {
const intensity = ripple.brightness * (1 - dist / ripple.radius)
const { r: rr, g: gg, b: bb } = hueToColor(ripple.hue).rgb()
r += rr * intensity
g += gg * intensity
b += bb * intensity
}
}
return [Math.min(255, Math.round(r)), Math.min(255, Math.round(g)), Math.min(255, Math.round(b))]
}

export function resetFftRandomRipples() {
ripples = []
lastTime = Date.now()
averages = []
}

export const fftRandomRippleMapper: IColorMapper = () => callIndexedGetter(getFftRandomRippleColor)
5 changes: 4 additions & 1 deletion src/backend/pattern/fftRipple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ interface Ripple {
let ripples: Ripple[] = []
let lastTime = Date.now()
let averages: number[] = []
let hueShift = 0
const attenuation = 0.9
const speed = 60
const shiftSpeed = 40

function spawnRipple(bin: number, mag: number) {
const hue = (bin / audioState.bins.length) * 270
const hue = ((bin / audioState.bins.length) * 360 + hueShift) % 360
ripples.push({
pos: (bin / audioState.bins.length) * pixelsCount,
radius: 1,
Expand All @@ -44,6 +46,7 @@ function update(time: number) {
r.brightness *= Math.pow(attenuation, dt / 16)
})
ripples = ripples.filter(r => r.brightness > 0.05)
hueShift = (hueShift + (shiftSpeed * dt) / 1000) % 360
}

export const getFftRippleColor: IColorGetter = (index, time) => {
Expand Down
6 changes: 4 additions & 2 deletions src/backend/pattern/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { getBreatheColor } from './breathe'
import { getWaveColor } from './wave'
import { getHeartbeatColor, getStrobeColor, getPulseColor, getGradientPulseColor, getMultiPulseColor } from './extra'
import { rippleMapper } from './ripple'
import { musicRippleMapper } from './musicRipple'
import { fftRippleMapper } from './fftRipple'
import { fftRandomRippleMapper } from './fftRandomRipple'
import { fftMirrorRippleMapper } from './fftMirrorRipple'
import { createIndexedMapper, createFlatMapper } from './mappers'

const transitionDuration = 250
Expand Down Expand Up @@ -39,8 +40,9 @@ const mappers: Record<IMode, IColorMapper> = {
[IMode.GradientPulse]: createIndexedMapper(getGradientPulseColor),
[IMode.MultiPulse]: createIndexedMapper(getMultiPulseColor),
[IMode.Ripple]: rippleMapper,
[IMode.MusicRipple]: musicRippleMapper,
[IMode.FftRipple]: fftRippleMapper,
[IMode.FftRandomRipple]: fftRandomRippleMapper,
[IMode.FftMirrorRipple]: fftMirrorRippleMapper,
}

export function getPixels(mode: IMode): IArrColor[][] {
Expand Down
52 changes: 0 additions & 52 deletions src/backend/pattern/musicRipple.ts

This file was deleted.

4 changes: 3 additions & 1 deletion src/backend/telegram/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export function selectMode(menuTemplate: MenuTemplate<Context>) {
[IMode.GradientPulse]: '🎇',
[IMode.MultiPulse]: '🎆',
[IMode.Ripple]: '💧',
[IMode.MusicRipple]: '🎶',
[IMode.FftRipple]: '🎶',
[IMode.FftRandomRipple]: '🔀',
[IMode.FftMirrorRipple]: '🔁',
},
{
formatState,
Expand Down
3 changes: 2 additions & 1 deletion src/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ export enum IMode {
GradientPulse,
MultiPulse,
Ripple,
MusicRipple,
FftRipple,
FftRandomRipple,
FftMirrorRipple,
}

export interface ISettings {
Expand Down
17 changes: 17 additions & 0 deletions tests/fftMirrorRipple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getFftMirrorRippleColor, resetFftMirrorRipples } from '../src/backend/pattern/fftMirrorRipple'
import { audioState } from '../src/backend/wsAudio'

beforeEach(() => {
resetFftMirrorRipples()
audioState.bins = Array(8).fill(0)
})

describe('fft mirror ripple pattern', () => {
test('spawns ripple on spike', () => {
audioState.bins[0] = 2
;(getFftMirrorRippleColor as any)(0, 0)
audioState.bins[0] = 10
const color = (getFftMirrorRippleColor as any)(0, 16)
expect(color).not.toEqual([0, 0, 0])
})
})
20 changes: 20 additions & 0 deletions tests/fftRandomRipple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getFftRandomRippleColor, resetFftRandomRipples } from '../src/backend/pattern/fftRandomRipple'
import { audioState } from '../src/backend/wsAudio'

beforeEach(() => {
resetFftRandomRipples()
audioState.bins = Array(8).fill(0)
})

describe('fft random ripple pattern', () => {
test('spawns ripple on spike', () => {
const orig = Math.random
;(Math as any).random = () => 0
audioState.bins[0] = 2
;(getFftRandomRippleColor as any)(0, 0)
audioState.bins[0] = 10
const color = (getFftRandomRippleColor as any)(0, 16)
expect(color).not.toEqual([0, 0, 0])
Math.random = orig
})
})
19 changes: 0 additions & 19 deletions tests/musicRipple.test.ts

This file was deleted.