pyfrilet 0.5.2 → 0.6.0
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 +146 -69
- package/package.json +1 -1
- package/pyfrilet.js +378 -21
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -6,8 +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
|
-
|
|
10
|
-
```html
|
|
9
|
+
````html
|
|
11
10
|
<!doctype html>
|
|
12
11
|
<html lang="fr">
|
|
13
12
|
<head>
|
|
@@ -32,7 +31,7 @@ def draw():
|
|
|
32
31
|
</script>
|
|
33
32
|
</body>
|
|
34
33
|
</html>
|
|
35
|
-
|
|
34
|
+
````
|
|
36
35
|
|
|
37
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.
|
|
38
37
|
|
|
@@ -118,24 +117,23 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
|
|
|
118
117
|
| `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
|
|
119
118
|
| `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
|
|
120
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
|
+
| `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
121
|
|
|
122
122
|
#### Propriétés dynamiques
|
|
123
123
|
|
|
124
124
|
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
|
|
125
|
+
````python
|
|
127
126
|
mouseX, mouseY # position de la souris dans le repère du canvas
|
|
128
127
|
width, height # dimensions logiques du canvas
|
|
129
128
|
frameCount # numéro de la frame courante
|
|
130
129
|
key # dernière touche appuyée (caractère)
|
|
131
130
|
keyCode # code numérique de la dernière touche
|
|
132
|
-
|
|
131
|
+
````
|
|
133
132
|
|
|
134
133
|
> **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 *`.
|
|
135
134
|
|
|
136
135
|
### Exemple : plein écran réactif
|
|
137
|
-
|
|
138
|
-
```python
|
|
136
|
+
````python
|
|
139
137
|
from p5 import *
|
|
140
138
|
|
|
141
139
|
def setup():
|
|
@@ -151,11 +149,10 @@ def draw():
|
|
|
151
149
|
noStroke()
|
|
152
150
|
fill('#7aa2f7')
|
|
153
151
|
circle(width / 2, height / 2, min(width, height) * 0.4)
|
|
154
|
-
|
|
152
|
+
````
|
|
155
153
|
|
|
156
154
|
### Exemple : navigation clavier
|
|
157
|
-
|
|
158
|
-
```python
|
|
155
|
+
````python
|
|
159
156
|
from p5 import *
|
|
160
157
|
|
|
161
158
|
page = 0
|
|
@@ -174,13 +171,12 @@ def draw():
|
|
|
174
171
|
fill('#e0af68')
|
|
175
172
|
textSize(16)
|
|
176
173
|
text("page " + str(page), 160, 150)
|
|
177
|
-
|
|
174
|
+
````
|
|
178
175
|
|
|
179
176
|
### Packages Python tiers
|
|
180
177
|
|
|
181
178
|
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 :
|
|
182
|
-
|
|
183
|
-
```python
|
|
179
|
+
````python
|
|
184
180
|
from p5 import *
|
|
185
181
|
import numpy as np # chargé automatiquement
|
|
186
182
|
import networkx as nx # chargé automatiquement
|
|
@@ -190,23 +186,23 @@ def setup():
|
|
|
190
186
|
|
|
191
187
|
def draw():
|
|
192
188
|
background(20)
|
|
193
|
-
|
|
189
|
+
````
|
|
194
190
|
|
|
195
191
|
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.
|
|
196
192
|
|
|
197
|
-
|
|
193
|
+
> **`rich` et `pygments`** sont chargés d'office au démarrage — ils alimentent l'affichage coloré des erreurs et sont disponibles dans vos sketches sans import supplémentaire.
|
|
198
194
|
|
|
199
|
-
|
|
195
|
+
### Glisser-déposer de fichiers
|
|
200
196
|
|
|
201
|
-
|
|
197
|
+
Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `safe_proxy` :
|
|
198
|
+
````python
|
|
202
199
|
from p5 import *
|
|
203
|
-
from pyodide.ffi import create_proxy
|
|
204
200
|
|
|
205
201
|
img = None
|
|
206
202
|
|
|
207
203
|
def setup():
|
|
208
204
|
size(400, 400)
|
|
209
|
-
getCanvas().drop(
|
|
205
|
+
getCanvas().drop(safe_proxy(on_drop))
|
|
210
206
|
|
|
211
207
|
def on_drop(file):
|
|
212
208
|
global img
|
|
@@ -221,21 +217,99 @@ def draw():
|
|
|
221
217
|
background(40)
|
|
222
218
|
if img:
|
|
223
219
|
image(img, 0, 0, width, height)
|
|
224
|
-
|
|
220
|
+
````
|
|
221
|
+
|
|
222
|
+
`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.
|
|
225
223
|
|
|
226
224
|
---
|
|
227
225
|
|
|
228
226
|
### Note sur `smooth()` / `noSmooth()` et le texte
|
|
229
227
|
|
|
230
228
|
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()` :
|
|
231
|
-
|
|
232
|
-
```python
|
|
229
|
+
````python
|
|
233
230
|
noSmooth()
|
|
234
231
|
rect(10, 10, 80, 80) # bords nets
|
|
235
232
|
smooth()
|
|
236
233
|
textSize(14)
|
|
237
234
|
text("lisible", 10, 120) # texte antialiasé
|
|
238
|
-
|
|
235
|
+
````
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### Gestion des erreurs
|
|
240
|
+
|
|
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
|
+
|
|
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
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Mode terminal
|
|
248
|
+
|
|
249
|
+
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
|
+
Ce mode permet d'écrire des programmes Python classiques — algorithmes, structures de données, visualisations texte — sans aucun lien avec p5.
|
|
252
|
+
````python
|
|
253
|
+
# Pas d'import p5 → mode terminal automatique
|
|
254
|
+
import asyncio
|
|
255
|
+
|
|
256
|
+
for i in range(5):
|
|
257
|
+
print(f"étape {i + 1}")
|
|
258
|
+
await asyncio.sleep(0.5)
|
|
259
|
+
|
|
260
|
+
print("terminé !")
|
|
261
|
+
````
|
|
262
|
+
|
|
263
|
+
### `input()`
|
|
264
|
+
|
|
265
|
+
`input()` est entièrement supporté : le programme se met en attente, le terminal affiche le prompt et accepte la saisie clavier.
|
|
266
|
+
````python
|
|
267
|
+
nom = input("Ton prénom : ")
|
|
268
|
+
print(f"Bonjour, {nom} !")
|
|
269
|
+
````
|
|
270
|
+
|
|
271
|
+
Ctrl+C interrompt une saisie en cours et retourne `None` — prévoir un guard si nécessaire :
|
|
272
|
+
````python
|
|
273
|
+
val = input("Valeur : ")
|
|
274
|
+
if val is None:
|
|
275
|
+
print("annulé")
|
|
276
|
+
````
|
|
277
|
+
|
|
278
|
+
### rich
|
|
279
|
+
|
|
280
|
+
`rich` est disponible directement — tableaux, couleurs ANSI, barres de progression :
|
|
281
|
+
````python
|
|
282
|
+
from rich.console import Console
|
|
283
|
+
from rich.table import Table
|
|
284
|
+
|
|
285
|
+
console = Console()
|
|
286
|
+
|
|
287
|
+
table = Table(title="Planètes")
|
|
288
|
+
table.add_column("Nom")
|
|
289
|
+
table.add_column("Diamètre (km)", justify="right")
|
|
290
|
+
table.add_row("Mercure", "4 879")
|
|
291
|
+
table.add_row("Vénus", "12 104")
|
|
292
|
+
table.add_row("Terre", "12 742")
|
|
293
|
+
console.print(table)
|
|
294
|
+
````
|
|
295
|
+
|
|
296
|
+
> **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
|
+
> ```python
|
|
299
|
+
> import asyncio
|
|
300
|
+
> from rich.progress import Progress
|
|
301
|
+
>
|
|
302
|
+
> with Progress(auto_refresh=False) as progress:
|
|
303
|
+
> task = progress.add_task("calcul…", total=100)
|
|
304
|
+
> for i in range(100):
|
|
305
|
+
> await asyncio.sleep(0.03)
|
|
306
|
+
> progress.advance(task)
|
|
307
|
+
> progress.refresh()
|
|
308
|
+
> ```
|
|
309
|
+
|
|
310
|
+
### Relancer
|
|
311
|
+
|
|
312
|
+
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.
|
|
239
313
|
|
|
240
314
|
---
|
|
241
315
|
|
|
@@ -288,8 +362,7 @@ Tous les blocs Python partagent le même namespace : les variables et fonctions
|
|
|
288
362
|
| `type="text/markdown"` | Le contenu est rendu en Markdown (non exécuté) |
|
|
289
363
|
|
|
290
364
|
### Exemple : énoncé + code utilitaire + zone élève
|
|
291
|
-
|
|
292
|
-
```html
|
|
365
|
+
````html
|
|
293
366
|
<script type="text/markdown" data-tab="Énoncé">
|
|
294
367
|
# Exercice
|
|
295
368
|
|
|
@@ -313,13 +386,12 @@ def draw():
|
|
|
313
386
|
background(20)
|
|
314
387
|
# à toi de jouer !
|
|
315
388
|
</script>
|
|
316
|
-
|
|
389
|
+
````
|
|
317
390
|
|
|
318
391
|
### Bloc caché
|
|
319
392
|
|
|
320
393
|
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 :
|
|
321
|
-
|
|
322
|
-
```html
|
|
394
|
+
````html
|
|
323
395
|
<script type="text/python" data-hidden>
|
|
324
396
|
# Code invisible — exécuté en premier
|
|
325
397
|
PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
|
|
@@ -336,7 +408,7 @@ def draw():
|
|
|
336
408
|
fill(PALETTE[frameCount // 60 % 3])
|
|
337
409
|
circle(200, 200, 100)
|
|
338
410
|
</script>
|
|
339
|
-
|
|
411
|
+
````
|
|
340
412
|
|
|
341
413
|
### Rendu des onglets Markdown
|
|
342
414
|
|
|
@@ -356,21 +428,19 @@ Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://ma
|
|
|
356
428
|
**Diagrammes (Mermaid) :**
|
|
357
429
|
|
|
358
430
|
Un bloc de code avec le langage `mermaid` est rendu comme un diagramme SVG :
|
|
359
|
-
|
|
360
431
|
````markdown
|
|
361
432
|
```mermaid
|
|
362
|
-
graph TD
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
433
|
+
graph TD
|
|
434
|
+
A[Départ] --> B{Condition}
|
|
435
|
+
B -->|oui| C[Résultat 1]
|
|
436
|
+
B -->|non| D[Résultat 2]
|
|
366
437
|
```
|
|
367
438
|
````
|
|
368
439
|
|
|
369
440
|
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.
|
|
370
441
|
|
|
371
442
|
**Exemple combiné :**
|
|
372
|
-
|
|
373
|
-
```html
|
|
443
|
+
````html
|
|
374
444
|
<script type="text/markdown" data-tab="Cours">
|
|
375
445
|
# Algorithme de Dijkstra
|
|
376
446
|
|
|
@@ -383,18 +453,15 @@ le sommet $u \notin S$ qui minimise :
|
|
|
383
453
|
$$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
|
|
384
454
|
|
|
385
455
|
La complexité est $O((V + E) \log V)$ avec un tas binaire.
|
|
386
|
-
|
|
387
456
|
```mermaid
|
|
388
|
-
graph LR
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
457
|
+
graph LR
|
|
458
|
+
A((1)) -->|4| B((2))
|
|
459
|
+
A -->|1| C((3))
|
|
460
|
+
C -->|2| B
|
|
461
|
+
B -->|1| D((4))
|
|
393
462
|
```
|
|
394
463
|
</script>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
464
|
+
````
|
|
398
465
|
|
|
399
466
|
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.
|
|
400
467
|
|
|
@@ -409,8 +476,7 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
|
|
|
409
476
|
### Configuration
|
|
410
477
|
|
|
411
478
|
`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">`.
|
|
412
|
-
|
|
413
|
-
```html
|
|
479
|
+
````html
|
|
414
480
|
<!-- Recommandé -->
|
|
415
481
|
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
|
|
416
482
|
|
|
@@ -422,13 +488,12 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
|
|
|
422
488
|
<script type="text/python" data-sources="local" data-vendor="vendor/">
|
|
423
489
|
…
|
|
424
490
|
</script>
|
|
425
|
-
|
|
491
|
+
````
|
|
426
492
|
|
|
427
493
|
`data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/`.
|
|
428
494
|
|
|
429
495
|
### Structure de fichiers
|
|
430
|
-
|
|
431
|
-
```
|
|
496
|
+
````
|
|
432
497
|
mon-projet/
|
|
433
498
|
├── pyfrilet.js
|
|
434
499
|
├── mon-sketch.html
|
|
@@ -439,6 +504,10 @@ mon-projet/
|
|
|
439
504
|
├── theme-monokai.min.js
|
|
440
505
|
├── ext-language_tools.min.js
|
|
441
506
|
├── ext-searchbox.min.js
|
|
507
|
+
├── xterm.min.css
|
|
508
|
+
├── xterm.min.js
|
|
509
|
+
├── addon-fit.min.js
|
|
510
|
+
├── addon-unicode11.min.js
|
|
442
511
|
├── marked.min.js ← uniquement si onglets Markdown
|
|
443
512
|
├── katex.min.css ← uniquement si onglets Markdown
|
|
444
513
|
├── katex.min.js ← uniquement si onglets Markdown
|
|
@@ -449,51 +518,59 @@ mon-projet/
|
|
|
449
518
|
├── pyodide.asm.wasm
|
|
450
519
|
├── python_stdlib.zip
|
|
451
520
|
└── … (autres fichiers Pyodide)
|
|
452
|
-
|
|
521
|
+
````
|
|
453
522
|
|
|
454
523
|
### Télécharger les dépendances
|
|
455
524
|
|
|
456
525
|
**p5.js**
|
|
457
|
-
|
|
526
|
+
````
|
|
458
527
|
https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js
|
|
459
|
-
|
|
528
|
+
````
|
|
460
529
|
|
|
461
530
|
**ACE editor** (5 fichiers)
|
|
462
|
-
|
|
531
|
+
````
|
|
463
532
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js
|
|
464
533
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js
|
|
465
534
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js
|
|
466
535
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
|
|
467
536
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js
|
|
468
|
-
|
|
537
|
+
````
|
|
538
|
+
|
|
539
|
+
**xterm.js** (4 fichiers)
|
|
540
|
+
````
|
|
541
|
+
https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css
|
|
542
|
+
https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js
|
|
543
|
+
https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js
|
|
544
|
+
https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js
|
|
545
|
+
````
|
|
546
|
+
Renommer respectivement en `xterm.min.css`, `xterm.min.js`, `addon-fit.min.js`, `addon-unicode11.min.js`.
|
|
469
547
|
|
|
470
548
|
**marked.js + KaTeX + Mermaid** (uniquement si onglets Markdown)
|
|
471
|
-
|
|
549
|
+
````
|
|
472
550
|
https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
|
|
473
551
|
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
|
|
474
552
|
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
|
|
475
553
|
https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js
|
|
476
554
|
https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
|
|
477
|
-
|
|
555
|
+
````
|
|
478
556
|
Renommer `index.umd.js` en `marked-katex-extension.js` dans le dossier vendor.
|
|
479
557
|
|
|
480
558
|
**Pyodide** — télécharger l'archive complète depuis les releases GitHub :
|
|
481
|
-
|
|
559
|
+
````
|
|
482
560
|
https://github.com/pyodide/pyodide/releases/tag/0.26.4
|
|
483
|
-
|
|
561
|
+
````
|
|
484
562
|
Extraire le contenu dans `vendor/pyodide/`.
|
|
485
563
|
|
|
486
564
|
### Serveur local
|
|
487
565
|
|
|
488
566
|
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 :
|
|
489
|
-
|
|
490
|
-
```bash
|
|
567
|
+
````bash
|
|
491
568
|
# Python 3
|
|
492
569
|
python -m http.server 8000
|
|
493
570
|
|
|
494
571
|
# Node.js
|
|
495
572
|
npx serve .
|
|
496
|
-
|
|
573
|
+
````
|
|
497
574
|
|
|
498
575
|
Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
499
576
|
|
|
@@ -502,14 +579,12 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
|
502
579
|
## Build et publication
|
|
503
580
|
|
|
504
581
|
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`.
|
|
505
|
-
|
|
506
|
-
```bash
|
|
582
|
+
````bash
|
|
507
583
|
npm run build # génère pyfrilet.js + pyfrilet.min.js
|
|
508
|
-
|
|
584
|
+
````
|
|
509
585
|
|
|
510
586
|
Le hook `prepublishOnly` dans `package.json` déclenche le build automatiquement avant chaque `npm publish`. Le flux complet d'une release :
|
|
511
|
-
|
|
512
|
-
```bash
|
|
587
|
+
````bash
|
|
513
588
|
git add .
|
|
514
589
|
git commit -m "feat: ..." # committer le travail
|
|
515
590
|
|
|
@@ -517,7 +592,7 @@ npm version patch # (ou minor / major) — modifie package.json, comm
|
|
|
517
592
|
npm publish # build automatique puis publication sur npm
|
|
518
593
|
|
|
519
594
|
git push && git push --tags # pousser commits et tag sur Codeberg
|
|
520
|
-
|
|
595
|
+
````
|
|
521
596
|
|
|
522
597
|
---
|
|
523
598
|
|
|
@@ -540,10 +615,12 @@ pyfrilet ne contient aucun code de ces bibliothèques ; elles sont chargées sé
|
|
|
540
615
|
| [p5.js](https://p5js.org/) | LGPL 2.1 |
|
|
541
616
|
| [Pyodide](https://pyodide.org/) | MPL 2.0 |
|
|
542
617
|
| [ACE editor](https://ace.c9.io/) | BSD 3-Clause |
|
|
618
|
+
| [xterm.js](https://xtermjs.org/) | MIT |
|
|
543
619
|
| [marked](https://marked.js.org/) | MIT |
|
|
544
620
|
| [KaTeX](https://katex.org/) | MIT |
|
|
545
621
|
| [marked-katex-extension](https://github.com/UziTech/marked-katex-extension) | MIT |
|
|
546
622
|
| [Mermaid](https://mermaid.js.org/) | MIT |
|
|
623
|
+
| [rich](https://rich.readthedocs.io/) | MIT |
|
|
547
624
|
|
|
548
625
|
---
|
|
549
626
|
|
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -39,6 +39,10 @@ const CDN = {
|
|
|
39
39
|
katex : 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js',
|
|
40
40
|
markedKatex : 'https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js',
|
|
41
41
|
mermaid : 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
|
|
42
|
+
xtermCss : 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css',
|
|
43
|
+
xterm : 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js',
|
|
44
|
+
xtermFit : 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js',
|
|
45
|
+
xtermUni : 'https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js',
|
|
42
46
|
};
|
|
43
47
|
|
|
44
48
|
/* ═══════════════════════════ STYLES ═════════════════════════════════ */
|
|
@@ -243,7 +247,7 @@ const STYLES = `html, body {
|
|
|
243
247
|
.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }
|
|
244
248
|
|
|
245
249
|
/* ── markdown view ── */
|
|
246
|
-
@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');
|
|
250
|
+
@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');
|
|
247
251
|
|
|
248
252
|
#pf-markdown-view {
|
|
249
253
|
flex: 1;
|
|
@@ -380,12 +384,36 @@ const STYLES = `html, body {
|
|
|
380
384
|
white-space: pre-wrap;
|
|
381
385
|
display: none;
|
|
382
386
|
border-top: 1px solid rgba(247, 118, 142, .35);
|
|
383
|
-
}
|
|
387
|
+
}
|
|
388
|
+
/* ── xterm terminal ── */
|
|
389
|
+
#pf-xterm {
|
|
390
|
+
display: none;
|
|
391
|
+
position: absolute;
|
|
392
|
+
inset: 0;
|
|
393
|
+
padding: 10px 12px;
|
|
394
|
+
box-sizing: border-box;
|
|
395
|
+
background: #000000;
|
|
396
|
+
overflow: hidden;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#pf-xterm.pf-xterm-overlay {
|
|
400
|
+
background: rgba(0, 0, 0, 0.82);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* xterm interne : prendre toute la hauteur */
|
|
404
|
+
#pf-xterm .xterm {
|
|
405
|
+
height: 100%;
|
|
406
|
+
}
|
|
407
|
+
#pf-xterm .xterm-screen {
|
|
408
|
+
height: 100% !important;
|
|
409
|
+
}
|
|
410
|
+
`;
|
|
384
411
|
|
|
385
412
|
/* ═══════════════════════════ MARKUP ═════════════════════════════════ */
|
|
386
413
|
const MARKUP = `<div id="pf-root">
|
|
387
414
|
<div id="pf-app" tabindex="-1">
|
|
388
415
|
<div id="pf-viewport"><div id="pf-sketch"></div></div>
|
|
416
|
+
<div id="pf-xterm"></div>
|
|
389
417
|
<div id="pf-loader">
|
|
390
418
|
<span id="pf-loader-msg">Chargement…</span>
|
|
391
419
|
<div id="pf-loader-bar"></div>
|
|
@@ -461,6 +489,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
461
489
|
katex : hasMarked ? CDN.katex : null,
|
|
462
490
|
markedKatex : hasMarked ? CDN.markedKatex : null,
|
|
463
491
|
mermaid : hasMarked ? CDN.mermaid : null,
|
|
492
|
+
xtermCss : CDN.xtermCss,
|
|
493
|
+
xterm : CDN.xterm,
|
|
494
|
+
xtermFit : CDN.xtermFit,
|
|
495
|
+
xtermUni : CDN.xtermUni,
|
|
464
496
|
} : {
|
|
465
497
|
p5 : vp + 'p5.min.js',
|
|
466
498
|
pyodide : vp + 'pyodide/pyodide.js',
|
|
@@ -475,6 +507,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
475
507
|
katex : hasMarked ? vp + 'katex.min.js' : null,
|
|
476
508
|
markedKatex : hasMarked ? vp + 'marked-katex-extension.js' : null,
|
|
477
509
|
mermaid : hasMarked ? vp + 'mermaid.min.js' : null,
|
|
510
|
+
xtermCss : vp + 'xterm.min.css',
|
|
511
|
+
xterm : vp + 'xterm.min.js',
|
|
512
|
+
xtermFit : vp + 'xterm-addon-fit.min.js',
|
|
513
|
+
xtermUni : vp + 'addon-unicode11.min.js',
|
|
478
514
|
};
|
|
479
515
|
|
|
480
516
|
const SK = 'pyfrilet:' + location.pathname;
|
|
@@ -513,6 +549,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
513
549
|
try { snap = JSON.parse(raw); } catch (e) { snap = null; }
|
|
514
550
|
}
|
|
515
551
|
|
|
552
|
+
if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
|
|
553
|
+
/* Compare structural signature: label, type, hidden, readonly for each tab.
|
|
554
|
+
If the HTML has changed since the snapshot was saved, mark as stale but
|
|
555
|
+
still serve the old content — the user keeps their work until they
|
|
556
|
+
explicitly confirm a Reset (which always deletes SK first). */
|
|
557
|
+
const sig = t => `${t.label}|${t.type}|${t.hidden ? 1 : 0}|${t.readonly ? 1 : 0}`;
|
|
558
|
+
const snapSig = snap.tabs.map(sig).join(',');
|
|
559
|
+
const htmlSig = htmlTabs.map(sig).join(',');
|
|
560
|
+
if (snapSig !== htmlSig) snap._stale = true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const staleSnapshot = !!(snap && snap._stale);
|
|
564
|
+
|
|
516
565
|
if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
|
|
517
566
|
/* Restore structure and content from snapshot */
|
|
518
567
|
tabs = snap.tabs.map((st, i) => {
|
|
@@ -544,15 +593,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
544
593
|
|
|
545
594
|
const noWatchdog = configTag.hasAttribute('data-no-watchdog');
|
|
546
595
|
|
|
547
|
-
main(tabs, htmlTabs, SK, URLS, noWatchdog);
|
|
596
|
+
main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot);
|
|
548
597
|
});
|
|
549
598
|
|
|
550
599
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
551
|
-
function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
600
|
+
function main(tabs, htmlTabs, SK, URLS, noWatchdog, staleSnapshot) {
|
|
552
601
|
|
|
553
602
|
/* tabs = working state (from snapshot or HTML), may be reassigned on reset
|
|
554
603
|
htmlTabs = ground truth from current HTML file, never mutated */
|
|
555
604
|
tabs = tabs.slice(); /* local mutable copy */
|
|
605
|
+
let _staleSnapshot = staleSnapshot; /* mutable — cleared after reset */
|
|
556
606
|
|
|
557
607
|
/* ── inject styles + markup ── */
|
|
558
608
|
const styleEl = document.createElement('style');
|
|
@@ -1041,7 +1091,7 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1041
1091
|
|
|
1042
1092
|
/* ── Dirty indicator ──────────────────────────────────── */
|
|
1043
1093
|
function refreshDirty() {
|
|
1044
|
-
const dirty = tabs.some(tab =>
|
|
1094
|
+
const dirty = _staleSnapshot || tabs.some(tab =>
|
|
1045
1095
|
!tab.hidden && !tab.readonly && tab.type === 'python' &&
|
|
1046
1096
|
aceSessions[tab.id] &&
|
|
1047
1097
|
aceSessions[tab.id].getValue() !== tab.starterCode
|
|
@@ -1051,11 +1101,9 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1051
1101
|
|
|
1052
1102
|
/* ── Reset: restore file structure + content, no reload ─ */
|
|
1053
1103
|
function resetAllTabs() {
|
|
1054
|
-
/* Cancel all pending saves first */
|
|
1055
1104
|
saveTimers.forEach(t => clearTimeout(t));
|
|
1056
1105
|
saveTimers.clear();
|
|
1057
1106
|
|
|
1058
|
-
/* Erase snapshot and any legacy per-tab keys */
|
|
1059
1107
|
try { localStorage.removeItem(SK); } catch (e) {}
|
|
1060
1108
|
tabs.forEach(tab => {
|
|
1061
1109
|
if (tab.label) {
|
|
@@ -1064,10 +1112,10 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1064
1112
|
});
|
|
1065
1113
|
try { localStorage.removeItem(SK + ':Code'); } catch (e) {}
|
|
1066
1114
|
|
|
1067
|
-
|
|
1115
|
+
_staleSnapshot = false;
|
|
1116
|
+
|
|
1068
1117
|
tabs = htmlTabs.map((ht, i) => ({ ...ht, id: 'tab-' + i, code: ht.starterCode }));
|
|
1069
1118
|
|
|
1070
|
-
/* Rebuild sessions and tab bar in-memory */
|
|
1071
1119
|
buildSessions();
|
|
1072
1120
|
buildTabBar();
|
|
1073
1121
|
refreshDirty();
|
|
@@ -1085,6 +1133,7 @@ function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
|
1085
1133
|
const opts = {};
|
|
1086
1134
|
if (URLS.pyodideIndex) opts.indexURL = URLS.pyodideIndex;
|
|
1087
1135
|
pyodide = await loadPyodide(opts);
|
|
1136
|
+
await pyodide.loadPackage(['rich', 'pygments']);
|
|
1088
1137
|
|
|
1089
1138
|
/* Build the "p5" Python module via dynamic introspection of a dummy instance */
|
|
1090
1139
|
pyodide.runPython(`
|
|
@@ -1260,17 +1309,47 @@ def _wdog_trace(frame, event, arg):
|
|
|
1260
1309
|
raise TimeoutError("draw() watchdog")
|
|
1261
1310
|
return _wdog_trace
|
|
1262
1311
|
|
|
1312
|
+
from rich.console import Console as _RichConsole
|
|
1313
|
+
_pf_rich_console = _RichConsole(stderr=True)
|
|
1314
|
+
|
|
1315
|
+
class _PfHandledError(Exception):
|
|
1316
|
+
"""Levée après que rich a déjà affiché le traceback vers xterm."""
|
|
1317
|
+
pass
|
|
1318
|
+
|
|
1319
|
+
def _pf_safe_call(fn):
|
|
1320
|
+
try:
|
|
1321
|
+
fn()
|
|
1322
|
+
except (_PfHandledError, TimeoutError):
|
|
1323
|
+
raise
|
|
1324
|
+
except Exception as _e:
|
|
1325
|
+
_tb = _e.__traceback__
|
|
1326
|
+
while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):
|
|
1327
|
+
_tb = _tb.tb_next
|
|
1328
|
+
if _tb: _e.__traceback__ = _tb
|
|
1329
|
+
_pf_rich_console.print_exception(show_locals=False)
|
|
1330
|
+
from js import _pfShowErrorTerminal
|
|
1331
|
+
_pfShowErrorTerminal()
|
|
1332
|
+
|
|
1333
|
+
def _pf_safe_proxy(fn):
|
|
1334
|
+
from pyodide.ffi import create_proxy as _cp
|
|
1335
|
+
def _wrapped(*args, **kwargs):
|
|
1336
|
+
_pf_safe_call(lambda: fn(*args, **kwargs))
|
|
1337
|
+
return _cp(_wrapped)
|
|
1338
|
+
|
|
1339
|
+
setattr(m, 'safe_proxy', _pf_safe_proxy)
|
|
1340
|
+
_p5_functions.add('safe_proxy')
|
|
1341
|
+
|
|
1263
1342
|
def _pf_draw_watchdog(fn, timeout_ms):
|
|
1264
1343
|
_wdog_count[0] = 0
|
|
1265
1344
|
_wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001
|
|
1266
1345
|
sys.settrace(_wdog_trace)
|
|
1267
1346
|
try:
|
|
1268
|
-
fn
|
|
1347
|
+
_pf_safe_call(fn)
|
|
1269
1348
|
finally:
|
|
1270
1349
|
sys.settrace(None)
|
|
1271
1350
|
|
|
1272
1351
|
def _pf_draw_direct(fn, timeout_ms):
|
|
1273
|
-
fn
|
|
1352
|
+
_pf_safe_call(fn)
|
|
1274
1353
|
|
|
1275
1354
|
def _snake_to_camel(name):
|
|
1276
1355
|
parts = name.split('_')
|
|
@@ -1298,6 +1377,71 @@ def _p5_getattr(name):
|
|
|
1298
1377
|
m.__getattr__ = _p5_getattr
|
|
1299
1378
|
`);
|
|
1300
1379
|
|
|
1380
|
+
|
|
1381
|
+
/* ── Terminal mode: async execution with input() support ── */
|
|
1382
|
+
pyodide.runPython(`
|
|
1383
|
+
import asyncio as _asyncio, ast as _ast
|
|
1384
|
+
import os as _os, sys as _sys
|
|
1385
|
+
_os.environ.setdefault('TERM', 'xterm-256color')
|
|
1386
|
+
_os.environ.setdefault('COLORTERM', 'truecolor')
|
|
1387
|
+
|
|
1388
|
+
# Wrapper file-like qui écrit directement vers xterm via JS.
|
|
1389
|
+
# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.
|
|
1390
|
+
class _PfStream:
|
|
1391
|
+
def __init__(self, js_fn):
|
|
1392
|
+
self._fn = js_fn
|
|
1393
|
+
self.encoding = 'utf-8'
|
|
1394
|
+
self.errors = 'replace'
|
|
1395
|
+
def write(self, s):
|
|
1396
|
+
if s:
|
|
1397
|
+
self._fn(s)
|
|
1398
|
+
return len(s)
|
|
1399
|
+
def writelines(self, lines):
|
|
1400
|
+
for l in lines: self.write(l)
|
|
1401
|
+
def flush(self): pass
|
|
1402
|
+
def isatty(self): return True
|
|
1403
|
+
@property
|
|
1404
|
+
def softspace(self): return 0
|
|
1405
|
+
|
|
1406
|
+
from js import _pfTermWrite, _pfTermWriteErr
|
|
1407
|
+
_sys.stdout = _PfStream(_pfTermWrite)
|
|
1408
|
+
_sys.stderr = _PfStream(_pfTermWriteErr)
|
|
1409
|
+
|
|
1410
|
+
async def _pf_async_input(prompt=""):
|
|
1411
|
+
from js import _pfTerminalInput
|
|
1412
|
+
result = await _pfTerminalInput(str(prompt) if prompt else "")
|
|
1413
|
+
return result
|
|
1414
|
+
|
|
1415
|
+
async def _pf_run_terminal(source):
|
|
1416
|
+
class _InputAwaiter(_ast.NodeTransformer):
|
|
1417
|
+
def visit_Call(self, node):
|
|
1418
|
+
self.generic_visit(node)
|
|
1419
|
+
if isinstance(node.func, _ast.Name) and node.func.id == 'input':
|
|
1420
|
+
return _ast.Await(value=node)
|
|
1421
|
+
return node
|
|
1422
|
+
|
|
1423
|
+
tree = _ast.parse(source)
|
|
1424
|
+
tree = _InputAwaiter().visit(tree)
|
|
1425
|
+
|
|
1426
|
+
wrapper = _ast.parse("async def programme(): pass")
|
|
1427
|
+
wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]
|
|
1428
|
+
_ast.fix_missing_locations(wrapper)
|
|
1429
|
+
|
|
1430
|
+
_ns = {'input': _pf_async_input}
|
|
1431
|
+
exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)
|
|
1432
|
+
try:
|
|
1433
|
+
await _ns['programme']()
|
|
1434
|
+
except SystemExit:
|
|
1435
|
+
pass
|
|
1436
|
+
except Exception as _e:
|
|
1437
|
+
_tb = _e.__traceback__
|
|
1438
|
+
while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':
|
|
1439
|
+
_tb = _tb.tb_next
|
|
1440
|
+
if _tb:
|
|
1441
|
+
_e.__traceback__ = _tb
|
|
1442
|
+
_pf_rich_console.print_exception(show_locals=False)
|
|
1443
|
+
`);
|
|
1444
|
+
|
|
1301
1445
|
/* Inject p5 symbols into ACE autocomplete */
|
|
1302
1446
|
if (aceInst) {
|
|
1303
1447
|
const p5all = pyodide.runPython('list(m.__all__)').toJs();
|
|
@@ -1336,6 +1480,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1336
1480
|
|
|
1337
1481
|
/* ─────────────────── RUN CODE ───────────────── */
|
|
1338
1482
|
let running = false;
|
|
1483
|
+
let _terminalRunning = false; /* true pendant qu'un runTerminalCode est actif */
|
|
1339
1484
|
let setupProxy = null, drawProxy = null,
|
|
1340
1485
|
preloadProxy = null,
|
|
1341
1486
|
mousePressedProxy = null, mouseReleasedProxy = null, mouseDraggedProxy = null,
|
|
@@ -1345,11 +1490,47 @@ m.__getattr__ = _p5_getattr
|
|
|
1345
1490
|
|
|
1346
1491
|
const WATCHDOG_MS = 300;
|
|
1347
1492
|
|
|
1493
|
+
/* ─────────────────── TERMINAL MODE ────────────── */
|
|
1494
|
+
function isTerminalMode(code) {
|
|
1495
|
+
/* Terminal mode = no p5 import detected */
|
|
1496
|
+
return !/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(code);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function runTerminalCode(code) {
|
|
1500
|
+
_terminalRunning = true;
|
|
1501
|
+
showTerminal();
|
|
1502
|
+
termClear();
|
|
1503
|
+
clearError();
|
|
1504
|
+
|
|
1505
|
+
try {
|
|
1506
|
+
const runner = pyodide.globals.get('_pf_run_terminal');
|
|
1507
|
+
await runner(code);
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
/* Seules les erreurs avant l'exec arrivent ici (ex: SyntaxError de parsing) */
|
|
1510
|
+
const msg = String(e);
|
|
1511
|
+
if (!msg.includes('SystemExit')) termWriteErr(msg + '\n');
|
|
1512
|
+
} finally {
|
|
1513
|
+
_terminalRunning = false;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1348
1517
|
async function runCode() {
|
|
1349
|
-
if (running)
|
|
1518
|
+
if (running) {
|
|
1519
|
+
if (_terminalRunning) {
|
|
1520
|
+
/* Terminal en cours (ex: input() en attente) — annuler et relancer */
|
|
1521
|
+
hideTerminal(); /* résout le _pfTerminalInput en attente avec '' */
|
|
1522
|
+
running = false;
|
|
1523
|
+
btnRun.classList.remove('pf-running');
|
|
1524
|
+
/* Laisser la micro-tâche Python se terminer avant de relancer */
|
|
1525
|
+
await new Promise(r => setTimeout(r, 30));
|
|
1526
|
+
} else {
|
|
1527
|
+
return; /* sketch p5 en cours — ignorer */
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1350
1530
|
running = true;
|
|
1351
1531
|
btnRun.classList.add('pf-running');
|
|
1352
1532
|
clearError();
|
|
1533
|
+
termClear();
|
|
1353
1534
|
stopSketch();
|
|
1354
1535
|
|
|
1355
1536
|
if (!pyodide) {
|
|
@@ -1361,7 +1542,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1361
1542
|
await ensurePyodide();
|
|
1362
1543
|
} catch (e) {
|
|
1363
1544
|
loaderEl.style.display = 'none';
|
|
1364
|
-
showError('Erreur Pyodide : ' + e);
|
|
1545
|
+
showError('Erreur Pyodide : ' + (e.message || String(e)));
|
|
1365
1546
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
1366
1547
|
}
|
|
1367
1548
|
|
|
@@ -1384,13 +1565,22 @@ m.__getattr__ = _p5_getattr
|
|
|
1384
1565
|
} catch (e) { console.warn('[pyfrilet] loadPackagesFromImports:', e); }
|
|
1385
1566
|
loaderEl.style.display = 'none';
|
|
1386
1567
|
|
|
1568
|
+
|
|
1569
|
+
/* ── Route: terminal mode (no p5) or p5 mode ── */
|
|
1570
|
+
if (isTerminalMode(code)) {
|
|
1571
|
+
btnRun.classList.remove('pf-running'); /* bouton actif pendant l'exécution terminal */
|
|
1572
|
+
await runTerminalCode(code);
|
|
1573
|
+
running = false; return;
|
|
1574
|
+
}
|
|
1575
|
+
hideTerminal(); /* make sure terminal hidden for p5 mode */
|
|
1576
|
+
|
|
1387
1577
|
pyodide.globals.set('_USER_CODE', code);
|
|
1388
1578
|
|
|
1389
1579
|
try {
|
|
1390
1580
|
pyodide.runPython('_ns = {}; exec(_USER_CODE, _ns, _ns)');
|
|
1391
1581
|
pyodide.runPython('_ns_ref[0] = _ns'); /* give size() access to current ns */
|
|
1392
1582
|
} catch (e) {
|
|
1393
|
-
|
|
1583
|
+
showErrorTerminal(e.message || String(e));
|
|
1394
1584
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
1395
1585
|
}
|
|
1396
1586
|
|
|
@@ -1413,7 +1603,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1413
1603
|
pyTM = _get('touchMoved', 'touch_moved');
|
|
1414
1604
|
pyTE = _get('touchEnded', 'touch_ended');
|
|
1415
1605
|
} catch (e) {
|
|
1416
|
-
|
|
1606
|
+
showErrorTerminal(e.message || String(e));
|
|
1417
1607
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
1418
1608
|
}
|
|
1419
1609
|
|
|
@@ -1427,13 +1617,15 @@ m.__getattr__ = _p5_getattr
|
|
|
1427
1617
|
const pyRefresh = pyodide.globals.get('_pf_refresh');
|
|
1428
1618
|
const pfDrawWatchdog = pyodide.globals.get(noWatchdog ? '_pf_draw_direct' : '_pf_draw_watchdog');
|
|
1429
1619
|
const pyNs = pyodide.globals.get('_ns');
|
|
1620
|
+
const pfSafeCall = pyodide.globals.get('_pf_safe_call');
|
|
1430
1621
|
|
|
1431
1622
|
const mkProxy = (fn) => fn ? create_proxy(() => {
|
|
1432
|
-
try { pyRefresh(pyNs); fn
|
|
1623
|
+
try { pyRefresh(pyNs); pfSafeCall(fn); } catch (e) { showErrorTerminal(''); }
|
|
1433
1624
|
}) : null;
|
|
1434
1625
|
|
|
1435
|
-
preloadProxy
|
|
1436
|
-
setupProxy
|
|
1626
|
+
preloadProxy = pyPreload ? create_proxy(() => { try { pfSafeCall(pyPreload); } catch (e) { showErrorTerminal(''); } }) : null;
|
|
1627
|
+
setupProxy = pySetup ? create_proxy(() => { try { pfSafeCall(pySetup); } catch (e) { showErrorTerminal(''); } }) : null;
|
|
1628
|
+
|
|
1437
1629
|
drawProxy = create_proxy(() => {
|
|
1438
1630
|
try {
|
|
1439
1631
|
pyRefresh(pyNs);
|
|
@@ -1444,7 +1636,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1444
1636
|
if (msg.includes('TimeoutError') || msg.includes('watchdog')) {
|
|
1445
1637
|
showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
|
|
1446
1638
|
} else {
|
|
1447
|
-
|
|
1639
|
+
showErrorTerminal('');
|
|
1448
1640
|
}
|
|
1449
1641
|
}
|
|
1450
1642
|
});
|
|
@@ -1459,7 +1651,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1459
1651
|
touchStartedProxy = mkProxy(pyTS);
|
|
1460
1652
|
touchMovedProxy = mkProxy(pyTM);
|
|
1461
1653
|
touchEndedProxy = mkProxy(pyTE);
|
|
1462
|
-
const windowResizedProxy = pyWR ? create_proxy(() => { try { pyWR
|
|
1654
|
+
const windowResizedProxy = pyWR ? create_proxy(() => { try { pfSafeCall(pyWR); } catch (e) { showErrorTerminal(''); } }) : null;
|
|
1463
1655
|
|
|
1464
1656
|
let setupDone = false;
|
|
1465
1657
|
pInst = new p5((p) => {
|
|
@@ -1505,7 +1697,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1505
1697
|
}
|
|
1506
1698
|
|
|
1507
1699
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1508
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.
|
|
1700
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.0/pyfrilet.min.js';
|
|
1509
1701
|
|
|
1510
1702
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1511
1703
|
<html lang="fr">
|
|
@@ -1722,6 +1914,14 @@ FILLME-SCRIPTS
|
|
|
1722
1914
|
await loadScript(URLS.aceLangTools);
|
|
1723
1915
|
await loadScript(URLS.aceSearchbox);
|
|
1724
1916
|
await loadScript(URLS.pyodide);
|
|
1917
|
+
/* xterm — terminal pour mode script sans p5 */
|
|
1918
|
+
const xtermLink = document.createElement('link');
|
|
1919
|
+
xtermLink.rel = 'stylesheet';
|
|
1920
|
+
xtermLink.href = URLS.xtermCss;
|
|
1921
|
+
document.head.appendChild(xtermLink);
|
|
1922
|
+
await loadScript(URLS.xterm);
|
|
1923
|
+
await loadScript(URLS.xtermFit);
|
|
1924
|
+
await loadScript(URLS.xtermUni);
|
|
1725
1925
|
} catch (e) {
|
|
1726
1926
|
loaderMsg.textContent = '⚠ ' + e.message;
|
|
1727
1927
|
document.getElementById('pf-loader-bar').style.display = 'none';
|
|
@@ -1733,6 +1933,163 @@ FILLME-SCRIPTS
|
|
|
1733
1933
|
loaderEl.style.display = 'none';
|
|
1734
1934
|
})();
|
|
1735
1935
|
|
|
1936
|
+
/* ─────────────────── TERMINAL (xterm.js) ──────── */
|
|
1937
|
+
const xtermEl = document.getElementById('pf-xterm');
|
|
1938
|
+
|
|
1939
|
+
let _xterm = null; /* Terminal instance */
|
|
1940
|
+
let _fitAddon = null; /* FitAddon instance */
|
|
1941
|
+
|
|
1942
|
+
/* Input state */
|
|
1943
|
+
let _inputResolve = null; /* non-null ↔ en attente d'une ligne */
|
|
1944
|
+
let _lineBuffer = ''; /* caractères tapés sur la ligne courante */
|
|
1945
|
+
|
|
1946
|
+
/* ── Initialise xterm (appelé une seule fois) ── */
|
|
1947
|
+
function _initXterm() {
|
|
1948
|
+
if (_xterm) return;
|
|
1949
|
+
|
|
1950
|
+
_xterm = new Terminal({
|
|
1951
|
+
theme: {
|
|
1952
|
+
background : '#000000',
|
|
1953
|
+
foreground : '#e8e8e8',
|
|
1954
|
+
cursor : '#ffffff',
|
|
1955
|
+
black : '#2a2a2a', brightBlack : '#555555',
|
|
1956
|
+
red : '#cc4444', brightRed : '#ff6666',
|
|
1957
|
+
green : '#44aa44', brightGreen : '#66cc66',
|
|
1958
|
+
yellow : '#aaaa00', brightYellow : '#dddd44',
|
|
1959
|
+
blue : '#4466cc', brightBlue : '#6688ff',
|
|
1960
|
+
magenta : '#aa44aa', brightMagenta: '#dd66dd',
|
|
1961
|
+
cyan : '#44aaaa', brightCyan : '#66cccc',
|
|
1962
|
+
white : '#cccccc', brightWhite : '#ffffff',
|
|
1963
|
+
},
|
|
1964
|
+
fontFamily : "'Fira Code', 'Consolas', 'Courier New', monospace",
|
|
1965
|
+
fontSize : 15,
|
|
1966
|
+
lineHeight : 1.0,
|
|
1967
|
+
letterSpacing : 0,
|
|
1968
|
+
cursorBlink : true,
|
|
1969
|
+
scrollback : 2000,
|
|
1970
|
+
convertEol : true,
|
|
1971
|
+
allowProposedApi: true,
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
_fitAddon = new FitAddon.FitAddon();
|
|
1975
|
+
_xterm.loadAddon(_fitAddon);
|
|
1976
|
+
|
|
1977
|
+
/* addon-unicode11 : emoji et CJK comptés comme 2 cellules (= wcwidth) */
|
|
1978
|
+
const _uniAddon = new Unicode11Addon.Unicode11Addon();
|
|
1979
|
+
_xterm.loadAddon(_uniAddon);
|
|
1980
|
+
_xterm.unicode.activeVersion = '11';
|
|
1981
|
+
|
|
1982
|
+
_xterm.open(xtermEl);
|
|
1983
|
+
_fitAddon.fit();
|
|
1984
|
+
|
|
1985
|
+
/* Redimensionner avec le drawer */
|
|
1986
|
+
new ResizeObserver(() => {
|
|
1987
|
+
if (_xterm && xtermEl.style.display !== 'none') _fitAddon.fit();
|
|
1988
|
+
}).observe(xtermEl);
|
|
1989
|
+
|
|
1990
|
+
/* ── Gestion clavier : édition de ligne + soumission ── */
|
|
1991
|
+
_xterm.onData(e => {
|
|
1992
|
+
if (!_inputResolve) return;
|
|
1993
|
+
|
|
1994
|
+
if (e === '\r') { /* Entrée */
|
|
1995
|
+
const val = _lineBuffer;
|
|
1996
|
+
_lineBuffer = '';
|
|
1997
|
+
_xterm.write('\r\n');
|
|
1998
|
+
const res = _inputResolve;
|
|
1999
|
+
_inputResolve = null;
|
|
2000
|
+
res(val);
|
|
2001
|
+
|
|
2002
|
+
} else if (e === '\x7f') { /* Backspace */
|
|
2003
|
+
if (_lineBuffer.length > 0) {
|
|
2004
|
+
_lineBuffer = _lineBuffer.slice(0, -1);
|
|
2005
|
+
_xterm.write('\b \b');
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
} else if (e === '\x03') { /* Ctrl+C */
|
|
2009
|
+
_lineBuffer = '';
|
|
2010
|
+
_xterm.write('^C\r\n');
|
|
2011
|
+
const res = _inputResolve;
|
|
2012
|
+
_inputResolve = null;
|
|
2013
|
+
res(null);
|
|
2014
|
+
|
|
2015
|
+
} else if (e.charCodeAt(0) >= 32) { /* imprimable */
|
|
2016
|
+
_lineBuffer += e;
|
|
2017
|
+
_xterm.write(e);
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/* ── Exposé à Python comme js._pfTerminalInput(prompt) ── */
|
|
2023
|
+
window._pfTerminalInput = function (prompt) {
|
|
2024
|
+
return new Promise(resolve => {
|
|
2025
|
+
_inputResolve = resolve;
|
|
2026
|
+
_lineBuffer = '';
|
|
2027
|
+
if (prompt) _xterm.write(prompt);
|
|
2028
|
+
_xterm.focus();
|
|
2029
|
+
});
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
/* ── stdout / stderr — exposés comme globals pour le wrapper Python ── */
|
|
2033
|
+
function termWrite(text) {
|
|
2034
|
+
if (_xterm) _xterm.write(text);
|
|
2035
|
+
}
|
|
2036
|
+
window._pfTermWrite = termWrite;
|
|
2037
|
+
|
|
2038
|
+
function termWriteErr(text) {
|
|
2039
|
+
_initXterm();
|
|
2040
|
+
_xterm.write('\x1b[31m');
|
|
2041
|
+
_xterm.write(text.replace(/\n/g, '\r\n'));
|
|
2042
|
+
_xterm.write('\x1b[0m');
|
|
2043
|
+
}
|
|
2044
|
+
window._pfTermWriteErr = termWriteErr;
|
|
2045
|
+
|
|
2046
|
+
/* ── Clear ── */
|
|
2047
|
+
function termClear() {
|
|
2048
|
+
if (_xterm) { _xterm.clear(); }
|
|
2049
|
+
_inputResolve = null;
|
|
2050
|
+
_lineBuffer = '';
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/* ── Afficher / cacher ── */
|
|
2054
|
+
function showTerminal() {
|
|
2055
|
+
viewEl.style.display = 'none';
|
|
2056
|
+
xtermEl.style.display = 'block';
|
|
2057
|
+
xtermEl.classList.remove('pf-xterm-overlay');
|
|
2058
|
+
_initXterm();
|
|
2059
|
+
_fitAddon.fit();
|
|
2060
|
+
_xterm.focus();
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
window._pfShowErrorTerminal = () => {
|
|
2064
|
+
stopSketch();
|
|
2065
|
+
showErrorTerminal('');
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
/* Overlay semi-transparent par-dessus le sketch — pour les erreurs p5 */
|
|
2069
|
+
function showErrorTerminal(msg) {
|
|
2070
|
+
xtermEl.style.display = 'block';
|
|
2071
|
+
xtermEl.classList.add('pf-xterm-overlay');
|
|
2072
|
+
_initXterm();
|
|
2073
|
+
_fitAddon.fit();
|
|
2074
|
+
if (msg) {
|
|
2075
|
+
termClear();
|
|
2076
|
+
_xterm.write('\x1b[1;31m── Erreur ──────────────────────────────────────\x1b[0m\r\n\r\n');
|
|
2077
|
+
_xterm.write(msg.replace(/\n/g, '\r\n') + '\r\n');
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function hideTerminal() {
|
|
2082
|
+
xtermEl.style.display = 'none';
|
|
2083
|
+
xtermEl.classList.remove('pf-xterm-overlay');
|
|
2084
|
+
viewEl.style.display = '';
|
|
2085
|
+
if (_inputResolve) {
|
|
2086
|
+
const res = _inputResolve;
|
|
2087
|
+
_inputResolve = null;
|
|
2088
|
+
_lineBuffer = '';
|
|
2089
|
+
res('');
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
1736
2093
|
|
|
1737
2094
|
} /* end main() */
|
|
1738
2095
|
|
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",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",r="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="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&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}",h='<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-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 _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],g=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),b=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const v=_.some(e=>"text/markdown"===e.getAttribute("type")),w=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:r,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:v?l:null,katexCss:v?c:null,katex:v?p:null,markedKatex:v?m:null,mermaid:v?f:null}:{p5:b+"p5.min.js",pyodide:b+"pyodide/pyodide.js",pyodideIndex:b+"pyodide/",ace:b+"ace.min.js",acePython:b+"mode-python.min.js",aceMonokai:b+"theme-monokai.min.js",aceLangTools:b+"ext-language_tools.min.js",aceSearchbox:b+"ext-searchbox.min.js",marked:v?b+"marked.min.js":null,katexCss:v?b+"katex.min.css":null,katex:v?b+"katex.min.js":null,markedKatex:v?b+"marked-katex-extension.js":null,mermaid:v?b+"mermaid.min.js":null},x="pyfrilet:"+location.pathname,k=_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let r=e.getAttribute("data-tab");null!==r||a||(r=1===_.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:r,hidden:a,readonly:o,type:t,starterCode:i,code:i}}),E=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let C;const S=E(x);let L=null;if(S)try{L=JSON.parse(S)}catch(e){L=null}C=L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0?L.tabs.map((e,n)=>{const t=k.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}}):k.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=E(x+":"+t);if(a||"Code"!==e.label||1!==k.length||(a=E(x)),a&&a.trim())return{...e,code:a}}return e});const j=y.hasAttribute("data-no-watchdog");!function(e,t,a,o,r){e=e.slice();const i=document.createElement("style");i.textContent=u,document.head.appendChild(i),document.body.innerHTML=h;const s=document.getElementById("pf-app"),d=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),m=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),_=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),g=document.getElementById("pf-btn-code"),b=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),w=document.getElementById("pf-btn-reset"),x=document.getElementById("pf-btn-help"),k=document.getElementById("pf-grip"),E=document.getElementById("pf-handle-hint"),C=document.getElementById("pf-tabs"),S=document.getElementById("pf-markdown-view");let L=!1,j=Math.round(.56*window.innerHeight);function z(){document.documentElement.style.setProperty("--pf-drawer-h",j+"px")}function I(){L=!0,d.classList.add("pf-open"),g.classList.add("pf-active"),setTimeout(()=>{H(),J&&J.focus()},280)}function R(){L=!1,d.classList.remove("pf-open"),g.classList.remove("pf-active"),setTimeout(()=>{H();const e=X._p?.canvas;e&&e.removeAttribute("tabindex"),s.focus()},280)}function P(){L?R():I()}z();let M=null;const A=5,T=120,B=document.createElement("div");function O(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:L?j:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function W(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>A&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<T?(d.style.transition="none",d.style.height="32px"):(j=a,z(),L||I(),d.style.transition="none",d.style.height=j+"px"),H()}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,o=M.h+a;M=null,B.style.display="none",document.body.style.userSelect="",d.style.transition="",d.style.height="",n&&(o<T?R():(j=Math.max(T,Math.min(window.innerHeight-50,o)),z(),L||I()),H())}Object.assign(B.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(B),k.addEventListener("click",e=>{e.stopPropagation(),P()}),l.addEventListener("mousedown",O,!0),document.addEventListener("mousemove",W),document.addEventListener("mouseup",D),l.addEventListener("touchstart",O,{passive:!1}),document.addEventListener("touchmove",W,{passive:!0}),document.addEventListener("touchend",D);let K=0,N=0;function U(e){_.textContent=e,_.style.display="block",I()}function F(){_.textContent="",_.style.display="none"}function $(){if(!X._p||"fit"!==X._mode)return;const e=X._w,n=X._h;if(!e||!n)return;const t=s.clientWidth,a=s.clientHeight,o=Math.min(t/e,a/n);p.style.transform=`scale(${o})`}function H(){if("fullscreen"===X._mode?X.size("max"):$(),Y&&"function"==typeof Y.windowResized)try{Y.windowResized()}catch(e){U(String(e))}J&&J.resize()}window.addEventListener("mousemove",e=>{K=e.clientX,N=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(K=e.touches[0].clientX,N=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=X._p?X._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=X._w/n.width,a=X._h/n.height;return[(K-n.left)*t,(N-n.top)*a]},window.addEventListener("resize",H);let Y=null;const X=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=s.clientWidth,this._h=s.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),p.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),$())},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){E.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 G(){if(Ae(),Y){try{Y.remove()}catch(e){}Y=null}c.innerHTML="",X._p=null,X._mode="fit",X._w=0,X._h=0,p.style.transform="scale(1)",E.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",he&&(he.destroy(),he=null),fe&&(fe.destroy(),fe=null),ue&&(ue.destroy(),ue=null),_e&&(_e.destroy(),_e=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),be&&(be.destroy(),be=null),ve&&(ve.destroy(),ve=null),we&&(we.destroy(),we=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)}window.p5py=X;let J=null,V=null;const q={},Z=new Set;function Q(){C.innerHTML="",V=null;const n=e.filter(e=>!e.hidden);C.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",()=>ee(e)),C.appendChild(n)}),n.length>0&&ee(n[0],!0)}function ee(e,n){if(n||V!==e)if(V=e,C.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",S.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>`)),S.innerHTML=`<div class="pf-md-inner">${n}</div>`}else S.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:S.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",S.style.display="none",J&&q[e.id]&&(J.setSession(q[e.id]),J.setReadOnly(e.readonly),J.focus())}function ne(){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 te(){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),Z.delete(e)),e=setTimeout(()=>{Z.delete(e),e=null,oe()},350),Z.add(e),ne(),ie()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);J&&n&&q[n.id]&&(J.setSession(q[n.id]),J.setReadOnly(n.readonly),J.renderer.updateFull(!0)),ne()}function ae(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),J=ace.edit("pf-ace"),J.setTheme("ace/theme/monokai"),J.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),J.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{J.completer?.popup?.isOpen||je()}}),J.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:R}),J.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:re}),J.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}}),te(),Q(),ie()}function oe(){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 re(){oe()}function ie(){const n=e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&q[e.id]&&q[e.id].getValue()!==e.starterCode);w.classList.toggle("pf-dirty",n)}function se(){Z.forEach(e=>clearTimeout(e)),Z.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){}e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),te(),Q(),ie(),je()}window.addEventListener("beforeunload",re);let de=null,le=null;async function ce(){return le||(le=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),de=await loadPyodide(e),de.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\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 fn()\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n 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"),J){pe(de.runPython("list(m.__all__)").toJs())}})(),le)}function pe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,r){r(null,o.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,J.completers=[...J.completers||[],t]}let me=!1,fe=null,ue=null,he=null,_e=null,ye=null,ge=null,be=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null,Se=null;const Le=300;async function je(){if(me)return;me=!0,y.classList.add("pf-running"),F(),G(),de||(f.textContent="Initialisation de Pyodide…",m.style.display="flex");try{await ce()}catch(e){return m.style.display="none",U("Erreur Pyodide : "+e),me=!1,void y.classList.remove("pf-running")}m.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{f.textContent="Chargement des dépendances…",m.style.display="flex",await de.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}m.style.display="none",de.globals.set("_USER_CODE",t);try{de.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),de.runPython("_ns_ref[0] = _ns")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}let a,o,i,s,d,l,p,u,h,_,g,b,v,w;try{const e=(e,n)=>de.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),u=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),b=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),w=e("touchEnded","touch_ended")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}if(!o)return U("Le script doit définir au moins une fonction draw()."),me=!1,void y.classList.remove("pf-running");const{create_proxy:x}=de.pyimport("pyodide.ffi"),k=de.runPython("_ns.get('windowResized')"),E=de.globals.get("_pf_refresh"),C=de.globals.get(r?"_pf_draw_direct":"_pf_draw_watchdog"),S=de.globals.get("_ns"),L=e=>e?x(()=>{try{E(S),e()}catch(e){U(String(e))}}):null;he=d?x(()=>{try{d()}catch(e){U(String(e))}}):null,fe=a?x(()=>{try{a()}catch(e){U(String(e))}}):null,ue=x(()=>{try{E(S),C(o,Le)}catch(e){const n=String(e);G(),n.includes("TimeoutError")||n.includes("watchdog")?U(`draw() a dépassé ${Le}ms — sketch arrêté (watchdog).`):U(n)}}),_e=L(i),ye=L(p),ge=L(l),be=L(u),ve=L(h),we=L(_),xe=L(s),ke=L(g),Ee=L(b),Ce=L(v),Se=L(w);const j=k?x(()=>{try{k()}catch(e){U(String(e))}}):null;let z=!1;Y=new p5(e=>{X._setP(e),he&&(e.preload=()=>{he()}),e.setup=()=>{fe&&fe(),e.canvas||X.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&ue()},e.mousePressed=()=>{z&&_e&&_e()},e.mouseReleased=()=>{z&&ye&&ye()},e.mouseDragged=()=>{z&&ge&&ge()},e.mouseMoved=()=>{z&&be&&be()},e.mouseWheel=e=>{z&&ve&&ve()},e.doubleClicked=()=>{z&&we&&we()},e.keyPressed=()=>{z&&xe&&xe()},e.keyReleased=()=>{z&&ke&&ke()},Ee&&(e.touchStarted=()=>{z&&Ee()}),Ce&&(e.touchMoved=()=>{z&&Ce()}),Se&&(e.touchEnded=()=>{z&&Se()}),e.windowResized=()=>{"fullscreen"===X._mode?X.size("max"):$(),j&&j()}},c),me=!1,y.classList.remove("pf-running")}const ze='<!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.5.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Ie(){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=[],o="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="${o}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=ze.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),r=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}let Re=null,Pe=[];function Me(){const e=X._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();Re=new MediaRecorder(t,{mimeType:n}),Pe=[],Re.ondataavailable=e=>{e.data.size&&Pe.push(e.data)},Re.onstop=()=>{const e=new Blob(Pe,{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),v.textContent="⏺",v.title="Enregistrer WebM",v.classList.remove("pf-recording"),Re=null},Re.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function Ae(){Re&&"inactive"!==Re.state&&Re.stop()}v.addEventListener("click",()=>{Re?Ae():Me()}),y.addEventListener("click",()=>je()),g.addEventListener("click",()=>{L?R():(j=window.innerHeight-32,z(),I())}),b.addEventListener("click",Ie);const Te="https://codeberg.org/nopid/pyfrilet";function Be(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)})}x.addEventListener("click",()=>window.open(Te,"_blank")),w.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}),window.addEventListener("keydown",e=>{const n=L&&J&&J.isFocused&&J.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void je();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),J.searchBox?J.searchBox.hide():t.style.display="none",void J.focus();if(n){const n=J.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void R()}return e.preventDefault(),e.stopPropagation(),void(L?R():I())}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.")&&se())):(e.preventDefault(),void re())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",m.style.display="flex";try{if(await Be(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await Be(o.marked),await Be(o.katex),await Be(o.markedKatex),await Be(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Be(o.ace),await Be(o.acePython),await Be(o.aceMonokai),await Be(o.aceLangTools),await Be(o.aceSearchbox),await Be(o.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}ae(),await je(),m.style.display="none"})()}(C,k,x,w,j)})}();
|
|
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 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 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 N(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",D),document.addEventListener("mouseup",N),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",N);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_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);try{ce.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ce.runPython("_ns_ref[0] = _ns")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let a,r,i,s,d,l,c,p,f,h,b,g,w,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),r=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),p=e("mouseMoved","mouse_moved"),f=e("mouseWheel","mouse_wheel"),h=e("doubleClicked","double_clicked"),b=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),w=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!r)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:x}=ce.pyimport("pyodide.ffi"),k=ce.runPython("_ns.get('windowResized')"),E=ce.globals.get("_pf_refresh"),C=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),S=ce.globals.get("_ns"),j=ce.globals.get("_pf_safe_call"),L=e=>e?x(()=>{try{E(S),j(e)}catch(e){Ze("")}}):null;be=d?x(()=>{try{j(d)}catch(e){Ze("")}}):null,he=a?x(()=>{try{j(a)}catch(e){Ze("")}}):null,ye=x(()=>{try{E(S),C(r,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=L(i),we=L(c),ve=L(l),xe=L(p),ke=L(f),Ee=L(h),Ce=L(s),Se=L(b),je=L(g),Le=L(w),ze=L(v);const z=k?x(()=>{try{j(k)}catch(e){Ze("")}}):null;let R=!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(),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()}),ze&&(e.touchEnded=()=>{R&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),z&&z()}},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.0/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 De="https://codeberg.org/nopid/pyfrilet";function Ne(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(De,"_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 Ne(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await Ne(r.marked),await Ne(r.katex),await Ne(r.markedKatex),await Ne(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ne(r.ace),await Ne(r.acePython),await Ne(r.aceMonokai),await Ne(r.aceLangTools),await Ne(r.aceSearchbox),await Ne(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await Ne(r.xterm),await Ne(r.xtermFit),await Ne(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)})}();
|