From 91217061846b6ba33d1d40dd303253a7bd820e11 Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Sun, 20 Mar 2022 21:21:43 +0100 Subject: [PATCH] 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 --- src/Beat.ts | 11 +++- src/BeatGroup.ts | 13 ++++- src/BeatUnit.ts | 29 ++++++++-- src/main.ts | 41 ++++---------- src/ui/{BeatGroup => }/Beat/Beat.css | 0 src/ui/{BeatGroup => }/Beat/BeatView.ts | 2 +- src/ui/BeatGroup/Beat/BeatUnit/BeatUnit.css | 37 ------------- src/ui/BeatGroup/BeatGroupView.ts | 8 ++- .../BeatGroupSettingsView.ts | 45 ++++++++++----- src/ui/BeatUnit/BeatUnit.css | 55 +++++++++++++++++++ .../Beat => }/BeatUnit/BeatUnitView.ts | 42 ++++++++++---- src/ui/Root/Root.css | 3 +- src/ui/Root/RootView.ts | 39 +++++++++++-- src/ui/Widgets/ActionButton/ActionButton.css | 8 +-- webpack.config.js | 2 +- 15 files changed, 222 insertions(+), 113 deletions(-) rename src/ui/{BeatGroup => }/Beat/Beat.css (100%) rename src/ui/{BeatGroup => }/Beat/BeatView.ts (98%) delete mode 100644 src/ui/BeatGroup/Beat/BeatUnit/BeatUnit.css create mode 100644 src/ui/BeatUnit/BeatUnit.css rename src/ui/{BeatGroup/Beat => }/BeatUnit/BeatUnitView.ts (64%) diff --git a/src/Beat.ts b/src/Beat.ts index 15cacca..0d01a4a 100644 --- a/src/Beat.ts +++ b/src/Beat.ts @@ -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, BeatLike { @@ -154,9 +155,15 @@ export default class Beat implements IPublisher, 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); } } } \ No newline at end of file diff --git a/src/BeatGroup.ts b/src/BeatGroup.ts index 649f73c..8d85cc7 100644 --- a/src/BeatGroup.ts +++ b/src/BeatGroup.ts @@ -46,10 +46,17 @@ export default class BeatGroup implements IPublisher(publisher: IPublisher, 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 beat.bakeLoops()); - this.setIsUsingAutoBeatLength(false); } } diff --git a/src/BeatUnit.ts b/src/BeatUnit.ts index 895d3ec..54670c6 100644 --- a/src/BeatUnit.ts +++ b/src/BeatUnit.ts @@ -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 { + private static readonly TypeRotation = [ + BeatUnitType.Normal, + BeatUnitType.GhostNote, + BeatUnitType.Accent, + ] as const; private publisher: Publisher = new Publisher(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 { } 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()); + } } diff --git a/src/main.ts b/src/main.ts index 896a5eb..72db28d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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!"); } \ No newline at end of file diff --git a/src/ui/BeatGroup/Beat/Beat.css b/src/ui/Beat/Beat.css similarity index 100% rename from src/ui/BeatGroup/Beat/Beat.css rename to src/ui/Beat/Beat.css diff --git a/src/ui/BeatGroup/Beat/BeatView.ts b/src/ui/Beat/BeatView.ts similarity index 98% rename from src/ui/BeatGroup/Beat/BeatView.ts rename to src/ui/Beat/BeatView.ts index d4fabcd..6d9733d 100644 --- a/src/ui/BeatGroup/Beat/BeatView.ts +++ b/src/ui/Beat/BeatView.ts @@ -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 & { diff --git a/src/ui/BeatGroup/Beat/BeatUnit/BeatUnit.css b/src/ui/BeatGroup/Beat/BeatUnit/BeatUnit.css deleted file mode 100644 index 5ba6096..0000000 --- a/src/ui/BeatGroup/Beat/BeatUnit/BeatUnit.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/ui/BeatGroup/BeatGroupView.ts b/src/ui/BeatGroup/BeatGroupView.ts index acd6ad9..83d9b8f 100644 --- a/src/ui/BeatGroup/BeatGroupView.ts +++ b/src/ui/BeatGroup/BeatGroupView.ts @@ -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++) { diff --git a/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts b/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts index 7c55e29..f868d75 100644 --- a/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts +++ b/src/ui/BeatGroupSettings/BeatGroupSettingsView.ts @@ -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(publisher: IPublisher, 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(publisher: IPublisher | 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; } } diff --git a/src/ui/BeatUnit/BeatUnit.css b/src/ui/BeatUnit/BeatUnit.css new file mode 100644 index 0000000..36f61a3 --- /dev/null +++ b/src/ui/BeatUnit/BeatUnit.css @@ -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); +} diff --git a/src/ui/BeatGroup/Beat/BeatUnit/BeatUnitView.ts b/src/ui/BeatUnit/BeatUnitView.ts similarity index 64% rename from src/ui/BeatGroup/Beat/BeatUnit/BeatUnitView.ts rename to src/ui/BeatUnit/BeatUnitView.ts index 3753e70..c4d452f 100644 --- a/src/ui/BeatGroup/Beat/BeatUnit/BeatUnitView.ts +++ b/src/ui/BeatUnit/BeatUnitView.ts @@ -12,6 +12,8 @@ export default class BeatUnitView extends UINode implements ISubscriber { private beatUnit: BeatUnit; private subscription: ISubscription | null = null; private publisher: IPublisher = new Publisher(this); + private touchTimeout: ReturnType | 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); } } diff --git a/src/ui/Root/Root.css b/src/ui/Root/Root.css index 855cd22..742bd1b 100644 --- a/src/ui/Root/Root.css +++ b/src/ui/Root/Root.css @@ -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 { diff --git a/src/ui/Root/RootView.ts b/src/ui/Root/RootView.ts index 2fdc87b..ce4e038 100644 --- a/src/ui/Root/RootView.ts +++ b/src/ui/Root/RootView.ts @@ -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", { diff --git a/src/ui/Widgets/ActionButton/ActionButton.css b/src/ui/Widgets/ActionButton/ActionButton.css index 9238689..a3f16a7 100644 --- a/src/ui/Widgets/ActionButton/ActionButton.css +++ b/src/ui/Widgets/ActionButton/ActionButton.css @@ -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); } diff --git a/webpack.config.js b/webpack.config.js index 0d11942..7ae6090 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -50,7 +50,7 @@ const webpackConfig = { output: { filename: "bundle.js", - publicPath: "static/", + publicPath: "/static/", path: path.resolve(__dirname, "./public/static"), },