Dominic Frontend Wizardry
This commit is contained in:
parent
63e4ced365
commit
2014e03a15
@ -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) {
|
||||||
|
@ -1,19 +1,8 @@
|
|||||||
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';
|
||||||
|
|
||||||
export class ChaserPattern implements Pattern {
|
const template: MovingHeadState = {
|
||||||
|
|
||||||
render(time: Time): PatternOutput {
|
|
||||||
|
|
||||||
if (time.beatRelative === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let t = time.beatRelative;
|
|
||||||
|
|
||||||
let head_number = Math.floor(t % 4);
|
|
||||||
|
|
||||||
let template: MovingHeadState = {
|
|
||||||
startAddress: 0,
|
startAddress: 0,
|
||||||
pan: 0,
|
pan: 0,
|
||||||
tilt: 0,
|
tilt: 0,
|
||||||
@ -24,18 +13,29 @@ export class ChaserPattern implements Pattern {
|
|||||||
rgbw: [255, 0, 0, 0],
|
rgbw: [255, 0, 0, 0],
|
||||||
speed: 1,
|
speed: 1,
|
||||||
reset: false,
|
reset: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChaserPattern implements Pattern {
|
||||||
|
|
||||||
|
render(update: RenderUpdate): PatternOutput {
|
||||||
|
|
||||||
|
if (update.beatRelative === null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = [];
|
let t = update.beatRelative;
|
||||||
|
|
||||||
for (let [i, startAddress] of [1, 15, 29, 43].entries()) {
|
let head_number = Math.floor(t % 4);
|
||||||
result[i] = { ...template };
|
|
||||||
|
let result: Tuple4<MovingHeadState> = [{ ...template }, { ...template }, { ...template }, { ...template }];
|
||||||
|
|
||||||
|
[1, 15, 29, 43].forEach((startAddress, i) => {
|
||||||
result[i].startAddress = startAddress;
|
result[i].startAddress = startAddress;
|
||||||
|
|
||||||
if (i === head_number) {
|
if (i === head_number) {
|
||||||
result[i].brightness = { type: 'dimmer', value: 1 };
|
result[i].brightness = { type: 'dimmer', value: 1 };
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 { 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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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 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>;
|
||||||
</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 { 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 <>
|
||||||
|
{!manual ?
|
||||||
|
<div>
|
||||||
<p>Delay:</p>
|
<p>Delay:</p>
|
||||||
<input ref={bruh} type="number" min="-1000" max="1000" onChange={
|
<input value={updateDelay} type="number" min="-1000" max="1000" onChange={
|
||||||
(e) => {
|
(e) => {
|
||||||
|
if (e.currentTarget.value)
|
||||||
ipcRenderer.send("update-delay", e.currentTarget.value)
|
ipcRenderer.send("update-delay", e.currentTarget.value)
|
||||||
}
|
}
|
||||||
} />
|
} />
|
||||||
<p>Manual mode:
|
</div>
|
||||||
<input type="checkbox" onClick = {
|
: null}
|
||||||
(e) => {
|
<div className="element-row" >
|
||||||
ipcRenderer.send("manual-mode", e.currentTarget.checked);
|
<div onClick={() => setManual(false)} className={"config-button" + (manual ? "" : " manual")}>
|
||||||
}
|
Auto
|
||||||
}></input>
|
</div>
|
||||||
</p>
|
<div onClick={() => setManual(true)} className={"config-button" + (manual ? " manual" : "")}>
|
||||||
|
Manual
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
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