pyfrilet 0.4.2 → 0.5.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 +158 -55
- package/package.json +1 -1
- package/pyfrilet.js +306 -75
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,7 @@ Il combine [p5.js](https://p5js.org/) (dessin 2D) et [Pyodide](https://pyodide.o
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
##
|
|
9
|
-
|
|
10
|
-
Un sketch est un fichier HTML minimal avec son code Python embarqué.
|
|
8
|
+
## Démarrage rapide
|
|
11
9
|
|
|
12
10
|
```html
|
|
13
11
|
<!doctype html>
|
|
@@ -18,7 +16,7 @@ Un sketch est un fichier HTML minimal avec son code Python embarqué.
|
|
|
18
16
|
<script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
|
|
19
17
|
</head>
|
|
20
18
|
<body>
|
|
21
|
-
<script type="text/python"
|
|
19
|
+
<script type="text/python">
|
|
22
20
|
|
|
23
21
|
from p5 import *
|
|
24
22
|
|
|
@@ -36,7 +34,11 @@ def draw():
|
|
|
36
34
|
</html>
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
C'est tout. pyfrilet se charge de démarrer Pyodide, de monter le module `p5`, et de lancer le sketch.
|
|
37
|
+
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
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Écrire un sketch Python
|
|
40
42
|
|
|
41
43
|
### API p5 disponible
|
|
42
44
|
|
|
@@ -98,6 +100,8 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
|
|
|
98
100
|
| `touchMoved()` | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
|
|
99
101
|
| `touchEnded()` | Fin de contact tactile. |
|
|
100
102
|
|
|
103
|
+
> **Limite de temps sur `draw()`** : si une exécution de `draw()` dépasse 200 ms, pyfrilet arrête le sketch et affiche une erreur. Cela protège le navigateur contre les boucles accidentellement bloquantes. Pour des calculs lourds, effectuer le travail en dehors de `draw()` (dans `setup()` ou via un état mis à jour progressivement).
|
|
104
|
+
|
|
101
105
|
#### Fonctions pyfrilet (hors p5.js standard)
|
|
102
106
|
|
|
103
107
|
| Fonction | Description |
|
|
@@ -215,12 +219,6 @@ def draw():
|
|
|
215
219
|
image(img, 0, 0, width, height)
|
|
216
220
|
```
|
|
217
221
|
|
|
218
|
-
`create_proxy` est nécessaire pour que Pyodide maintienne la référence à la fonction Python active entre les appels JS. `file.type` contient le type MIME (`"image/png"`…), `file.data` la data URL base64.
|
|
219
|
-
|
|
220
|
-
### Protection contre les boucles infinies
|
|
221
|
-
|
|
222
|
-
Si `draw()` met plus de 200 ms à s'exécuter, pyfrilet arrête le sketch automatiquement et affiche un message d'erreur. Cela protège le navigateur contre les calculs trop lourds qui bloqueraient l'interface. Pour des traitements lents, il est recommandé de les découper sur plusieurs frames via `frameCount`.
|
|
223
|
-
|
|
224
222
|
---
|
|
225
223
|
|
|
226
224
|
### Note sur `smooth()` / `noSmooth()` et le texte
|
|
@@ -249,7 +247,7 @@ La barre de contrôle est collée en bas de l'écran.
|
|
|
249
247
|
| ✏️ | Ouvre l'éditeur en **plein écran** ; referme si déjà ouvert |
|
|
250
248
|
| 💾 | Télécharge la page en HTML autonome (voir ci-dessous) |
|
|
251
249
|
| ⏺ | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement — cliquer pour arrêter et télécharger |
|
|
252
|
-
| ↻ | Réinitialise le code à la version d'origine (confirmation demandée). Un point orange apparaît sur le bouton quand le code a été modifié. |
|
|
250
|
+
| ↻ | Réinitialise le code de l'onglet actif à la version d'origine (confirmation demandée). Un point orange apparaît sur le bouton quand le code a été modifié. |
|
|
253
251
|
|
|
254
252
|
### Raccourcis clavier
|
|
255
253
|
|
|
@@ -258,50 +256,162 @@ La barre de contrôle est collée en bas de l'écran.
|
|
|
258
256
|
| `Shift+Entrée` | Relance l'exécution (depuis l'éditeur ou depuis le sketch) |
|
|
259
257
|
| `Échap` | Ouvre le tiroir si fermé, ferme si ouvert |
|
|
260
258
|
| `Ctrl+S` | Sauvegarde le code dans le localStorage |
|
|
261
|
-
| `Ctrl+R` | Réinitialise
|
|
259
|
+
| `Ctrl+R` | Réinitialise l'onglet actif à la version d'origine (confirmation demandée) |
|
|
262
260
|
|
|
263
261
|
Quand le tiroir se ferme, le focus clavier est automatiquement donné au canvas — les événements `keyPressed()` du sketch fonctionnent sans clic préalable.
|
|
264
262
|
|
|
265
|
-
Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification. Il est restauré à l'ouverture de la page.
|
|
263
|
+
Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification (par onglet). Il est restauré à l'ouverture de la page.
|
|
266
264
|
|
|
267
265
|
### Télécharger
|
|
268
266
|
|
|
269
|
-
Le bouton 💾 génère un fichier `sketch.html` autonome : il référence pyfrilet depuis jsDelivr et
|
|
267
|
+
Le bouton 💾 génère un fichier `sketch.html` autonome : il référence pyfrilet depuis jsDelivr et reconstruit tous les blocs `<script>` tels qu'ils sont dans l'éditeur au moment du clic, en préservant leur structure (onglets, blocs cachés, blocs en lecture seule). Le fichier produit charge p5.js et Pyodide depuis le CDN et n'a besoin d'aucun serveur pour fonctionner.
|
|
270
268
|
|
|
271
269
|
---
|
|
272
270
|
|
|
273
|
-
##
|
|
271
|
+
## Onglets (utilisation avancée)
|
|
272
|
+
|
|
273
|
+
Quand une page contient plusieurs blocs de code, pyfrilet affiche une barre d'onglets dans l'éditeur. Chaque bloc est déclaré avec un `<script>` séparé.
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
Tous les blocs Python partagent le même namespace : les variables et fonctions définies dans un bloc sont visibles dans les autres.
|
|
276
|
+
|
|
277
|
+
### Attributs disponibles
|
|
278
|
+
|
|
279
|
+
| Attribut | Effet |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `data-tab="Nom"` | Crée un onglet visible avec ce libellé |
|
|
282
|
+
| `data-readonly` | L'onglet est visible mais non modifiable (cadenas affiché) |
|
|
283
|
+
| `data-hidden` | Le bloc est exécuté mais n'apparaît pas dans l'éditeur |
|
|
284
|
+
| `type="text/markdown"` | Le contenu est rendu en Markdown (non exécuté) |
|
|
285
|
+
|
|
286
|
+
### Exemple : énoncé + code utilitaire + zone élève
|
|
276
287
|
|
|
277
288
|
```html
|
|
278
|
-
<script
|
|
289
|
+
<script type="text/markdown" data-tab="Énoncé">
|
|
290
|
+
# Exercice
|
|
291
|
+
|
|
292
|
+
Complète la fonction `dessiner()` pour afficher un cercle rouge au centre du canvas.
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
<script type="text/python" data-tab="Utilitaires" data-readonly>
|
|
296
|
+
# Fonctions fournies — non modifiables
|
|
297
|
+
def rouge():
|
|
298
|
+
fill(255, 0, 0)
|
|
299
|
+
no_stroke()
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<script type="text/python" data-tab="Ta solution">
|
|
303
|
+
from p5 import *
|
|
304
|
+
|
|
305
|
+
def setup():
|
|
306
|
+
size(400, 400)
|
|
307
|
+
|
|
308
|
+
def draw():
|
|
309
|
+
background(20)
|
|
310
|
+
# à toi de jouer !
|
|
311
|
+
</script>
|
|
279
312
|
```
|
|
280
313
|
|
|
281
|
-
|
|
314
|
+
### Bloc caché
|
|
315
|
+
|
|
316
|
+
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 :
|
|
282
317
|
|
|
283
318
|
```html
|
|
284
|
-
<script
|
|
319
|
+
<script type="text/python" data-hidden>
|
|
320
|
+
# Code invisible — exécuté en premier
|
|
321
|
+
PALETTE = ['#7aa2f7', '#e0af68', '#9ece6a']
|
|
322
|
+
</script>
|
|
323
|
+
|
|
324
|
+
<script type="text/python" data-tab="Sketch">
|
|
325
|
+
from p5 import *
|
|
326
|
+
|
|
327
|
+
def setup():
|
|
328
|
+
size(400, 400)
|
|
329
|
+
|
|
330
|
+
def draw():
|
|
331
|
+
background(20)
|
|
332
|
+
fill(PALETTE[frameCount // 60 % 3])
|
|
333
|
+
circle(200, 200, 100)
|
|
334
|
+
</script>
|
|
285
335
|
```
|
|
286
336
|
|
|
287
|
-
|
|
337
|
+
### Onglets Markdown
|
|
338
|
+
|
|
339
|
+
Le contenu d'un onglet `type="text/markdown"` est rendu avec [marked](https://marked.js.org/) et supporte les formules mathématiques via [KaTeX](https://katex.org/) et les diagrammes via [Mermaid](https://mermaid.js.org/).
|
|
340
|
+
|
|
341
|
+
**Formules mathématiques (KaTeX) :**
|
|
342
|
+
|
|
343
|
+
| Syntaxe | Rendu |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `$f(x) = x^2$` | Formule inline dans le texte |
|
|
346
|
+
| `$$\sum_{i=0}^{n} i = \frac{n(n+1)}{2}$$` | Bloc centré sur sa propre ligne |
|
|
347
|
+
|
|
348
|
+
**Diagrammes (Mermaid) :**
|
|
349
|
+
|
|
350
|
+
Un bloc de code avec le langage `mermaid` est rendu comme un diagramme SVG :
|
|
351
|
+
|
|
352
|
+
````markdown
|
|
353
|
+
```mermaid
|
|
354
|
+
graph TD
|
|
355
|
+
A[Départ] --> B{Condition}
|
|
356
|
+
B -->|oui| C[Résultat 1]
|
|
357
|
+
B -->|non| D[Résultat 2]
|
|
358
|
+
```
|
|
359
|
+
````
|
|
360
|
+
|
|
361
|
+
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.
|
|
362
|
+
|
|
363
|
+
**Exemple combiné :**
|
|
364
|
+
|
|
365
|
+
```html
|
|
366
|
+
<script type="text/markdown" data-tab="Cours">
|
|
367
|
+
# Algorithme de Dijkstra
|
|
368
|
+
|
|
369
|
+
Soit un graphe $G = (V, E)$ avec des poids $w(u, v) \geq 0$.
|
|
370
|
+
|
|
371
|
+
L'algorithme maintient un ensemble $S$ de sommets dont la distance
|
|
372
|
+
minimale depuis la source $s$ est connue. À chaque étape on extrait
|
|
373
|
+
le sommet $u \notin S$ qui minimise :
|
|
374
|
+
|
|
375
|
+
$$d(s, u) = \min_{v \in S} \left( d(s, v) + w(v, u) \right)$$
|
|
376
|
+
|
|
377
|
+
La complexité est $O((V + E) \log V)$ avec un tas binaire.
|
|
378
|
+
|
|
379
|
+
```mermaid
|
|
380
|
+
graph LR
|
|
381
|
+
A((1)) -->|4| B((2))
|
|
382
|
+
A -->|1| C((3))
|
|
383
|
+
C -->|2| B
|
|
384
|
+
B -->|1| D((4))
|
|
385
|
+
```
|
|
386
|
+
</script>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
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.
|
|
288
392
|
|
|
289
393
|
---
|
|
290
394
|
|
|
291
395
|
## Déploiement local (mode `vendor/`)
|
|
292
396
|
|
|
293
|
-
Par défaut
|
|
397
|
+
Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour un déploiement entièrement local — intranet, usage hors ligne, ou pour ne pas dépendre de services tiers — on peut héberger les dépendances soi-même.
|
|
294
398
|
|
|
295
|
-
###
|
|
399
|
+
### Configuration
|
|
296
400
|
|
|
297
|
-
|
|
401
|
+
`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">`.
|
|
298
402
|
|
|
299
403
|
```html
|
|
300
|
-
<!--
|
|
404
|
+
<!-- Recommandé -->
|
|
405
|
+
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
|
|
406
|
+
|
|
407
|
+
<!-- Rétrocompat (ancienne syntaxe) -->
|
|
301
408
|
<script src="pyfrilet.js"></script>
|
|
409
|
+
<script type="text/python" data-sources="local" data-vendor="vendor/">
|
|
410
|
+
…
|
|
411
|
+
</script>
|
|
302
412
|
```
|
|
303
413
|
|
|
304
|
-
|
|
414
|
+
`data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/`.
|
|
305
415
|
|
|
306
416
|
### Structure de fichiers
|
|
307
417
|
|
|
@@ -316,6 +426,11 @@ mon-projet/
|
|
|
316
426
|
├── theme-monokai.min.js
|
|
317
427
|
├── ext-language_tools.min.js
|
|
318
428
|
├── ext-searchbox.min.js
|
|
429
|
+
├── marked.min.js ← uniquement si onglets Markdown
|
|
430
|
+
├── katex.min.css ← uniquement si onglets Markdown
|
|
431
|
+
├── katex.min.js ← uniquement si onglets Markdown
|
|
432
|
+
├── marked-katex-extension.js ← uniquement si onglets Markdown
|
|
433
|
+
├── mermaid.min.js ← uniquement si onglets Markdown
|
|
319
434
|
└── pyodide/
|
|
320
435
|
├── pyodide.js
|
|
321
436
|
├── pyodide.asm.wasm
|
|
@@ -339,25 +454,25 @@ https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js
|
|
|
339
454
|
https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js
|
|
340
455
|
```
|
|
341
456
|
|
|
457
|
+
**marked.js + KaTeX + Mermaid** (uniquement si onglets Markdown)
|
|
458
|
+
```
|
|
459
|
+
https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js
|
|
460
|
+
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css
|
|
461
|
+
https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js
|
|
462
|
+
https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js
|
|
463
|
+
https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js
|
|
464
|
+
```
|
|
465
|
+
Renommer `index.umd.js` en `marked-katex-extension.js` dans le dossier vendor.
|
|
466
|
+
|
|
342
467
|
**Pyodide** — télécharger l'archive complète depuis les releases GitHub :
|
|
343
468
|
```
|
|
344
469
|
https://github.com/pyodide/pyodide/releases/tag/0.26.4
|
|
345
470
|
```
|
|
346
471
|
Extraire le contenu dans `vendor/pyodide/`.
|
|
347
472
|
|
|
348
|
-
### Balise HTML
|
|
349
|
-
|
|
350
|
-
```html
|
|
351
|
-
<script type="text/python" data-sources="local" data-vendor="vendor/">
|
|
352
|
-
…code python…
|
|
353
|
-
</script>
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`data-vendor` indique le chemin vers le dossier `vendor/` **relatif à la page HTML**. La valeur par défaut est `vendor/` si l'attribut est absent.
|
|
357
|
-
|
|
358
473
|
### Serveur local
|
|
359
474
|
|
|
360
|
-
Les navigateurs bloquent le chargement de fichiers locaux (`file://`) pour des raisons de sécurité. Il faut
|
|
475
|
+
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 :
|
|
361
476
|
|
|
362
477
|
```bash
|
|
363
478
|
# Python 3
|
|
@@ -373,10 +488,10 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
|
373
488
|
|
|
374
489
|
## Build et publication
|
|
375
490
|
|
|
376
|
-
Le build est géré par `build.js` (Node.js + Terser). Il
|
|
491
|
+
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`.
|
|
377
492
|
|
|
378
493
|
```bash
|
|
379
|
-
npm run build # génère pyfrilet.min.js
|
|
494
|
+
npm run build # génère pyfrilet.js + pyfrilet.min.js
|
|
380
495
|
```
|
|
381
496
|
|
|
382
497
|
Le hook `prepublishOnly` dans `package.json` déclenche le build automatiquement avant chaque `npm publish`. Le flux complet d'une release :
|
|
@@ -391,8 +506,6 @@ npm publish # build automatique puis publication sur npm
|
|
|
391
506
|
git push && git push --tags # pousser commits et tag sur Codeberg
|
|
392
507
|
```
|
|
393
508
|
|
|
394
|
-
`npm version` incrémente le numéro de version selon les conventions semver : `patch` pour `z`, `minor` pour `y` (remet `z` à 0), `major` pour `x` (remet `y` et `z` à 0).
|
|
395
|
-
|
|
396
509
|
---
|
|
397
510
|
|
|
398
511
|
## Licence
|
|
@@ -414,20 +527,10 @@ pyfrilet ne contient aucun code de ces bibliothèques ; elles sont chargées sé
|
|
|
414
527
|
| [p5.js](https://p5js.org/) | LGPL 2.1 |
|
|
415
528
|
| [Pyodide](https://pyodide.org/) | MPL 2.0 |
|
|
416
529
|
| [ACE editor](https://ace.c9.io/) | BSD 3-Clause |
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
```
|
|
423
|
-
mon-sketch/
|
|
424
|
-
├── LICENSE ← texte de la LGPL 2.1
|
|
425
|
-
├── README.md ← cette doc, ou un résumé
|
|
426
|
-
├── pyfrilet.js
|
|
427
|
-
└── mon-sketch.html
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
Les dépendances (`vendor/`) n'ont pas à être versionnées si on utilise le mode CDN. Si on veut un déploiement hors ligne, on les ajoute au dépôt en respectant leurs licences respectives — un fichier `NOTICE` listant les bibliothèques tierces et leurs licences est une bonne pratique.
|
|
530
|
+
| [marked](https://marked.js.org/) | MIT |
|
|
531
|
+
| [KaTeX](https://katex.org/) | MIT |
|
|
532
|
+
| [marked-katex-extension](https://github.com/UziTech/marked-katex-extension) | MIT |
|
|
533
|
+
| [Mermaid](https://mermaid.js.org/) | MIT |
|
|
431
534
|
|
|
432
535
|
---
|
|
433
536
|
|
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
(function () {
|
|
22
22
|
'use strict';
|
|
23
23
|
|
|
24
|
+
/* Capture immediately — document.currentScript becomes null after script execution */
|
|
25
|
+
const _pfScriptTag = document.currentScript;
|
|
26
|
+
|
|
24
27
|
/* ═══════════════════════════ CDN URLS ═══════════════════════════════ */
|
|
25
28
|
let isCdn = false; /* set in DOMContentLoaded from data-sources attribute */
|
|
26
29
|
const CDN = {
|
|
@@ -31,11 +34,15 @@ const CDN = {
|
|
|
31
34
|
aceMonokai : 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js',
|
|
32
35
|
aceLangTools: 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js',
|
|
33
36
|
aceSearchbox: 'https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js',
|
|
37
|
+
marked : 'https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js',
|
|
38
|
+
katexCss : 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css',
|
|
39
|
+
katex : 'https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js',
|
|
40
|
+
markedKatex : 'https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js',
|
|
41
|
+
mermaid : 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
|
|
34
42
|
};
|
|
35
43
|
|
|
36
44
|
/* ═══════════════════════════ STYLES ═════════════════════════════════ */
|
|
37
|
-
const STYLES = `
|
|
38
|
-
html, body {
|
|
45
|
+
const STYLES = `html, body {
|
|
39
46
|
height: 100%; margin: 0; overflow: hidden;
|
|
40
47
|
background: #111;
|
|
41
48
|
}
|
|
@@ -204,8 +211,50 @@ html, body {
|
|
|
204
211
|
flex: 1;
|
|
205
212
|
min-height: 80px;
|
|
206
213
|
position: relative;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
}
|
|
217
|
+
#pf-ace { flex: 1; position: relative; min-height: 0; }
|
|
218
|
+
|
|
219
|
+
/* ── tab bar ── */
|
|
220
|
+
#pf-tabs {
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-shrink: 0;
|
|
223
|
+
background: #1a1b2e;
|
|
224
|
+
border-bottom: 1px solid #414868;
|
|
225
|
+
overflow-x: auto;
|
|
226
|
+
scrollbar-width: none;
|
|
227
|
+
}
|
|
228
|
+
#pf-tabs:empty { display: none; }
|
|
229
|
+
.pf-tab {
|
|
230
|
+
padding: 5px 14px;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
background: transparent;
|
|
233
|
+
border: none;
|
|
234
|
+
border-bottom: 2px solid transparent;
|
|
235
|
+
color: #737aa2;
|
|
236
|
+
cursor: pointer;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
transition: color .15s, border-color .15s;
|
|
207
239
|
}
|
|
208
|
-
|
|
240
|
+
.pf-tab:hover { color: #c0caf5; }
|
|
241
|
+
.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }
|
|
242
|
+
.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }
|
|
243
|
+
.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }
|
|
244
|
+
|
|
245
|
+
/* ── markdown view ── */
|
|
246
|
+
#pf-markdown-view {
|
|
247
|
+
flex: 1;
|
|
248
|
+
overflow: auto;
|
|
249
|
+
padding: 14px 18px;
|
|
250
|
+
background: #1a1b2e;
|
|
251
|
+
color: #c0caf5;
|
|
252
|
+
font-size: 14px;
|
|
253
|
+
line-height: 1.6;
|
|
254
|
+
}
|
|
255
|
+
#pf-markdown-view h1,#pf-markdown-view h2,#pf-markdown-view h3 { color: #7aa2f7; }
|
|
256
|
+
#pf-markdown-view code { background: #24283b; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
|
|
257
|
+
#pf-markdown-view pre code { display: block; padding: 10px; overflow: auto; }
|
|
209
258
|
|
|
210
259
|
/* ── error panel (below editor, never overlaps ACE) ── */
|
|
211
260
|
#pf-err {
|
|
@@ -219,12 +268,10 @@ html, body {
|
|
|
219
268
|
white-space: pre-wrap;
|
|
220
269
|
display: none;
|
|
221
270
|
border-top: 1px solid rgba(247, 118, 142, .35);
|
|
222
|
-
}
|
|
223
|
-
`;
|
|
271
|
+
}`;
|
|
224
272
|
|
|
225
273
|
/* ═══════════════════════════ MARKUP ═════════════════════════════════ */
|
|
226
|
-
const MARKUP =
|
|
227
|
-
<div id="pf-root">
|
|
274
|
+
const MARKUP = `<div id="pf-root">
|
|
228
275
|
<div id="pf-app" tabindex="-1">
|
|
229
276
|
<div id="pf-viewport"><div id="pf-sketch"></div></div>
|
|
230
277
|
<div id="pf-loader">
|
|
@@ -246,37 +293,50 @@ const MARKUP = `
|
|
|
246
293
|
</div>
|
|
247
294
|
</div>
|
|
248
295
|
<div id="pf-editor-wrap">
|
|
296
|
+
<div id="pf-tabs"></div>
|
|
297
|
+
<div id="pf-markdown-view" style="display:none"></div>
|
|
249
298
|
<div id="pf-ace"></div>
|
|
250
299
|
</div>
|
|
251
300
|
<pre id="pf-err"></pre>
|
|
252
301
|
</div>
|
|
253
|
-
</div
|
|
254
|
-
`;
|
|
302
|
+
</div>`;
|
|
255
303
|
|
|
256
304
|
/* ═══════════════════════════ ENTRY POINT ════════════════════════════ */
|
|
257
305
|
document.addEventListener('DOMContentLoaded', function () {
|
|
258
306
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
307
|
+
/* Collect all python/markdown script blocks in DOM order */
|
|
308
|
+
const allScripts = [
|
|
309
|
+
...document.querySelectorAll(
|
|
310
|
+
'script[type="text/python"], script[type="text/markdown"], python'
|
|
311
|
+
)
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
if (allScripts.length === 0) {
|
|
262
315
|
console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');
|
|
263
316
|
return;
|
|
264
317
|
}
|
|
265
318
|
|
|
319
|
+
/* Read sources/vendor from the first script tag */
|
|
320
|
+
const firstScript = allScripts[0];
|
|
321
|
+
/* Config tag: prefer the <script src="pyfrilet.js"> itself, fallback to first python tag (retro-compat) */
|
|
322
|
+
const configTag = _pfScriptTag || firstScript;
|
|
323
|
+
|
|
266
324
|
const sources = (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
'
|
|
325
|
+
configTag.getAttribute('data-sources') ||
|
|
326
|
+
configTag.getAttribute('sources') ||
|
|
327
|
+
'cdn'
|
|
270
328
|
).toLowerCase().trim();
|
|
271
329
|
|
|
272
330
|
const vpRaw = (
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
configTag.getAttribute('data-vendor') ||
|
|
332
|
+
configTag.getAttribute('vendor') ||
|
|
275
333
|
'vendor/'
|
|
276
334
|
);
|
|
277
335
|
const vp = vpRaw.replace(/\/?$/, '/'); /* ensure trailing slash */
|
|
278
336
|
|
|
279
337
|
isCdn = sources === 'cdn';
|
|
338
|
+
const hasMarked = allScripts.some(el => el.getAttribute('type') === 'text/markdown');
|
|
339
|
+
|
|
280
340
|
const URLS = isCdn ? {
|
|
281
341
|
p5 : CDN.p5,
|
|
282
342
|
pyodide : CDN.pyodide,
|
|
@@ -286,6 +346,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
286
346
|
aceMonokai : CDN.aceMonokai,
|
|
287
347
|
aceLangTools: CDN.aceLangTools,
|
|
288
348
|
aceSearchbox: CDN.aceSearchbox,
|
|
349
|
+
marked : hasMarked ? CDN.marked : null,
|
|
350
|
+
katexCss : hasMarked ? CDN.katexCss : null,
|
|
351
|
+
katex : hasMarked ? CDN.katex : null,
|
|
352
|
+
markedKatex : hasMarked ? CDN.markedKatex : null,
|
|
353
|
+
mermaid : hasMarked ? CDN.mermaid : null,
|
|
289
354
|
} : {
|
|
290
355
|
p5 : vp + 'p5.min.js',
|
|
291
356
|
pyodide : vp + 'pyodide/pyodide.js',
|
|
@@ -295,20 +360,42 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
295
360
|
aceMonokai : vp + 'theme-monokai.min.js',
|
|
296
361
|
aceLangTools: vp + 'ext-language_tools.min.js',
|
|
297
362
|
aceSearchbox: vp + 'ext-searchbox.min.js',
|
|
363
|
+
marked : hasMarked ? vp + 'marked.min.js' : null,
|
|
364
|
+
katexCss : hasMarked ? vp + 'katex.min.css' : null,
|
|
365
|
+
katex : hasMarked ? vp + 'katex.min.js' : null,
|
|
366
|
+
markedKatex : hasMarked ? vp + 'marked-katex-extension.js' : null,
|
|
367
|
+
mermaid : hasMarked ? vp + 'mermaid.min.js' : null,
|
|
298
368
|
};
|
|
299
369
|
|
|
300
|
-
|
|
301
|
-
|
|
370
|
+
const SK = 'pyfrilet:' + location.pathname;
|
|
371
|
+
|
|
372
|
+
/* Build tabs array from DOM */
|
|
373
|
+
const tabs = allScripts.map((el, i) => {
|
|
374
|
+
const type = el.getAttribute('type') === 'text/markdown' ? 'markdown' : 'python';
|
|
375
|
+
const hidden = el.hasAttribute('data-hidden');
|
|
376
|
+
const readonly = el.hasAttribute('data-readonly');
|
|
377
|
+
/* label: data-tab value, or null for hidden, or default label for untagged block */
|
|
378
|
+
let label = el.getAttribute('data-tab');
|
|
379
|
+
if (label === null && !hidden) label = allScripts.length === 1 ? 'Code' : `Bloc ${i + 1}`;
|
|
380
|
+
|
|
381
|
+
const rawCode = el.textContent.replace(/^\n/, ''); /* strip leading blank line */
|
|
382
|
+
const tabSK = SK + ':' + i;
|
|
383
|
+
|
|
384
|
+
/* Load saved code from localStorage for editable python tabs */
|
|
385
|
+
let code = rawCode;
|
|
386
|
+
if (type === 'python' && !hidden && !readonly) {
|
|
387
|
+
const saved = (() => { try { return localStorage.getItem(tabSK); } catch (e) { return null; } })();
|
|
388
|
+
if (saved && saved.trim()) code = saved;
|
|
389
|
+
}
|
|
302
390
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const initialCode = (saved && saved.trim()) ? saved : starterCode;
|
|
391
|
+
return { id: 'tab-' + i, label, hidden, readonly, type, starterCode: rawCode, code, sk: tabSK };
|
|
392
|
+
});
|
|
306
393
|
|
|
307
|
-
main(
|
|
394
|
+
main(tabs, SK, URLS);
|
|
308
395
|
});
|
|
309
396
|
|
|
310
397
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
311
|
-
function main(
|
|
398
|
+
function main(tabs, SK, URLS) {
|
|
312
399
|
|
|
313
400
|
/* ── inject styles + markup ── */
|
|
314
401
|
const styleEl = document.createElement('style');
|
|
@@ -333,6 +420,8 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
333
420
|
const btnHelp = document.getElementById('pf-btn-help');
|
|
334
421
|
const gripEl = document.getElementById('pf-grip');
|
|
335
422
|
const hintEl = document.getElementById('pf-handle-hint');
|
|
423
|
+
const tabsEl = document.getElementById('pf-tabs');
|
|
424
|
+
const markdownEl = document.getElementById('pf-markdown-view');
|
|
336
425
|
|
|
337
426
|
/* ─────────────────── DRAWER ─────────────────── */
|
|
338
427
|
let drawerOpen = false;
|
|
@@ -475,6 +564,7 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
475
564
|
(_rawMouseY - r.top) * sy,
|
|
476
565
|
];
|
|
477
566
|
};
|
|
567
|
+
|
|
478
568
|
function showError(txt) { errEl.textContent = txt; errEl.style.display = 'block'; openDrawer(); }
|
|
479
569
|
function clearError() { errEl.textContent = ''; errEl.style.display = 'none'; }
|
|
480
570
|
|
|
@@ -605,20 +695,85 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
605
695
|
if (touchEndedProxy) { touchEndedProxy.destroy(); touchEndedProxy = null; }
|
|
606
696
|
}
|
|
607
697
|
|
|
608
|
-
/* ─────────────────── ACE EDITOR ─────────────── */
|
|
609
|
-
let aceInst
|
|
698
|
+
/* ─────────────────── ACE EDITOR + TABS ─────────────── */
|
|
699
|
+
let aceInst = null;
|
|
700
|
+
let activeTab = null; /* current tab object */
|
|
701
|
+
|
|
702
|
+
/* Map tab.id → ACE EditSession (only for python editable visible tabs) */
|
|
703
|
+
const aceSessions = {};
|
|
704
|
+
|
|
705
|
+
/* ── Build tab bar ── */
|
|
706
|
+
function initTabs() {
|
|
707
|
+
const visibleTabs = tabs.filter(t => !t.hidden);
|
|
708
|
+
if (visibleTabs.length <= 1) {
|
|
709
|
+
tabsEl.style.display = 'none';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
visibleTabs.forEach(tab => {
|
|
713
|
+
const btn = document.createElement('button');
|
|
714
|
+
btn.className = 'pf-tab';
|
|
715
|
+
btn.dataset.tabId = tab.id;
|
|
716
|
+
btn.textContent = tab.label;
|
|
717
|
+
if (tab.readonly) btn.classList.add('pf-tab-readonly');
|
|
718
|
+
if (tab.type === 'markdown') btn.classList.add('pf-tab-markdown');
|
|
719
|
+
btn.addEventListener('click', () => switchTab(tab));
|
|
720
|
+
tabsEl.appendChild(btn);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
/* Activate first visible tab */
|
|
724
|
+
if (visibleTabs.length > 0) switchTab(visibleTabs[0], true);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function switchTab(tab, init) {
|
|
728
|
+
if (!init && activeTab === tab) return;
|
|
729
|
+
activeTab = tab;
|
|
730
|
+
|
|
731
|
+
/* Update tab button styles */
|
|
732
|
+
tabsEl.querySelectorAll('.pf-tab').forEach(btn => {
|
|
733
|
+
btn.classList.toggle('pf-tab-active', btn.dataset.tabId === tab.id);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (tab.type === 'markdown') {
|
|
737
|
+
/* Show markdown, hide ACE */
|
|
738
|
+
document.getElementById('pf-ace').style.display = 'none';
|
|
739
|
+
markdownEl.style.display = 'block';
|
|
740
|
+
if (window.marked) {
|
|
741
|
+
let html = marked.parse(tab.starterCode);
|
|
742
|
+
/* marked HTML-escapes code block content — unescape mermaid blocks
|
|
743
|
+
so mermaid can parse the diagram syntax correctly */
|
|
744
|
+
if (window.mermaid) {
|
|
745
|
+
html = html.replace(
|
|
746
|
+
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
747
|
+
(_, code) => `<div class="mermaid">${
|
|
748
|
+
code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
749
|
+
}</div>`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
markdownEl.innerHTML = html;
|
|
753
|
+
} else {
|
|
754
|
+
markdownEl.innerHTML = `<pre>${tab.starterCode}</pre>`;
|
|
755
|
+
}
|
|
756
|
+
if (window.mermaid) mermaid.run({ nodes: markdownEl.querySelectorAll('.mermaid') });
|
|
757
|
+
} else {
|
|
758
|
+
/* Show ACE, hide markdown */
|
|
759
|
+
document.getElementById('pf-ace').style.display = 'block';
|
|
760
|
+
markdownEl.style.display = 'none';
|
|
761
|
+
if (aceInst && aceSessions[tab.id]) {
|
|
762
|
+
aceInst.setSession(aceSessions[tab.id]);
|
|
763
|
+
aceInst.setReadOnly(tab.readonly);
|
|
764
|
+
aceInst.focus();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
610
768
|
|
|
611
769
|
function initAce() {
|
|
612
|
-
/* In local mode ACE cannot auto-detect where its dynamic modules live
|
|
613
|
-
(searchbox, keybindings…), so we set basePath explicitly. */
|
|
770
|
+
/* In local mode ACE cannot auto-detect where its dynamic modules live */
|
|
614
771
|
if (URLS.ace.startsWith('vendor') || !URLS.ace.startsWith('http')) {
|
|
615
772
|
ace.config.set('basePath', URLS.ace.replace(/\/[^/]+$/, '/'));
|
|
616
773
|
}
|
|
774
|
+
|
|
617
775
|
aceInst = ace.edit('pf-ace');
|
|
618
|
-
aceInst.session.setMode('ace/mode/python');
|
|
619
776
|
aceInst.setTheme('ace/theme/monokai');
|
|
620
|
-
aceInst.setValue(initialCode, -1);
|
|
621
|
-
btnReset.classList.toggle('pf-dirty', initialCode !== starterCode);
|
|
622
777
|
aceInst.setOptions({
|
|
623
778
|
fontSize : '15px',
|
|
624
779
|
showPrintMargin: false,
|
|
@@ -630,13 +785,31 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
630
785
|
enableSnippets : true,
|
|
631
786
|
});
|
|
632
787
|
|
|
788
|
+
/* Create one ACE session per visible python tab */
|
|
789
|
+
tabs.filter(t => !t.hidden && t.type === 'python').forEach(tab => {
|
|
790
|
+
const session = ace.createEditSession(tab.code, 'ace/mode/python');
|
|
791
|
+
session.setUseWorker(false);
|
|
792
|
+
session.setTabSize(4);
|
|
793
|
+
aceSessions[tab.id] = session;
|
|
794
|
+
|
|
795
|
+
if (!tab.readonly) {
|
|
796
|
+
let saveTimer = null;
|
|
797
|
+
session.on('change', () => {
|
|
798
|
+
clearTimeout(saveTimer);
|
|
799
|
+
saveTimer = setTimeout(() => saveTab(tab), 350);
|
|
800
|
+
/* dirty indicator only for the active tab */
|
|
801
|
+
if (activeTab === tab) {
|
|
802
|
+
btnReset.classList.toggle('pf-dirty', session.getValue() !== tab.starterCode);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
/* Keyboard shortcuts */
|
|
633
809
|
aceInst.commands.addCommand({
|
|
634
810
|
name: 'pfRun',
|
|
635
811
|
bindKey: { win: 'Shift-Enter', mac: 'Shift-Enter' },
|
|
636
|
-
exec: () => {
|
|
637
|
-
if (aceInst.completer?.popup?.isOpen) return;
|
|
638
|
-
runCode();
|
|
639
|
-
},
|
|
812
|
+
exec: () => { if (aceInst.completer?.popup?.isOpen) return; runCode(); },
|
|
640
813
|
});
|
|
641
814
|
aceInst.commands.addCommand({
|
|
642
815
|
name: 'pfClose',
|
|
@@ -652,24 +825,33 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
652
825
|
name: 'pfReset',
|
|
653
826
|
bindKey: { win: 'Ctrl-R', mac: 'Command-R' },
|
|
654
827
|
exec: () => {
|
|
655
|
-
if (
|
|
656
|
-
|
|
828
|
+
if (!activeTab || activeTab.readonly || activeTab.type !== 'python') return;
|
|
829
|
+
if (confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
|
|
830
|
+
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
657
831
|
runCode();
|
|
658
832
|
}
|
|
659
833
|
},
|
|
660
834
|
});
|
|
661
835
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
836
|
+
/* Activate first visible python tab in ACE (or first python session if tab is markdown) */
|
|
837
|
+
const firstPythonTab = tabs.find(t => !t.hidden && t.type === 'python');
|
|
838
|
+
if (firstPythonTab && aceSessions[firstPythonTab.id]) {
|
|
839
|
+
aceInst.setSession(aceSessions[firstPythonTab.id]);
|
|
840
|
+
aceInst.setReadOnly(firstPythonTab.readonly);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
initTabs();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function saveTab(tab) {
|
|
847
|
+
if (!tab || tab.readonly || tab.type !== 'python' || !aceSessions[tab.id]) return;
|
|
848
|
+
try { localStorage.setItem(tab.sk, aceSessions[tab.id].getValue()); } catch (e) {}
|
|
668
849
|
}
|
|
669
850
|
|
|
670
851
|
function saveCode() {
|
|
671
|
-
|
|
852
|
+
tabs.forEach(tab => saveTab(tab));
|
|
672
853
|
}
|
|
854
|
+
|
|
673
855
|
window.addEventListener('beforeunload', saveCode);
|
|
674
856
|
|
|
675
857
|
/* ─────────────────── PYODIDE ────────────────── */
|
|
@@ -931,7 +1113,14 @@ m.__getattr__ = _p5_getattr
|
|
|
931
1113
|
|
|
932
1114
|
loaderEl.style.display = 'none';
|
|
933
1115
|
|
|
934
|
-
|
|
1116
|
+
/* Concatenate all python tabs in DOM order (hidden + visible) */
|
|
1117
|
+
const code = tabs
|
|
1118
|
+
.filter(t => t.type === 'python')
|
|
1119
|
+
.map(t => {
|
|
1120
|
+
if (!t.hidden && !t.readonly && aceSessions[t.id]) return aceSessions[t.id].getValue();
|
|
1121
|
+
return t.code; /* hidden or readonly: use original/starter code */
|
|
1122
|
+
})
|
|
1123
|
+
.join('\n');
|
|
935
1124
|
|
|
936
1125
|
/* Auto-load any Pyodide-bundled packages the sketch imports. */
|
|
937
1126
|
try {
|
|
@@ -1043,9 +1232,9 @@ m.__getattr__ = _p5_getattr
|
|
|
1043
1232
|
p.doubleClicked = () => { if (setupDone && doubleClickedProxy) doubleClickedProxy(); };
|
|
1044
1233
|
p.keyPressed = () => { if (setupDone && keyPressedProxy) keyPressedProxy(); };
|
|
1045
1234
|
p.keyReleased = () => { if (setupDone && keyReleasedProxy) keyReleasedProxy(); };
|
|
1046
|
-
p.touchStarted
|
|
1047
|
-
p.touchMoved
|
|
1048
|
-
p.touchEnded
|
|
1235
|
+
if (touchStartedProxy) p.touchStarted = () => { if (setupDone) touchStartedProxy(); };
|
|
1236
|
+
if (touchMovedProxy) p.touchMoved = () => { if (setupDone) touchMovedProxy(); };
|
|
1237
|
+
if (touchEndedProxy) p.touchEnded = () => { if (setupDone) touchEndedProxy(); };
|
|
1049
1238
|
/* Called by p5 on actual window resize AND by notifyResize() */
|
|
1050
1239
|
p.windowResized = () => {
|
|
1051
1240
|
if (p5Bridge._mode === 'fullscreen') p5Bridge.size('max');
|
|
@@ -1059,7 +1248,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1059
1248
|
}
|
|
1060
1249
|
|
|
1061
1250
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1062
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@
|
|
1251
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.0/pyfrilet.min.js';
|
|
1063
1252
|
|
|
1064
1253
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1065
1254
|
<html lang="fr">
|
|
@@ -1071,16 +1260,36 @@ m.__getattr__ = _p5_getattr
|
|
|
1071
1260
|
</head>
|
|
1072
1261
|
<body>
|
|
1073
1262
|
|
|
1074
|
-
|
|
1075
|
-
FILLME-PYTHON
|
|
1076
|
-
<\/script>
|
|
1263
|
+
FILLME-SCRIPTS
|
|
1077
1264
|
|
|
1078
1265
|
</body>
|
|
1079
1266
|
</html>`;
|
|
1080
1267
|
|
|
1081
1268
|
function download() {
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1269
|
+
/* Reconstruct all script tags preserving structure, attributes, and current editor content */
|
|
1270
|
+
const scripts = tabs.map((tab, i) => {
|
|
1271
|
+
/* Get current content */
|
|
1272
|
+
let content;
|
|
1273
|
+
if (tab.type === 'python' && !tab.hidden && !tab.readonly && aceSessions[tab.id]) {
|
|
1274
|
+
content = aceSessions[tab.id].getValue();
|
|
1275
|
+
} else {
|
|
1276
|
+
content = tab.code;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/* Rebuild attributes */
|
|
1280
|
+
const attrs = [];
|
|
1281
|
+
const scriptType = tab.type === 'markdown' ? 'text/markdown' : 'text/python';
|
|
1282
|
+
if (tab.label !== null) attrs.push(`data-tab="${tab.label.replace(/"/g, '"')}"`);
|
|
1283
|
+
if (tab.hidden) attrs.push('data-hidden');
|
|
1284
|
+
if (tab.readonly) attrs.push('data-readonly');
|
|
1285
|
+
|
|
1286
|
+
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
1287
|
+
/* Escape </script> inside content */
|
|
1288
|
+
const safe = content.replace(/<\/script>/gi, '<\\/script>');
|
|
1289
|
+
return `<script type="${scriptType}"${attrStr}>\n${safe}\n<\/script>`;
|
|
1290
|
+
}).join('\n\n');
|
|
1291
|
+
|
|
1292
|
+
const html = STANDALONE_TEMPLATE.replace('FILLME-SCRIPTS', scripts);
|
|
1084
1293
|
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
|
1085
1294
|
const url = URL.createObjectURL(blob);
|
|
1086
1295
|
const a = Object.assign(document.createElement('a'), { href: url, download: 'sketch.html' });
|
|
@@ -1090,20 +1299,6 @@ FILLME-PYTHON
|
|
|
1090
1299
|
URL.revokeObjectURL(url);
|
|
1091
1300
|
}
|
|
1092
1301
|
|
|
1093
|
-
/* ─────────────────── BUTTON HANDLERS ────────── */
|
|
1094
|
-
btnRun.addEventListener('click', () => runCode());
|
|
1095
|
-
|
|
1096
|
-
/* Code button: open at full screen height, or close if already open */
|
|
1097
|
-
btnCode.addEventListener('click', () => {
|
|
1098
|
-
if (drawerOpen) {
|
|
1099
|
-
closeDrawer();
|
|
1100
|
-
} else {
|
|
1101
|
-
drawerH = window.innerHeight - 32;
|
|
1102
|
-
_applyDrawerH();
|
|
1103
|
-
openDrawer();
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
1302
|
/* ── WebM recording ─────────────────────────────────────────────── */
|
|
1108
1303
|
let mediaRecorder = null;
|
|
1109
1304
|
let recChunks = [];
|
|
@@ -1142,13 +1337,29 @@ FILLME-PYTHON
|
|
|
1142
1337
|
mediaRecorder ? stopRecording() : startRecording();
|
|
1143
1338
|
});
|
|
1144
1339
|
|
|
1340
|
+
/* ─────────────────── BUTTON HANDLERS ────────── */
|
|
1341
|
+
btnRun.addEventListener('click', () => runCode());
|
|
1342
|
+
|
|
1343
|
+
/* Code button: open at full screen height, or close if already open */
|
|
1344
|
+
btnCode.addEventListener('click', () => {
|
|
1345
|
+
if (drawerOpen) {
|
|
1346
|
+
closeDrawer();
|
|
1347
|
+
} else {
|
|
1348
|
+
drawerH = window.innerHeight - 32;
|
|
1349
|
+
_applyDrawerH();
|
|
1350
|
+
openDrawer();
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1145
1354
|
btnDl.addEventListener('click', download);
|
|
1355
|
+
|
|
1146
1356
|
const HELP_URL = 'https://codeberg.org/nopid/pyfrilet';
|
|
1147
1357
|
btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
|
|
1148
1358
|
|
|
1149
1359
|
btnReset.addEventListener('click', () => {
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1360
|
+
if (!activeTab || activeTab.readonly || activeTab.type !== 'python') return;
|
|
1361
|
+
if (aceSessions[activeTab.id] && confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
|
|
1362
|
+
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
1152
1363
|
runCode();
|
|
1153
1364
|
}
|
|
1154
1365
|
});
|
|
@@ -1210,9 +1421,11 @@ FILLME-PYTHON
|
|
|
1210
1421
|
/* Ctrl/Cmd+R: reset code (prevent browser reload) */
|
|
1211
1422
|
if ((ev.key === 'r' || ev.key === 'R') && (ev.ctrlKey || ev.metaKey) && !ev.altKey) {
|
|
1212
1423
|
ev.preventDefault();
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
|
|
1424
|
+
if (activeTab && !activeTab.readonly && activeTab.type === 'python' && aceSessions[activeTab.id]) {
|
|
1425
|
+
if (confirm('Réinitialiser cet onglet ? Les modifications seront perdues.')) {
|
|
1426
|
+
aceSessions[activeTab.id].setValue(activeTab.starterCode, -1);
|
|
1427
|
+
runCode();
|
|
1428
|
+
}
|
|
1216
1429
|
}
|
|
1217
1430
|
return;
|
|
1218
1431
|
}
|
|
@@ -1235,6 +1448,22 @@ FILLME-PYTHON
|
|
|
1235
1448
|
|
|
1236
1449
|
try {
|
|
1237
1450
|
await loadScript(URLS.p5);
|
|
1451
|
+
if (URLS.marked) {
|
|
1452
|
+
/* Load KaTeX CSS first (no JS dependency) */
|
|
1453
|
+
const katexLink = document.createElement('link');
|
|
1454
|
+
katexLink.rel = 'stylesheet';
|
|
1455
|
+
katexLink.href = URLS.katexCss;
|
|
1456
|
+
document.head.appendChild(katexLink);
|
|
1457
|
+
/* Then JS libs in order */
|
|
1458
|
+
await loadScript(URLS.marked);
|
|
1459
|
+
await loadScript(URLS.katex);
|
|
1460
|
+
await loadScript(URLS.markedKatex);
|
|
1461
|
+
await loadScript(URLS.mermaid);
|
|
1462
|
+
/* Configure marked: KaTeX extension */
|
|
1463
|
+
marked.use(markedKatex({ throwOnError: false }));
|
|
1464
|
+
/* Initialize mermaid (startOnLoad:false — we call run() manually) */
|
|
1465
|
+
mermaid.initialize({ startOnLoad: false, theme: 'dark' });
|
|
1466
|
+
}
|
|
1238
1467
|
await loadScript(URLS.ace);
|
|
1239
1468
|
await loadScript(URLS.acePython);
|
|
1240
1469
|
await loadScript(URLS.aceMonokai);
|
|
@@ -1251,6 +1480,8 @@ FILLME-PYTHON
|
|
|
1251
1480
|
await runCode();
|
|
1252
1481
|
loaderEl.style.display = 'none';
|
|
1253
1482
|
})();
|
|
1254
|
-
}
|
|
1255
1483
|
|
|
1256
|
-
|
|
1484
|
+
|
|
1485
|
+
} /* end main() */
|
|
1486
|
+
|
|
1487
|
+
})(); /* end IIFE */
|
package/pyfrilet.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";let e=!1;const n="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",t="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",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="\nhtml, 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}\n#pf-ace { position: absolute; inset: 0; }\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",d='\n<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-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>\n';document.addEventListener("DOMContentLoaded",function(){const c=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!c)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const p=(c.getAttribute("data-sources")||c.getAttribute("sources")||"local").toLowerCase().trim(),u=(c.getAttribute("data-vendor")||c.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");e="cdn"===p;const f=e?{p5:n,pyodide:t,pyodideIndex:null,ace:o,acePython:a,aceMonokai:s,aceLangTools:i,aceSearchbox:r}:{p5:u+"p5.min.js",pyodide:u+"pyodide/pyodide.js",pyodideIndex:u+"pyodide/",ace:u+"ace.min.js",acePython:u+"mode-python.min.js",aceMonokai:u+"theme-monokai.min.js",aceLangTools:u+"ext-language_tools.min.js",aceSearchbox:u+"ext-searchbox.min.js"},m=c.textContent.replace(/^\n/,""),_="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(_)}catch(e){return null}})();!function(n,t,o,a){const s=document.createElement("style");s.textContent=l,document.head.appendChild(s),document.body.innerHTML=d;const i=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),c=document.getElementById("pf-handle"),p=document.getElementById("pf-sketch"),u=document.getElementById("pf-viewport"),f=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),_=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),g=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-rec"),v=document.getElementById("pf-btn-reset"),x=document.getElementById("pf-btn-help"),w=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint");let E=!1,C=Math.round(.56*window.innerHeight);function L(){document.documentElement.style.setProperty("--pf-drawer-h",C+"px")}function S(){E=!0,r.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{F(),V&&V.focus()},280)}function z(){E=!1,r.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{F();const e=K._p?.canvas;e&&e.removeAttribute("tabindex"),i.focus()},280)}function R(){E?z():S()}L();let j=null;const I=5,P=120,M=document.createElement("div");function B(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;j={y:n,h:E?C:0,moved:!1},M.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function T(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>I&&(j.moved=!0),!j.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,j.h+t));o<P?(r.style.transition="none",r.style.height="32px"):(C=o,L(),E||S(),r.style.transition="none",r.style.height=C+"px"),F()}function O(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,o=j.y-t,a=j.h+o;j=null,M.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(a<P?z():(C=Math.max(P,Math.min(window.innerHeight-50,a)),L(),E||S()),F())}Object.assign(M.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(M),w.addEventListener("click",e=>{e.stopPropagation(),R()}),c.addEventListener("mousedown",B,!0),document.addEventListener("mousemove",T),document.addEventListener("mouseup",O),c.addEventListener("touchstart",B,{passive:!1}),document.addEventListener("touchmove",T,{passive:!0}),document.addEventListener("touchend",O);let A=0,W=0;function D(e){_.textContent=e,_.style.display="block",S()}function U(){_.textContent="",_.style.display="none"}function N(){if(!K._p||"fit"!==K._mode)return;const e=K._w,n=K._h;if(!e||!n)return;const t=i.clientWidth,o=i.clientHeight,a=Math.min(t/e,o/n);u.style.transform=`scale(${a})`}function F(){if("fullscreen"===K._mode?K.size("max"):N(),Y&&"function"==typeof Y.windowResized)try{Y.windowResized()}catch(e){D(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{A=e.clientX,W=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(A=e.touches[0].clientX,W=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=K._p?K._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=K._w/n.width,o=K._h/n.height;return[(A-n.left)*t,(W-n.top)*o]},window.addEventListener("resize",F);let Y=null;const K=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const o=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=i.clientWidth,this._h=i.clientHeight,void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),u.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),N())},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){k.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 H(){if(ve(),Y){try{Y.remove()}catch(e){}Y=null}p.innerHTML="",K._p=null,K._mode="fit",K._w=0,K._h=0,u.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",te&&(te.destroy(),te=null),ee&&(ee.destroy(),ee=null),ne&&(ne.destroy(),ne=null),oe&&(oe.destroy(),oe=null),ae&&(ae.destroy(),ae=null),se&&(se.destroy(),se=null),ie&&(ie.destroy(),ie=null),re&&(re.destroy(),re=null),le&&(le.destroy(),le=null),de&&(de.destroy(),de=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null),ue&&(ue.destroy(),ue=null),fe&&(fe.destroy(),fe=null)}window.p5py=K;let V=null;function X(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.session.setMode("ace/mode/python"),V.setTheme("ace/theme/monokai"),V.setValue(n,-1),v.classList.toggle("pf-dirty",n!==t),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{V.completer?.popup?.isOpen||me()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:z}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:J}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me())}});let e=null;V.session.on("change",()=>{clearTimeout(e),e=setTimeout(J,350),v.classList.toggle("pf-dirty",V.getValue()!==t)})}function J(){try{localStorage.setItem(o,V?V.getValue():n)}catch(e){}}window.addEventListener("beforeunload",J);let $=null,G=null;async function q(){return G||(G=(async()=>{const e={};if(a.pyodideIndex&&(e.indexURL=a.pyodideIndex),$=await loadPyodide(e),$.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\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"),V){Z($.runPython("list(m.__all__)").toJs())}})(),G)}function Z(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,o,a,s){s(null,a.length>0?n:[])}},o=ace.require("ace/ext/language_tools");o&&Array.isArray(o.completers)&&(o.completers=o.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,V.completers=[...V.completers||[],t]}let Q=!1,ee=null,ne=null,te=null,oe=null,ae=null,se=null,ie=null,re=null,le=null,de=null,ce=null,pe=null,ue=null,fe=null;async function me(){if(Q)return;Q=!0,h.classList.add("pf-running"),U(),H(),$||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await q()}catch(e){return f.style.display="none",D("Erreur Pyodide : "+e),Q=!1,void h.classList.remove("pf-running")}f.style.display="none";const t=V?V.getValue():n;try{m.textContent="Chargement des dépendances…",f.style.display="flex",await $.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",$.globals.set("_USER_CODE",t);try{$.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),$.runPython("_ns_ref[0] = _ns")}catch(e){return D(String(e)),Q=!1,void h.classList.remove("pf-running")}let o,a,s,i,r,l,d,c,u,_,y,g,b,v;try{const e=(e,n)=>$.runPython(`_ns.get('${e}') or _ns.get('${n}')`);r=e("preload","preload"),o=e("setup","setup"),a=e("draw","draw"),s=e("mousePressed","mouse_pressed"),i=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),d=e("mouseReleased","mouse_released"),c=e("mouseMoved","mouse_moved"),u=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),y=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),b=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return D(String(e)),Q=!1,void h.classList.remove("pf-running")}if(!a)return D("Le script doit définir au moins une fonction draw()."),Q=!1,void h.classList.remove("pf-running");const{create_proxy:x}=$.pyimport("pyodide.ffi"),w=$.runPython("_ns.get('windowResized')"),k=$.globals.get("_pf_refresh"),E=$.globals.get("_ns"),C=e=>e?x(()=>{try{k(E),e()}catch(e){D(String(e))}}):null;te=r?x(()=>{try{r()}catch(e){D(String(e))}}):null,ee=o?x(()=>{try{o()}catch(e){D(String(e))}}):null;const L=200;ne=x(()=>{try{k(E);const e=performance.now();a(),performance.now()-e>L&&(H(),D(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){D(String(e)),H()}}),oe=C(s),ae=C(d),se=C(l),ie=C(c),re=C(u),le=C(_),de=C(i),ce=C(y),pe=C(g),ue=C(b),fe=C(v);const S=w?x(()=>{try{w()}catch(e){D(String(e))}}):null;let z=!1;Y=new p5(e=>{K._setP(e),te&&(e.preload=()=>{te()}),e.setup=()=>{ee&&ee(),e.canvas||K.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&ne()},e.mousePressed=()=>{z&&oe&&oe()},e.mouseReleased=()=>{z&&ae&&ae()},e.mouseDragged=()=>{z&&se&&se()},e.mouseMoved=()=>{z&&ie&&ie()},e.mouseWheel=e=>{z&&re&&re()},e.doubleClicked=()=>{z&&le&&le()},e.keyPressed=()=>{z&&de&&de()},e.keyReleased=()=>{z&&ce&&ce()},e.touchStarted=()=>{z&&pe&&pe()},e.touchMoved=()=>{z&&ue&&ue()},e.touchEnded=()=>{z&&fe&&fe()},e.windowResized=()=>{"fullscreen"===K._mode?K.size("max"):N(),S&&S()}},p),Q=!1,h.classList.remove("pf-running")}const _e='<!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.4.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function he(){const e=V?V.getValue():n,t=_e.replace("FILLME-PYTHON",e),o=new Blob([t],{type:"text/html;charset=utf-8"}),a=URL.createObjectURL(o),s=Object.assign(document.createElement("a"),{href:a,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(a)}h.addEventListener("click",()=>me()),y.addEventListener("click",()=>{E?z():(C=window.innerHeight-32,L(),S())});let ye=null,ge=[];function be(){const e=K._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();ye=new MediaRecorder(t,{mimeType:n}),ge=[],ye.ondataavailable=e=>{e.data.size&&ge.push(e.data)},ye.onstop=()=>{const e=new Blob(ge,{type:n}),t=URL.createObjectURL(e),o=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${o}`}).click(),URL.revokeObjectURL(t),b.textContent="⏺",b.title="Enregistrer WebM",b.classList.remove("pf-recording"),ye=null},ye.start(),b.textContent="⏹",b.title="Arrêter l'enregistrement",b.classList.add("pf-recording")}function ve(){ye&&"inactive"!==ye.state&&ye.stop()}b.addEventListener("click",()=>{ye?ve():be()}),g.addEventListener("click",he);const xe="https://codeberg.org/nopid/pyfrilet";function we(e){return new Promise((n,t)=>{const o=document.createElement("script");o.src=e,o.onload=n,o.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(o)})}x.addEventListener("click",()=>window.open(xe,"_blank")),v.addEventListener("click",()=>{V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me())}),window.addEventListener("keydown",e=>{const n=E&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void me();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),V.searchBox?V.searchBox.hide():t.style.display="none",void V.focus();if(n){const n=V.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void z()}return e.preventDefault(),e.stopPropagation(),void(E?z():S())}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(V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me()))):(e.preventDefault(),void J())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await we(a.p5),await we(a.ace),await we(a.acePython),await we(a.aceMonokai),await we(a.aceLangTools),await we(a.aceSearchbox),await we(a.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}X(),await me(),f.style.display="none"})()}(h&&h.trim()?h:m,m,_,f)})}();
|
|
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",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",r="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",u="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",m="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",f="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#pf-markdown-view {\n flex: 1;\n overflow: auto;\n padding: 14px 18px;\n background: #1a1b2e;\n color: #c0caf5;\n font-size: 14px;\n line-height: 1.6;\n}\n#pf-markdown-view h1,#pf-markdown-view h2,#pf-markdown-view h3 { color: #7aa2f7; }\n#pf-markdown-view code { background: #24283b; padding: 1px 5px; border-radius: 3px; font-size: 13px; }\n#pf-markdown-view pre code { display: block; padding: 10px; overflow: auto; }\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=_[0],b=e||y,g=(b.getAttribute("data-sources")||b.getAttribute("sources")||"cdn").toLowerCase().trim(),v=(b.getAttribute("data-vendor")||b.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const x=_.some(e=>"text/markdown"===e.getAttribute("type")),k=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:i,aceMonokai:r,aceLangTools:s,aceSearchbox:d,marked:x?l:null,katexCss:x?c:null,katex:x?p:null,markedKatex:x?u:null,mermaid:x?m:null}:{p5:v+"p5.min.js",pyodide:v+"pyodide/pyodide.js",pyodideIndex:v+"pyodide/",ace:v+"ace.min.js",acePython:v+"mode-python.min.js",aceMonokai:v+"theme-monokai.min.js",aceLangTools:v+"ext-language_tools.min.js",aceSearchbox:v+"ext-searchbox.min.js",marked:x?v+"marked.min.js":null,katexCss:x?v+"katex.min.css":null,katex:x?v+"katex.min.js":null,markedKatex:x?v+"marked-katex-extension.js":null,mermaid:x?v+"mermaid.min.js":null},w="pyfrilet:"+location.pathname;!function(e,t,a){const o=document.createElement("style");o.textContent=f,document.head.appendChild(o),document.body.innerHTML=h;const i=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),s=document.getElementById("pf-handle"),d=document.getElementById("pf-sketch"),l=document.getElementById("pf-viewport"),c=document.getElementById("pf-loader"),p=document.getElementById("pf-loader-msg"),u=document.getElementById("pf-err"),m=document.getElementById("pf-btn-run"),_=document.getElementById("pf-btn-code"),y=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-rec"),g=document.getElementById("pf-btn-reset"),v=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint"),w=document.getElementById("pf-tabs"),E=document.getElementById("pf-markdown-view");let C=!1,S=Math.round(.56*window.innerHeight);function L(){document.documentElement.style.setProperty("--pf-drawer-h",S+"px")}function j(){C=!0,r.classList.add("pf-open"),_.classList.add("pf-active"),setTimeout(()=>{F(),V&&V.focus()},280)}function z(){C=!1,r.classList.remove("pf-open"),_.classList.remove("pf-active"),setTimeout(()=>{F();const e=H._p?.canvas;e&&e.removeAttribute("tabindex"),i.focus()},280)}function R(){C?z():j()}L();let I=null;const P=5,M=120,B=document.createElement("div");function T(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;I={y:n,h:C?S:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function A(e){if(!I)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=I.y-n;if(Math.abs(t)>P&&(I.moved=!0),!I.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,I.h+t));a<M?(r.style.transition="none",r.style.height="32px"):(S=a,L(),C||j(),r.style.transition="none",r.style.height=S+"px"),F()}function O(e){if(!I)return;const n=I.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??I.y,a=I.y-t,o=I.h+a;I=null,B.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(o<M?z():(S=Math.max(M,Math.min(window.innerHeight-50,o)),L(),C||j()),F())}Object.assign(B.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(B),x.addEventListener("click",e=>{e.stopPropagation(),R()}),s.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",A),document.addEventListener("mouseup",O),s.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",A,{passive:!0}),document.addEventListener("touchend",O);let W=0,D=0;function K(e){u.textContent=e,u.style.display="block",j()}function U(){u.textContent="",u.style.display="none"}function $(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=i.clientWidth,a=i.clientHeight,o=Math.min(t/e,a/n);l.style.transform=`scale(${o})`}function F(){if("fullscreen"===H._mode?H.size("max"):$(),N&&"function"==typeof N.windowResized)try{N.windowResized()}catch(e){K(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{W=e.clientX,D=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(W=e.touches[0].clientX,D=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=H._p?H._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=H._w/n.width,a=H._h/n.height;return[(W-n.left)*t,(D-n.top)*a]},window.addEventListener("resize",F);let N=null;const H=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=i.clientWidth,this._h=i.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),l.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){k.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 Y(){if(Se(),N){try{N.remove()}catch(e){}N=null}d.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,l.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",de&&(de.destroy(),de=null),re&&(re.destroy(),re=null),se&&(se.destroy(),se=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null),ue&&(ue.destroy(),ue=null),me&&(me.destroy(),me=null),fe&&(fe.destroy(),fe=null),he&&(he.destroy(),he=null),_e&&(_e.destroy(),_e=null),ye&&(ye.destroy(),ye=null),be&&(be.destroy(),be=null),ge&&(ge.destroy(),ge=null)}window.p5py=H;let V=null,X=null;const J={};function q(){const n=e.filter(e=>!e.hidden);n.length<=1&&(w.style.display="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",()=>G(e)),w.appendChild(n)}),n.length>0&&G(n[0],!0)}function G(e,n){if(n||X!==e)if(X=e,w.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",E.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>`)),E.innerHTML=n}else E.innerHTML=`<pre>${e.starterCode}</pre>`;window.mermaid&&mermaid.run({nodes:E.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",E.style.display="none",V&&J[e.id]&&(V.setSession(J[e.id]),V.setReadOnly(e.readonly),V.focus())}function Z(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.setTheme("ace/theme/monokai"),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),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),J[e.id]=n,!e.readonly){let t=null;n.on("change",()=>{clearTimeout(t),t=setTimeout(()=>Q(e),350),X===e&&g.classList.toggle("pf-dirty",n.getValue()!==e.starterCode)})}}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{V.completer?.popup?.isOpen||ve()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:z}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:ee}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{X&&!X.readonly&&"python"===X.type&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve())}});const n=e.find(e=>!e.hidden&&"python"===e.type);n&&J[n.id]&&(V.setSession(J[n.id]),V.setReadOnly(n.readonly)),q()}function Q(e){if(e&&!e.readonly&&"python"===e.type&&J[e.id])try{localStorage.setItem(e.sk,J[e.id].getValue())}catch(e){}}function ee(){e.forEach(e=>Q(e))}window.addEventListener("beforeunload",ee);let ne=null,te=null;async function ae(){return te||(te=(async()=>{const e={};if(a.pyodideIndex&&(e.indexURL=a.pyodideIndex),ne=await loadPyodide(e),ne.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\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"),V){oe(ne.runPython("list(m.__all__)").toJs())}})(),te)}function oe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,i){i(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,V.completers=[...V.completers||[],t]}let ie=!1,re=null,se=null,de=null,le=null,ce=null,pe=null,ue=null,me=null,fe=null,he=null,_e=null,ye=null,be=null,ge=null;async function ve(){if(ie)return;ie=!0,m.classList.add("pf-running"),U(),Y(),ne||(p.textContent="Initialisation de Pyodide…",c.style.display="flex");try{await ae()}catch(e){return c.style.display="none",K("Erreur Pyodide : "+e),ie=!1,void m.classList.remove("pf-running")}c.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!J[e.id]?e.code:J[e.id].getValue()).join("\n");try{p.textContent="Chargement des dépendances…",c.style.display="flex",await ne.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}c.style.display="none",ne.globals.set("_USER_CODE",t);try{ne.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ne.runPython("_ns_ref[0] = _ns")}catch(e){return K(String(e)),ie=!1,void m.classList.remove("pf-running")}let a,o,i,r,s,l,u,f,h,_,y,b,g,v;try{const e=(e,n)=>ne.runPython(`_ns.get('${e}') or _ns.get('${n}')`);s=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),r=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),u=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),y=e("keyReleased","key_released"),b=e("touchStarted","touch_started"),g=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return K(String(e)),ie=!1,void m.classList.remove("pf-running")}if(!o)return K("Le script doit définir au moins une fonction draw()."),ie=!1,void m.classList.remove("pf-running");const{create_proxy:x}=ne.pyimport("pyodide.ffi"),k=ne.runPython("_ns.get('windowResized')"),w=ne.globals.get("_pf_refresh"),E=ne.globals.get("_ns"),C=e=>e?x(()=>{try{w(E),e()}catch(e){K(String(e))}}):null;de=s?x(()=>{try{s()}catch(e){K(String(e))}}):null,re=a?x(()=>{try{a()}catch(e){K(String(e))}}):null;const S=200;se=x(()=>{try{w(E);const e=performance.now();o(),performance.now()-e>S&&(Y(),K(`draw() a mis plus de ${S} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){K(String(e)),Y()}}),le=C(i),ce=C(u),pe=C(l),ue=C(f),me=C(h),fe=C(_),he=C(r),_e=C(y),ye=C(b),be=C(g),ge=C(v);const L=k?x(()=>{try{k()}catch(e){K(String(e))}}):null;let j=!1;N=new p5(e=>{H._setP(e),de&&(e.preload=()=>{de()}),e.setup=()=>{re&&re(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),j=!0},e.draw=()=>{j&&se()},e.mousePressed=()=>{j&&le&&le()},e.mouseReleased=()=>{j&&ce&&ce()},e.mouseDragged=()=>{j&&pe&&pe()},e.mouseMoved=()=>{j&&ue&&ue()},e.mouseWheel=e=>{j&&me&&me()},e.doubleClicked=()=>{j&&fe&&fe()},e.keyPressed=()=>{j&&he&&he()},e.keyReleased=()=>{j&&_e&&_e()},ye&&(e.touchStarted=()=>{j&&ye()}),be&&(e.touchMoved=()=>{j&&be()}),ge&&(e.touchEnded=()=>{j&&ge()}),e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):$(),L&&L()}},d),ie=!1,m.classList.remove("pf-running")}const xe='<!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.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function ke(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!J[e.id]?e.code:J[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=xe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),i=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(o)}let we=null,Ee=[];function Ce(){const e=H._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();we=new MediaRecorder(t,{mimeType:n}),Ee=[],we.ondataavailable=e=>{e.data.size&&Ee.push(e.data)},we.onstop=()=>{const e=new Blob(Ee,{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),b.textContent="⏺",b.title="Enregistrer WebM",b.classList.remove("pf-recording"),we=null},we.start(),b.textContent="⏹",b.title="Arrêter l'enregistrement",b.classList.add("pf-recording")}function Se(){we&&"inactive"!==we.state&&we.stop()}b.addEventListener("click",()=>{we?Se():Ce()}),m.addEventListener("click",()=>ve()),_.addEventListener("click",()=>{C?z():(S=window.innerHeight-32,L(),j())}),y.addEventListener("click",ke);const Le="https://codeberg.org/nopid/pyfrilet";function je(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)})}v.addEventListener("click",()=>window.open(Le,"_blank")),g.addEventListener("click",()=>{X&&!X.readonly&&"python"===X.type&&J[X.id]&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve())}),window.addEventListener("keydown",e=>{const n=C&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void ve();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),V.searchBox?V.searchBox.hide():t.style.display="none",void V.focus();if(n){const n=V.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void z()}return e.preventDefault(),e.stopPropagation(),void(C?z():j())}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(X&&!X.readonly&&"python"===X.type&&J[X.id]&&confirm("Réinitialiser cet onglet ? Les modifications seront perdues.")&&(J[X.id].setValue(X.starterCode,-1),ve()))):(e.preventDefault(),void ee())}else e.preventDefault()},!0),(async()=>{p.textContent="Chargement des dépendances…",c.style.display="flex";try{if(await je(a.p5),a.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=a.katexCss,document.head.appendChild(e),await je(a.marked),await je(a.katex),await je(a.markedKatex),await je(a.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"dark"})}await je(a.ace),await je(a.acePython),await je(a.aceMonokai),await je(a.aceLangTools),await je(a.aceSearchbox),await je(a.pyodide)}catch(e){return p.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}Z(),await ve(),c.style.display="none"})()}(_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let i=e.getAttribute("data-tab");null!==i||a||(i=1===_.length?"Code":`Bloc ${n+1}`);const r=e.textContent.replace(/^\n/,""),s=w+":"+n;let d=r;if("python"===t&&!a&&!o){const e=(()=>{try{return localStorage.getItem(s)}catch(e){return null}})();e&&e.trim()&&(d=e)}return{id:"tab-"+n,label:i,hidden:a,readonly:o,type:t,starterCode:r,code:d,sk:s}}),0,k)})}();
|