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 { 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) {

View File

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

View File

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

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

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

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 { 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>
</>;
}

View File

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

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