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.
senzapaura_es/src/lib/components/Tocadiscos.svelte

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>

Powered by TurnKey Linux.