pyfrilet 0.4.3 → 0.5.1
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 +523 -77
- 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,162 @@ 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;
|
|
239
|
+
}
|
|
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
|
+
@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');
|
|
247
|
+
|
|
248
|
+
#pf-markdown-view {
|
|
249
|
+
flex: 1;
|
|
250
|
+
overflow: auto;
|
|
251
|
+
background: #f4f4f0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#pf-markdown-view .pf-md-inner {
|
|
255
|
+
width: 100%;
|
|
256
|
+
max-width: 680px;
|
|
257
|
+
margin: 0 auto;
|
|
258
|
+
padding: 48px 48px 72px;
|
|
259
|
+
box-sizing: border-box;
|
|
260
|
+
font-family: 'Alegreya Sans', Georgia, serif;
|
|
261
|
+
font-size: 17px;
|
|
262
|
+
line-height: 1.8;
|
|
263
|
+
color: #1c1c2e;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#pf-markdown-view h1 {
|
|
267
|
+
font-size: 2.1em;
|
|
268
|
+
font-weight: 700;
|
|
269
|
+
color: #1c1c2e;
|
|
270
|
+
margin: 0 0 .3em;
|
|
271
|
+
padding-bottom: .3em;
|
|
272
|
+
border-bottom: 2px solid #d8d8e8;
|
|
273
|
+
line-height: 1.2;
|
|
274
|
+
}
|
|
275
|
+
#pf-markdown-view h2 {
|
|
276
|
+
font-size: 1.4em;
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
color: #1c1c2e;
|
|
279
|
+
margin: 2em 0 .5em;
|
|
280
|
+
padding-bottom: .2em;
|
|
281
|
+
border-bottom: 1px solid #e0e0ec;
|
|
282
|
+
}
|
|
283
|
+
#pf-markdown-view h3 {
|
|
284
|
+
font-size: 1.1em;
|
|
285
|
+
font-weight: 700;
|
|
286
|
+
color: #2a2a4a;
|
|
287
|
+
margin: 1.6em 0 .4em;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#pf-markdown-view p { margin: .75em 0; }
|
|
291
|
+
#pf-markdown-view ul,
|
|
292
|
+
#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }
|
|
293
|
+
#pf-markdown-view li { margin: .3em 0; }
|
|
294
|
+
#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }
|
|
295
|
+
#pf-markdown-view blockquote {
|
|
296
|
+
margin: 1em 0;
|
|
297
|
+
padding: .5em 1em;
|
|
298
|
+
border-left: 3px solid #aab;
|
|
299
|
+
color: #555;
|
|
300
|
+
background: #ededf5;
|
|
301
|
+
border-radius: 0 4px 4px 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#pf-markdown-view code {
|
|
305
|
+
font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;
|
|
306
|
+
font-size: .84em;
|
|
307
|
+
background: #e8e8f2;
|
|
308
|
+
color: #3a3a6a;
|
|
309
|
+
padding: .15em .45em;
|
|
310
|
+
border-radius: 4px;
|
|
311
|
+
}
|
|
312
|
+
#pf-markdown-view pre {
|
|
313
|
+
background: #1a1b2e;
|
|
314
|
+
border-radius: 8px;
|
|
315
|
+
padding: 1em 1.2em;
|
|
316
|
+
overflow: auto;
|
|
317
|
+
margin: 1.2em 0;
|
|
318
|
+
box-shadow: 0 2px 8px rgba(0,0,0,.12);
|
|
319
|
+
}
|
|
320
|
+
#pf-markdown-view pre code {
|
|
321
|
+
background: transparent;
|
|
322
|
+
color: #c0caf5;
|
|
323
|
+
font-size: .86em;
|
|
324
|
+
padding: 0;
|
|
325
|
+
line-height: 1.6;
|
|
326
|
+
border-radius: 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#pf-markdown-view table {
|
|
330
|
+
border-collapse: collapse;
|
|
331
|
+
width: 100%;
|
|
332
|
+
margin: 1.2em 0;
|
|
333
|
+
font-size: .95em;
|
|
334
|
+
}
|
|
335
|
+
#pf-markdown-view th {
|
|
336
|
+
background: #e4e4f0;
|
|
337
|
+
color: #1c1c2e;
|
|
338
|
+
font-weight: 700;
|
|
339
|
+
text-align: left;
|
|
340
|
+
padding: .55em .85em;
|
|
341
|
+
border: 1px solid #d0d0e8;
|
|
342
|
+
}
|
|
343
|
+
#pf-markdown-view td {
|
|
344
|
+
padding: .5em .85em;
|
|
345
|
+
border: 1px solid #e0e0ee;
|
|
346
|
+
vertical-align: top;
|
|
347
|
+
}
|
|
348
|
+
#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }
|
|
349
|
+
|
|
350
|
+
#pf-markdown-view a {
|
|
351
|
+
color: #3a5fc8;
|
|
352
|
+
text-decoration: none;
|
|
353
|
+
border-bottom: 1px solid rgba(58,95,200,.3);
|
|
354
|
+
transition: color .15s, border-color .15s;
|
|
355
|
+
}
|
|
356
|
+
#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }
|
|
357
|
+
|
|
358
|
+
#pf-markdown-view .katex-display {
|
|
359
|
+
overflow-x: auto;
|
|
360
|
+
padding: .5em 0;
|
|
361
|
+
margin: 1.2em 0;
|
|
362
|
+
}
|
|
363
|
+
#pf-markdown-view .mermaid {
|
|
364
|
+
text-align: center;
|
|
365
|
+
margin: 1.5em 0;
|
|
366
|
+
background: #ededf5;
|
|
367
|
+
border-radius: 8px;
|
|
368
|
+
padding: 1em;
|
|
207
369
|
}
|
|
208
|
-
#pf-ace { position: absolute; inset: 0; }
|
|
209
370
|
|
|
210
371
|
/* ── error panel (below editor, never overlaps ACE) ── */
|
|
211
372
|
#pf-err {
|
|
@@ -219,12 +380,10 @@ html, body {
|
|
|
219
380
|
white-space: pre-wrap;
|
|
220
381
|
display: none;
|
|
221
382
|
border-top: 1px solid rgba(247, 118, 142, .35);
|
|
222
|
-
}
|
|
223
|
-
`;
|
|
383
|
+
}`;
|
|
224
384
|
|
|
225
385
|
/* ═══════════════════════════ MARKUP ═════════════════════════════════ */
|
|
226
|
-
const MARKUP =
|
|
227
|
-
<div id="pf-root">
|
|
386
|
+
const MARKUP = `<div id="pf-root">
|
|
228
387
|
<div id="pf-app" tabindex="-1">
|
|
229
388
|
<div id="pf-viewport"><div id="pf-sketch"></div></div>
|
|
230
389
|
<div id="pf-loader">
|
|
@@ -246,37 +405,48 @@ const MARKUP = `
|
|
|
246
405
|
</div>
|
|
247
406
|
</div>
|
|
248
407
|
<div id="pf-editor-wrap">
|
|
408
|
+
<div id="pf-tabs"></div>
|
|
409
|
+
<div id="pf-markdown-view" style="display:none"></div>
|
|
249
410
|
<div id="pf-ace"></div>
|
|
250
411
|
</div>
|
|
251
412
|
<pre id="pf-err"></pre>
|
|
252
413
|
</div>
|
|
253
|
-
</div
|
|
254
|
-
`;
|
|
414
|
+
</div>`;
|
|
255
415
|
|
|
256
416
|
/* ═══════════════════════════ ENTRY POINT ════════════════════════════ */
|
|
257
417
|
document.addEventListener('DOMContentLoaded', function () {
|
|
258
418
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
419
|
+
/* Collect all python/markdown script blocks in DOM order */
|
|
420
|
+
const allScripts = [
|
|
421
|
+
...document.querySelectorAll(
|
|
422
|
+
'script[type="text/python"], script[type="text/markdown"], python'
|
|
423
|
+
)
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
if (allScripts.length === 0) {
|
|
262
427
|
console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');
|
|
263
428
|
return;
|
|
264
429
|
}
|
|
265
430
|
|
|
431
|
+
/* Config tag: prefer the <script src="pyfrilet.js"> itself, fallback to first python tag */
|
|
432
|
+
const configTag = _pfScriptTag || allScripts[0];
|
|
433
|
+
|
|
266
434
|
const sources = (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
'
|
|
435
|
+
configTag.getAttribute('data-sources') ||
|
|
436
|
+
configTag.getAttribute('sources') ||
|
|
437
|
+
'cdn'
|
|
270
438
|
).toLowerCase().trim();
|
|
271
439
|
|
|
272
440
|
const vpRaw = (
|
|
273
|
-
|
|
274
|
-
|
|
441
|
+
configTag.getAttribute('data-vendor') ||
|
|
442
|
+
configTag.getAttribute('vendor') ||
|
|
275
443
|
'vendor/'
|
|
276
444
|
);
|
|
277
|
-
const vp = vpRaw.replace(/\/?$/, '/');
|
|
445
|
+
const vp = vpRaw.replace(/\/?$/, '/');
|
|
278
446
|
|
|
279
447
|
isCdn = sources === 'cdn';
|
|
448
|
+
const hasMarked = allScripts.some(el => el.getAttribute('type') === 'text/markdown');
|
|
449
|
+
|
|
280
450
|
const URLS = isCdn ? {
|
|
281
451
|
p5 : CDN.p5,
|
|
282
452
|
pyodide : CDN.pyodide,
|
|
@@ -286,6 +456,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
286
456
|
aceMonokai : CDN.aceMonokai,
|
|
287
457
|
aceLangTools: CDN.aceLangTools,
|
|
288
458
|
aceSearchbox: CDN.aceSearchbox,
|
|
459
|
+
marked : hasMarked ? CDN.marked : null,
|
|
460
|
+
katexCss : hasMarked ? CDN.katexCss : null,
|
|
461
|
+
katex : hasMarked ? CDN.katex : null,
|
|
462
|
+
markedKatex : hasMarked ? CDN.markedKatex : null,
|
|
463
|
+
mermaid : hasMarked ? CDN.mermaid : null,
|
|
289
464
|
} : {
|
|
290
465
|
p5 : vp + 'p5.min.js',
|
|
291
466
|
pyodide : vp + 'pyodide/pyodide.js',
|
|
@@ -295,20 +470,87 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
295
470
|
aceMonokai : vp + 'theme-monokai.min.js',
|
|
296
471
|
aceLangTools: vp + 'ext-language_tools.min.js',
|
|
297
472
|
aceSearchbox: vp + 'ext-searchbox.min.js',
|
|
473
|
+
marked : hasMarked ? vp + 'marked.min.js' : null,
|
|
474
|
+
katexCss : hasMarked ? vp + 'katex.min.css' : null,
|
|
475
|
+
katex : hasMarked ? vp + 'katex.min.js' : null,
|
|
476
|
+
markedKatex : hasMarked ? vp + 'marked-katex-extension.js' : null,
|
|
477
|
+
mermaid : hasMarked ? vp + 'mermaid.min.js' : null,
|
|
298
478
|
};
|
|
299
479
|
|
|
300
|
-
|
|
301
|
-
|
|
480
|
+
const SK = 'pyfrilet:' + location.pathname;
|
|
481
|
+
|
|
482
|
+
/* ── Parse HTML as ground truth ─────────────────────────────────────
|
|
483
|
+
htmlTabs = what the current file says, used for:
|
|
484
|
+
- starterCode (reset target)
|
|
485
|
+
- fallback when no snapshot exists
|
|
486
|
+
──────────────────────────────────────────────────────────────────── */
|
|
487
|
+
const htmlTabs = allScripts.map((el, i) => {
|
|
488
|
+
const type = el.getAttribute('type') === 'text/markdown' ? 'markdown' : 'python';
|
|
489
|
+
const hidden = el.hasAttribute('data-hidden');
|
|
490
|
+
const readonly = el.hasAttribute('data-readonly');
|
|
491
|
+
let label = el.getAttribute('data-tab');
|
|
492
|
+
if (label === null && !hidden) label = allScripts.length === 1 ? 'Code' : `Bloc ${i + 1}`;
|
|
493
|
+
const rawCode = el.textContent.replace(/^\n/, '');
|
|
494
|
+
return { id: 'tab-' + i, label, hidden, readonly, type, starterCode: rawCode, code: rawCode };
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
/* ── Try to restore full snapshot from localStorage ─────────────────
|
|
498
|
+
Snapshot format (v1): { v:1, tabs:[{ label, hidden, readonly, type, content }, ...] }
|
|
499
|
+
|
|
500
|
+
If a valid snapshot exists → use it as the working tabs (same structure
|
|
501
|
+
as last visit, including number of tabs and all content).
|
|
502
|
+
The starterCode of each tab is taken from the matching htmlTab (by label+type)
|
|
503
|
+
so that Reset always targets the current file, not the stored version.
|
|
302
504
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
505
|
+
If no snapshot → use htmlTabs, with retro-compat fallback on old per-tab keys.
|
|
506
|
+
──────────────────────────────────────────────────────────────────── */
|
|
507
|
+
const tryLS = (key) => { try { return localStorage.getItem(key); } catch (e) { return null; } };
|
|
508
|
+
|
|
509
|
+
let tabs;
|
|
510
|
+
const raw = tryLS(SK);
|
|
511
|
+
let snap = null;
|
|
512
|
+
if (raw) {
|
|
513
|
+
try { snap = JSON.parse(raw); } catch (e) { snap = null; }
|
|
514
|
+
}
|
|
306
515
|
|
|
307
|
-
|
|
516
|
+
if (snap && snap.v === 1 && Array.isArray(snap.tabs) && snap.tabs.length > 0) {
|
|
517
|
+
/* Restore structure and content from snapshot */
|
|
518
|
+
tabs = snap.tabs.map((st, i) => {
|
|
519
|
+
/* Find current htmlTab with same label+type to get up-to-date starterCode */
|
|
520
|
+
const html = htmlTabs.find(h => h.label === st.label && h.type === st.type) || null;
|
|
521
|
+
return {
|
|
522
|
+
id : 'tab-' + i,
|
|
523
|
+
label : st.label,
|
|
524
|
+
hidden : st.hidden,
|
|
525
|
+
readonly : st.readonly,
|
|
526
|
+
type : st.type,
|
|
527
|
+
starterCode : html ? html.starterCode : st.content, /* reset target = current file */
|
|
528
|
+
code : st.content, /* working content = last visit */
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
} else {
|
|
532
|
+
/* No snapshot: use htmlTabs with retro-compat for old per-tab keys */
|
|
533
|
+
tabs = htmlTabs.map((tab, i) => {
|
|
534
|
+
if (!tab.hidden && !tab.readonly && tab.type === 'python') {
|
|
535
|
+
const safe = tab.label ? tab.label.replace(/[^a-zA-Z0-9]/g, '_') : String(i);
|
|
536
|
+
let saved = tryLS(SK + ':' + safe);
|
|
537
|
+
/* Retro-compat: single unnamed tab used bare SK as key */
|
|
538
|
+
if (!saved && tab.label === 'Code' && htmlTabs.length === 1) saved = tryLS(SK);
|
|
539
|
+
if (saved && saved.trim()) return { ...tab, code: saved };
|
|
540
|
+
}
|
|
541
|
+
return tab;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
main(tabs, htmlTabs, SK, URLS);
|
|
308
546
|
});
|
|
309
547
|
|
|
310
548
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
311
|
-
function main(
|
|
549
|
+
function main(tabs, htmlTabs, SK, URLS) {
|
|
550
|
+
|
|
551
|
+
/* tabs = working state (from snapshot or HTML), may be reassigned on reset
|
|
552
|
+
htmlTabs = ground truth from current HTML file, never mutated */
|
|
553
|
+
tabs = tabs.slice(); /* local mutable copy */
|
|
312
554
|
|
|
313
555
|
/* ── inject styles + markup ── */
|
|
314
556
|
const styleEl = document.createElement('style');
|
|
@@ -333,6 +575,8 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
333
575
|
const btnHelp = document.getElementById('pf-btn-help');
|
|
334
576
|
const gripEl = document.getElementById('pf-grip');
|
|
335
577
|
const hintEl = document.getElementById('pf-handle-hint');
|
|
578
|
+
const tabsEl = document.getElementById('pf-tabs');
|
|
579
|
+
const markdownEl = document.getElementById('pf-markdown-view');
|
|
336
580
|
|
|
337
581
|
/* ─────────────────── DRAWER ─────────────────── */
|
|
338
582
|
let drawerOpen = false;
|
|
@@ -475,6 +719,7 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
475
719
|
(_rawMouseY - r.top) * sy,
|
|
476
720
|
];
|
|
477
721
|
};
|
|
722
|
+
|
|
478
723
|
function showError(txt) { errEl.textContent = txt; errEl.style.display = 'block'; openDrawer(); }
|
|
479
724
|
function clearError() { errEl.textContent = ''; errEl.style.display = 'none'; }
|
|
480
725
|
|
|
@@ -605,20 +850,134 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
605
850
|
if (touchEndedProxy) { touchEndedProxy.destroy(); touchEndedProxy = null; }
|
|
606
851
|
}
|
|
607
852
|
|
|
608
|
-
/* ─────────────────── ACE EDITOR ─────────────── */
|
|
609
|
-
let aceInst
|
|
853
|
+
/* ─────────────────── ACE EDITOR + TABS ─────────────── */
|
|
854
|
+
let aceInst = null;
|
|
855
|
+
let activeTab = null;
|
|
856
|
+
|
|
857
|
+
/* Map tab.id → ACE EditSession */
|
|
858
|
+
const aceSessions = {};
|
|
859
|
+
|
|
860
|
+
/* All pending save debounce timers — cancelled before any reset */
|
|
861
|
+
const saveTimers = new Set();
|
|
862
|
+
|
|
863
|
+
/* ── Tab bar ──────────────────────────────────────────── */
|
|
864
|
+
function buildTabBar() {
|
|
865
|
+
/* Clear existing buttons */
|
|
866
|
+
tabsEl.innerHTML = '';
|
|
867
|
+
activeTab = null;
|
|
868
|
+
|
|
869
|
+
const visibleTabs = tabs.filter(t => !t.hidden);
|
|
870
|
+
tabsEl.style.display = visibleTabs.length <= 1 ? 'none' : '';
|
|
871
|
+
|
|
872
|
+
visibleTabs.forEach(tab => {
|
|
873
|
+
const btn = document.createElement('button');
|
|
874
|
+
btn.className = 'pf-tab';
|
|
875
|
+
btn.dataset.tabId = tab.id;
|
|
876
|
+
btn.textContent = tab.label;
|
|
877
|
+
if (tab.readonly) btn.classList.add('pf-tab-readonly');
|
|
878
|
+
if (tab.type === 'markdown') btn.classList.add('pf-tab-markdown');
|
|
879
|
+
btn.addEventListener('click', () => switchTab(tab));
|
|
880
|
+
tabsEl.appendChild(btn);
|
|
881
|
+
});
|
|
610
882
|
|
|
883
|
+
if (visibleTabs.length > 0) switchTab(visibleTabs[0], true);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function switchTab(tab, init) {
|
|
887
|
+
if (!init && activeTab === tab) return;
|
|
888
|
+
activeTab = tab;
|
|
889
|
+
|
|
890
|
+
tabsEl.querySelectorAll('.pf-tab').forEach(btn => {
|
|
891
|
+
btn.classList.toggle('pf-tab-active', btn.dataset.tabId === tab.id);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
if (tab.type === 'markdown') {
|
|
895
|
+
document.getElementById('pf-ace').style.display = 'none';
|
|
896
|
+
markdownEl.style.display = 'block';
|
|
897
|
+
if (window.marked) {
|
|
898
|
+
let html = marked.parse(tab.starterCode);
|
|
899
|
+
if (window.mermaid) {
|
|
900
|
+
html = html.replace(
|
|
901
|
+
/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,
|
|
902
|
+
(_, code) => `<div class="mermaid">${
|
|
903
|
+
code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
904
|
+
}</div>`
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
markdownEl.innerHTML = `<div class="pf-md-inner">${html}</div>`;
|
|
908
|
+
} else {
|
|
909
|
+
markdownEl.innerHTML = `<div class="pf-md-inner"><pre>${tab.starterCode}</pre></div>`;
|
|
910
|
+
}
|
|
911
|
+
if (window.mermaid) mermaid.run({ nodes: markdownEl.querySelectorAll('.mermaid') });
|
|
912
|
+
} else {
|
|
913
|
+
document.getElementById('pf-ace').style.display = 'block';
|
|
914
|
+
markdownEl.style.display = 'none';
|
|
915
|
+
if (aceInst && aceSessions[tab.id]) {
|
|
916
|
+
aceInst.setSession(aceSessions[tab.id]);
|
|
917
|
+
aceInst.setReadOnly(tab.readonly);
|
|
918
|
+
aceInst.focus();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* ── Line number offsets ──────────────────────────────── */
|
|
924
|
+
/* Each visible tab's firstLineNumber is set so ACE line numbers match
|
|
925
|
+
Python traceback line numbers (all python tabs concatenated with join('\n')). */
|
|
926
|
+
function updateLineOffsets() {
|
|
927
|
+
let offset = 1;
|
|
928
|
+
tabs.filter(t => t.type === 'python').forEach(tab => {
|
|
929
|
+
if (!tab.hidden && !tab.readonly && aceSessions[tab.id]) {
|
|
930
|
+
aceSessions[tab.id].setOption('firstLineNumber', offset);
|
|
931
|
+
offset += aceSessions[tab.id].getLength();
|
|
932
|
+
} else {
|
|
933
|
+
offset += tab.code.split('\n').length;
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/* ── ACE sessions ─────────────────────────────────────── */
|
|
939
|
+
function buildSessions() {
|
|
940
|
+
/* Always create fresh sessions — reusing an active session causes ACE
|
|
941
|
+
to skip the display refresh even after setValue(). */
|
|
942
|
+
Object.keys(aceSessions).forEach(id => delete aceSessions[id]);
|
|
943
|
+
|
|
944
|
+
tabs.filter(t => !t.hidden && t.type === 'python').forEach(tab => {
|
|
945
|
+
const session = ace.createEditSession(tab.code, 'ace/mode/python');
|
|
946
|
+
session.setUseWorker(false);
|
|
947
|
+
session.setTabSize(4);
|
|
948
|
+
aceSessions[tab.id] = session;
|
|
949
|
+
|
|
950
|
+
if (!tab.readonly) {
|
|
951
|
+
let saveTimer = null;
|
|
952
|
+
session.on('change', () => {
|
|
953
|
+
if (saveTimer !== null) { clearTimeout(saveTimer); saveTimers.delete(saveTimer); }
|
|
954
|
+
saveTimer = setTimeout(() => { saveTimers.delete(saveTimer); saveTimer = null; saveSnapshot(); }, 350);
|
|
955
|
+
saveTimers.add(saveTimer);
|
|
956
|
+
updateLineOffsets();
|
|
957
|
+
refreshDirty();
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
/* Point ACE instance at first python session and force a full redraw */
|
|
963
|
+
const firstPython = tabs.find(t => !t.hidden && t.type === 'python');
|
|
964
|
+
if (aceInst && firstPython && aceSessions[firstPython.id]) {
|
|
965
|
+
aceInst.setSession(aceSessions[firstPython.id]);
|
|
966
|
+
aceInst.setReadOnly(firstPython.readonly);
|
|
967
|
+
aceInst.renderer.updateFull(true);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
updateLineOffsets();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/* ── Init ACE (once) ──────────────────────────────────── */
|
|
611
974
|
function initAce() {
|
|
612
|
-
/* In local mode ACE cannot auto-detect where its dynamic modules live
|
|
613
|
-
(searchbox, keybindings…), so we set basePath explicitly. */
|
|
614
975
|
if (URLS.ace.startsWith('vendor') || !URLS.ace.startsWith('http')) {
|
|
615
976
|
ace.config.set('basePath', URLS.ace.replace(/\/[^/]+$/, '/'));
|
|
616
977
|
}
|
|
978
|
+
|
|
617
979
|
aceInst = ace.edit('pf-ace');
|
|
618
|
-
aceInst.session.setMode('ace/mode/python');
|
|
619
980
|
aceInst.setTheme('ace/theme/monokai');
|
|
620
|
-
aceInst.setValue(initialCode, -1);
|
|
621
|
-
btnReset.classList.toggle('pf-dirty', initialCode !== starterCode);
|
|
622
981
|
aceInst.setOptions({
|
|
623
982
|
fontSize : '15px',
|
|
624
983
|
showPrintMargin: false,
|
|
@@ -630,13 +989,11 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
630
989
|
enableSnippets : true,
|
|
631
990
|
});
|
|
632
991
|
|
|
992
|
+
/* Keyboard shortcuts (registered once) */
|
|
633
993
|
aceInst.commands.addCommand({
|
|
634
994
|
name: 'pfRun',
|
|
635
995
|
bindKey: { win: 'Shift-Enter', mac: 'Shift-Enter' },
|
|
636
|
-
exec: () => {
|
|
637
|
-
if (aceInst.completer?.popup?.isOpen) return;
|
|
638
|
-
runCode();
|
|
639
|
-
},
|
|
996
|
+
exec: () => { if (aceInst.completer?.popup?.isOpen) return; runCode(); },
|
|
640
997
|
});
|
|
641
998
|
aceInst.commands.addCommand({
|
|
642
999
|
name: 'pfClose',
|
|
@@ -652,24 +1009,69 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
652
1009
|
name: 'pfReset',
|
|
653
1010
|
bindKey: { win: 'Ctrl-R', mac: 'Command-R' },
|
|
654
1011
|
exec: () => {
|
|
655
|
-
if (confirm('Réinitialiser
|
|
656
|
-
aceInst.setValue(starterCode, -1);
|
|
657
|
-
runCode();
|
|
658
|
-
}
|
|
1012
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) resetAllTabs();
|
|
659
1013
|
},
|
|
660
1014
|
});
|
|
661
1015
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
saveTimer = setTimeout(saveCode, 350);
|
|
666
|
-
btnReset.classList.toggle('pf-dirty', aceInst.getValue() !== starterCode);
|
|
667
|
-
});
|
|
1016
|
+
buildSessions();
|
|
1017
|
+
buildTabBar();
|
|
1018
|
+
refreshDirty();
|
|
668
1019
|
}
|
|
669
1020
|
|
|
670
|
-
|
|
671
|
-
|
|
1021
|
+
/* ── Persistence ──────────────────────────────────────── */
|
|
1022
|
+
function saveSnapshot() {
|
|
1023
|
+
const snapshot = {
|
|
1024
|
+
v: 1,
|
|
1025
|
+
tabs: tabs.map(tab => ({
|
|
1026
|
+
label : tab.label,
|
|
1027
|
+
hidden : tab.hidden,
|
|
1028
|
+
readonly: tab.readonly,
|
|
1029
|
+
type : tab.type,
|
|
1030
|
+
content : (!tab.hidden && !tab.readonly && tab.type === 'python' && aceSessions[tab.id])
|
|
1031
|
+
? aceSessions[tab.id].getValue()
|
|
1032
|
+
: tab.code,
|
|
1033
|
+
})),
|
|
1034
|
+
};
|
|
1035
|
+
try { localStorage.setItem(SK, JSON.stringify(snapshot)); } catch (e) {}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function saveCode() { saveSnapshot(); }
|
|
1039
|
+
|
|
1040
|
+
/* ── Dirty indicator ──────────────────────────────────── */
|
|
1041
|
+
function refreshDirty() {
|
|
1042
|
+
const dirty = tabs.some(tab =>
|
|
1043
|
+
!tab.hidden && !tab.readonly && tab.type === 'python' &&
|
|
1044
|
+
aceSessions[tab.id] &&
|
|
1045
|
+
aceSessions[tab.id].getValue() !== tab.starterCode
|
|
1046
|
+
);
|
|
1047
|
+
btnReset.classList.toggle('pf-dirty', dirty);
|
|
672
1048
|
}
|
|
1049
|
+
|
|
1050
|
+
/* ── Reset: restore file structure + content, no reload ─ */
|
|
1051
|
+
function resetAllTabs() {
|
|
1052
|
+
/* Cancel all pending saves first */
|
|
1053
|
+
saveTimers.forEach(t => clearTimeout(t));
|
|
1054
|
+
saveTimers.clear();
|
|
1055
|
+
|
|
1056
|
+
/* Erase snapshot and any legacy per-tab keys */
|
|
1057
|
+
try { localStorage.removeItem(SK); } catch (e) {}
|
|
1058
|
+
tabs.forEach(tab => {
|
|
1059
|
+
if (tab.label) {
|
|
1060
|
+
try { localStorage.removeItem(SK + ':' + tab.label.replace(/[^a-zA-Z0-9]/g, '_')); } catch (e) {}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
try { localStorage.removeItem(SK + ':Code'); } catch (e) {}
|
|
1064
|
+
|
|
1065
|
+
/* Rebuild tabs from htmlTabs (file structure, starterCode as working code) */
|
|
1066
|
+
tabs = htmlTabs.map((ht, i) => ({ ...ht, id: 'tab-' + i, code: ht.starterCode }));
|
|
1067
|
+
|
|
1068
|
+
/* Rebuild sessions and tab bar in-memory */
|
|
1069
|
+
buildSessions();
|
|
1070
|
+
buildTabBar();
|
|
1071
|
+
refreshDirty();
|
|
1072
|
+
runCode();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
673
1075
|
window.addEventListener('beforeunload', saveCode);
|
|
674
1076
|
|
|
675
1077
|
/* ─────────────────── PYODIDE ────────────────── */
|
|
@@ -931,7 +1333,14 @@ m.__getattr__ = _p5_getattr
|
|
|
931
1333
|
|
|
932
1334
|
loaderEl.style.display = 'none';
|
|
933
1335
|
|
|
934
|
-
|
|
1336
|
+
/* Concatenate all python tabs in DOM order (hidden + visible) */
|
|
1337
|
+
const code = tabs
|
|
1338
|
+
.filter(t => t.type === 'python')
|
|
1339
|
+
.map(t => {
|
|
1340
|
+
if (!t.hidden && !t.readonly && aceSessions[t.id]) return aceSessions[t.id].getValue();
|
|
1341
|
+
return t.code; /* hidden or readonly: use original/starter code */
|
|
1342
|
+
})
|
|
1343
|
+
.join('\n');
|
|
935
1344
|
|
|
936
1345
|
/* Auto-load any Pyodide-bundled packages the sketch imports. */
|
|
937
1346
|
try {
|
|
@@ -1059,7 +1468,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1059
1468
|
}
|
|
1060
1469
|
|
|
1061
1470
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1062
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@
|
|
1471
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.1/pyfrilet.min.js';
|
|
1063
1472
|
|
|
1064
1473
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1065
1474
|
<html lang="fr">
|
|
@@ -1071,16 +1480,36 @@ m.__getattr__ = _p5_getattr
|
|
|
1071
1480
|
</head>
|
|
1072
1481
|
<body>
|
|
1073
1482
|
|
|
1074
|
-
|
|
1075
|
-
FILLME-PYTHON
|
|
1076
|
-
<\/script>
|
|
1483
|
+
FILLME-SCRIPTS
|
|
1077
1484
|
|
|
1078
1485
|
</body>
|
|
1079
1486
|
</html>`;
|
|
1080
1487
|
|
|
1081
1488
|
function download() {
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1489
|
+
/* Reconstruct all script tags preserving structure, attributes, and current editor content */
|
|
1490
|
+
const scripts = tabs.map((tab, i) => {
|
|
1491
|
+
/* Get current content */
|
|
1492
|
+
let content;
|
|
1493
|
+
if (tab.type === 'python' && !tab.hidden && !tab.readonly && aceSessions[tab.id]) {
|
|
1494
|
+
content = aceSessions[tab.id].getValue();
|
|
1495
|
+
} else {
|
|
1496
|
+
content = tab.code;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/* Rebuild attributes */
|
|
1500
|
+
const attrs = [];
|
|
1501
|
+
const scriptType = tab.type === 'markdown' ? 'text/markdown' : 'text/python';
|
|
1502
|
+
if (tab.label !== null) attrs.push(`data-tab="${tab.label.replace(/"/g, '"')}"`);
|
|
1503
|
+
if (tab.hidden) attrs.push('data-hidden');
|
|
1504
|
+
if (tab.readonly) attrs.push('data-readonly');
|
|
1505
|
+
|
|
1506
|
+
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
1507
|
+
/* Escape </script> inside content */
|
|
1508
|
+
const safe = content.replace(/<\/script>/gi, '<\\/script>');
|
|
1509
|
+
return `<script type="${scriptType}"${attrStr}>\n${safe}\n<\/script>`;
|
|
1510
|
+
}).join('\n\n');
|
|
1511
|
+
|
|
1512
|
+
const html = STANDALONE_TEMPLATE.replace('FILLME-SCRIPTS', scripts);
|
|
1084
1513
|
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
|
1085
1514
|
const url = URL.createObjectURL(blob);
|
|
1086
1515
|
const a = Object.assign(document.createElement('a'), { href: url, download: 'sketch.html' });
|
|
@@ -1090,20 +1519,6 @@ FILLME-PYTHON
|
|
|
1090
1519
|
URL.revokeObjectURL(url);
|
|
1091
1520
|
}
|
|
1092
1521
|
|
|
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
1522
|
/* ── WebM recording ─────────────────────────────────────────────── */
|
|
1108
1523
|
let mediaRecorder = null;
|
|
1109
1524
|
let recChunks = [];
|
|
@@ -1142,14 +1557,28 @@ FILLME-PYTHON
|
|
|
1142
1557
|
mediaRecorder ? stopRecording() : startRecording();
|
|
1143
1558
|
});
|
|
1144
1559
|
|
|
1560
|
+
/* ─────────────────── BUTTON HANDLERS ────────── */
|
|
1561
|
+
btnRun.addEventListener('click', () => runCode());
|
|
1562
|
+
|
|
1563
|
+
/* Code button: open at full screen height, or close if already open */
|
|
1564
|
+
btnCode.addEventListener('click', () => {
|
|
1565
|
+
if (drawerOpen) {
|
|
1566
|
+
closeDrawer();
|
|
1567
|
+
} else {
|
|
1568
|
+
drawerH = window.innerHeight - 32;
|
|
1569
|
+
_applyDrawerH();
|
|
1570
|
+
openDrawer();
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1145
1574
|
btnDl.addEventListener('click', download);
|
|
1575
|
+
|
|
1146
1576
|
const HELP_URL = 'https://codeberg.org/nopid/pyfrilet';
|
|
1147
1577
|
btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
|
|
1148
1578
|
|
|
1149
1579
|
btnReset.addEventListener('click', () => {
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1152
|
-
runCode();
|
|
1580
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
|
|
1581
|
+
resetAllTabs();
|
|
1153
1582
|
}
|
|
1154
1583
|
});
|
|
1155
1584
|
|
|
@@ -1210,9 +1639,8 @@ FILLME-PYTHON
|
|
|
1210
1639
|
/* Ctrl/Cmd+R: reset code (prevent browser reload) */
|
|
1211
1640
|
if ((ev.key === 'r' || ev.key === 'R') && (ev.ctrlKey || ev.metaKey) && !ev.altKey) {
|
|
1212
1641
|
ev.preventDefault();
|
|
1213
|
-
if (
|
|
1214
|
-
|
|
1215
|
-
runCode();
|
|
1642
|
+
if (confirm('Réinitialiser ? Les modifications seront perdues.')) {
|
|
1643
|
+
resetAllTabs();
|
|
1216
1644
|
}
|
|
1217
1645
|
return;
|
|
1218
1646
|
}
|
|
@@ -1235,6 +1663,22 @@ FILLME-PYTHON
|
|
|
1235
1663
|
|
|
1236
1664
|
try {
|
|
1237
1665
|
await loadScript(URLS.p5);
|
|
1666
|
+
if (URLS.marked) {
|
|
1667
|
+
/* Load KaTeX CSS first (no JS dependency) */
|
|
1668
|
+
const katexLink = document.createElement('link');
|
|
1669
|
+
katexLink.rel = 'stylesheet';
|
|
1670
|
+
katexLink.href = URLS.katexCss;
|
|
1671
|
+
document.head.appendChild(katexLink);
|
|
1672
|
+
/* Then JS libs in order */
|
|
1673
|
+
await loadScript(URLS.marked);
|
|
1674
|
+
await loadScript(URLS.katex);
|
|
1675
|
+
await loadScript(URLS.markedKatex);
|
|
1676
|
+
await loadScript(URLS.mermaid);
|
|
1677
|
+
/* Configure marked: KaTeX extension */
|
|
1678
|
+
marked.use(markedKatex({ throwOnError: false }));
|
|
1679
|
+
/* Initialize mermaid (startOnLoad:false — we call run() manually) */
|
|
1680
|
+
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
|
1681
|
+
}
|
|
1238
1682
|
await loadScript(URLS.ace);
|
|
1239
1683
|
await loadScript(URLS.acePython);
|
|
1240
1684
|
await loadScript(URLS.aceMonokai);
|
|
@@ -1251,6 +1695,8 @@ FILLME-PYTHON
|
|
|
1251
1695
|
await runCode();
|
|
1252
1696
|
loaderEl.style.display = 'none';
|
|
1253
1697
|
})();
|
|
1254
|
-
}
|
|
1255
1698
|
|
|
1256
|
-
|
|
1699
|
+
|
|
1700
|
+
} /* end main() */
|
|
1701
|
+
|
|
1702
|
+
})(); /* 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()},pe&&(e.touchStarted=()=>{z&&pe()}),ue&&(e.touchMoved=()=>{z&&ue()}),fe&&(e.touchEnded=()=>{z&&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.3/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",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}",h='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],b=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),g=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===b;const v=_.some(e=>"text/markdown"===e.getAttribute("type")),w=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:r,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:v?l:null,katexCss:v?c:null,katex:v?p:null,markedKatex:v?m:null,mermaid:v?f:null}:{p5:g+"p5.min.js",pyodide:g+"pyodide/pyodide.js",pyodideIndex:g+"pyodide/",ace:g+"ace.min.js",acePython:g+"mode-python.min.js",aceMonokai:g+"theme-monokai.min.js",aceLangTools:g+"ext-language_tools.min.js",aceSearchbox:g+"ext-searchbox.min.js",marked:v?g+"marked.min.js":null,katexCss:v?g+"katex.min.css":null,katex:v?g+"katex.min.js":null,markedKatex:v?g+"marked-katex-extension.js":null,mermaid:v?g+"mermaid.min.js":null},x="pyfrilet:"+location.pathname,k=_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let r=e.getAttribute("data-tab");null!==r||a||(r=1===_.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:r,hidden:a,readonly:o,type:t,starterCode:i,code:i}}),E=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let C;const S=E(x);let L=null;if(S)try{L=JSON.parse(S)}catch(e){L=null}C=L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0?L.tabs.map((e,n)=>{const t=k.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):k.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=E(x+":"+t);if(a||"Code"!==e.label||1!==k.length||(a=E(x)),a&&a.trim())return{...e,code:a}}return e}),function(e,t,a,o){e=e.slice();const r=document.createElement("style");r.textContent=u,document.head.appendChild(r),document.body.innerHTML=h;const i=document.getElementById("pf-app"),s=document.getElementById("pf-drawer"),d=document.getElementById("pf-handle"),l=document.getElementById("pf-sketch"),c=document.getElementById("pf-viewport"),p=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),f=document.getElementById("pf-err"),_=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),b=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-rec"),v=document.getElementById("pf-btn-reset"),w=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint"),E=document.getElementById("pf-tabs"),C=document.getElementById("pf-markdown-view");let S=!1,L=Math.round(.56*window.innerHeight);function j(){document.documentElement.style.setProperty("--pf-drawer-h",L+"px")}function z(){S=!0,s.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{$(),J&&J.focus()},280)}function I(){S=!1,s.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{$();const e=Y._p?.canvas;e&&e.removeAttribute("tabindex"),i.focus()},280)}function R(){S?I():z()}j();let M=null;const P=5,A=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;M={y:n,h:S?L:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function O(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>P&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<A?(s.style.transition="none",s.style.height="32px"):(L=a,j(),S||z(),s.style.transition="none",s.style.height=L+"px"),$()}function W(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,o=M.h+a;M=null,B.style.display="none",document.body.style.userSelect="",s.style.transition="",s.style.height="",n&&(o<A?I():(L=Math.max(A,Math.min(window.innerHeight-50,o)),j(),S||z()),$())}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()}),d.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",O),document.addEventListener("mouseup",W),d.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",O,{passive:!0}),document.addEventListener("touchend",W);let D=0,K=0;function N(e){f.textContent=e,f.style.display="block",z()}function U(){f.textContent="",f.style.display="none"}function F(){if(!Y._p||"fit"!==Y._mode)return;const e=Y._w,n=Y._h;if(!e||!n)return;const t=i.clientWidth,a=i.clientHeight,o=Math.min(t/e,a/n);c.style.transform=`scale(${o})`}function $(){if("fullscreen"===Y._mode?Y.size("max"):F(),H&&"function"==typeof H.windowResized)try{H.windowResized()}catch(e){N(String(e))}J&&J.resize()}window.addEventListener("mousemove",e=>{D=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(D=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=Y._p?Y._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=Y._w/n.width,a=Y._h/n.height;return[(D-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",$);let H=null;const Y=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),c.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),F())},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 X(){if(Me(),H){try{H.remove()}catch(e){}H=null}l.innerHTML="",Y._p=null,Y._mode="fit",Y._w=0,Y._h=0,c.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",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),ve&&(ve.destroy(),ve=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null)}window.p5py=Y;let J=null,q=null;const G={},V=new Set;function Z(){E.innerHTML="",q=null;const n=e.filter(e=>!e.hidden);E.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>Q(e)),E.appendChild(n)}),n.length>0&&Q(n[0],!0)}function Q(e,n){if(n||q!==e)if(q=e,E.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",C.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>`)),C.innerHTML=`<div class="pf-md-inner">${n}</div>`}else C.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:C.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",C.style.display="none",J&&G[e.id]&&(J.setSession(G[e.id]),J.setReadOnly(e.readonly),J.focus())}function ee(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!G[e.id]?n+=e.code.split("\n").length:(G[e.id].setOption("firstLineNumber",n),n+=G[e.id].getLength())})}function ne(){Object.keys(G).forEach(e=>delete G[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),G[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),V.delete(e)),e=setTimeout(()=>{V.delete(e),e=null,ae()},350),V.add(e),ee(),re()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);J&&n&&G[n.id]&&(J.setSession(G[n.id]),J.setReadOnly(n.readonly),J.renderer.updateFull(!0)),ee()}function te(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),J=ace.edit("pf-ace"),J.setTheme("ace/theme/monokai"),J.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),J.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{J.completer?.popup?.isOpen||Se()}}),J.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:I}),J.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:oe}),J.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&ie()}}),ne(),Z(),re()}function ae(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!G[e.id]?e.code:G[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function oe(){ae()}function re(){const n=e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&G[e.id]&&G[e.id].getValue()!==e.starterCode);v.classList.toggle("pf-dirty",n)}function ie(){V.forEach(e=>clearTimeout(e)),V.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),ne(),Z(),re(),Se()}window.addEventListener("beforeunload",oe);let se=null,de=null;async function le(){return de||(de=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),se=await loadPyodide(e),se.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"),J){ce(se.runPython("list(m.__all__)").toJs())}})(),de)}function ce(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,r){r(null,o.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,J.completers=[...J.completers||[],t]}let pe=!1,me=null,fe=null,ue=null,he=null,_e=null,ye=null,be=null,ge=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null;async function Se(){if(pe)return;pe=!0,_.classList.add("pf-running"),U(),X(),se||(m.textContent="Initialisation de Pyodide…",p.style.display="flex");try{await le()}catch(e){return p.style.display="none",N("Erreur Pyodide : "+e),pe=!1,void _.classList.remove("pf-running")}p.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!G[e.id]?e.code:G[e.id].getValue()).join("\n");try{m.textContent="Chargement des dépendances…",p.style.display="flex",await se.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}p.style.display="none",se.globals.set("_USER_CODE",t);try{se.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),se.runPython("_ns_ref[0] = _ns")}catch(e){return N(String(e)),pe=!1,void _.classList.remove("pf-running")}let a,o,r,i,s,d,c,f,u,h,y,b,g,v;try{const e=(e,n)=>se.runPython(`_ns.get('${e}') or _ns.get('${n}')`);s=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),r=e("mousePressed","mouse_pressed"),i=e("keyPressed","key_pressed"),d=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),u=e("mouseWheel","mouse_wheel"),h=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 N(String(e)),pe=!1,void _.classList.remove("pf-running")}if(!o)return N("Le script doit définir au moins une fonction draw()."),pe=!1,void _.classList.remove("pf-running");const{create_proxy:w}=se.pyimport("pyodide.ffi"),x=se.runPython("_ns.get('windowResized')"),k=se.globals.get("_pf_refresh"),E=se.globals.get("_ns"),C=e=>e?w(()=>{try{k(E),e()}catch(e){N(String(e))}}):null;ue=s?w(()=>{try{s()}catch(e){N(String(e))}}):null,me=a?w(()=>{try{a()}catch(e){N(String(e))}}):null;const S=200;fe=w(()=>{try{k(E);const e=performance.now();o(),performance.now()-e>S&&(X(),N(`draw() a mis plus de ${S} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){N(String(e)),X()}}),he=C(r),_e=C(c),ye=C(d),be=C(f),ge=C(u),ve=C(h),we=C(i),xe=C(y),ke=C(b),Ee=C(g),Ce=C(v);const L=x?w(()=>{try{x()}catch(e){N(String(e))}}):null;let j=!1;H=new p5(e=>{Y._setP(e),ue&&(e.preload=()=>{ue()}),e.setup=()=>{me&&me(),e.canvas||Y.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),j=!0},e.draw=()=>{j&&fe()},e.mousePressed=()=>{j&&he&&he()},e.mouseReleased=()=>{j&&_e&&_e()},e.mouseDragged=()=>{j&&ye&&ye()},e.mouseMoved=()=>{j&&be&&be()},e.mouseWheel=e=>{j&&ge&&ge()},e.doubleClicked=()=>{j&&ve&&ve()},e.keyPressed=()=>{j&&we&&we()},e.keyReleased=()=>{j&&xe&&xe()},ke&&(e.touchStarted=()=>{j&&ke()}),Ee&&(e.touchMoved=()=>{j&&Ee()}),Ce&&(e.touchEnded=()=>{j&&Ce()}),e.windowResized=()=>{"fullscreen"===Y._mode?Y.size("max"):F(),L&&L()}},l),pe=!1,_.classList.remove("pf-running")}const Le='<!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.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function je(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!G[e.id]?e.code:G[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=Le.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),r=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}let ze=null,Ie=[];function Re(){const e=Y._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();ze=new MediaRecorder(t,{mimeType:n}),Ie=[],ze.ondataavailable=e=>{e.data.size&&Ie.push(e.data)},ze.onstop=()=>{const e=new Blob(Ie,{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),g.textContent="⏺",g.title="Enregistrer WebM",g.classList.remove("pf-recording"),ze=null},ze.start(),g.textContent="⏹",g.title="Arrêter l'enregistrement",g.classList.add("pf-recording")}function Me(){ze&&"inactive"!==ze.state&&ze.stop()}g.addEventListener("click",()=>{ze?Me():Re()}),_.addEventListener("click",()=>Se()),y.addEventListener("click",()=>{S?I():(L=window.innerHeight-32,j(),z())}),b.addEventListener("click",je);const Pe="https://codeberg.org/nopid/pyfrilet";function Ae(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)})}w.addEventListener("click",()=>window.open(Pe,"_blank")),v.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&ie()}),window.addEventListener("keydown",e=>{const n=S&&J&&J.isFocused&&J.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Se();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),J.searchBox?J.searchBox.hide():t.style.display="none",void J.focus();if(n){const n=J.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void I()}return e.preventDefault(),e.stopPropagation(),void(S?I():z())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&ie())):(e.preventDefault(),void oe())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",p.style.display="flex";try{if(await Ae(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await Ae(o.marked),await Ae(o.katex),await Ae(o.markedKatex),await Ae(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ae(o.ace),await Ae(o.acePython),await Ae(o.aceMonokai),await Ae(o.aceLangTools),await Ae(o.aceSearchbox),await Ae(o.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}te(),await Se(),p.style.display="none"})()}(C,k,x,w)})}();
|