pyfrilet 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -54
- package/package.json +1 -1
- package/pyfrilet.js +89 -27
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Il combine [p5.js](https://p5js.org/) (dessin 2D) et [Pyodide](https://pyodide.o
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Démarrage rapide
|
|
9
|
-
|
|
9
|
+
```html
|
|
10
10
|
<!doctype html>
|
|
11
11
|
<html lang="fr">
|
|
12
12
|
<head>
|
|
@@ -31,7 +31,7 @@ def draw():
|
|
|
31
31
|
</script>
|
|
32
32
|
</body>
|
|
33
33
|
</html>
|
|
34
|
-
|
|
34
|
+
```
|
|
35
35
|
|
|
36
36
|
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.
|
|
37
37
|
|
|
@@ -118,22 +118,23 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
|
|
|
118
118
|
| `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
|
|
119
119
|
| `getCanvas()` | Retourne le `p5.Element` wrappant le canvas. Utile pour appeler des méthodes p5.Element comme `.drop()`. À appeler dans `setup()` ou après. |
|
|
120
120
|
| `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…). |
|
|
121
|
+
| `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. |
|
|
121
122
|
|
|
122
123
|
#### Propriétés dynamiques
|
|
123
124
|
|
|
124
125
|
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 *` :
|
|
125
|
-
|
|
126
|
+
```python
|
|
126
127
|
mouseX, mouseY # position de la souris dans le repère du canvas
|
|
127
128
|
width, height # dimensions logiques du canvas
|
|
128
129
|
frameCount # numéro de la frame courante
|
|
129
130
|
key # dernière touche appuyée (caractère)
|
|
130
131
|
keyCode # code numérique de la dernière touche
|
|
131
|
-
|
|
132
|
+
```
|
|
132
133
|
|
|
133
134
|
> **Note sur `keyCode`** : p5.js expose des constantes nommées (`LEFT_ARROW`, `RIGHT_ARROW`, `UP_ARROW`, `DOWN_ARROW`, `ENTER`, `BACKSPACE`…) directement importables avec `from p5 import *`.
|
|
134
135
|
|
|
135
136
|
### Exemple : plein écran réactif
|
|
136
|
-
|
|
137
|
+
```python
|
|
137
138
|
from p5 import *
|
|
138
139
|
|
|
139
140
|
def setup():
|
|
@@ -149,10 +150,10 @@ def draw():
|
|
|
149
150
|
noStroke()
|
|
150
151
|
fill('#7aa2f7')
|
|
151
152
|
circle(width / 2, height / 2, min(width, height) * 0.4)
|
|
152
|
-
|
|
153
|
+
```
|
|
153
154
|
|
|
154
155
|
### Exemple : navigation clavier
|
|
155
|
-
|
|
156
|
+
```python
|
|
156
157
|
from p5 import *
|
|
157
158
|
|
|
158
159
|
page = 0
|
|
@@ -171,12 +172,12 @@ def draw():
|
|
|
171
172
|
fill('#e0af68')
|
|
172
173
|
textSize(16)
|
|
173
174
|
text("page " + str(page), 160, 150)
|
|
174
|
-
|
|
175
|
+
```
|
|
175
176
|
|
|
176
177
|
### Packages Python tiers
|
|
177
178
|
|
|
178
179
|
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 :
|
|
179
|
-
|
|
180
|
+
```python
|
|
180
181
|
from p5 import *
|
|
181
182
|
import numpy as np # chargé automatiquement
|
|
182
183
|
import networkx as nx # chargé automatiquement
|
|
@@ -186,7 +187,7 @@ def setup():
|
|
|
186
187
|
|
|
187
188
|
def draw():
|
|
188
189
|
background(20)
|
|
189
|
-
|
|
190
|
+
```
|
|
190
191
|
|
|
191
192
|
Un message "Chargement des dépendances…" s'affiche pendant le téléchargement. Seuls les packages inclus dans la [distribution Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) sont supportés (numpy, scipy, pandas, networkx, pillow…). Les packages pip arbitraires ne sont pas supportés.
|
|
192
193
|
|
|
@@ -195,7 +196,7 @@ Un message "Chargement des dépendances…" s'affiche pendant le téléchargemen
|
|
|
195
196
|
### Glisser-déposer de fichiers
|
|
196
197
|
|
|
197
198
|
Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `safe_proxy` :
|
|
198
|
-
|
|
199
|
+
```python
|
|
199
200
|
from p5 import *
|
|
200
201
|
|
|
201
202
|
img = None
|
|
@@ -217,7 +218,7 @@ def draw():
|
|
|
217
218
|
background(40)
|
|
218
219
|
if img:
|
|
219
220
|
image(img, 0, 0, width, height)
|
|
220
|
-
|
|
221
|
+
```
|
|
221
222
|
|
|
222
223
|
`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.
|
|
223
224
|
|
|
@@ -226,21 +227,21 @@ def draw():
|
|
|
226
227
|
### Note sur `smooth()` / `noSmooth()` et le texte
|
|
227
228
|
|
|
228
229
|
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()` :
|
|
229
|
-
|
|
230
|
+
```python
|
|
230
231
|
noSmooth()
|
|
231
232
|
rect(10, 10, 80, 80) # bords nets
|
|
232
233
|
smooth()
|
|
233
234
|
textSize(14)
|
|
234
235
|
text("lisible", 10, 120) # texte antialiasé
|
|
235
|
-
|
|
236
|
+
```
|
|
236
237
|
|
|
237
238
|
---
|
|
238
239
|
|
|
239
240
|
### Gestion des erreurs
|
|
240
241
|
|
|
241
|
-
Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par [rich](https://rich.readthedocs.io/) dans le terminal intégré : cadre coloré, fichier et numéro de ligne, extrait du code source. Les frames internes de pyfrilet sont automatiquement filtrées — seul le code utilisateur apparaît.
|
|
242
|
+
Quelle que soit l'origine de l'erreur (mode p5 ou mode terminal), pyfrilet affiche un traceback formaté par [rich](https://rich.readthedocs.io/) 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.
|
|
242
243
|
|
|
243
|
-
En mode p5, le sketch est arrêté dès la première erreur et le traceback s'affiche en overlay semi-transparent par-dessus le canvas. Corriger le code et relancer suffit à reprendre.
|
|
244
|
+
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.
|
|
244
245
|
|
|
245
246
|
---
|
|
246
247
|
|
|
@@ -249,7 +250,7 @@ En mode p5, le sketch est arrêté dès la première erreur et le traceback s'af
|
|
|
249
250
|
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](https://xtermjs.org/) qui reçoit `stdout` et `stderr`.
|
|
250
251
|
|
|
251
252
|
Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, visualisations texte — sans aucun lien avec p5.
|
|
252
|
-
|
|
253
|
+
```python
|
|
253
254
|
# Pas d'import p5 → mode terminal automatique
|
|
254
255
|
import asyncio
|
|
255
256
|
|
|
@@ -258,27 +259,27 @@ for i in range(5):
|
|
|
258
259
|
await asyncio.sleep(0.5)
|
|
259
260
|
|
|
260
261
|
print("terminé !")
|
|
261
|
-
|
|
262
|
+
```
|
|
262
263
|
|
|
263
264
|
### `input()`
|
|
264
265
|
|
|
265
266
|
`input()` est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
|
|
266
|
-
|
|
267
|
+
```python
|
|
267
268
|
nom = input("Ton prénom : ")
|
|
268
269
|
print(f"Bonjour, {nom} !")
|
|
269
|
-
|
|
270
|
+
```
|
|
270
271
|
|
|
271
272
|
Ctrl+C interrompt une saisie en cours et retourne `None` — prévoir un guard si nécessaire :
|
|
272
|
-
|
|
273
|
+
```python
|
|
273
274
|
val = input("Valeur : ")
|
|
274
275
|
if val is None:
|
|
275
276
|
print("annulé")
|
|
276
|
-
|
|
277
|
+
```
|
|
277
278
|
|
|
278
279
|
### rich
|
|
279
280
|
|
|
280
281
|
`rich` est disponible directement — tableaux, couleurs ANSI, barres de progression :
|
|
281
|
-
|
|
282
|
+
```python
|
|
282
283
|
from rich.console import Console
|
|
283
284
|
from rich.table import Table
|
|
284
285
|
|
|
@@ -291,7 +292,7 @@ table.add_row("Mercure", "4 879")
|
|
|
291
292
|
table.add_row("Vénus", "12 104")
|
|
292
293
|
table.add_row("Terre", "12 742")
|
|
293
294
|
console.print(table)
|
|
294
|
-
|
|
295
|
+
```
|
|
295
296
|
|
|
296
297
|
> **Boucle d'événements** : le mode terminal tourne dans la boucle asyncio de Pyodide. Pour obtenir un rendu progressif avec `rich.Progress` ou toute autre animation, remplacer `time.sleep()` par `await asyncio.sleep()` et passer `auto_refresh=False` aux composants rich qui gèrent leur propre thread de rafraîchissement :
|
|
297
298
|
>
|
|
@@ -307,6 +308,31 @@ console.print(table)
|
|
|
307
308
|
> progress.refresh()
|
|
308
309
|
> ```
|
|
309
310
|
|
|
311
|
+
### Persistence (IndexedDB)
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
|
|
315
|
+
La fonction `persist()` déclenche la synchronisation MEMFS → IndexedDB. Elle fonctionne dans les deux modes sans `await` :
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
import json, os
|
|
319
|
+
|
|
320
|
+
DB = '/persist/data.json'
|
|
321
|
+
|
|
322
|
+
# Lire
|
|
323
|
+
data = json.loads(open(DB).read()) if os.path.exists(DB) else {}
|
|
324
|
+
|
|
325
|
+
# Modifier et persister
|
|
326
|
+
data['compteur'] = data.get('compteur', 0) + 1
|
|
327
|
+
with open(DB, 'w') as f:
|
|
328
|
+
json.dump(data, f)
|
|
329
|
+
persist() # fire-and-forget — fonctionne en p5 et en terminal
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
En mode p5, importer avec `from p5 import persist` ou `from p5 import *`. En mode terminal, `persist` est disponible directement sans import.
|
|
333
|
+
|
|
334
|
+
> 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()` depuis `js` pour attendre explicitement la fin de l'écriture.
|
|
335
|
+
|
|
310
336
|
### Relancer
|
|
311
337
|
|
|
312
338
|
Le bouton ▶ (ou `Shift+Entrée`) interrompt proprement le programme en cours (y compris si `input()` est en attente) et relance l'exécution depuis le début.
|
|
@@ -362,7 +388,7 @@ Tous les blocs Python partagent le même namespace : les variables et fonctions
|
|
|
362
388
|
| `type="text/markdown"` | Le contenu est rendu en Markdown (non exécuté) |
|
|
363
389
|
|
|
364
390
|
### Exemple : énoncé + code utilitaire + zone élève
|
|
365
|
-
|
|
391
|
+
```html
|
|
366
392
|
<script type="text/markdown" data-tab="Énoncé">
|
|
367
393
|
# Exercice
|
|
368
394
|
|
|
@@ -386,12 +412,12 @@ def draw():
|
|
|
386
412
|
background(20)
|
|
387
413
|
# à toi de jouer !
|
|
388
414
|
</script>
|
|
389
|
-
|
|
415
|
+
```
|
|
390
416
|
|
|
391
417
|
### Bloc caché
|
|
392
418
|
|
|
393
419
|
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 :
|
|
394
|
-
|
|
420
|
+
```html
|
|
395
421
|
<script type="text/python" data-hidden>
|
|
396
422
|
# Code invisible — exécuté en premier
|
|
397
423
|
PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
|
|
@@ -408,11 +434,11 @@ def draw():
|
|
|
408
434
|
fill(PALETTE[frameCount // 60 % 3])
|
|
409
435
|
circle(200, 200, 100)
|
|
410
436
|
</script>
|
|
411
|
-
|
|
437
|
+
```
|
|
412
438
|
|
|
413
439
|
### Rendu des onglets Markdown
|
|
414
440
|
|
|
415
|
-
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](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les
|
|
441
|
+
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](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les diagrammes Mermaid utilisent le thème neutre (clair).
|
|
416
442
|
|
|
417
443
|
En mode déploiement local sans accès internet, la fonte se replie sur Georgia.
|
|
418
444
|
|
|
@@ -428,19 +454,19 @@ Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://ma
|
|
|
428
454
|
**Diagrammes (Mermaid) :**
|
|
429
455
|
|
|
430
456
|
Un bloc de code avec le langage `mermaid` est rendu comme un diagramme SVG :
|
|
431
|
-
|
|
432
|
-
```mermaid
|
|
457
|
+
```markdown
|
|
458
|
+
```mermaid
|
|
433
459
|
graph TD
|
|
434
460
|
A[Départ] --> B{Condition}
|
|
435
461
|
B -->|oui| C[Résultat 1]
|
|
436
462
|
B -->|non| D[Résultat 2]
|
|
463
|
+
```
|
|
437
464
|
```
|
|
438
|
-
````
|
|
439
465
|
|
|
440
466
|
Mermaid supporte de nombreux types de diagrammes : `graph`, `sequenceDiagram`, `classDiagram`, `flowchart`, `gantt`, `pie`… Voir la [documentation Mermaid](https://mermaid.js.org/intro/) pour la syntaxe complète.
|
|
441
467
|
|
|
442
468
|
**Exemple combiné :**
|
|
443
|
-
|
|
469
|
+
```html
|
|
444
470
|
<script type="text/markdown" data-tab="Cours">
|
|
445
471
|
# Algorithme de Dijkstra
|
|
446
472
|
|
|
@@ -453,15 +479,15 @@ le sommet $u \notin S$ qui minimise :
|
|
|
453
479
|
$$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
|
|
454
480
|
|
|
455
481
|
La complexité est $O((V + E) \log V)$ avec un tas binaire.
|
|
456
|
-
```mermaid
|
|
482
|
+
```mermaid
|
|
457
483
|
graph LR
|
|
458
484
|
A((1)) -->|4| B((2))
|
|
459
485
|
A -->|1| C((3))
|
|
460
486
|
C -->|2| B
|
|
461
487
|
B -->|1| D((4))
|
|
462
|
-
```
|
|
488
|
+
```
|
|
463
489
|
</script>
|
|
464
|
-
|
|
490
|
+
```
|
|
465
491
|
|
|
466
492
|
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.
|
|
467
493
|
|
|
@@ -476,7 +502,7 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
|
|
|
476
502
|
### Configuration
|
|
477
503
|
|
|
478
504
|
`data-sources` et `data-vendor` se placent de préférence sur la balise `<script src="pyfrilet.js">` elle-même. Pour la rétrocompatibilité, ces attributs sont également acceptés sur le premier bloc `<script type="text/python">`.
|
|
479
|
-
|
|
505
|
+
```html
|
|
480
506
|
<!-- Recommandé -->
|
|
481
507
|
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
|
|
482
508
|
|
|
@@ -488,12 +514,12 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
|
|
|
488
514
|
<script type="text/python" data-sources="local" data-vendor="vendor/">
|
|
489
515
|
…
|
|
490
516
|
</script>
|
|
491
|
-
|
|
517
|
+
```
|
|
492
518
|
|
|
493
519
|
`data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/`.
|
|
494
520
|
|
|
495
521
|
### Structure de fichiers
|
|
496
|
-
|
|
522
|
+
```
|
|
497
523
|
mon-projet/
|
|
498
524
|
├── pyfrilet.js
|
|
499
525
|
├── mon-sketch.html
|
|
@@ -518,59 +544,59 @@ mon-projet/
|
|
|
518
544
|
├── pyodide.asm.wasm
|
|
519
545
|
├── python_stdlib.zip
|
|
520
546
|
└── … (autres fichiers Pyodide)
|
|
521
|
-
|
|
547
|
+
```
|
|
522
548
|
|
|
523
549
|
### Télécharger les dépendances
|
|
524
550
|
|
|
525
551
|
**p5.js**
|
|
526
|
-
|
|
552
|
+
```
|
|
527
553
|
https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js
|
|
528
|
-
|
|
554
|
+
```
|
|
529
555
|
|
|
530
556
|
**ACE editor** (5 fichiers)
|
|
531
|
-
|
|
557
|
+
```
|
|
532
558
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js
|
|
533
559
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js
|
|
534
560
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js
|
|
535
561
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
|
|
536
562
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js
|
|
537
|
-
|
|
563
|
+
```
|
|
538
564
|
|
|
539
565
|
**xterm.js** (4 fichiers)
|
|
540
|
-
|
|
566
|
+
```
|
|
541
567
|
https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css
|
|
542
568
|
https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js
|
|
543
569
|
https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js
|
|
544
570
|
https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js
|
|
545
|
-
|
|
571
|
+
```
|
|
546
572
|
Renommer respectivement en `xterm.min.css`, `xterm.min.js`, `addon-fit.min.js`, `addon-unicode11.min.js`.
|
|
547
573
|
|
|
548
574
|
**marked.js + KaTeX + Mermaid** (uniquement si onglets Markdown)
|
|
549
|
-
|
|
575
|
+
```
|
|
550
576
|
https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
|
|
551
577
|
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
|
|
552
578
|
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
|
|
553
579
|
https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js
|
|
554
580
|
https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
|
|
555
|
-
|
|
581
|
+
```
|
|
556
582
|
Renommer `index.umd.js` en `marked-katex-extension.js` dans le dossier vendor.
|
|
557
583
|
|
|
558
584
|
**Pyodide** — télécharger l'archive complète depuis les releases GitHub :
|
|
559
|
-
|
|
585
|
+
```
|
|
560
586
|
https://github.com/pyodide/pyodide/releases/tag/0.26.4
|
|
561
|
-
|
|
587
|
+
```
|
|
562
588
|
Extraire le contenu dans `vendor/pyodide/`.
|
|
563
589
|
|
|
564
590
|
### Serveur local
|
|
565
591
|
|
|
566
592
|
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 :
|
|
567
|
-
|
|
593
|
+
```bash
|
|
568
594
|
# Python 3
|
|
569
595
|
python -m http.server 8000
|
|
570
596
|
|
|
571
597
|
# Node.js
|
|
572
598
|
npx serve .
|
|
573
|
-
|
|
599
|
+
```
|
|
574
600
|
|
|
575
601
|
Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
576
602
|
|
|
@@ -579,7 +605,7 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
|
579
605
|
## Build et publication
|
|
580
606
|
|
|
581
607
|
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`.
|
|
582
|
-
|
|
608
|
+
```bash
|
|
583
609
|
npm run build # génère pyfrilet.js + pyfrilet.min.js
|
|
584
610
|
````
|
|
585
611
|
|
|
@@ -592,7 +618,7 @@ npm version patch # (ou minor / major) — modifie package.json, comm
|
|
|
592
618
|
npm publish # build automatique puis publication sur npm
|
|
593
619
|
|
|
594
620
|
git push && git push --tags # pousser commits et tag sur Codeberg
|
|
595
|
-
|
|
621
|
+
```
|
|
596
622
|
|
|
597
623
|
---
|
|
598
624
|
|
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -1135,6 +1135,36 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
|
|
|
1135
1135
|
pyodide = await loadPyodide(opts);
|
|
1136
1136
|
await pyodide.loadPackage(['rich', 'pygments']);
|
|
1137
1137
|
|
|
1138
|
+
/* ── Interrupt buffer — nécessite COOP/COEP headers (SharedArrayBuffer) ── */
|
|
1139
|
+
try {
|
|
1140
|
+
const _interruptBuf = new Uint8Array(new SharedArrayBuffer(1));
|
|
1141
|
+
pyodide.setInterruptBuffer(_interruptBuf);
|
|
1142
|
+
window._pfInterrupt = () => {
|
|
1143
|
+
_interruptBuf[0] = 2; /* SIGINT */
|
|
1144
|
+
setTimeout(() => { _interruptBuf[0] = 0; }, 50);
|
|
1145
|
+
};
|
|
1146
|
+
} catch(_e) {
|
|
1147
|
+
/* SharedArrayBuffer indisponible (headers COOP/COEP manquants) — interruption désactivée */
|
|
1148
|
+
window._pfInterrupt = null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/* ── IDBFS helpers — persistence IndexedDB accessible depuis Python ── */
|
|
1152
|
+
window._pfMountIdbfs = (path) => new Promise((resolve, reject) => {
|
|
1153
|
+
try {
|
|
1154
|
+
pyodide.FS.mkdirTree(path);
|
|
1155
|
+
try {
|
|
1156
|
+
pyodide.FS.mount(pyodide.FS.filesystems.IDBFS, {}, path);
|
|
1157
|
+
} catch(e) {
|
|
1158
|
+
if (e.errno !== 10) { reject(e); return; }
|
|
1159
|
+
/* errno 10 = EBUSY : déjà monté, on re-syncfs seulement */
|
|
1160
|
+
}
|
|
1161
|
+
pyodide.FS.syncfs(true, (err) => err ? reject(err) : resolve());
|
|
1162
|
+
} catch(e) { reject(e); }
|
|
1163
|
+
});
|
|
1164
|
+
window._pfSyncIdbfs = () => new Promise((resolve, reject) => {
|
|
1165
|
+
pyodide.FS.syncfs(false, (err) => err ? reject(err) : resolve());
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1138
1168
|
/* Build the "p5" Python module via dynamic introspection of a dummy instance */
|
|
1139
1169
|
pyodide.runPython(`
|
|
1140
1170
|
import sys, types, js
|
|
@@ -1309,9 +1339,6 @@ def _wdog_trace(frame, event, arg):
|
|
|
1309
1339
|
raise TimeoutError("draw() watchdog")
|
|
1310
1340
|
return _wdog_trace
|
|
1311
1341
|
|
|
1312
|
-
from rich.console import Console as _RichConsole
|
|
1313
|
-
_pf_rich_console = _RichConsole(stderr=True)
|
|
1314
|
-
|
|
1315
1342
|
class _PfHandledError(Exception):
|
|
1316
1343
|
"""Levée après que rich a déjà affiché le traceback vers xterm."""
|
|
1317
1344
|
pass
|
|
@@ -1323,10 +1350,10 @@ def _pf_safe_call(fn):
|
|
|
1323
1350
|
raise
|
|
1324
1351
|
except Exception as _e:
|
|
1325
1352
|
_tb = _e.__traceback__
|
|
1326
|
-
while _tb and _tb.tb_frame.f_code.co_filename
|
|
1353
|
+
while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):
|
|
1327
1354
|
_tb = _tb.tb_next
|
|
1328
1355
|
if _tb: _e.__traceback__ = _tb
|
|
1329
|
-
_pf_rich_console.print_exception(show_locals=
|
|
1356
|
+
_pf_rich_console.print_exception(extra_lines=8, show_locals=True)
|
|
1330
1357
|
from js import _pfShowErrorTerminal
|
|
1331
1358
|
_pfShowErrorTerminal()
|
|
1332
1359
|
|
|
@@ -1339,16 +1366,35 @@ def _pf_safe_proxy(fn):
|
|
|
1339
1366
|
setattr(m, 'safe_proxy', _pf_safe_proxy)
|
|
1340
1367
|
_p5_functions.add('safe_proxy')
|
|
1341
1368
|
|
|
1369
|
+
def _pf_persist():
|
|
1370
|
+
"""Synchronise /persist vers IndexedDB (fire-and-forget).
|
|
1371
|
+
Fonctionne en mode p5 (synchrone) et en mode terminal."""
|
|
1372
|
+
from js import _pfSyncIdbfs
|
|
1373
|
+
_pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère
|
|
1374
|
+
|
|
1375
|
+
setattr(m, 'persist', _pf_persist)
|
|
1376
|
+
_p5_functions.add('persist')
|
|
1377
|
+
persist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)
|
|
1378
|
+
|
|
1379
|
+
import linecache as _linecache
|
|
1380
|
+
_pf_run_counter = [0]
|
|
1381
|
+
|
|
1342
1382
|
def _pf_exec_user_code():
|
|
1343
1383
|
_ns = {}
|
|
1384
|
+
_pf_run_counter[0] += 1
|
|
1385
|
+
_pf_fname = f'sketch_{_pf_run_counter[0]}'
|
|
1386
|
+
with open(_pf_fname, 'w') as _f:
|
|
1387
|
+
_f.write(_USER_CODE)
|
|
1388
|
+
lines = _USER_CODE.splitlines(keepends=True)
|
|
1389
|
+
_linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)
|
|
1344
1390
|
try:
|
|
1345
|
-
exec(compile(_USER_CODE,
|
|
1391
|
+
exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)
|
|
1346
1392
|
except Exception as _e:
|
|
1347
1393
|
_tb = _e.__traceback__
|
|
1348
|
-
while _tb and _tb.tb_frame.f_code.co_filename !=
|
|
1394
|
+
while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:
|
|
1349
1395
|
_tb = _tb.tb_next
|
|
1350
1396
|
if _tb: _e.__traceback__ = _tb
|
|
1351
|
-
_pf_rich_console.print_exception(show_locals=
|
|
1397
|
+
_pf_rich_console.print_exception(extra_lines=8, show_locals=True)
|
|
1352
1398
|
from js import _pfShowErrorTerminal
|
|
1353
1399
|
_pfShowErrorTerminal()
|
|
1354
1400
|
return None
|
|
@@ -1361,6 +1407,9 @@ def _pf_draw_watchdog(fn, timeout_ms):
|
|
|
1361
1407
|
sys.settrace(_wdog_trace)
|
|
1362
1408
|
try:
|
|
1363
1409
|
_pf_safe_call(fn)
|
|
1410
|
+
except TimeoutError:
|
|
1411
|
+
from js import _pfShowWatchdogError
|
|
1412
|
+
_pfShowWatchdogError(timeout_ms)
|
|
1364
1413
|
finally:
|
|
1365
1414
|
sys.settrace(None)
|
|
1366
1415
|
|
|
@@ -1423,6 +1472,9 @@ from js import _pfTermWrite, _pfTermWriteErr
|
|
|
1423
1472
|
_sys.stdout = _PfStream(_pfTermWrite)
|
|
1424
1473
|
_sys.stderr = _PfStream(_pfTermWriteErr)
|
|
1425
1474
|
|
|
1475
|
+
from rich.console import Console as _RichConsole
|
|
1476
|
+
_pf_rich_console = _RichConsole(stderr=True)
|
|
1477
|
+
|
|
1426
1478
|
async def _pf_async_input(prompt=""):
|
|
1427
1479
|
from js import _pfTerminalInput
|
|
1428
1480
|
result = await _pfTerminalInput(str(prompt) if prompt else "")
|
|
@@ -1442,20 +1494,25 @@ async def _pf_run_terminal(source):
|
|
|
1442
1494
|
wrapper = _ast.parse("async def programme(): pass")
|
|
1443
1495
|
wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]
|
|
1444
1496
|
_ast.fix_missing_locations(wrapper)
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1497
|
+
_pf_run_counter[0] += 1
|
|
1498
|
+
_pf_fname = f'programme_{_pf_run_counter[0]}'
|
|
1499
|
+
with open(_pf_fname, 'w') as _f:
|
|
1500
|
+
_f.write(source)
|
|
1501
|
+
lines = source.splitlines(keepends=True)
|
|
1502
|
+
_linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)
|
|
1503
|
+
_ns = {'input': _pf_async_input, 'persist': _pf_persist}
|
|
1504
|
+
exec(compile(wrapper, _pf_fname, 'exec'), _ns)
|
|
1448
1505
|
try:
|
|
1449
1506
|
await _ns['programme']()
|
|
1450
|
-
except SystemExit:
|
|
1507
|
+
except (SystemExit, KeyboardInterrupt):
|
|
1451
1508
|
pass
|
|
1452
1509
|
except Exception as _e:
|
|
1453
1510
|
_tb = _e.__traceback__
|
|
1454
|
-
while _tb and _tb.tb_frame.f_code.co_filename !=
|
|
1511
|
+
while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:
|
|
1455
1512
|
_tb = _tb.tb_next
|
|
1456
1513
|
if _tb:
|
|
1457
1514
|
_e.__traceback__ = _tb
|
|
1458
|
-
_pf_rich_console.print_exception(show_locals=
|
|
1515
|
+
_pf_rich_console.print_exception(extra_lines=8, show_locals=True)
|
|
1459
1516
|
`);
|
|
1460
1517
|
|
|
1461
1518
|
/* Inject p5 symbols into ACE autocomplete */
|
|
@@ -1518,6 +1575,8 @@ async def _pf_run_terminal(source):
|
|
|
1518
1575
|
termClear();
|
|
1519
1576
|
clearError();
|
|
1520
1577
|
|
|
1578
|
+
await window._pfMountIdbfs('/persist');
|
|
1579
|
+
|
|
1521
1580
|
try {
|
|
1522
1581
|
const runner = pyodide.globals.get('_pf_run_terminal');
|
|
1523
1582
|
await runner(code);
|
|
@@ -1533,12 +1592,13 @@ async def _pf_run_terminal(source):
|
|
|
1533
1592
|
async function runCode() {
|
|
1534
1593
|
if (running) {
|
|
1535
1594
|
if (_terminalRunning) {
|
|
1536
|
-
/* Terminal en cours
|
|
1537
|
-
|
|
1595
|
+
/* Terminal en cours — envoyer KeyboardInterrupt + résoudre input() en attente */
|
|
1596
|
+
window._pfInterrupt && window._pfInterrupt();
|
|
1597
|
+
hideTerminal(); /* résout le _pfTerminalInput en attente avec null */
|
|
1538
1598
|
running = false;
|
|
1539
1599
|
btnRun.classList.remove('pf-running');
|
|
1540
|
-
/* Laisser
|
|
1541
|
-
await new Promise(r => setTimeout(r,
|
|
1600
|
+
/* Laisser Python recevoir et traiter le KeyboardInterrupt */
|
|
1601
|
+
await new Promise(r => setTimeout(r, 80));
|
|
1542
1602
|
} else {
|
|
1543
1603
|
return; /* sketch p5 en cours — ignorer */
|
|
1544
1604
|
}
|
|
@@ -1590,6 +1650,8 @@ async def _pf_run_terminal(source):
|
|
|
1590
1650
|
}
|
|
1591
1651
|
hideTerminal(); /* make sure terminal hidden for p5 mode */
|
|
1592
1652
|
|
|
1653
|
+
await window._pfMountIdbfs('/persist');
|
|
1654
|
+
|
|
1593
1655
|
pyodide.globals.set('_USER_CODE', code);
|
|
1594
1656
|
const pfExecUser = pyodide.globals.get('_pf_exec_user_code');
|
|
1595
1657
|
|
|
@@ -1649,13 +1711,8 @@ async def _pf_run_terminal(source):
|
|
|
1649
1711
|
pyRefresh(pyNs);
|
|
1650
1712
|
pfDrawWatchdog(pyDraw, WATCHDOG_MS);
|
|
1651
1713
|
} catch (e) {
|
|
1652
|
-
const msg = String(e);
|
|
1653
1714
|
stopSketch();
|
|
1654
|
-
|
|
1655
|
-
showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
|
|
1656
|
-
} else {
|
|
1657
|
-
showErrorTerminal('');
|
|
1658
|
-
}
|
|
1715
|
+
showErrorTerminal(''); /* erreur JS inattendue uniquement */
|
|
1659
1716
|
}
|
|
1660
1717
|
});
|
|
1661
1718
|
mousePressedProxy = mkProxy(pyMP);
|
|
@@ -1715,7 +1772,7 @@ async def _pf_run_terminal(source):
|
|
|
1715
1772
|
}
|
|
1716
1773
|
|
|
1717
1774
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1718
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.
|
|
1775
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.3/pyfrilet.min.js';
|
|
1719
1776
|
|
|
1720
1777
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1721
1778
|
<html lang="fr">
|
|
@@ -2063,7 +2120,7 @@ FILLME-SCRIPTS
|
|
|
2063
2120
|
|
|
2064
2121
|
/* ── Clear ── */
|
|
2065
2122
|
function termClear() {
|
|
2066
|
-
if (_xterm) { _xterm.
|
|
2123
|
+
if (_xterm) { _xterm.reset(); } /* reset() efface tout y compris le scrollback et repositionne le curseur en (0,0) */
|
|
2067
2124
|
_inputResolve = null;
|
|
2068
2125
|
_lineBuffer = '';
|
|
2069
2126
|
}
|
|
@@ -2078,6 +2135,11 @@ FILLME-SCRIPTS
|
|
|
2078
2135
|
_xterm.focus();
|
|
2079
2136
|
}
|
|
2080
2137
|
|
|
2138
|
+
window._pfShowWatchdogError = (ms) => {
|
|
2139
|
+
stopSketch();
|
|
2140
|
+
showError(`draw() a dépassé ${ms}ms — sketch arrêté (watchdog).`);
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2081
2143
|
window._pfShowErrorTerminal = () => {
|
|
2082
2144
|
stopSketch();
|
|
2083
2145
|
showErrorTerminal('');
|
|
@@ -2104,7 +2166,7 @@ FILLME-SCRIPTS
|
|
|
2104
2166
|
const res = _inputResolve;
|
|
2105
2167
|
_inputResolve = null;
|
|
2106
2168
|
_lineBuffer = '';
|
|
2107
|
-
res(
|
|
2169
|
+
res(null); /* null → None en Python → propage l'annulation proprement */
|
|
2108
2170
|
}
|
|
2109
2171
|
}
|
|
2110
2172
|
|
package/pyfrilet.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?m:null,mermaid:E?f:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let z;const R=L(S);let I=null;if(R)try{I=JSON.parse(R)}catch(e){I=null}if(I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;I.tabs.map(e).join(",")!==j.map(e).join(",")&&(I._stale=!0)}const T=!(!I||!I._stale);z=I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0?I.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=L(S+":"+t);if(a||"Code"!==e.label||1!==j.length||(a=L(S)),a&&a.trim())return{...e,code:a}}return e});const A=v.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),v=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let z=!1,R=Math.round(.56*window.innerHeight);function I(){document.documentElement.style.setProperty("--pf-drawer-h",R+"px")}function T(){z=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{G(),q&&q.focus()},280)}function A(){z=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{G();const e=V._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){z?A():T()}I();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:z?R:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function N(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(R=a,I(),z||T(),c.style.transition="none",c.style.height=R+"px"),G()}function D(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?A():(R=Math.max(O,Math.min(window.innerHeight-50,r)),I(),z||T()),G())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",N),document.addEventListener("mouseup",D),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",N,{passive:!0}),document.addEventListener("touchend",D);let U=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!V._p||"fit"!==V._mode)return;const e=V._w,n=V._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);f.style.transform=`scale(${r})`}function G(){if("fullscreen"===V._mode?V.size("max"):Y(),J&&"function"==typeof J.windowResized)try{J.windowResized()}catch(e){$(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=V._p?V._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=V._w/n.width,a=V._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",G);let J=null;const V=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function X(){if(Fe(),J){try{J.remove()}catch(e){}J=null}m.innerHTML="",V._p=null,V._mode="fit",V._w=0,V._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),ze&&(ze.destroy(),ze=null)}window.p5py=V;let q=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ae()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_exec_user_code():\n _ns = {}\n try:\n exec(compile(_USER_CODE, '<string>', 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<string>':\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n\n _ns = {'input': _pf_async_input}\n exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)\n try:\n await _ns['programme']()\n except SystemExit:\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,q.completers=[...q.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,ze=null;const Re=300;function Ie(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,qe(),Xe(),H();try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ve(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,30))}ue=!0,y.classList.add("pf-running"),H(),Xe(),X(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await me()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Ie(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),ce.globals.set("_USER_CODE",t);const a=ce.globals.get("_pf_exec_user_code");try{if(!a())return ue=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let r,i,s,d,l,c,p,f,h,b,g,w,v,x;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),r=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!i)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),j=ce.globals.get("_ns"),L=ce.globals.get("_pf_safe_call"),z=e=>e?k(()=>{try{C(j),L(e)}catch(e){Ze("")}}):null;be=l?k(()=>{try{L(l)}catch(e){Ze("")}}):null,he=r?k(()=>{try{L(r)}catch(e){Ze("")}}):null,ye=k(()=>{try{C(j),S(i,Re)}catch(e){const n=String(e);X(),n.includes("TimeoutError")||n.includes("watchdog")?$(`draw() a dépassé ${Re}ms — sketch arrêté (watchdog).`):Ze("")}}),ge=z(s),we=z(p),ve=z(c),xe=z(f),ke=z(h),Ee=z(b),Ce=z(d),Se=z(g),je=z(w),Le=z(v),ze=z(x);const R=E?k(()=>{try{L(E)}catch(e){Ze("")}}):null;let I=!1;J=new p5(e=>{V._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||V.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),I=!0},e.draw=()=>{I&&ye()},e.mousePressed=()=>{I&&ge&&ge()},e.mouseReleased=()=>{I&&we&&we()},e.mouseDragged=()=>{I&&ve&&ve()},e.mouseMoved=()=>{I&&xe&&xe()},e.mouseWheel=e=>{I&&ke&&ke()},e.doubleClicked=()=>{I&&Ee&&Ee()},e.keyPressed=()=>{I&&Ce&&Ce()},e.keyReleased=()=>{I&&Se&&Se()},je&&(e.touchStarted=()=>{I&&je()}),Le&&(e.touchMoved=()=>{I&&Le()}),ze&&(e.touchEnded=()=>{I&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),R&&R()}},m),ue=!1,y.classList.remove("pf-running")}const Pe='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,""")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Pe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Be=null,Oe=[];function We(){const e=V._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{z?A():(R=window.innerHeight-32,I(),T())}),v.addEventListener("click",Me);const Ne="https://codeberg.org/nopid/pyfrilet";function De(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Ne,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(z?A():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await De(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await De(r.marked),await De(r.katex),await De(r.markedKatex),await De(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await De(r.ace),await De(r.acePython),await De(r.aceMonokai),await De(r.aceLangTools),await De(r.aceSearchbox),await De(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await De(r.xterm),await De(r.xtermFit),await De(r.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ue=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Ge(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ue),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ue.style.display&&$e.fit()}).observe(Ue),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}function Je(e){Ke&&Ke.write(e)}function Ve(e){Ge(),Ke.write("[31m"),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("[0m")}function Xe(){Ke&&Ke.clear(),He=null,Ye=""}function qe(){f.style.display="none",Ue.style.display="block",Ue.classList.remove("pf-xterm-overlay"),Ge(),$e.fit(),Ke.focus()}function Ze(e){Ue.style.display="block",Ue.classList.add("pf-xterm-overlay"),Ge(),$e.fit(),e&&(Xe(),Ke.write("[1;31m── Erreur ──────────────────────────────────────[0m\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ue.style.display="none",Ue.classList.remove("pf-xterm-overlay"),f.style.display="",He){const e=He;He=null,Ye="",e("")}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=Je,window._pfTermWriteErr=Ve,window._pfShowErrorTerminal=()=>{X(),Ze("")}}(z,j,S,C,A,T)})}();
|
|
1
|
+
!function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",r="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",f="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:r,pyodideIndex:null,ace:a,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?f:null,mermaid:E?m:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",r=e.hasAttribute("data-hidden"),a=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||r||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:r,readonly:a,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let I;const z=L(S);let R=null;if(z)try{R=JSON.parse(z)}catch(e){R=null}if(R&&1===R.v&&Array.isArray(R.tabs)&&R.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;R.tabs.map(e).join(",")!==j.map(e).join(",")&&(R._stale=!0)}const T=!(!R||!R._stale);I=R&&1===R.v&&Array.isArray(R.tabs)&&R.tabs.length>0?R.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let r=L(S+":"+t);if(r||"Code"!==e.label||1!==j.length||(r=L(S)),r&&r.trim())return{...e,code:r}}return e});const A=v.hasAttribute("data-no-watchdog");!function(e,t,r,a,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),f=document.getElementById("pf-sketch"),m=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),v=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let I=!1,z=Math.round(.56*window.innerHeight);function R(){document.documentElement.style.setProperty("--pf-drawer-h",z+"px")}function T(){I=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{J(),X&&X.focus()},280)}function A(){I=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{J();const e=G._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){I?A():T()}R();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:I?z:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const r=Math.max(0,Math.min(window.innerHeight-50,M.h+t));r<O?(c.style.transition="none",c.style.height="32px"):(z=r,R(),I||T(),c.style.transition="none",c.style.height=z+"px"),J()}function U(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,r=M.y-t,a=M.h+r;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(a<O?A():(z=Math.max(O,Math.min(window.innerHeight-50,a)),R(),I||T()),J())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",U),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",U);let N=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!G._p||"fit"!==G._mode)return;const e=G._w,n=G._h;if(!e||!n)return;const t=l.clientWidth,r=l.clientHeight,a=Math.min(t/e,r/n);m.style.transform=`scale(${a})`}function J(){if("fullscreen"===G._mode?G.size("max"):Y(),q&&"function"==typeof q.windowResized)try{q.windowResized()}catch(e){$(String(e))}X&&X.resize()}window.addEventListener("mousemove",e=>{N=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(N=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=G._p?G._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=G._w/n.width,r=G._h/n.height;return[(N-n.left)*t,(K-n.top)*r]},window.addEventListener("resize",J);let q=null;const G=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const r=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===r&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,r),m.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===r&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,r),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function V(){if(Fe(),q){try{q.remove()}catch(e){}q=null}f.innerHTML="",G._p=null,G._mode="fit",G._w=0,G._h=0,m.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),Ie&&(Ie.destroy(),Ie=null)}window.p5py=G;let X=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",X&&Q[e.id]&&(X.setSession(Q[e.id]),X.setReadOnly(e.readonly),X.focus())}function re(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function ae(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),re(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);X&&n&&Q[n.id]&&(X.setSession(Q[n.id]),X.setReadOnly(n.readonly),X.renderer.updateFull(!0)),re()}function oe(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),X=ace.edit("pf-ace"),X.setTheme("ace/theme/monokai"),X.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),X.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{X.completer?.popup?.isOpen||Ae()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),X.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),X.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),ae(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(r,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(r)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(r+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(r+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),ae(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function fe(){return pe||(pe=(async()=>{const e={};a.pyodideIndex&&(e.indexURL=a.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]);try{const e=new Uint8Array(new SharedArrayBuffer(1));ce.setInterruptBuffer(e),window._pfInterrupt=()=>{e[0]=2,setTimeout(()=>{e[0]=0},50)}}catch(e){window._pfInterrupt=null}if(window._pfMountIdbfs=e=>new Promise((n,t)=>{try{ce.FS.mkdirTree(e);try{ce.FS.mount(ce.FS.filesystems.IDBFS,{},e)}catch(e){if(10!==e.errno)return void t(e)}ce.FS.syncfs(!0,e=>e?t(e):n())}catch(e){t(e)}}),window._pfSyncIdbfs=()=>new Promise((e,n)=>{ce.FS.syncfs(!1,t=>t?n(t):e())}),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and not _tb.tb_frame.f_code.co_filename.startswith(('sketch_', 'programme_')):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_persist():\n \"\"\"Synchronise /persist vers IndexedDB (fire-and-forget).\n Fonctionne en mode p5 (synchrone) et en mode terminal.\"\"\"\n from js import _pfSyncIdbfs\n _pfSyncIdbfs() # Promise — le navigateur l'exécute dès que la stack JS se libère\n\nsetattr(m, 'persist', _pf_persist)\n_p5_functions.add('persist')\npersist = _pf_persist # accessible aussi hors p5 (mode terminal sans import p5)\n\nimport linecache as _linecache\n_pf_run_counter = [0]\n\ndef _pf_exec_user_code():\n _ns = {}\n _pf_run_counter[0] += 1\n _pf_fname = f'sketch_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(_USER_CODE)\n lines = _USER_CODE.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(_USER_CODE), None, lines, _pf_fname)\n try:\n exec(compile(_USER_CODE, _pf_fname, 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n except TimeoutError:\n from js import _pfShowWatchdogError\n _pfShowWatchdogError(timeout_ms)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n _pf_run_counter[0] += 1\n _pf_fname = f'programme_{_pf_run_counter[0]}'\n with open(_pf_fname, 'w') as _f:\n _f.write(source)\n lines = source.splitlines(keepends=True)\n _linecache.cache[_pf_fname] = (len(source), None, lines, _pf_fname)\n _ns = {'input': _pf_async_input, 'persist': _pf_persist}\n exec(compile(wrapper, _pf_fname, 'exec'), _ns)\n try:\n await _ns['programme']()\n except (SystemExit, KeyboardInterrupt):\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != _pf_fname:\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(extra_lines=8, show_locals=True)\n"),X){me(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function me(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,r,a,o){o(null,a.length>0?n:[])}},r=ace.require("ace/ext/language_tools");r&&Array.isArray(r.completers)&&(r.completers=r.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,X.completers=[...X.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,Ie=null;const ze=300;function Re(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,Xe(),Ve(),H(),await window._pfMountIdbfs("/persist");try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ge(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;window._pfInterrupt&&window._pfInterrupt(),Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,80))}ue=!0,y.classList.add("pf-running"),H(),Ve(),V(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await fe()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Re(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),await window._pfMountIdbfs("/persist"),ce.globals.set("_USER_CODE",t);const r=ce.globals.get("_pf_exec_user_code");try{if(!r())return ue=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let a,i,s,d,l,c,p,m,h,b,g,w,v,x;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),a=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),m=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!i)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),j=ce.globals.get("_ns"),L=ce.globals.get("_pf_safe_call"),I=e=>e?k(()=>{try{C(j),L(e)}catch(e){Ze("")}}):null;be=l?k(()=>{try{L(l)}catch(e){Ze("")}}):null,he=a?k(()=>{try{L(a)}catch(e){Ze("")}}):null,ye=k(()=>{try{C(j),S(i,ze)}catch(e){V(),Ze("")}}),ge=I(s),we=I(p),ve=I(c),xe=I(m),ke=I(h),Ee=I(b),Ce=I(d),Se=I(g),je=I(w),Le=I(v),Ie=I(x);const z=E?k(()=>{try{L(E)}catch(e){Ze("")}}):null;let R=!1;q=new p5(e=>{G._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||G.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),R=!0},e.draw=()=>{R&&ye()},e.mousePressed=()=>{R&&ge&&ge()},e.mouseReleased=()=>{R&&we&&we()},e.mouseDragged=()=>{R&&ve&&ve()},e.mouseMoved=()=>{R&&xe&&xe()},e.mouseWheel=e=>{R&&ke&&ke()},e.doubleClicked=()=>{R&&Ee&&Ee()},e.keyPressed=()=>{R&&Ce&&Ce()},e.keyReleased=()=>{R&&Se&&Se()},je&&(e.touchStarted=()=>{R&&je()}),Le&&(e.touchMoved=()=>{R&&Le()}),Ie&&(e.touchEnded=()=>{R&&Ie()}),e.windowResized=()=>{"fullscreen"===G._mode?G.size("max"):Y(),z&&z()}},f),ue=!1,y.classList.remove("pf-running")}const Pe='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.6.3/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const r=[],a="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&r.push(`data-tab="${e.label.replace(/"/g,""")}"`),e.hidden&&r.push("data-hidden"),e.readonly&&r.push("data-readonly");return`<script type="${a}"${r.length?" "+r.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Pe.replace("FILLME-SCRIPTS",n),r=new Blob([t],{type:"text/html;charset=utf-8"}),a=URL.createObjectURL(r),o=Object.assign(document.createElement("a"),{href:a,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(a)}let Be=null,Oe=[];function We(){const e=G._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),r=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${r}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{I?A():(z=window.innerHeight-32,R(),T())}),v.addEventListener("click",Me);const De="https://codeberg.org/nopid/pyfrilet";function Ue(e){return new Promise((n,t)=>{const r=document.createElement("script");r.src=e,r.onload=n,r.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(r)})}E.addEventListener("click",()=>window.open(De,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=I&&X&&X.isFocused&&X.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),X.searchBox?X.searchBox.hide():t.style.display="none",void X.focus();if(n){const n=X.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(I?A():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await Ue(a.p5),a.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=a.katexCss,document.head.appendChild(e),await Ue(a.marked),await Ue(a.katex),await Ue(a.markedKatex),await Ue(a.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ue(a.ace),await Ue(a.acePython),await Ue(a.aceMonokai),await Ue(a.aceLangTools),await Ue(a.aceSearchbox),await Ue(a.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=a.xtermCss,document.head.appendChild(e),await Ue(a.xterm),await Ue(a.xtermFit),await Ue(a.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ne=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Je(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ne),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ne.style.display&&$e.fit()}).observe(Ne),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}function qe(e){Ke&&Ke.write(e)}function Ge(e){Je(),Ke.write("[31m"),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("[0m")}function Ve(){Ke&&Ke.reset(),He=null,Ye=""}function Xe(){m.style.display="none",Ne.style.display="block",Ne.classList.remove("pf-xterm-overlay"),Je(),$e.fit(),Ke.focus()}function Ze(e){Ne.style.display="block",Ne.classList.add("pf-xterm-overlay"),Je(),$e.fit(),e&&(Ve(),Ke.write("[1;31m── Erreur ──────────────────────────────────────[0m\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ne.style.display="none",Ne.classList.remove("pf-xterm-overlay"),m.style.display="",He){const e=He;He=null,Ye="",e(null)}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=qe,window._pfTermWriteErr=Ge,window._pfShowWatchdogError=e=>{V(),$(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{V(),Ze("")}}(I,j,S,C,A,T)})}();
|