fixed button styling, dev server, added switches for event handling, global baking, global reset, three BeatUnit states with styles, and mobile long touch to change beatunit type

This commit is contained in:
Daniel Ledda
2022-03-20 21:21:43 +01:00
parent 3ba01eb86b
commit 9121706184
15 changed files with 222 additions and 113 deletions

View File

@@ -1,4 +1,4 @@
import BeatUnit from "@/BeatUnit";
import BeatUnit, {BeatUnitType} from "@/BeatUnit";
import {IPublisher, Publisher} from "@/Publisher";
import ISubscriber from "@/Subscriber";
import BeatLike from "@/BeatLike";
@@ -22,6 +22,7 @@ export const enum BeatEvents {
DisplayTypeChanged="BE3",
LoopLengthChanged="BE4",
WantsRemoval="BE5",
Baked="BE6",
}
export default class Beat implements IPublisher<BeatEvents>, BeatLike {
@@ -154,9 +155,15 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
bakeLoops(): void {
if (this.isLooping()) {
this.unitRecord.forEach((unit, i) => {
unit.setOn(this.getUnitByIndex(i)?.isOn() ?? false);
const reprUnitAtPos = this.getUnitByIndex(i);
if (reprUnitAtPos) {
unit.mimic(reprUnitAtPos);
}
});
this.publisher.notifySubs(BeatEvents.Baked);
this.setLooping(false);
} else {
this.publisher.notifySubs(BeatEvents.Baked);
}
}
}

View File

@@ -46,10 +46,17 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatEvents.LoopLengthChanged || event === BeatEvents.DisplayTypeChanged) {
switch (event) {
case BeatEvents.LoopLengthChanged:
case BeatEvents.DisplayTypeChanged:
this.autoBeatLength();
} else if (event === BeatEvents.WantsRemoval) {
break;
case BeatEvents.WantsRemoval:
this.removeBeat((publisher as Beat).getKey());
break;
case BeatEvents.Baked:
this.setIsUsingAutoBeatLength(false);
break;
}
}
@@ -213,6 +220,7 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
BeatEvents.LoopLengthChanged,
BeatEvents.WantsRemoval,
BeatEvents.DisplayTypeChanged,
BeatEvents.Baked,
]);
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
return newBeat;
@@ -267,6 +275,5 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
bakeLoops(): void {
this.beats.forEach(beat => beat.bakeLoops());
this.setIsUsingAutoBeatLength(false);
}
}

View File

@@ -4,6 +4,7 @@ import ISubscriber from "./Subscriber";
export enum BeatUnitType {
Normal,
GhostNote,
Accent,
}
@@ -16,12 +17,18 @@ export const enum BeatUnitEvents {
export default class BeatUnit implements IPublisher<BeatUnitEvents> {
private static readonly TypeRotation = [
BeatUnitType.Normal,
BeatUnitType.GhostNote,
BeatUnitType.Accent,
] as const;
private publisher: Publisher<BeatUnitEvents, BeatUnit> = new Publisher<BeatUnitEvents, BeatUnit>(this);
private on = false;
private type: BeatUnitType = BeatUnitType.Normal;
private typeIndex = 0;
constructor(on = false) {
constructor(on = false, type = BeatUnitType.Normal) {
this.on = on;
this.setType(type);
}
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatUnitEvents | BeatUnitEvents[]): ISubscription {
@@ -44,15 +51,29 @@ export default class BeatUnit implements IPublisher<BeatUnitEvents> {
}
setType(type: BeatUnitType): void {
this.type = type;
this.typeIndex = BeatUnit.TypeRotation.indexOf(type);
this.publisher.notifySubs(BeatUnitEvents.TypeChange);
}
getType(): BeatUnitType {
return this.type;
return BeatUnit.TypeRotation[this.typeIndex];
}
rotateType(): void {
if (this.typeIndex === BeatUnit.TypeRotation.length - 1) {
this.typeIndex = 0;
} else {
this.typeIndex += 1;
}
this.publisher.notifySubs(BeatUnitEvents.TypeChange);
}
isOn(): boolean {
return this.on;
}
mimic(beatUnit: BeatUnit): void {
this.setOn(beatUnit.isOn());
this.setType(beatUnit.getType());
}
}

View File

@@ -1,38 +1,21 @@
import BeatGroup from "@/BeatGroup";
import RootView from "@/ui/Root/RootView";
import "@/ui/global.css";
const defaultSettings = {
barCount: 2,
isLooping: false,
timeSigUp: 8,
};
const mainBeatGroup = new BeatGroup(defaultSettings);
mainBeatGroup.addBeat({
name: "LF"
});
mainBeatGroup.addBeat({
name: "LH"
});
mainBeatGroup.addBeat({
name: "RH"
});
mainBeatGroup.addBeat({
name: "RF"
});
const appNode = document.querySelector("#app");
if (appNode) {
const appRoot = new RootView({
title: "Drum Slayer",
mainBeatGroup: mainBeatGroup,
});
//@ts-ignore
window.appRoot = appRoot;
appNode.appendChild(appRoot.render());
console.log("OK!");
try {
const appRoot = new RootView({
title: "Drum Slayer",
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
window.appRoot = appRoot;
appNode.appendChild(appRoot.render());
console.log("OK!");
} catch (e) {
console.error("FUCK!", e);
}
} else {
console.error("FUCK!");
}

View File

@@ -2,7 +2,7 @@ import UINode, {UINodeOptions} from "@/ui/UINode";
import Beat, {BeatEvents} from "@/Beat";
import {IPublisher} from "@/Publisher";
import ISubscriber from "@/Subscriber";
import BeatUnitView from "./BeatUnit/BeatUnitView";
import BeatUnitView from "@/ui/BeatUnit/BeatUnitView";
import "./Beat.css";
export type BeatUINodeOptions = UINodeOptions & {

View File

@@ -1,37 +0,0 @@
.beat-unit {
width: 2em;
height: 2em;
margin-right: 4px;
background-color: var(--color-ui-neutral-dark);
border-width: 0.1em 0.1em 0.1em 0.1em;
border-color: var(--color-ui-neutral-dark);
border-style: solid;
display: inline-block;
transition: background-color 150ms;
cursor: pointer;
}
.vertical-mode .beat-unit {
margin-bottom: 4px;
display: block;
}
.beat-unit:hover {
border-color: var(--color-ui-neutral-dark-hover);
background-color: var(--color-ui-neutral-dark-hover);
transition: none;
}
.beat-unit.beat-unit-ghost:hover {
background-color: #c9e2c9;
}
.beat-unit.beat-unit-on {
background-color: var(--color-ui-accent);
border-color: var(--color-ui-accent);
transition: none;
}
.beat-unit.beat-unit-on.beat-unit-ghost {
background-color: darkseagreen;
}

View File

@@ -1,6 +1,6 @@
import UINode, {UINodeOptions} from "@/ui/UINode";
import BeatGroup, {BeatGroupEvents} from "@/BeatGroup";
import BeatView from "./Beat/BeatView";
import BeatView from "@/ui/Beat/BeatView";
import "./BeatGroup.css";
import ISubscriber from "@/Subscriber";
import {IPublisher} from "@/Publisher";
@@ -28,6 +28,12 @@ export default class BeatGroupView extends UINode implements ISubscriber {
}
}
setBeatGroup(newBeatGroup: BeatGroup): void {
this.beatGroup = newBeatGroup;
this.beatGroup.addSubscriber(this, BeatGroupEvents.BeatListChanged);
this.redraw();
}
build(): HTMLDivElement {
this.beatViews = [];
for (let i = 0; i < this.beatGroup.getBeatCount(); i++) {

View File

@@ -20,35 +20,52 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
private autoBeatLengthCheckbox!: BoolBoxView;
private beatSettingsViews: BeatSettingsView[] = [];
private beatSettingsContainer!: HTMLDivElement;
private static readonly EventTypeSubscriptions = [
BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged,
];
constructor(options: BeatGroupSettingsUINodeOptions) {
super(options);
this.beatGroup = options.beatGroup;
this.beatGroup.addSubscriber(this, [
BeatGroupEvents.BarCountChanged,
BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
BeatGroupEvents.BeatListChanged,
BeatGroupEvents.LockingChanged,
BeatGroupEvents.AutoBeatSettingsChanged,
]);
this.setupBindings();
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatGroupEvents.BarCountChanged) {
setBeatGroup(newBeatGroup: BeatGroup): void {
this.beatGroup = newBeatGroup;
this.setupBindings();
BeatGroupSettingsView.EventTypeSubscriptions.forEach(eventType => this.notify(null, eventType));
}
setupBindings(): void {
this.beatGroup.addSubscriber(this, BeatGroupSettingsView.EventTypeSubscriptions);
}
notify<T extends string | number>(publisher: IPublisher<T> | null, event: "all" | T[] | T): void {
switch(event) {
case BeatGroupEvents.BarCountChanged:
this.barCountInput.setValue(this.beatGroup.getBarCount());
} else if (event === BeatGroupEvents.TimeSigUpChanged) {
break;
case BeatGroupEvents.TimeSigUpChanged:
this.timeSigUpInput.setValue(this.beatGroup.getTimeSigUp());
} else if (event === BeatGroupEvents.BeatListChanged) {
break;
case BeatGroupEvents.BeatListChanged:
this.remakeBeatSettingsViews();
} else if (event === BeatGroupEvents.LockingChanged) {
break;
case BeatGroupEvents.LockingChanged:
if (this.beatGroup.barsLocked()) {
this.barCountInput.disable();
} else {
this.barCountInput.enable();
}
} else if (event === BeatGroupEvents.AutoBeatSettingsChanged) {
break;
case BeatGroupEvents.AutoBeatSettingsChanged:
this.autoBeatLengthCheckbox.setValue(this.beatGroup.autoBeatLengthOn());
break;
}
}

View File

@@ -0,0 +1,55 @@
.beat-unit {
width: 2em;
height: 2em;
margin-right: 4px;
background-color: #464646;
border-color: #464646;
border-width: 0.1em 0.1em 0.1em 0.1em;
border-style: solid;
display: inline-block;
transition: background-color 100ms, border-color 100ms;
cursor: pointer;
}
.beat-unit:hover {
border-color: #5f5f5f;
background-color: #5f5f5f;
transition: none;
}
.vertical-mode .beat-unit {
margin-bottom: 4px;
display: block;
}
.beat-unit.beat-unit-on {
border-color: var(--color-ui-accent);
background-color: var(--color-ui-accent);
transition: none;
}
.beat-unit.beat-unit-on:hover {
border-color: var(--color-ui-accent-hover);
background-color: var(--color-ui-accent-hover);
}
.beat-unit.beat-unit-on.beat-unit-ghost {
border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent);
}
.beat-unit.beat-unit-on.beat-unit-ghost:hover {
border-color: var(--color-ui-neutral-light);
background-color: var(--color-ui-accent-hover);
}
.beat-unit.beat-unit-on.beat-unit-accent {
border-color: var(--color-ui-accent);
background-color: var(--color-ui-accent);
opacity: 60%;
}
.beat-unit.beat-unit-on.beat-unit-accent:hover {
border-color: var(--color-ui-accent-hover);
background-color: var(--color-ui-accent-hover);
}

View File

@@ -12,6 +12,8 @@ export default class BeatUnitView extends UINode implements ISubscriber {
private beatUnit: BeatUnit;
private subscription: ISubscription | null = null;
private publisher: IPublisher<BeatUnitEvents> = new Publisher<BeatUnitEvents, BeatUnitView>(this);
private touchTimeout: ReturnType<typeof setTimeout> | null = null;
constructor(options: BeatUnitUINodeOptions) {
super(options);
@@ -29,10 +31,21 @@ export default class BeatUnitView extends UINode implements ISubscriber {
private setupBindings() {
this.subscription?.unbind();
this.subscription = this.beatUnit.addSubscriber(this, "all");
this.onMouseUp((ev: MouseEvent) => {
this.onMouseDown((ev: MouseEvent) => {
if (ev.button === 1) {
const currentType = this.beatUnit.getType();
this.beatUnit.setType(currentType === BeatUnitType.GhostNote ? BeatUnitType.Normal : BeatUnitType.GhostNote);
this.beatUnit.rotateType();
}
});
this.getNode().addEventListener("touchstart", () => {
this.touchTimeout = setTimeout(() => {
this.beatUnit.rotateType();
this.touchTimeout = null;
}, 600);
});
this.getNode().addEventListener("touchend", () => {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
this.touchTimeout = null;
}
});
}
@@ -55,12 +68,19 @@ export default class BeatUnitView extends UINode implements ISubscriber {
} else if (event === BeatUnitEvents.Off) {
this.getNode().classList.remove("beat-unit-on");
} else if (event === BeatUnitEvents.TypeChange) {
const showingAsGhost = this.getNode().classList.contains("beat-unit-ghost");
const isGhost = this.beatUnit.getType() === BeatUnitType.GhostNote;
if (isGhost && !showingAsGhost) {
this.getNode().classList.add("beat-unit-ghost");
} else if (!isGhost && showingAsGhost) {
switch (this.beatUnit.getType()) {
case BeatUnitType.Normal:
this.getNode().classList.remove("beat-unit-ghost");
this.getNode().classList.remove("beat-unit-accent");
break;
case BeatUnitType.GhostNote:
this.getNode().classList.remove("beat-unit-accent");
this.getNode().classList.add("beat-unit-ghost");
break;
case BeatUnitType.Accent:
this.getNode().classList.remove("beat-unit-ghost");
this.getNode().classList.add("beat-unit-accent");
break;
}
}
}
@@ -77,14 +97,14 @@ export default class BeatUnitView extends UINode implements ISubscriber {
}
onHover(cb: () => void): void {
this.getNode().onmouseover = cb;
this.getNode().addEventListener("mouseover", cb);
}
onMouseDown(cb: (ev: MouseEvent) => void): void {
this.getNode().onmousedown = cb;
this.getNode().addEventListener("mousedown", cb);
}
onMouseUp(cb: (ev: MouseEvent) => void): void {
this.getNode().onmouseup = cb;
this.getNode().addEventListener("mouseup", cb);
}
}

View File

@@ -64,11 +64,12 @@
left: 0;
}
.root-hamburger, .root-switch-mode {
.root-quick-access-button {
right: 0;
width: 2em;
height: 2em;
cursor: pointer;
margin-bottom: 0.5em;
}
.root-beat-stage-container {

View File

@@ -7,7 +7,7 @@ import IconView from "@/ui/Widgets/Icon/IconView";
export type RootUINodeOptions = UINodeOptions & {
title: string,
mainBeatGroup: BeatGroup,
mainBeatGroup?: BeatGroup,
};
export default class RootView extends UINode {
@@ -20,11 +20,25 @@ export default class RootView extends UINode {
constructor(options: RootUINodeOptions) {
super(options);
this.mainBeatGroup = options.mainBeatGroup;
this.mainBeatGroup = options.mainBeatGroup ?? RootView.defaultMainBeatGroup();
this.beatGroupView = new BeatGroupView({title: options.title, beatGroup: this.mainBeatGroup});
this.title = options.title;
}
static defaultMainBeatGroup(): BeatGroup {
const defaultSettings = {
barCount: 2,
isLooping: false,
timeSigUp: 8,
};
const mainBeatGroup = new BeatGroup(defaultSettings);
mainBeatGroup.addBeat({name: "LF"});
mainBeatGroup.addBeat({name: "LH"});
mainBeatGroup.addBeat({name: "RH"});
mainBeatGroup.addBeat({name: "RF"});
return mainBeatGroup;
}
toggleSidebar(): void {
this.getNode().classList.toggle("sidebar-visible");
}
@@ -46,15 +60,30 @@ export default class RootView extends UINode {
classes: ["root-sidebar-toggle"],
subs: [
UINode.make("div", {
classes: ["root-hamburger"],
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "list", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleSidebar(),
}),
UINode.make("div", {
classes: ["root-switch-mode"],
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "arrowClockwise", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.toggleOrientation(),
})
}),
UINode.make("div", {
classes: ["root-quick-access-button"],
subs: [new IconView({iconName: "snowflake", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => this.mainBeatGroup.bakeLoops(),
}),
UINode.make("div", {
classes: ["root-quick-access-button"],
title: "Reset all",
subs: [new IconView({iconName: "trash", color: "var(--color-ui-neutral-dark)"}).render()],
onclick: () => {
this.mainBeatGroup = RootView.defaultMainBeatGroup();
this.beatGroupSettingsView.setBeatGroup(this.mainBeatGroup);
this.beatGroupView.setBeatGroup(this.mainBeatGroup);
},
}),
]
});
this.sidebar = UINode.make("div", {

View File

@@ -14,11 +14,11 @@
background-color: var(--color-ui-accent);
color: var(--color-p-light);
}
.action-button.action-button-primary:hover:not.disabled {
.action-button.action-button-primary:hover {
background-color: var(--color-ui-accent-hover);
color: var(--color-p-light-hover);
}
.action-button.action-button-primary:active:not.disabled {
.action-button.action-button-primary:active {
background-color: var(--color-ui-accent-active);
color: var(--color-p-light-active);
}
@@ -27,11 +27,11 @@
background-color: var(--color-ui-neutral-dark);
color: var(--color-p-light);
}
.action-button.action-button-secondary:hover:not.disabled {
.action-button.action-button-secondary:hover:not(.disabled) {
background-color: var(--color-ui-neutral-dark-hover);
color: var(--color-p-light-hover);
}
.action-button.action-button-secondary:active:not.disabled {
.action-button.action-button-secondary:active:not(.disabled) {
background-color: var(--color-ui-neutral-dark-active);
color: var(--color-p-light-active);
}

View File

@@ -50,7 +50,7 @@ const webpackConfig = {
output: {
filename: "bundle.js",
publicPath: "static/",
publicPath: "/static/",
path: path.resolve(__dirname, "./public/static"),
},