You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
5.4 KiB
237 lines
5.4 KiB
<script lang="ts">
|
|
/**
|
|
* Tocadiscos: el disco gira mientras suena y el brazo se posa al empezar.
|
|
*
|
|
* No es un adorno suelto: está atado al reproductor de verdad. El plato gira
|
|
* cuando suena algo de este contexto y se detiene al pausar, así que el
|
|
* estado de la música se ve desde el otro lado de la habitación.
|
|
*
|
|
* El disco es un botón, no un `<div>` con un manejador: se enfoca con
|
|
* teclado, se activa con Enter o espacio y se anuncia por lo que hace. Aun
|
|
* así los controles normales siguen debajo, porque nadie tiene por qué
|
|
* adivinar que una imagen redonda se pulsa.
|
|
*
|
|
* El giro es una animación CSS que se pausa con `animation-play-state`, no un
|
|
* bucle en JavaScript: así lo mueve el compositor del navegador y no cuesta
|
|
* nada aunque esté sonando media hora.
|
|
*/
|
|
import { reproductor, type PistaReproductor } from '$lib/player/player.svelte';
|
|
|
|
interface Props {
|
|
/** Cola que carga el disco al pulsarlo. */
|
|
pistas: PistaReproductor[];
|
|
/** Imagen que hace de etiqueta central. */
|
|
portada: string;
|
|
/** Qué se está poniendo, para el nombre accesible del botón. */
|
|
titulo: string;
|
|
}
|
|
|
|
let { pistas, portada, titulo }: Props = $props();
|
|
|
|
/** Suena algo de este contexto (no de otra parte del sitio). */
|
|
const activo = $derived(pistas.some((p) => reproductor.esActual(p.id)));
|
|
const girando = $derived(activo && reproductor.sonando);
|
|
|
|
function alternar() {
|
|
if (activo) {
|
|
reproductor.alternar();
|
|
return;
|
|
}
|
|
reproductor.reproducirLista(pistas, 0);
|
|
}
|
|
</script>
|
|
|
|
<div class="tocadiscos" class:sonando={girando}>
|
|
<div class="plato">
|
|
<button
|
|
type="button"
|
|
class="disco"
|
|
onclick={alternar}
|
|
aria-pressed={girando}
|
|
aria-label={girando ? `Pausar ${titulo}` : `Reproducir ${titulo}`}
|
|
>
|
|
<span class="surcos"></span>
|
|
<img class="etiqueta" src={portada} alt="" />
|
|
<span class="eje"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- El brazo es decorativo: lo que se pulsa es el disco. -->
|
|
<div class="brazo" aria-hidden="true">
|
|
<span class="pivote"></span>
|
|
<span class="varilla"></span>
|
|
<span class="capsula"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.tocadiscos {
|
|
position: relative;
|
|
width: min(100%, 26rem);
|
|
aspect-ratio: 1;
|
|
}
|
|
|
|
.plato {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle at 50% 50%, var(--c-bg-sunken) 0%, var(--c-bg-raised) 70%);
|
|
box-shadow:
|
|
0 40px 80px -40px rgb(0 0 0 / 0.9),
|
|
inset 0 0 0 1px var(--c-border);
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
/* El vinilo se queda oscuro en los dos temas: un disco es negro. */
|
|
.disco {
|
|
position: relative;
|
|
width: 88%;
|
|
height: 88%;
|
|
border: none;
|
|
border-radius: 50%;
|
|
padding: 0;
|
|
background: radial-gradient(
|
|
circle at 50% 50%,
|
|
oklch(0.14 0.03 264) 0%,
|
|
oklch(0.09 0.02 264) 70%
|
|
);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
/*
|
|
* El giro real de un LP: una vuelta cada 1,8 s. Está siempre declarado y
|
|
* solo se pausa, para que al reanudar continúe donde estaba en vez de
|
|
* saltar al principio.
|
|
*/
|
|
animation: girar 1.8s linear infinite;
|
|
animation-play-state: paused;
|
|
}
|
|
|
|
.tocadiscos.sonando .disco {
|
|
animation-play-state: running;
|
|
}
|
|
|
|
@keyframes girar {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Los surcos van anchos a propósito: unos finos, girando a la velocidad
|
|
* real, producen un parpadeo desagradable en pantalla.
|
|
*/
|
|
.surcos {
|
|
position: absolute;
|
|
inset: 6%;
|
|
border-radius: 50%;
|
|
background: repeating-radial-gradient(
|
|
circle at 50% 50%,
|
|
rgb(255 255 255 / 0.045) 0 1px,
|
|
transparent 1px 5px
|
|
);
|
|
}
|
|
|
|
/* Un brillo asimétrico: sin él el disco parece un círculo plano. */
|
|
.disco::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
background: conic-gradient(
|
|
from 210deg,
|
|
transparent 0deg,
|
|
rgb(255 255 255 / 0.07) 24deg,
|
|
transparent 60deg,
|
|
transparent 180deg,
|
|
rgb(255 255 255 / 0.05) 208deg,
|
|
transparent 244deg
|
|
);
|
|
}
|
|
|
|
.etiqueta {
|
|
position: relative;
|
|
z-index: 1;
|
|
width: 40%;
|
|
height: 40%;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.eje {
|
|
position: absolute;
|
|
z-index: 2;
|
|
width: 3.2%;
|
|
height: 3.2%;
|
|
border-radius: 50%;
|
|
background: var(--c-bg);
|
|
box-shadow: 0 0 0 1px rgb(255 255 255 / 0.15);
|
|
}
|
|
|
|
.brazo {
|
|
position: absolute;
|
|
top: 4%;
|
|
right: 2%;
|
|
width: 46%;
|
|
height: 46%;
|
|
transform-origin: 88% 12%;
|
|
transform: rotate(-24deg);
|
|
transition: transform 0.9s cubic-bezier(0.33, 1, 0.68, 1);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Al sonar, el brazo se posa sobre el disco. */
|
|
.tocadiscos.sonando .brazo {
|
|
transform: rotate(6deg);
|
|
}
|
|
|
|
.pivote {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 22%;
|
|
height: 22%;
|
|
border-radius: 50%;
|
|
background: var(--c-border);
|
|
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
|
|
}
|
|
|
|
.varilla {
|
|
position: absolute;
|
|
top: 10%;
|
|
right: 10%;
|
|
width: 4px;
|
|
height: 86%;
|
|
border-radius: 2px;
|
|
background: linear-gradient(to bottom, var(--c-text-muted), var(--c-border));
|
|
transform-origin: top center;
|
|
transform: rotate(28deg);
|
|
}
|
|
|
|
.capsula {
|
|
position: absolute;
|
|
bottom: 4%;
|
|
left: 34%;
|
|
width: 12%;
|
|
height: 8%;
|
|
border-radius: 2px;
|
|
background: var(--c-accent);
|
|
transform: rotate(28deg);
|
|
}
|
|
|
|
/*
|
|
* Con movimiento reducido no gira ni se mueve el brazo, pero el brazo se
|
|
* queda posado mientras suena: sigue diciendo si hay música sin animar nada.
|
|
*/
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.disco {
|
|
animation: none;
|
|
}
|
|
|
|
.brazo {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|