Update
This commit is contained in:
parent
2014e03a15
commit
035405bfcc
@ -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: {},
|
||||
|
98
boilerbloat/src/patterns/hslRandom.ts
Normal file
98
boilerbloat/src/patterns/hslRandom.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
22
boilerbloat/src/patterns/util.ts
Normal file
22
boilerbloat/src/patterns/util.ts
Normal 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);
|
||||
}
|
@ -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">
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user