This commit is contained in:
Kai Vogelgesang 2021-11-13 12:22:41 +01:00
parent 2014e03a15
commit 035405bfcc
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
9 changed files with 169 additions and 29 deletions

View File

@ -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: {},

View File

@ -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<number> = [
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;
}
}

View File

@ -3,8 +3,8 @@ import { Tuple4 } from "./types";
export const startAddresses: Tuple4<number> = [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;

View File

@ -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);
}

View File

@ -45,7 +45,7 @@ const Frontend: React.FC<{ state: AppState }> = ({ state }) => {
return <div className="container">
<ConfigControls updateDelay={state.trackerConfig.zeroCrossingBeatDelay} />
<div>
<BongoCat onClick={tap} beatProgress={state.beatProgress} high />
<BongoCat onClick={tap} beatProgress={state.beatProgress} />
</div>
<div style={{ width: "80%", marginTop: 80 }} className="element-row">
{

View File

@ -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 ?
<div>
<p>Delay:</p>
<input value={updateDelay} type="number" min="-1000" max="1000" onChange={
(e) => {
if (e.currentTarget.value)
ipcRenderer.send("update-delay", e.currentTarget.value)
}
} />
</div>
: null}
<div className="element-row" >
<div onClick={() => setManual(false)} className={"config-button" + (manual ? "" : " manual")}>
<div onContextMenu={() => setShowDelay(!showDelay)} onClick={() => setManual(false)} className={"config-button" + (manual ? "" : " manual")}>
Auto
{showDelay ?
<p>Delay:
<input value={updateDelay} type="number" min="-1000" max="1000" onChange={
(e) => {
if (e.currentTarget.value)
ipcRenderer.send("update-delay", e.currentTarget.value)
}
} /></p>
: null}
</div>
<div onClick={() => setManual(true)} className={"config-button" + (manual ? " manual" : "")}>
Manual

View File

@ -11,32 +11,38 @@ const PatternPreview: React.FC<{ patternId: string, output: PatternOutput , sele
return <div style={{ margin: 5 }}>
<svg className={styleClass}/*style={{ padding: 15, backgroundColor: "#333333", borderColor: "white", borderRadius: 15 }}*/ onClick={() =>
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 <ColorCone white={w} color={`rgba(${r},${g},${b},${(0.3 + 0.7 * brightness)})`} translate={[index * 65, 0]} />
return <ColorCone tilt={headState.tilt} pan={headState.pan} white={w} color={`rgba(${r},${g},${b},${(0.3 + 0.7 * brightness)})`} translate={[70 + index * 65, 0]} />
}
)}
</svg>
</div>
}
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 <g transform={`translate(${x} ${y})`}>
{white === 0 ? null
: <Cone color={`rgba(1, 1, 1, ${white}`} translate={[x, y]} />
: <Cone pan={pan} tilt={tilt} color={`rgba(1, 1, 1, ${white}`} translate={[x, y]} />
}
<Cone color={color} translate={[x, y]} />
<Cone pan={pan} tilt={tilt} color={color} translate={[x, y]} />
</g>
};
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 <g className="cone" style={{transform: `rotate(${degreesTilt}deg) rotateX(${degreesPan}deg)`}} >
<path stroke="black" fill="gray" d="M 15 0 L 45 0 L 60 30 L 0 30 L 15 0" />
<path fill={color} d="M 15 30 L 0 150 L 60 150 L 45 30 " />
</>;
</g>;
};
export default PatternPreview;

View File

@ -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);
}

View File

@ -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);