mirror of
https://github.com/curtgrimes/obsidian-metronome-plugin.git
synced 2026-07-22 06:10:24 +00:00
Refactor Metronome.vue. Split into sub components.
This commit is contained in:
parent
f1ce86fc8c
commit
17956e676c
19 changed files with 562 additions and 510 deletions
16
.eslintrc
16
.eslintrc
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
|
|
@ -19,5 +19,19 @@
|
|||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"vue/script-setup-uses-vars": "error"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
"map": [
|
||||
["@", "./src"] //default @ -> ./src alias in Vue, it exists even if vue.config.js is not present
|
||||
/*
|
||||
*... add your own webpack aliases if you have them in vue.config.js/other webpack config file
|
||||
* if you forget to add them, eslint-plugin-import will not throw linting error in .vue imports that contain the webpack alias you forgot to add
|
||||
*/
|
||||
],
|
||||
"extensions": [".vue", ".json", ".js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import esbuild from "esbuild";
|
||||
import * as esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from 'builtin-modules';
|
||||
import vuePlugin from 'esbuild-plugin-vue3';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
||||
"@typescript-eslint/parser": "^5.2.0",
|
||||
"@typescript-eslint/parser": "^5.8.0",
|
||||
"@vapurrmaid/bpm": "^0.1.8",
|
||||
"@vueuse/core": "^7.4.0",
|
||||
"builtin-modules": "^3.2.0",
|
||||
|
|
@ -22,6 +22,8 @@
|
|||
"esbuild-plugin-vue3": "^0.3.0",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"obsidian": "^0.12.17",
|
||||
"stylelint": "^14.1.0",
|
||||
|
|
|
|||
|
|
@ -1,78 +1,78 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
size?: "small" | "medium" | "large";
|
||||
disabled?: boolean;
|
||||
size?: "small" | "medium" | "large";
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="button"
|
||||
:disabled="disabled"
|
||||
:data-size="props.size || 'small'"
|
||||
v-bind="{ size }"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
:disabled="disabled"
|
||||
:data-size="props.size || 'small'"
|
||||
v-bind="{ size }"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button {
|
||||
/* using !important because some themes may try to override these */
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0.25rem !important;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0.2rem;
|
||||
width: 2.5rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
color: var(--text-normal);
|
||||
/* using !important because some themes may try to override these */
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
padding: 0.25rem !important;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
border-radius: 0.2rem;
|
||||
width: 2.5rem;
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
color: var(--text-normal);
|
||||
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&[data-size="medium"] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
&[data-size="medium"] {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
width: 2.5rem;
|
||||
height: 2.3rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
&[data-size="large"] {
|
||||
width: 2.5rem;
|
||||
height: 2.3rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&[data-size="xlarge"] {
|
||||
width: 4rem;
|
||||
height: 3.5rem;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
&[data-size="xlarge"] {
|
||||
width: 4rem;
|
||||
height: 3.5rem;
|
||||
padding: 0.35rem;
|
||||
}
|
||||
|
||||
& > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
& > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-accent-hover);
|
||||
}
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-accent-hover);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&.enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover,
|
||||
&:active,
|
||||
&.enabled {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
25
src/components/Controls.vue
Normal file
25
src/components/Controls.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="controls"><slot /></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
/* background color with opacity */
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--background-secondary);
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { MetronomeCodeBlockParameters } from "../main";
|
||||
import Button from "./Button.vue";
|
||||
import OverlayToggle from "./OverlayToggle.vue";
|
||||
import Controls from "./Controls.vue";
|
||||
import Status from "./Status.vue";
|
||||
import MuteToggle from "./MuteToggle.vue";
|
||||
import MetronomeToggle from "./MetronomeToggle.vue";
|
||||
import { ref, watch, toRefs, onBeforeUnmount } from "vue";
|
||||
import MetronomeIcon from "../components/MetronomeIcon.vue";
|
||||
import StyleLine from "../components/StyleLine.vue";
|
||||
import Visualization from "./visualizations/Visualization.vue";
|
||||
import { ref, watch, toRefs, onBeforeUnmount, CSSProperties } from "vue";
|
||||
import { playTick, playTickUpbeat, playTock, playSynth } from "../sounds";
|
||||
import { useTick } from "../hooks/useTick";
|
||||
import { useParentMarkdownWrapperVisibilityWatcher } from "../hooks/useParentMarkdownWrapperVisibilityWatcher";
|
||||
import { useCSSAnimationSynchronizer } from "../hooks/useCSSAnimationSynchronizer";
|
||||
|
||||
const props = defineProps<{
|
||||
bpm: MetronomeCodeBlockParameters["bpm"];
|
||||
muted: MetronomeCodeBlockParameters["muted"];
|
||||
meter: MetronomeCodeBlockParameters["meter"];
|
||||
size: MetronomeCodeBlockParameters["size"];
|
||||
style: MetronomeCodeBlockParameters["style"];
|
||||
autoStart: MetronomeCodeBlockParameters["autoStart"];
|
||||
instrument: MetronomeCodeBlockParameters["instrument"];
|
||||
tickNotes: MetronomeCodeBlockParameters["tickNotes"];
|
||||
tockNotes: MetronomeCodeBlockParameters["tockNotes"];
|
||||
bpm: MetronomeCodeBlockParameters["bpm"];
|
||||
muted: MetronomeCodeBlockParameters["muted"];
|
||||
meter: MetronomeCodeBlockParameters["meter"];
|
||||
size: MetronomeCodeBlockParameters["size"];
|
||||
style: MetronomeCodeBlockParameters["style"];
|
||||
autoStart: MetronomeCodeBlockParameters["autoStart"];
|
||||
instrument: MetronomeCodeBlockParameters["instrument"];
|
||||
tickNotes: MetronomeCodeBlockParameters["tickNotes"];
|
||||
tockNotes: MetronomeCodeBlockParameters["tockNotes"];
|
||||
}>();
|
||||
|
||||
const metronome = ref<HTMLElement>(null);
|
||||
|
|
@ -29,59 +29,59 @@ const started = ref(props.autoStart ?? true);
|
|||
const tickColor = ref("");
|
||||
const { meter } = toRefs(props);
|
||||
const { doBeat, onBeat, onTick, onTickAlternate, onTock, resetTick } =
|
||||
useTick(meter);
|
||||
useTick(meter);
|
||||
const parentWrapperIsVisible =
|
||||
useParentMarkdownWrapperVisibilityWatcher(metronome);
|
||||
useParentMarkdownWrapperVisibilityWatcher(metronome);
|
||||
|
||||
const haltAnimationStyle = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: metronome,
|
||||
onBeat,
|
||||
const { haltAnimationStyle } = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: metronome,
|
||||
onBeat,
|
||||
});
|
||||
|
||||
// Do sounds
|
||||
onTick(
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTick()
|
||||
: playSynth(props.tickNotes, props.instrument))
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTick()
|
||||
: playSynth(props.tickNotes, props.instrument))
|
||||
);
|
||||
onTickAlternate(
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTickUpbeat()
|
||||
: playSynth(props.tockNotes, props.instrument))
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTickUpbeat()
|
||||
: playSynth(props.tockNotes, props.instrument))
|
||||
);
|
||||
onTock(
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTock()
|
||||
: playSynth(props.tockNotes, props.instrument))
|
||||
() =>
|
||||
!muted.value &&
|
||||
(props.instrument === "click"
|
||||
? playTock()
|
||||
: playSynth(props.tockNotes, props.instrument))
|
||||
);
|
||||
|
||||
const emit = defineEmits(["didCreateInterval"]);
|
||||
const interval = ref(null);
|
||||
|
||||
const startInterval = () =>
|
||||
(interval.value = window.setInterval(
|
||||
doBeat,
|
||||
props.bpm.getBeatDurationSeconds(meter) * 1000
|
||||
));
|
||||
(interval.value = window.setInterval(
|
||||
doBeat,
|
||||
props.bpm.getBeatDurationSeconds(meter) * 1000
|
||||
));
|
||||
|
||||
const stopInterval = () => clearInterval(interval.value);
|
||||
|
||||
watch(
|
||||
[started, parentWrapperIsVisible],
|
||||
() => {
|
||||
if (started.value && parentWrapperIsVisible.value) {
|
||||
startInterval();
|
||||
} else {
|
||||
stopInterval();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
[started, parentWrapperIsVisible],
|
||||
() => {
|
||||
if (started.value && parentWrapperIsVisible.value) {
|
||||
startInterval();
|
||||
} else {
|
||||
stopInterval();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopInterval);
|
||||
|
|
@ -89,188 +89,94 @@ onBeforeUnmount(stopInterval);
|
|||
// Do visuals
|
||||
onTick(() => (tickColor.value = "var(--text-accent)"));
|
||||
onTock(() => (tickColor.value = "var(--text-faint)"));
|
||||
|
||||
const getMetronomeStyle = () =>
|
||||
({
|
||||
"--tick-color": tickColor,
|
||||
"--metronome-duration": `${props.bpm.getBeatDurationSeconds(meter)}s`,
|
||||
...haltAnimationStyle,
|
||||
} as CSSProperties);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="metronome"
|
||||
class="metronome"
|
||||
:style="{
|
||||
'--tick-color': tickColor,
|
||||
'--metronome-duration': `${bpm.getBeatDurationSeconds(meter)}s`,
|
||||
...haltAnimationStyle,
|
||||
}"
|
||||
:data-size="props.size"
|
||||
:data-started="started"
|
||||
>
|
||||
<button
|
||||
class="toggle-on-off-button"
|
||||
:aria-label="started ? 'Stop metronome' : 'Start metronome'"
|
||||
aria-label-position="top"
|
||||
@click="started = !started"
|
||||
></button>
|
||||
<div class="styleContainer">
|
||||
<MetronomeIcon
|
||||
v-if="props.style === 'pendulum'"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: var(--text-normal);
|
||||
"
|
||||
:swinging="started"
|
||||
:on-beat="onBeat"
|
||||
/>
|
||||
<StyleLine
|
||||
v-else-if="props.style === 'line'"
|
||||
:swinging="started"
|
||||
:on-beat="onBeat"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<MuteToggle v-model="muted" :size="props.size" @unmuted="resetTick" />
|
||||
<div class="description">
|
||||
<MetronomeIcon
|
||||
:swinging="started"
|
||||
:on-beat="onBeat"
|
||||
class="microphone-icon"
|
||||
/>
|
||||
<span v-if="bpm.isSuperFast()">🔥</span> {{ bpm }}
|
||||
{{ meter && "·" }} {{ meter }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="metronome"
|
||||
class="metronome"
|
||||
:style="getMetronomeStyle()"
|
||||
:data-size="props.size"
|
||||
:data-started="started"
|
||||
>
|
||||
<OverlayToggle :started="started" @click="started = !started" />
|
||||
<Visualization
|
||||
:visualization="props.style"
|
||||
:started="started"
|
||||
:on-beat="onBeat"
|
||||
/>
|
||||
<Controls>
|
||||
<MuteToggle
|
||||
v-model="muted"
|
||||
:size="props.size"
|
||||
@unmuted="resetTick"
|
||||
/>
|
||||
<Status
|
||||
:started="started"
|
||||
:onBeat="onBeat"
|
||||
:bpm="bpm"
|
||||
:meter="meter"
|
||||
:size="props.size"
|
||||
/>
|
||||
</Controls>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$metronome-resting-background-color: var(--background-primary-alt);
|
||||
.metronome {
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
animation: metronome-pulse var(--metronome-duration) var(--sync-delay, "0s")
|
||||
infinite;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 0.68rem;
|
||||
background-color: $metronome-resting-background-color;
|
||||
color: var(--text-normal);
|
||||
z-index: 0;
|
||||
border-radius: 0.25rem;
|
||||
animation: metronome-pulse var(--metronome-duration) var(--sync-delay, "0s")
|
||||
infinite;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 0.68rem;
|
||||
background-color: $metronome-resting-background-color;
|
||||
color: var(--text-normal);
|
||||
|
||||
.toggle-on-off-button {
|
||||
/* using !important because some themes may try to override these */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: background-color 100ms;
|
||||
&[data-started="false"] {
|
||||
background: var(--scrollbar-bg);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(150, 150, 150, 0.1);
|
||||
}
|
||||
}
|
||||
@keyframes metronome-pulse {
|
||||
0% {
|
||||
background-color: var(--tick-color);
|
||||
}
|
||||
|
||||
&[data-started="false"] {
|
||||
background: var(--scrollbar-bg);
|
||||
animation: none;
|
||||
}
|
||||
100% {
|
||||
background-color: $metronome-resting-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes metronome-pulse {
|
||||
0% {
|
||||
background-color: var(--tick-color);
|
||||
}
|
||||
/* Sizes: ["small" (default), "medium", "large"] */
|
||||
&[data-size="medium"] {
|
||||
height: 6rem;
|
||||
align-items: flex-end;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: $metronome-resting-background-color;
|
||||
}
|
||||
}
|
||||
&[data-size="large"] {
|
||||
height: 15rem;
|
||||
align-items: flex-end;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Sizes: ["small" (default), "medium", "large"] */
|
||||
&[data-size="small"] {
|
||||
.microphone-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="medium"] {
|
||||
height: 6rem;
|
||||
align-items: flex-end;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.microphone-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
height: 15rem;
|
||||
align-items: flex-end;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.microphone-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="xlarge"] {
|
||||
height: 18rem;
|
||||
align-items: flex-end;
|
||||
font-size: 1rem;
|
||||
|
||||
.microphone-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
/* background color with opacity */
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--background-secondary);
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
font-weight: bold;
|
||||
padding: 0 0.25rem 0 0;
|
||||
line-height: 1;
|
||||
color: var(--text-normal);
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.styleContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
&[data-size="xlarge"] {
|
||||
height: 18rem;
|
||||
align-items: flex-end;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useCSSAnimationSynchronizer } from "src/hooks/useCSSAnimationSynchronizer";
|
||||
import { ref } from "vue";
|
||||
const props = defineProps<{
|
||||
swinging?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
|
||||
const metronomeIconElement = ref(null);
|
||||
const pendulum = ref(null);
|
||||
|
||||
const haltAnimationStyle = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: pendulum,
|
||||
onBeat: props.onBeat,
|
||||
observeVisibilityElement: metronomeIconElement,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
ref="metronomeIconElement"
|
||||
class="metronome-icon"
|
||||
>
|
||||
<path
|
||||
d="M9.867,2.646C9.832,2.47 9.695,2.333 9.52,2.295C8.507,2.08 7.494,2.083 6.482,2.295C6.306,2.332 6.168,2.47 6.133,2.646L3.998,13.175C3.971,13.307 4.005,13.445 4.091,13.549C4.176,13.654 4.304,13.715 4.439,13.715L11.561,13.715C11.696,13.715 11.824,13.654 11.909,13.549C11.995,13.445 12.029,13.307 12.002,13.175L9.867,2.646ZM10.492,10.257L11.01,12.815C11.01,12.815 4.99,12.815 4.99,12.815L5.508,10.257L10.492,10.257ZM10.309,9.357L5.691,9.357L6.954,3.122C7.651,3.008 8.348,3.006 9.045,3.121L9.045,3.121L10.309,9.357Z"
|
||||
/>
|
||||
<g transform="matrix(1,0,0,0.972522,4.29531,0.0786472)">
|
||||
<path
|
||||
d="M4.155,9.891L4.155,3.157C4.155,2.902 3.953,2.694 3.705,2.694C3.456,2.694 3.255,2.902 3.255,3.157L3.255,9.891C3.255,10.147 3.456,10.354 3.705,10.354C3.953,10.354 4.155,10.147 4.155,9.891Z"
|
||||
:class="['pendulum', { swinging }]"
|
||||
ref="pendulum"
|
||||
:style="{ ...haltAnimationStyle }"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.metronome-icon {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.metronome-metronome[data-started="false"] .pendulum {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.pendulum {
|
||||
transform-origin: bottom;
|
||||
transform-box: fill-box;
|
||||
|
||||
&.swinging {
|
||||
animation: pendulum-swing calc(var(--metronome-duration) * 2)
|
||||
calc(calc(var(--metronome-duration) / -2) + var(--sync-delay, "0s"))
|
||||
infinite cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pendulum-swing {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { watch } from "vue";
|
||||
import MetronomeIcon from "./MetronomeIcon.vue";
|
||||
import Button from "./Button.vue";
|
||||
import type { MetronomeSize } from "../models/MetronomeSize";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Boolean;
|
||||
size?: MetronomeSize;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:class="{ enabled: props.modelValue }"
|
||||
@click="emit('update:modelValue', !props.modelValue)"
|
||||
:aria-label="props.modelValue ? 'Stop metronome' : 'Start metronome'"
|
||||
v-bind="{ size: props.size }"
|
||||
>
|
||||
<MetronomeIcon :swinging="props.modelValue" :on-beat="props.onBeat" />
|
||||
</Button>
|
||||
</template>
|
||||
|
|
@ -6,30 +6,30 @@ import Button from "./Button.vue";
|
|||
import type { MetronomeSize } from "../models/MetronomeSize";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: Boolean;
|
||||
size: MetronomeSize;
|
||||
modelValue?: Boolean;
|
||||
size: MetronomeSize;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "muted", "unmuted"]);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(modelValue) => {
|
||||
if (modelValue) {
|
||||
emit("muted");
|
||||
} else {
|
||||
emit("unmuted");
|
||||
}
|
||||
}
|
||||
() => props.modelValue,
|
||||
(modelValue) => {
|
||||
if (modelValue) {
|
||||
emit("muted");
|
||||
} else {
|
||||
emit("unmuted");
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:class="{ enabled: props.modelValue }"
|
||||
@click="emit('update:modelValue', !props.modelValue)"
|
||||
:aria-label="props.modelValue ? 'Unmute' : 'Mute'"
|
||||
v-bind="{ size: props.size }"
|
||||
v-html="props.modelValue ? VolumeMuteIcon : VolumeUpIcon"
|
||||
/>
|
||||
</template>
|
||||
<Button
|
||||
:class="{ enabled: props.modelValue }"
|
||||
@click="emit('update:modelValue', !props.modelValue)"
|
||||
:aria-label="props.modelValue ? 'Unmute' : 'Mute'"
|
||||
v-bind="{ size: props.size }"
|
||||
v-html="props.modelValue ? VolumeMuteIcon : VolumeUpIcon"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
35
src/components/OverlayToggle.vue
Normal file
35
src/components/OverlayToggle.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
started?: Boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="overlay-toggle"
|
||||
:aria-label="started ? 'Stop metronome' : 'Start metronome'"
|
||||
aria-label-position="top"
|
||||
></button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.overlay-toggle {
|
||||
/* using !important because some themes may try to override these */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
transition: background-color 100ms;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(150, 150, 150, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
67
src/components/Status.vue
Normal file
67
src/components/Status.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { Meter } from "../models/Meter";
|
||||
import { MetronomeBPM } from "../models/MetronomeBPM";
|
||||
import { MetronomeSize } from "../models/MetronomeSize";
|
||||
import MetronomeVisualization from "./visualizations/MetronomeVisualization.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
started?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
bpm: MetronomeBPM;
|
||||
meter: Meter;
|
||||
size: MetronomeSize;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="status" :data-size="size">
|
||||
<MetronomeVisualization
|
||||
:started="started"
|
||||
:onBeat="onBeat"
|
||||
class="microphone-icon"
|
||||
/>
|
||||
<span v-if="bpm.isSuperFast()">🔥</span> {{ bpm }}
|
||||
{{ meter && "·" }} {{ meter }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status {
|
||||
font-weight: bold;
|
||||
padding: 0 0.25rem 0 0;
|
||||
line-height: 1;
|
||||
color: var(--text-normal);
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[data-size="small"] {
|
||||
.microphone-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="medium"] {
|
||||
.microphone-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="large"] {
|
||||
.microphone-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-size="xlarge"] {
|
||||
.microphone-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useCSSAnimationSynchronizer } from "src/hooks/useCSSAnimationSynchronizer";
|
||||
|
||||
const props = defineProps<{
|
||||
swinging?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
|
||||
const line = ref(null);
|
||||
|
||||
const haltAnimationStyle = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: line,
|
||||
onBeat: props.onBeat,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: relative; width: 100%; height: 100%">
|
||||
<div
|
||||
class="line"
|
||||
:class="{ swinging }"
|
||||
ref="line"
|
||||
:style="{ ...haltAnimationStyle }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$width: 10px;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: $width;
|
||||
background: var(--text-normal);
|
||||
border-radius: 10rem;
|
||||
|
||||
&.swinging {
|
||||
animation: pendulum-swing calc(var(--metronome-duration) * 2)
|
||||
var(--sync-delay, "0s") infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pendulum-swing {
|
||||
0%,
|
||||
100% {
|
||||
left: calc(100% - #{$width});
|
||||
}
|
||||
|
||||
2%,
|
||||
48%,
|
||||
52%,
|
||||
98% {
|
||||
/* If I want to have an effect right before it hits an edge */
|
||||
}
|
||||
|
||||
50% {
|
||||
left: 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
65
src/components/visualizations/LineVisualization.vue
Normal file
65
src/components/visualizations/LineVisualization.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useCSSAnimationSynchronizer } from "../../hooks/useCSSAnimationSynchronizer";
|
||||
|
||||
const props = defineProps<{
|
||||
started?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
|
||||
const line = ref(null);
|
||||
|
||||
const { haltAnimationStyle } = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: line,
|
||||
onBeat: props.onBeat,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: relative; width: 100%; height: 100%">
|
||||
<div
|
||||
class="line"
|
||||
:class="{ started }"
|
||||
ref="line"
|
||||
:style="{ ...haltAnimationStyle }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$width: 10px;
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: $width;
|
||||
background: var(--text-normal);
|
||||
border-radius: 10rem;
|
||||
|
||||
&.started {
|
||||
animation: pendulum-swing calc(var(--metronome-duration) * 2)
|
||||
var(--sync-delay, "0s") infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pendulum-swing {
|
||||
0%,
|
||||
100% {
|
||||
left: calc(100% - #{$width});
|
||||
}
|
||||
|
||||
2%,
|
||||
48%,
|
||||
52%,
|
||||
98% {
|
||||
/* If I want to have an effect right before it hits an edge */
|
||||
}
|
||||
|
||||
50% {
|
||||
left: 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
73
src/components/visualizations/MetronomeVisualization.vue
Normal file
73
src/components/visualizations/MetronomeVisualization.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import { useCSSAnimationSynchronizer } from "../../hooks/useCSSAnimationSynchronizer";
|
||||
import { ref } from "vue";
|
||||
const props = defineProps<{
|
||||
started?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
|
||||
const rootElement = ref(null);
|
||||
const pendulum = ref(null);
|
||||
|
||||
const { haltAnimationStyle } = useCSSAnimationSynchronizer({
|
||||
synchronizeElement: pendulum,
|
||||
onBeat: props.onBeat,
|
||||
observeVisibilityElement: rootElement,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
ref="rootElement"
|
||||
class="metronome-icon"
|
||||
>
|
||||
<path
|
||||
d="M9.867,2.646C9.832,2.47 9.695,2.333 9.52,2.295C8.507,2.08 7.494,2.083 6.482,2.295C6.306,2.332 6.168,2.47 6.133,2.646L3.998,13.175C3.971,13.307 4.005,13.445 4.091,13.549C4.176,13.654 4.304,13.715 4.439,13.715L11.561,13.715C11.696,13.715 11.824,13.654 11.909,13.549C11.995,13.445 12.029,13.307 12.002,13.175L9.867,2.646ZM10.492,10.257L11.01,12.815C11.01,12.815 4.99,12.815 4.99,12.815L5.508,10.257L10.492,10.257ZM10.309,9.357L5.691,9.357L6.954,3.122C7.651,3.008 8.348,3.006 9.045,3.121L9.045,3.121L10.309,9.357Z"
|
||||
/>
|
||||
<g transform="matrix(1,0,0,0.972522,4.29531,0.0786472)">
|
||||
<path
|
||||
d="M4.155,9.891L4.155,3.157C4.155,2.902 3.953,2.694 3.705,2.694C3.456,2.694 3.255,2.902 3.255,3.157L3.255,9.891C3.255,10.147 3.456,10.354 3.705,10.354C3.953,10.354 4.155,10.147 4.155,9.891Z"
|
||||
:class="['pendulum', { started }]"
|
||||
ref="pendulum"
|
||||
:style="{ ...haltAnimationStyle }"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.metronome-icon {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.metronome-metronome[data-started="false"] .pendulum {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.pendulum {
|
||||
transform-origin: bottom;
|
||||
transform-box: fill-box;
|
||||
|
||||
&.started {
|
||||
animation: pendulum-swing calc(var(--metronome-duration) * 2)
|
||||
calc(calc(var(--metronome-duration) / -2) + var(--sync-delay, "0s"))
|
||||
infinite cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pendulum-swing {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
src/components/visualizations/Visualization.vue
Normal file
42
src/components/visualizations/Visualization.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import MetronomeVisualization from "./MetronomeVisualization.vue";
|
||||
import LineVisualization from "./LineVisualization.vue";
|
||||
import { MetronomeCodeBlockParameters } from "../../main";
|
||||
|
||||
const props = defineProps<{
|
||||
visualization: MetronomeCodeBlockParameters["style"];
|
||||
started?: Boolean;
|
||||
onBeat: CallableFunction;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="visualization">
|
||||
<MetronomeVisualization
|
||||
v-if="props.visualization === 'pendulum'"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: var(--text-normal);
|
||||
"
|
||||
:started="props.started"
|
||||
:on-beat="props.onBeat"
|
||||
/>
|
||||
<LineVisualization
|
||||
v-else-if="props.visualization === 'line'"
|
||||
:started="props.started"
|
||||
:on-beat="props.onBeat"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visualization {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,27 +1,6 @@
|
|||
import { computed, ComputedRef, onBeforeUnmount, Ref, ref } from "vue";
|
||||
import { useIntersectionObserver } from "@vueuse/core";
|
||||
|
||||
// Use this to restart a CSS animation after an element
|
||||
// becomes visible again according to when the onBeat
|
||||
// function fires.
|
||||
//
|
||||
// An element that supports this needs to honor the
|
||||
// --sync-delay CSS variable in their animation like this:
|
||||
//
|
||||
// .metronome {
|
||||
// animation:
|
||||
// metronome-pulse
|
||||
// var(--metronome-duration)
|
||||
// var(--sync-delay, "0s")
|
||||
// infinite;
|
||||
// }
|
||||
//
|
||||
// This returns an object that should be applied to the
|
||||
// element as a style. Example:
|
||||
//
|
||||
// const haltAnimationStyle = useCSSAnimationSynchronizer(metronome, onBeat);
|
||||
// <div class="metronome" :style={...haltAnimationStyle}></div>
|
||||
|
||||
/**
|
||||
* Restart a CSS animation after an element becomes visible again according to when the onBeat
|
||||
* function fires.
|
||||
|
|
@ -55,7 +34,7 @@ export function useCSSAnimationSynchronizer({
|
|||
synchronizeElement: Ref<HTMLElement>;
|
||||
onBeat: CallableFunction;
|
||||
observeVisibilityElement?: Ref<HTMLElement>;
|
||||
}): ComputedRef<object> {
|
||||
}): { haltAnimationStyle: ComputedRef<object> } {
|
||||
// If we weren't given an element to observe visibility on, default to the given
|
||||
// elementWithAnimationToSynchronize. This will be different elements in the case
|
||||
// that we want to synchronize something inside an SVG (IntersectionObserver doesn't
|
||||
|
|
@ -107,5 +86,5 @@ export function useCSSAnimationSynchronizer({
|
|||
|
||||
onBeforeUnmount(stop);
|
||||
|
||||
return haltAnimationStyle;
|
||||
return { haltAnimationStyle };
|
||||
}
|
||||
|
|
|
|||
13
src/main.ts
13
src/main.ts
|
|
@ -1,15 +1,15 @@
|
|||
import { MarkdownPostProcessor, MarkdownRenderChild, Plugin } from "obsidian";
|
||||
import Metronome from "./components/Metronome.vue";
|
||||
import Metronome from "@/components/Metronome.vue";
|
||||
import { Frequency } from "tone/build/esm/core/type/Units";
|
||||
import { MetronomeBPM } from "@/models/MetronomeBPM";
|
||||
import { App, createApp } from "vue";
|
||||
import { Meter } from "./models/Meter";
|
||||
import { MetronomeSize, isMetronomeSize } from "./models/MetronomeSize";
|
||||
import { Meter } from "@/models/Meter";
|
||||
import { MetronomeSize, isMetronomeSize } from "@/models/MetronomeSize";
|
||||
import {
|
||||
MetronomeInstrument,
|
||||
isMetronomeInstrument,
|
||||
} from "./models/MetronomeInstrument";
|
||||
import { MetronomeStyle, isMetronomeStyle } from "./models/MetronomeStyle";
|
||||
import { Frequency } from "tone/build/esm/core/type/Units";
|
||||
import { MetronomeBPM } from "./models/MetronomeBPM";
|
||||
import { MetronomeStyle, isMetronomeStyle } from "@/models/MetronomeStyle";
|
||||
|
||||
export interface MetronomeCodeBlockParameters {
|
||||
bpm: MetronomeBPM;
|
||||
|
|
@ -59,6 +59,7 @@ export default class MetronomePlugin extends Plugin {
|
|||
}
|
||||
|
||||
getCodeBlockParameters(src: string): MetronomeCodeBlockParameters {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const values: { [key: string]: any } = {};
|
||||
|
||||
src.split("\n")
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
10
vue-shim.d.ts
vendored
Normal file
10
vue-shim.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// I have no idea how this code works, but I found it on the internet
|
||||
|
||||
// For some odd reason, TypeScript wasn't able to locate *.vue files such as '@/App.vue' in the index.ts file
|
||||
// So I found this on Stackoverflow https://stackoverflow.com/questions/42002394/importing-vue-components-in-typescript-file
|
||||
// and it works for now
|
||||
|
||||
declare module "*.vue" {
|
||||
import Vue from "vue";
|
||||
export default Vue;
|
||||
}
|
||||
Loading…
Reference in a new issue