# 11 — Images en full JS : blocs `pixel` et `textcard` pour motion et stories

**Statut : accepté · Phase : P6 à P11 · Mis à jour : 25/10/2026.**

Ce document reprend l'idée de `sites/plans/pages/archive/BACKLOG_IDEA.md` (section motion.recit.dev)
: « récupération du skill afin de faire le brocoli » et « récupération de projet basic afin de créer
des images en full JS, création de Pixel Image + format Text afin de créer des stories ». Il la
pousse jusqu'à une spécification, au même niveau que `05-video.md` et `08-formats.md`, avec les
mêmes règles : une seule stack, une seule source, aucune métrique inventée, aucune devinette.

Ce plan ne crée pas de nouveau format : deux blocs de contenu suffisent (D1). Le backlog disait «
format Text » ; la question 1 a été tranchée le 24/10/2026, sans l'utilisateur et à sa demande
(décision réversible) : `textcard` est un bloc de `motion` et de `stories`, pas un format. Le
personnage (« le brocoli »), sa persona, ses décors et ses prompts relèvent du plan 14
(`14-ia-scenario.md`), qui en est propriétaire. Ici, on ne traite que ce qui produit des **images**
: le sprite, la carte de texte, l'export.

Placement, domaines, exemples et règles communes aux plans 10 à 14 : `06-roadmap.md`, section «
Backlog : plans 10 à 14 ». En résumé : un lot avancé en P11 (bloc de code ` ```pixel ` et
`textcard`), rien en P4 ni en P5. « Pet » et « le skill » sont identifiés depuis le 25/10
(`pets.md`) : c'est le skill `hatch-pet` d'OpenAI, outil externe, et le personnage s'appelle
**Broc** (§2.3, D9). Seul ce qui dépend du « projet basic » reste bloqué.

## 1. L'idée en une phrase

**Une image de story s'écrit comme le reste : en Markdown ou en JSON, rendue par du code, jamais par
un fichier binaire figé.**

Deux blocs :

- `pixel` : une image en pixel art décrite en **données** (une grille de caractères et une palette,
  ou une graine pour un générateur), rendue en SVG. Pas de PNG à versionner, pas de modèle, pas de
  droits ambigus.
- `textcard` : une carte de texte (citation, titre, chiffre, légende, liste) au format `9:16` ou
  `1:1`, mise en page **autour des blocs emaki existants** (`statement`, `title`, `stat`, `list`).

Ce que cela apporte à « écrire une fois, raconter partout » :

| Sans ces blocs                                                                 | Avec ces blocs                                                                                       |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| une story Instagram demande un outil graphique et un PNG par langue            | la carte est du texte Markdown, traduite par les compagnons `*.fr.md` (plan 09), l'image est refaite |
| un exemple public a besoin d'images sous licence                               | un sprite écrit en grille donne une identité visuelle sans aucun asset tiers                         |
| la mascotte d'une marque est un PNG figé, impossible à animer sans un logiciel | la mascotte est une grille versionnée, avec des poses en `frames`, animée par motion                 |
| les goldens dépendent d'images externes                                        | tout est déterministe : même source, même graine, même SVG, même PNG                                 |
| une carte de story, un plan vidéo et une slide sont trois fichiers             | la même scène se lit en stories, se rend en MP4 `9:16`, s'affiche en slide ou dans la doc            |

## 2. Ce que la recherche a appris

### 2.1 Dans le dépôt et dans emaki

- emaki rend déjà des images fixes : `packages/render/src/render.ts` appelle `renderStill` de
  `@remotion/renderer` (`renderDeckStill`, `renderDeckStills`) après un seul bundle. C'est ce qui
  produit ses goldens PNG. Le même fichier intègre les images locales en data URI (`inlineImages`).
- emaki a un nœud `image` (`src`, `w`, `h`, `fit`, `radius`, `alt`) et un nœud `vector` (chemins
  SVG). Les blocs se déclarent par `defineBlock({ type, props: zod, text })`.
- Les thèmes emaki exposent des jetons `colors` (`bg`, `surface`, `text`, `muted`, `accent`), `data`
  (`peach`, `mint`…), `fonts` et `type` (tailles). C'est la palette naturelle d'un sprite.
- `@recit/cli` déclare déjà `playwright` en peer dépendance optionnelle pour
  `recit export --to pdf`. Le plan 15 déplace ce pair dans `@recit/paged`, dont le sous-export
  `@recit/paged/runner` est le seul module « lancer Chromium, servir un build, attendre prêt,
  mesurer, capturer » de Récit : les captures de ce plan passent par lui.
- Plan 08 : `recit export --to images` doit produire un PNG `1080×1920` par carte (ST3), et le pont
  stories ⇄ motion (ST2) est déjà promis. `:::card` est **déjà** une directive du deck. `sharp` est
  désactivé dans `allowBuilds` et réservé à la découpe du toon.
- Le guide MCP d'emaki pose la règle : le serveur **n'appelle jamais de modèle**, l'hôte réfléchit
  et les outils sont déterministes. `describe_block` sert déjà de catalogue de schémas.
- Aucun skill « brocoli » ni « projet basic » n'a été trouvé dans emaki, dans `.claude/skills`
  (seulement `recit-import` et `upstream-sync`), dans `sites/plans/pages/` ou dans `context.md`. Le
  skill a été identifié le 25/10 par l'utilisateur : `hatch-pet` (§2.3). Le « projet basic » reste
  inconnu.

### 2.2 Techniques d'image en JavaScript

| Technique                                      | Node sans binaire natif               | Verdict pour Récit                                                                                           |
| ---------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| SVG `<rect>` + `shape-rendering="crispEdges"`  | oui (une chaîne)                      | **retenu** : vectoriel, net à toute taille, déterministe, identique en SSR, dans Remotion et dans Playwright |
| Canvas 2D / `OffscreenCanvas.convertToBlob()`  | non (node-canvas, `@napi-rs/canvas`…) | navigateur seulement : bouton « Télécharger PNG » éventuel                                                   |
| satori + resvg                                 | oui en wasm, natif sinon              | **rejeté** : second moteur de mise en page et de texte, trois dépendances, alors que Chromium est déjà là    |
| Remotion `renderStill()`                       | Chrome Headless Shell téléchargé      | **retenu** pour une frame précise d'une scène animée, déjà dans emaki, confiné à `@recit/motion-render`      |
| Playwright `locator.screenshot()`              | Chromium déjà requis par l'export PDF | **retenu** pour les cartes statiques : fidélité exacte au lecteur web, appelé par `@recit/paged/runner`      |
| Encodeurs PNG en JS (pngjs, UPNG.js, fast-png) | oui                                   | inutile tant que Chromium rasterise ; en réserve seulement                                                   |

Pixel art procédural sans dépendance : PRNG à graine (mulberry32 ou sfc32, quelques lignes), sprites
symétriques par demi-grille reflétée avec contour automatique (technique des identicons), dithering
ordonné (matrice de Bayer) ou par diffusion d'erreur (Floyd–Steinberg), quantification vers une
palette (distance RGB ou OKLab). Tout tient dans un module TypeScript sans dépendance.

### 2.3 Le skill `hatch-pet` : c'est « Pet » (répondu le 25/10, vérifié)

L'utilisateur l'a confirmé le 25/10 (`pets.md`) : « Pet » et « le skill du brocoli » sont le skill
open source `hatch-pet` d'OpenAI (`openai/skills`, Apache-2.0), lancé avec l'image de référence de
Broc. Les faits vérifiés (entrées, sorties, lignes, contrôles, licence, `imagegen`) sont dans
`14-ia-scenario.md` §2.2, propriétaire du personnage. Ce qui compte pour les images :

- la sortie est une **planche raster** (PNG ou WebP, `1536×1872`, cases `192×208`, 8 colonnes, 9
  lignes, fond transparent), dans le style de la référence : pour Broc, un rendu 3D doux. Ce n'est
  **pas** une grille ` ```pixel ` ;
- une liste fermée de 9 états, une ligne chacun : `idle` (6 images), `running-right` (8),
  `running-left` (8), `waving` (4), `jumping` (5), `failed` (8), `waiting` (6), `running` (6),
  `review` (6), avec des durées image par image ;
- la première image d'`idle` est l'image fixe de `prefers-reduced-motion` ;
- l'application Codex anime la planche par décalage CSS de l'arrière-plan, case par case : un rendu
  qui ne demande ni canvas ni décodage ;
- la génération est déléguée au skill `imagegen` (outil `image_gen` de l'hôte, ou API OpenAI sur
  demande explicite). `hatch-pet` et `imagegen` restent **hors de Récit** : jamais une dépendance,
  aucun script copié.

### 2.4 lebrocoli.fr

- Le site appartient à l'écosystème de l'utilisateur (Base for Music). Il sert d'exemple « gabarit »
  parce que l'utilisateur l'a nommé.
- La mascotte (`lebrocoli.png`) est un brocoli de dessin animé plat, contour noir épais, sans
  visage, sans texte, fond transparent. Le site ne lui donne **ni nom, ni voix, ni histoire**.
- Polices : Lexend (OFL) pour le texte, Monument Extended (Pangram Pangram, gratuite pour un usage
  personnel seulement) pour les titres. La seconde **ne peut pas** entrer dans un exemple public :
  **Unbounded** (Google Fonts, NT Type, SIL OFL 1.1) la remplace dans le dépôt (décision du
  25/10/2026).
- Les vignettes du site actuel viennent de gstatic, sans licence connue : à ne jamais réutiliser.
- Broc, le personnage nommé le 25/10, n'est **pas** cette mascotte plate : son image de référence
  (`examples/scenario-lebrocoli/cast/broc/reference.png`, 1024×1024, fournie par l'utilisateur)
  montre un brocoli vert souriant au rendu 3D doux, en T-pose, **sans smoking ni casquette**.
  L'image fait foi pour toutes ses images (plan 14, D12) : toute description qui s'en écarte est
  corrigée d'après elle.
- Les couleurs relevées à l'œil sur la mascotte sont des estimations. La palette du sprite devra
  être **extraite de l'image réelle** et validée par l'utilisateur, pas recopiée de ce relevé.

### 2.5 Sources

- Remotion `renderStill` : https://www.remotion.dev/docs/renderer/render-still ; licence :
  https://www.remotion.dev/license
- Satori : https://github.com/vercel/satori ; resvg-js (natif) et `@resvg/resvg-wasm` :
  https://github.com/yisibl/resvg-js
- `OffscreenCanvas.convertToBlob` :
  https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/convertToBlob
- `image-rendering: pixelated` : https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
- Playwright, captures : https://playwright.dev/docs/screenshots
- Dithering ordonné : https://en.wikipedia.org/wiki/Ordered_dithering ; Floyd–Steinberg :
  https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering
- Web Stories : https://amp.dev/documentation/guides-and-tutorials/start/create_successful_stories
- hatch-pet (vérifié le 25/10) :
  https://github.com/openai/skills/blob/main/skills/.curated/hatch-pet/SKILL.md ; lignes :
  https://github.com/openai/skills/blob/main/skills/.curated/hatch-pet/references/animation-rows.md
  ; contrat Codex :
  https://github.com/openai/skills/blob/main/skills/.curated/hatch-pet/references/codex-pet-contract.md
  ; licence : https://github.com/openai/skills/blob/main/skills/.curated/hatch-pet/LICENSE.txt
- imagegen : https://github.com/openai/skills/tree/main/skills/.system/imagegen
- Agent Skills (format `SKILL.md`) : https://agentskills.io/specification
- Droit d'auteur et images générées (US Copyright Office, Part 2, 29/01/2025) :
  https://ipwatchdog.com/2025/01/29/part-two-copyright-office-ai-report-says-creative-prompting-doesnt-constitute-authorship/
- AI Act, article 50 (transparence) :
  https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content

## 3. Décisions proposées

| #   | Sujet                                       | Recommandation                                                                                                                                                                                                                                           |
| --- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| D1  | Nouveau format ou nouveaux blocs ?          | **Tranché (24/10, question 1)** : deux blocs, pas de format. Le format reste `motion` ou `stories`                                                                                                                                                       |
| D2  | Où vit le code pixel ?                      | un **module pur** dans `@recit/components`, utilisé par `PixelImage` et par le bloc animé de `@recit/motion-blocks`. Pas de package `@recit/pixel` pour l'instant (tranché)                                                                              |
| D3  | Syntaxe `pixel` en v1                       | le bloc de code ` ```pixel ` (grille + légende → SVG). Générateurs, fonds générés et images source : plus tard                                                                                                                                           |
| D4  | Syntaxe `textcard`                          | une directive conteneur `:::textcard{variant=…}` qui habille un bloc emaki existant ; d'abord un plan motion `9:16`, puis une carte stories                                                                                                              |
| D5  | Schéma JSON                                 | `PixelBlock` et `TextCardBlock` dans `@recit/core/schema`. En v1, les blocs restent des directives dans le `body` (règle de 06)                                                                                                                          |
| D6  | Scène = ?                                   | inchangée : un plan en motion, une carte en stories. Raccourci motion `--- type=pixel` / `type=textcard`                                                                                                                                                 |
| D7  | Rasterisation                               | **Playwright** pour les cartes (déjà prévu par ST3), par `@recit/paged/runner`. `renderStill` pour une frame animée : plus tard                                                                                                                          |
| D8  | Palettes et polices                         | palette `theme` par défaut. Palettes nommées et police pixel seulement sous licence libre, inscrites dans NOTICE.md                                                                                                                                      |
| D9  | Personnage (Broc)                           | une entrée `entry=character` de la bible du plan 14, avec son kit (plan 14 D12). Ce plan rend ses poses avec un bloc `sprite` qui lit la planche `hatch-pet` importée. Débloqué le 25/10 ; la publication attend la licence de l'image de référence (§8) |
| D10 | Reprise du « skill » et du « projet basic » | le skill est `hatch-pet` : outil externe, jamais repris ni appelé ; sa planche s'importe par une étape manuelle (plan 14 D12). Le « projet basic » reste **bloqué**                                                                                      |
| D11 | Placement dans la feuille de route          | lot avancé en P11 (IM1, IM2). Le reste est plus tard ou bloqué. Pas de phase propre                                                                                                                                                                      |

### D1 — Deux blocs, pas un format (tranché le 24/10)

Le backlog dit « Pixel Image + format Text afin de créer des storys ». Deux lectures sont possibles
:

- **un format « texte »** : une nouvelle manière de lire, faite de cartes de texte seules ;
- **un bloc « carte de texte »** dans les formats existants (`motion`, `stories`).

Recommandation : la seconde. Un format Récit se définit par sa lecture (slide par slide, timeline,
défilement, tap). Une carte de texte au format `9:16` se lit déjà au tap dans `stories` et se joue
déjà en timeline dans `motion`. Ces images vivent donc dans les scènes des formats existants. Elles
s'enregistrent comme blocs emaki (`defineBlock`), et `describe_block` (MCP), la validation et
l'inférence fonctionnent sans code de plus. La question 1 (§8) a été tranchée dans ce sens le 24/10.

### D2 — Où vit le code pixel

| Option                                                                       | Pour                                                    | Contre                                                                |
| ---------------------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------- |
| A. Tout dans `@recit/motion-blocks`                                          | aucun package de plus                                   | deck et docs devraient installer les blocs motion pour une image fixe |
| **B. Module pur dans `@recit/components`, rendu animé dans `motion-blocks`** | aucun package de plus, un sprite fixe s'affiche partout | `@recit/components` grossit d'un module sans dépendance               |
| C. Fondation `@recit/pixel` dès le départ                                    | réutilisable hors de Récit                              | un package à publier avant d'avoir un second consommateur             |

**Tranché (24/10) : B.** Le module ne contient **que des fonctions pures** : lecture de grille,
légende, palette `theme`, grille → rectangles fusionnés → SVG (chaîne ou hast). Pas de DOM.

- `PixelImage` (statique, SSR) et la version statique de `TextCard` vivent dans `@recit/components`.
  Une fondation ne dépend jamais d'un format : deck, docs et report n'installent donc pas
  `@recit/motion-blocks` pour une image fixe ou une carte de texte.
- Les blocs animés `pixel` et `textcard` vivent dans `@recit/motion-blocks`, qui importe le module
  de `@recit/components`.
- **Pas de package `@recit/pixel` pour l'instant** (question 3, tranchée le 24/10). Le nom reste
  réservé (🔒) ; le module n'en est extrait que si un second consommateur hors de
  `@recit/components` apparaît (par exemple le « projet basic », s'il existe).

### D3 — Syntaxe `pixel`

**v1 (lot P11) : le bloc de code ` ```pixel `**, dessiné à la main, lisible et diffable sur GitHub.

````md
---
title: Lorem Sprites
format: motion
lang: en
aspect: 9:16
---

--- id=hand-drawn type=pixel

```pixel id=sprout alt="Lorem sprout, waving" scale=auto fps=4 reveal=scan
. = transparent
k = text
g = accent
m = data.mint

...kk...
..kggk..
.kggggk.
..kmmk..
...kk...

...kk...
..kggk.k
.kggggk.
..kmmk..
...kk...
```

Dolor sit amet.
````

| Élément                     | Rôle                                                                                                                                                              |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ` ```pixel ` (bloc de code) | image dessinée à la main. En tête, la légende `caractère = couleur` ; après une ligne vide, la grille. Plusieurs grilles séparées par une ligne vide = des frames |
| légende                     | **jetons de thème** (`text`, `accent`, `data.mint`) ou couleurs hex. Un sprite écrit avec des jetons suit le thème et le mode sombre                              |
| `scale`                     | facteur d'affichage entier (`auto` = le plus grand entier qui tient)                                                                                              |
| `fps`                       | cadence des frames                                                                                                                                                |
| `reveal`                    | animation d'apparition en motion : `scan`, `none`                                                                                                                 |
| `alt`                       | texte alternatif, **obligatoire** en mode strict, localisable                                                                                                     |

Plus tard (IM3, IM5), sans engagement : directive `::pixel{gen=sprite|noise seed=…}` (générateurs à
graine), fond de scène généré `background=pixel:<seed>`, `reveal=dissolve`, rotation de palette
(`cycle`), image source réduite et tramée (`src` + `dither`). Ces options ne font pas partie de la
syntaxe v1.

Règles (erreurs avec numéro de ligne, jamais de correction silencieuse) :

- `story.md:24 (pixel): row 3 has 7 cells, expected 8` ;
- caractère absent de la légende, jeton de thème inconnu, couleur hex invalide : erreur ;
- frames de tailles différentes : erreur ;
- grille au-delà du plafond de taille (valeur à fixer après mesure, voir risques) : erreur ;
- `alt` manquant : avertissement, erreur en mode strict ;
- deux couleurs de légende identiques : avertissement.

### D4 — Syntaxe `textcard`

Le nom `textcard` évite `:::card` (déjà pris par le deck) et `poster` (attribut vidéo). La carte
existe **d'abord comme plan motion `9:16`** (motion est disponible depuis P4), puis elle est reprise
telle quelle comme carte de `stories` quand le lecteur stories arrive (P11, ST2). Exemple complet :

```md
---
title: Lorem Quotes
format: stories
lang: en
duration: 6s
---

--- id=quote

:::textcard{variant=quote background=theme}
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

— Lorem Ipsum
:::

--- id=headline

:::textcard{variant=title background=gradient}
# Dolor sit amet

consectetur adipiscing
:::

--- id=figure

:::textcard{variant=stat}
:value{data=sales field=revenue agg=sum format=currency}

dolor sit amet this quarter
:::

--- id=steps

:::textcard{variant=list background=theme}
- Lorem ipsum
- Dolor sit
- Amet consectetur
:::
```

| Variante  | Bloc emaki habillé | Contenu attendu                                                    |
| --------- | ------------------ | ------------------------------------------------------------------ |
| `quote`   | `statement`        | un paragraphe, puis une ligne `— attribution` facultative          |
| `title`   | `title`            | un titre, puis un sous-titre facultatif                            |
| `stat`    | `stat`             | un `:value` de `@recit/data`, ou un nombre littéral avec `source=` |
| `caption` | `statement`        | un paragraphe court                                                |
| `list`    | `list`             | une liste de trois à cinq éléments (règle de densité)              |

Attributs communs : `background` (`theme`, `gradient`, une image locale avec `alt`), `aspect`
(hérité de la story, sinon `9:16` ou `1:1`). Le nom `background` est le même que l'attribut de scène
de `stories` (09 §2.3) : il n'y a pas de second nom `bg`. Plus tard : `safe` (préréglages de zones
sûres par plateforme), `mark` (filigrane), `background=pixel:<seed>`.

Principe : **aucun nouveau moteur de texte.** `textcard` est un cadre (marges sûres, fond) autour
d'un bloc existant. En motion, on garde donc les animations déjà écrites (révélation mot à mot de
`statement`, compteur de `stat`).

Ajustement du texte : le composant cherche par dichotomie la plus grande taille de police qui tient,
mesurée dans le navigateur, entre les bornes `type.*` du thème. Avant mesure, le rendu SSR utilise
la borne basse (aucun débordement possible). `@remotion/layout-utils` est exclu, car Remotion reste
confiné à `@recit/motion-render`.

Règles :

- variante inconnue, ou contenu qui ne correspond pas à la variante : erreur avec ligne ;
- `variant=stat` sans `:value` ni `source=` : erreur (règle « aucune métrique inventée ») ;
- texte qui ne tient pas même à la borne basse : erreur de densité, avec la langue concernée
  (`story.fr.md:12 (textcard): text overflows at minimum size`) ;
- fond image sans `alt` en mode strict : erreur (même règle que les fonds de stories, plan 08 §6).

### D5 — Schéma JSON typé

Les schémas zod vont dans `@recit/core/schema`, à côté de ceux du plan 09. Le navigateur utilise
`zod/mini`. Esquisse :

```ts
type PixelBlock = {
  type: "pixel";
  id?: string;
  props: {
    grid: string[] | string[][]; // rows, or frames of rows
    legend: Record<string, string>; // char -> theme token or hex
    scale?: number | "auto";
    fps?: number;
    reveal?: "scan" | "none";
    alt: Localized<string>;
  };
};

type TextCardBlock = {
  type: "textcard";
  id?: string;
  props: {
    variant: "quote" | "title" | "stat" | "caption" | "list";
    body: Localized<string>; // Markdown of the inner block
    attribution?: Localized<string>;
    source?: string; // required for a literal stat
    background?: "theme" | "gradient" | { src: string; alt: Localized<string> };
    aspect?: "9:16" | "1:1";
  };
};
```

Règle commune des plans 10 à 14 (06, « Backlog ») : en v1, un bloc transverse reste une **directive
dans le `body`** Markdown de la scène. `SceneSource` (09 §2.2) ne gagne aucun champ `blocks`.
`PixelBlock` et `TextCardBlock` valident la directive ou le bloc de code, et servent de props aux
blocs emaki (`defineBlock`). En JSON, une scène de stories s'écrit donc :

```json
{
  "$schema": "https://recit.dev/schema/v1/stories.json",
  "kind": "story",
  "version": 1,
  "format": "stories",
  "meta": { "title": { "en": "Lorem Quotes", "fr": "Citations Lorem" } },
  "scenes": [
    {
      "id": "quote",
      "body": {
        "en": ":::textcard{variant=quote}\nLorem ipsum dolor sit amet.\n\n— Lorem Ipsum\n:::",
        "fr": ":::textcard{variant=quote}\nLorem ipsum dolor, en français.\n\n— Lorem Ipsum\n:::"
      }
    }
  ]
}
```

- Champs localisables : `alt`, `body`, `attribution`. Jamais localisables : grille, légende,
  couleurs, tailles, `source` (règle du plan 09 §2.3).
- Équivalence testée (plan 09 §2.4) : `compile(md) == compile(toJson(md))` pour chaque exemple.
- Un champ typé `blocks?` dans `SceneSource` serait un ajout compatible plus tard, décidé une fois
  pour tous les blocs transverses (form, pixel, textcard, map), pas dans ce plan.

### D6 — Scène = quoi, et rendu dans les formats existants

La scène ne change pas de sens. En motion, le raccourci reprend la convention `--- type=…` de
`05-video.md` V3 (`type=` est réservé aux blocs motion) : `--- type=pixel id=sprout` fait du sprite
le bloc principal du plan, et `--- type=textcard variant=quote` fait de la carte le plan entier.

| Format    | `pixel`                                                                          | `textcard`                                                                             |
| --------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `motion`  | bloc animé (`reveal`, `frames`), fonction pure de la frame (lot P11)             | un plan `9:16` ou `1:1`, avec les animations du bloc habillé (lot P11)                 |
| `stories` | image de carte (lot P11)                                                         | **cas principal** : une carte = une `textcard` ; export PNG `1080×1920` (ST3, lot P11) |
| `deck`    | plus tard, hors v1 (question 5) : image fixe (`PixelImage`, `@recit/components`) | plus tard, hors v1 (question 5) : slide pleine, version statique                       |
| `docs`    | plus tard, hors v1 (question 5) : image fixe dans une page ou en MDX             | plus tard, hors v1 (question 5) : encart de citation ou de chiffre, version statique   |
| autres    | plus tard (report, book, toon), sur besoin réel                                  | plus tard                                                                              |

Ponts : `stories ⇄ motion` est déjà prévu en P11 (`08-formats.md` §0) et couvre les deux blocs. Les
ponts `report → stories` (`:::kpi` → `textcard variant=stat`) et `book → stories` (épigraphe →
`textcard variant=quote`) sont plus tard (IM3).

### D7 — Rasterisation : deux moteurs, déjà présents

- **Cartes statiques** (stories, textcard, sprite fixe) : `recit export --to images` (ST3) ouvre le
  lecteur dans le Chromium de Playwright par `@recit/paged/runner` (le module partagé avec
  `recit export --to pdf`, plan 15) et capture chaque carte. Le rendu attend le signal « prêt » du
  contrat d'impression commun (`whenPrintable()` de `@recit/core/print`, plan 15), posé après
  l'ajustement du texte et le chargement des polices, au lieu d'un attribut `data-ready` propre.
  Préréglage v1 : `1080×1920`, déjà dans le plan 08.
- **Plus tard** : frame d'une scène animée (`--frame <n>` ou `still=<n>`) par `renderStill` de
  `@recit/motion-render`, seulement si ce package est installé ; préréglages `1080×1080` et
  `1200×630` ; bouton « Télécharger PNG » par `OffscreenCanvas`.
- Exclus : satori, resvg, `@napi-rs/canvas`, node-canvas, et `sharp` hors du toon.

### D8 — Palettes et polices

- Légende en jetons de thème par défaut (`bg`, `surface`, `text`, `muted`, `accent`, `data.*`). Un
  sprite suit donc le mode sombre et le thème du site.
- Palettes nommées : seulement des palettes publiées sous licence explicite (CC0 ou équivalent),
  chacune avec une ligne NOTICE.md. Aucune en v1.
- `textcard` utilise les polices du thème. Une police pixel n'est possible qu'en OFL, avec sa ligne
  NOTICE.md, et servie localement (pas de CDN).
- Plus tard : un lint de contraste texte / fond de `textcard` (seuils WCAG AA).

### D9 — Le personnage (Broc) : une entrée de la bible du plan 14, rendue par un bloc `sprite`

Le plan 14 est **propriétaire** du personnage, de la bible, du kit et de la provenance des images
(D12 : `character.md` et son jumeau `character.json`, schéma `SpriteSheet`, layout `codex-pet-v1`,
import manuel de la planche). Il n'y a ni `kind: "character"` ni schéma de personnage propre à ce
plan. Ce plan n'ajoute que le **rendu** :

- un bloc `sprite` (props zod `SpriteBlock` dans `@recit/core/schema`) : `src`, `layout`, `pose`,
  `loop`. Il est déclaré par `defineBlock` dans `@recit/motion-blocks` ; c'est une **fonction pure
  de la frame** : l'image à montrer se calcule depuis les durées cumulées de la ligne, puis la case
  s'affiche par `background-position` sur la planche, comme dans l'application Codex. Aucun décodage
  ni canvas, donc le même rendu dans le lecteur web, dans Remotion et dans une capture Playwright ;
- une version statique `SpriteImage` dans `@recit/components` (une case fixe, image 0 par défaut)
  pour les cartes de stories, l'export PNG (ST3) et `prefers-reduced-motion` (image 0 d'`idle`) ;
- une scène utilise le personnage par les attributs communs du plan 14 (`cast=broc pose=waving`) ;
  `::sprite{character=broc pose=waving}` place le sprite dans un bloc précis. Une pose absente du
  layout, ou un personnage sans `sheet`, est une erreur avec ligne ;
- lecture : boucle ou une fois puis tenue, selon le tableau de correspondance du plan 14 D12,
  modifiable par `loop=` ;
- mise à l'échelle lisse (`image-rendering: auto`) : la planche est en 3D, pas en pixel art. Une
  case de `192×208` affichée bien plus grand sera floue : la taille maximale sans perte se mesure
  sur les goldens, elle n'est pas fixée d'avance ;
- `inferred` et provenance : modèle unique du plan 14 (`Trait<T>`, sidecar `kind: "asset"`).
  `recit validate --strict` refuse une planche sans provenance ou sans licence.

Le bloc ` ```pixel ` reste pour les sprites dessinés à la main (IM1). Un Broc en pixel art n'est pas
prévu : ce serait une seconde apparence à maintenir à côté de la référence 3D (§8, question 2).

### D10 — « Récupération du skill » (répondu) et « du projet basic » (bloqué)

- **Le skill** : c'est `hatch-pet` (répondu le 25/10). La décision « génération d'images toujours
  hors de Récit » (plan 14, §8 question 1) s'applique : `hatch-pet` et `imagegen` sont lancés par
  l'auteur hors de Récit, ne sont jamais une dépendance, et aucun de leurs textes ou scripts n'est
  copié. Leurs requêtes et leurs planches sont des données du projet. L'import d'une planche est une
  **étape manuelle documentée** (plan 14 D12) ; une commande `recit import pet` ne viendra que si
  des imports répétés en montrent le besoin.
- **Projet basic** : toujours inconnu, donc aucune fonction conçue d'avance. S'il existe, il passe
  par le skill `recit-import` (renommage, outillage, style), avec sa ligne dans NOTICE.md. Il
  deviendrait alors le second consommateur qui justifie `@recit/pixel` (D2).
- **Skill produit** : s'il en faut un, il est généré par la commande unique `recit skills export`
  (plan 14, IA3.3) depuis la ressource `recit://guide/pixel`, sans second générateur.
- Licence : `hatch-pet` est sous Apache-2.0. Comme rien n'en est repris, il n'a pas de ligne «
  Third-party assets » ; seul le layout `codex-pet-v1` (faits d'interopérabilité réécrits) cite sa
  source (§5).

### D11 — Placement dans la feuille de route

Rien en P4 ni en P5 (l'import de motion est en cours). Lot avancé en **P11** (IM1, IM2), parce que
le lecteur stories et l'export d'images (ST2, ST3) y arrivent et que motion est disponible depuis
P4. IM3 et IM5 sont plus tard, IM4 (débloqué le 25/10) rejoint P16 (Scenario). Détail dans
`06-roadmap.md`, « Backlog : plans 10 à 14 ».

## 4. Packages, site, exemples et tests

### 4.1 Packages

| Package                | Groupe       | Statut     | Contenu                                                                                                                                                                               | Dépendances                                         |
| ---------------------- | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `@recit/components`    | fondations   | ✏️ P11     | module pixel pur (grille, légende, rectangles fusionnés, SVG), `PixelImage`, version statique de `TextCard` ; `SpriteImage` (IM4)                                                     | aucune nouvelle                                     |
| `@recit/core/schema`   | fondations   | ✏️ P11     | `PixelBlock`, `TextCardBlock` ; noms `pixel` et `textcard` dans le registre de directives ; `SpriteBlock` et nom `sprite` (IM4), `SpriteSheet` et layout `codex-pet-v1` (plan 14 D12) | zod (existant)                                      |
| `@recit/motion-blocks` | formats      | ✏️ P11     | blocs animés `pixel` et `textcard` ; `sprite` (IM4)                                                                                                                                   | `@recit/components`                                 |
| `@recit/stories`       | formats      | ✏️ P11     | `textcard` comme carte                                                                                                                                                                | `@recit/motion-blocks` (peer optionnel, déjà prévu) |
| `@recit/mcp`           | distribution | ✏️ P11     | ressource `recit://guide/pixel` ; `describe_block` couvre déjà les deux blocs                                                                                                         | MCP SDK (existant, confiné ici)                     |
| `@recit/pixel`         | fondations   | 🔒 réservé | pas de package pour l'instant ; extraction du module pixel seulement au second consommateur (D2)                                                                                      | aucune                                              |

- Aucune dépendance nouvelle dans le monorepo.
- À reporter : `@recit/pixel` dans la liste des noms npm à réserver de `03-packaging.md` §6.

### 4.2 Site

- **Pas de nouveau sous-domaine** : l'idée du backlog est rangée sous motion.recit.dev. Les images
  ont une **section** `motion.recit.dev/images` (sprites animés, cartes de texte) et une rangée sur
  story.recit.dev (cartes et export PNG).
- La doc reste sur docs.recit.dev (règle anti-doublon), dans la section unique des blocs transverses
  : `docs.recit.dev/components/pixel` et `docs.recit.dev/components/textcard`.

| Composant site-kit  | Rôle sur motion.recit.dev/images                                                                                                                                                                                       |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FormatHero`        | un sprite qui se révèle, écrit en ` ```pixel `, avec la source dépliable                                                                                                                                               |
| `SourceToggle`      | la même carte en rendu, en Markdown et en JSON                                                                                                                                                                         |
| `LocaleDemo`        | une `textcard` en `en` et en `fr`, l'image se refait avec la langue                                                                                                                                                    |
| `ComponentShowcase` | `PixelImage`, les variantes de `textcard`                                                                                                                                                                              |
| `ExportPanel`       | composant unique de `@recit/site-kit`, lit `exports.json` : PNG `1080×1920` et MP4 `9:16` produits en CI, variantes `kind: "png"` et `kind: "mp4"` de `ExportManifestSchema` (`@recit/core`), ajoutées avec IM2 et ST3 |
| `BridgeCard`        | stories ⇄ motion                                                                                                                                                                                                       |
| `TemplateCard`      | `npm create recit -- --template stories-textcards`                                                                                                                                                                     |
| `DocsLinks`         | renvoi vers les pages de docs.recit.dev                                                                                                                                                                                |

### 4.3 Exemples

| Exemple                      | Contenu                                                                                                                                                      | Langues | Phase   |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ------- |
| `examples/motion-pixel`      | sprites dessinés en ` ```pixel `, `reveal`, `frames` ; MP4 `9:16` et goldens                                                                                 | en, fr  | lot P11 |
| `examples/stories-textcards` | les cinq variantes de `textcard`, un `:value` de données Lorem                                                                                               | en, fr  | lot P11 |
| `examples/stories-character` | kit de personnage Lorem : bible à une entrée et planche synthétique `codex-pet-v1` (cases numérotées, dessinées par un script de test), aucune image générée | en, fr  | IM4     |

Contenu Lorem Ipsum, aucun asset tiers. Broc n'a **pas** de dossier propre : son kit (plan 14 D12)
est une section du dossier unique `examples/scenario-lebrocoli`, porté par le plan 14, avec un seul
jeu de lignes NOTICE et une seule exception à la règle Lorem.

### 4.4 Tests

- Unitaires (vitest, module pixel) : déterminisme (même grille → même SVG, octet pour octet), fusion
  des rectangles, lecture de grille et diagnostics.
- Équivalence Markdown ⇄ JSON (plan 09) sur chaque exemple.
- Goldens : cartes via Playwright (`@recit/paged/runner`) ; frames motion selon le moyen de rendu
  des goldens de motion.
- e2e : `e2e/motion.e2e.ts` et `e2e/stories.e2e.ts` étendus (captures clair et sombre, mobile,
  `prefers-reduced-motion`, en et fr), contrôle axe (`role="img"`, `alt`).
- Erreurs : un test par diagnostic (ligne trop courte, caractère inconnu, `stat` sans source,
  débordement par langue).

## 5. Sécurité, vie privée, licences

Sécurité :

- Le SVG est construit à partir de données **validées** : couleurs filtrées par une expression
  stricte (hex ou jeton connu), entiers bornés, aucune chaîne de l'auteur insérée dans un attribut
  SVG hors `alt` (échappé). Pas de `<foreignObject>`, pas de script, pas de lien.
- Plafonds de taille de grille et de nombre de frames, pour éviter un rendu ou un DOM démesuré.
- Plus tard, si `src` est retenu : fichiers locaux du projet seulement, décodés dans Chromium.

Vie privée :

- Aucun appel réseau pour produire une image. Aucune API de génération d'images dans Récit.
- Polices et palettes servies localement, sans CDN.

Droits et IA :

- La provenance et la mention IA suivent le plan 14 (sidecar `kind: "asset"`, `aiGenerated`, AI Act
  article 50). Ce plan n'a pas de second schéma de provenance.
- Les manifestes C2PA présents dans une image d'entrée ne sont pas retirés. Aucune signature en v1.

Lignes NOTICE.md à prévoir :

| Section               | Ligne                                                                                                                                                                      |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| What comes from where | rien de nouveau tant que `@recit/pixel` n'est pas extrait (le module vit dans `packages/components`)                                                                       |
| Third-party assets    | chaque palette nommée ou police pixel embarquée (OFL seulement), plus tard                                                                                                 |
| What comes from where | layout `codex-pet-v1` : faits d'interopérabilité relevés dans `hatch-pet` (Apache-2.0), réécrits ; aucun texte ni script repris (ligne unique, portée par le plan 14 §5.2) |
| Third-party assets    | le « projet basic », s'il est importé, avec son origine et sa licence                                                                                                      |
| Third-party assets    | algorithmes repris plus tard (mulberry32 ou sfc32, Bayer, Floyd–Steinberg) : licence de l'implémentation de référence, ou réécriture                                       |

L'image de référence et la planche de Broc, Lexend et **Unbounded** (la police de titre retenue le
25/10, SIL OFL 1.1) relèvent des lignes NOTICE du dossier unique `examples/scenario-lebrocoli` (plan
14). L'image de référence de Broc est **© vguerand, tous droits réservés** : elle n'est pas couverte
par la licence MIT du dépôt, et sa ligne NOTICE le dit. Monument Extended et les vignettes gstatic
de lebrocoli.fr ne doivent jamais entrer dans le dépôt.

## 6. Phases

Tâches `IM`, lots IM1 à IM5, sous-tâches `IMn.m` (convention de 06, « Backlog »). Rien en P4 ni en
P5.

### IM1 — Le sprite dessiné, fixe et animé (S/M), lot avancé de P11

- [ ] IM1.1 — Module pixel pur dans `@recit/components` : lecture de grille et de légende, jetons de
      thème, grille → rectangles fusionnés → SVG.
- [ ] IM1.2 — Schéma `PixelBlock` dans `@recit/core/schema`, nom `pixel` dans le registre.
- [ ] IM1.3 — Plugin remark du bloc de code ` ```pixel ` dans le module pixel de `@recit/components`
      (le cœur n'en garde que le nom, règle B4), avec les diagnostics de §3 D3.
- [ ] IM1.4 — `PixelImage` (statique, SSR, `role="img"`) et bloc `pixel` animé dans
      `@recit/motion-blocks` (`reveal`, `frames`, repli statique avec `prefers-reduced-motion`),
      raccourci `--- type=pixel`.
- [ ] IM1.5 — `examples/motion-pixel` (en, fr), section `motion.recit.dev/images`, page
      `docs.recit.dev/components/pixel`, ressource `recit://guide/pixel`.
- [ ] Tests : déterminisme, diagnostics, équivalence Markdown ⇄ JSON, goldens, MP4 `9:16`.

**Sortie :** `examples/motion-pixel` se joue sur le web et produit un MP4 `9:16` identique aux
goldens ; la même grille donne deux fois le même SVG ; aucune dépendance ajoutée.

### IM2 — `textcard` : plan motion puis carte stories (M), lot avancé de P11 (avec ST2, ST3)

- [ ] IM2.1 — Schéma `TextCardBlock`, directive `:::textcard`, raccourci `--- type=textcard`.
- [ ] IM2.2 — Bloc `textcard` dans `@recit/motion-blocks` (plan `9:16` d'abord) : variantes,
      ajustement du texte, fonds `theme` et `gradient`. Version statique dans `@recit/components`.
- [ ] IM2.3 — La même `textcard` comme carte dans `@recit/stories`, export PNG `1080×1920` par ST3.
- [ ] IM2.4 — Lints : `stat` sans source, densité par langue.
- [ ] IM2.5 — `examples/stories-textcards` (en, fr), rangée sur story.recit.dev, page
      `docs.recit.dev/components/textcard`, gabarit `stories-textcards`.
- [ ] Tests : goldens des cinq variantes en `en` et `fr`, e2e (tap, clavier), débordement en `fr`.

**Sortie :** une même source `.md` donne le lecteur stories, un MP4 vertical et un PNG par carte,
dans chaque langue, sans dépendance native ajoutée.

### IM3 — Extensions (plus tard, sur besoin réel)

- [ ] Générateurs `::pixel{gen=sprite|noise seed=…}`, fond de scène `background=pixel:<seed>`,
      `reveal=dissolve`, `cycle`.
- [ ] Préréglages `1080×1080` et `1200×630`, zones sûres versionnées, filigrane `mark`.
- [ ] `--frame` / `still=` par `renderStill` (`@recit/motion-render`).
- [ ] Ponts `report → stories` et `book → stories`, ajoutés à `08-formats.md` §0.
- [ ] Lint de contraste de `textcard`.

### IM4 — Personnage en sprites (M), P16 (débloqué le 25/10)

Débloqué par les réponses du 25/10 (`pets.md`) : « Pet » = `hatch-pet`, personnage Broc. La
publication du kit de Broc attend encore la licence de l'image de référence (06, question 19) ; le
repli Lorem avance sans elle.

- [ ] IM4.1 — Bloc `sprite` (`SpriteBlock`) dans `@recit/motion-blocks` et `SpriteImage` dans
      `@recit/components`, résolus depuis la bible et le layout `codex-pet-v1` (plan 14 D12).
- [ ] IM4.2 — `examples/stories-character` en Lorem, avec une planche synthétique ; puis le sprite
      de Broc dans `examples/scenario-lebrocoli` (plan 14, IA5), après accord sur la licence.
- [ ] Tests : image calculée pour chaque frame (durées cumulées), boucle et tenue, image 0 d'`idle`
      en `prefers-reduced-motion`, pose absente (erreur avec ligne), planche de mauvaise taille («
      layout inconnu »), trait inféré refusé par `recit validate --strict`.

**Sortie :** le personnage d'exemple joue chacune des 9 poses dans une story et dans un plan motion
depuis la seule bible, et aucun trait inféré ni aucune planche sans provenance n'est publié.

### IM5 — Options (après P12)

- [ ] Image source réduite et tramée (`src` + `dither`), traitée dans Chromium seulement.
- [ ] Bouton « Télécharger PNG » par `OffscreenCanvas`.
- [ ] Palettes nommées et police pixel, après vérification des licences.
- [ ] Extraction de `@recit/pixel` si un second consommateur apparaît.

**Sortie :** chaque option a son exemple, ses goldens et sa ligne NOTICE.md.

## 7. Risques

| Risque                                                          | Parade                                                                                                                                                         |
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Les blocs motion d'emaki dépendent de Remotion au rendu web     | vérifier après P4-V1 qu'ils se rendent avec un numéro de frame fourni par le lecteur web ; la version statique de `textcard` vit déjà dans `@recit/components` |
| Texte qui déborde dans une langue                               | ajustement par dichotomie, erreur de densité avec la langue et la ligne                                                                                        |
| Goldens instables entre macOS et Linux (polices, anticrénelage) | rendu en conteneur (déjà prévu au plan 05) ; les sprites en `crispEdges` sont peu sensibles                                                                    |
| Grilles très grandes, DOM lourd                                 | fusion des rectangles identiques ; plafond de taille fixé après mesure, pas à l'avance                                                                         |
| Fonctions conçues avant de connaître le « projet basic »        | D10 bloqué pour le projet basic ; `hatch-pet` reste un outil externe, rien n'en est réimplémenté                                                               |
| Planche floue une fois agrandie (cases de `192×208`)            | taille maximale mesurée sur les goldens ; Broc reste petit dans le cadre, ou on régénère une planche plus grande quand un layout le permet                     |
| Mascotte réelle utilisée sans droits clairs                     | dossier unique du plan 14, conditionné à l'accord ; repli Lorem                                                                                                |
| Un chiffre affiché sans source dans une carte                   | `variant=stat` exige `:value` ou `source=`                                                                                                                     |
| Confusion avec `:::card` du deck                                | nom `textcard`, documenté dans le glossaire                                                                                                                    |
| Dérive vers un éditeur graphique                                | hors périmètre : la grille s'écrit en texte                                                                                                                    |

## 8. Questions ouvertes

Les questions communes au backlog (« Pet », « le skill », « le projet basic », la mascotte, le nom
et la voix du personnage, la police de titre, la mention IA, l'exception lebrocoli.fr) sont
regroupées une seule fois dans `06-roadmap.md`, « Questions ouvertes ». « Pet », « le skill » et le
nom du personnage y sont répondus depuis le 25/10 (`pets.md`). Restent ici :

Les questions 1, 3, 4, 5 et 6 ont été tranchées le 24/10/2026, sans l'utilisateur et à sa demande
(décisions réversibles). La question 2 est répondue le 25/10 par `pets.md`.

1. ~~**« Format Text »** : un nouveau format fait de cartes de texte seules, ou une carte de texte
   (`textcard`) dans les formats `motion` et `stories` ?~~ Tranché (24/10) : `textcard` est un bloc
   de `motion` et `stories`, pas un nouveau format (D1).
2. ~~**Le brocoli en pixel art** : grille dessinée à la main (versionnée), ou dérivée de la mascotte
   ? Qui valide la palette ?~~ Répondu (25/10, pets.md) : ni l'un ni l'autre. Broc est en 3D ; ses
   poses viennent de la planche `hatch-pet` générée depuis son image de référence (D9).
   L'utilisateur valide les poses sur la planche contact et les GIF de `hatch-pet`, avant l'import.
   Pas de Broc en pixel art.
3. ~~**Package** : garder le module pixel dans `@recit/components`, ou créer la fondation tout de
   suite ?~~ Tranché (24/10) : le module reste dans `@recit/components` tant qu'il n'a pas de second
   consommateur ; pas de package `@recit/pixel` pour l'instant (nom réservé).
4. ~~**Nom** de la directive texte-image : `textcard` convient-il ?~~ Tranché (24/10) : `textcard`
   est retenu.
5. ~~**Portée v1** : `pixel` et `textcard` dans motion et stories seulement, ou aussi dans deck et
   docs dès P11 ?~~ Tranché (24/10) : v1 limitée à motion et stories.
6. ~~**Placement** : lot avancé en P11, le reste plus tard ou bloqué : d'accord ?~~ Tranché (24/10)
   : oui (ordre du backlog accepté, 06 question 14).
