Voltar para o Blog

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-guide

O 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á

👉 Seguir @jeffbruchado no X

💡 Conteúdo diário sobre desenvolvimento, carreira e as ferramentas que eu realmente uso

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário