302 Exercícios Ilustrados em Open Source: O Pacote npm Que Resolve o Visual do App de Treino
Olá HaWkers, tem um repositório subindo no GitHub nesta semana que resolve um problema bem específico e bem chato: o Workout Guide, do desenvolvedor Bryl Lim, publica 302 exercícios físicos ilustrados, três frames por exercício, num total de 906 SVGs transparentes de 512 × 512, mais um pacote npm tipado para consultar tudo isso. O projeto passou de 1,2 mil estrelas e já está no npm como @bryllim/workout-guide, sem nenhuma dependência em runtime.
Se você nunca tentou colocar de pé um app de treino, talvez não entenda por que isso é notícia. Quem já tentou sabe: o código da ficha de treino sai em uma tarde, e a ilustração dos exercícios trava o projeto por semanas. Este artigo mostra o que o pacote entrega, como usar de verdade com código, e — a parte que quase todo mundo pula — o que a licença dos desenhos exige de você antes de publicar na loja.
O Que É o Workout Guide
O projeto é um monorepo com duas peças. A primeira é o pacote publicado no npm, que carrega os assets, um manifest.json com os metadados e uma API pequena em TypeScript. A segunda é um site em Astro com uma galeria estática pesquisável, onde dá para ver os 302 exercícios antes de instalar qualquer coisa.
A regra visual é rígida e é isso que dá valor ao conjunto: todo exercício tem exatamente três frames ordenados, transparentes, no mesmo viewBox de 512 × 512. Não é uma pasta de desenhos aleatórios com estilos misturados que alguém juntou da internet. É um catálogo normalizado, e essa normalização é justamente o trabalho que ninguém quer fazer.
Os PNGs originais continuam no repositório por compatibilidade, mas o formato principal é SVG. Para uma tela de execução de exercício, isso significa nitidez em qualquer densidade de pixel, controle de cor por CSS e um peso por arquivo que costuma ficar na casa dos poucos KB.
A origem do material importa para entender a licença mais adiante: a arte das poses vem do Everkinetic, um projeto de dados abertos de fitness criado por Greg Priday, publicado sob CC BY-SA 4.0. O que Bryl Lim fez foi expandir aquela base com exercícios adicionais e frames de animação, normalizar os assets, estruturar os metadados, escrever a API do pacote e montar a galeria de documentação.
Por Que Ilustração de Exercício É Um Problema Caro
Vale explicar o tamanho do buraco que isso tapa, porque quem nunca esbarrou nele acha exagero.
Um app de treino precisa mostrar como o movimento é feito. Sem isso o produto não existe: uma lista de texto dizendo "supino reto, 4 x 10" não ensina ninguém a executar o movimento. E aí você tem três caminhos, todos ruins.
O primeiro é contratar um ilustrador. Sai caro e, pior, sai lento: 300 exercícios em estilo consistente é um projeto de meses, não de semanas. O segundo é licenciar um banco de imagens comercial, onde o preço costuma ser por asset ou por assinatura anual, e a licença normalmente restringe redistribuição — o que complica se você quiser cachear os arquivos no cliente. O terceiro é usar vídeo, que resolve a explicação mas explode o custo de banda e transforma cada tela de exercício num player para manter.
O quarto caminho é o que quase todo mundo acaba escolhendo: raspar imagens de algum site de fitness e rezar. Isso não é uma estratégia de licenciamento, é uma dívida esperando notificação.
Um catálogo aberto, normalizado e distribuído por npm muda essa conta. O visual deixa de ser o gargalo do MVP e vira uma linha no package.json.
A API em Cinco Minutos
A instalação é o que você espera:
npm install @bryllim/workout-guideO pacote expõe três funções que resolvem 90% dos casos. getExercise busca por id ou slug, searchExercises filtra o catálogo e getAssetUrl devolve o caminho de um frame específico:
import {
getExercise,
searchExercises,
getAssetUrl,
} from '@bryllim/workout-guide'
// Busca direta por slug
const flexao = getExercise('push-up')
// Busca textual combinada com filtro de equipamento
const peitoralSemEquipamento = searchExercises('chest', {
equipment: 'bodyweight',
})
// Caminho do primeiro dos três frames
const primeiroFrame = getAssetUrl('push-up', 1)getExercise devolve null quando o slug não existe, e getAssetUrl também. Isso é um detalhe de tipagem que agradece quem escreve TypeScript: você é obrigado a tratar a ausência em vez de descobrir o undefined na tela do usuário.
Além dessas três, o pacote exporta o array completo exercises, os tipos (Exercise, ExerciseFrame, ExerciseSearchFilters, ExerciseType, AssetUrlOptions, ExerciseAttribution) e dois utilitários de baixo nível, normalizeSearchText e matchesFilter, úteis se você quiser montar a sua própria camada de busca por cima do manifesto.
O Formato de Cada Exercício
Cada item do catálogo carrega os campos que você usaria para montar filtros de UI: id, slug, name, equipment, primaryMuscle, secondaryMuscles, exerciseType, isStretch e frames, onde cada frame tem index e path.
Com isso dá para montar um seletor de exercícios por grupo muscular sem nenhum backend:
import { searchExercises, type Exercise } from '@bryllim/workout-guide'
type Grupo = { musculo: string; itens: Exercise[] }
// Agrupa o catálogo pelo músculo principal, ignorando alongamentos
function agruparPorMusculo(): Grupo[] {
const mapa = new Map<string, Exercise[]>()
for (const exercicio of searchExercises()) {
if (exercicio.isStretch) continue
const chave = exercicio.primaryMuscle
const lista = mapa.get(chave) ?? []
lista.push(exercicio)
mapa.set(chave, lista)
}
return [...mapa.entries()]
.map(([musculo, itens]) => ({ musculo, itens }))
.sort((a, b) => b.itens.length - a.itens.length)
}Repare que searchExercises() sem argumento devolve o catálogo inteiro, o que evita ter que importar o array e a função separadamente.
Os Três Frames: Animação Sem Vídeo
A decisão de projeto mais inteligente do Workout Guide é ter exatamente três frames por exercício. Três é pouco o suficiente para caber em qualquer tela sem estourar banda, e o bastante para comunicar um movimento: posição inicial, meio da execução, posição final.
Na prática, você tem duas escolhas de uso. A estática mostra o frame do meio, que quase sempre é o mais legível da série. A animada alterna os três em loop e vira uma demonstração do movimento sem um único byte de vídeo.
Em React, o ciclo é um useEffect de cinco linhas:
import { useEffect, useState } from 'react'
import { getExercise, getAssetUrl } from '@bryllim/workout-guide'
export function DemoExercicio({ slug, intervalo = 700 }) {
const exercicio = getExercise(slug)
const [frame, setFrame] = useState(1)
useEffect(() => {
// Cicla entre os frames 1, 2 e 3 enquanto o componente estiver montado
const timer = setInterval(() => {
setFrame((atual) => (atual % 3) + 1)
}, intervalo)
return () => clearInterval(timer)
}, [intervalo])
if (!exercicio) return null
return (
<figure>
<img
src={getAssetUrl(exercicio.slug, frame)}
alt={`Execução do exercício ${exercicio.name}, quadro ${frame} de 3`}
width={512}
height={512}
loading="lazy"
/>
<figcaption>
{exercicio.name} — {exercicio.primaryMuscle}
</figcaption>
</figure>
)
}Duas coisas nesse trecho não são enfeite. O alt descreve o movimento e o número do quadro, porque uma sequência de imagens sem texto alternativo é uma tela invisível para quem usa leitor de tela. E o width/height explícito reserva o espaço antes do carregamento, evitando o pulo de layout que derruba seu CLS.
Se você quiser ir além do troca-imagem e animar o traço do próprio SVG, o caminho é inline em vez de <img> — e aí valem as mesmas técnicas que eu já detalhei no artigo sobre animar SVG com CSS e dar vida aos gráficos vetoriais. Com o SVG no DOM você controla cor, espessura e transição por variável CSS, o que resolve tema claro e escuro de graça:
.demo-exercicio svg {
/* O traço herda a cor do tema em vez de vir fixo do arquivo */
color: var(--cor-texto);
transition: opacity 180ms ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
/* Respeita quem pediu menos movimento: mostra só o frame do meio */
.demo-exercicio [data-frame='1'],
.demo-exercicio [data-frame='3'] {
display: none;
}
}O bloco de prefers-reduced-motion não é preciosismo. Uma animação em loop infinito na tela inteira é exatamente o tipo de movimento que incomoda quem tem sensibilidade vestibular, e o custo de respeitar isso são seis linhas de CSS.
A Licença: Onde Mora a Armadilha
Aqui está a parte que decide se você pode usar isso no seu produto, e é onde eu vejo mais gente errar.
O repositório tem duas licenças diferentes. O código e a documentação saem sob MIT, que é permissiva e não pede quase nada. Os assets visuais — ou seja, os 906 SVGs, a parte que você realmente quer — saem sob CC BY-SA 4.0, herdada do Everkinetic. Não é a mesma coisa, e tratar as duas como se fossem é o erro clássico.
CC BY-SA 4.0 significa duas obrigações. A primeira é BY: atribuição. Você precisa creditar a autoria, indicar a licença e sinalizar se houve modificação. A segunda é SA, share-alike: se você adaptar o material, a adaptação precisa ser distribuída sob a mesma licença.
O ponto que costuma ser mal entendido é o alcance do share-alike. Ele recai sobre a obra adaptada, não sobre todo software que exibe a imagem. Usar os SVGs como estão dentro do seu app não transforma o seu código em CC BY-SA — o código continua seu, sob a licença que você quiser. Agora, se você redesenhar as poses, recolorir, recortar ou gerar frames derivados, esses arquivos derivados nascem CC BY-SA 4.0 e precisam ser distribuídos assim.
Isso tem consequência prática em app comercial: CC BY-SA não é uma licença "de graça e pronto", é uma licença com contrapartida. Se o seu plano era pegar os desenhos, mudar o estilo para combinar com a marca e tratar o resultado como arte proprietária, o plano não funciona.
O que funciona é o caminho simples: use os assets sem modificar, credite direito e mantenha os arquivos de licença junto na distribuição. Na dúvida sobre o seu caso específico, essa é uma daquelas horas de conversar com um advogado em vez de com um blog.
Uma tela de créditos gerada do próprio manifesto resolve o BY sem virar dívida de manutenção:
import { exercises } from '@bryllim/workout-guide'
// Monta o texto de créditos a partir do que está realmente em uso no app
export function creditosDeAssets(slugsUsados) {
const usados = exercises.filter((ex) => slugsUsados.includes(ex.slug))
return {
total: usados.length,
fonte: 'Workout Guide, por Bryl Lim',
arteOriginal: 'Everkinetic',
licenca: 'CC BY-SA 4.0',
modificado: false,
url: 'https://github.com/bryllim/workout-guide',
}
}Gerar isso a partir do catálogo, e não de uma string escrita à mão, garante que o crédito continua correto no dia em que alguém adicionar exercícios novos ao app.
Cuidados de Produção
O pacote não tem dependência em runtime e expõe ESM e CJS pelo mapa de exports, além de dar acesso direto ao manifest.json e à pasta assets/. Isso é ótimo para bundlers, mas abre uma armadilha de peso: importar o catálogo inteiro no cliente para mostrar um exercício é jogar metadado de 302 itens dentro do bundle.
A saída é decidir onde o catálogo vive. Em app com build no servidor — Nuxt, Next, Astro — o mais barato é consultar o pacote em tempo de build ou no servidor e mandar para o cliente só o subconjunto que a tela precisa:
// server/api/exercicios.get.ts — Nuxt 3
import { searchExercises } from '@bryllim/workout-guide'
export default defineEventHandler((event) => {
const { musculo = '', equipamento } = getQuery(event)
// Filtra no servidor e devolve só o que a UI renderiza
return searchExercises(String(musculo), {
equipment: equipamento ? String(equipamento) : undefined,
}).map(({ slug, name, primaryMuscle, equipment }) => ({
slug,
name,
primaryMuscle,
equipment,
}))
})Sobre os arquivos em si: 906 SVGs transparentes não são um problema de banda se você servir com Cache-Control longo e imutável, já que os assets são versionados junto do pacote. Em React Native, onde não existe a mesma noção de URL pública, o caminho é copiar os assets usados para a pasta de recursos do app no build e resolver o caminho local — o getAssetUrl aceita opções justamente para acomodar prefixos diferentes.
E, como sempre que uma dependência nova entra num projeto, vale fixar a versão e olhar o que está sendo instalado. Um pacote de assets com zero dependências é um alvo pequeno, mas o cuidado é o mesmo de qualquer instalação: eu detalhei o roteiro completo no artigo sobre publicação no npm e pacotes maliciosos.
O Que Esse Projeto Diz Sobre Dados Abertos
Tem uma leitura maior aqui, e ela não é sobre fitness.
O Everkinetic publicou dados abertos de exercícios anos atrás. Aquilo ficou lá, útil mas cru: imagens em PNG, estilos irregulares, metadados de estrutura simples. O que aconteceu agora foi alguém pegar essa base, fazer o trabalho ingrato de normalização — mesmo viewBox, mesmo número de frames, mesmo esquema de metadados — e empacotar num formato que o ecossistema consome sem pensar, que é npm install.
Esse segundo passo é subestimado. Dado aberto que exige três dias de limpeza antes do primeiro uso tem alcance de nicho. O mesmo dado, normalizado, tipado e publicado num registry, vira infraestrutura. A diferença entre os dois estados não é a licença nem a qualidade original do material: é o empacotamento.
Vale a pena olhar isso com um pouco de ceticismo também. Um pacote mantido por uma pessoa é um ponto único de falha, e a resposta sensata é a de sempre: fixe a versão, espelhe os assets que você usa e não construa a experiência central do produto em cima da suposição de que o repositório vai continuar recebendo commits em 2028. Com licença aberta, o pior cenário é você manter um fork — o que é bem melhor do que o pior cenário de um banco de imagens comercial, onde a licença simplesmente vence.
Se você tem um app de treino parado na gaveta porque a parte visual parecia inviável, o argumento acabou. São 302 exercícios, três frames cada, uma API de três funções e uma licença que pede crédito, não dinheiro. O trabalho que sobra é o seu de verdade: a ficha, a progressão de carga, o histórico — a parte que diferencia o produto e que nenhum pacote vai entregar pronto.
Bora pra cima! 🦅
📚 Quer Acompanhar o Que Vem Por Aí?
Este artigo cobriu o Workout Guide e como usar 302 exercícios ilustrados em open source no seu app, mas o ecossistema muda toda semana e nem tudo vira artigo aqui.
No X eu compartilho o que estou testando, os bastidores dos projetos e as novidades que aparecem antes de virarem post.
Me Segue Lá
💡 Conteúdo diário sobre desenvolvimento, carreira e as ferramentas que eu realmente uso

