Skip to content

Commit

Permalink
Add new "dots" style
Browse files Browse the repository at this point in the history
Move PulseVisualization into its own component
  • Loading branch information
curtgrimes committed Dec 27, 2021
1 parent 17956e6 commit efb5a9c
Show file tree
Hide file tree
Showing 10 changed files with 332 additions and 43 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ These are all the options you can change when creating a metronome. Only the `bp
| `muted` | `yes` or `no` | Whether or not the metronome's audio starts muted. | `yes` |
| `autoStart` | `yes` or `no` | Whether or not the metronome starts flashing visually right away. If `autoStart` is `yes` and `muted` is `no`, then the metronome's sound will also start playing immediately. | `yes` |
| `size` | `small`, `medium`, `large`, or `xlarge` | Control the size of the metronome in the note. | `small` |
| `style` | `pulse`, `pendulum`, `line` | Control the style of the metronome. `pulse` makes the whole area flash color. `pendulum` shows an illustration of a metronome with a swinging pendulum (works best with `large` and up sizes). `line` shows a vertical line moving left and right (works best with `large` and up sizes). | `pulse` |
| `style` | `pulse`, `pendulum`, `line`, `dots` | Control the style of the metronome. `pulse` makes the whole area flash color. `pendulum` shows an illustration of a metronome with a swinging pendulum. `line` shows a vertical line moving left and right. `dots` shows dots. Only `pulse` is available when `size` is `small`. The other styles are only available on `size` = `medium` and up. | `pulse` |
| `instrument` | `click`, `beep`, `AMSynth`, `DuoSynth`, `FMSynth`, `MembraneSynth`, `MetalSynth`, `MonoSynth`, or `PluckSynth` | Change the metronome's instrument. | `click` |
| `tickNotes` | string | This determines the note(s) played on the **downbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. Has no effect when `instrument` is `click`. | `C6`<br/>`F5,A5,C5` |
| `tockNotes` | string | This determines the note(s) played on the **upbeat**, specified in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation). (For example, middle C is C4.) Multiple tones can be played at the same time by providing a comma-separated list of tones. If there is no meter, every beat is considered a downbeat. Has no effect when `instrument` is `click`. | `C5`<br/>`F4,A4,C4 ` |
Expand Down Expand Up @@ -219,6 +219,21 @@ style: line

![](images/demo-8.gif)

### `dots` style

````markdown
```metronome
bpm: 86
meter: 3/4
size: large
style: dots
```
````

_Theme: [Solarized](https://github.com/Slowbad/obsidian-solarized) (dark)_

![](images/demo-style-dots.gif)

### Works with themes

The metronome works great with Obsidian's community themes in light or dark mode.
Expand Down
Binary file added images/demo-style-dots.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
"typescript": "4.4.4",
"vue": "^3.2.26"
}
}
}
22 changes: 21 additions & 1 deletion src/components/Controls.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
<script setup lang="ts">
import PulseVisualization from "./visualizations/PulseVisualization.vue";
const props = defineProps<{
started: boolean;
onBeat: CallableFunction;
onTick: CallableFunction;
onTock: CallableFunction;
}>();
</script>

<template>
<div class="controls"><slot /></div>
<div class="controls">
<slot />
<PulseVisualization
style="opacity: 0.3"
:started="props.started"
:on-beat="props.onBeat"
:on-tick="props.onTick"
:on-tock="props.onTock"
/>
</div>
</template>

<style lang="scss" scoped>
Expand Down
71 changes: 37 additions & 34 deletions src/components/Metronome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ import Controls from "./Controls.vue";
import Status from "./Status.vue";
import MuteToggle from "./MuteToggle.vue";
import Visualization from "./visualizations/Visualization.vue";
import { ref, watch, toRefs, onBeforeUnmount, CSSProperties } from "vue";
import {
ref,
watch,
toRefs,
onBeforeUnmount,
CSSProperties,
computed,
Ref,
StyleValue,
} from "vue";
import { playTick, playTickUpbeat, playTock, playSynth } from "../sounds";
import { useTick } from "../hooks/useTick";
import { useParentMarkdownWrapperVisibilityWatcher } from "../hooks/useParentMarkdownWrapperVisibilityWatcher";
Expand All @@ -26,10 +35,16 @@ const props = defineProps<{
const metronome = ref<HTMLElement>(null);
const muted = ref(props.muted);
const started = ref(props.autoStart ?? true);
const tickColor = ref("");
const { meter } = toRefs(props);
const { doBeat, onBeat, onTick, onTickAlternate, onTock, resetTick } =
useTick(meter);
const {
doBeat,
onBeat,
onTick,
onTickAlternate,
onTock,
resetTick,
currentBeat,
} = useTick(meter);
const parentWrapperIsVisible =
useParentMarkdownWrapperVisibilityWatcher(metronome);
Expand Down Expand Up @@ -85,24 +100,18 @@ watch(
);
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="getMetronomeStyle()"
:style="{
'--metronome-duration': `${props.bpm.getBeatDurationSeconds(
meter
)}s`,
...haltAnimationStyle,
}"
:data-size="props.size"
:data-started="started"
>
Expand All @@ -111,8 +120,18 @@ const getMetronomeStyle = () =>
:visualization="props.style"
:started="started"
:on-beat="onBeat"
:meter="props.meter"
:current-beat="currentBeat"
:size="props.size"
:on-tick="onTick"
:on-tock="onTock"
/>
<Controls>
<Controls
:started="started"
:on-beat="onBeat"
:on-tick="onTick"
:on-tock="onTock"
>
<MuteToggle
v-model="muted"
:size="props.size"
Expand All @@ -134,8 +153,7 @@ $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;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
Expand All @@ -145,21 +163,6 @@ $metronome-resting-background-color: var(--background-primary-alt);
background-color: $metronome-resting-background-color;
color: var(--text-normal);
&[data-started="false"] {
background: var(--scrollbar-bg);
animation: none;
}
@keyframes metronome-pulse {
0% {
background-color: var(--tick-color);
}
100% {
background-color: $metronome-resting-background-color;
}
}
/* Sizes: ["small" (default), "medium", "large"] */
&[data-size="medium"] {
height: 6rem;
Expand Down
138 changes: 138 additions & 0 deletions src/components/visualizations/DotsVisualization.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { computed, ref } from "vue";
import { useCSSAnimationSynchronizer } from "../../hooks/useCSSAnimationSynchronizer";
import { Meter } from "../../models/Meter";
import { MetronomeSize } from "../../models/MetronomeSize";
const props = defineProps<{
started?: Boolean;
onBeat: CallableFunction;
meter?: Meter;
currentBeat: number;
size: MetronomeSize;
}>();
const line = ref(null);
const { haltAnimationStyle } = useCSSAnimationSynchronizer({
synchronizeElement: line,
onBeat: props.onBeat,
});
// We don't want to show a lot of dots
const MAX_DOTS = 12;
const dotsCount = computed(() =>
props.meter?.isValid() && props.meter.upper <= MAX_DOTS
? props.meter.upper
: 2
);
const activeIndex = computed(() => props.currentBeat % dotsCount.value);
</script>

<template>
<div
v-if="props.size !== 'small'"
class="dots"
:style="{ '--dots-count': dotsCount }"
>
<div
v-for="x in dotsCount"
:key="x"
class="dot-wrap"
:data-size="props.size"
>
<div class="dot-wrap-width-constraint">
<div
:class="[
'dot',
{
active: started && x === activeIndex + 1,
downbeat: x === 1,
},
]"
></div>
</div>
</div>
</div>
</template>

<style lang="scss" scoped>
.dots {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
max-width: 100%;
margin: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0 5%;
.dot-wrap {
max-height: 100%;
display: flex;
align-items: center;
flex-grow: 0;
&[data-size="medium"] {
--active-scale: 0.6;
--inactive-scale: 0.35;
width: calc(20% / var(--dots-count, 1));
}
&[data-size="large"],
&[data-size="xlarge"] {
--active-scale: 1;
--inactive-scale: 0.85;
width: calc(80% / var(--dots-count, 1));
}
.dot-wrap-width-constraint {
width: calc(100% - calc(80% / var(--dots-count, 1)));
margin: 0 auto;
aspect-ratio: 1;
display: flex;
align-items: center;
.dot {
width: 100%;
max-height: 100%;
background: var(--text-faint);
aspect-ratio: 1;
border-radius: 100%;
transform: scale(var(--inactive-scale, 0.5));
&.active {
background: var(--text-accent);
box-shadow: 0 0 0 0.5rem var(--background-primary-alt);
transform: scale(var(--active-scale, 1));
&.downbeat {
box-shadow: 0 0 0 0.5rem var(--background-primary-alt),
0 0 0 0.75rem var(--text-accent),
0 0 0 0.75rem var(--text-faint);
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: scale(0.5);
border-radius: 100%;
background-color: var(--background-primary-alt);
}
}
}
}
}
}
}
</style>
63 changes: 63 additions & 0 deletions src/components/visualizations/PulseVisualization.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script setup lang="ts">
import { useCSSAnimationSynchronizer } from "../../hooks/useCSSAnimationSynchronizer";
import { ref } from "vue";
const props = defineProps<{
started: boolean;
onBeat: CallableFunction;
onTick: CallableFunction;
onTock: CallableFunction;
}>();
const rootElement = ref(null);
const tickColor = ref("");
const { haltAnimationStyle } = useCSSAnimationSynchronizer({
synchronizeElement: rootElement,
onBeat: props.onBeat,
});
props.onTick(() => (tickColor.value = "var(--text-accent)"));
props.onTock(() => (tickColor.value = "var(--text-faint)"));
</script>

<template>
<div
class="pulse"
ref="rootElement"
:style="{
'--tick-color': tickColor,
}"
:data-started="props.started"
></div>
</template>

<style lang="scss" scoped>
$metronome-resting-background-color: var(--background-primary-alt);
.pulse {
z-index: -1;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: $metronome-resting-background-color;
animation: metronome-pulse var(--metronome-duration) var(--sync-delay, "0s")
infinite;
@keyframes metronome-pulse {
0% {
background-color: var(--tick-color);
}
65%,
100% {
background-color: $metronome-resting-background-color;
}
}
&[data-started="false"] {
background: var(--scrollbar-bg);
animation: none;
}
}
</style>
Loading

0 comments on commit efb5a9c

Please sign in to comment.