Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Show a generated artwork when the audio thumbnail is absent #600

Merged
merged 2 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions src/components/AudioThumbnail/AudioThumbnail.vue

This file was deleted.

149 changes: 149 additions & 0 deletions src/components/VAudioThumbnail/VAudioThumbnail.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<template>
<!-- Should be wrapped by a fixed-width parent -->
<div class="relative h-0 w-full pt-full" :title="helpText">
<div class="thumbnail absolute inset-0 bg-yellow">
<img
v-if="audio.thumbnail && ok"
class="h-full w-full object-cover object-center overflow-clip"
:src="audio.thumbnail"
:alt="helpText"
@error="handleError"
/>

<!-- Programmatic thumbnail -->
<svg
v-else
class="h-full w-full"
:viewBox="`0 0 ${canvasSize} ${canvasSize}`"
>
<template v-for="i in dotCount">
<circle
v-for="j in dotCount"
:key="`${i}-${j}`"
class="fill-dark-charcoal"
:cx="offset(j)"
:cy="offset(i)"
:r="radius(i, j)"
/>
</template>
</svg>
</div>
</div>
</template>

<script>
import { ref } from '@nuxtjs/composition-api'

/**
* Displays the cover art for the audio in a square aspect ratio.
*/
export default {
name: 'VAudioThumbnail',
props: {
/**
* the details of the audio whose artwork is to be shown; The properties
* `thumbnail`, `title` and `creator` are used.
*/
audio: {
type: Object,
required: true,
},
},
setup() {
/* Switching */

const ok = ref(true)
const handleError = () => {
ok.value = false
}

/* Math utilities */

/**
* Perform linear interpolation to find a value that is fractionally between
* the low and high limits of the given range.
*
* @param {number} low - the lower limit of the range
* @param {number} high - the upper limit of the range
* @param {number} frac - fraction controlling position of interpolated number
* @returns {number} the interpolated number
*/
const lerp = (low, high, frac) => low + (high - low) * frac

/**
* Interpolate twice to solve the Bézier equation for three points P0, P1
* and P2.
*
* @param {[number, number]} p0 - point #0
* @param {[number, number]} p1 - point #1
* @param {[number, number]} p2 - point #2
* @param {number} frac - the fraction at which to solve the Bézier equation
* @returns {[number,number]} a solution to the 3-point Bézier equation
*/
const doubleLerp = (p0, p1, p2, frac) => [
lerp(lerp(p0[0], p1[0], frac), lerp(p1[0], p2[0], frac), frac),
lerp(lerp(p0[1], p1[1], frac), lerp(p1[1], p2[1], frac), frac),
]

/**
* Find the distance between two points P0 and P1.
*
* @param {[number, number]} p0 - point #0
* @param {[number, number]} p1 - point #1
* @returns {number} the distance between the two points
*/
const dist = (p0, p1) =>
Math.sqrt(Math.pow(p0[0] - p1[0], 2) + Math.pow(p0[1] - p1[1], 2))

/* Artwork */

const dotCount = 10
const canvasSize = 768
const minRadius = 2
const maxRadius = 27

const ctrlPts = Array.from({ length: 4 }, (_, idx) => [
Math.random() * canvasSize,
(idx / 3) * canvasSize,
])

const pointCount = dotCount + 1
const bezierPoints = []
for (let i = 0; i <= pointCount; i++) {
const frac = i / pointCount
const a = doubleLerp(ctrlPts[0], ctrlPts[1], ctrlPts[2], frac)
const b = doubleLerp(ctrlPts[1], ctrlPts[2], ctrlPts[3], frac)
const x = lerp(a[0], b[0], frac)
bezierPoints.push(x)
}

const offset = (i) => {
return i * (canvasSize / (dotCount + 1))
}
const radius = (i, j) => {
const bezierPoint = bezierPoints[i]
const distance = dist([0, bezierPoint], [0, offset(j)])
const maxFeasibleDistance = canvasSize * ((dotCount - 1) / (dotCount + 1))
return lerp(maxRadius, minRadius, distance / maxFeasibleDistance)
}

return {
ok,
handleError,

canvasSize,
dotCount,
offset,
radius,
}
},
computed: {
helpText() {
return this.$t('audio-thumbnail.alt', {
title: this.audio.title,
creator: this.audio.creator,
})
},
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
Story,
} from '@storybook/addon-docs'

import AudioThumbnail from '~/components/AudioThumbnail/AudioThumbnail.vue'
import VAudioThumbnail from '~/components/VAudioThumbnail/VAudioThumbnail.vue'

<Meta
title="Components/Audio thumbnail"
components={AudioThumbnail}
title="Components/VAudioThumbnail"
components={VAudioThumbnail}
argTypes={{
toggle: {
action: 'toggle',
Expand All @@ -21,18 +21,18 @@ import AudioThumbnail from '~/components/AudioThumbnail/AudioThumbnail.vue'
export const Template = (args, { argTypes }) => ({
template: `
<div class="w-30">
<AudioThumbnail v-bind="$props" v-on="$props"/>
<VAudioThumbnail v-bind="$props" v-on="$props"/>
</div>
`,
components: { AudioThumbnail },
components: { VAudioThumbnail },
props: Object.keys(argTypes),
})

# Audio thumbnail
# VAudioThumbnail

<Description of={AudioThumbnail} />
<Description of={VAudioThumbnail} />

<ArgsTable of={AudioThumbnail} />
<ArgsTable of={VAudioThumbnail} />

It does not have any intrinsic width and scales horizontally to take up the full
width of the parent. It also does not have any rounding so the parent layout
Expand Down Expand Up @@ -129,7 +129,7 @@ André Karwath aka [Aka](https://commons.wikimedia.org/wiki/User:Aka)

## Fallback

In the absence of album art, some generic imagery can be shown.
In the absence of album art, a randomised artwork is generated.

<Canvas>
<Story
Expand Down
6 changes: 3 additions & 3 deletions src/components/VAudioTrack/layouts/VGlobalLayout.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="global-track flex flex-row w-full">
<div class="flex-shrink-0">
<AudioThumbnail :audio="audio" />
<VAudioThumbnail :audio="audio" />
<slot name="play-pause" size="medium" />
</div>

Expand All @@ -20,12 +20,12 @@
<script>
import { defineComponent } from '@nuxtjs/composition-api'

import AudioThumbnail from '~/components/AudioThumbnail/AudioThumbnail.vue'
import VAudioThumbnail from '~/components/VAudioThumbnail/VAudioThumbnail.vue'

export default defineComponent({
name: 'VGlobalLayout',
components: {
AudioThumbnail,
VAudioThumbnail,
},
props: ['audio'],
})
Expand Down
6 changes: 3 additions & 3 deletions src/components/VAudioTrack/layouts/VRowLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class="relative flex-shrink-0 rounded-sm overflow-hidden"
:class="isLarge ? 'w-30 me-6' : 'w-20 me-4'"
>
<AudioThumbnail :audio="audio" />
<VAudioThumbnail :audio="audio" />
<div v-if="isSmall" class="absolute bottom-0 end-0">
<slot name="play-pause" size="tiny" />
</div>
Expand Down Expand Up @@ -80,13 +80,13 @@

<script>
import { computed, defineComponent } from '@nuxtjs/composition-api'
import AudioThumbnail from '~/components/AudioThumbnail/AudioThumbnail.vue'
import VAudioThumbnail from '~/components/VAudioThumbnail/VAudioThumbnail.vue'
import VLicense from '~/components/License/VLicense.vue'

export default defineComponent({
name: 'VRowLayout',
components: {
AudioThumbnail,
VAudioThumbnail,
VLicense,
},
props: ['audio', 'size'],
Expand Down