Dominic Frontend Wizardry
This commit is contained in:
parent
63e4ced365
commit
2014e03a15
@ -1,7 +1,7 @@
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import { blackout } from '../patterns/blackout';
|
||||
import { Pattern, PatternOutput, Time } from '../patterns/proto';
|
||||
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';
|
||||
@ -70,7 +70,7 @@ class Backend {
|
||||
trackerConfig: {
|
||||
mode: "auto",
|
||||
acThreshold: 1000,
|
||||
zeroCrossingBeatDelay: 0,
|
||||
zeroCrossingBeatDelay: 50,
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,13 +88,14 @@ class Backend {
|
||||
this.beatTracker.setConfig(this.state.trackerConfig);
|
||||
});
|
||||
|
||||
let time: Time = {
|
||||
let update: RenderUpdate = {
|
||||
absolute: 0,
|
||||
beatRelative: this.state.beatProgress,
|
||||
bassVolume: 0,
|
||||
}
|
||||
|
||||
for (let [patternId, pattern] of this.patterns.entries()) {
|
||||
let patternOutput = pattern.render(time);
|
||||
let patternOutput = pattern.render(update);
|
||||
this.state.patterns[patternId] = patternOutput;
|
||||
}
|
||||
|
||||
@ -121,9 +122,10 @@ class Backend {
|
||||
this.state.graphData = this.beatTracker.getGraphPoints();
|
||||
|
||||
let date = new Date();
|
||||
let time: Time = {
|
||||
let update: RenderUpdate = {
|
||||
absolute: date.getTime() / 1000,
|
||||
beatRelative: this.state.beatProgress,
|
||||
bassVolume: this.beatTracker.getCurrentBass(),
|
||||
}
|
||||
|
||||
// render all patterns and write selected pattern to DMX
|
||||
@ -131,7 +133,7 @@ class Backend {
|
||||
let output: Array<MovingHeadState> = blackout;
|
||||
|
||||
for (let [patternId, pattern] of this.patterns.entries()) {
|
||||
let patternOutput = pattern.render(time);
|
||||
let patternOutput = pattern.render(update);
|
||||
this.state.patterns[patternId] = patternOutput;
|
||||
|
||||
if (patternId === this.state.selectedPattern && patternOutput) {
|
||||
|
@ -1,42 +1,42 @@
|
||||
import { MovingHeadState } from 'rust_native_module';
|
||||
import { Pattern, PatternOutput, Time } from './proto';
|
||||
import { Pattern, PatternOutput, RenderUpdate } from './proto';
|
||||
import { Tuple4 } from './types';
|
||||
|
||||
const template: MovingHeadState = {
|
||||
startAddress: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
brightness: {
|
||||
type: 'dimmer',
|
||||
value: 0.2,
|
||||
},
|
||||
rgbw: [255, 0, 0, 0],
|
||||
speed: 1,
|
||||
reset: false,
|
||||
}
|
||||
|
||||
export class ChaserPattern implements Pattern {
|
||||
|
||||
render(time: Time): PatternOutput {
|
||||
render(update: RenderUpdate): PatternOutput {
|
||||
|
||||
if (time.beatRelative === null) {
|
||||
return null;
|
||||
if (update.beatRelative === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let t = update.beatRelative;
|
||||
|
||||
let head_number = Math.floor(t % 4);
|
||||
|
||||
let result: Tuple4<MovingHeadState> = [{ ...template }, { ...template }, { ...template }, { ...template }];
|
||||
|
||||
[1, 15, 29, 43].forEach((startAddress, i) => {
|
||||
result[i].startAddress = startAddress;
|
||||
|
||||
if (i === head_number) {
|
||||
result[i].brightness = { type: 'dimmer', value: 1 };
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let t = time.beatRelative;
|
||||
|
||||
let head_number = Math.floor(t % 4);
|
||||
|
||||
let template: MovingHeadState = {
|
||||
startAddress: 0,
|
||||
pan: 0,
|
||||
tilt: 0,
|
||||
brightness: {
|
||||
type: 'dimmer',
|
||||
value: 0.2,
|
||||
},
|
||||
rgbw: [255, 0, 0, 0],
|
||||
speed: 1,
|
||||
reset: false,
|
||||
}
|
||||
|
||||
let result = [];
|
||||
|
||||
for (let [i, startAddress] of [1, 15, 29, 43].entries()) {
|
||||
result[i] = { ...template };
|
||||
result[i].startAddress = startAddress;
|
||||
|
||||
if (i === head_number) {
|
||||
result[i].brightness = { type: 'dimmer', value: 1 };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { MovingHeadState } from "rust_native_module";
|
||||
import { Tuple4 } from "./types";
|
||||
|
||||
export type Time = {
|
||||
export type RenderUpdate = {
|
||||
absolute: number,
|
||||
beatRelative: number | null,
|
||||
bassVolume: number,
|
||||
};
|
||||
|
||||
export type PatternOutput = Array<MovingHeadState> | null;
|
||||
export type PatternOutput = Tuple4<MovingHeadState> | null;
|
||||
|
||||
export interface Pattern {
|
||||
render(time: Time): PatternOutput;
|
||||
render(update: RenderUpdate): PatternOutput;
|
||||
}
|
||||
|
@ -1 +1,10 @@
|
||||
export const startAddresses = [1, 15, 29, 43]
|
||||
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 tiltUp = 0;
|
||||
export const tiltDown = -0.5 * Math.PI; // TODO verify
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { MovingHeadState } from "rust_native_module";
|
||||
import { Pattern, PatternOutput, Time } from "./proto";
|
||||
import { Pattern, PatternOutput, RenderUpdate } from "./proto";
|
||||
import { startAddresses } from "./stage";
|
||||
import { Tuple4 } from "./types";
|
||||
|
||||
export class TestPattern implements Pattern {
|
||||
|
||||
@ -11,8 +12,8 @@ export class TestPattern implements Pattern {
|
||||
[0, 0, 0, 255],
|
||||
]
|
||||
|
||||
render(time: Time): PatternOutput {
|
||||
let t = time.absolute % this.rgbw.length;
|
||||
render(update: RenderUpdate): PatternOutput {
|
||||
let t = update.absolute % this.rgbw.length;
|
||||
let second = Math.floor(t);
|
||||
let brightness = 1 - (t % 1);
|
||||
|
||||
@ -33,7 +34,7 @@ export class TestPattern implements Pattern {
|
||||
reset: false
|
||||
}
|
||||
return state;
|
||||
})
|
||||
}) as Tuple4<MovingHeadState>;
|
||||
}
|
||||
|
||||
}
|
||||
|
1
boilerbloat/src/patterns/types.ts
Normal file
1
boilerbloat/src/patterns/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Tuple4<T> = [T, T, T, T];
|
@ -8,6 +8,7 @@ import { AppState } from '../main/backend';
|
||||
import PatternPreview from './PatternPreview';
|
||||
import GraphVisualization from './Graph';
|
||||
import ConfigControls from './ConfigControls';
|
||||
import { BongoCat } from './BongoCat';
|
||||
|
||||
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
|
||||
|
||||
@ -41,14 +42,17 @@ const FrontendRoot: React.FC = () => {
|
||||
|
||||
const Frontend: React.FC<{ state: AppState }> = ({ state }) => {
|
||||
|
||||
return <>
|
||||
return <div className="container">
|
||||
<ConfigControls updateDelay={state.trackerConfig.zeroCrossingBeatDelay} />
|
||||
<div>
|
||||
<button onClick={tap}>Tap</button>
|
||||
<BongoCat onClick={tap} beatProgress={state.beatProgress} high />
|
||||
</div>
|
||||
<div>
|
||||
{Object.entries(state.patterns).map(([patternId, output]) => (
|
||||
<PatternPreview key={patternId} patternId={patternId} output={output} />
|
||||
))}
|
||||
<div style={{ width: "80%", marginTop: 80 }} className="element-row">
|
||||
{
|
||||
Object.entries(state.patterns).map(([patternId, output]) => (
|
||||
<PatternPreview selected={state.selectedPattern === patternId} key={patternId} patternId={patternId} output={output} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
state.graphData
|
||||
@ -64,8 +68,7 @@ const Frontend: React.FC<{ state: AppState }> = ({ state }) => {
|
||||
</div>
|
||||
: <div> no graph data </div>
|
||||
}
|
||||
<ConfigControls/>
|
||||
</>;
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
|
28
boilerbloat/src/renderer/BongoCat.tsx
Normal file
28
boilerbloat/src/renderer/BongoCat.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import cat2 from '../res/bongo2.png';
|
||||
import cat_left from '../res/bongo_l.png';
|
||||
import cat_right from '../res/bongo_r.png';
|
||||
import cat_high from '../res/bongo_w.png';
|
||||
|
||||
export const BongoCat: React.FC<{ beatProgress: number | null, onClick: () => void, high?: boolean }> = ({ beatProgress, onClick, high }) => {
|
||||
|
||||
let image: string = cat2;
|
||||
|
||||
if (beatProgress !== null) {
|
||||
const fractional = beatProgress % 1;
|
||||
const rounded = Math.round(beatProgress);
|
||||
if (fractional < 0.5)
|
||||
if (rounded % 2 == 0) {
|
||||
image = cat_left;
|
||||
} else {
|
||||
image = cat_right;
|
||||
}
|
||||
}
|
||||
if (high)
|
||||
image=cat_high;
|
||||
|
||||
return <div onClick={onClick} className="float-right" >
|
||||
<div className={"bongo-box" + (high ? " high" : "")} >
|
||||
<img style={{ objectFit: "scale-down", height: "100%" }} src={image} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
@ -1,26 +1,38 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
import { IpcRenderer } from 'electron/renderer';
|
||||
import { useState } from "react";
|
||||
import './patterns.css';
|
||||
|
||||
|
||||
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
|
||||
|
||||
const ConfigControls: React.FC = () => {
|
||||
const ConfigControls: React.FC<{ updateDelay: number }> = ({ updateDelay }) => {
|
||||
|
||||
const bruh = useRef<HTMLInputElement>(null);
|
||||
const [manual, _setManual] = useState(false);
|
||||
const setManual = (b: boolean) => {
|
||||
_setManual(b);
|
||||
ipcRenderer.send("manual-mode", b);
|
||||
};
|
||||
|
||||
return <>
|
||||
<p>Delay:</p>
|
||||
<input ref={bruh} type="number" min="-1000" max="1000" onChange={
|
||||
(e) => {
|
||||
ipcRenderer.send("update-delay", e.currentTarget.value)
|
||||
}
|
||||
} />
|
||||
<p>Manual mode:
|
||||
<input type="checkbox" onClick = {
|
||||
(e) => {
|
||||
ipcRenderer.send("manual-mode", e.currentTarget.checked);
|
||||
}
|
||||
}></input>
|
||||
</p>
|
||||
{!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")}>
|
||||
Auto
|
||||
</div>
|
||||
<div onClick={() => setManual(true)} className={"config-button" + (manual ? " manual" : "")}>
|
||||
Manual
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,42 @@
|
||||
import { IpcRenderer } from "electron/renderer";
|
||||
import { PatternOutput } from "../patterns/proto";
|
||||
|
||||
import './patterns.css';
|
||||
|
||||
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
|
||||
|
||||
const PatternPreview: React.FC<{ patternId: string, output: PatternOutput }> = ({ patternId }) => {
|
||||
return <button onClick={() => {
|
||||
ipcRenderer.send("pattern-select", patternId);
|
||||
}}>
|
||||
{patternId}
|
||||
</button>;
|
||||
const PatternPreview: React.FC<{ patternId: string, output: PatternOutput , selected: boolean}> = ({ patternId, output, selected}) => {
|
||||
const selectedClass = selected ? " selected" : "";
|
||||
const styleClass = "pattern-button" + selectedClass;
|
||||
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">
|
||||
{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]} />
|
||||
}
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
|
||||
const ColorCone: React.FC<{ color: string, translate: [number, number], white: number }> = ({ color, translate: [x, y], white }) => {
|
||||
return <g transform={`translate(${x} ${y})`}>
|
||||
{white === 0 ? null
|
||||
: <Cone color={`rgba(1, 1, 1, ${white}`} translate={[x, y]} />
|
||||
}
|
||||
<Cone color={color} translate={[x, y]} />
|
||||
</g>
|
||||
};
|
||||
|
||||
|
||||
const Cone: React.FC<{ color: string, translate: [number, number] }> = ({ color }) => {
|
||||
return <>
|
||||
<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 " />
|
||||
</>;
|
||||
};
|
||||
|
||||
export default PatternPreview;
|
||||
|
85
boilerbloat/src/renderer/patterns.css
Normal file
85
boilerbloat/src/renderer/patterns.css
Normal file
@ -0,0 +1,85 @@
|
||||
.pattern-button {
|
||||
padding: 15px;
|
||||
background-color: #333333;
|
||||
border-radius: 15px;
|
||||
border-color: #333333;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
}
|
||||
.pattern-button:hover {
|
||||
border-color: #efefef;
|
||||
}
|
||||
.pattern-button.selected {
|
||||
border-color: #fc2424;
|
||||
}
|
||||
.pattern-button.selected:hover{
|
||||
border-color: #f77878;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.bongo-box {
|
||||
user-select: none;
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 100px;
|
||||
border-radius: 15px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.bongo-box.high {
|
||||
background-color: greenyellow;
|
||||
}
|
||||
|
||||
.bongo-box.easteregg {
|
||||
animation: spin 300ms linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
width: auto;
|
||||
min-width: 75px;
|
||||
height: 30px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
background-color: #474747;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-button.manual {
|
||||
background-color: #a7a7a7;
|
||||
}
|
||||
|
||||
.element-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.element-row > div {
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
BIN
boilerbloat/src/res/bongo2.png
Normal file
BIN
boilerbloat/src/res/bongo2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
boilerbloat/src/res/bongo_l.png
Normal file
BIN
boilerbloat/src/res/bongo_l.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
boilerbloat/src/res/bongo_r.png
Normal file
BIN
boilerbloat/src/res/bongo_r.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
boilerbloat/src/res/bongo_w.png
Normal file
BIN
boilerbloat/src/res/bongo_w.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
Loading…
Reference in New Issue
Block a user