feat: more ui, looping

This commit is contained in:
Daniel Ledda
2021-08-30 14:12:41 +02:00
parent 76b8a427bc
commit 7fa530f070
22 changed files with 529 additions and 166 deletions

View File

@@ -1,6 +1,8 @@
import BeatUnit, {BeatUnitType} from "./BeatUnit";
import {IPublisher, Publisher} from "./Publisher";
import ISubscriber from "./Subscriber";
import BeatLike from "./BeatLike";
import {isPosInt} from "./utils";
export type BeatInitOptions = {
timeSig?: {
@@ -9,6 +11,8 @@ export type BeatInitOptions = {
},
name?: string,
bars?: number,
isLooping?: boolean,
loopLength?: number,
};
export enum BeatEvents {
@@ -16,9 +20,11 @@ export enum BeatEvents {
NewBarCount,
NewName,
UnitChanged,
DisplayTypeChanged,
LoopLengthChanged,
}
export default class Beat implements IPublisher<BeatEvents>{
export default class Beat implements IPublisher<BeatEvents>, BeatLike {
private static count = 0;
private readonly key: string;
private name: string;
@@ -27,13 +33,30 @@ export default class Beat implements IPublisher<BeatEvents>{
private readonly unitRecord: BeatUnit[] = [];
private barCount = 1;
private publisher = new Publisher<BeatEvents>();
private loopLength: number;
private looping: boolean;
constructor(options?: BeatInitOptions) {
this.key = `Beat-${Beat.count}`;
this.name = options?.name ?? this.key;
this.setTimeSignature({up: options?.timeSig?.up ?? 4, down: options?.timeSig?.down ?? 4});
this.setBars(options?.bars ?? 4);
this.setBarCount(options?.bars ?? 4);
Beat.count++;
this.loopLength = options?.loopLength ?? this.timeSigUp * this.barCount;
this.looping = options?.isLooping ?? false;
}
setLoopLength(loopLength: number): void {
if (!isPosInt(loopLength) || loopLength < 2) {
return;
}
this.loopLength = loopLength | 0;
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
}
setLooping(isLooping: boolean): void {
this.looping = isLooping;
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
}
addSubscriber(subscriber: ISubscriber, eventType: BeatEvents | "all"): { unbind: () => void } {
@@ -43,6 +66,7 @@ export default class Beat implements IPublisher<BeatEvents>{
setTimeSignature(timeSig: {up?: number, down?: number}): void {
if (timeSig.up && Beat.isValidTimeSigRange(timeSig.up)) {
this.timeSigUp = timeSig.up | 0;
this.loopLength = this.timeSigUp * this.barCount;
}
if (timeSig.down && Beat.isValidTimeSigRange(timeSig.down)) {
this.timeSigDown = timeSig.down | 0;
@@ -51,17 +75,20 @@ export default class Beat implements IPublisher<BeatEvents>{
this.publisher.notifySubs(BeatEvents.NewTimeSig);
}
setBars(barCount: number): void {
const isPosInt = (barCount > 0 && (barCount | 0) === barCount);
if (!isPosInt || barCount == this.barCount) {
setBarCount(barCount: number): void {
if (!isPosInt(barCount) || barCount == this.barCount) {
return;
}
this.barCount = barCount;
this.loopLength = this.timeSigUp * this.barCount;
this.updateBeatUnitLength();
this.publisher.notifySubs(BeatEvents.NewBarCount);
}
getUnitByIndex(index: number): BeatUnit | null {
if (this.looping) {
return this.unitRecord[index % this.loopLength];
}
return this.unitRecord[index] ?? null;
}
@@ -86,7 +113,7 @@ export default class Beat implements IPublisher<BeatEvents>{
}
turnUnitOn(index: number): void {
if (Math.abs(index | 0) !== index) {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
@@ -97,7 +124,7 @@ export default class Beat implements IPublisher<BeatEvents>{
}
turnUnitOff(index: number): void {
if (Math.abs(index | 0) !== index) {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
@@ -109,7 +136,7 @@ export default class Beat implements IPublisher<BeatEvents>{
toggleUnit(index: number): void {
if (Math.abs(index | 0) !== index) {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
@@ -120,7 +147,7 @@ export default class Beat implements IPublisher<BeatEvents>{
}
setUnitType(index: number, type: BeatUnitType): void {
if (Math.abs(index | 0) !== index) {
if (!isPosInt(index)) {
return;
}
this.getUnit(index).setType(type);
@@ -162,4 +189,12 @@ export default class Beat implements IPublisher<BeatEvents>{
getName(): string {
return this.name;
}
isLooping(): boolean {
return this.looping;
}
getLoopLength(): number {
return this.loopLength;
}
}

View File

@@ -1,9 +1,15 @@
import Beat, {BeatInitOptions} from "./Beat";
import Beat, {BeatEvents, BeatInitOptions} from "./Beat";
import {IPublisher, Publisher} from "./Publisher";
import ISubscriber from "./Subscriber";
import BeatLike from "./BeatLike";
import {isPosInt} from "./utils";
type BeatGroupInitOptions = {
barCount: number;
isLooping: boolean;
timeSigUp: number;
beats: BeatInitOptions[],
loopLength?: number,
}
export const enum BeatGroupEvents {
@@ -13,12 +19,14 @@ export const enum BeatGroupEvents {
GlobalTimeSigUpChanged,
}
export default class BeatGroup implements IPublisher<BeatGroupEvents> {
export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvents>, BeatLike {
private beats: Beat[] = [];
private beatKeyMap: Record<string, number> = {};
private publisher: Publisher<BeatGroupEvents> = new Publisher<BeatGroupEvents>();
private lastGlobalBarCount: number;
private lastGlobalTimeSigUp: number;
private publisher: Publisher<BeatGroupEvents | BeatEvents> = new Publisher<BeatGroupEvents | BeatEvents>();
private globalBarCount: number;
private globalTimeSigUp: number;
private globalLoopLength: number;
private globalIsLooping: boolean;
constructor(options?: BeatGroupInitOptions) {
if (options?.beats) {
@@ -27,34 +35,64 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents> {
this.beats.push(newBeat);
this.beatKeyMap[newBeat.getKey()] = this.beats.length - 1;
}
this.lastGlobalBarCount = this.beats[0].getBarCount();
this.lastGlobalTimeSigUp = this.beats[0].getTimeSigUp();
} else {
this.lastGlobalBarCount = 4;
this.lastGlobalTimeSigUp = 4;
}
this.globalBarCount = options?.barCount ?? 4;
this.globalTimeSigUp = options?.timeSigUp ?? 4;
this.globalLoopLength = options?.loopLength ?? this.globalBarCount * this.globalTimeSigUp;
this.globalIsLooping = options?.isLooping ?? false;
}
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatGroupEvents | BeatGroupEvents[]): { unbind: () => void } {
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatGroupEvents | BeatEvents | (BeatGroupEvents | BeatEvents)[]): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType);
}
setGlobalBarCount(barCount: number): void {
setBarCount(barCount: number): void {
if (barCount <= 0 || (barCount | 0) !== barCount) {
return;
}
this.lastGlobalBarCount = barCount;
this.globalBarCount = barCount;
for (const beat of this.beats) {
beat.setBars(barCount);
beat.setBarCount(barCount);
}
this.publisher.notifySubs(BeatGroupEvents.GlobalBarCountChanged);
}
getBarCount(): number {
return this.globalBarCount;
}
setLoopLength(loopLength: number): void {
if (!isPosInt(loopLength)) {
return;
}
this.globalLoopLength = loopLength;
for (const beat of this.beats) {
beat.setLoopLength(loopLength);
}
this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
}
getLoopLength(): number {
return this.globalLoopLength;
}
setLooping(isLooping: boolean): void {
this.globalIsLooping = isLooping;
for (const beat of this.beats) {
beat.setLooping(isLooping);
}
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
}
isLooping(): boolean {
return this.globalIsLooping;
}
setGlobalTimeSigUp(timeSigUp: number): void {
if (!Beat.isValidTimeSigRange(timeSigUp)) {
return;
}
this.lastGlobalTimeSigUp = timeSigUp;
this.globalTimeSigUp = timeSigUp;
for (const beat of this.beats) {
beat.setTimeSignature({up: timeSigUp});
}

11
src/BeatLike.ts Normal file
View File

@@ -0,0 +1,11 @@
import {IPublisher} from "./Publisher";
import {BeatEvents} from "./Beat";
export default interface BeatLike extends IPublisher<BeatEvents>{
setBarCount(barCount: number): void;
getBarCount(): void;
setLooping(isLooping: boolean): void;
isLooping(): boolean;
setLoopLength(loopLength: number): void;
getLoopLength(): number;
}

View File

@@ -40,12 +40,12 @@ export default class BeatUnit implements IPublisher<BeatUnitEvents> {
setOn(on: boolean): void {
this.on = on;
this.publisher.notifySubs(BeatUnitEvents.On);
this.publisher.notifySubs(this.on ? BeatUnitEvents.On : BeatUnitEvents.Off);
}
setType(type: BeatUnitType): void {
this.type = type;
this.publisher.notifySubs(BeatUnitEvents.Off);
this.publisher.notifySubs(BeatUnitEvents.TypeChange);
}
getType(): BeatUnitType {

View File

@@ -28,13 +28,12 @@ const appNode = document.querySelector("#app");
if (appNode) {
const appRoot = new RootView({
parent: appNode as HTMLDivElement,
title: "Drum Slayer",
mainBeatGroup: mainBeatGroup,
});
//@ts-ignore
window.appRoot = appRoot;
appRoot.render();
appNode.appendChild(appRoot.render());
console.log("OK!");
} else {
console.error("FUCK!");

View File

@@ -12,16 +12,31 @@
}
.beat-settings-btn {
margin: 0;
cursor: pointer;
line-height: 2em;
display: inline-block;
height: 2em;
border: 1px solid grey;
border-radius: 1em;
user-select: none;
transition: background-color 150ms;
}
.beat-settings-btn:hover {
background-color: #eaeaea;
}
.beat-settings-btn.active {
background-color: lightgrey;
transition: none;
}
.beat-unit-block {
display: inline-block;
height: 2em;
}
.beat-title {
display: inline-block;
line-height: 2em;
width: 3em;
margin: 0;
}
@@ -29,4 +44,17 @@
.beat-spacer {
display: inline-block;
width: 1em;
height: 2em;
}
.beat-main {
display: inline-flex;
}
.beat-settings-container {
display: flex;
}
.beat {
width: max-content;
}

View File

@@ -1,7 +1,31 @@
.beat-settings {
padding: 1em;
display: none;
text-align: center;
width: 40em;
justify-content: space-evenly;
}
.beat-settings.visible {
display: inline-flex;
}
.beat-settings-time-sig-up {
display: block;
}
.beat-settings-time-sig-down {
display: block;
}
.beat-settings-option {
display: inline-block;
}
.beat-settings-option-group {
align-self: stretch;
}
.beat-settings-time-sig input {
margin: auto;
}

View File

@@ -3,6 +3,8 @@ import Beat, {BeatEvents} from "../../../../Beat";
import {IPublisher} from "../../../../Publisher";
import ISubscriber from "../../../../Subscriber";
import "./BeatSettings.css";
import BeatLike from "../../../../BeatLike";
import BeatLikeLoopSettingsView from "../../BeatLikeLoopSettings/BeatLikeLoopSettingsView";
export type BeatSettingsViewUINodeOptions = UINodeOptions & {
beat: Beat,
@@ -14,6 +16,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
private timeSigUp!: HTMLInputElement;
private timeSigDown!: HTMLInputElement;
private barCountInput!: HTMLInputElement;
private loopSettingsView!: BeatLikeLoopSettingsView;
constructor(options: BeatSettingsViewUINodeOptions) {
super(options);
@@ -25,7 +28,7 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
this.beat.addSubscriber(this, "all");
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T) {
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatEvents.NewTimeSig) {
this.timeSigUp.value = this.beat.getTimeSigUp().toString();
this.timeSigDown.value = this.beat.getTimeSigDown().toString();
@@ -48,38 +51,51 @@ export default class BeatSettingsView extends UINode implements ISubscriber {
}
rebuild(): HTMLElement {
this.loopSettingsView = new BeatLikeLoopSettingsView({beatLike: this.beat});
this.timeSigUp = UINode.make("input", {
classes: ["time-sig-up"],
type: "number",
value: this.beat.getTimeSigUp().toString(),
oninput: (event) => {
oninput: (event: Event) => {
this.beat.setTimeSignature({up: Number((event.target as HTMLInputElement).value) });
},
});
this.timeSigDown = UINode.make("input", {
classes: ["beat-settings-time-sig-down"],
type: "number",
value: this.beat.getTimeSigDown().toString(),
oninput: (event) => {
oninput: (event: Event) => {
this.beat.setTimeSignature({down: Number((event.target as HTMLInputElement).value) });
},
});
this.barCountInput = UINode.make("input", {
classes: ["beat-settings-bars-count"],
type: "number",
value: this.beat.getBarCount().toString(),
oninput: (event) => {
this.beat.setBars(Number((event.target as HTMLInputElement).value));
oninput: (event: Event) => {
this.beat.setBarCount(Number((event.target as HTMLInputElement).value));
},
});
this.node = UINode.make("div", {
classes: ["beat-settings"],
subs: [
UINode.make("div", {
classes: ["beat-settings-time-sig"],
classes: ["beat-settings-time-sig", "beat-settings-option-group", "beat-settings-option"],
subs: [
UINode.make("label", {innerText: "Time Signature:"}),
this.timeSigUp,
this.timeSigDown,
]
}),
UINode.make("label", {innerText: "Bars:"}),
UINode.make("div", {
classes: ["beat-settings-bar", "beat-settings-option-group", "beat-settings-option"],
subs: [
UINode.make("label", {innerText: "Bar Count:"}),
this.barCountInput,
],
classes: ["beat-settings"]
}),
this.loopSettingsView.render(),
],
});
return this.node;
}

View File

@@ -1,11 +1,30 @@
.beat-unit {
width: 2em;
height: 2em;
margin-right: 0.2em;
background-color: white;
border: 0.1em solid black;
border-width: 0.1em 0.1em 0.1em 0.1em;
border-color: black;
border-style: solid;
display: inline-block;
transition: background-color 150ms;
cursor: pointer;
}
.beat-unit.on {
background-color: darksalmon;
.beat-unit:hover {
background-color: #f1c3b6;
transition: none;
}
.beat-unit.beat-unit-ghost:hover {
background-color: #c9e2c9;
}
.beat-unit.beat-unit-on {
background-color: darksalmon;
transition: none;
}
.beat-unit.beat-unit-on.beat-unit-ghost {
background-color: darkseagreen;
}

View File

@@ -1,4 +1,4 @@
import BeatUnit, {BeatUnitEvents} from "../../../../BeatUnit";
import BeatUnit, {BeatUnitEvents, BeatUnitType} from "../../../../BeatUnit";
import ISubscriber from "../../../../Subscriber";
import UINode, {UINodeOptions} from "../../../UINode";
import {IPublisher} from "../../../../Publisher";
@@ -10,37 +10,79 @@ export type BeatUnitUINodeOptions = UINodeOptions & {
export default class BeatUnitView extends UINode implements ISubscriber {
private beatUnit: BeatUnit;
private subscription!: {unbind: () => void};
constructor(options: BeatUnitUINodeOptions) {
super(options);
this.beatUnit = options.beatUnit;
this.setupBindings();
}
setUnit(beatUnit: BeatUnit): void {
this.subscription.unbind();
this.beatUnit = beatUnit;
this.setupBindings();
this.rebuild();
this.redraw();
}
private setupBindings() {
this.beatUnit.addSubscriber(this, "all");
this.subscription = this.beatUnit.addSubscriber(this, "all");
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T) {
toggle(): void {
this.beatUnit.toggle();
}
turnOn(): void {
this.beatUnit.setOn(true);
}
turnOff(): void {
this.beatUnit.setOn(false);
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatUnitEvents.On) {
this.node?.classList.add("on");
this.node?.classList.add("beat-unit-on");
} else if (event === BeatUnitEvents.Off) {
this.node?.classList.remove("on");
this.node?.classList.remove("beat-unit-on");
} else if (event === BeatUnitEvents.TypeChange) {
if (this.beatUnit.getType() === BeatUnitType.GhostNote) {
this.node?.classList.add("beat-unit-ghost");
} else {
this.node?.classList.remove("beat-unit-ghost");
}
}
}
rebuild(): HTMLElement {
const classes = ["beat-unit"];
if (this.beatUnit.isOn()) {
classes.push("on");
classes.push("beat-unit-on");
}
this.node = UINode.make("div", {
classes: classes,
onclick: () => {
this.beatUnit.toggle();
oncontextmenu: () => false,
});
this.onMouseUp((ev: MouseEvent) => {
if (ev.button === 1) {
const currentType = this.beatUnit.getType();
this.beatUnit.setType(currentType === BeatUnitType.GhostNote ? BeatUnitType.Normal : BeatUnitType.GhostNote);
}
});
return this.node;
}
onHover(cb: () => void): void {
this.getNode().onmouseover = cb;
}
onMouseDown(cb: (ev: MouseEvent) => void): void {
this.getNode().onmousedown = cb;
}
onMouseUp(cb: (ev: MouseEvent) => void): void {
this.getNode().onmouseup = cb;
}
}

View File

@@ -1,53 +0,0 @@
import ISubscriber from "../../../../Subscriber";
import UINode, {UINodeOptions} from "../../../UINode";
import BeatGroup, {BeatGroupEvents} from "../../../../BeatGroup";
import {IPublisher} from "../../../../Publisher";
export type BeatUnitCollectionUINodeOptions = UINodeOptions & {
beatGroup: BeatGroup,
};
export default class BeatUnitCollectionView extends UINode implements ISubscriber {
private beatGroup: BeatGroup;
private barCountInput!: HTMLInputElement;
private timeSigUpInput!: HTMLInputElement;
constructor(options: BeatUnitCollectionUINodeOptions) {
super(options);
this.beatGroup = options.beatGroup;
this.beatGroup.addSubscriber(this, []);
this.rebuild();
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatGroupEvents.GlobalBarCountChanged) {
this.barCountInput.value = this.beatGroup.getBeatByIndex(0).getBarCount().toString();
} else if (event === BeatGroupEvents.GlobalTimeSigUpChanged) {
this.barCountInput.value = this.beatGroup.getBeatByIndex(0).getBarCount().toString();
}
}
rebuild(): HTMLElement {
this.barCountInput = UINode.make("input", {
type: "text",
classes: ["beat-group-settings-view-bar-count"],
value: this.beatGroup.getBeatByIndex(0).getBarCount(),
oninput: () => {
this.beatGroup.setGlobalBarCount(Number(this.barCountInput.value));
},
});
this.timeSigUpInput = UINode.make("input", {
type: "text",
value: this.beatGroup.getBeatByIndex(0).getTimeSigUp(),
classes: ["beat-group-settings-view-time-sig-up"],
oninput: () => {
this.beatGroup.setGlobalTimeSigUp(Number(this.timeSigUpInput.value));
},
});
this.node = UINode.make("div", {
subs: [
],
});
return this.node;
}
}

View File

@@ -14,15 +14,17 @@ export default class BeatView extends UINode implements ISubscriber {
private beat: Beat;
private title!: HTMLHeadingElement;
private settingsView!: BeatSettingsView;
private settingsToggleButton!: HTMLButtonElement;
private settingsToggleButton!: HTMLDivElement;
private beatUnitViews: BeatUnitView[] = [];
private beatUnitViewBlock!: HTMLElement;
private beatUnitViewBlock: HTMLElement | null = null;
private lastHoveredBeatUnitView: BeatUnitView | null = null;
private static deselectingUnits = false;
private static selectingUnits = false;
constructor(options: BeatUINodeOptions) {
super(options);
this.beat = options.beat;
this.setupBindings();
this.rebuild();
}
private setupBindings() {
@@ -33,29 +35,85 @@ export default class BeatView extends UINode implements ISubscriber {
if (event === BeatEvents.NewName) {
this.title.innerText = this.beat.getName();
} else if (event === BeatEvents.NewTimeSig) {
this.render();
this.setupBeatUnits();
} else if (event === BeatEvents.NewBarCount) {
this.render();
this.setupBeatUnits();
} else if (event === BeatEvents.DisplayTypeChanged) {
this.setupBeatUnits();
} else if (event === BeatEvents.LoopLengthChanged) {
this.setupBeatUnits();
}
}
private toggleSettings() {
this.settingsView.toggleVisible();
this.settingsToggleButton.innerText = this.settingsView.isOpen() ? "Hide Settings" : "Show Settings";
if (this.settingsView.isOpen()) {
this.settingsToggleButton.classList.add("active");
} else {
this.settingsToggleButton.classList.remove("active");
}
}
private makeBeatUnits() {
private rebuildBeatUnitViews() {
const beatUnitCount = this.beat.getBarCount() * this.beat.getTimeSigUp();
this.beatUnitViews = [];
this.beatUnitViews.splice(beatUnitCount, this.beatUnitViews.length - beatUnitCount);
for (let i = 0; i < beatUnitCount; i++) {
const beatUnit = this.beat.getUnitByIndex(i);
if (beatUnit) {
this.beatUnitViews.push(new BeatUnitView({beatUnit}));
let view: BeatUnitView;
if (this.beatUnitViews[i]) {
view = this.beatUnitViews[i];
view.setUnit(beatUnit);
} else {
view = new BeatUnitView({beatUnit});
this.beatUnitViews.push(view);
}
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
window.addEventListener("mouseup", (event: MouseEvent) => {
BeatView.selectingUnits = false;
BeatView.deselectingUnits = false;
});
view.onHover(() => {
this.lastHoveredBeatUnitView = view;
if (BeatView.selectingUnits) {
this.lastHoveredBeatUnitView.turnOn();
} else if (BeatView.deselectingUnits) {
this.lastHoveredBeatUnitView.turnOff();
}
});
}
}
}
private onBeatUnitClick(button: number, index: number) {
if (button === 0) {
BeatView.selectingUnits = true;
this.beat.getUnitByIndex(index)?.toggle();
} else if (button === 2) {
BeatView.deselectingUnits = true;
this.beat.getUnitByIndex(index)?.setOn(false);
}
}
private buildBeatUnitViewBlock(): void {
const beatUnitNodes: HTMLElement[] = [];
for (let i = 0; i < this.beatUnitViews.length; i++) {
beatUnitNodes.push(this.beatUnitViews[i].render());
}
if (this.beatUnitViewBlock) {
this.beatUnitViewBlock.replaceChildren(...beatUnitNodes);
} else {
this.beatUnitViewBlock = UINode.make("div", {
classes: ["beat-unit-block"],
subs: [...beatUnitNodes],
});
}
}
private respaceBeatUnits(): void {
if (!this.beatUnitViewBlock) {
return;
}
this.beatUnitViewBlock.querySelectorAll(".beat-spacer").forEach(spacer => spacer.remove());
const barLength = this.beat.getTimeSigUp();
const barCount = this.beat.getBarCount();
@@ -79,32 +137,39 @@ export default class BeatView extends UINode implements ISubscriber {
}
}
private setupBeatUnits(): void {
this.rebuildBeatUnitViews();
this.buildBeatUnitViewBlock();
this.respaceBeatUnits();
}
rebuild(): HTMLElement {
this.title = UINode.make("h3", {
innerText: this.beat.getName(),
classes: ["beat-title"],
});
this.makeBeatUnits();
this.setupBeatUnits();
this.settingsView = new BeatSettingsView({beat: this.beat});
this.settingsToggleButton = UINode.make("button", {
this.settingsToggleButton = UINode.make("div", {
classes: ["beat-settings-btn"],
innerText: this.settingsView.isOpen() ? "Hide Settings" : "Show Settings",
innerText: "Settings",
onclick: () => this.toggleSettings()
});
this.settingsToggleButton.addEventListener("click", () => this.toggleSettings());
this.beatUnitViewBlock = UINode.make("div", {
classes: ["beat-unit-block"],
subs: [
...this.beatUnitViews.map(view => view.rebuild()),
],
});
this.respaceBeatUnits();
this.node = UINode.make("div", {
classes: ["beat"],
subs: [
UINode.make("div", {
classes: ["beat-main"],
subs: [
this.title,
this.beatUnitViewBlock,
this.beatUnitViewBlock!,
]
}),
this.settingsToggleButton,
this.settingsView.rebuild(),
UINode.make("div", {
classes: ["beat-settings-container"],
subs: [this.settingsView.render()],
}),
],
});
return this.node;

View File

@@ -1,3 +1,14 @@
.beat-group-settings {
}
.beat-group-settings-options {
padding: 1em;
display: inline-flex;
width: 40em;
justify-content: space-evenly;
}
.beat-group-settings-option {
display: inline-block;
text-align: center;
}

View File

@@ -3,6 +3,8 @@ import UINode, {UINodeOptions} from "../../UINode";
import ISubscriber from "../../../Subscriber";
import {IPublisher} from "../../../Publisher";
import {BeatGroupEvents} from "../../../BeatGroup";
import BeatLikeLoopSettingsView from "../BeatLikeLoopSettings/BeatLikeLoopSettingsView";
import "./BeatGroupSettings.css";
export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
beatGroup: BeatGroup,
@@ -12,6 +14,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
private beatGroup: BeatGroup;
private barCountInput!: HTMLInputElement;
private timeSigUpInput!: HTMLInputElement;
private loopSettingsView!: BeatLikeLoopSettingsView;
constructor(options: BeatGroupSettingsUINodeOptions) {
super(options);
@@ -20,7 +23,6 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
BeatGroupEvents.GlobalBarCountChanged,
BeatGroupEvents.GlobalTimeSigUpChanged
]);
this.rebuild();
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
@@ -32,34 +34,46 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
}
rebuild(): HTMLElement {
this.loopSettingsView = new BeatLikeLoopSettingsView({beatLike: this.beatGroup});
this.barCountInput = UINode.make("input", {
type: "text",
classes: ["beat-group-settings-view-bar-count"],
value: this.beatGroup.getBeatByIndex(0).getBarCount(),
type: "number",
value: this.beatGroup.getBeatByIndex(0).getBarCount().toString(),
oninput: () => {
this.beatGroup.setGlobalBarCount(Number(this.barCountInput.value));
this.beatGroup.setBarCount(Number(this.barCountInput.value));
},
});
this.timeSigUpInput = UINode.make("input", {
type: "text",
value: this.beatGroup.getBeatByIndex(0).getTimeSigUp(),
classes: ["beat-group-settings-view-time-sig-up"],
type: "number",
value: this.beatGroup.getBeatByIndex(0).getTimeSigUp().toString(),
oninput: () => {
this.beatGroup.setGlobalTimeSigUp(Number(this.timeSigUpInput.value));
},
});
this.node = UINode.make("div", {
classes: ["beat-group-settings"],
subs: [
UINode.make("h4", {innerText: "Settings for beat"}),
UINode.make("label", {
innerText: "Bars:",
}),
UINode.make("h4", { innerText: "Settings for beat" }),
UINode.make("div", {
classes: ["beat-group-settings-options"],
subs: [
UINode.make("div", {
classes: ["beat-group-settings-bar-count", "beat-group-settings-option"],
subs: [
UINode.make("label", { innerText: "Bars:" }),
this.barCountInput,
UINode.make("label", {
innerText: "Boxes per bar:",
],
}),
UINode.make("div", {
classes: ["beat-group-settings-boxes", "beat-group-settings-option"],
subs: [
UINode.make("label", { innerText: "Boxes per bar:" }),
this.timeSigUpInput,
],
}),
this.loopSettingsView.render(),
],
}),
],
});
return this.node;
}

View File

@@ -12,6 +12,7 @@ export default class BeatGroupView extends UINode {
private title: string;
private beatGroup: BeatGroup;
private beatGroupSettingsView!: BeatGroupSettingsView;
private beatViews: BeatView[] = [];
constructor(options: BeatGroupUINodeOptions) {
super(options);
@@ -20,17 +21,17 @@ export default class BeatGroupView extends UINode {
}
rebuild(): HTMLDivElement {
const beatViews = [];
this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {
beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
this.beatViews.push(new BeatView({beat: this.beatGroup.getBeatByIndex(i)}));
}
this.beatGroupSettingsView = new BeatGroupSettingsView({beatGroup: this.beatGroup});
return UINode.make("div", {
classes: ["beat-group"],
subs: [
UINode.make("h3", {innerText: this.title}),
this.beatGroupSettingsView.rebuild(),
...beatViews.map(bv => bv.rebuild())
this.beatGroupSettingsView.render(),
...this.beatViews.map(bv => bv.render())
],
});
}

View File

@@ -0,0 +1,17 @@
.loop-settings-option-group {
margin-top: 1em;
}
.loop-settings-option > label {
width: 5em;
text-align: left;
}
.loop-settings > p {
margin: 0;
text-align: center;
}
.loop-settings-option {
display: flex;
}

View File

@@ -0,0 +1,79 @@
import BeatLike from "../../../BeatLike";
import UINode, {UINodeOptions} from "../../UINode";
import ISubscriber from "../../../Subscriber";
import {IPublisher} from "../../../Publisher";
import {BeatEvents} from "../../../Beat";
import "./BeatLikeLoopSettings.css";
export type BeatLikeLoopSettingsViewUINodeOptions = UINodeOptions & {
beatLike: BeatLike,
};
export default class BeatLikeLoopSettingsView extends UINode implements ISubscriber {
private beatLike: BeatLike;
private loopLengthInput!: HTMLInputElement;
private loopCheckbox!: HTMLInputElement;
constructor(options: BeatLikeLoopSettingsViewUINodeOptions) {
super(options);
this.beatLike = options.beatLike;
this.setupBindings();
}
private setupBindings() {
this.beatLike.addSubscriber(this, "all");
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatEvents.LoopLengthChanged) {
this.loopLengthInput.value = this.beatLike.getLoopLength().toString();
} else if (event === BeatEvents.DisplayTypeChanged) {
this.loopCheckbox.checked = this.beatLike.isLooping();
}
}
rebuild(): HTMLElement {
this.loopLengthInput = UINode.make("input", {
classes: ["loop-settings-loop-length"],
type: "number",
value: this.beatLike.getLoopLength().toString(),
oninput: (event: Event) => {
this.beatLike.setLoopLength(Number((event.target as HTMLInputElement).value));
},
});
this.loopCheckbox = UINode.make("input", {
classes: ["loop-settings-loop-toggle"],
type: "checkbox",
checked: this.beatLike.isLooping(),
oninput: (event: Event) => {
this.beatLike.setLooping((event.target as HTMLInputElement).checked);
},
});
this.node = UINode.make("div", {
classes: ["loop-settings"],
subs: [
UINode.make("p", {innerText: "Looping:"}),
UINode.make("div", {
classes: ["loop-settings-option-group"],
subs: [
UINode.make("div", {
classes: ["loop-settings-option"],
subs: [
UINode.make("label", {innerText: "Length:"}),
this.loopLengthInput,
],
}),
UINode.make("div", {
classes: ["loop-settings-option"],
subs: [
UINode.make("label", {innerText: "On:"}),
this.loopCheckbox,
],
}),
],
}),
]
});
return this.node;
}
}

View File

@@ -1,12 +1,17 @@
.root {
margin: auto;
width: 80%;
margin-left: 10em;
margin-right: 10em;
}
.root .title {
text-align: center;
}
.root input {
.root input[type="number"] {
width: 5em;
}
* {
user-drag: none;
user-select: none;
}

View File

@@ -6,12 +6,10 @@ import "./Root.css";
export type RootUINodeOptions = UINodeOptions & {
title: string,
mainBeatGroup: BeatGroup,
parent: HTMLElement,
};
export default class RootView extends UINode {
private title: string;
protected parent: HTMLElement;
private beatGroupView: BeatGroupView;
private mainBeatGroup: BeatGroup;
@@ -20,8 +18,6 @@ export default class RootView extends UINode {
this.beatGroupView = new BeatGroupView({title: "THE BEAT", beatGroup: options.mainBeatGroup});
this.mainBeatGroup = options.mainBeatGroup;
this.title = options.title;
this.parent = options.parent;
this.rebuild();
}
rebuild(): HTMLDivElement {
@@ -29,7 +25,7 @@ export default class RootView extends UINode {
classes: ["root"],
subs: [
UINode.make("h1", {innerText: this.title, classes: ["title"]}),
this.beatGroupView.rebuild(),
this.beatGroupView.render(),
],
});
}

View File

@@ -12,20 +12,33 @@ type IRenderAttributes<
export default abstract class UINode {
protected node: HTMLElement | null = null;
protected parent: HTMLElement | null = null;
constructor(options: UINodeOptions) {}
render(): void {
const oldNode = this.node;
render(): HTMLElement {
if (!this.node) {
this.node = this.rebuild();
if (oldNode) {
if (!this.parent) {
this.parent = oldNode.parentElement;
}
this.parent!.replaceChild(this.node, oldNode);
return this.node;
}
protected getNode(): HTMLElement {
if (!this.node) {
return this.render();
} else {
this.parent!.appendChild(this.node);
return this.node;
}
}
redraw(): void {
const oldNode = this.node;
this.render();
if (!oldNode || !this.node) {
return;
}
const parent = this.node.parentElement;
if (parent) {
parent.replaceChild(oldNode, this.node);
}
}

3
src/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isPosInt(maybePosInt: number): boolean {
return (maybePosInt | 0) === maybePosInt && maybePosInt > 0;
}