diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6dde38c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,jsx,ts,tsx,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/src/index.ts b/src/index.ts index 193146c..ccfb458 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,8 @@ async function createApp(): Promise { const emitterStore = EmitterStore.getInstance(); + ArtificialBeatGenerator.getInstance().init(emitterStore.musicEmitter); + const handlerManager = HandlerManager.getInstance(io, emitterStore); await handlerManager.init(); const socketConnectionManager = new SocketConnectionManager( @@ -64,7 +66,6 @@ async function createApp(): Promise { ); ModeManager.getInstance().init(emitterStore); - ArtificialBeatGenerator.getInstance().init(emitterStore.musicEmitter); if ( process.env.SPOTIFY_ENABLE === 'true' && diff --git a/src/modules/handlers/lights/develop-effects-handler.ts b/src/modules/handlers/lights/develop-effects-handler.ts index 29f16ad..8f97f28 100644 --- a/src/modules/handlers/lights/develop-effects-handler.ts +++ b/src/modules/handlers/lights/develop-effects-handler.ts @@ -1,13 +1,51 @@ import EffectsHandler from './effects-handler'; import { LightsGroup } from '../../lights/entities'; import { RgbColor } from '../../lights/color-definitions'; -import { Fire, StaticColor } from '../../lights/effects/color'; +import { Wave } from '../../lights/effects/color'; import TableRotate from '../../lights/effects/movement/table-rotate'; +import logger from '../../../logger'; +import { ArtificialBeatGenerator } from '../../beats/artificial-beat-generator'; +import { + LightsEffectDirection, + LightsEffectPattern, +} from '../../lights/effects/lights-effect-pattern'; export default class DevelopEffectsHandler extends EffectsHandler { public registerEntity(entity: LightsGroup) { + if (this.entities.length === 0) { + ArtificialBeatGenerator.getInstance().start(120); + } + super.registerEntity(entity); - this.groupColorEffects.set(entity, [new Fire(entity, {})]); - this.groupMovementEffects.set(entity, [new TableRotate(entity, {})]); + setTimeout(() => { + logger.info(`Set develop effects for ${entity.name}`); + this.groupColorEffects.set(entity, [ + // new BeatFadeOut(entity, { + // colors: [ + // RgbColor.ORANGE, + // RgbColor.BLUE, + // // RgbColor.GREEN, + // // RgbColor.YELLOW, + // // RgbColor.LIGHTPINK, + // ], + // nrBlacks: 1, + // }), + new Wave(entity, { + colors: [RgbColor.ORANGE], + nrWaves: 1, + pattern: LightsEffectPattern.HORIZONTAL, + direction: LightsEffectDirection.BACKWARDS, + }), + ]); + this.groupMovementEffects.set(entity, [new TableRotate(entity, {})]); + }, 3000); + } + + public removeEntity(entityCopy: LightsGroup) { + super.removeEntity(entityCopy); + + if (this.entities.length === 0) { + ArtificialBeatGenerator.getInstance().stop(); + } } } diff --git a/src/modules/handlers/lights/random-effects-handler.ts b/src/modules/handlers/lights/random-effects-handler.ts index 4d9e674..7657593 100644 --- a/src/modules/handlers/lights/random-effects-handler.ts +++ b/src/modules/handlers/lights/random-effects-handler.ts @@ -42,7 +42,7 @@ export default class RandomEffectsHandler extends EffectsHandler { ), ); } else if (random < 0.9) { - this.groupColorEffects.set(entity, new Wave(entity, { color: this.colors[0] })); + this.groupColorEffects.set(entity, new Wave(entity, { colors: this.colors })); } else { this.groupColorEffects.set(entity, new Sparkle(entity, { colors: this.colors })); } diff --git a/src/modules/lights/effects/color/beat-fade-out.ts b/src/modules/lights/effects/color/beat-fade-out.ts index a07588c..5a142a9 100644 --- a/src/modules/lights/effects/color/beat-fade-out.ts +++ b/src/modules/lights/effects/color/beat-fade-out.ts @@ -1,4 +1,9 @@ -import LightsEffect, { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import LightsEffect, { + BaseLightsEffectCreateParams, + BaseLightsEffectProgressionProps, + BaseLightsEffectProps, + LightsEffectBuilder, +} from '../lights-effect'; import { BeatEvent, TrackPropertiesEvent } from '../../../events/music-emitter-events'; import { LightsGroup, @@ -8,13 +13,16 @@ import { } from '../../entities'; import { RgbColor } from '../../color-definitions'; import { ColorEffects } from './color-effects'; - -export interface BeatFadeOutProps { - /** - * One or more colors that should be shown - */ - colors: RgbColor[]; - +import { + EffectProgressionBeatStrategy, + EffectProgressionTickStrategy, +} from '../progression-strategies'; +import EffectProgressionStrategy from '../progression-strategies/effect-progression-strategy'; +import LightsGroupFixture from '../../entities/lights-group-fixture'; +import { LightsEffectDirection, LightsEffectPattern } from '../lights-effect-pattern'; +import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory'; + +export interface BeatFadeOutProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps { /** * Whether the lights should be turned off using a fade effect * on each beat @@ -43,14 +51,32 @@ export type BeatFadeOutCreateParams = BaseLightsEffectCreateParams & { }; export default class BeatFadeOut extends LightsEffect { - private phase = 0; + private readonly nrSteps: number; private lastBeat = new Date().getTime(); // in ms since epoch; private beatLength: number = 1; // in ms; constructor(lightsGroup: LightsGroup, props: BeatFadeOutProps, features?: TrackPropertiesEvent) { - super(lightsGroup, features); + const nrSteps = props.colors.length + (props.nrBlacks ?? 0); + + const progressionMapperStrategy = new EffectProgressionMapFactory(lightsGroup).getMapper( + props.pattern, + nrSteps, + ); + + let progressionStrategy: EffectProgressionStrategy; + if (props.customCycleTime) { + progressionStrategy = new EffectProgressionTickStrategy(props.customCycleTime); + } else { + progressionStrategy = new EffectProgressionBeatStrategy( + progressionMapperStrategy.getNrFixtures(), + ); + } + + super(lightsGroup, progressionStrategy, progressionMapperStrategy, props.direction, features); + + this.nrSteps = nrSteps; this.props = props; if (this.props.customCycleTime) { @@ -72,21 +98,22 @@ export default class BeatFadeOut extends LightsEffect { destroy(): void {} beat(event: BeatEvent): void { + super.beat(event); + // If we use a custom cycle time, ignore all beats if (this.props.customCycleTime) return; this.lastBeat = new Date().getTime(); this.beatLength = event.beat.duration * 1000; - this.phase = (this.phase + 1) % (this.props.colors.length + (this.props.nrBlacks ?? 0)); } - getCurrentColor(i: number) { - const { colors, nrBlacks } = this.props; - const nrColors = colors.length + (nrBlacks || 0); - const index = (i + this.phase) % nrColors; - if (index === colors.length) { - return null; - } + getCurrentColor(fixture: LightsGroupFixture, i: number) { + const { colors } = this.props; + const progression = this.getProgression(new Date(), fixture); + + const phase = progression * this.getEffectNrFixtures(); + const index = Math.round(phase % this.nrSteps); + return colors[index]; } @@ -99,7 +126,7 @@ export default class BeatFadeOut extends LightsEffect { ? Math.max(1 - (new Date().getTime() - this.lastBeat) / this.beatLength, 0) : 1; - const color = this.getCurrentColor(i); + const color = this.getCurrentColor(p, i); if (color == null) { p.fixture.setMasterDimmer(0); } else { @@ -109,17 +136,15 @@ export default class BeatFadeOut extends LightsEffect { } tick(): LightsGroup { - if (this.props.customCycleTime) { - const now = new Date().getTime(); - const msDiff = now - this.lastBeat; - if (msDiff >= this.props.customCycleTime) { - this.lastBeat = now; - this.phase = (this.phase + 1) % (this.props.colors.length + (this.props.nrBlacks ?? 0)); - } - } - - this.lightsGroup.pars.forEach(this.applyColorToFixture.bind(this)); - this.lightsGroup.movingHeadRgbs.forEach(this.applyColorToFixture.bind(this)); + super.tick(); + + [ + ...this.lightsGroup.pars, + ...this.lightsGroup.movingHeadRgbs, + ...this.lightsGroup.movingHeadWheels, + ] + .sort((a, b) => a.positionX - b.positionX) + .forEach(this.applyColorToFixture.bind(this)); return this.lightsGroup; } diff --git a/src/modules/lights/effects/color/fire.ts b/src/modules/lights/effects/color/fire.ts index e9169c8..7a392dc 100644 --- a/src/modules/lights/effects/color/fire.ts +++ b/src/modules/lights/effects/color/fire.ts @@ -11,7 +11,7 @@ export type FireCreateParams = BaseLightsEffectCreateParams & { export default class Fire extends LightsEffect { constructor(lightsGroup: LightsGroup, props?: FireProps, features?: TrackPropertiesEvent) { - super(lightsGroup, features); + super(lightsGroup, undefined, undefined, undefined, features); } public static build(props?: FireProps): LightsEffectBuilder { diff --git a/src/modules/lights/effects/color/single-flood.ts b/src/modules/lights/effects/color/single-flood.ts index 61447f6..9e8ec36 100644 --- a/src/modules/lights/effects/color/single-flood.ts +++ b/src/modules/lights/effects/color/single-flood.ts @@ -44,7 +44,7 @@ export default class SingleFlood extends LightsEffect { return (lightsGroup) => new SingleFlood(lightsGroup, props); } - private getProgression(currentTick: Date) { + protected getProgression(currentTick: Date) { const dimMilliseconds = this.props.dimMilliseconds ?? DEFAULT_DIM_MILLISECONDS; const diff = Math.max(0, currentTick.getTime() - this.effectStartTime.getTime()); if (diff < TURN_ON_TIME) return diff / TURN_ON_TIME; diff --git a/src/modules/lights/effects/color/sparkle.ts b/src/modules/lights/effects/color/sparkle.ts index fe586a9..8068c45 100644 --- a/src/modules/lights/effects/color/sparkle.ts +++ b/src/modules/lights/effects/color/sparkle.ts @@ -1,15 +1,14 @@ -import LightsEffect, { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import LightsEffect, { + BaseLightsEffectCreateParams, + BaseLightsEffectProps, + LightsEffectBuilder, +} from '../lights-effect'; import { RgbColor } from '../../color-definitions'; import { LightsGroup } from '../../entities'; import { TrackPropertiesEvent } from '../../../events/music-emitter-events'; import { ColorEffects } from './color-effects'; -export interface SparkleProps { - /** - * Colors of the lights - */ - colors: RgbColor[]; - +export interface SparkleProps extends BaseLightsEffectProps { /** * What percentage (on average) of the lights should be turned on * @minimum 0 @@ -55,7 +54,7 @@ export default class Sparkle extends LightsEffect { * @param features */ constructor(lightsGroup: LightsGroup, props: SparkleProps, features?: TrackPropertiesEvent) { - super(lightsGroup, features); + super(lightsGroup, undefined, undefined, undefined, features); const nrFixtures = lightsGroup.pars.length + lightsGroup.movingHeadRgbs.length; this.beats = new Array(nrFixtures).fill(new Date(0)); @@ -97,7 +96,7 @@ export default class Sparkle extends LightsEffect { if (!this.props.cycleTime) this.enableLights(); } - private getProgression(beat: Date) { + private getDimProgression(beat: Date) { const dimDuration = this.props.dimDuration ?? DEFAULT_DIM_DURATION; return Math.max(1 - (new Date().getTime() - beat.getTime()) / dimDuration, 0); @@ -115,7 +114,7 @@ export default class Sparkle extends LightsEffect { this.lightsGroup.pars.forEach((p, i) => { const index = i; - const progression = this.getProgression(this.beats[index]); + const progression = this.getDimProgression(this.beats[index]); const colorIndex = this.colorIndices[index]; const color = colors[colorIndex % colors.length]; p.fixture.setColor(color); @@ -123,7 +122,7 @@ export default class Sparkle extends LightsEffect { }); this.lightsGroup.movingHeadRgbs.forEach((p, i) => { const index = i; - const progression = this.getProgression(this.beats[nrPars + index]); + const progression = this.getDimProgression(this.beats[nrPars + index]); const colorIndex = this.colorIndices[nrPars + index]; const color = colors[colorIndex % colors.length]; p.fixture.setColor(color); diff --git a/src/modules/lights/effects/color/static-color.ts b/src/modules/lights/effects/color/static-color.ts index 8bef52e..ca93f10 100644 --- a/src/modules/lights/effects/color/static-color.ts +++ b/src/modules/lights/effects/color/static-color.ts @@ -58,7 +58,7 @@ export default class StaticColor extends LightsEffect { private cycleStartTick: Date = new Date(); constructor(lightsGroup: LightsGroup, props: StaticColorProps, features?: TrackPropertiesEvent) { - super(lightsGroup, features); + super(lightsGroup, undefined, undefined, undefined, features); this.props = props; this.lightsGroup.fixtures.forEach((f) => { @@ -87,14 +87,14 @@ export default class StaticColor extends LightsEffect { destroy(): void {} - private getProgression(durationMs: number) { + private getDimProgression(durationMs: number) { return Math.min(1, (new Date().getTime() - this.cycleStartTick.getTime()) / durationMs); } tick(): LightsGroup { let progression = 1; - if (this.props.brightenTimeMs) progression = this.getProgression(this.props.brightenTimeMs); - if (this.props.dimTimeMs) progression = 1 - this.getProgression(this.props.dimTimeMs); + if (this.props.brightenTimeMs) progression = this.getDimProgression(this.props.brightenTimeMs); + if (this.props.dimTimeMs) progression = 1 - this.getDimProgression(this.props.dimTimeMs); this.lightsGroup.fixtures .sort((f1, f2) => f1.firstChannel - f2.firstChannel) diff --git a/src/modules/lights/effects/color/strobe.ts b/src/modules/lights/effects/color/strobe.ts index 8826236..92d7b2f 100644 --- a/src/modules/lights/effects/color/strobe.ts +++ b/src/modules/lights/effects/color/strobe.ts @@ -19,7 +19,7 @@ export type StrobeCreateParams = BaseLightsEffectCreateParams & { export default class Strobe extends LightsEffect { constructor(lightsGroup: LightsGroup, props: StrobeProps, features?: TrackPropertiesEvent) { - super(lightsGroup, features); + super(lightsGroup, undefined, undefined, undefined, features); this.props = props; this.lightsGroup.pars.forEach((p) => { diff --git a/src/modules/lights/effects/color/wave.ts b/src/modules/lights/effects/color/wave.ts index f5914dd..1d218c3 100644 --- a/src/modules/lights/effects/color/wave.ts +++ b/src/modules/lights/effects/color/wave.ts @@ -1,14 +1,15 @@ -import LightsEffect, { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import LightsEffect, { + BaseLightsEffectCreateParams, + BaseLightsEffectProgressionProps, + BaseLightsEffectProps, + LightsEffectBuilder, +} from '../lights-effect'; import { LightsGroup, LightsGroupMovingHeadRgbs, LightsGroupPars } from '../../entities'; -import { RgbColor } from '../../color-definitions'; import { ColorEffects } from './color-effects'; +import { EffectProgressionTickStrategy } from '../progression-strategies'; +import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory'; -export interface WaveProps { - /** - * Color of the lights - */ - color: RgbColor; - +export interface WaveProps extends BaseLightsEffectProps, BaseLightsEffectProgressionProps { /** * Number of waves, ignored if singleWave=true (1 by default) * @isInt @@ -36,14 +37,18 @@ export type WaveCreateParams = BaseLightsEffectCreateParams & { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const DEFAULT_NR_WAVES = 2; +const DEFAULT_NR_WAVES = 1; const DEFAULT_CYCLE_TIME = 2000; export default class Wave extends LightsEffect { - private cycleStartTick: Date = new Date(); - constructor(lightsGroup: LightsGroup, props: WaveProps) { - super(lightsGroup); + const cycleTime = props.cycleTime ?? DEFAULT_CYCLE_TIME; + super( + lightsGroup, + new EffectProgressionTickStrategy(cycleTime, props.singleWave), + new EffectProgressionMapFactory(lightsGroup).getMapper(props.pattern), + props.direction, + ); this.props = props; } @@ -55,27 +60,6 @@ export default class Wave extends LightsEffect { beat(): void {} - /** - * Get progression of the complete animation (in the range [0, 1]) - * @param currentTick - * @private - */ - private getProgression(currentTick: Date) { - const cycleTime = this.props.cycleTime ?? DEFAULT_CYCLE_TIME; - return Math.min(1, (currentTick.getTime() - this.cycleStartTick.getTime()) / cycleTime); - } - - /** - * Get the relative progression of an individual fixture. The fixture at the start of the chain - * will be in range [0, 2]; the fixture at the end in range [-1, 1]; all other fixtures have a - * range relatively between those two ranges. - * @private - */ - private getRelativeProgression(absoluteProgression: number, fixtureIndex: number) { - const nrLights = this.lightsGroup.pars.length + this.lightsGroup.movingHeadRgbs.length; - return fixtureIndex / nrLights - 1 + 2 * absoluteProgression; - } - /** * Get a fixture's brightness level * @private @@ -84,34 +68,30 @@ export default class Wave extends LightsEffect { private getBrightness(relativeProgression: number) { // If we only show a single wave, we want it to be visible. So, by trial and error a size of // 1.5 fits best. This works, because the singleWave prop - const nrWaves = this.props.singleWave ? 1.5 : (this.props.nrWaves ?? DEFAULT_NR_WAVES); + const nrWaves = this.props.singleWave ? 0.75 : (this.props.nrWaves ?? DEFAULT_NR_WAVES); // If we are outside the first half sine wave, we set some bounds. if (this.props.singleWave && relativeProgression < 0) return 0; if (this.props.singleWave && relativeProgression > 1) return 0; - return Math.sin(relativeProgression * nrWaves * Math.PI); + return Math.sin(relativeProgression * nrWaves * Math.PI * 2); } tick(): LightsGroup { + super.tick(); + const currentTick = new Date(); - const progression = this.getProgression(currentTick); - if (progression >= 1 && !this.props.singleWave) { - this.cycleStartTick = currentTick; - } // Apply the wave effect to the fixture in a group - const apply = (p: LightsGroupPars | LightsGroupMovingHeadRgbs, i: number) => { - const relativeProgression = this.getRelativeProgression(progression, i); - const brightness = this.getBrightness(relativeProgression); + const apply = (p: LightsGroupPars | LightsGroupMovingHeadRgbs) => { + const progression = this.getProgression(currentTick, p); + const brightness = this.getBrightness(progression); p.fixture.setMasterDimmer(Math.max(0, brightness * 255)); - p.fixture.setColor(this.props.color); + p.fixture.setColor(this.props.colors[0]); }; - this.lightsGroup.pars.sort((p1, p2) => p2.firstChannel - p1.firstChannel).forEach(apply); - this.lightsGroup.movingHeadRgbs - .sort((p1, p2) => p2.firstChannel - p1.firstChannel) - .forEach(apply); + this.lightsGroup.pars.forEach(apply); + this.lightsGroup.movingHeadRgbs.forEach(apply); return this.lightsGroup; } diff --git a/src/modules/lights/effects/lights-effect-pattern.ts b/src/modules/lights/effects/lights-effect-pattern.ts new file mode 100644 index 0000000..56f53f6 --- /dev/null +++ b/src/modules/lights/effects/lights-effect-pattern.ts @@ -0,0 +1,14 @@ +export enum LightsEffectPattern { + HORIZONTAL, + VERTICAL, + DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT, + DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT, + CENTERED_CIRCULAR, + CENTERED_SQUARED, + ROTATIONAL, +} + +export enum LightsEffectDirection { + FORWARDS, + BACKWARDS, +} diff --git a/src/modules/lights/effects/lights-effect.ts b/src/modules/lights/effects/lights-effect.ts index b27387f..cd030b8 100644 --- a/src/modules/lights/effects/lights-effect.ts +++ b/src/modules/lights/effects/lights-effect.ts @@ -1,33 +1,96 @@ import { BeatEvent, TrackPropertiesEvent } from '../../events/music-emitter-events'; import { LightsGroup } from '../entities'; +import EffectProgressionStrategy from './progression-strategies/effect-progression-strategy'; +import LightsGroupFixture from '../entities/lights-group-fixture'; +import { LightsEffectDirection, LightsEffectPattern } from './lights-effect-pattern'; +import EffectProgressionMapStrategy from './progression-strategies/mappers/effect-progression-map-strategy'; +import EffectProgressionMapFactory from './progression-strategies/mappers/effect-progression-map-factory'; +import { RgbColor } from '../color-definitions'; export type LightsEffectBuilder

= LightsEffect

> = ( lightsGroup: LightsGroup, features?: TrackPropertiesEvent, ) => T; -export type BaseLightsEffectCreateParams = {}; +export interface BaseLightsEffectProps { + /** + * One or more colors that should be shown + */ + colors: RgbColor[]; +} -export default abstract class LightsEffect

{ - public lightsGroup: LightsGroup; +export interface BaseLightsEffectProgressionProps { + /** + * 2D pattern for this effect. Defaults to "HORIZONTAL" + */ + pattern?: LightsEffectPattern; - protected features?: TrackPropertiesEvent; + /** + * Direction of this effect. Defaults to "FORWARDS" + */ + direction?: LightsEffectDirection; +} +export type BaseLightsEffectCreateParams = {}; + +export default abstract class LightsEffect

{ protected props: P; - public constructor(lightsGroup: LightsGroup, features?: TrackPropertiesEvent) { - this.lightsGroup = lightsGroup; - this.features = features; + private readonly progressionMapperStrategy: EffectProgressionMapStrategy; + + protected constructor( + public readonly lightsGroup: LightsGroup, + private readonly progressionStrategy?: EffectProgressionStrategy, + progressionMapperStrategy?: EffectProgressionMapStrategy, + private patternDirection = LightsEffectDirection.FORWARDS, + protected features?: TrackPropertiesEvent, + ) { + if (!progressionMapperStrategy) { + this.progressionMapperStrategy = new EffectProgressionMapFactory(this.lightsGroup).getMapper( + LightsEffectPattern.HORIZONTAL, + ); + } else { + this.progressionMapperStrategy = progressionMapperStrategy; + } } public setNewProps(props: P) { this.props = props; } + protected getEffectNrFixtures(): number { + return this.progressionMapperStrategy.getNrFixtures(); + } + + protected getProgression(currentTick: Date, fixture: LightsGroupFixture): number { + if (!this.progressionStrategy) return 0; + + let progression = 1 - this.progressionStrategy.getProgression(currentTick); + if (this.patternDirection === LightsEffectDirection.FORWARDS) { + progression = 1 - progression; + } + + return this.progressionMapperStrategy.getProgression(progression, fixture); + } + /** * Clean up effect when it is destroyed */ abstract destroy(): void; - abstract tick(): LightsGroup; - abstract beat(event: BeatEvent): void; + + /** + * Process the tick in the effect's progression + */ + public tick(): LightsGroup { + this.progressionStrategy?.tick(); + return this.lightsGroup; + } + + /** + * Process the beat in the effect's progression + * @param event + */ + public beat(event: BeatEvent): void { + this.progressionStrategy?.beat(event); + } } diff --git a/src/modules/lights/effects/movement/base-rotate.ts b/src/modules/lights/effects/movement/base-rotate.ts index c644037..eac81d6 100644 --- a/src/modules/lights/effects/movement/base-rotate.ts +++ b/src/modules/lights/effects/movement/base-rotate.ts @@ -1,5 +1,9 @@ -import LightsEffect from '../lights-effect'; +import LightsEffect, { BaseLightsEffectProgressionProps } from '../lights-effect'; import { LightsGroup, LightsMovingHeadRgb, LightsMovingHeadWheel } from '../../entities'; +import { EffectProgressionTickStrategy } from '../progression-strategies'; +import { BeatEvent } from '../../../events/music-emitter-events'; +import { LightsEffectDirection, LightsEffectPattern } from '../lights-effect-pattern'; +import EffectProgressionMapFactory from '../progression-strategies/mappers/effect-progression-map-factory'; export interface BaseRotateProps { /** @@ -21,18 +25,17 @@ export interface BaseRotateProps { * setPosition() function. */ export default abstract class BaseRotate extends LightsEffect { - private cycleStartTick: Date = new Date(); - protected constructor( lightsGroup: LightsGroup, protected readonly defaults: Required, + progressionProps: BaseLightsEffectProgressionProps, + cycleTime?: number, ) { - super(lightsGroup); - } - - protected getProgression(currentTick: Date) { - const cycleTime = this.props.cycleTime ?? this.defaults.cycleTime; - return Math.min(1, (currentTick.getTime() - this.cycleStartTick.getTime()) / cycleTime); + super( + lightsGroup, + new EffectProgressionTickStrategy(cycleTime ?? defaults.cycleTime), + new EffectProgressionMapFactory(lightsGroup).getMapper(progressionProps.pattern), + ); } /** @@ -50,25 +53,26 @@ export default abstract class BaseRotate extends Ligh destroy(): void {} - beat(): void {} + beat(event: BeatEvent): void { + super.beat(event); + } tick(): LightsGroup { const currentTick = new Date(); - const progression = this.getProgression(currentTick); const offsetFactor = this.props.offsetFactor ?? this.defaults.offsetFactor; - if (progression >= 1) { - this.cycleStartTick = currentTick; - } this.lightsGroup.movingHeadWheels.forEach((m, i) => { + const progression = this.getProgression(currentTick, m); this.setPosition(m.fixture, progression, i * offsetFactor * 2 * Math.PI); }); this.lightsGroup.movingHeadRgbs.forEach((m, i) => { + const progression = this.getProgression(currentTick, m); const index = i + this.lightsGroup.movingHeadWheels.length; this.setPosition(m.fixture, progression, index * offsetFactor * 2 * Math.PI); }); + super.tick(); return this.lightsGroup; } } diff --git a/src/modules/lights/effects/movement/classic-rotate.ts b/src/modules/lights/effects/movement/classic-rotate.ts index 05edfc3..51434e7 100644 --- a/src/modules/lights/effects/movement/classic-rotate.ts +++ b/src/modules/lights/effects/movement/classic-rotate.ts @@ -1,9 +1,14 @@ -import { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import { + BaseLightsEffectCreateParams, + BaseLightsEffectProgressionProps, + LightsEffectBuilder, +} from '../lights-effect'; import { LightsGroup, LightsMovingHeadRgb, LightsMovingHeadWheel } from '../../entities'; import BaseRotate, { BaseRotateProps } from './base-rotate'; import { MovementEffects } from './movement-effetcs'; +import { LightsEffectDirection, LightsEffectPattern } from '../lights-effect-pattern'; -export interface ClassicRotateProps extends BaseRotateProps {} +export interface ClassicRotateProps extends BaseRotateProps, BaseLightsEffectProgressionProps {} export type ClassicRotateCreateParams = BaseLightsEffectCreateParams & { type: MovementEffects.ClassicRotate; @@ -19,7 +24,14 @@ export default class ClassicRotate extends BaseRotate { * @param props */ constructor(lightsGroup: LightsGroup, props: ClassicRotateProps) { - super(lightsGroup, { cycleTime: DEFAULT_CYCLE_TIME, offsetFactor: DEFAULT_OFFSET_FACTOR }); + super( + lightsGroup, + { + cycleTime: DEFAULT_CYCLE_TIME, + offsetFactor: DEFAULT_OFFSET_FACTOR, + }, + { pattern: props.pattern, direction: props.direction }, + ); this.props = props; } diff --git a/src/modules/lights/effects/movement/search-light.ts b/src/modules/lights/effects/movement/search-light.ts index f52c538..9ca4e50 100644 --- a/src/modules/lights/effects/movement/search-light.ts +++ b/src/modules/lights/effects/movement/search-light.ts @@ -1,9 +1,15 @@ -import { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import { + BaseLightsEffectCreateParams, + BaseLightsEffectProgressionProps, + BaseLightsEffectProps, + LightsEffectBuilder, +} from '../lights-effect'; import { LightsGroup, LightsMovingHeadRgb, LightsMovingHeadWheel } from '../../entities'; import BaseRotate, { BaseRotateProps } from './base-rotate'; import { MovementEffects } from './movement-effetcs'; +import { LightsEffectDirection, LightsEffectPattern } from '../lights-effect-pattern'; -export interface SearchLightProps extends BaseRotateProps { +export interface SearchLightProps extends BaseRotateProps, BaseLightsEffectProgressionProps { /** * Radius of the search light * @minimum 0 @@ -27,7 +33,14 @@ export default class SearchLight extends BaseRotate { * @param props */ constructor(lightsGroup: LightsGroup, props: SearchLightProps) { - super(lightsGroup, { cycleTime: DEFAULT_CYCLE_TIME, offsetFactor: DEFAULT_OFFSET_FACTOR }); + super( + lightsGroup, + { + cycleTime: DEFAULT_CYCLE_TIME, + offsetFactor: DEFAULT_OFFSET_FACTOR, + }, + { pattern: props.pattern, direction: props.direction }, + ); this.props = props; } diff --git a/src/modules/lights/effects/movement/table-rotate.ts b/src/modules/lights/effects/movement/table-rotate.ts index 6d02982..ab77c43 100644 --- a/src/modules/lights/effects/movement/table-rotate.ts +++ b/src/modules/lights/effects/movement/table-rotate.ts @@ -1,9 +1,13 @@ -import { BaseLightsEffectCreateParams, LightsEffectBuilder } from '../lights-effect'; +import { + BaseLightsEffectCreateParams, + BaseLightsEffectProgressionProps, + LightsEffectBuilder, +} from '../lights-effect'; import { LightsGroup, LightsMovingHeadRgb, LightsMovingHeadWheel } from '../../entities'; import BaseRotate, { BaseRotateProps } from './base-rotate'; import { MovementEffects } from './movement-effetcs'; -export interface TableRotateProps extends BaseRotateProps {} +export interface TableRotateProps extends BaseRotateProps, BaseLightsEffectProgressionProps {} export type TableRotateCreateParams = BaseLightsEffectCreateParams & { type: MovementEffects.TableRotate; @@ -19,7 +23,11 @@ export default class TableRotate extends BaseRotate { * @param props */ constructor(lightsGroup: LightsGroup, props: TableRotateProps) { - super(lightsGroup, { cycleTime: DEFAULT_CYCLE_TIME, offsetFactor: DEFAULT_OFFSET_FACTOR }); + super( + lightsGroup, + { cycleTime: DEFAULT_CYCLE_TIME, offsetFactor: DEFAULT_OFFSET_FACTOR }, + { pattern: props.pattern, direction: props.direction }, + ); this.props = props; } diff --git a/src/modules/lights/effects/progression-strategies/effect-progression-beat-strategy.ts b/src/modules/lights/effects/progression-strategies/effect-progression-beat-strategy.ts new file mode 100644 index 0000000..3c313ef --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/effect-progression-beat-strategy.ts @@ -0,0 +1,22 @@ +import { BeatEvent } from 'src/modules/events/music-emitter-events'; +import EffectProgressionStrategy from './effect-progression-strategy'; + +export default class EffectProgressionBeatStrategy extends EffectProgressionStrategy { + private phase = 0; + + /** + * @param nrSteps Number of steps in the effect (integer) + */ + constructor(private nrSteps: number) { + super(); + } + + getProgression(currentTick: Date): number { + return this.phase / this.nrSteps; + } + tick(): void {} + + beat(event: BeatEvent): void { + this.phase = (this.phase + 1) % this.nrSteps; + } +} diff --git a/src/modules/lights/effects/progression-strategies/effect-progression-strategy.ts b/src/modules/lights/effects/progression-strategies/effect-progression-strategy.ts new file mode 100644 index 0000000..4303d36 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/effect-progression-strategy.ts @@ -0,0 +1,11 @@ +import { LightsGroup } from '../../entities'; +import { BeatEvent } from '../../../events/music-emitter-events'; + +export default abstract class EffectProgressionStrategy { + /** + * @returns The progression of the effect, as a float in the range [0, 1] + */ + abstract getProgression(currentTick: Date): number; + abstract tick(): void; + abstract beat(event: BeatEvent): void; +} diff --git a/src/modules/lights/effects/progression-strategies/effect-progression-tick-strategy.ts b/src/modules/lights/effects/progression-strategies/effect-progression-tick-strategy.ts new file mode 100644 index 0000000..ca1a535 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/effect-progression-tick-strategy.ts @@ -0,0 +1,27 @@ +import { BeatEvent } from 'src/modules/events/music-emitter-events'; +import { LightsGroup } from '../../entities'; +import EffectProgressionStrategy from './effect-progression-strategy'; + +export default class EffectProgressionTickStrategy extends EffectProgressionStrategy { + private cycleStartTick: Date = new Date(); + + constructor( + private cycleTime: number, + private singleCycle = false, + ) { + super(); + } + + getProgression(currentTick: Date): number { + return Math.min(1, (currentTick.getTime() - this.cycleStartTick.getTime()) / this.cycleTime); + } + + tick(): void { + const currentTick = new Date(); + const progression = this.getProgression(currentTick); + if (progression >= 1 && !this.singleCycle) { + this.cycleStartTick = currentTick; + } + } + beat(event: BeatEvent): void {} +} diff --git a/src/modules/lights/effects/progression-strategies/index.ts b/src/modules/lights/effects/progression-strategies/index.ts new file mode 100644 index 0000000..2d56607 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/index.ts @@ -0,0 +1,2 @@ +export { default as EffectProgressionTickStrategy } from './effect-progression-tick-strategy'; +export { default as EffectProgressionBeatStrategy } from './effect-progression-beat-strategy'; diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-circular-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-circular-strategy.ts new file mode 100644 index 0000000..1ff232a --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-circular-strategy.ts @@ -0,0 +1,23 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapCenteredCircularStrategy extends EffectProgressionMapStrategy { + getNrFixtures(): number { + const { x: centerX, y: centerY } = this.getCenter(); + + const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2); + + return Math.ceil(maxDistance); + } + + getProgression(progression: number, fixture: LightsGroupFixture): number { + const { x: centerX, y: centerY } = this.getCenter(); + + const distanceX = fixture.positionX - centerX; + const distanceY = fixture.positionY - centerY; + const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2); + const relativePosition = Math.sqrt(distanceX ** 2 + distanceY ** 2) / maxDistance; + + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-squared-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-squared-strategy.ts new file mode 100644 index 0000000..9afcf7b --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-centered-squared-strategy.ts @@ -0,0 +1,18 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapCenteredSquaredStrategy extends EffectProgressionMapStrategy { + public getNrFixtures(): number { + return (this.lightsGroup.gridSizeX + this.lightsGroup.gridSizeY) / 2; + } + + public getProgression(progression: number, fixture: LightsGroupFixture): number { + const { x: centerX, y: centerY } = this.getCenter(); + + const distanceX = Math.abs(fixture.positionX - centerX); + const distanceY = Math.abs(fixture.positionY - centerY); + const relativePosition = (distanceX + distanceY) / this.getNrFixtures(); + + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-bottom-left-top-right-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-bottom-left-top-right-strategy.ts new file mode 100644 index 0000000..2802d8c --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-bottom-left-top-right-strategy.ts @@ -0,0 +1,13 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapDiagonalBottomLeftTopRightStrategy extends EffectProgressionMapStrategy { + public getNrFixtures(): number { + return this.lightsGroup.gridSizeX + this.lightsGroup.gridSizeY; + } + + public getProgression(progression: number, fixture: LightsGroupFixture): number { + const relativePosition = (fixture.positionX - fixture.positionY) / this.getNrFixtures(); + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-top-left-bottom-right-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-top-left-bottom-right-strategy.ts new file mode 100644 index 0000000..82332df --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-diagonal-top-left-bottom-right-strategy.ts @@ -0,0 +1,13 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapDiagonalTopLeftBottomRightStrategy extends EffectProgressionMapStrategy { + public getNrFixtures(): number { + return this.lightsGroup.gridSizeX + this.lightsGroup.gridSizeY; + } + + public getProgression(progression: number, fixture: LightsGroupFixture): number { + const relativePosition = (fixture.positionX + fixture.positionY) / this.getNrFixtures(); + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-factory.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-factory.ts new file mode 100644 index 0000000..1488d70 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-factory.ts @@ -0,0 +1,42 @@ +import { LightsEffectPattern } from '../../lights-effect-pattern'; +import EffectProgressionMapHorizontalStrategy from './effect-progression-map-horizontal-strategy'; +import { LightsGroup } from '../../../entities'; +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import EffectProgressionMapVerticalStrategy from './effect-progression-map-vertical-strategy'; +import EffectProgressionMapDiagonalBottomLeftTopRightStrategy from './effect-progression-map-diagonal-bottom-left-top-right-strategy'; +import EffectProgressionMapDiagonalTopLeftBottomRightStrategy from './effect-progression-map-diagonal-top-left-bottom-right-strategy'; +import EffectProgressionMapCenteredSquaredStrategy from './effect-progression-map-centered-squared-strategy'; +import EffectProgressionMapCenteredCircularStrategy from './effect-progression-map-centered-circular-strategy'; +import EffectProgressionMapRotationalStrategy from './effect-progression-map-rotational-strategy'; + +export default class EffectProgressionMapFactory { + constructor(private lightsGroup: LightsGroup) {} + + public getMapper( + pattern: LightsEffectPattern = LightsEffectPattern.HORIZONTAL, + multiplier?: number, + ): EffectProgressionMapStrategy { + switch (pattern) { + case LightsEffectPattern.HORIZONTAL: + return new EffectProgressionMapHorizontalStrategy(this.lightsGroup, multiplier); + case LightsEffectPattern.VERTICAL: + return new EffectProgressionMapVerticalStrategy(this.lightsGroup, multiplier); + case LightsEffectPattern.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT: + return new EffectProgressionMapDiagonalBottomLeftTopRightStrategy( + this.lightsGroup, + multiplier, + ); + case LightsEffectPattern.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT: + return new EffectProgressionMapDiagonalTopLeftBottomRightStrategy( + this.lightsGroup, + multiplier, + ); + case LightsEffectPattern.CENTERED_SQUARED: + return new EffectProgressionMapCenteredSquaredStrategy(this.lightsGroup, multiplier); + case LightsEffectPattern.CENTERED_CIRCULAR: + return new EffectProgressionMapCenteredCircularStrategy(this.lightsGroup, multiplier); + case LightsEffectPattern.ROTATIONAL: + return new EffectProgressionMapRotationalStrategy(this.lightsGroup, multiplier); + } + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-horizontal-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-horizontal-strategy.ts new file mode 100644 index 0000000..3e26149 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-horizontal-strategy.ts @@ -0,0 +1,13 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapHorizontalStrategy extends EffectProgressionMapStrategy { + public getNrFixtures(): number { + return this.lightsGroup.gridSizeX * this.multiplier; + } + + public getProgression(progression: number, fixture: LightsGroupFixture): number { + const relativePosition = fixture.positionX / this.getNrFixtures(); + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-rotational-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-rotational-strategy.ts new file mode 100644 index 0000000..f4af70a --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-rotational-strategy.ts @@ -0,0 +1,19 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapRotationalStrategy extends EffectProgressionMapStrategy { + public getNrFixtures(): number { + return this.lightsGroup.gridSizeX * 2 + this.lightsGroup.gridSizeY * 2; + } + + public getProgression(progression: number, fixture: LightsGroupFixture): number { + const { x: centerX, y: centerY } = this.getCenter(); + + const x = fixture.positionX - centerX; + const y = fixture.positionY - centerY; + const angle = Math.atan2(x, y) / Math.PI; + // Transform range [-1, 1] to [0, 1] + const relativePosition = (angle + 1) / 2; + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-strategy.ts new file mode 100644 index 0000000..f2ea526 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-strategy.ts @@ -0,0 +1,20 @@ +import { LightsGroup } from '../../../entities'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default abstract class EffectProgressionMapStrategy { + constructor( + protected lightsGroup: LightsGroup, + protected multiplier = 1, + ) {} + + protected getCenter(): { x: number; y: number } { + return { + x: (this.lightsGroup.gridSizeX - 1) / 2, + y: (this.lightsGroup.gridSizeY - 1) / 2, + }; + } + + public abstract getNrFixtures(): number; + + public abstract getProgression(progression: number, fixture: LightsGroupFixture): number; +} diff --git a/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-vertical-strategy.ts b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-vertical-strategy.ts new file mode 100644 index 0000000..b612332 --- /dev/null +++ b/src/modules/lights/effects/progression-strategies/mappers/effect-progression-map-vertical-strategy.ts @@ -0,0 +1,13 @@ +import EffectProgressionMapStrategy from './effect-progression-map-strategy'; +import LightsGroupFixture from '../../../entities/lights-group-fixture'; + +export default class EffectProgressionMapVerticalStrategy extends EffectProgressionMapStrategy { + getNrFixtures(): number { + return this.lightsGroup.gridSizeY * this.multiplier; + } + + getProgression(progression: number, fixture: LightsGroupFixture): number { + const relativePosition = fixture.positionY / this.getNrFixtures(); + return (relativePosition + progression) % 1; + } +} diff --git a/src/modules/lights/entities/lights-group-fixture.ts b/src/modules/lights/entities/lights-group-fixture.ts new file mode 100644 index 0000000..5cca359 --- /dev/null +++ b/src/modules/lights/entities/lights-group-fixture.ts @@ -0,0 +1,17 @@ +import BaseEntity from '../../root/entities/base-entity'; +import { Column } from 'typeorm'; + +export default abstract class LightsGroupFixture extends BaseEntity { + @Column({ type: 'real', unsigned: true, nullable: false }) + public positionX: number; + + @Column({ type: 'real', unsigned: true, nullable: false, default: 0 }) + public positionY: number; + + @Column({ type: 'smallint', unsigned: true }) + public firstChannel: number; + + public getActualChannel(relativeChannel: number) { + return relativeChannel + this.firstChannel - 1; + } +} diff --git a/src/modules/lights/entities/lights-group-moving-head-rgbs.ts b/src/modules/lights/entities/lights-group-moving-head-rgbs.ts index b7d6c42..48272c3 100644 --- a/src/modules/lights/entities/lights-group-moving-head-rgbs.ts +++ b/src/modules/lights/entities/lights-group-moving-head-rgbs.ts @@ -3,9 +3,10 @@ import BaseEntity from '../../root/entities/base-entity'; // eslint-disable-next-line import/no-cycle import LightsGroup from './lights-group'; import LightsMovingHeadRgb from './lights-moving-head-rgb'; +import LightsGroupFixture from './lights-group-fixture'; @Entity() -export default class LightsGroupMovingHeadRgbs extends BaseEntity { +export default class LightsGroupMovingHeadRgbs extends LightsGroupFixture { @ManyToOne(() => LightsGroup) @JoinColumn() public group: LightsGroup; @@ -13,11 +14,4 @@ export default class LightsGroupMovingHeadRgbs extends BaseEntity { @ManyToOne(() => LightsMovingHeadRgb, { eager: true }) @JoinColumn() public fixture: LightsMovingHeadRgb; - - @Column({ type: 'smallint', unsigned: true }) - public firstChannel: number; - - public getActualChannel(relativeChannel: number) { - return relativeChannel + this.firstChannel - 1; - } } diff --git a/src/modules/lights/entities/lights-group-moving-head-wheels.ts b/src/modules/lights/entities/lights-group-moving-head-wheels.ts index c68beca..042b2d2 100644 --- a/src/modules/lights/entities/lights-group-moving-head-wheels.ts +++ b/src/modules/lights/entities/lights-group-moving-head-wheels.ts @@ -1,11 +1,11 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; -import BaseEntity from '../../root/entities/base-entity'; // eslint-disable-next-line import/no-cycle import LightsGroup from './lights-group'; import LightsMovingHeadWheel from './lights-moving-head-wheel'; +import LightsGroupFixture from './lights-group-fixture'; @Entity() -export default class LightsGroupMovingHeadWheels extends BaseEntity { +export default class LightsGroupMovingHeadWheels extends LightsGroupFixture { @ManyToOne(() => LightsGroup) @JoinColumn() public group: LightsGroup; @@ -13,11 +13,4 @@ export default class LightsGroupMovingHeadWheels extends BaseEntity { @ManyToOne(() => LightsMovingHeadWheel, { eager: true }) @JoinColumn() public fixture: LightsMovingHeadWheel; - - @Column({ type: 'smallint', unsigned: true }) - public firstChannel: number; - - public getActualChannel(relativeChannel: number) { - return relativeChannel + this.firstChannel - 1; - } } diff --git a/src/modules/lights/entities/lights-group-pars.ts b/src/modules/lights/entities/lights-group-pars.ts index 7128153..129a2ae 100644 --- a/src/modules/lights/entities/lights-group-pars.ts +++ b/src/modules/lights/entities/lights-group-pars.ts @@ -1,11 +1,11 @@ -import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; -import BaseEntity from '../../root/entities/base-entity'; +import { Entity, JoinColumn, ManyToOne } from 'typeorm'; // eslint-disable-next-line import/no-cycle import LightsGroup from './lights-group'; import LightsPar from './lights-par'; +import LightsGroupFixture from './lights-group-fixture'; @Entity() -export default class LightsGroupPars extends BaseEntity { +export default class LightsGroupPars extends LightsGroupFixture { @ManyToOne(() => LightsGroup, (group) => group.pars) @JoinColumn() public group: LightsGroup; @@ -13,11 +13,4 @@ export default class LightsGroupPars extends BaseEntity { @ManyToOne(() => LightsPar, { eager: true }) @JoinColumn() public fixture: LightsPar; - - @Column({ type: 'smallint', unsigned: true }) - public firstChannel: number; - - public getActualChannel(relativeChannel: number) { - return relativeChannel + this.firstChannel - 1; - } } diff --git a/src/modules/lights/entities/lights-group.ts b/src/modules/lights/entities/lights-group.ts index 2fb3276..e3a3414 100644 --- a/src/modules/lights/entities/lights-group.ts +++ b/src/modules/lights/entities/lights-group.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; // eslint-disable-next-line import/no-cycle import { LightsController } from '../../root/entities'; // eslint-disable-next-line import/no-cycle @@ -24,6 +24,20 @@ export default class LightsGroup extends SubscribeEntity { @OneToMany(() => LightsGroupMovingHeadRgbs, (pars) => pars.group, { eager: true }) public movingHeadRgbs: LightsGroupMovingHeadRgbs[]; + /** + * Size (width) of the X axis where all the fixtures are positioned. + * All fixtures should have their positionX be in range [0, gridSizeX). + */ + @Column({ type: 'real', unsigned: true, nullable: false }) + public gridSizeX: number; + + /** + * Size (width) of the Y axis where all the fixtures are positioned. + * 0 if the lights are positioned in a line (and not in a grid) + */ + @Column({ type: 'real', unsigned: true, nullable: false, default: 0 }) + public gridSizeY: number; + public get fixtures(): ( | LightsGroupPars | LightsGroupMovingHeadWheels diff --git a/src/modules/modes/centurion/centurion-mode.ts b/src/modules/modes/centurion/centurion-mode.ts index bb3f6fb..ee40f99 100644 --- a/src/modules/modes/centurion/centurion-mode.ts +++ b/src/modules/modes/centurion/centurion-mode.ts @@ -190,7 +190,7 @@ export default class CenturionMode extends BaseMode< probability: 0.1, }, { - effect: Wave.build({ color: colors[0] }), + effect: Wave.build({ colors: colors }), probability: 0.1, }, ]; diff --git a/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion-2-original.ts b/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion-2-original.ts index 5b325a4..feda03c 100644 --- a/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion-2-original.ts +++ b/src/modules/modes/centurion/tapes/gebroeders-scooter-centurion-2-original.ts @@ -63,7 +63,7 @@ const centurion2Original: MixTape = { type: 'effect', data: { effects: { - pars: [Wave.build({ color: RgbColor.GOLD, singleWave: true, cycleTime: 1000 })], + pars: [Wave.build({ colors: [RgbColor.GOLD], singleWave: true, cycleTime: 1000 })], }, }, }), diff --git a/src/modules/root/lights-controller-manager.ts b/src/modules/root/lights-controller-manager.ts index 2a6d34c..514865c 100644 --- a/src/modules/root/lights-controller-manager.ts +++ b/src/modules/root/lights-controller-manager.ts @@ -115,7 +115,7 @@ export default class LightsControllerManager { /** * Given a fixture, put the new DMX values in the correct spot in the packet * @param p - * @param packet Array of 512 integers [0, 255] + * @param packet Array of at least 512 integers in the range [0, 255] * @private */ private calculateNewDmxValues( @@ -123,7 +123,11 @@ export default class LightsControllerManager { packet: number[], ) { const dmxValues = p.fixture.toDmx(); - packet.splice(p.firstChannel - 1, dmxValues.length, ...dmxValues); + + for (let i = 0; i < dmxValues.length; i++) { + packet[p.firstChannel - 1 + i] = dmxValues[i]; + } + return packet; } diff --git a/src/modules/root/root-lights-service.ts b/src/modules/root/root-lights-service.ts index a35c0ec..0e3d8fa 100644 --- a/src/modules/root/root-lights-service.ts +++ b/src/modules/root/root-lights-service.ts @@ -76,6 +76,8 @@ export interface FixtureInGroupResponse< fixture: T; id: number; firstChannel: number; + positionX: number; + positionY: number; } export interface BaseLightsGroupResponse @@ -83,6 +85,8 @@ export interface BaseLightsGroupResponse export interface LightsGroupResponse extends BaseLightsGroupResponse { controller: LightsControllerResponse; + gridSizeX: number; + gridSizeY: number; pars: FixtureInGroupResponse[]; movingHeadRgbs: FixtureInGroupResponse[]; movingHeadWheels: FixtureInGroupResponse[]; @@ -140,10 +144,43 @@ export interface LightsMovingHeadWheelCreateParams extends LightsMovingHeadParam export interface LightsInGroup { fixtureId: number; + /** + * @isInt + * @minimum 0 + */ firstChannel: number; + /** + * Position of the fixture within the group's grid/line + * @isFloat + * @minimum 0 + */ + positionX: number; + /** + * Position of the fixture within the group's grid. + * Should be undefined if the group is a line of fixtures + * (and not a grid). + * @isFloat + * @minimum 0 + */ + positionY?: number; } export interface LightsGroupCreateParams extends Pick { + /** + * Size (width) of the X axis where all the fixtures are positioned. + * All fixtures should have their positionX be in range [0, gridSizeX). + * @isFloat + * @minimum 0 + */ + gridSizeX: number; + + /** + * Size (width) of the Y axis where all the fixtures are positioned. + * 0 if the lights are positioned in a line (and not in a grid) + * @isFloat + * @minimum 0 + */ + gridSizeY?: number; pars: LightsInGroup[]; movingHeadRgbs: LightsInGroup[]; movingHeadWheels: LightsInGroup[]; @@ -266,21 +303,29 @@ export default class RootLightsService { createdAt: g.createdAt, updatedAt: g.updatedAt, name: g.name, + gridSizeX: g.gridSizeX, + gridSizeY: g.gridSizeY, controller: this.toLightsControllerResponse(g.controller), pars: g.pars.map((p) => ({ fixture: this.toParResponse(p.fixture, p.firstChannel), id: p.id, firstChannel: p.firstChannel, + positionX: p.positionX, + positionY: p.positionY, })), movingHeadRgbs: g.movingHeadRgbs.map((m) => ({ fixture: this.toMovingHeadRgbResponse(m.fixture, m.firstChannel), id: m.id, firstChannel: m.firstChannel, + positionX: m.positionX, + positionY: m.positionY, })), movingHeadWheels: g.movingHeadWheels.map((m) => ({ fixture: this.toMovingHeadWheelResponse(m.fixture, m.firstChannel), id: m.id, firstChannel: m.firstChannel, + positionX: m.positionX, + positionY: m.positionY, })), }; } @@ -331,6 +376,8 @@ export default class RootLightsService { name: params.name, defaultHandler: params.defaultHandler, controller, + gridSizeX: params.gridSizeX, + gridSizeY: params.gridSizeY, })) as LightsGroup; await Promise.all( @@ -341,6 +388,8 @@ export default class RootLightsService { group, fixture: par, firstChannel: p.firstChannel, + positionX: p.positionX, + positionY: p.positionY, }); }), ); @@ -356,6 +405,8 @@ export default class RootLightsService { group, fixture: movingHead, firstChannel: p.firstChannel, + positionX: p.positionX, + positionY: p.positionY, }); }), ); @@ -371,6 +422,8 @@ export default class RootLightsService { group, fixture: movingHead, firstChannel: p.firstChannel, + positionX: p.positionX, + positionY: p.positionY, }); }), ); diff --git a/src/seed/index.ts b/src/seed/index.ts index 9598c48..1cc252d 100644 --- a/src/seed/index.ts +++ b/src/seed/index.ts @@ -1,6 +1,6 @@ import '../env'; import dataSource from '../database'; -import seedDatabase, { seedBorrelLights, seedOpeningSequence } from './seed'; +import seedDatabase, { seedBorrelLights, seedDiscoFloor, seedOpeningSequence } from './seed'; import logger from '../logger'; async function createSeeder() { @@ -11,6 +11,7 @@ async function createSeeder() { const [room, bar, lounge, movingHeadsGEWIS, movingHeadsRoy] = await seedDatabase(); await seedBorrelLights(room!, bar!, lounge!, movingHeadsGEWIS!); await seedOpeningSequence(room!, bar!, movingHeadsGEWIS!, movingHeadsRoy!); + await seedDiscoFloor(12, 8); } if (require.main === module) { diff --git a/src/seed/seed.ts b/src/seed/seed.ts index 4117156..46e0792 100644 --- a/src/seed/seed.ts +++ b/src/seed/seed.ts @@ -1,8 +1,8 @@ import RootAudioService from '../modules/root/root-audio-service'; import RootScreenService from '../modules/root/root-screen-service'; -import RootLightsService from '../modules/root/root-lights-service'; +import RootLightsService, { LightsInGroup } from '../modules/root/root-lights-service'; import dataSource from '../database'; -import { LightsGroup, LightsMovingHeadWheel } from '../modules/lights/entities'; +import { LightsGroup, LightsMovingHeadWheel, LightsPar } from '../modules/lights/entities'; import { RgbColor, WheelColor } from '../modules/lights/color-definitions'; import { SparkleCreateParams } from '../modules/lights/effects/color/sparkle'; import { StaticColorCreateParams } from '../modules/lights/effects/color/static-color'; @@ -144,19 +144,21 @@ export default async function seedDatabase() { const gewisRoom = await rootLightsService.createLightGroup(controller.id, { name: 'Ruimte', defaultHandler: '', + gridSizeX: 12, + gridSizeY: 0, pars: [ - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 1 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 17 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 33 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 49 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 65 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 81 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 193 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 209 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 225 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 241 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 257 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 273 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 1, positionX: 0 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 17, positionX: 1 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 33, positionX: 2 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 49, positionX: 3 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 65, positionX: 4 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 81, positionX: 5 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 193, positionX: 6 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 209, positionX: 7 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 225, positionX: 8 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 241, positionX: 9 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 257, positionX: 10 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 273, positionX: 11 }, ], movingHeadRgbs: [], movingHeadWheels: [], @@ -164,11 +166,13 @@ export default async function seedDatabase() { const gewisBar = await rootLightsService.createLightGroup(controller.id, { name: 'Bar', defaultHandler: '', + gridSizeX: 4, + gridSizeY: 0, pars: [ - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 97 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 113 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 289 }, - { fixtureId: eurolite_LED_7C_7.id, firstChannel: 305 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 97, positionX: 0 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 113, positionX: 1 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 289, positionX: 2 }, + { fixtureId: eurolite_LED_7C_7.id, firstChannel: 305, positionX: 3 }, ], movingHeadRgbs: [], movingHeadWheels: [], @@ -176,28 +180,34 @@ export default async function seedDatabase() { const gewisLounge = await rootLightsService.createLightGroup(controller.id, { name: 'Lounge', defaultHandler: '', - pars: [{ fixtureId: eurolite_LED_7C_7.id, firstChannel: 129 }], + gridSizeX: 1, + gridSizeY: 0, + pars: [{ fixtureId: eurolite_LED_7C_7.id, firstChannel: 129, positionX: 0 }], movingHeadRgbs: [], movingHeadWheels: [], }); const gewisMHRoom = await rootLightsService.createLightGroup(controller.id, { name: 'Ruimte MH', defaultHandler: '', + gridSizeX: 2, + gridSizeY: 0, pars: [], movingHeadRgbs: [], movingHeadWheels: [ - { fixtureId: eurolite_LED_TMH_S30.id, firstChannel: 161 }, - { fixtureId: eurolite_LED_TMH_S30.id, firstChannel: 177 }, + { fixtureId: eurolite_LED_TMH_S30.id, firstChannel: 161, positionX: 0 }, + { fixtureId: eurolite_LED_TMH_S30.id, firstChannel: 177, positionX: 1 }, ], }); if (!gewisMHRoom) throw new Error('GEWIS MHs not created'); const royMHs = await rootLightsService.createLightGroup(controller.id, { name: 'Roy MH', defaultHandler: '', + gridSizeX: 2, + gridSizeY: 0, pars: [], movingHeadRgbs: [ - { fixtureId: ayra_ERO_506.id, firstChannel: 353 }, - { fixtureId: ayra_ERO_506.id, firstChannel: 369 }, + { fixtureId: ayra_ERO_506.id, firstChannel: 353, positionX: 0 }, + { fixtureId: ayra_ERO_506.id, firstChannel: 369, positionX: 1 }, ], movingHeadWheels: [], }); @@ -563,21 +573,21 @@ export async function seedOpeningSequence( await addStep(movingHeadsWhite, 28000, 32000, [movingHeadsGEWIS]); await addStep(allOfTheLights, 27500, 1500, [room, bar]); await addStep( - { type: 'Wave', props: { color: RgbColor.ROSERED } } as WaveCreateParams, + { type: 'Wave', props: { colors: [RgbColor.ROSERED] } } as WaveCreateParams, 29000, 4000, spots, ); await addStep(allOfTheLights, 33000, 2000, [room, bar]); await addStep( - { type: 'Wave', props: { color: RgbColor.ROSERED } } as WaveCreateParams, + { type: 'Wave', props: { colors: [RgbColor.ROSERED] } } as WaveCreateParams, 35000, 5000, spots, ); await addStep(allOfTheLights, 40000, 1500, [room, bar]); await addStep( - { type: 'Wave', props: { color: RgbColor.ROSERED } } as WaveCreateParams, + { type: 'Wave', props: { colors: [RgbColor.ROSERED] } } as WaveCreateParams, 41500, 13500, spots, @@ -606,9 +616,19 @@ export async function seedOpeningSequence( await addStep(movingHeadsWhite, 63000, 155000, [movingHeadsGEWIS]); await addStep(allOfTheLights, 81500, 2000, spots); - await addStep({ type: ColorEffects.Wave, props: { color: RgbColor.PINK } }, 83500, 4500, spots); + await addStep( + { type: ColorEffects.Wave, props: { colors: [RgbColor.PINK] } }, + 83500, + 4500, + spots, + ); await addStep(allOfTheLights, 88000, 2000, spots); - await addStep({ type: ColorEffects.Wave, props: { color: RgbColor.PINK } }, 90000, 4800, spots); + await addStep( + { type: ColorEffects.Wave, props: { colors: [RgbColor.PINK] } }, + 90000, + 4800, + spots, + ); await addStep(allOfTheLights, 94800, 1600, spots); await addStep( { type: ColorEffects.Sparkle, props: { colors: [RgbColor.GREEN, RgbColor.BROWN] } }, @@ -630,14 +650,14 @@ export async function seedOpeningSequence( // Refrein await addStep(allOfTheLights, 135800, 1200, spots); await addStep( - { type: ColorEffects.Wave, props: { color: RgbColor.YELLOW } }, + { type: ColorEffects.Wave, props: { colors: [RgbColor.YELLOW] } }, 138000, 4000, spots, ); await addStep(allOfTheLights, 142000, 2000, spots); await addStep( - { type: ColorEffects.Wave, props: { color: RgbColor.YELLOW } }, + { type: ColorEffects.Wave, props: { colors: [RgbColor.YELLOW] } }, 144000, 4000, spots, @@ -666,7 +686,7 @@ export async function seedOpeningSequence( spots, ); await addStep( - { type: ColorEffects.Wave, props: { color: RgbColor.YELLOW, cycleTime: 425 } }, + { type: ColorEffects.Wave, props: { colors: [RgbColor.YELLOW], cycleTime: 425 } }, 190500, 27500, spots, @@ -719,3 +739,42 @@ export async function seedOpeningSequence( await addStep(borrelRuimte, 285000, 15000, [room]); await addStep(borrelBar, 285000, 15000, [bar]); } + +export async function seedDiscoFloor(width: number, height: number) { + const service = await new RootLightsService(); + const controller = await service.createController({ name: 'GEWIS-DISCO-FLOOR' }); + const fixture = await service.createLightsPar({ + name: 'Disco floor panel', + colorRedChannel: 1, + colorGreenChannel: 2, + colorBlueChannel: 3, + masterDimChannel: 4, + shutterChannel: 5, + shutterOptionValues: { + open: 0, + strobe: 220, + }, + }); + + const pars: LightsInGroup[] = []; + for (let positionY = 0; positionY < height; positionY++) { + for (let positionX = 0; positionX < width; positionX++) { + pars.push({ + fixtureId: fixture.id, + firstChannel: pars.length * 16 + 1, + positionX, + positionY, + }); + } + } + + return await service.createLightGroup(controller.id, { + name: 'Disco floor', + defaultHandler: '', + gridSizeX: width, + gridSizeY: height, + pars, + movingHeadWheels: [], + movingHeadRgbs: [], + }); +}