302 Ejercicios Ilustrados en Open Source: El Paquete npm Que Resuelve el Visual de la App de Entrenamiento
Hola HaWkers, hay un repositorio subiendo en GitHub esta semana que resuelve un problema muy específico y muy molesto: el Workout Guide, del desarrollador Bryl Lim, publica 302 ejercicios físicos ilustrados, tres frames por ejercicio, en un total de 906 SVG transparentes de 512 × 512, más un paquete npm tipado para consultar todo eso. El proyecto ya pasó de las 1,2 mil estrellas y está en npm como @bryllim/workout-guide, sin ninguna dependencia en runtime.
Si nunca intentaste levantar una app de entrenamiento, quizá no entiendas por qué esto es noticia. Quien ya lo intentó lo sabe: el código de la ficha de entrenamiento sale en una tarde, y la ilustración de los ejercicios traba el proyecto por semanas. Este artículo muestra lo que entrega el paquete, cómo usarlo de verdad con código y —la parte que casi todo el mundo se salta— lo que la licencia de los dibujos te exige antes de publicar en la tienda.
Qué Es el Workout Guide
El proyecto es un monorepo con dos piezas. La primera es el paquete publicado en npm, que carga los assets, un manifest.json con los metadatos y una API pequeña en TypeScript. La segunda es un sitio hecho en Astro con una galería estática buscable, donde puedes ver los 302 ejercicios antes de instalar cualquier cosa.
La regla visual es rígida y eso es lo que le da valor al conjunto: todo ejercicio tiene exactamente tres frames ordenados, transparentes, en el mismo viewBox de 512 × 512. No es una carpeta de dibujos aleatorios con estilos mezclados que alguien juntó de internet. Es un catálogo normalizado, y esa normalización es justamente el trabajo que nadie quiere hacer.
Los PNG originales siguen en el repositorio por compatibilidad, pero el formato principal es SVG. Para una pantalla de ejecución de ejercicio, eso significa nitidez en cualquier densidad de píxel, control de color por CSS y un peso por archivo que suele quedar en el orden de unos pocos KB.
El origen del material importa para entender la licencia más adelante: el arte de las poses viene de Everkinetic, un proyecto de datos abiertos de fitness creado por Greg Priday, publicado bajo CC BY-SA 4.0. Lo que hizo Bryl Lim fue expandir esa base con ejercicios adicionales y frames de animación, normalizar los assets, estructurar los metadatos, escribir la API del paquete y montar la galería de documentación.
Por Qué la Ilustración de Ejercicios Es un Problema Caro
Vale la pena explicar el tamaño del agujero que esto tapa, porque quien nunca se topó con él cree que es exageración.
Una app de entrenamiento necesita mostrar cómo se hace el movimiento. Sin eso el producto no existe: una lista de texto que dice "press de banca, 4 x 10" no le enseña a nadie a ejecutar el movimiento. Y ahí tienes tres caminos, todos malos.
El primero es contratar a un ilustrador. Sale caro y, peor, sale lento: 300 ejercicios con un estilo consistente es un proyecto de meses, no de semanas. El segundo es licenciar un banco de imágenes comercial, donde el precio suele ser por asset o por suscripción anual, y la licencia normalmente restringe la redistribución —lo que complica las cosas si quieres cachear los archivos en el cliente—. El tercero es usar video, que resuelve la explicación pero dispara el costo de banda y convierte cada pantalla de ejercicio en un reproductor que hay que mantener.
El cuarto camino es el que casi todo el mundo termina eligiendo: raspar imágenes de algún sitio de fitness y rezar. Eso no es una estrategia de licenciamiento, es una deuda esperando la notificación.
Un catálogo abierto, normalizado y distribuido por npm cambia esa cuenta. El visual deja de ser el cuello de botella del MVP y se convierte en una línea del package.json.
La API en Cinco Minutos
La instalación es la que esperas:
npm install @bryllim/workout-guideEl paquete expone tres funciones que resuelven el 90% de los casos. getExercise busca por id o slug, searchExercises filtra el catálogo y getAssetUrl devuelve la ruta de un frame específico:
import {
getExercise,
searchExercises,
getAssetUrl,
} from '@bryllim/workout-guide'
// Búsqueda directa por slug
const flexion = getExercise('push-up')
// Búsqueda textual combinada con filtro de equipamiento
const pectoralSinEquipo = searchExercises('chest', {
equipment: 'bodyweight',
})
// Ruta del primero de los tres frames
const primerFrame = getAssetUrl('push-up', 1)getExercise devuelve null cuando el slug no existe, y getAssetUrl también. Ese es un detalle de tipado que agradece quien escribe TypeScript: estás obligado a tratar la ausencia en vez de descubrir el undefined en la pantalla del usuario.
Además de esas tres, el paquete exporta el array completo exercises, los tipos (Exercise, ExerciseFrame, ExerciseSearchFilters, ExerciseType, AssetUrlOptions, ExerciseAttribution) y dos utilidades de bajo nivel, normalizeSearchText y matchesFilter, útiles si quieres montar tu propia capa de búsqueda por encima del manifiesto.
El Formato de Cada Ejercicio
Cada ítem del catálogo lleva los campos que usarías para montar filtros de UI: id, slug, name, equipment, primaryMuscle, secondaryMuscles, exerciseType, isStretch y frames, donde cada frame tiene index y path.
Con eso puedes montar un selector de ejercicios por grupo muscular sin ningún backend:
import { searchExercises, type Exercise } from '@bryllim/workout-guide'
type Grupo = { musculo: string; items: Exercise[] }
// Agrupa el catálogo por el músculo principal, ignorando los estiramientos
function agruparPorMusculo(): Grupo[] {
const mapa = new Map<string, Exercise[]>()
for (const ejercicio of searchExercises()) {
if (ejercicio.isStretch) continue
const clave = ejercicio.primaryMuscle
const lista = mapa.get(clave) ?? []
lista.push(ejercicio)
mapa.set(clave, lista)
}
return [...mapa.entries()]
.map(([musculo, items]) => ({ musculo, items }))
.sort((a, b) => b.items.length - a.items.length)
}Fíjate en que searchExercises() sin argumento devuelve el catálogo entero, lo que evita tener que importar el array y la función por separado.
Los Tres Frames: Animación Sin Video
La decisión de diseño más inteligente del Workout Guide es tener exactamente tres frames por ejercicio. Tres es poco lo suficiente para caber en cualquier pantalla sin reventar la banda, y lo bastante para comunicar un movimiento: posición inicial, mitad de la ejecución, posición final.
En la práctica, tienes dos opciones de uso. La estática muestra el frame del medio, que casi siempre es el más legible de la serie. La animada alterna los tres en loop y se convierte en una demostración del movimiento sin un solo byte de video.
En React, el ciclo es un useEffect de cinco líneas:
import { useEffect, useState } from 'react'
import { getExercise, getAssetUrl } from '@bryllim/workout-guide'
export function DemoEjercicio({ slug, intervalo = 700 }) {
const ejercicio = getExercise(slug)
const [frame, setFrame] = useState(1)
useEffect(() => {
// Cicla entre los frames 1, 2 y 3 mientras el componente esté montado
const timer = setInterval(() => {
setFrame((actual) => (actual % 3) + 1)
}, intervalo)
return () => clearInterval(timer)
}, [intervalo])
if (!ejercicio) return null
return (
<figure>
<img
src={getAssetUrl(ejercicio.slug, frame)}
alt={`Ejecución del ejercicio ${ejercicio.name}, cuadro ${frame} de 3`}
width={512}
height={512}
loading="lazy"
/>
<figcaption>
{ejercicio.name} — {ejercicio.primaryMuscle}
</figcaption>
</figure>
)
}Dos cosas en ese fragmento no son adorno. El alt describe el movimiento y el número del cuadro, porque una secuencia de imágenes sin texto alternativo es una pantalla invisible para quien usa lector de pantalla. Y el width/height explícito reserva el espacio antes de la carga, evitando el salto de layout que tumba tu CLS.
Si quieres ir más allá del cambio de imagen y animar el trazo del propio SVG, el camino es inline en vez de <img> —y ahí valen las mismas técnicas que ya detallé en el artículo sobre animar SVG con CSS y dar vida a los gráficos vectoriales—. Con el SVG en el DOM controlas color, grosor y transición por variable CSS, lo que resuelve el tema claro y oscuro gratis:
.demo-ejercicio svg {
/* El trazo hereda el color del tema en vez de venir fijo del archivo */
color: var(--color-texto);
transition: opacity 180ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
/* Respeta a quien pidió menos movimiento: muestra solo el frame del medio */
.demo-ejercicio [data-frame='1'],
.demo-ejercicio [data-frame='3'] {
display: none;
}
}El bloque de prefers-reduced-motion no es preciosismo. Una animación en loop infinito a pantalla completa es exactamente el tipo de movimiento que molesta a quien tiene sensibilidad vestibular, y el costo de respetar eso son seis líneas de CSS.
La Licencia: Donde Vive la Trampa
Aquí está la parte que decide si puedes usar esto en tu producto, y es donde veo que más gente se equivoca.
El repositorio tiene dos licencias diferentes. El código y la documentación salen bajo MIT, que es permisiva y no pide casi nada. Los assets visuales —o sea, los 906 SVG, la parte que realmente quieres— salen bajo CC BY-SA 4.0, heredada de Everkinetic. No es lo mismo, y tratar a las dos como si lo fueran es el error clásico.
CC BY-SA 4.0 significa dos obligaciones. La primera es BY: atribución. Necesitas acreditar la autoría, indicar la licencia y señalar si hubo modificación. La segunda es SA, share-alike: si adaptas el material, la adaptación tiene que distribuirse bajo la misma licencia.
El punto que suele malinterpretarse es el alcance del share-alike. Recae sobre la obra adaptada, no sobre todo el software que muestra la imagen. Usar los SVG tal como están dentro de tu app no convierte tu código en CC BY-SA —el código sigue siendo tuyo, bajo la licencia que quieras—. Ahora bien, si rediseñas las poses, las recoloreas, las recortas o generas frames derivados, esos archivos derivados nacen CC BY-SA 4.0 y tienen que distribuirse así.
Eso tiene consecuencias prácticas en una app comercial: CC BY-SA no es una licencia "gratis y listo", es una licencia con contrapartida. Si tu plan era agarrar los dibujos, cambiar el estilo para que combine con la marca y tratar el resultado como arte propietario, el plan no funciona.
Lo que sí funciona es el camino simple: usa los assets sin modificar, acredita bien y mantén los archivos de licencia junto a la distribución. Ante la duda sobre tu caso específico, esa es una de esas horas de hablar con un abogado en vez de con un blog.
Una pantalla de créditos generada desde el propio manifiesto resuelve el BY sin volverse deuda de mantenimiento:
import { exercises } from '@bryllim/workout-guide'
// Arma el texto de créditos a partir de lo que está realmente en uso en la app
export function creditosDeAssets(slugsUsados) {
const usados = exercises.filter((ex) => slugsUsados.includes(ex.slug))
return {
total: usados.length,
fuente: 'Workout Guide, por Bryl Lim',
arteOriginal: 'Everkinetic',
licencia: 'CC BY-SA 4.0',
modificado: false,
url: 'https://github.com/bryllim/workout-guide',
}
}Generar eso a partir del catálogo, y no de un string escrito a mano, garantiza que el crédito siga siendo correcto el día en que alguien agregue ejercicios nuevos a la app.
Cuidados de Producción
El paquete no tiene dependencias en runtime y expone ESM y CJS por el mapa de exports, además de dar acceso directo al manifest.json y a la carpeta assets/. Eso es genial para los bundlers, pero abre una trampa de peso: importar el catálogo entero en el cliente para mostrar un ejercicio es meter los metadatos de 302 ítems dentro del bundle.
La salida es decidir dónde vive el catálogo. En una app con build en el servidor —Nuxt, Next, Astro— lo más barato es consultar el paquete en tiempo de build o en el servidor y mandarle al cliente solo el subconjunto que la pantalla necesita:
// server/api/ejercicios.get.ts — Nuxt 3
import { searchExercises } from '@bryllim/workout-guide'
export default defineEventHandler((event) => {
const { musculo = '', equipamiento } = getQuery(event)
// Filtra en el servidor y devuelve solo lo que la UI renderiza
return searchExercises(String(musculo), {
equipment: equipamiento ? String(equipamiento) : undefined,
}).map(({ slug, name, primaryMuscle, equipment }) => ({
slug,
name,
primaryMuscle,
equipment,
}))
})Sobre los archivos en sí: 906 SVG transparentes no son un problema de banda si los sirves con Cache-Control largo e inmutable, ya que los assets están versionados junto al paquete. En React Native, donde no existe la misma noción de URL pública, el camino es copiar los assets usados a la carpeta de recursos de la app en el build y resolver la ruta local —getAssetUrl acepta opciones justamente para acomodar prefijos diferentes—.
Y, como siempre que una dependencia nueva entra en un proyecto, vale la pena fijar la versión y mirar lo que se está instalando. Un paquete de assets con cero dependencias es un blanco pequeño, pero el cuidado es el mismo de cualquier instalación: detallé el guion completo en el artículo sobre publicación en npm y paquetes maliciosos.
Lo Que Este Proyecto Dice Sobre los Datos Abiertos
Hay una lectura más grande aquí, y no es sobre fitness.
Everkinetic publicó datos abiertos de ejercicios hace años. Aquello quedó ahí, útil pero crudo: imágenes en PNG, estilos irregulares, metadatos de estructura simple. Lo que pasó ahora fue que alguien agarró esa base, hizo el trabajo ingrato de la normalización —mismo viewBox, mismo número de frames, mismo esquema de metadatos— y lo empaquetó en un formato que el ecosistema consume sin pensar, que es npm install.
Ese segundo paso está subestimado. Un dato abierto que exige tres días de limpieza antes del primer uso tiene alcance de nicho. El mismo dato, normalizado, tipado y publicado en un registry, se vuelve infraestructura. La diferencia entre los dos estados no es la licencia ni la calidad original del material: es el empaquetado.
Vale la pena mirar esto con un poco de escepticismo también. Un paquete mantenido por una sola persona es un punto único de falla, y la respuesta sensata es la de siempre: fija la versión, espeja los assets que usas y no construyas la experiencia central del producto encima de la suposición de que el repositorio va a seguir recibiendo commits en 2028. Con licencia abierta, el peor escenario es que mantengas un fork —lo que es bastante mejor que el peor escenario de un banco de imágenes comercial, donde la licencia simplemente vence—.
Si tienes una app de entrenamiento parada en el cajón porque la parte visual parecía inviable, el argumento se acabó. Son 302 ejercicios, tres frames cada uno, una API de tres funciones y una licencia que pide crédito, no dinero. El trabajo que sobra es el tuyo de verdad: la ficha, la progresión de carga, el historial —la parte que diferencia el producto y que ningún paquete te va a entregar hecho—.
Vamos con todo! 🦅
📚 ¿Quieres Seguir lo Que Viene Por Delante?
Este artículo cubrió el Workout Guide y cómo usar 302 ejercicios ilustrados en open source en tu app, pero el ecosistema cambia cada semana y no todo se convierte en artículo aquí.
En X comparto lo que estoy probando, la trastienda de los proyectos y las novedades que aparecen antes de volverse post.
Sígueme Allá
💡 Contenido diario sobre desarrollo, carrera y las herramientas que realmente uso

