feat: cleaned up mouse behaviour and saving
This commit is contained in:
@@ -2,62 +2,63 @@
|
||||
<div
|
||||
class="root"
|
||||
:class="{ 'sidebar-visible': sidebarActive, 'vertical-mode': currentOrientation === 'vertical' }">
|
||||
<div class="root-sidebar">
|
||||
<div class="root-sidebar-left-strip">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-left-strip">
|
||||
<div v-for="(beat, i) in beats"
|
||||
:key="beat.name.value"
|
||||
class="root-sidebar-left-tab"
|
||||
class="sidebar-left-tab"
|
||||
:class="{ 'active': i === activeBeatIndex }"
|
||||
@click="activeBeatIndex = i">
|
||||
{{ beat.name.value }}
|
||||
</div>
|
||||
<div
|
||||
class="root-sidebar-add-beat"
|
||||
class="sidebar-add-beat"
|
||||
@click="addNewBeat()">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
<div class="root-settings">
|
||||
<h1 class="root-title">{{ title }}</h1>
|
||||
<div class="settings">
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
<beat-settings :beat-index="activeBeatIndex" />
|
||||
</div>
|
||||
<div class="root-sidebar-toggle">
|
||||
<div class="sidebar-toggle">
|
||||
<div
|
||||
class="root-quick-access-button"
|
||||
class="quick-access-button"
|
||||
:title="`${ sidebarActive ? 'Hide' : 'Show' } sidebar`"
|
||||
@click="sidebarActive = !sidebarActive">
|
||||
<icon icon-name="list" color="var(--color-ui-neutral-dark)" />
|
||||
</div>
|
||||
<div
|
||||
class="root-quick-access-button"
|
||||
class="quick-access-button"
|
||||
title="Change orientation"
|
||||
@click="toggleOrientation">
|
||||
<icon icon-name="arrowClockwise" color="var(--color-ui-neutral-dark)" />
|
||||
</div>
|
||||
<div
|
||||
class="root-quick-access-button"
|
||||
class="quick-access-button"
|
||||
title="Bake all tracks"
|
||||
@click="bakeAll">
|
||||
<icon icon-name="snowflake" color="var(--color-ui-neutral-dark)" />
|
||||
</div>
|
||||
<div
|
||||
class="root-quick-access-button"
|
||||
class="quick-access-button"
|
||||
title="Reset all"
|
||||
@click="resetActiveBeat">
|
||||
<icon icon-name="trash" color="var(--color-ui-neutral-dark)" />
|
||||
</div>
|
||||
<div
|
||||
class="root-quick-access-button"
|
||||
title="Reset all"
|
||||
class="quick-access-button"
|
||||
:class="{ 'unclickable': !saveDirty }"
|
||||
:title="saveDirty ? 'Save changes' : 'No unsaved changes'"
|
||||
@click="save('localStorage')">
|
||||
Save
|
||||
<icon icon-name="download" color="var(--color-ui-neutral-dark)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="root-beat-stage-container">
|
||||
<div class="beat-stage-container">
|
||||
<toolbox />
|
||||
<div class="root-beat-stage">
|
||||
<div class="beat-stage">
|
||||
<beat-view
|
||||
:beat-index="activeBeatIndex"
|
||||
:orientation="currentOrientation" />
|
||||
@@ -67,7 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, provide, ref } from "vue";
|
||||
import { onBeforeUnmount, onMounted, provide, ref, watch } from "vue";
|
||||
import BeatSettings from "@/ui/BeatSettings/BeatSettings.vue";
|
||||
import BeatView from "@/ui/Beat/Beat.vue";
|
||||
import Icon from "@/ui/Widgets/Icon/Icon.vue";
|
||||
@@ -95,8 +96,19 @@
|
||||
beats,
|
||||
addNewBeat,
|
||||
bakeAll,
|
||||
saveDirty,
|
||||
} = beatStore;
|
||||
|
||||
const TITLE = 'Drum Slayer';
|
||||
|
||||
watch(saveDirty, (dirty) => {
|
||||
if (dirty) {
|
||||
document.title = `${ TITLE } (unsaved changes)`;
|
||||
} else {
|
||||
document.title = TITLE;
|
||||
}
|
||||
});
|
||||
|
||||
const mediaQueryList = window.matchMedia("screen and (max-width: 900px)");
|
||||
const onMediaChange = (event: MediaQueryListEvent | MediaQueryList) => {
|
||||
sidebarActive.value = event.matches;
|
||||
@@ -107,6 +119,7 @@
|
||||
function windowMouseUp() {
|
||||
appStateStore.selectingUnits.value = false;
|
||||
appStateStore.deselectingUnits.value = false;
|
||||
appStateStore.unitMouseStart.value = null;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -134,142 +147,149 @@
|
||||
background-color: var(--color-bg-dark);
|
||||
height: 100vh;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.root-sidebar {
|
||||
position: absolute;
|
||||
left: -28em;
|
||||
width: 30em;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
transition: left 400ms;
|
||||
}
|
||||
|
||||
.sidebar-visible .root-sidebar {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.root-settings {
|
||||
z-index: 1;
|
||||
width: 28em;
|
||||
background-color: var(--color-bg-light);
|
||||
overflow: scroll;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.root-settings .root-title {
|
||||
color: var(--color-title-light);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root-sidebar-toggle {
|
||||
z-index: 1;
|
||||
height: 100vh;
|
||||
min-width: 2em;
|
||||
background-color: var(--color-bg-light);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.root-quick-access-button {
|
||||
right: 0;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.root-beat-stage-container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: left 400ms, width 400ms;
|
||||
}
|
||||
|
||||
.sidebar-visible .root-beat-stage-container {
|
||||
left: 30em;
|
||||
width: calc(100vw - 30em);
|
||||
}
|
||||
|
||||
.root-beat-stage {
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
margin: auto;
|
||||
max-width: 100vw;
|
||||
transition: max-width 400ms;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.vertical-mode .root-beat-stage {
|
||||
margin: auto auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-visible .root-beat-stage {
|
||||
max-width: calc(100vw - 30em);
|
||||
}
|
||||
|
||||
.root-sidebar-left-strip {
|
||||
writing-mode: vertical-rl;
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.root-sidebar-left-strip > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.root-sidebar-left-tab {
|
||||
transform: rotate(-180deg);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 8px 3px 8px 3px;
|
||||
}
|
||||
|
||||
.root-sidebar-left-tab.active {
|
||||
background-color: var(--color-bg-medium);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.root-sidebar-add-beat {
|
||||
width: 100%;
|
||||
padding: 8px 3px 8px 3px;
|
||||
}
|
||||
|
||||
.root-sidebar-add-beat:hover,
|
||||
.root-sidebar-left-tab:hover:not(.active) {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-ui-neutral-dark);
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.sidebar-visible .root-sidebar {
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
left: -28em;
|
||||
width: 30em;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
transition: left 400ms;
|
||||
}
|
||||
.root-sidebar {
|
||||
left: calc(-100vw + 2em);
|
||||
width: 100vw;
|
||||
|
||||
&.sidebar-visible {
|
||||
.beat-stage {
|
||||
max-width: calc(100vw - 30em);
|
||||
}
|
||||
|
||||
.beat-stage-container {
|
||||
left: 30em;
|
||||
width: calc(100vw - 30em);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.root-settings {
|
||||
width: calc(100vw - 2em);
|
||||
|
||||
.settings {
|
||||
z-index: 1;
|
||||
width: 28em;
|
||||
background-color: var(--color-bg-light);
|
||||
overflow: scroll;
|
||||
display: inline-block;
|
||||
}
|
||||
.sidebar-visible .root-beat-stage-container {
|
||||
left: 100vw;
|
||||
|
||||
.settings .title {
|
||||
color: var(--color-title-light);
|
||||
text-align: center;
|
||||
}
|
||||
.root-beat-stage-container {
|
||||
|
||||
.sidebar-toggle {
|
||||
z-index: 1;
|
||||
height: 100vh;
|
||||
min-width: 2em;
|
||||
background-color: var(--color-bg-light);
|
||||
left: 0;
|
||||
}
|
||||
.sidebar-visible .root-beat-stage {
|
||||
|
||||
.quick-access-button {
|
||||
right: 0;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&.unclickable {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.beat-stage-container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: left 400ms, width 400ms;
|
||||
}
|
||||
|
||||
.beat-stage {
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
margin: auto;
|
||||
max-width: 100vw;
|
||||
transition: max-width 400ms;
|
||||
padding-left: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
&.vertical-mode .beat-stage {
|
||||
margin: auto auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-left-strip {
|
||||
writing-mode: vertical-rl;
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.sidebar-left-strip > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sidebar-left-tab {
|
||||
transform: rotate(-180deg);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 8px 3px 8px 3px;
|
||||
}
|
||||
|
||||
.sidebar-left-tab.active {
|
||||
background-color: var(--color-bg-medium);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sidebar-add-beat {
|
||||
width: 100%;
|
||||
padding: 8px 3px 8px 3px;
|
||||
}
|
||||
|
||||
.sidebar-add-beat:hover,
|
||||
.sidebar-left-tab:hover:not(.active) {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-ui-neutral-dark);
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
&.sidebar-visible .sidebar {
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
}
|
||||
.sidebar {
|
||||
left: calc(-100vw + 2em);
|
||||
width: 100vw;
|
||||
}
|
||||
.settings {
|
||||
width: calc(100vw - 2em);
|
||||
}
|
||||
.sidebar-visible .beat-stage-container {
|
||||
left: 100vw;
|
||||
}
|
||||
.beat-stage-container {
|
||||
left: 0;
|
||||
}
|
||||
.sidebar-visible .beat-stage {
|
||||
max-width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
<div class="track-main">
|
||||
<div class="track-unit-block">
|
||||
<track-unit v-for="(trackUnit, i) in trackUnits"
|
||||
:key="`tu${ trackIndex }${ i }`"
|
||||
:id="`tu${ trackIndex }${ i }`"
|
||||
class="track-unit"
|
||||
:class="{ spaced: (i + 1) % beat!.timeSigUp.value === 0 }"
|
||||
:sticking-type="trackUnit.stickingType"
|
||||
:type="trackUnit.type"
|
||||
:on="trackUnit.on"
|
||||
@rotate-type="rotateTrackUnit(i)"
|
||||
@mouseup="applyCurrentToolToTrackUnit(i)"
|
||||
@mousedown="applyCurrentToolToTrackUnit(i)"
|
||||
@mouseover="applyCurrentToolToTrackUnit(i)" />
|
||||
@deactivate="deactivateUnit(i)"
|
||||
@toggle="toggle(i)"
|
||||
@apply-tool="applyCurrentToolToTrackUnit(i)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,27 +55,34 @@
|
||||
return units;
|
||||
});
|
||||
|
||||
function toggle(index: number) {
|
||||
if (!track.value) return;
|
||||
track.value.toggleUnit(index);
|
||||
if (track.value.getUnitByIndex(index).on) {
|
||||
applyCurrentToolToTrackUnit(index);
|
||||
}
|
||||
}
|
||||
|
||||
function rotateTrackUnit(index: number) {
|
||||
track.value?.rotateUnit(index);
|
||||
}
|
||||
|
||||
function deactivateUnit(index: number) {
|
||||
track.value?.setStickingType(index, 0);
|
||||
track.value?.setUnitOn(index, false);
|
||||
}
|
||||
|
||||
function applyCurrentToolToTrackUnit(index: number) {
|
||||
if (selectedTool.value === "sticking") {
|
||||
if (selectingUnits.value) {
|
||||
switch (selectedTool.value) {
|
||||
case "sticking":
|
||||
track.value?.setStickingType(index, activeStickingType.value);
|
||||
} else if (deselectingUnits.value) {
|
||||
track.value?.setStickingType(index, 0);
|
||||
}
|
||||
} else if (selectedTool.value === "track-unit-type") {
|
||||
if (selectingUnits.value) {
|
||||
break;
|
||||
case "track-unit-type":
|
||||
track.value?.updateUnit(index, { on: true, type: activeTrackUnitType.value });
|
||||
} else if (deselectingUnits.value) {
|
||||
break;
|
||||
case "eraser":
|
||||
track.value?.setUnitOn(index, false);
|
||||
}
|
||||
} else if (selectedTool.value === "eraser") {
|
||||
if (selectingUnits.value || deselectingUnits.value) {
|
||||
track.value?.setUnitOn(index, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
<div :class="classes"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseover="handleMouseOver"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseout="handleMouseOut"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchend="handleTouchEnd"
|
||||
@contextmenu="() => false">
|
||||
@contextmenu.prevent.stop="() => false">
|
||||
<icon :icon-name="iconName" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,6 +20,7 @@
|
||||
import { useAppStateStore } from "@/AppState";
|
||||
|
||||
const props = defineProps<{
|
||||
id: string,
|
||||
stickingType: number,
|
||||
type: number,
|
||||
on: boolean,
|
||||
@@ -25,17 +28,22 @@
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'rotateType'): true,
|
||||
(e: 'mousedown'): true,
|
||||
(e: 'mouseup'): true,
|
||||
(e: 'toggle'): true,
|
||||
(e: 'deactivate'): true,
|
||||
(e: 'applyTool'): true,
|
||||
}>();
|
||||
|
||||
const {
|
||||
selectingUnits,
|
||||
lastTrackUnit,
|
||||
deselectingUnits,
|
||||
unitMouseStart,
|
||||
} = useAppStateStore();
|
||||
|
||||
let rotationTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let blockNextMouseUp = false;
|
||||
let mouseHeld = false;
|
||||
let movement = 0;
|
||||
|
||||
const classes = computed(() => getClasses({
|
||||
on: props.on,
|
||||
@@ -47,33 +55,56 @@
|
||||
const iconName = computed(() => StickingTypeIconMap[TrackUnitStickingTypeList[props.stickingType] ?? 'none']);
|
||||
|
||||
function handleMouseDown(ev: MouseEvent): void {
|
||||
blockNextMouseUp = false;
|
||||
if (ev.button === 0) {
|
||||
unitMouseStart.value = props.id;
|
||||
selectingUnits.value = true;
|
||||
emit('mousedown');
|
||||
} else if (ev.button === 2) {
|
||||
unitMouseStart.value = props.id;
|
||||
deselectingUnits.value = true;
|
||||
emit('mousedown');
|
||||
} else if (ev.button === 1) {
|
||||
emit('rotateType');
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(ev: MouseEvent): void {
|
||||
if (!blockNextMouseUp && unitMouseStart.value === props.id) {
|
||||
if (ev.button === 0) {
|
||||
emit('toggle');
|
||||
} else if (ev.button === 2) {
|
||||
emit('deactivate');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(ev: MouseEvent): void {
|
||||
if (selectingUnits.value || deselectingUnits.value) {
|
||||
movement += ev.movementX**2 + ev.movementY**2;
|
||||
}
|
||||
if (unitMouseStart.value === props.id && movement > 2**2) {
|
||||
handleMouseOver();
|
||||
blockNextMouseUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseOver(ev?: MouseEvent): void {
|
||||
if (selectingUnits.value) {
|
||||
emit('applyTool');
|
||||
} else if (deselectingUnits.value) {
|
||||
emit('deactivate');
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseOut(ev: MouseEvent): void {
|
||||
movement = 0;
|
||||
if (rotationTimeout) {
|
||||
clearTimeout(rotationTimeout);
|
||||
rotationTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp(ev: MouseEvent): void {
|
||||
if (!blockNextMouseUp) {
|
||||
emit('mouseup');
|
||||
}
|
||||
blockNextMouseUp = false;
|
||||
}
|
||||
|
||||
function handleTouchStart(ev: TouchEvent): void {
|
||||
rotationTimeout = rotationTimeout || setTimeout(() => {
|
||||
rotationTimeout ??= setTimeout(() => {
|
||||
emit('rotateType');
|
||||
rotationTimeout = null;
|
||||
}, 400);
|
||||
@@ -118,15 +149,15 @@
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.track-unit.on.Accent, .track-unit.on.Accent.highlightable:hover {
|
||||
border-color: var(--color-ui-neutral-light);
|
||||
}
|
||||
|
||||
.track-unit.on.highlightable:hover {
|
||||
border-color: var(--color-ui-accent-hover);
|
||||
background-color: var(--color-ui-accent-hover);
|
||||
}
|
||||
|
||||
.track-unit.on.Accent {
|
||||
border-color: var(--color-ui-neutral-light);
|
||||
}
|
||||
|
||||
.track-unit.on.Ghost {
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ import ArrowClockwise from "assets/svgs/arrow-clockwise.svg";
|
||||
import Trash from "assets/svgs/trash.svg";
|
||||
import Snowflake from "assets/svgs/snowflake.svg";
|
||||
import LeftHand from "assets/svgs/LH.png";
|
||||
import Download from "assets/svgs/download.svg";
|
||||
import RightHand from "assets/svgs/RH.png";
|
||||
import LeftFoot from "assets/svgs/LF.png";
|
||||
import RightFoot from "assets/svgs/RF.png";
|
||||
|
||||
export const IconUrlMap = {
|
||||
arrowClockwise: ArrowClockwise,
|
||||
download: Download,
|
||||
list: List,
|
||||
trash: Trash,
|
||||
snowflake: Snowflake,
|
||||
|
||||
Reference in New Issue
Block a user