feat: finished adding painting features, added eraser

This commit is contained in:
Daniel Ledda
2023-01-04 12:35:02 +01:00
parent bcdebd3372
commit 59f48c8398
11 changed files with 506 additions and 440 deletions

688
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"@typescript-eslint/parser": "^5.26.0", "@typescript-eslint/parser": "^5.26.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.16.0", "eslint": "^8.16.0",
"typescript": "^4.7.2", "typescript": "^4.9.4",
"vite": "^2.9.9" "vite": "^3.2.0"
} }
} }

View File

@@ -1,19 +1,11 @@
import Track from "@/Track"; import Track from "@/Track";
import { IPublisher, ISubscriber, Publisher } from "@djledda/ladder"; import { IPublisher, ISubscriber, Publisher } from "@djledda/ladder";
export const enum TrackUnitType { export const TrackUnitTypeList = [ "Normal", "GhostNote", "Accent", "GhostNoteAccent" ] as const;
Normal="tut-0", export type TrackUnitType = typeof TrackUnitTypeList[number];
GhostNote="tut-1",
Accent="tut-2",
GhostNoteAccent="tut-3",
}
export type TrackUnitStickingType = export const TrackUnitStickingTypeList = [ "none", "lh", "rh", "lf", "rf" ] as const;
| "none" export type TrackUnitStickingType = typeof TrackUnitStickingTypeList[number];
| "lh"
| "rh"
| "lf"
| "rf";
export const enum TrackUnitEvent { export const enum TrackUnitEvent {
Toggle="tue-0", Toggle="tue-0",
@@ -23,12 +15,6 @@ export const enum TrackUnitEvent {
} }
export default class TrackUnit implements IPublisher<TrackUnitEvent> { export default class TrackUnit implements IPublisher<TrackUnitEvent> {
private static readonly TypeRotation = [
TrackUnitType.Normal,
TrackUnitType.GhostNote,
TrackUnitType.Accent,
TrackUnitType.GhostNoteAccent,
] as const;
private publisher: Publisher<TrackUnitEvent, TrackUnit> = new Publisher<TrackUnitEvent, TrackUnit>(this); private publisher: Publisher<TrackUnitEvent, TrackUnit> = new Publisher<TrackUnitEvent, TrackUnit>(this);
private on = false; private on = false;
private typeIndex = 0; private typeIndex = 0;
@@ -42,7 +28,7 @@ export default class TrackUnit implements IPublisher<TrackUnitEvent> {
}) { }) {
this.parent = options.parent; this.parent = options.parent;
this.on = options.on ?? false; this.on = options.on ?? false;
this.setType(options.type ?? TrackUnitType.Normal); this.setType(options.type ?? "Normal");
} }
addSubscriber(subscriber: ISubscriber<TrackUnitEvent>, eventType: TrackUnitEvent[]): { unbind: () => void } { addSubscriber(subscriber: ISubscriber<TrackUnitEvent>, eventType: TrackUnitEvent[]): { unbind: () => void } {
@@ -67,13 +53,12 @@ export default class TrackUnit implements IPublisher<TrackUnitEvent> {
} }
setType(type: TrackUnitType): void { setType(type: TrackUnitType): void {
this.typeIndex = TrackUnit.TypeRotation.indexOf(type); this.typeIndex = TrackUnitTypeList.indexOf(type);
this.publisher.notifySubs(TrackUnitEvent.TypeChange); this.publisher.notifySubs(TrackUnitEvent.TypeChange);
this.parent.alertDeepChange(); this.parent.alertDeepChange();
} }
setStickingType(type: TrackUnitStickingType) { setStickingType(type: TrackUnitStickingType) {
console.log('set stickin');
this.stickingType = type; this.stickingType = type;
this.publisher.notifySubs(TrackUnitEvent.TypeChange); this.publisher.notifySubs(TrackUnitEvent.TypeChange);
this.parent.alertDeepChange(); this.parent.alertDeepChange();
@@ -84,11 +69,11 @@ export default class TrackUnit implements IPublisher<TrackUnitEvent> {
} }
getType(): TrackUnitType { getType(): TrackUnitType {
return TrackUnit.TypeRotation[this.typeIndex]; return TrackUnitTypeList[this.typeIndex];
} }
rotateType(): void { rotateType(): void {
if (this.typeIndex === TrackUnit.TypeRotation.length - 1) { if (this.typeIndex === TrackUnitTypeList.length - 1) {
this.typeIndex = 0; this.typeIndex = 0;
} else { } else {
this.typeIndex += 1; this.typeIndex += 1;

View File

@@ -125,4 +125,4 @@ export default class BeatSettingsView extends Rung implements ISubscriber<EventT
</div> </div>
</div>; </div>;
} }
} }

View File

@@ -138,23 +138,6 @@
transition: background-color 200ms; transition: background-color 200ms;
} }
.root-toolbox {
display: flex;
}
.root-toolbox .toolbox-button {
padding: 1em;
cursor: pointer;
color: black;
background-color: var(--color-ui-neutral-dark);
}
.root-toolbox .toolbox-button:hover {
background-color: var(--color-ui-neutral-dark-hover);
}
.root-toolbox .toolbox-button.active {
background-color: var(--color-ui-neutral-dark-active);
}
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
.sidebar-visible .root-sidebar { .sidebar-visible .root-sidebar {
left: 0; left: 0;

View File

@@ -181,8 +181,8 @@ export default class RootView extends Rung<HTMLDivElement> {
<div classes={["root", "sidebar-visible"]}> <div classes={["root", "sidebar-visible"]}>
<this.Sidebar/> <this.Sidebar/>
<div className={"root-beat-stage-container"}> <div className={"root-beat-stage-container"}>
{this.toolboxView}
<div className={"root-beat-stage"}> <div className={"root-beat-stage"}>
{this.toolboxView}
{this.beatView} {this.beatView}
</div> </div>
</div> </div>

View File

@@ -24,7 +24,6 @@ export default class TrackView extends Rung implements ISubscriber<EventTypeSubs
private title: HTMLHeadingElement; private title: HTMLHeadingElement;
private trackUnitViews: TrackUnitView[] = []; private trackUnitViews: TrackUnitView[] = [];
private trackUnitViewBlock: HTMLElement | null = null; private trackUnitViewBlock: HTMLElement | null = null;
private lastHoveredTrackUnitView: TrackUnitView | null = null;
private sub: ISubscription | null = null; private sub: ISubscription | null = null;
private state: AppState; private state: AppState;
static deselectingUnits = false; static deselectingUnits = false;
@@ -79,8 +78,8 @@ export default class TrackView extends Rung implements ISubscriber<EventTypeSubs
} else { } else {
view = new TrackUnitView({ trackUnit }); view = new TrackUnitView({ trackUnit });
this.trackUnitViews.push(view); this.trackUnitViews.push(view);
view.onHover(() => this.onTrackUnitViewHover(view)); view.onHover(() => this.applyCurrentToolToTrackUnit(view));
view.onMouseDown((event: MouseEvent) => this.onTrackUnitClick(event.button, i)); view.onMouseDown((event: MouseEvent) => this.onTrackUnitClick(event.button, view));
} }
} }
} }
@@ -88,28 +87,32 @@ export default class TrackView extends Rung implements ISubscriber<EventTypeSubs
deadViews.forEach(trackUnitView => trackUnitView.setUnit(null)); deadViews.forEach(trackUnitView => trackUnitView.setUnit(null));
} }
private onTrackUnitClick(button: number, index: number) { private onTrackUnitClick(button: number, view: TrackUnitView) {
if (button === 0) { if (button === 0) {
TrackView.selectingUnits = true; TrackView.selectingUnits = true;
} else if (button === 2) { } else if (button === 2) {
TrackView.deselectingUnits = true; TrackView.deselectingUnits = true;
} }
this.applyCurrentToolToTrackUnit(view);
} }
private onTrackUnitViewHover(trackUnitView: TrackUnitView) { private applyCurrentToolToTrackUnit(trackUnitView: TrackUnitView) {
this.lastHoveredTrackUnitView = trackUnitView; if (this.state.selectedTool === "sticking") {
if (TrackView.selectingUnits) { if (TrackView.selectingUnits) {
if (this.state.selectedTool === 'sticking') { trackUnitView.setStickingType(this.state.activeStickingType);
this.lastHoveredTrackUnitView.setStickingType(this.state.activeStickingType); } else if (TrackView.deselectingUnits) {
} else if (this.state.selectedTool === 'track-unit-type') { trackUnitView.setStickingType("none");
this.lastHoveredTrackUnitView.turnOn();
this.lastHoveredTrackUnitView.setType(this.state.activeTrackUnitType);
} }
} else if (TrackView.deselectingUnits) { } else if (this.state.selectedTool === "track-unit-type") {
if (this.state.selectedTool === 'sticking') { if (TrackView.selectingUnits) {
this.lastHoveredTrackUnitView.setStickingType("none"); trackUnitView.turnOn();
} else if (this.state.selectedTool === 'track-unit-type') { trackUnitView.setType(this.state.activeTrackUnitType);
this.lastHoveredTrackUnitView.turnOff(); } else if (TrackView.deselectingUnits) {
trackUnitView.turnOff();
}
} else if (this.state.selectedTool === "eraser") {
if (TrackView.selectingUnits || TrackView.deselectingUnits) {
trackUnitView.turnOff();
} }
} }
} }

View File

@@ -51,4 +51,4 @@
.track-settings .loop-settings-option { .track-settings .loop-settings-option {
flex: auto; flex: auto;
padding-right: 1em; padding-right: 1em;
} }

View File

@@ -11,7 +11,7 @@
cursor: pointer; cursor: pointer;
} }
.track-unit:hover { .track-unit.highlightable:hover {
border-color: #5f5f5f; border-color: #5f5f5f;
background-color: #5f5f5f; background-color: #5f5f5f;
transition: none; transition: none;
@@ -22,34 +22,28 @@
display: block; display: block;
} }
.track-unit.track-unit-on { .track-unit.on {
border-color: var(--color-ui-accent); border-color: var(--color-ui-accent);
background-color: var(--color-ui-accent); background-color: var(--color-ui-accent);
transition: none; transition: none;
} }
.track-unit.track-unit-on:hover { .track-unit.on.highlightable:hover {
border-color: var(--color-ui-accent-hover); border-color: var(--color-ui-accent-hover);
background-color: var(--color-ui-accent-hover); background-color: var(--color-ui-accent-hover);
} }
.track-unit.track-unit-on.track-unit-accent { .track-unit.on.Accent {
border-color: var(--color-ui-neutral-light); border-color: var(--color-ui-neutral-light);
} }
.track-unit.track-unit-on.track-unit-ghost { .track-unit.on.Ghost {
opacity: 60%; opacity: 60%;
} }
.track-unit.track-unit-on.track-unit-lh { .track-unit .icon-view {
content: 'hello'; display: none;
} }
.track-unit.track-unit-on.track-unit-lf { .track-unit.on.icon-visible .icon-view {
content: 'world'; display: block;
}
.track-unit.track-unit-on.track-unit-rh {
content: 'world2';
}
.track-unit.track-unit-on.track-unit-rf {
content: 'world';
} }

View File

@@ -1,6 +1,7 @@
import TrackUnit, { TrackUnitEvent, TrackUnitStickingType, TrackUnitType } from "@/TrackUnit"; import TrackUnit, { TrackUnitEvent, TrackUnitStickingType, TrackUnitType } from "@/TrackUnit";
import "./TrackUnit.css"; import "./TrackUnit.css";
import { h, IPublisher, ISubscriber, ISubscription, Publisher, Rung, RungOptions } from "@djledda/ladder"; import { Capsule, h, IPublisher, ISubscriber, ISubscription, Publisher, Rung, RungOptions } from "@djledda/ladder";
import IconView, { IconName } from "@/ui/Widgets/Icon/IconView";
export type TrackUnitUINodeOptions = RungOptions & { export type TrackUnitUINodeOptions = RungOptions & {
trackUnit: TrackUnit, trackUnit: TrackUnit,
@@ -13,7 +14,23 @@ const EventTypeSubscriptions = [
]; ];
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number]; type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
export default class TrackUnitView extends Rung<HTMLElement> implements ISubscriber<EventTypeSubscriptions> { export const StickingTypeIconMap = {
none: null,
lf: 'lf',
lh: 'lh',
rf: 'rf',
rh: 'rh',
} as const satisfies Readonly<Record<TrackUnitStickingType, IconName | null>>;
const TypeClasses = [ "Ghost", "Accent" ] as const;
export const TrackUnitTypeClassMap = {
"Normal": [],
"GhostNote": ["Ghost"],
"Accent": ["Accent"],
"GhostNoteAccent": ["Ghost", "Accent"],
} as const satisfies Readonly<Record<TrackUnitType, Readonly<string[]>>>;
export default class TrackUnitView extends Rung<HTMLDivElement> implements ISubscriber<EventTypeSubscriptions> {
private trackUnit: TrackUnit; private trackUnit: TrackUnit;
private subscription: ISubscription | null = null; private subscription: ISubscription | null = null;
private publisher: IPublisher<TrackUnitEvent> = new Publisher<TrackUnitEvent, TrackUnitView>(this); private publisher: IPublisher<TrackUnitEvent> = new Publisher<TrackUnitEvent, TrackUnitView>(this);
@@ -21,6 +38,7 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
private mouseDownListeners: ((ev: MouseEvent) => void)[] = []; private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
private hoverListeners: ((ev: MouseEvent) => void)[] = []; private hoverListeners: ((ev: MouseEvent) => void)[] = [];
private blockNextMouseUp = false; private blockNextMouseUp = false;
private icon: IconView = new IconView({ iconName: 'list' });
constructor(options: TrackUnitUINodeOptions) { constructor(options: TrackUnitUINodeOptions) {
super(options); super(options);
@@ -57,12 +75,6 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
private handleMouseDown(ev: MouseEvent): void { private handleMouseDown(ev: MouseEvent): void {
if (ev.button === 1) { if (ev.button === 1) {
this.trackUnit.rotateType(); this.trackUnit.rotateType();
} else {
this.rotationTimeout = this.rotationTimeout || setTimeout(() => {
this.trackUnit.rotateType();
this.blockNextMouseUp = true;
this.rotationTimeout = null;
}, 400);
} }
} }
@@ -74,10 +86,6 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
} }
private handleMouseUp(ev: MouseEvent): void { private handleMouseUp(ev: MouseEvent): void {
if (this.rotationTimeout) {
clearTimeout(this.rotationTimeout);
this.rotationTimeout = null;
}
if (!this.blockNextMouseUp) { if (!this.blockNextMouseUp) {
this.mouseDownListeners.forEach(listener => listener(ev)); this.mouseDownListeners.forEach(listener => listener(ev));
} }
@@ -103,7 +111,9 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
} }
setStickingType(type: TrackUnitStickingType): void { setStickingType(type: TrackUnitStickingType): void {
this.trackUnit.setStickingType(type); if (this.trackUnit.isOn()) {
this.trackUnit.setStickingType(type);
}
} }
setType(type: TrackUnitType): void { setType(type: TrackUnitType): void {
@@ -115,16 +125,17 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
} }
turnOff(): void { turnOff(): void {
this.trackUnit.setStickingType("none");
this.trackUnit.setOn(false); this.trackUnit.setOn(false);
} }
notify(publisher: unknown, event: EventTypeSubscriptions): void { notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch (event) { switch (event) {
case TrackUnitEvent.On: case TrackUnitEvent.On:
this.render().classList.add("track-unit-on"); this.render().classList.add("on");
break; break;
case TrackUnitEvent.Off: case TrackUnitEvent.Off:
this.render().classList.remove("track-unit-on"); this.render().classList.remove("on");
break; break;
case TrackUnitEvent.TypeChange: case TrackUnitEvent.TypeChange:
this.syncTrackUnitType(); this.syncTrackUnitType();
@@ -133,43 +144,55 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
} }
private syncStickingType(type: TrackUnitStickingType) { private syncStickingType(type: TrackUnitStickingType) {
const node = this.render(); if (StickingTypeIconMap[this.trackUnit.getStickingType()]) {
node.classList.remove("track-unit-lh"); this.render().classList.add("icon-visible");
node.classList.remove("track-unit-rh"); } else {
node.classList.remove("track-unit-lf"); this.render().classList.remove("icon-visible");
node.classList.remove("track-unit-rf");
if (type !== "none") {
node.classList.add(`track-unit-${ type }`);
} }
const icon = StickingTypeIconMap[this.trackUnit.getStickingType()];
if (icon) {
this.icon.setIcon(icon);
}
} }
private syncTrackUnitType() { private syncTrackUnitType() {
switch (this.trackUnit.getType()) { for (const className of TypeClasses) {
case TrackUnitType.Normal: this.render().classList.remove(className);
this.render().classList.remove("track-unit-ghost"); }
this.render().classList.remove("track-unit-accent"); for (const className of TrackUnitTypeClassMap[this.trackUnit.getType()]) {
break; this.render().classList.add(className);
case TrackUnitType.GhostNote:
this.render().classList.add("track-unit-ghost");
this.render().classList.remove("track-unit-accent");
break;
case TrackUnitType.Accent:
this.render().classList.remove("track-unit-ghost");
this.render().classList.add("track-unit-accent");
break;
case TrackUnitType.GhostNoteAccent:
this.render().classList.add("track-unit-ghost");
this.render().classList.add("track-unit-accent");
break;
} }
} }
build(): HTMLElement { static getClasses(options: { on: boolean, stickingType: TrackUnitStickingType, type: TrackUnitType, highlightable?: boolean }) {
const classes = ["track-unit"]; const classes = ["track-unit"];
if (this.trackUnit.isOn()) { if (options.on) {
classes.push("track-unit-on"); classes.push("on");
} }
return <div classes={classes} oncontextmenu={() => false} /> as HTMLElement; if (StickingTypeIconMap[options.stickingType]) {
classes.push("icon-visible");
}
if (options.type) {
classes.push(...TrackUnitTypeClassMap[options.type]);
}
if (options.highlightable) {
classes.push("highlightable");
}
return classes;
}
build() {
const classes = TrackUnitView.getClasses({
on: this.trackUnit.isOn(),
stickingType: this.trackUnit.getStickingType(),
type: this.trackUnit.getType(),
highlightable: true,
});
return (
<div classes={classes} oncontextmenu={() => false}>
{this.icon}
</div>
) as HTMLDivElement;
} }
onHover(cb: () => void): void { onHover(cb: () => void): void {

View File

@@ -1,15 +1,24 @@
import { h, Rung, RungOptions } from "@djledda/ladder";
import "./Icon.css"; import "./Icon.css";
import List from "assets/svgs/list.svg"; import List from "assets/svgs/list.svg";
import ArrowClockwise from "assets/svgs/arrow-clockwise.svg"; import ArrowClockwise from "assets/svgs/arrow-clockwise.svg";
import Trash from "assets/svgs/trash.svg"; import Trash from "assets/svgs/trash.svg";
import Snowflake from "assets/svgs/snowflake.svg"; import Snowflake from "assets/svgs/snowflake.svg";
import { h, Rung, RungOptions } from "@djledda/ladder"; import LeftHand from "assets/svgs/LH.png";
import RightHand from "assets/svgs/RH.png";
import LeftFoot from "assets/svgs/LF.png";
import RightFoot from "assets/svgs/RF.png";
const IconUrlMap = { const IconUrlMap = {
arrowClockwise: ArrowClockwise, arrowClockwise: ArrowClockwise,
list: List, list: List,
trash: Trash, trash: Trash,
snowflake: Snowflake, snowflake: Snowflake,
lh: LeftHand,
rh: RightHand,
lf: LeftFoot,
rf: RightFoot,
} as const; } as const;
export type IconName = keyof typeof IconUrlMap; export type IconName = keyof typeof IconUrlMap;
@@ -19,7 +28,7 @@ export type IconViewOptions = RungOptions & {
color?: string, color?: string,
}; };
export default class IconView extends Rung { export default class IconView extends Rung<HTMLDivElement> {
private iconUrl: string; private iconUrl: string;
private color: string | null; private color: string | null;
@@ -29,10 +38,19 @@ export default class IconView extends Rung {
this.iconUrl = IconUrlMap[options.iconName]; this.iconUrl = IconUrlMap[options.iconName];
} }
build(): HTMLSpanElement { setIcon(name: IconName) {
const icon = <div className={"icon-view"} /> as HTMLDivElement; this.iconUrl = IconUrlMap[name];
this.render().style.cssText = this.cssText();
}
cssText() {
const colorString = this.color ? `--icon-bg:${this.color}` : ""; const colorString = this.color ? `--icon-bg:${this.color}` : "";
icon.style.cssText = `-webkit-mask-image: url(${this.iconUrl}); mask-image: url(${this.iconUrl});${colorString}`; return `-webkit-mask-image: url(${this.iconUrl}); mask-image: url(${this.iconUrl});${colorString ?? ''}`;
}
build() {
const icon = <div className={"icon-view"} /> as HTMLDivElement;
icon.style.cssText = this.cssText();
return icon; return icon;
} }
} }