Skip to content

Commit

Permalink
fix(lights): several small fixes and improvements (#47)
Browse files Browse the repository at this point in the history
* fix(moving-heads): not working synchronous in a line

* fix(moving-heads): light always on

* feat(lights-effects): use hex colors if pars have only RGB LEDs

* fix(lights-effects): clean up when effect is destroyed
  • Loading branch information
Yoronex committed Feb 5, 2025
1 parent 5687ed0 commit 3189a61
Show file tree
Hide file tree
Showing 25 changed files with 285 additions and 102 deletions.
17 changes: 15 additions & 2 deletions src/modules/lights/color-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ export const wheelColors = Object.values(WheelColor);

export const rgbColors = Object.values(RgbColor);

export type HexColor = string;

export type RgbColorSpecification = {
definition: Required<IColorsRgb>;
alternative: WheelColor;
complementary: RgbColor[];
hex: string;
hex: HexColor;
};

export type RgbColorSet = {
Expand All @@ -50,6 +52,17 @@ export type RgbColorAlternatives = {
[color in RgbColor]: WheelColor;
};

export function hexToRgb(hex: HexColor): { r: number; g: number; b: number } {
const parts = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return parts
? {
r: parseInt(parts[1], 16),
g: parseInt(parts[2], 16),
b: parseInt(parts[3], 16),
}
: { r: 0, g: 0, b: 0 };
}

export const rgbColorDefinitions: RgbColorSet = {
[RgbColor.WHITE]: {
definition: {
Expand All @@ -63,7 +76,7 @@ export const rgbColorDefinitions: RgbColorSet = {
},
alternative: WheelColor.WHITE,
complementary: [],
hex: '#fff',
hex: '#ffffff',
},
[RgbColor.RED]: {
definition: {
Expand Down
103 changes: 84 additions & 19 deletions src/modules/lights/effects/color/background-pulse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import LightsEffect, {
LightsEffectBuilder,
} from '../lights-effect';
import { LightsGroup } from '../../entities';
import { RgbColor, rgbColorDefinitions } from '../../color-definitions';
import {
hexToRgb,
RgbColor,
rgbColorDefinitions,
RgbColorSpecification,
} from '../../color-definitions';
import { EffectProgressionTickStrategy } from '../progression-strategies';
import { IColorsRgb } from '../../entities/colors-rgb';
import { ColorEffects } from './color-effects';
Expand Down Expand Up @@ -71,7 +76,12 @@ export default class BackgroundPulse extends LightsEffect<BackgroundPulseProps>
this.props.colors = colors;
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.setBrightness(1);
f.fixture.resetColor();
});
}

/**
* Whether we should start a new tick
Expand Down Expand Up @@ -129,31 +139,82 @@ export default class BackgroundPulse extends LightsEffect<BackgroundPulseProps>
return (1 - fixtureAbsoluteProgression) * 2;
}

private rgbToIColorsRgb({ r, g, b }: { r: number; b: number; g: number }): Required<IColorsRgb> {
return {
redChannel: r,
greenChannel: g,
blueChannel: b,
coldWhiteChannel: 0,
warmWhiteChannel: 0,
amberChannel: 0,
uvChannel: 0,
};
}

/**
* Mix two colors together
* @param colorA
* @param colorB
* @param p factor B present in the new color
* @param rgbOnly Whether the output should be an RGB-only color
* @private
*/
private mixColors(
colorA: RgbColorSpecification,
colorB: RgbColorSpecification,
p: number,
rgbOnly: boolean,
): Required<IColorsRgb> {
if (!rgbOnly) {
return {
redChannel: colorA.definition.redChannel * (1 - p) + colorB.definition.redChannel * p,
greenChannel: colorA.definition.greenChannel * (1 - p) + colorB.definition.greenChannel * p,
blueChannel: colorA.definition.blueChannel * (1 - p) + colorB.definition.blueChannel * p,
warmWhiteChannel:
colorA.definition.warmWhiteChannel! * (1 - p) + colorB.definition.warmWhiteChannel! * p,
coldWhiteChannel:
colorA.definition.coldWhiteChannel! * (1 - p) + colorB.definition.coldWhiteChannel! * p,
amberChannel:
colorA.definition.amberChannel! * (1 - p) + colorB.definition.amberChannel! * p,
uvChannel: colorA.definition.uvChannel! * (1 - p) + colorB.definition.uvChannel! * p,
};
}

const rgbA = hexToRgb(colorA.hex);
const rgbB = hexToRgb(colorB.hex);

return this.rgbToIColorsRgb({
r: rgbA.r * (1 - p) + rgbB.r * p,
g: rgbA.g * (1 - p) + rgbB.g * p,
b: rgbA.b * (1 - p) + rgbB.b * p,
});
}

/**
* Get the mixed color for the given progression and the given color index
* @param p progression, in range [0, 1]
* @param colorIndex
* @param rgbOnly Whether only an RGB color should be returned, instead of an extensive palette
* @private
*/
private getColor(p: number, colorIndex: number): Required<IColorsRgb> | undefined {
const baseColor = rgbColorDefinitions[this.props.colors[0]]?.definition;
if (p <= 0 || this.props.colors.length < 2) {
return baseColor;
private getColor(
p: number,
colorIndex: number,
rgbOnly: boolean,
): Required<IColorsRgb> | undefined {
const baseColor = rgbColorDefinitions[this.props.colors[0]];
if (!baseColor) return undefined;

let compositeColor: RgbColorSpecification | undefined;
if (this.props.colors.length >= 2) {
compositeColor = rgbColorDefinitions[this.props.colors[colorIndex]];
}

const compositeColor = rgbColorDefinitions[this.props.colors[colorIndex]]?.definition;
if (p <= 0 || !compositeColor) {
return rgbOnly ? this.rgbToIColorsRgb(hexToRgb(baseColor.hex)) : baseColor.definition;
}

return {
redChannel: baseColor.redChannel * (1 - p) + compositeColor.redChannel * p,
greenChannel: baseColor.greenChannel * (1 - p) + compositeColor.greenChannel * p,
blueChannel: baseColor.blueChannel * (1 - p) + compositeColor.blueChannel * p,
warmWhiteChannel:
baseColor.warmWhiteChannel! * (1 - p) + compositeColor.warmWhiteChannel! * p,
coldWhiteChannel:
baseColor.coldWhiteChannel! * (1 - p) + compositeColor.coldWhiteChannel! * p,
amberChannel: baseColor.amberChannel! * (1 - p) + compositeColor.amberChannel! * p,
uvChannel: baseColor.uvChannel! * (1 - p) + compositeColor.uvChannel! * p,
};
return this.mixColors(baseColor, compositeColor, p, rgbOnly);
}

tick(): LightsGroup {
Expand All @@ -171,7 +232,11 @@ export default class BackgroundPulse extends LightsEffect<BackgroundPulseProps>
p = this.getRelativeProgression(progressionStrategy.getProgression(tick));
}

const color = this.getColor(p, this.colorIndices[i] + 1);
const color = this.getColor(
p,
this.colorIndices[i] + 1,
!f.fixture.color.hasExtendedColorPalette(),
);
if (color) {
f.fixture.setCustomColor(color);
} else {
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/beat-fade-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ export default class BeatFadeOut extends LightsEffect<BeatFadeOutProps> {
this.props.colors = colors;
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

beat(event: BeatEvent): void {
super.beat(event);
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/fire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export default class Fire extends LightsEffect<FireProps> {

beat(): void {}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

tick(): LightsGroup {
[...this.lightsGroup.pars, ...this.lightsGroup.movingHeadRgbs].forEach((p) => {
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/random-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ export default class RandomColor extends LightsEffect<RandomColorProps> {
this.props.colors = colors;
}

destroy() {}
destroy() {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

beat(event: BeatEvent) {
super.beat(event);
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/single-flood.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export default class SingleFlood extends LightsEffect<SingleFloodProps> {
this.props = props;
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

beat(): void {}

Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/sparkle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ export default class Sparkle extends LightsEffect<SparkleProps> {
this.previousTick = new Date();
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

beat(): void {
if (!this.props.cycleTime) this.enableLights();
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/static-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ export default class StaticColor extends LightsEffect<StaticColorProps> {
this.ping = (this.ping + 1) % 2;
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

private getDimProgression(durationMs: number) {
return Math.min(1, (new Date().getTime() - this.cycleStartTick.getTime()) / durationMs);
Expand Down
6 changes: 5 additions & 1 deletion src/modules/lights/effects/color/wave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ export default class Wave extends LightsEffect<WaveProps> {
this.props.colors = colors;
}

destroy(): void {}
destroy(): void {
this.lightsGroup.fixtures.forEach((f) => {
f.fixture.resetColor();
});
}

beat(): void {}

Expand Down
26 changes: 12 additions & 14 deletions src/modules/lights/effects/lights-effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,24 @@ export type BaseLightsEffectCreateParams = {};
export default abstract class LightsEffect<P = {}> {
protected props: P;

private readonly progressionMapperStrategy: EffectProgressionMapStrategy;

protected constructor(
public readonly lightsGroup: LightsGroup,
private readonly progressionStrategy?: EffectProgressionStrategy,
progressionMapperStrategy?: EffectProgressionMapStrategy,
private readonly progressionMapperStrategy?: EffectProgressionMapStrategy,
private patternDirection = LightsEffectDirection.FORWARDS,
) {
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();
if (this.progressionMapperStrategy) return this.progressionMapperStrategy.getNrFixtures();
return (
this.lightsGroup.pars.length +
this.lightsGroup.movingHeadWheels.length +
this.lightsGroup.movingHeadRgbs.length
);
}

protected getProgression(currentTick: Date, fixture: LightsGroupFixture): number {
Expand All @@ -68,7 +63,10 @@ export default abstract class LightsEffect<P = {}> {
progression = 1 - progression;
}

return this.progressionMapperStrategy.getProgression(progression, fixture);
if (this.progressionMapperStrategy) {
return this.progressionMapperStrategy.getProgression(progression, fixture);
}
return this.progressionStrategy.getProgression(currentTick);
}

/**
Expand Down
12 changes: 6 additions & 6 deletions src/modules/lights/effects/movement/base-rotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,7 @@ export default abstract class BaseRotate<T extends BaseRotateProps> extends Ligh
progressionProps: BaseLightsEffectProgressionProps,
cycleTime?: number,
) {
super(
lightsGroup,
new EffectProgressionTickStrategy(cycleTime ?? defaults.cycleTime),
new EffectProgressionMapFactory(lightsGroup).getMapper(progressionProps.pattern),
);
super(lightsGroup, new EffectProgressionTickStrategy(cycleTime ?? defaults.cycleTime));
}

/**
Expand All @@ -50,7 +46,11 @@ export default abstract class BaseRotate<T extends BaseRotateProps> extends Ligh
offset: number,
): void;

destroy(): void {}
destroy(): void {
[...this.lightsGroup.movingHeadRgbs, ...this.lightsGroup.movingHeadWheels].forEach((f) => {
f.fixture.movement.reset();
});
}

beat(event: BeatEvent): void {
super.beat(event);
Expand Down
6 changes: 3 additions & 3 deletions src/modules/lights/effects/movement/classic-rotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export default class ClassicRotate extends BaseRotate<ClassicRotateProps> {
progression: number,
offset: number = 0,
) {
const pan = this.triangleFunction(progression + offset) * 128 + 128;
const tilt = this.triangleFunction(progression * 4 + offset) * 128 + 128;
movingHead.setPosition(pan, tilt);
const pan = this.triangleFunction(progression + offset) / 2 + 0.5;
const tilt = this.triangleFunction(progression * 4 + offset) / 2 + 0.5;
movingHead.setPositionRel(pan, tilt);
}
}
Loading

0 comments on commit 3189a61

Please sign in to comment.