Refactor Metronome.vue. Split into sub components.

This commit is contained in:
Curt Grimes 2021-12-26 01:12:47 -06:00
parent f1ce86fc8c
commit 17956e676c
19 changed files with 562 additions and 510 deletions

View file

@ -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"]
}
}
}
}

View file

@ -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';

View file

@ -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",

View file

@ -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>

View 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>

View file

@ -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 && "&middot;" }} {{ 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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
View 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 && "&middot;" }} {{ 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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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 };
}

View file

@ -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")

View file

@ -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
View 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;
}