From 5b3abea70c6269ef456e34b79914d574964f659f Mon Sep 17 00:00:00 2001 From: Roy Kakkenberg <38660000+Yoronex@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:42:51 +0100 Subject: [PATCH] feat(lights-sparkle): make sparkle effect permutation-based (#14) * feat(lights-sparkle): enable lights by permutation instead of random * fix(lights-sparkle): not all lights switching color * feat(centurion): add random moving head movement effects --- src/modules/lights/effects/color/sparkle.ts | 44 ++++++++++---- src/modules/modes/centurion/centurion-mode.ts | 60 ++++++++++++++++--- .../tapes/gebroeders-scooter-centurion.ts | 13 ---- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/src/modules/lights/effects/color/sparkle.ts b/src/modules/lights/effects/color/sparkle.ts index 5ea7c7e..fe586a9 100644 --- a/src/modules/lights/effects/color/sparkle.ts +++ b/src/modules/lights/effects/color/sparkle.ts @@ -25,7 +25,8 @@ export interface SparkleProps { dimDuration?: number; /** - * After how many ms (approximately) a ratio of lights should be turned on + * After how many ms (approximately) a ratio of lights should be turned on. + * Defaults to 0, which enables the ratio of lights on the beat of the music. * @isInt * @minimum 0 */ @@ -37,9 +38,9 @@ export type SparkleCreateParams = BaseLightsEffectCreateParams & { props: SparkleProps; }; -const DEFAULT_RATIO = 0.2; +const DEFAULT_RATIO = 0.3; const DEFAULT_DIM_DURATION = 800; -const DEFAULT_CYCLE_TIME = 200; +const DEFAULT_CYCLE_TIME = 0; export default class Sparkle extends LightsEffect { private beats: Date[]; @@ -68,9 +69,33 @@ export default class Sparkle extends LightsEffect { new Sparkle(lightsGroup, props, features); } + /** + * Enable a subset of lights, i.e. set their brightness back to 1 + * @private + */ + private enableLights() { + const ratio = this.props.ratio ?? DEFAULT_RATIO; + + // Create a random permutation of the lights + const indices = this.beats.map((b, i) => i); + const permutation = indices.sort((a, b) => Math.random() - 0.5); + // Enable only the ratio of all lights + const toEnable = permutation.slice(0, Math.round(ratio * this.beats.length)); + + this.beats?.forEach((b, i) => { + if (toEnable.includes(i)) { + this.colorIndices[i] = (this.colorIndices[i] + 1) % Math.max(this.props.colors.length); + this.beats[i] = new Date(); + } + }); + this.previousTick = new Date(); + } + destroy(): void {} - beat(): void {} + beat(): void { + if (!this.props.cycleTime) this.enableLights(); + } private getProgression(beat: Date) { const dimDuration = this.props.dimDuration ?? DEFAULT_DIM_DURATION; @@ -82,17 +107,10 @@ export default class Sparkle extends LightsEffect { const nrPars = this.lightsGroup.pars.length; const { colors, cycleTime: propsCycleTime, ratio: propsRatio } = this.props; const cycleTime = propsCycleTime ?? DEFAULT_CYCLE_TIME; - const ratio = propsRatio ?? DEFAULT_RATIO; // Turn on some lights according to the ratio if we have reached the time - if (new Date().getTime() - this.previousTick.getTime() >= cycleTime) { - this.beats?.forEach((b, i) => { - if (Math.random() <= ratio) { - this.colorIndices[i] = (this.colorIndices[i] + i) % Math.max(colors.length); - this.beats[i] = new Date(); - } - }); - this.previousTick = new Date(); + if (cycleTime > 0 && new Date().getTime() - this.previousTick.getTime() >= cycleTime) { + this.enableLights(); } this.lightsGroup.pars.forEach((p, i) => { diff --git a/src/modules/modes/centurion/centurion-mode.ts b/src/modules/modes/centurion/centurion-mode.ts index ff6a12c..bb3f6fb 100644 --- a/src/modules/modes/centurion/centurion-mode.ts +++ b/src/modules/modes/centurion/centurion-mode.ts @@ -5,7 +5,7 @@ import SetEffectsHandler from '../../handlers/lights/set-effects-handler'; import SimpleAudioHandler from '../../handlers/audio/simple-audio-handler'; import MixTape, { FeedEvent, Horn, Song, SongData } from './tapes/mix-tape'; import { BeatFadeOut, StaticColor } from '../../lights/effects/color'; -import { SearchLight } from '../../lights/effects/movement'; +import { ClassicRotate, SearchLight } from '../../lights/effects/movement'; import { getTwoComplementaryRgbColors, RgbColor } from '../../lights/color-definitions'; import { MusicEmitter } from '../../events'; import { TrackChangeEvent } from '../../events/music-emitter-events'; @@ -143,27 +143,68 @@ export default class CenturionMode extends BaseMode< ); } + private getRandomMovementEffect(): LightsEffectBuilder { + // If we do not have a bpm, choose SearchLight as the default + const defaultEffect = SearchLight.build({ radiusFactor: 1.5, cycleTime: 3000 }); + if (!this.beatGenerator.bpm) { + return defaultEffect; + } + + const baseCycleTime = Math.round(360000 / this.beatGenerator.bpm); + const classicRotateProb = Math.max(0, Math.min(1, (this.beatGenerator.bpm - 120) / 60)); + console.log(this.beatGenerator.bpm, classicRotateProb); + + const effects = [ + { + effect: ClassicRotate.build({ cycleTime: baseCycleTime * 4 }), + probability: classicRotateProb, + }, + { + effect: SearchLight.build({ radiusFactor: 1.5, cycleTime: baseCycleTime }), + probability: 1 - classicRotateProb, + }, + ]; + let factor = Math.random(); + + return ( + effects.find((effect, i) => { + const previous = effects.slice(0, i).reduce((x, e) => x + e.probability, 0); + return previous <= factor && previous + effect.probability > factor; + })?.effect || defaultEffect + ); + } + /** * Get a random effect given a set of colors * @param colors * @private */ - private getRandomEffect(colors: RgbColor[]): LightsEffectBuilder { + private getRandomParEffect(colors: RgbColor[]): LightsEffectBuilder { const effects = [ { effect: BeatFadeOut.build({ colors, enableFade: false, nrBlacks: 1 }), probability: 0.8, }, { - effect: Wave.build({ color: colors[0] }), + effect: Sparkle.build({ colors }), probability: 0.1, }, { - effect: Sparkle.build({ colors }), + effect: Wave.build({ color: colors[0] }), probability: 0.1, }, ]; - const factor = Math.random(); + let factor = Math.random(); + + if (!this.beatGenerator.bpm || this.beatGenerator.bpm < 120) { + // If the music is relatively slow or really fast, do not use wave or sparkle + factor = factor - 0.2; + } else if (this.beatGenerator.bpm > 160) { + // If the music is really fast, do not use wave, as the other effects will be + // much more energetic + factor = factor - 0.1; + } + return ( effects.find((effect, i) => { const previous = effects.slice(0, i).reduce((x, e) => x + e.probability, 0); @@ -195,14 +236,14 @@ export default class CenturionMode extends BaseMode< const { colorNames } = getTwoComplementaryRgbColors(); this.currentColors = colorNames; - const movingHeadEffectMovement = SearchLight.build({ radiusFactor: 1.5, cycleTime: 3000 }); + const newMovementEffectBuilder = this.getRandomMovementEffect(); const movingHeadEffectColor = StaticColor.build({ color: RgbColor.WHITE }); - const newEffectBuilder = this.getRandomEffect(colorNames); + const newEffectBuilder = this.getRandomParEffect(colorNames); this.lights.forEach((l) => { // If we have a moving head, assign a movement effect if (l.movingHeadRgbs.length > 0 || l.movingHeadWheels.length > 0) { - this.lightsHandler.setMovementEffect(l, [movingHeadEffectMovement]); + this.lightsHandler.setMovementEffect(l, [newMovementEffectBuilder]); } // If we have a wheel moving head, assign a static color if (l.movingHeadWheels.length > 0) { @@ -237,8 +278,9 @@ export default class CenturionMode extends BaseMode< this.screenHandler.horn(event.data.strobeTime ?? STROBE_TIME, event.data.counter); } else if (event.type === 'song') { this.lastSongEvent = event; - this.setRandomLightEffects(); this.setBpm(event.data); + // Set effects after setting bpm, because the effects might need the bpm + this.setRandomLightEffects(); this.emitSong(event.data); } else if (event.type === 'effect') { this.lights.forEach((l) => { diff --git a/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion.ts b/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion.ts index 7e24af4..df885f5 100644 --- a/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion.ts +++ b/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion.ts @@ -1414,23 +1414,10 @@ const centurion: MixTape = { bpm: 147, }, }, - { - timestamp: 4573.001, - type: 'effect', - data: { - reset: true, - effects: { - pars: [], - movingHeadWheelColor: [], - movingHeadWheelMovement: [], - }, - }, - }, { timestamp: 4573.002, type: 'effect', data: { - reset: true, effects: { pars: [StaticColor.build({ color: RgbColor.GOLD, dimTimeMs: 3000 })], movingHeadWheelColor: [],