This repository has been archived by the owner on Feb 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { | ||
ArgsTable, | ||
Canvas, | ||
Description, | ||
Meta, | ||
Story, | ||
} from '@storybook/addon-docs' | ||
|
||
import Waveform from '~/components/AudioTrack/Waveform.vue' | ||
|
||
<Meta title="Components/Waveform" components={Waveform} /> | ||
|
||
export const Template = (args, { argTypes }) => ({ | ||
template: `<Waveform class="w-full h-30" v-bind="$props" v-on="$props"/>`, | ||
components: { Waveform }, | ||
props: Object.keys(argTypes), | ||
}) | ||
|
||
# Waveform | ||
|
||
<Description of={Waveform} /> | ||
|
||
<ArgsTable of={Waveform} /> | ||
|
||
## Sampling | ||
|
||
The waveform can automatically upsample and downsample the peaks data to the | ||
required number of points, calculated using a combination of `barWidth` and | ||
`barGap` values. | ||
|
||
### Upsampling | ||
|
||
Here 9 points are upsampled to as many as required to fill the viewport. | ||
|
||
<Canvas> | ||
<Story | ||
name="Upsampling" | ||
args={{ | ||
peaks: [0.5, 1, 0.5, 0, 0.5, 1, 0.5, 0, 0.5], // triangular wave with 9 points | ||
percentage: 0.33, | ||
}} | ||
> | ||
{Template.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
### Downsampling | ||
|
||
Here 1000 points are downsampled to as many as required to fill the viewport. | ||
|
||
<Canvas> | ||
<Story | ||
name="Downsampling" | ||
args={{ | ||
peaks: Array.from( | ||
{ length: 1000 }, | ||
(_, k) => 0.5 * Math.sin((k * 2 * Math.PI) / 500) + 0.5 | ||
), // sine wave with 1000 points | ||
percentage: 0.67, | ||
}} | ||
> | ||
{Template.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
## Sizing | ||
|
||
The waveform always takes the height and width of its container. | ||
|
||
- **Height:** the bars will elongate proportionally to scale vertically | ||
- **Width:** the number of samples will be adjusted to scale horizontally | ||
|
||
Thus the `barWidth` and `barGap` will be maintained even in the case of | ||
horizontal scaling. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<template> | ||
<div class="waveform bg-dark-charcoal-04"> | ||
<svg | ||
class="w-full h-full" | ||
xmlns="http://www.w3.org/2000/svg" | ||
:viewBox="viewBox" | ||
preserveAspectRatio="none" | ||
> | ||
<rect | ||
class="fill-yellow" | ||
x="0" | ||
y="0" | ||
:width="progressBarWidth" | ||
:height="tallestPeak" | ||
/> | ||
<rect | ||
v-for="(peak, index) in normalizedPeaks" | ||
:key="index" | ||
class="transform origin-bottom" | ||
:class="[ | ||
spaceBefore(index) < progressBarWidth | ||
? 'fill-black' | ||
: 'fill-dark-charcoal-20', | ||
]" | ||
:x="spaceBefore(index)" | ||
:y="spaceAbove(peak)" | ||
:width="barWidth" | ||
:height="peak" | ||
/> | ||
</svg> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
import { upsampleArray, downsampleArray } from '~/utils/resampling.js' | ||
/** | ||
* Renders an SVG representation of the waveform given a list of heights for the | ||
* bars. | ||
*/ | ||
export default { | ||
name: 'Waveform', | ||
props: { | ||
/** | ||
* an array of heights of the bars; The waveform will be generated with | ||
* bars of random length if the prop is not provided. | ||
*/ | ||
peaks: { | ||
type: Array, | ||
required: false, | ||
default: () => Array.from({ length: 100 }, () => Math.random()), | ||
validator: (val) => val.every((item) => item >= 0 && item <= 1), | ||
}, | ||
/** | ||
* the percentage of the graph that has been played; This represents the | ||
* seekbar of the audio player. | ||
*/ | ||
percentage: { | ||
type: Number, | ||
validator: (val) => val >= 0 && val <= 100, | ||
default: 0, | ||
}, | ||
}, | ||
data: () => ({ | ||
barWidth: 2, // px | ||
barGap: 2, // px | ||
waveformWidth: 100, // dummy start value | ||
observer: null, // ResizeObserver | ||
}), | ||
computed: { | ||
peakCount() { | ||
return Math.floor( | ||
(this.waveformWidth - this.barGap) / (this.barWidth + this.barGap) | ||
) | ||
}, | ||
normalizedPeaks() { | ||
if (this.peaks.length < this.peakCount) { | ||
return upsampleArray(this.peaks, this.peakCount) | ||
} else if (this.peaks.length > this.peakCount) { | ||
return downsampleArray(this.peaks, this.peakCount) | ||
} | ||
return this.peaks | ||
}, | ||
tallestPeak() { | ||
return Math.max(...this.normalizedPeaks) | ||
}, | ||
viewBox() { | ||
return `0 0 ${this.waveformWidth} 1` | ||
}, | ||
progressBarWidth() { | ||
return this.waveformWidth * this.percentage | ||
}, | ||
}, | ||
async mounted() { | ||
this.updateWaveformWidth() | ||
this.observer = new ResizeObserver(this.updateWaveformWidth) | ||
this.observer.observe(this.$el) | ||
}, | ||
beforeDestroy() { | ||
this.observer.disconnect() | ||
}, | ||
methods: { | ||
spaceBefore(index = this.peakCount) { | ||
return index * this.barWidth + (index + 1) * this.barGap | ||
}, | ||
spaceAbove(peak) { | ||
return 1 - peak | ||
}, | ||
updateWaveformWidth() { | ||
this.waveformWidth = this.$el.clientWidth | ||
}, | ||
}, | ||
} | ||
</script> |