Dominic Frontend Wizardry

This commit is contained in:
Kai Vogelgesang 2021-11-13 03:27:44 +01:00
parent 63e4ced365
commit 2014e03a15
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
15 changed files with 251 additions and 80 deletions

View File

@ -1,7 +1,7 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { blackout } from '../patterns/blackout'; 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 { TestPattern } from '../patterns/test';
import rust, { BeatTrackerHandle, MovingHeadState, OutputHandle, TrackerConfig } from 'rust_native_module'; import rust, { BeatTrackerHandle, MovingHeadState, OutputHandle, TrackerConfig } from 'rust_native_module';
import { ChaserPattern } from '../patterns/chaser'; import { ChaserPattern } from '../patterns/chaser';
@ -70,7 +70,7 @@ class Backend {
trackerConfig: { trackerConfig: {
mode: "auto", mode: "auto",
acThreshold: 1000, acThreshold: 1000,
zeroCrossingBeatDelay: 0, zeroCrossingBeatDelay: 50,
} }
} }
@ -88,13 +88,14 @@ class Backend {
this.beatTracker.setConfig(this.state.trackerConfig); this.beatTracker.setConfig(this.state.trackerConfig);
}); });
let time: Time = { let update: RenderUpdate = {
absolute: 0, absolute: 0,
beatRelative: this.state.beatProgress, beatRelative: this.state.beatProgress,
bassVolume: 0,
} }
for (let [patternId, pattern] of this.patterns.entries()) { for (let [patternId, pattern] of this.patterns.entries()) {
let patternOutput = pattern.render(time); let patternOutput = pattern.render(update);
this.state.patterns[patternId] = patternOutput; this.state.patterns[patternId] = patternOutput;
} }
@ -121,9 +122,10 @@ class Backend {
this.state.graphData = this.beatTracker.getGraphPoints(); this.state.graphData = this.beatTracker.getGraphPoints();
let date = new Date(); let date = new Date();
let time: Time = { let update: RenderUpdate = {
absolute: date.getTime() / 1000, absolute: date.getTime() / 1000,
beatRelative: this.state.beatProgress, beatRelative: this.state.beatProgress,
bassVolume: this.beatTracker.getCurrentBass(),
} }
// render all patterns and write selected pattern to DMX // render all patterns and write selected pattern to DMX
@ -131,7 +133,7 @@ class Backend {
let output: Array<MovingHeadState> = blackout; let output: Array<MovingHeadState> = blackout;
for (let [patternId, pattern] of this.patterns.entries()) { for (let [patternId, pattern] of this.patterns.entries()) {
let patternOutput = pattern.render(time); let patternOutput = pattern.render(update);
this.state.patterns[patternId] = patternOutput; this.state.patterns[patternId] = patternOutput;
if (patternId === this.state.selectedPattern && patternOutput) { if (patternId === this.state.selectedPattern && patternOutput) {

View File

@ -1,42 +1,42 @@
import { MovingHeadState } from 'rust_native_module'; 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 { export class ChaserPattern implements Pattern {
render(time: Time): PatternOutput { render(update: RenderUpdate): PatternOutput {
if (time.beatRelative === null) { if (update.beatRelative === null) {
return 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;
}
} }

View File

@ -1,12 +1,14 @@
import { MovingHeadState } from "rust_native_module"; import { MovingHeadState } from "rust_native_module";
import { Tuple4 } from "./types";
export type Time = { export type RenderUpdate = {
absolute: number, absolute: number,
beatRelative: number | null, beatRelative: number | null,
bassVolume: number,
}; };
export type PatternOutput = Array<MovingHeadState> | null; export type PatternOutput = Tuple4<MovingHeadState> | null;
export interface Pattern { export interface Pattern {
render(time: Time): PatternOutput; render(update: RenderUpdate): PatternOutput;
} }

View File

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

View File

@ -1,6 +1,7 @@
import { MovingHeadState } from "rust_native_module"; import { MovingHeadState } from "rust_native_module";
import { Pattern, PatternOutput, Time } from "./proto"; import { Pattern, PatternOutput, RenderUpdate } from "./proto";
import { startAddresses } from "./stage"; import { startAddresses } from "./stage";
import { Tuple4 } from "./types";
export class TestPattern implements Pattern { export class TestPattern implements Pattern {
@ -11,8 +12,8 @@ export class TestPattern implements Pattern {
[0, 0, 0, 255], [0, 0, 0, 255],
] ]
render(time: Time): PatternOutput { render(update: RenderUpdate): PatternOutput {
let t = time.absolute % this.rgbw.length; let t = update.absolute % this.rgbw.length;
let second = Math.floor(t); let second = Math.floor(t);
let brightness = 1 - (t % 1); let brightness = 1 - (t % 1);
@ -33,7 +34,7 @@ export class TestPattern implements Pattern {
reset: false reset: false
} }
return state; return state;
}) }) as Tuple4<MovingHeadState>;
} }
} }

View File

@ -0,0 +1 @@
export type Tuple4<T> = [T, T, T, T];

View File

@ -8,6 +8,7 @@ import { AppState } from '../main/backend';
import PatternPreview from './PatternPreview'; import PatternPreview from './PatternPreview';
import GraphVisualization from './Graph'; import GraphVisualization from './Graph';
import ConfigControls from './ConfigControls'; import ConfigControls from './ConfigControls';
import { BongoCat } from './BongoCat';
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
@ -41,14 +42,17 @@ const FrontendRoot: React.FC = () => {
const Frontend: React.FC<{ state: AppState }> = ({ state }) => { const Frontend: React.FC<{ state: AppState }> = ({ state }) => {
return <> return <div className="container">
<ConfigControls updateDelay={state.trackerConfig.zeroCrossingBeatDelay} />
<div> <div>
<button onClick={tap}>Tap</button> <BongoCat onClick={tap} beatProgress={state.beatProgress} high />
</div> </div>
<div> <div style={{ width: "80%", marginTop: 80 }} className="element-row">
{Object.entries(state.patterns).map(([patternId, output]) => ( {
<PatternPreview key={patternId} patternId={patternId} output={output} /> Object.entries(state.patterns).map(([patternId, output]) => (
))} <PatternPreview selected={state.selectedPattern === patternId} key={patternId} patternId={patternId} output={output} />
))
}
</div> </div>
{ {
state.graphData state.graphData
@ -64,8 +68,7 @@ const Frontend: React.FC<{ state: AppState }> = ({ state }) => {
</div> </div>
: <div> no graph data </div> : <div> no graph data </div>
} }
<ConfigControls/> </div>;
</>;
} }

View 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>;
}

View File

@ -1,26 +1,38 @@
import { useRef } from "react";
import { IpcRenderer } from 'electron/renderer'; import { IpcRenderer } from 'electron/renderer';
import { useState } from "react";
import './patterns.css';
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; 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 <> return <>
<p>Delay:</p> {!manual ?
<input ref={bruh} type="number" min="-1000" max="1000" onChange={ <div>
(e) => { <p>Delay:</p>
ipcRenderer.send("update-delay", e.currentTarget.value) <input value={updateDelay} type="number" min="-1000" max="1000" onChange={
} (e) => {
} /> if (e.currentTarget.value)
<p>Manual mode: ipcRenderer.send("update-delay", e.currentTarget.value)
<input type="checkbox" onClick = { }
(e) => { } />
ipcRenderer.send("manual-mode", e.currentTarget.checked); </div>
} : null}
}></input> <div className="element-row" >
</p> <div onClick={() => setManual(false)} className={"config-button" + (manual ? "" : " manual")}>
Auto
</div>
<div onClick={() => setManual(true)} className={"config-button" + (manual ? " manual" : "")}>
Manual
</div>
</div>
</>; </>;
} }

View File

@ -1,14 +1,42 @@
import { IpcRenderer } from "electron/renderer"; import { IpcRenderer } from "electron/renderer";
import { PatternOutput } from "../patterns/proto"; import { PatternOutput } from "../patterns/proto";
import './patterns.css';
const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer; const ipcRenderer = (window as any).electron.ipcRenderer as IpcRenderer;
const PatternPreview: React.FC<{ patternId: string, output: PatternOutput }> = ({ patternId }) => { const PatternPreview: React.FC<{ patternId: string, output: PatternOutput , selected: boolean}> = ({ patternId, output, selected}) => {
return <button onClick={() => { const selectedClass = selected ? " selected" : "";
ipcRenderer.send("pattern-select", patternId); const styleClass = "pattern-button" + selectedClass;
}}> return <div style={{ margin: 5 }}>
{patternId} <svg className={styleClass}/*style={{ padding: 15, backgroundColor: "#333333", borderColor: "white", borderRadius: 15 }}*/ onClick={() =>
</button>; 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; export default PatternPreview;

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB