diff --git a/boilerbloat/src/main/backend.ts b/boilerbloat/src/main/backend.ts index 3ce09b3..0b8fd5d 100644 --- a/boilerbloat/src/main/backend.ts +++ b/boilerbloat/src/main/backend.ts @@ -5,6 +5,7 @@ import { Pattern, PatternOutput, RenderUpdate } from '../patterns/proto'; import { TestPattern } from '../patterns/test'; import rust, { BeatTrackerHandle, MovingHeadState, OutputHandle, TrackerConfig } from 'rust_native_module'; import { ChaserPattern } from '../patterns/chaser'; +import { HSLRandomMovementPattern } from '../patterns/hslRandom'; export type AppState = { patterns: { [key: string]: PatternOutput }, @@ -61,6 +62,7 @@ class Backend { this.patterns = new Map(); this.patterns.set("test", new TestPattern()); this.patterns.set("chaser", new ChaserPattern()); + this.patterns.set("hslRandom", new HSLRandomMovementPattern()); this.state = { patterns: {}, diff --git a/boilerbloat/src/patterns/hslRandom.ts b/boilerbloat/src/patterns/hslRandom.ts new file mode 100644 index 0000000..5cbf0dd --- /dev/null +++ b/boilerbloat/src/patterns/hslRandom.ts @@ -0,0 +1,98 @@ + +import { panForward, panLeft, panRight, startAddresses, tiltDown, tiltUp } from './stage' +import { Pattern, PatternOutput, RenderUpdate } from './proto'; +import { MovingHeadState } from 'rust_native_module'; +import { Tuple4 } from './types'; +import { hsl2rgb, clamp, rescale } from './util'; + +const HSL_CYCLE_LENGTH: number = 7; // seconds +const MOVEMENT_CYCLE_LENGTH: number = 0.5; // seconds? +const THRESHOLD: number = 1.5; + +const panBounds: Tuple4<[number, number]> = [ + [panLeft, panRight], + [panLeft, panRight], + [panLeft, panRight], + [panLeft, panRight], +]; + +const tiltBound: [number, number] = [tiltDown, tiltUp]; + +export class HSLRandomMovementPattern implements Pattern { + + brightness: number; + targets: Tuple4<[number, number]>; + lastUpdate: number; + + constructor() { + this.brightness = 0; + this.targets = [ + [panLeft, tiltUp], + [panForward, tiltUp], + [panForward, tiltUp], + [panRight, tiltUp], + ] + this.lastUpdate = 0; + } + + render(update: RenderUpdate): PatternOutput { + + // color + + const t = update.absolute; + const h = 360 * ((t % HSL_CYCLE_LENGTH) / HSL_CYCLE_LENGTH); + const s = 1; + const v = 0.5; + + const [r, g, b] = hsl2rgb(h, s, v); + const rgbw: Tuple4 = [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255), + 0 + ]; + + // brightness + + this.brightness = update.bassVolume > THRESHOLD + ? 1 + : 0.75 * this.brightness; + + // movement + + if (t - this.lastUpdate > MOVEMENT_CYCLE_LENGTH) { + + this.lastUpdate = t; + + for (let index in [0, 1, 2, 3]) { + + const bound = panBounds[index]; + + const pan = rescale(Math.random(), { to: bound }); + const tilt = clamp(Math.acos(Math.random()), tiltBound); + + this.targets[index] = [pan, tilt]; + } + } + + return startAddresses.map((startAddress, index) => { + const [pan, tilt] = this.targets[index]; + + let state: MovingHeadState = { + startAddress: startAddress, + pan: pan, + tilt: tilt, + brightness: { + type: 'dimmer', + value: 0.2 + 0.8 * this.brightness, + }, + rgbw: rgbw, + speed: 1, + reset: false + } + + return state; + + }) as PatternOutput; + } +} diff --git a/boilerbloat/src/patterns/stage.ts b/boilerbloat/src/patterns/stage.ts index a710856..7e5a564 100644 --- a/boilerbloat/src/patterns/stage.ts +++ b/boilerbloat/src/patterns/stage.ts @@ -3,8 +3,8 @@ import { Tuple4 } from "./types"; export const startAddresses: Tuple4 = [1, 15, 29, 43] export const panLeft = 0; -export const panRight = -Math.PI; -export const panForward = -0.5 * Math.PI; +export const panRight = Math.PI; +export const panForward = (panLeft + panRight) / 2; -export const tiltUp = 0; -export const tiltDown = -0.5 * Math.PI; // TODO verify +export const tiltUp = 0.5 * Math.PI; +export const tiltDown = 0; diff --git a/boilerbloat/src/patterns/util.ts b/boilerbloat/src/patterns/util.ts new file mode 100644 index 0000000..7af80b9 --- /dev/null +++ b/boilerbloat/src/patterns/util.ts @@ -0,0 +1,22 @@ +// https://stackoverflow.com/a/54014428 +// input: h in [0,360] and s,v in [0,1] - output: r,g,b in [0,1] +export function hsl2rgb(h: number, s: number, l: number) { + let a = s * Math.min(l, 1 - l); + let f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return [f(0), f(8), f(4)]; +} + + +export function clamp(x: number, bounds: [number, number]): number { + const lower = Math.min(...bounds); + const upper = Math.max(...bounds); + + return Math.max(lower, Math.min(upper, x)); +} + +export function rescale(x: number, range: { from?: [number, number], to?: [number, number] }): number { + const [a, b] = range.from ? range.from : [0, 1]; + const [c, d] = range.to ? range.to : [0, 1]; + + return c + (d - c) * (x - a) / (b - a); +} diff --git a/boilerbloat/src/renderer/App.tsx b/boilerbloat/src/renderer/App.tsx index cd671ee..e441ed0 100644 --- a/boilerbloat/src/renderer/App.tsx +++ b/boilerbloat/src/renderer/App.tsx @@ -45,7 +45,7 @@ const Frontend: React.FC<{ state: AppState }> = ({ state }) => { return
- +
{ diff --git a/boilerbloat/src/renderer/ConfigControls.tsx b/boilerbloat/src/renderer/ConfigControls.tsx index ead0c58..4d5db2c 100644 --- a/boilerbloat/src/renderer/ConfigControls.tsx +++ b/boilerbloat/src/renderer/ConfigControls.tsx @@ -7,6 +7,8 @@ const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; const ConfigControls: React.FC<{ updateDelay: number }> = ({ updateDelay }) => { + const [showDelay, setShowDelay] = useState(false); + const [manual, _setManual] = useState(false); const setManual = (b: boolean) => { _setManual(b); @@ -14,20 +16,19 @@ const ConfigControls: React.FC<{ updateDelay: number }> = ({ updateDelay }) => { }; return <> - {!manual ? -
-

Delay:

- { - if (e.currentTarget.value) - ipcRenderer.send("update-delay", e.currentTarget.value) - } - } /> -
- : null}
-
setManual(false)} className={"config-button" + (manual ? "" : " manual")}> +
setShowDelay(!showDelay)} onClick={() => setManual(false)} className={"config-button" + (manual ? "" : " manual")}> Auto + {showDelay ? +

Delay: + { + if (e.currentTarget.value) + ipcRenderer.send("update-delay", e.currentTarget.value) + } + } />

+ : null} +
setManual(true)} className={"config-button" + (manual ? " manual" : "")}> Manual diff --git a/boilerbloat/src/renderer/PatternPreview.tsx b/boilerbloat/src/renderer/PatternPreview.tsx index 391480c..8596e40 100644 --- a/boilerbloat/src/renderer/PatternPreview.tsx +++ b/boilerbloat/src/renderer/PatternPreview.tsx @@ -11,32 +11,38 @@ const PatternPreview: React.FC<{ patternId: string, output: PatternOutput , sele return
ipcRenderer.send("pattern-select", patternId) - } width="260" height="150"> + } width={260 + 2*70} height="150"> {output?.map((headState, index) => { const [r, g, b, w] = headState.rgbw; const brightness = "value" in headState.brightness ? headState.brightness.value : 0; - return + return } )}
} -const ColorCone: React.FC<{ color: string, translate: [number, number], white: number }> = ({ color, translate: [x, y], white }) => { +const ColorCone: React.FC<{ pan: number, tilt: number, color: string, translate: [number, number], white: number}> = ({ color, translate: [x, y], white, pan, tilt }) => { return {white === 0 ? null - : + : } - + }; -const Cone: React.FC<{ color: string, translate: [number, number] }> = ({ color }) => { - return <> +const Cone: React.FC<{ color: string, translate: [number, number] , pan : number, tilt: number}> = ({ color , tilt, pan}) => { + // tilt is correct with -45 degree offset, but if lights are pointing down, they will be displayed to point down right + const degreesTilt = tilt / (2* Math.PI) * 360 - 45; + // TODO: adjust if necessary + const degreesPan = pan / (2* Math.PI) * 360 - 45; + + console.log(degreesTilt); + return - ; + ; }; export default PatternPreview; diff --git a/boilerbloat/src/renderer/patterns.css b/boilerbloat/src/renderer/patterns.css index 992c822..3b0cc3c 100644 --- a/boilerbloat/src/renderer/patterns.css +++ b/boilerbloat/src/renderer/patterns.css @@ -1,6 +1,7 @@ .pattern-button { - padding: 15px; - background-color: #333333; + padding: 20px; + /*background-color: #333333;*/ + background: linear-gradient(#333333, #777777); border-radius: 15px; border-color: #333333; border-width: 5px; @@ -59,7 +60,7 @@ .config-button { width: auto; min-width: 75px; - height: 30px; + min-height: 30px; padding: 15px; text-align: center; vertical-align: middle; @@ -78,8 +79,14 @@ .element-row { display: flex; flex-direction: row; + flex-wrap: wrap; } .element-row > div { margin: 5px; display: inline-block; } + +.cone { + transform-origin: 30px 30px; + transform: rotateX(90deg); +} diff --git a/rust_native_module/src/beat_tracking/tracker.rs b/rust_native_module/src/beat_tracking/tracker.rs index 4d56b28..dbce5ba 100644 --- a/rust_native_module/src/beat_tracking/tracker.rs +++ b/rust_native_module/src/beat_tracking/tracker.rs @@ -182,7 +182,11 @@ impl BeatTracker { if let Some((period_length, crossing)) = self.get_correlation_data() { let last_update_timestamp = self.audio_capture_thread.get_last_update(); - let mut dt = (now - last_update_timestamp).as_millis() as i64; + let mut dt = if now > last_update_timestamp { + (now - last_update_timestamp).as_millis() as i64 + } else { + -1 * (last_update_timestamp - now).as_millis() as i64 + }; dt += ((POINT_BUFFER_SIZE - crossing) * MILLIS_PER_POINT) as i64; let mut prev = now - Duration::from_millis(dt as u64);