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

690
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",
"cross-env": "^7.0.3",
"eslint": "^8.16.0",
"typescript": "^4.7.2",
"vite": "^2.9.9"
"typescript": "^4.9.4",
"vite": "^3.2.0"
}
}

View File

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

View File

@@ -138,23 +138,6 @@
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) {
.sidebar-visible .root-sidebar {
left: 0;

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import TrackUnit, { TrackUnitEvent, TrackUnitStickingType, TrackUnitType } from "@/TrackUnit";
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 & {
trackUnit: TrackUnit,
@@ -13,7 +14,23 @@ const EventTypeSubscriptions = [
];
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 subscription: ISubscription | null = null;
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 hoverListeners: ((ev: MouseEvent) => void)[] = [];
private blockNextMouseUp = false;
private icon: IconView = new IconView({ iconName: 'list' });
constructor(options: TrackUnitUINodeOptions) {
super(options);
@@ -57,12 +75,6 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
private handleMouseDown(ev: MouseEvent): void {
if (ev.button === 1) {
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 {
if (this.rotationTimeout) {
clearTimeout(this.rotationTimeout);
this.rotationTimeout = null;
}
if (!this.blockNextMouseUp) {
this.mouseDownListeners.forEach(listener => listener(ev));
}
@@ -103,8 +111,10 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
}
setStickingType(type: TrackUnitStickingType): void {
if (this.trackUnit.isOn()) {
this.trackUnit.setStickingType(type);
}
}
setType(type: TrackUnitType): void {
this.trackUnit.setType(type);
@@ -115,16 +125,17 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
}
turnOff(): void {
this.trackUnit.setStickingType("none");
this.trackUnit.setOn(false);
}
notify(publisher: unknown, event: EventTypeSubscriptions): void {
switch (event) {
case TrackUnitEvent.On:
this.render().classList.add("track-unit-on");
this.render().classList.add("on");
break;
case TrackUnitEvent.Off:
this.render().classList.remove("track-unit-on");
this.render().classList.remove("on");
break;
case TrackUnitEvent.TypeChange:
this.syncTrackUnitType();
@@ -133,43 +144,55 @@ export default class TrackUnitView extends Rung<HTMLElement> implements ISubscri
}
private syncStickingType(type: TrackUnitStickingType) {
const node = this.render();
node.classList.remove("track-unit-lh");
node.classList.remove("track-unit-rh");
node.classList.remove("track-unit-lf");
node.classList.remove("track-unit-rf");
if (type !== "none") {
node.classList.add(`track-unit-${ type }`);
if (StickingTypeIconMap[this.trackUnit.getStickingType()]) {
this.render().classList.add("icon-visible");
} else {
this.render().classList.remove("icon-visible");
}
const icon = StickingTypeIconMap[this.trackUnit.getStickingType()];
if (icon) {
this.icon.setIcon(icon);
}
}
private syncTrackUnitType() {
switch (this.trackUnit.getType()) {
case TrackUnitType.Normal:
this.render().classList.remove("track-unit-ghost");
this.render().classList.remove("track-unit-accent");
break;
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;
for (const className of TypeClasses) {
this.render().classList.remove(className);
}
for (const className of TrackUnitTypeClassMap[this.trackUnit.getType()]) {
this.render().classList.add(className);
}
}
build(): HTMLElement {
static getClasses(options: { on: boolean, stickingType: TrackUnitStickingType, type: TrackUnitType, highlightable?: boolean }) {
const classes = ["track-unit"];
if (this.trackUnit.isOn()) {
classes.push("track-unit-on");
if (options.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 {

View File

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