Retour au blog

302 Exercices Illustrés en Open Source : Le Paquet npm Qui Règle le Visuel de l'App de Sport

Salut HaWkers, il y a un dépôt qui grimpe sur GitHub cette semaine et qui résout un problème très précis et très pénible : le Workout Guide, du développeur Bryl Lim, publie 302 exercices physiques illustrés, trois images par exercice, soit un total de 906 SVG transparents en 512 × 512, plus un paquet npm typé pour interroger tout ça. Le projet a dépassé les 1,2 mille étoiles et il est déjà sur npm sous le nom @bryllim/workout-guide, sans aucune dépendance en runtime.

Si vous n'avez jamais essayé de monter une app d'entraînement, vous ne voyez peut-être pas pourquoi c'est une nouvelle. Ceux qui ont essayé le savent : le code de la fiche d'entraînement sort en un après-midi, et l'illustration des exercices bloque le projet pendant des semaines. Cet article montre ce que le paquet livre, comment s'en servir pour de vrai avec du code, et — la partie que presque tout le monde saute — ce que la licence des dessins exige de vous avant de publier sur le store.

Ce Qu'Est le Workout Guide

Le projet est un monorepo à deux pièces. La première est le paquet publié sur npm, qui embarque les assets, un manifest.json avec les métadonnées et une petite API en TypeScript. La seconde est un site en Astro avec une galerie statique et cherchable, où l'on peut voir les 302 exercices avant d'installer quoi que ce soit.

La règle visuelle est stricte et c'est ce qui donne sa valeur à l'ensemble : chaque exercice a exactement trois images ordonnées, transparentes, dans le même viewBox de 512 × 512. Ce n'est pas un dossier de dessins aléatoires aux styles mélangés que quelqu'un a ramassés sur internet. C'est un catalogue normalisé, et cette normalisation est justement le travail que personne ne veut faire.

Les PNG d'origine restent dans le dépôt par compatibilité, mais le format principal est le SVG. Pour un écran d'exécution d'exercice, cela signifie de la netteté à n'importe quelle densité de pixels, un contrôle de la couleur par CSS et un poids par fichier qui tourne autour de quelques Ko.

L'origine du matériel compte pour comprendre la licence plus loin : l'illustration des postures vient d'Everkinetic, un projet de données ouvertes sur le fitness créé par Greg Priday, publié sous CC BY-SA 4.0. Ce que Bryl Lim a fait, c'est étendre cette base avec des exercices supplémentaires et des images d'animation, normaliser les assets, structurer les métadonnées, écrire l'API du paquet et monter la galerie de documentation.

Pourquoi l'Illustration d'Exercice Est un Problème Coûteux

Il vaut la peine d'expliquer la taille du trou que ça bouche, parce que ceux qui ne sont jamais tombés dedans trouvent ça exagéré.

Une app d'entraînement doit montrer comment le mouvement se fait. Sans ça le produit n'existe pas : une liste de texte disant « développé couché, 4 × 10 » n'apprend à personne à exécuter le mouvement. Et là vous avez trois chemins, tous mauvais.

Le premier est d'embaucher un illustrateur. Ça coûte cher et, pire, c'est lent : 300 exercices dans un style cohérent, c'est un projet de plusieurs mois, pas de plusieurs semaines. Le deuxième est de licencier une banque d'images commerciale, où le prix est en général à l'asset ou à l'abonnement annuel, et où la licence restreint normalement la redistribution — ce qui complique les choses si vous voulez mettre les fichiers en cache côté client. Le troisième est d'utiliser de la vidéo, ce qui règle l'explication mais fait exploser le coût de bande passante et transforme chaque écran d'exercice en un lecteur à maintenir.

Le quatrième chemin est celui que presque tout le monde finit par choisir : scraper des images sur un site de fitness et prier. Ce n'est pas une stratégie de licence, c'est une dette qui attend sa mise en demeure.

Un catalogue ouvert, normalisé et distribué par npm change ce calcul. Le visuel cesse d'être le goulot d'étranglement du MVP et devient une ligne dans le package.json.

L'API en Cinq Minutes

L'installation est ce à quoi vous vous attendez :

npm install @bryllim/workout-guide

Le paquet expose trois fonctions qui règlent 90 % des cas. getExercise cherche par id ou slug, searchExercises filtre le catalogue et getAssetUrl renvoie le chemin d'une image précise :

import {
  getExercise,
  searchExercises,
  getAssetUrl,
} from '@bryllim/workout-guide'

// Recherche directe par slug
const pompe = getExercise('push-up')

// Recherche textuelle combinée à un filtre de matériel
const pectorauxSansMateriel = searchExercises('chest', {
  equipment: 'bodyweight',
})

// Chemin de la première des trois images
const premiereImage = getAssetUrl('push-up', 1)

getExercise renvoie null quand le slug n'existe pas, et getAssetUrl aussi. C'est un détail de typage dont vous remercierez l'auteur si vous écrivez du TypeScript : vous êtes obligé de traiter l'absence au lieu de découvrir le undefined sur l'écran de l'utilisateur.

En plus de ces trois-là, le paquet exporte le tableau complet exercises, les types (Exercise, ExerciseFrame, ExerciseSearchFilters, ExerciseType, AssetUrlOptions, ExerciseAttribution) et deux utilitaires de bas niveau, normalizeSearchText et matchesFilter, utiles si vous voulez monter votre propre couche de recherche par-dessus le manifeste.

Le Format de Chaque Exercice

Chaque élément du catalogue porte les champs que vous utiliseriez pour construire des filtres d'UI : id, slug, name, equipment, primaryMuscle, secondaryMuscles, exerciseType, isStretch et frames, où chaque image a un index et un path.

Avec ça, on peut monter un sélecteur d'exercices par groupe musculaire sans aucun backend :

import { searchExercises, type Exercise } from '@bryllim/workout-guide'

type Groupe = { muscle: string; elements: Exercise[] }

// Regroupe le catalogue par muscle principal, en ignorant les étirements
function regrouperParMuscle(): Groupe[] {
  const carte = new Map<string, Exercise[]>()

  for (const exercice of searchExercises()) {
    if (exercice.isStretch) continue

    const cle = exercice.primaryMuscle
    const liste = carte.get(cle) ?? []
    liste.push(exercice)
    carte.set(cle, liste)
  }

  return [...carte.entries()]
    .map(([muscle, elements]) => ({ muscle, elements }))
    .sort((a, b) => b.elements.length - a.elements.length)
}

Remarquez que searchExercises() sans argument renvoie le catalogue entier, ce qui évite d'avoir à importer le tableau et la fonction séparément.

Les Trois Images : de l'Animation Sans Vidéo

La décision de conception la plus intelligente du Workout Guide, c'est d'avoir exactement trois images par exercice. Trois, c'est assez peu pour tenir sur n'importe quel écran sans faire exploser la bande passante, et assez pour communiquer un mouvement : position de départ, milieu de l'exécution, position finale.

En pratique, vous avez deux façons de vous en servir. La statique montre l'image du milieu, qui est presque toujours la plus lisible de la série. L'animée alterne les trois en boucle et devient une démonstration du mouvement sans un seul octet de vidéo.

En React, le cycle tient dans un useEffect de cinq lignes :

import { useEffect, useState } from 'react'
import { getExercise, getAssetUrl } from '@bryllim/workout-guide'

export function DemoExercice({ slug, intervalle = 700 }) {
  const exercice = getExercise(slug)
  const [image, setImage] = useState(1)

  useEffect(() => {
    // Boucle entre les images 1, 2 et 3 tant que le composant est monté
    const timer = setInterval(() => {
      setImage((actuelle) => (actuelle % 3) + 1)
    }, intervalle)

    return () => clearInterval(timer)
  }, [intervalle])

  if (!exercice) return null

  return (
    <figure>
      <img
        src={getAssetUrl(exercice.slug, image)}
        alt={`Exécution de l'exercice ${exercice.name}, image ${image} sur 3`}
        width={512}
        height={512}
        loading="lazy"
      />
      <figcaption>
        {exercice.name} — {exercice.primaryMuscle}
      </figcaption>
    </figure>
  )
}

Deux choses dans cet extrait ne sont pas de la décoration. Le alt décrit le mouvement et le numéro de l'image, parce qu'une séquence d'images sans texte alternatif est un écran invisible pour qui utilise un lecteur d'écran. Et le width/height explicite réserve la place avant le chargement, ce qui évite le saut de mise en page qui plombe votre CLS.

Si vous voulez aller au-delà du changement d'image et animer le tracé du SVG lui-même, le chemin est l'inline plutôt que le <img> — et là valent les mêmes techniques que j'ai déjà détaillées dans l'article sur animer un SVG avec CSS et donner vie aux graphiques vectoriels. Avec le SVG dans le DOM, vous contrôlez la couleur, l'épaisseur et la transition par variable CSS, ce qui règle le thème clair et sombre gratuitement :

.demo-exercice svg {
  /* Le tracé hérite de la couleur du thème au lieu de venir figé du fichier */
  color: var(--couleur-texte);
  transition: opacity 180ms ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  /* Respecte qui a demandé moins de mouvement : affiche seulement l'image du milieu */
  .demo-exercice [data-frame='1'],
  .demo-exercice [data-frame='3'] {
    display: none;
  }
}

Le bloc prefers-reduced-motion n'est pas du perfectionnisme. Une animation en boucle infinie en plein écran est exactement le type de mouvement qui gêne les personnes ayant une sensibilité vestibulaire, et le coût de respecter ça, c'est six lignes de CSS.

La Licence : Là Où le Piège Se Cache

Voici la partie qui décide si vous pouvez utiliser ça dans votre produit, et c'est là que je vois le plus de monde se tromper.

Le dépôt a deux licences différentes. Le code et la documentation sortent sous MIT, qui est permissive et ne demande presque rien. Les assets visuels — c'est-à-dire les 906 SVG, la partie que vous voulez vraiment — sortent sous CC BY-SA 4.0, héritée d'Everkinetic. Ce n'est pas la même chose, et traiter les deux comme si elles l'étaient est l'erreur classique.

CC BY-SA 4.0 signifie deux obligations. La première est BY : l'attribution. Vous devez créditer l'auteur, indiquer la licence et signaler s'il y a eu modification. La seconde est SA, share-alike : si vous adaptez le matériel, l'adaptation doit être distribuée sous la même licence.

Le point souvent mal compris est la portée du share-alike. Il retombe sur l'œuvre adaptée, pas sur tout logiciel qui affiche l'image. Utiliser les SVG tels quels dans votre app ne transforme pas votre code en CC BY-SA — le code reste le vôtre, sous la licence que vous voulez. En revanche, si vous redessinez les postures, les recolorez, les recadrez ou générez des images dérivées, ces fichiers dérivés naissent en CC BY-SA 4.0 et doivent être distribués ainsi.

Ça a une conséquence pratique en app commerciale : CC BY-SA n'est pas une licence « gratuit et basta », c'est une licence avec contrepartie. Si votre plan était de prendre les dessins, de changer le style pour coller à la marque et de traiter le résultat comme de l'art propriétaire, le plan ne fonctionne pas.

Ce qui fonctionne, c'est le chemin simple : utilisez les assets sans les modifier, créditez correctement et gardez les fichiers de licence avec la distribution. Dans le doute sur votre cas précis, c'est une de ces heures où il faut parler à un avocat plutôt qu'à un blog.

Un écran de crédits généré depuis le manifeste lui-même règle le BY sans devenir une dette de maintenance :

import { exercises } from '@bryllim/workout-guide'

// Construit le texte de crédits à partir de ce qui est réellement utilisé dans l'app
export function creditsDesAssets(slugsUtilises) {
  const utilises = exercises.filter((ex) => slugsUtilises.includes(ex.slug))

  return {
    total: utilises.length,
    source: 'Workout Guide, par Bryl Lim',
    artOriginal: 'Everkinetic',
    licence: 'CC BY-SA 4.0',
    modifie: false,
    url: 'https://github.com/bryllim/workout-guide',
  }
}

Générer ça depuis le catalogue, et non depuis une chaîne écrite à la main, garantit que le crédit reste correct le jour où quelqu'un ajoutera de nouveaux exercices à l'app.

Précautions de Production

Le paquet n'a aucune dépendance en runtime et expose de l'ESM et du CJS via la carte d'exports, en plus de donner un accès direct au manifest.json et au dossier assets/. C'est excellent pour les bundlers, mais ça ouvre un piège de poids : importer le catalogue entier côté client pour afficher un exercice, c'est jeter les métadonnées de 302 éléments dans le bundle.

La solution est de décider où vit le catalogue. Dans une app avec build côté serveur — Nuxt, Next, Astro — le moins cher est d'interroger le paquet au moment du build ou sur le serveur et de n'envoyer au client que le sous-ensemble dont l'écran a besoin :

// server/api/exercices.get.ts — Nuxt 3
import { searchExercises } from '@bryllim/workout-guide'

export default defineEventHandler((event) => {
  const { muscle = '', materiel } = getQuery(event)

  // Filtre sur le serveur et ne renvoie que ce que l'UI affiche
  return searchExercises(String(muscle), {
    equipment: materiel ? String(materiel) : undefined,
  }).map(({ slug, name, primaryMuscle, equipment }) => ({
    slug,
    name,
    primaryMuscle,
    equipment,
  }))
})

Sur les fichiers eux-mêmes : 906 SVG transparents ne sont pas un problème de bande passante si vous les servez avec un Cache-Control long et immuable, puisque les assets sont versionnés avec le paquet. En React Native, où la même notion d'URL publique n'existe pas, le chemin est de copier les assets utilisés dans le dossier de ressources de l'app au build et de résoudre le chemin local — getAssetUrl accepte justement des options pour accommoder différents préfixes.

Et, comme chaque fois qu'une nouvelle dépendance entre dans un projet, il vaut mieux figer la version et regarder ce qui est en train d'être installé. Un paquet d'assets avec zéro dépendance est une petite cible, mais la précaution est la même que pour n'importe quelle installation : j'ai détaillé la marche à suivre complète dans l'article sur la publication sur npm et les paquets malveillants.

Ce Que Ce Projet Dit des Données Ouvertes

Il y a une lecture plus large ici, et elle ne parle pas de fitness.

Everkinetic a publié des données ouvertes d'exercices il y a des années. Ça est resté là, utile mais brut : des images en PNG, des styles irréguliers, des métadonnées à structure simple. Ce qui vient d'arriver, c'est que quelqu'un a pris cette base, fait le travail ingrat de normalisation — même viewBox, même nombre d'images, même schéma de métadonnées — et l'a empaquetée dans un format que l'écosystème consomme sans réfléchir, à savoir npm install.

Ce deuxième pas est sous-estimé. Une donnée ouverte qui exige trois jours de nettoyage avant le premier usage a une portée de niche. La même donnée, normalisée, typée et publiée dans un registry, devient de l'infrastructure. La différence entre les deux états n'est ni la licence ni la qualité d'origine du matériel : c'est l'empaquetage.

Ça vaut la peine de regarder ça avec un peu de scepticisme aussi. Un paquet maintenu par une seule personne est un point unique de défaillance, et la réponse sensée est celle de toujours : figez la version, mettez en miroir les assets que vous utilisez et ne construisez pas l'expérience centrale du produit sur la supposition que le dépôt continuera de recevoir des commits en 2028. Avec une licence ouverte, le pire scénario est que vous mainteniez un fork — ce qui est bien mieux que le pire scénario d'une banque d'images commerciale, où la licence expire tout simplement.

Si vous avez une app d'entraînement rangée dans un tiroir parce que la partie visuelle semblait infaisable, l'argument est tombé. Ce sont 302 exercices, trois images chacun, une API de trois fonctions et une licence qui demande du crédit, pas de l'argent. Le travail qui reste est le vôtre pour de vrai : la fiche, la progression des charges, l'historique — la partie qui différencie le produit et qu'aucun paquet ne livrera clé en main.

Allez, on y va! 🦅

📚 Vous Voulez Suivre Ce Qui Arrive?

Cet article a couvert le Workout Guide et comment utiliser 302 exercices illustrés en open source dans votre app, mais l'écosystème change chaque semaine et tout ne devient pas un article ici.

Sur X je partage ce que je teste, les coulisses des projets et les nouveautés qui apparaissent avant de devenir un post.

Suivez-Moi Là-Bas

👉 Suivre @jeffbruchado sur X

💡 Du contenu quotidien sur le développement, la carrière et les outils que j'utilise vraiment

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires