|
<script lang="ts"> |
|
import { afterUpdate, onMount } from 'svelte'; |
|
import { fade } from 'svelte/transition'; |
|
import { audioBlob, notesImage, style } from './stores'; |
|
import { styles } from './config.json'; |
|
|
|
let section: HTMLElement; |
|
|
|
let currentTime: number; |
|
let duration: number; |
|
let paused = true; |
|
|
|
let player: HTMLDivElement; |
|
let visualisation: HTMLImageElement; |
|
let imageWidth: number; |
|
let imageHeight: number; |
|
|
|
const updateDimensions = (): void => { |
|
imageWidth = visualisation && visualisation.clientWidth; |
|
imageHeight = visualisation && visualisation.clientHeight; |
|
}; |
|
|
|
onMount(() => { |
|
updateDimensions(); |
|
|
|
if ('mediaSession' in navigator) { |
|
navigator.mediaSession.setActionHandler('play', () => (paused = false)); |
|
navigator.mediaSession.setActionHandler('pause', () => (paused = true)); |
|
navigator.mediaSession.setActionHandler('stop', () => { |
|
paused = true; |
|
currentTime = 0; |
|
}); |
|
} |
|
|
|
window.scrollTo({ top: section.offsetTop, behavior: 'smooth' }); |
|
}); |
|
|
|
afterUpdate((): void => { |
|
updateDimensions(); |
|
}); |
|
|
|
const mouseMove = (event: MouseEvent): void => { |
|
if (!duration) { |
|
return; |
|
} |
|
|
|
if (!event.buttons) { |
|
return; |
|
} |
|
|
|
const { left, right } = player.getBoundingClientRect(); |
|
currentTime = (duration * (event.clientX - left)) / (right - left); |
|
}; |
|
|
|
const touchMove = (event: TouchEvent): void => { |
|
if (!duration) { |
|
return; |
|
} |
|
|
|
const { left, right } = player.getBoundingClientRect(); |
|
currentTime = (duration * (event.touches[0].clientX - left)) / (right - left); |
|
}; |
|
|
|
const keyDown = (event: KeyboardEvent): void => { |
|
event.preventDefault(); |
|
|
|
if (event.code === 'Space') { |
|
paused = !paused; |
|
} |
|
if (event.code === 'ArrowLeft') { |
|
currentTime = currentTime >= 1 ? currentTime - 1 : 0; |
|
} |
|
if (event.code === 'ArrowRight') { |
|
currentTime = currentTime <= duration - 1 ? currentTime + 1 : duration; |
|
} |
|
}; |
|
</script> |
|
|
|
<section bind:this={section} transition:fade> |
|
<img class="notes" src={$notesImage} alt="" bind:this={visualisation} /> |
|
<div |
|
bind:this={player} |
|
class="player" |
|
style:width={imageWidth + 'px'} |
|
style:height={imageHeight + 'px'} |
|
on:mousemove={mouseMove} |
|
on:touchmove|preventDefault={touchMove} |
|
on:keydown={keyDown} |
|
on:click={() => (paused = !paused)} |
|
tabindex="0" |
|
> |
|
<audio bind:currentTime bind:duration bind:paused src={$audioBlob} /> |
|
<div |
|
class="handle" |
|
style:transform="translate({Math.min(imageWidth * (currentTime / (duration - 0.9)), imageWidth)}px, -2%)" |
|
/> |
|
{#if paused} |
|
<img |
|
class="play-button" |
|
src="static/play.svg" |
|
alt="Play button" |
|
draggable="false" |
|
transition:fade |
|
style:width={imageHeight > 100 ? '20%' : '7.5%'} |
|
/> |
|
{/if} |
|
</div> |
|
<a href={$audioBlob} download={`${styles[$style]} Composition - AI Guru ft. Hugging Face.wav`} class="download" |
|
>Download</a |
|
> |
|
</section> |
|
|
|
<style> |
|
section { |
|
display: flex; |
|
flex-direction: column; |
|
position: relative; |
|
border: 2px solid hsl(0 0% 80%); |
|
border-radius: 0.375rem; |
|
padding: 1rem; |
|
} |
|
|
|
.player { |
|
position: absolute; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
cursor: pointer; |
|
} |
|
.notes { |
|
width: min(100%, 512px); |
|
margin: auto; |
|
box-shadow: 0 0 5px 0.1px hsl(210, 10%, 20%); |
|
} |
|
|
|
audio { |
|
width: 100%; |
|
margin: 1rem auto; |
|
} |
|
.play-button { |
|
position: absolute; |
|
left: 50%; |
|
top: 50%; |
|
width: 20%; |
|
aspect-ratio: 1 / 1; |
|
transform: translate(-50%, -50%); |
|
filter: drop-shadow(0 0 5px black); |
|
pointer-events: none; |
|
cursor: pointer; |
|
} |
|
.handle { |
|
position: absolute; |
|
left: 0; |
|
top: 0; |
|
height: 104%; |
|
width: 0.2rem; |
|
border-radius: 0.1rem; |
|
background-color: white; |
|
cursor: pointer; |
|
transform: translate(0, -2%); |
|
} |
|
|
|
a.download { |
|
display: block; |
|
font-size: 1.2rem; |
|
font-family: 'Lato', sans-serif; |
|
font-weight: 700; |
|
color: hsl(0 0% 97%); |
|
background: transparent; |
|
border: 3px solid hsl(0 0% 97%); |
|
border-radius: 0.375rem; |
|
padding: 0.5rem 1rem; |
|
cursor: pointer; |
|
margin: 1rem auto auto; |
|
} |
|
|
|
@media (min-width: 600px) { |
|
section { |
|
padding: 2rem; |
|
} |
|
} |
|
</style> |
|
|