Skip to content

Commit

Permalink
feat(lights-sparkle): make sparkle effect permutation-based (#14)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Yoronex authored Nov 17, 2024
1 parent 6841367 commit 5b3abea
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 35 deletions.
44 changes: 31 additions & 13 deletions src/modules/lights/effects/color/sparkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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<SparkleProps> {
private beats: Date[];
Expand Down Expand Up @@ -68,9 +69,33 @@ export default class Sparkle extends LightsEffect<SparkleProps> {
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;
Expand All @@ -82,17 +107,10 @@ export default class Sparkle extends LightsEffect<SparkleProps> {
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) => {
Expand Down
60 changes: 51 additions & 9 deletions src/modules/modes/centurion/centurion-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand Down
13 changes: 0 additions & 13 deletions src/modules/modes/centurion/tapes/gebroeders-scooter-centurion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down

0 comments on commit 5b3abea

Please sign in to comment.