pyfrilet
v0.7.5
Published
pyfrilet est un petit système pour publier des animations Python interactives sur une page web. Il combine [p5.js](https://p5js.org/) (dessin 2D) et [Pyodide](https://pyodide.org/) (Python dans le navigateur) avec un éditeur de code intégré qu'on tire dep
Downloads
2,232
Readme
pyfrilet — documentation
pyfrilet est un petit système pour publier des animations Python interactives sur une page web. Il combine p5.js (dessin 2D) et Pyodide (Python dans le navigateur) avec un éditeur de code intégré qu'on tire depuis le bas de la page.
Démarrage rapide
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Mon sketch</title>
<script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
</head>
<body>
<script type="text/python">
from p5 import *
def setup():
size(400, 400)
frameRate(30)
def draw():
background(20)
fill('#e0af68')
circle(mouseX, mouseY, 40)
</script>
</body>
</html>C'est tout. pyfrilet se charge de démarrer Pyodide, de monter le module p5, et de lancer le sketch. Les dépendances (p5.js, Pyodide, ACE) sont chargées depuis des CDN publics par défaut.
Écrire un sketch Python
API p5 disponible
Le module p5 expose toute l'API p5.js via introspection dynamique : au démarrage, pyfrilet instancie p5.js en arrière-plan, inspecte ses membres, et construit le module Python en conséquence. from p5 import * importe donc automatiquement toutes les fonctions et constantes de p5.js, à l'exception des builtins Python qui pourraient entrer en conflit (map, filter, set, int, print, etc.) et des hooks de cycle de vie (setup, draw…) que vous définissez vous-même.
L'autocomplétion dans l'éditeur intégré connaît l'ensemble de ces symboles.
Noms snake_case : pyfrilet accepte les noms de fonctions, callbacks et attributs en style Python et les convertit silencieusement en camelCase. Les deux styles sont équivalents :
- fonctions :
create_graphics,load_image,no_stroke…- callbacks :
key_pressed,mouse_pressed,mouse_dragged…- attributs dynamiques :
key_is_pressed,frame_count,mouse_x,mouse_y…
Les fonctions ci-dessous sont un aperçu des plus couramment utilisées, à titre d'illustration :
| Fonction | Description |
|---|---|
| background(v) | Efface le fond. Valeur grise (0–255) ou couleur CSS ('#ff0000', 'red'…). |
| fill(...) | Couleur de remplissage. |
| stroke(...) | Couleur de contour. |
| noStroke() | Supprime le contour. |
| noFill() | Supprime le remplissage. |
| rect(x, y, w, h) | Rectangle. |
| circle(x, y, d) | Cercle (x, y = centre, d = diamètre). |
| ellipse(x, y, w, h) | Ellipse. |
| line(x1, y1, x2, y2) | Segment. |
| text(s, x, y) | Affiche une chaîne. |
| textSize(sz) | Taille du texte en pixels. |
| textAlign(h, v) | Alignement horizontal et vertical. |
| push() / pop() | Sauvegarde / restaure l'état graphique. |
| translate(x, y) | Déplace l'origine. |
| rotate(angle) | Rotation (angle en radians). |
| scale(sx, sy) | Mise à l'échelle. |
| image(img, x, y) | Affiche une image. |
| loadImage(url) | Charge une image (à appeler dans preload()). |
| random(min, max) | Nombre aléatoire entre min et max. |
| noise(x) | Bruit de Perlin. |
Pour la référence complète, consulter la documentation p5.js.
Cycle de vie
Ces fonctions sont définies par l'utilisateur et appelées automatiquement par pyfrilet :
| Fonction | Rôle |
|---|---|
| preload() | Appelée avant setup(). Utilisée pour charger des ressources (loadImage…) — p5 attend la fin du chargement avant de continuer. |
| setup() | Appelée une seule fois au démarrage. Obligatoire pour appeler size() ou frameRate(). |
| draw() | Appelée à chaque frame. Obligatoire. |
| windowResized() | Appelée quand la zone d'affichage change (redimensionnement de la fenêtre ou du tiroir éditeur). |
| mousePressed() | Bouton souris enfoncé. |
| mouseReleased() | Bouton souris relâché. |
| mouseDragged() | Souris déplacée bouton enfoncé. |
| mouseMoved() | Souris déplacée sans bouton (survol). |
| mouseWheel() | Molette / pinch trackpad. deltaY est accessible via p5py._p.deltaY. |
| doubleClicked() | Double-clic. |
| keyPressed() | Touche enfoncée. keyCode et key sont déjà à jour au moment de l'appel. |
| keyReleased() | Touche relâchée. Utile pour les mouvements continus dans les jeux. |
| keyTyped() | Touche pressée avec caractère imprimable. |
| touchStarted() | Début de contact tactile. touches[] est accessible via p5. |
| touchMoved() | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
| touchEnded() | Fin de contact tactile. |
Watchdog
draw(): pyfrilet surveille le temps d'exécution dedraw()viasys.settrace. Si une exécution dépasse 300 ms, le sketch est arrêté et une erreur est affichée — cela protège le navigateur contre les boucles infinies accidentelles (while True: pass, récursion sans fin…). La trace est vérifiée toutes les 100 lignes Python pour minimiser l'overhead. Pour désactiver ce comportement (calculs intensifs légitimes hors dedraw()), ajouterdata-no-watchdogsur la balise<script src="pyfrilet.js">:<script src="pyfrilet.js" data-no-watchdog></script>
Fonctions pyfrilet (hors p5.js standard)
| Fonction | Description |
|---|---|
| size(w, h) | Crée ou redimensionne le canvas. Le canvas est mis à l'échelle CSS pour remplir l'écran sans déformation. Retourne le p5.Element du canvas. |
| size(w, h, WEBGL) | Crée un canvas en mode WebGL 3D. WEBGL est une constante p5 importable avec from p5 import *. |
| size('max') | Canvas plein écran, redimensionné dynamiquement. |
| createCanvas(w, h) | Alias de size() — accepte les mêmes arguments et retourne le canvas. Permet de copier des exemples p5.js sans adaptation. |
| smooth() | Active l'antialiasing (par défaut). |
| noSmooth() | Désactive l'antialiasing (pixel art). Affecte formes et texte. |
| sketchTitle(s) | Affiche un texte dans la barre de contrôle. À appeler dans setup(). |
| getCanvas() | Retourne le p5.Element wrappant le canvas. Utile pour appeler des méthodes p5.Element comme .drop(). À appeler dans setup() ou après. |
| safe_proxy(fn) | Wrape une fonction Python pour l'utiliser comme callback JS (équivalent de create_proxy) en capturant les erreurs et en les affichant dans le terminal. À préférer à create_proxy pour les callbacks utilisateur (.drop(), événements DOM…). |
| persist() | Synchronise /persist vers IndexedDB (fire-and-forget). Fonctionne en mode p5 et en mode terminal. Disponible via from p5 import * ou directement en mode terminal. |
Propriétés dynamiques
Ces variables sont mises à jour automatiquement avant chaque appel à draw(), keyPressed() et mousePressed(), y compris si elles ont été importées avec from p5 import * :
mouseX, mouseY # position de la souris dans le repère du canvas
width, height # dimensions logiques du canvas
frameCount # numéro de la frame courante
key # dernière touche appuyée (caractère)
keyCode # code numérique de la dernière toucheNote sur
keyCode: p5.js expose des constantes nommées (LEFT_ARROW,RIGHT_ARROW,UP_ARROW,DOWN_ARROW,ENTER,BACKSPACE…) directement importables avecfrom p5 import *.
Exemple : plein écran réactif
from p5 import *
def setup():
size('max')
frameRate(60)
sketchTitle("Mon sketch · Échap → éditeur")
def windowResized():
size('max') # redimensionne le canvas quand le tiroir bouge
def draw():
background(0)
noStroke()
fill('#7aa2f7')
circle(width / 2, height / 2, min(width, height) * 0.4)Exemple : navigation clavier
from p5 import *
page = 0
def setup():
size(400, 300)
def keyPressed():
global page
if keyCode == RIGHT_ARROW: page += 1
if keyCode == LEFT_ARROW: page -= 1
def draw():
background(20)
smooth()
fill('#e0af68')
textSize(16)
text("page " + str(page), 160, 150)Packages Python tiers
pyfrilet détecte automatiquement les imports du sketch et charge les packages disponibles dans la distribution Pyodide avant l'exécution. Il n'y a rien à faire :
from p5 import *
import numpy as np # chargé automatiquement
import networkx as nx # chargé automatiquement
def setup():
size(400, 400)
def draw():
background(20)Un message "Chargement des dépendances…" s'affiche pendant le téléchargement. Seuls les packages inclus dans la distribution Pyodide sont supportés (numpy, scipy, pandas, networkx, pillow…). Les packages pip arbitraires ne sont pas supportés.
richetpygmentssont chargés d'office au démarrage — ils alimentent l'affichage coloré des erreurs et sont disponibles dans vos sketches sans import supplémentaire.
Glisser-déposer de fichiers
Pour recevoir des fichiers glissés sur le canvas, on utilise getCanvas().drop() avec safe_proxy :
from p5 import *
img = None
def setup():
size(400, 400)
getCanvas().drop(safe_proxy(on_drop))
def on_drop(file):
global img
if not file.type.startswith("image"):
return
if img:
img.remove()
img = createImg(file.data, "")
img.hide()
def draw():
background(40)
if img:
image(img, 0, 0, width, height)safe_proxy est préférable à create_proxy (de pyodide.ffi) pour les callbacks utilisateur : les erreurs qui surviennent dans le callback sont capturées et affichées proprement dans le terminal au lieu de disparaître silencieusement dans la console du navigateur.
Note sur smooth() / noSmooth() et le texte
Par défaut le canvas est rendu en mode antialiasé. noSmooth() bascule en mode pixel art — formes et texte sont pixelisés. Pour mélanger les deux dans le même draw() :
noSmooth()
rect(10, 10, 80, 80) # bords nets
smooth()
textSize(14)
text("lisible", 10, 120) # texte antialiaséGestion des erreurs
Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par rich dans le terminal intégré : cadre coloré, fichier et numéro de ligne, extrait du code source avec quelques lignes de contexte autour de la ligne fautive. Les frames internes de pyfrilet sont automatiquement filtrées — seul le code utilisateur apparaît.
En mode p5, le sketch est arrêté dès la première erreur (y compris les erreurs au niveau module, hors des fonctions setup/draw) et le traceback s'affiche en overlay semi-transparent par-dessus le canvas. Corriger le code et relancer suffit à reprendre.
Mode terminal
Quand le code Python ne contient aucun import de p5, pyfrilet bascule automatiquement en mode terminal : le canvas est remplacé par un terminal xterm.js qui reçoit stdout et stderr.
Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, jeux textuels, visualisations — sans aucun lien avec p5.
async/await transparent
Le mode terminal tourne dans la boucle asyncio de Pyodide. Comme input() doit attendre la saisie de l'utilisateur, le moteur d'exécution est fondamentalement asynchrone. pyfrilet s'en charge automatiquement : le code utilisateur n'a pas besoin de connaître async/await.
# Pas d'import p5 → mode terminal automatique
# Ni async, ni await : ça marche simplement
def demander_nombre():
return int(input("Un nombre : "))
n = demander_nombre()
print(f"Le double : {n * 2}")En coulisses, pyfrilet applique une transformation AST avant l'exécution :
- toutes les
defsont converties enasync def - tous les appels de fonctions sont wrappés dans
awaitautomatiquement time.sleep(x)est redirigé versasyncio.sleep(x)pour ne pas geler l'onglet
import time
def compte_a_rebours(n):
for i in range(n, 0, -1):
print(i)
time.sleep(1) # → asyncio.sleep automatiquement
print("Go !")
compte_a_rebours(3)Limites de la transformation automatique
La transformation couvre les cas courants mais a des angles morts :
Les méthodes dunder (__init__, __str__…) restent synchrones. Python les appelle directement sans passer par le wrapper. Elles ne peuvent donc pas appeler input() :
class Animal:
def __init__(self, nom):
self.nom = nom # OK
# self.nom = input() # ← ne fonctionnerait pas ici
def parler(self): # converti en async def automatiquement
return input(f"{self.nom} dit : ") # OK iciLes lambdas restent synchrones (Python ne supporte pas les lambdas async) — ne pas y appeler input().
Le sleep rebindé n'est pas détecté. pyfrilet reconnaît sleep(x), time.sleep(x) et asyncio.sleep(x) et les redirige tous vers un sleep annulable. En revanche mon_sleep = time.sleep; mon_sleep(x) ne sera pas intercepté.
Quand écrire async/await explicitement
Pour des programmes plus avancés, on peut écrire async/await directement — pyfrilet les respecte et ne double-wrappe pas :
import asyncio
from rich.progress import Progress
async def charger():
with Progress(auto_refresh=False) as progress:
task = progress.add_task("calcul…", total=100)
for i in range(100):
await asyncio.sleep(0.03)
progress.advance(task)
progress.refresh()
await charger()En pratique : pour des scripts simples avec input() et time.sleep(), pas besoin de async/await. Dès qu'on fait de l'animation fine, des appels réseau, ou qu'on combine plusieurs coroutines, les écrire explicitement est plus sûr et plus lisible.
input()
input() est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
nom = input("Ton prénom : ")
print(f"Bonjour, {nom} !")Ctrl+C interrompt une saisie en cours et retourne None — prévoir un guard si nécessaire :
val = input("Valeur : ")
if val is None:
print("annulé")rich
rich est disponible directement — tableaux, couleurs ANSI, barres de progression :
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Planètes")
table.add_column("Nom")
table.add_column("Diamètre (km)", justify="right")
table.add_row("Mercure", "4 879")
table.add_row("Vénus", "12 104")
table.add_row("Terre", "12 742")
console.print(table)Persistence (IndexedDB)
Pour conserver des données entre les rechargements de page, pyfrilet monte automatiquement un répertoire IndexedDB sur /persist au démarrage de chaque sketch — aussi bien en mode p5 qu'en mode terminal. Ce répertoire est disponible sans aucune configuration.
La fonction persist() déclenche la synchronisation MEMFS → IndexedDB. Elle fonctionne dans les deux modes sans await :
import json, os
DB = '/persist/data.json'
# Lire
data = json.loads(open(DB).read()) if os.path.exists(DB) else {}
# Modifier et persister
data['compteur'] = data.get('compteur', 0) + 1
with open(DB, 'w') as f:
json.dump(data, f)
persist() # fire-and-forget — fonctionne en p5 et en terminalEn mode p5, importer avec from p5 import persist ou from p5 import *. En mode terminal, persist est disponible directement sans import.
La synchronisation est asynchrone côté navigateur : la Promise est lancée dès l'appel et exécutée dès que la stack JS se libère. En mode terminal, on peut aussi
await _pfSyncIdbfs()depuisjspour attendre explicitement la fin de l'écriture.
Relancer
Le bouton ▶ (ou Shift+Entrée) interrompt le programme en cours et relance l'exécution depuis le début. L'interruption est propre même si le programme est en attente de input() ou en train de dormir dans un time.sleep() — ces deux opérations sont annulables sans délai. Si le programme est suspendu sur autre chose (réseau, calcul long…), pyfrilet attend au maximum 3 secondes avant de forcer le nouveau départ.
Cliquer plusieurs fois rapidement sur ▶ est sans danger : seul le dernier clic produit une exécution, les intermédiaires sont ignorés.
Mode sans éditeur
Deux attributs permettent d'intégrer pyfrilet dans un contexte où l'éditeur de code n'est pas souhaité.
data-no-editor
Cache entièrement le tiroir éditeur. Le sketch tourne en pleine page, sans interface visible. Les erreurs s'affichent en overlay semi-transparent au bas du canvas plutôt que dans le tiroir.
<script src="pyfrilet.js" data-no-editor></script>Shift+Entrée reste actif pour relancer le sketch. Tous les autres raccourcis clavier (Échap, Ctrl+S, Ctrl+R) sont désactivés pour ne pas interférer avec la page hôte.
data-pyfrilet-detached
Mode de rendu découplé, conçu pour intégrer un sketch dans une page qui a sa propre mise en page (présentation RevealJS, article interactif…). Implique data-no-editor.
<script src="pyfrilet.js" data-pyfrilet-detached></script>Différences avec le mode normal :
- Le markup pyfrilet est ajouté à
document.bodyau lieu de le remplacer — la page hôte est préservée. #pf-sketch(le conteneur du canvas) est rendu hors-écran et exposé souswindow.__pyfriletSketchElpour qu'un plugin externe puisse le déplacer dans n'importe quel élément de la page.- Le localStorage est ignoré — le code Python est toujours lu depuis le HTML source. Dans un contexte intégré, la page est la source de vérité.
Intégration RevealJS (reveal-pyfrilet.js)
reveal-pyfrilet.js est un plugin RevealJS qui connecte un sketch pyfrilet à une présentation. Le canvas devient un élément comme les autres dans les slides — RevealJS gère entièrement le layout.
Principe
Le plugin crée un conteneur hors-écran (le « garage ») au démarrage. À chaque changement de slide, il déplace #pf-sketch dans l'élément [data-pyfrilet-canvas] de la slide active, et appelle window.pyfrilet_on_step(n) avec le step déclaré sur la section. Le canvas est ainsi partagé entre toutes les slides — l'état Python persiste d'une slide à l'autre.
Démarrage rapide
1. Déclarer le sketch hors des slides :
<script src="pyfrilet.js" data-pyfrilet-detached></script>
<script type="text/python">
from p5 import *
import js
step = 0
def setup():
size(540, 380)
frame_rate(30)
def draw():
background(20)
# utilise la variable `step` pour changer d'état visuel
def on_step(n, payload=None):
global step
step = int(n)
js.window.pyfrilet_on_step = on_step
</script>2. Placer [data-pyfrilet-canvas] dans les slides qui doivent afficher le canvas :
<section data-pyfrilet-step="1">
<div class="slide-split">
<div class="text">
<h2>Mon titre</h2>
<p>Mon texte...</p>
</div>
<div data-pyfrilet-canvas></div>
</div>
</section>Les slides sans [data-pyfrilet-canvas] n'affichent pas le canvas — le sketch continue de tourner en arrière-plan.
3. Initialiser RevealJS avec le plugin :
<script src="reveal.js"></script>
<script src="reveal-pyfrilet.js"></script>
<script>
Reveal.initialize({
plugins: [ RevealPyfrilet ],
});
</script>Attributs
| Attribut | Emplacement | Rôle |
|---|---|---|
| data-pyfrilet-step="n" | <section> | Step envoyé à on_step(n) à l'activation de la slide |
| data-pyfrilet-step="n" | .fragment | Step envoyé à on_step(n) à l'apparition du fragment |
| data-pyfrilet-canvas | n'importe quel élément dans la slide | Emplacement où le canvas est injecté |
Dimensionnement du canvas
Le plugin injecte max-width:100%; max-height:100%; width:auto; height:auto sur le canvas. Le slot [data-pyfrilet-canvas] doit donc être un conteneur dont la largeur et la hauteur sont définies — le canvas s'y inscrit en conservant son ratio (défini par size(W, H) en Python), exactement comme object-fit:contain. La mise en page à l'intérieur des slides est entièrement libre et ne dépend pas du plugin.
Relancer le sketch en cours de présentation
Shift+Entrée relance le sketch (Python repart de zéro). Le plugin écoute l'événement pyfrilet:ready déclenché à la fin du re-run et ré-envoie automatiquement le step de la slide courante — le sketch restaure son état visuel sans intervention.
Hook Python
Le seul contrat entre le sketch et le plugin est l'exposition d'un callable JS :
import js
def on_step(n, payload=None):
global step
step = int(n)
js.window.pyfrilet_on_step = on_steppayload est toujours None dans cette version — réservé pour des données structurées futures.
Interface utilisateur
La barre de contrôle est collée en bas de l'écran.
| Élément | Action | |---|---| | ☰ (hamburger, à gauche) | Ouvre / ferme le tiroir éditeur | | Glisser la barre vers le haut/bas | Redimensionne le tiroir — relâcher sous 120 px le referme | | ▶ | Relance l'exécution du code (sans fermer l'éditeur) | | ✏️ | Ouvre l'éditeur en plein écran ; referme si déjà ouvert | | 💾 | Télécharge la page en HTML autonome (voir ci-dessous) | | ⏺ | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement — cliquer pour arrêter et télécharger | | ↻ | Réinitialise tous les onglets à la version du fichier source (confirmation demandée). Un point orange apparaît sur le bouton dès qu'un onglet a été modifié par rapport à sa version d'origine. |
Raccourcis clavier
| Raccourci | Action |
|---|---|
| Shift+Entrée | Relance l'exécution (depuis l'éditeur ou depuis le sketch) |
| Échap | Ouvre le tiroir si fermé, ferme si ouvert |
| Ctrl+S | Sauvegarde le code dans le localStorage |
| Ctrl+R | Réinitialise tous les onglets à la version d'origine (confirmation demandée) |
Quand le tiroir se ferme, le focus clavier est automatiquement donné au canvas — les événements keyPressed() du sketch fonctionnent sans clic préalable.
Le code est sauvegardé automatiquement dans le localStorage à chaque modification. pyfrilet enregistre un snapshot JSON complet de tous les onglets (structure, types, contenus — y compris les blocs cachés et Markdown). À la prochaine visite, la page s'affiche exactement telle qu'elle a été laissée, avec le même nombre d'onglets et les mêmes contenus. Cliquer sur ↻ efface le snapshot et restaure la structure et le contenu du fichier HTML source.
Télécharger
Le bouton 💾 génère un fichier sketch.html autonome : il référence pyfrilet depuis jsDelivr et reconstruit tous les blocs <script> tels qu'ils sont dans l'éditeur au moment du clic, en préservant leur structure (onglets, blocs cachés, blocs en lecture seule). Le fichier produit charge p5.js et Pyodide depuis le CDN et n'a besoin d'aucun serveur pour fonctionner.
Onglets (utilisation avancée)
Quand une page contient plusieurs blocs de code, pyfrilet affiche une barre d'onglets dans l'éditeur. Chaque bloc est déclaré avec un <script> séparé.
Tous les blocs Python partagent le même namespace : les variables et fonctions définies dans un bloc sont visibles dans les autres.
Attributs disponibles
| Attribut | Effet |
|---|---|
| data-tab="Nom" | Crée un onglet visible avec ce libellé |
| data-readonly | L'onglet est visible mais non modifiable (cadenas affiché) |
| data-hidden | Le bloc est exécuté mais n'apparaît pas dans l'éditeur |
| type="text/markdown" | Le contenu est rendu en Markdown (non exécuté) |
Exemple : énoncé + code utilitaire + zone élève
<script type="text/markdown" data-tab="Énoncé">
# Exercice
Complète la fonction `dessiner()` pour afficher un cercle rouge au centre du canvas.
</script>
<script type="text/python" data-tab="Utilitaires" data-readonly>
# Fonctions fournies — non modifiables
def rouge():
fill(255, 0, 0)
no_stroke()
</script>
<script type="text/python" data-tab="Ta solution">
from p5 import *
def setup():
size(400, 400)
def draw():
background(20)
# à toi de jouer !
</script>Bloc caché
Un bloc data-hidden est concaténé au code exécuté mais invisible dans l'éditeur. Utile pour du code d'initialisation ou de vérification qu'on ne souhaite pas exposer :
<script type="text/python" data-hidden>
# Code invisible — exécuté en premier
PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
</script>
<script type="text/python" data-tab="Sketch">
from p5 import *
def setup():
size(400, 400)
def draw():
background(20)
fill(PALETTE[frameCount // 60 % 3])
circle(200, 200, 100)
</script>Rendu des onglets Markdown
Les onglets Markdown sont affichés sur fond clair avec une mise en page soignée : largeur de lecture limitée à 680 px, centrage automatique sur grand écran, police Alegreya Sans (chargée depuis Google Fonts en mode CDN). Les diagrammes Mermaid utilisent le thème neutre (clair).
En mode déploiement local sans accès internet, la fonte se replie sur Georgia.
Le contenu d'un onglet type="text/markdown" est rendu avec marked et supporte les formules mathématiques via KaTeX et les diagrammes via Mermaid.
Formules mathématiques (KaTeX) :
| Syntaxe | Rendu |
|---|---|
| $f(x) = x^2$ | Formule inline dans le texte |
| $$\sum_{i=0}^{n} i = \frac{n(n+1)}{2}$$ | Bloc centré sur sa propre ligne |
Diagrammes (Mermaid) :
Un bloc de code avec le langage mermaid est rendu comme un diagramme SVG :
```mermaid
graph TD
A[Départ] --> B{Condition}
B -->|oui| C[Résultat 1]
B -->|non| D[Résultat 2]
```Mermaid supporte de nombreux types de diagrammes : graph, sequenceDiagram, classDiagram, flowchart, gantt, pie… Voir la documentation Mermaid pour la syntaxe complète.
Exemple combiné :
<script type="text/markdown" data-tab="Cours">
# Algorithme de Dijkstra
Soit un graphe $G = (V, E)$ avec des poids $w(u, v) \geq 0$.
L'algorithme maintient un ensemble $S$ de sommets dont la distance
minimale depuis la source $s$ est connue. À chaque étape on extrait
le sommet $u \notin S$ qui minimise :
$$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
La complexité est $O((V + E) \log V)$ avec un tas binaire.
```mermaid
graph LR
A((1)) -->|4| B((2))
A -->|1| C((3))
C -->|2| B
B -->|1| D((4))
```
</script>Les blocs Python sont concaténés dans l'ordre du DOM avant exécution : blocs cachés et visibles sont traités ensemble, dans l'ordre où ils apparaissent dans le HTML. L'onglet actif dans l'éditeur n'influence pas l'exécution.
Numéros de ligne : les numéros affichés par l'éditeur ACE dans chaque onglet correspondent aux numéros réels dans le code Python exécuté. Un message d'erreur mentionnant la ligne 142 pointe donc directement vers la bonne ligne dans le bon onglet.
Déploiement local (mode vendor/)
Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour un déploiement entièrement local — intranet, usage hors ligne, ou pour ne pas dépendre de services tiers — on peut héberger les dépendances soi-même.
Configuration
Ces attributs se placent de préférence sur la balise <script src="pyfrilet.js"> elle-même. Pour la rétrocompatibilité, data-sources et data-vendor sont également acceptés sur le premier bloc <script type="text/python">.
| Attribut | Valeurs | Description |
|---|---|---|
| data-sources | cdn (défaut) / local | Source des dépendances JS (CDN ou dossier vendor/) |
| data-vendor | chemin | Chemin vers le dossier vendor, relatif à la page HTML (défaut : vendor/) |
| data-no-watchdog | — | Désactive le watchdog draw() (calculs intensifs légitimes) |
| data-no-editor | — | Cache le tiroir éditeur — canvas uniquement, erreurs en overlay |
| data-pyfrilet-detached | — | Mode découplé pour intégration dans une page hôte (implique data-no-editor) |
<!-- CDN, éditeur visible (comportement par défaut) -->
<script src="pyfrilet.js"></script>
<!-- Local, sans éditeur -->
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/" data-no-editor></script>
<!-- Intégration RevealJS -->
<script src="pyfrilet.js" data-pyfrilet-detached></script>
<!-- Rétrocompat (ancienne syntaxe) -->
<script src="pyfrilet.js"></script>
<script type="text/python" data-sources="local" data-vendor="vendor/">
…
</script>data-vendor indique le chemin vers le dossier vendor/ relatif à la page HTML. La valeur par défaut est vendor/.
Structure de fichiers
mon-projet/
├── pyfrilet.js
├── mon-sketch.html
└── vendor/
├── p5.min.js
├── ace.min.js
├── mode-python.min.js
├── theme-monokai.min.js
├── ext-language_tools.min.js
├── ext-searchbox.min.js
├── xterm.min.css
├── xterm.min.js
├── addon-fit.min.js
├── addon-unicode11.min.js
├── marked.min.js ← uniquement si onglets Markdown
├── katex.min.css ← uniquement si onglets Markdown
├── katex.min.js ← uniquement si onglets Markdown
├── marked-katex-extension.js ← uniquement si onglets Markdown
├── mermaid.min.js ← uniquement si onglets Markdown
└── pyodide/
├── pyodide.js
├── pyodide.asm.wasm
├── python_stdlib.zip
└── … (autres fichiers Pyodide)Télécharger les dépendances
p5.js
https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.jsACE editor (5 fichiers)
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.jsxterm.js (4 fichiers)
https://cdn.jsdelivr.net/npm/@xterm/[email protected]/css/xterm.min.css
https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/xterm.min.js
https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/addon-fit.min.js
https://cdn.jsdelivr.net/npm/@xterm/[email protected]/lib/addon-unicode11.min.jsRenommer respectivement en xterm.min.css, xterm.min.js, addon-fit.min.js, addon-unicode11.min.js.
marked.js + KaTeX + Mermaid (uniquement si onglets Markdown)
https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.js
https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.jsRenommer index.umd.js en marked-katex-extension.js dans le dossier vendor.
Pyodide — télécharger l'archive complète depuis les releases GitHub :
https://github.com/pyodide/pyodide/releases/tag/0.26.4Extraire le contenu dans vendor/pyodide/.
Serveur local
Les navigateurs bloquent le chargement de fichiers locaux (file://) pour des raisons de sécurité. Il faut un serveur HTTP minimal pour tester en local :
# Python 3
python -m http.server 8000
# Node.js
npx serve .Puis ouvrir http://localhost:8000/mon-sketch.html.
Build et publication
Le build est géré par build.js (Node.js + Terser). Il concatène les fragments de src/, injecte CSS et HTML, puis génère pyfrilet.js (lisible) et pyfrilet.min.js.
npm run build # génère pyfrilet.js + pyfrilet.min.jsLe hook prepublishOnly dans package.json déclenche le build automatiquement avant chaque npm publish. Le flux complet d'une release :
git add .
git commit -m "feat: ..." # committer le travail
npm version patch # (ou minor / major) — modifie package.json, commit + tag
npm publish # build automatique puis publication sur npm
git push && git push --tags # pousser commits et tag sur CodebergLicence
pyfrilet.js est distribué sous GNU Lesser General Public License v2.1 (LGPL-2.1).
En pratique cela signifie :
- Tu peux l'utiliser dans un projet propriétaire ou open source sans contrainte.
- Si tu modifies pyfrilet.js lui-même, tu dois publier ces modifications sous LGPL.
- Tu n'as pas à inclure p5.js, Pyodide ou ACE dans ton dépôt : pyfrilet les charge dynamiquement, il n'y a pas de liaison statique.
Licences des dépendances
pyfrilet ne contient aucun code de ces bibliothèques ; elles sont chargées séparément au moment de l'exécution. Leurs licences s'appliquent à elles seules :
| Bibliothèque | Licence | |---|---| | p5.js | LGPL 2.1 | | Pyodide | MPL 2.0 | | ACE editor | BSD 3-Clause | | xterm.js | MIT | | marked | MIT | | KaTeX | MIT | | marked-katex-extension | MIT | | Mermaid | MIT | | rich | MIT |
Remerciements
La gestion du module Python p5 s'inspire du travail de Basthon (Romain Casati). L'idée d'instancier p5.js en arrière-plan pour en inspecter les membres dynamiquement, puis de construire le module Python par introspection plutôt qu'en listant les fonctions à la main, est directement issue de cette approche. Merci à Basthon pour ce travail pionnier sur Python dans le navigateur pour l'enseignement.
