pyfrilet 0.5.1 → 0.5.2
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 +18 -5
- package/package.json +1 -1
- package/pyfrilet.js +50 -13
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,11 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
|
|
|
100
100
|
| `touchMoved()` | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
|
|
101
101
|
| `touchEnded()` | Fin de contact tactile. |
|
|
102
102
|
|
|
103
|
-
> **
|
|
103
|
+
> **Watchdog `draw()`** : pyfrilet surveille le temps d'exécution de `draw()` via `sys.settrace`. Si une exécution dépasse **300 ms**, le sketch est arrêté et une erreur est affichée — cela protège le navigateur contre les boucles infinies accidentelles (`while True: pass`, récursion sans fin…). La trace est vérifiée toutes les 100 lignes Python pour minimiser l'overhead. Pour désactiver ce comportement (calculs intensifs légitimes hors de `draw()`), ajouter `data-no-watchdog` sur la balise `<script src="pyfrilet.js">` :
|
|
104
|
+
>
|
|
105
|
+
> ```html
|
|
106
|
+
> <script src="pyfrilet.js" data-no-watchdog></script>
|
|
107
|
+
> ```
|
|
104
108
|
|
|
105
109
|
#### Fonctions pyfrilet (hors p5.js standard)
|
|
106
110
|
|
|
@@ -247,7 +251,7 @@ La barre de contrôle est collée en bas de l'écran.
|
|
|
247
251
|
| ✏️ | Ouvre l'éditeur en **plein écran** ; referme si déjà ouvert |
|
|
248
252
|
| 💾 | Télécharge la page en HTML autonome (voir ci-dessous) |
|
|
249
253
|
| ⏺ | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement — cliquer pour arrêter et télécharger |
|
|
250
|
-
| ↻ | Réinitialise
|
|
254
|
+
| ↻ | Réinitialise **tous les onglets** à la version du fichier source (confirmation demandée). Un point orange apparaît sur le bouton dès qu'un onglet a été modifié par rapport à sa version d'origine. |
|
|
251
255
|
|
|
252
256
|
### Raccourcis clavier
|
|
253
257
|
|
|
@@ -256,11 +260,11 @@ La barre de contrôle est collée en bas de l'écran.
|
|
|
256
260
|
| `Shift+Entrée` | Relance l'exécution (depuis l'éditeur ou depuis le sketch) |
|
|
257
261
|
| `Échap` | Ouvre le tiroir si fermé, ferme si ouvert |
|
|
258
262
|
| `Ctrl+S` | Sauvegarde le code dans le localStorage |
|
|
259
|
-
| `Ctrl+R` | Réinitialise
|
|
263
|
+
| `Ctrl+R` | Réinitialise **tous les onglets** à la version d'origine (confirmation demandée) |
|
|
260
264
|
|
|
261
265
|
Quand le tiroir se ferme, le focus clavier est automatiquement donné au canvas — les événements `keyPressed()` du sketch fonctionnent sans clic préalable.
|
|
262
266
|
|
|
263
|
-
Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification (
|
|
267
|
+
Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque modification. pyfrilet enregistre un snapshot JSON complet de tous les onglets (structure, types, contenus — y compris les blocs cachés et Markdown). À la prochaine visite, la page s'affiche exactement telle qu'elle a été laissée, avec le même nombre d'onglets et les mêmes contenus. Cliquer sur ↻ efface le snapshot et restaure la structure et le contenu du fichier HTML source.
|
|
264
268
|
|
|
265
269
|
### Télécharger
|
|
266
270
|
|
|
@@ -334,7 +338,11 @@ def draw():
|
|
|
334
338
|
</script>
|
|
335
339
|
```
|
|
336
340
|
|
|
337
|
-
###
|
|
341
|
+
### Rendu des onglets Markdown
|
|
342
|
+
|
|
343
|
+
Les onglets Markdown sont affichés sur fond clair avec une mise en page soignée : largeur de lecture limitée à 680 px, centrage automatique sur grand écran, police [Alegreya Sans](https://fonts.google.com/specimen/Alegreya+Sans) (chargée depuis Google Fonts en mode CDN). Les blocs de code sont rendus sur fond sombre pour un bon contraste. Les diagrammes Mermaid utilisent le thème neutre (clair).
|
|
344
|
+
|
|
345
|
+
En mode déploiement local sans accès internet, la fonte se replie sur Georgia.
|
|
338
346
|
|
|
339
347
|
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
348
|
|
|
@@ -390,6 +398,8 @@ graph LR
|
|
|
390
398
|
|
|
391
399
|
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.
|
|
392
400
|
|
|
401
|
+
> **Numéros de ligne** : les numéros affichés par l'éditeur ACE dans chaque onglet correspondent aux numéros réels dans le code Python exécuté. Un message d'erreur mentionnant la ligne 142 pointe donc directement vers la bonne ligne dans le bon onglet.
|
|
402
|
+
|
|
393
403
|
---
|
|
394
404
|
|
|
395
405
|
## Déploiement local (mode `vendor/`)
|
|
@@ -404,6 +414,9 @@ Par défaut, pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics. Pour
|
|
|
404
414
|
<!-- Recommandé -->
|
|
405
415
|
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/"></script>
|
|
406
416
|
|
|
417
|
+
<!-- Avec watchdog désactivé (calculs intensifs) -->
|
|
418
|
+
<script src="pyfrilet.js" data-sources="local" data-vendor="vendor/" data-no-watchdog></script>
|
|
419
|
+
|
|
407
420
|
<!-- Rétrocompat (ancienne syntaxe) -->
|
|
408
421
|
<script src="pyfrilet.js"></script>
|
|
409
422
|
<script type="text/python" data-sources="local" data-vendor="vendor/">
|
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -542,11 +542,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
542
542
|
});
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
|
|
545
|
+
const noWatchdog = configTag.hasAttribute('data-no-watchdog');
|
|
546
|
+
|
|
547
|
+
main(tabs, htmlTabs, SK, URLS, noWatchdog);
|
|
546
548
|
});
|
|
547
549
|
|
|
548
550
|
/* ═══════════════════════════ MAIN ═══════════════════════════════════ */
|
|
549
|
-
function main(tabs, htmlTabs, SK, URLS) {
|
|
551
|
+
function main(tabs, htmlTabs, SK, URLS, noWatchdog) {
|
|
550
552
|
|
|
551
553
|
/* tabs = working state (from snapshot or HTML), may be reassigned on reset
|
|
552
554
|
htmlTabs = ground truth from current HTML file, never mutated */
|
|
@@ -1240,6 +1242,36 @@ def _pf_refresh(ns):
|
|
|
1240
1242
|
|
|
1241
1243
|
sys.modules["p5"] = m
|
|
1242
1244
|
|
|
1245
|
+
# ── draw() watchdog via sys.settrace ──────────────────────────────
|
|
1246
|
+
# Trace is called on every Python line event. We only call time.monotonic()
|
|
1247
|
+
# every N events to minimize overhead — a tight loop still triggers within
|
|
1248
|
+
# a few microseconds, so detection latency is negligible.
|
|
1249
|
+
import time as _time
|
|
1250
|
+
|
|
1251
|
+
_WDOG_CHECK_EVERY = 100
|
|
1252
|
+
_wdog_deadline = [0.0]
|
|
1253
|
+
_wdog_count = [0]
|
|
1254
|
+
|
|
1255
|
+
def _wdog_trace(frame, event, arg):
|
|
1256
|
+
_wdog_count[0] += 1
|
|
1257
|
+
if _wdog_count[0] >= _WDOG_CHECK_EVERY:
|
|
1258
|
+
_wdog_count[0] = 0
|
|
1259
|
+
if _time.monotonic() > _wdog_deadline[0]:
|
|
1260
|
+
raise TimeoutError("draw() watchdog")
|
|
1261
|
+
return _wdog_trace
|
|
1262
|
+
|
|
1263
|
+
def _pf_draw_watchdog(fn, timeout_ms):
|
|
1264
|
+
_wdog_count[0] = 0
|
|
1265
|
+
_wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001
|
|
1266
|
+
sys.settrace(_wdog_trace)
|
|
1267
|
+
try:
|
|
1268
|
+
fn()
|
|
1269
|
+
finally:
|
|
1270
|
+
sys.settrace(None)
|
|
1271
|
+
|
|
1272
|
+
def _pf_draw_direct(fn, timeout_ms):
|
|
1273
|
+
fn()
|
|
1274
|
+
|
|
1243
1275
|
def _snake_to_camel(name):
|
|
1244
1276
|
parts = name.split('_')
|
|
1245
1277
|
return parts[0] + ''.join(p.capitalize() for p in parts[1:])
|
|
@@ -1311,6 +1343,8 @@ m.__getattr__ = _p5_getattr
|
|
|
1311
1343
|
keyPressedProxy = null, keyReleasedProxy = null,
|
|
1312
1344
|
touchStartedProxy = null, touchMovedProxy = null, touchEndedProxy = null;
|
|
1313
1345
|
|
|
1346
|
+
const WATCHDOG_MS = 300;
|
|
1347
|
+
|
|
1314
1348
|
async function runCode() {
|
|
1315
1349
|
if (running) return;
|
|
1316
1350
|
running = true;
|
|
@@ -1389,9 +1423,10 @@ m.__getattr__ = _p5_getattr
|
|
|
1389
1423
|
}
|
|
1390
1424
|
|
|
1391
1425
|
const { create_proxy } = pyodide.pyimport('pyodide.ffi');
|
|
1392
|
-
const pyWR
|
|
1393
|
-
const pyRefresh
|
|
1394
|
-
const
|
|
1426
|
+
const pyWR = pyodide.runPython("_ns.get('windowResized')");
|
|
1427
|
+
const pyRefresh = pyodide.globals.get('_pf_refresh');
|
|
1428
|
+
const pfDrawWatchdog = pyodide.globals.get(noWatchdog ? '_pf_draw_direct' : '_pf_draw_watchdog');
|
|
1429
|
+
const pyNs = pyodide.globals.get('_ns');
|
|
1395
1430
|
|
|
1396
1431
|
const mkProxy = (fn) => fn ? create_proxy(() => {
|
|
1397
1432
|
try { pyRefresh(pyNs); fn(); } catch (e) { showError(String(e)); }
|
|
@@ -1399,17 +1434,19 @@ m.__getattr__ = _p5_getattr
|
|
|
1399
1434
|
|
|
1400
1435
|
preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
|
|
1401
1436
|
setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
|
|
1402
|
-
const DRAW_TIMEOUT_MS = 200;
|
|
1403
1437
|
drawProxy = create_proxy(() => {
|
|
1404
1438
|
try {
|
|
1405
1439
|
pyRefresh(pyNs);
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1440
|
+
pfDrawWatchdog(pyDraw, WATCHDOG_MS);
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
const msg = String(e);
|
|
1443
|
+
stopSketch();
|
|
1444
|
+
if (msg.includes('TimeoutError') || msg.includes('watchdog')) {
|
|
1445
|
+
showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
|
|
1446
|
+
} else {
|
|
1447
|
+
showError(msg);
|
|
1411
1448
|
}
|
|
1412
|
-
}
|
|
1449
|
+
}
|
|
1413
1450
|
});
|
|
1414
1451
|
mousePressedProxy = mkProxy(pyMP);
|
|
1415
1452
|
mouseReleasedProxy = mkProxy(pyMR);
|
|
@@ -1468,7 +1505,7 @@ m.__getattr__ = _p5_getattr
|
|
|
1468
1505
|
}
|
|
1469
1506
|
|
|
1470
1507
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
1471
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.
|
|
1508
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.5.2/pyfrilet.min.js';
|
|
1472
1509
|
|
|
1473
1510
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
1474
1511
|
<html lang="fr">
|
package/pyfrilet.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}",h='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],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)})}();
|
|
1
|
+
!function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}",h='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const _=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===_.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const y=e||_[0],g=(y.getAttribute("data-sources")||y.getAttribute("sources")||"cdn").toLowerCase().trim(),b=(y.getAttribute("data-vendor")||y.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===g;const v=_.some(e=>"text/markdown"===e.getAttribute("type")),w=n?{p5:t,pyodide:a,pyodideIndex:null,ace:o,acePython:r,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:v?l:null,katexCss:v?c:null,katex:v?p:null,markedKatex:v?m:null,mermaid:v?f:null}:{p5:b+"p5.min.js",pyodide:b+"pyodide/pyodide.js",pyodideIndex:b+"pyodide/",ace:b+"ace.min.js",acePython:b+"mode-python.min.js",aceMonokai:b+"theme-monokai.min.js",aceLangTools:b+"ext-language_tools.min.js",aceSearchbox:b+"ext-searchbox.min.js",marked:v?b+"marked.min.js":null,katexCss:v?b+"katex.min.css":null,katex:v?b+"katex.min.js":null,markedKatex:v?b+"marked-katex-extension.js":null,mermaid:v?b+"mermaid.min.js":null},x="pyfrilet:"+location.pathname,k=_.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),o=e.hasAttribute("data-readonly");let r=e.getAttribute("data-tab");null!==r||a||(r=1===_.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:r,hidden:a,readonly:o,type:t,starterCode:i,code:i}}),E=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let C;const S=E(x);let L=null;if(S)try{L=JSON.parse(S)}catch(e){L=null}C=L&&1===L.v&&Array.isArray(L.tabs)&&L.tabs.length>0?L.tabs.map((e,n)=>{const t=k.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):k.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=E(x+":"+t);if(a||"Code"!==e.label||1!==k.length||(a=E(x)),a&&a.trim())return{...e,code:a}}return e});const j=y.hasAttribute("data-no-watchdog");!function(e,t,a,o,r){e=e.slice();const i=document.createElement("style");i.textContent=u,document.head.appendChild(i),document.body.innerHTML=h;const s=document.getElementById("pf-app"),d=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),m=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),_=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),g=document.getElementById("pf-btn-code"),b=document.getElementById("pf-btn-dl"),v=document.getElementById("pf-btn-rec"),w=document.getElementById("pf-btn-reset"),x=document.getElementById("pf-btn-help"),k=document.getElementById("pf-grip"),E=document.getElementById("pf-handle-hint"),C=document.getElementById("pf-tabs"),S=document.getElementById("pf-markdown-view");let L=!1,j=Math.round(.56*window.innerHeight);function z(){document.documentElement.style.setProperty("--pf-drawer-h",j+"px")}function I(){L=!0,d.classList.add("pf-open"),g.classList.add("pf-active"),setTimeout(()=>{H(),J&&J.focus()},280)}function R(){L=!1,d.classList.remove("pf-open"),g.classList.remove("pf-active"),setTimeout(()=>{H();const e=X._p?.canvas;e&&e.removeAttribute("tabindex"),s.focus()},280)}function P(){L?R():I()}z();let M=null;const A=5,T=120,B=document.createElement("div");function O(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:L?j:0,moved:!1},B.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function W(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>A&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<T?(d.style.transition="none",d.style.height="32px"):(j=a,z(),L||I(),d.style.transition="none",d.style.height=j+"px"),H()}function D(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,o=M.h+a;M=null,B.style.display="none",document.body.style.userSelect="",d.style.transition="",d.style.height="",n&&(o<T?R():(j=Math.max(T,Math.min(window.innerHeight-50,o)),z(),L||I()),H())}Object.assign(B.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(B),k.addEventListener("click",e=>{e.stopPropagation(),P()}),l.addEventListener("mousedown",O,!0),document.addEventListener("mousemove",W),document.addEventListener("mouseup",D),l.addEventListener("touchstart",O,{passive:!1}),document.addEventListener("touchmove",W,{passive:!0}),document.addEventListener("touchend",D);let K=0,N=0;function U(e){_.textContent=e,_.style.display="block",I()}function F(){_.textContent="",_.style.display="none"}function $(){if(!X._p||"fit"!==X._mode)return;const e=X._w,n=X._h;if(!e||!n)return;const t=s.clientWidth,a=s.clientHeight,o=Math.min(t/e,a/n);p.style.transform=`scale(${o})`}function H(){if("fullscreen"===X._mode?X.size("max"):$(),Y&&"function"==typeof Y.windowResized)try{Y.windowResized()}catch(e){U(String(e))}J&&J.resize()}window.addEventListener("mousemove",e=>{K=e.clientX,N=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(K=e.touches[0].clientX,N=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=X._p?X._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=X._w/n.width,a=X._h/n.height;return[(K-n.left)*t,(N-n.top)*a]},window.addEventListener("resize",H);let Y=null;const X=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=s.clientWidth,this._h=s.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),p.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),$())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){E.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function G(){if(Ae(),Y){try{Y.remove()}catch(e){}Y=null}c.innerHTML="",X._p=null,X._mode="fit",X._w=0,X._h=0,p.style.transform="scale(1)",E.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",he&&(he.destroy(),he=null),fe&&(fe.destroy(),fe=null),ue&&(ue.destroy(),ue=null),_e&&(_e.destroy(),_e=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),be&&(be.destroy(),be=null),ve&&(ve.destroy(),ve=null),we&&(we.destroy(),we=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null)}window.p5py=X;let J=null,V=null;const q={},Z=new Set;function Q(){C.innerHTML="",V=null;const n=e.filter(e=>!e.hidden);C.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>ee(e)),C.appendChild(n)}),n.length>0&&ee(n[0],!0)}function ee(e,n){if(n||V!==e)if(V=e,C.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",S.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</div>`)),S.innerHTML=`<div class="pf-md-inner">${n}</div>`}else S.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:S.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",S.style.display="none",J&&q[e.id]&&(J.setSession(q[e.id]),J.setReadOnly(e.readonly),J.focus())}function ne(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!q[e.id]?n+=e.code.split("\n").length:(q[e.id].setOption("firstLineNumber",n),n+=q[e.id].getLength())})}function te(){Object.keys(q).forEach(e=>delete q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),Z.delete(e)),e=setTimeout(()=>{Z.delete(e),e=null,oe()},350),Z.add(e),ne(),ie()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);J&&n&&q[n.id]&&(J.setSession(q[n.id]),J.setReadOnly(n.readonly),J.renderer.updateFull(!0)),ne()}function ae(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),J=ace.edit("pf-ace"),J.setTheme("ace/theme/monokai"),J.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),J.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{J.completer?.popup?.isOpen||je()}}),J.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:R}),J.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:re}),J.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}}),te(),Q(),ie()}function oe(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!q[e.id]?e.code:q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function re(){oe()}function ie(){const n=e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&q[e.id]&&q[e.id].getValue()!==e.starterCode);w.classList.toggle("pf-dirty",n)}function se(){Z.forEach(e=>clearTimeout(e)),Z.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),te(),Q(),ie(),je()}window.addEventListener("beforeunload",re);let de=null,le=null;async function ce(){return le||(le=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),de=await loadPyodide(e),de.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n fn()\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n fn()\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),J){pe(de.runPython("list(m.__all__)").toJs())}})(),le)}function pe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,o,r){r(null,o.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,J.completers=[...J.completers||[],t]}let me=!1,fe=null,ue=null,he=null,_e=null,ye=null,ge=null,be=null,ve=null,we=null,xe=null,ke=null,Ee=null,Ce=null,Se=null;const Le=300;async function je(){if(me)return;me=!0,y.classList.add("pf-running"),F(),G(),de||(f.textContent="Initialisation de Pyodide…",m.style.display="flex");try{await ce()}catch(e){return m.style.display="none",U("Erreur Pyodide : "+e),me=!1,void y.classList.remove("pf-running")}m.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!q[e.id]?e.code:q[e.id].getValue()).join("\n");try{f.textContent="Chargement des dépendances…",m.style.display="flex",await de.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}m.style.display="none",de.globals.set("_USER_CODE",t);try{de.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),de.runPython("_ns_ref[0] = _ns")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}let a,o,i,s,d,l,p,u,h,_,g,b,v,w;try{const e=(e,n)=>de.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),o=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),u=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),b=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),w=e("touchEnded","touch_ended")}catch(e){return U(String(e)),me=!1,void y.classList.remove("pf-running")}if(!o)return U("Le script doit définir au moins une fonction draw()."),me=!1,void y.classList.remove("pf-running");const{create_proxy:x}=de.pyimport("pyodide.ffi"),k=de.runPython("_ns.get('windowResized')"),E=de.globals.get("_pf_refresh"),C=de.globals.get(r?"_pf_draw_direct":"_pf_draw_watchdog"),S=de.globals.get("_ns"),L=e=>e?x(()=>{try{E(S),e()}catch(e){U(String(e))}}):null;he=d?x(()=>{try{d()}catch(e){U(String(e))}}):null,fe=a?x(()=>{try{a()}catch(e){U(String(e))}}):null,ue=x(()=>{try{E(S),C(o,Le)}catch(e){const n=String(e);G(),n.includes("TimeoutError")||n.includes("watchdog")?U(`draw() a dépassé ${Le}ms — sketch arrêté (watchdog).`):U(n)}}),_e=L(i),ye=L(p),ge=L(l),be=L(u),ve=L(h),we=L(_),xe=L(s),ke=L(g),Ee=L(b),Ce=L(v),Se=L(w);const j=k?x(()=>{try{k()}catch(e){U(String(e))}}):null;let z=!1;Y=new p5(e=>{X._setP(e),he&&(e.preload=()=>{he()}),e.setup=()=>{fe&&fe(),e.canvas||X.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&ue()},e.mousePressed=()=>{z&&_e&&_e()},e.mouseReleased=()=>{z&&ye&&ye()},e.mouseDragged=()=>{z&&ge&&ge()},e.mouseMoved=()=>{z&&be&&be()},e.mouseWheel=e=>{z&&ve&&ve()},e.doubleClicked=()=>{z&&we&&we()},e.keyPressed=()=>{z&&xe&&xe()},e.keyReleased=()=>{z&&ke&&ke()},Ee&&(e.touchStarted=()=>{z&&Ee()}),Ce&&(e.touchMoved=()=>{z&&Ce()}),Se&&(e.touchEnded=()=>{z&&Se()}),e.windowResized=()=>{"fullscreen"===X._mode?X.size("max"):$(),j&&j()}},c),me=!1,y.classList.remove("pf-running")}const ze='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.5.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Ie(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!q[e.id]?e.code:q[e.id].getValue();const a=[],o="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,""")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${o}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=ze.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(a),r=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(o)}let Re=null,Pe=[];function Me(){const e=X._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Re=new MediaRecorder(t,{mimeType:n}),Pe=[],Re.ondataavailable=e=>{e.data.size&&Pe.push(e.data)},Re.onstop=()=>{const e=new Blob(Pe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),v.textContent="⏺",v.title="Enregistrer WebM",v.classList.remove("pf-recording"),Re=null},Re.start(),v.textContent="⏹",v.title="Arrêter l'enregistrement",v.classList.add("pf-recording")}function Ae(){Re&&"inactive"!==Re.state&&Re.stop()}v.addEventListener("click",()=>{Re?Ae():Me()}),y.addEventListener("click",()=>je()),g.addEventListener("click",()=>{L?R():(j=window.innerHeight-32,z(),I())}),b.addEventListener("click",Ie);const Te="https://codeberg.org/nopid/pyfrilet";function Be(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}x.addEventListener("click",()=>window.open(Te,"_blank")),w.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&se()}),window.addEventListener("keydown",e=>{const n=L&&J&&J.isFocused&&J.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void je();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),J.searchBox?J.searchBox.hide():t.style.display="none",void J.focus();if(n){const n=J.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void R()}return e.preventDefault(),e.stopPropagation(),void(L?R():I())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&se())):(e.preventDefault(),void re())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",m.style.display="flex";try{if(await Be(o.p5),o.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=o.katexCss,document.head.appendChild(e),await Be(o.marked),await Be(o.katex),await Be(o.markedKatex),await Be(o.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Be(o.ace),await Be(o.acePython),await Be(o.aceMonokai),await Be(o.aceLangTools),await Be(o.aceSearchbox),await Be(o.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}ae(),await je(),m.style.display="none"})()}(C,k,x,w,j)})}();
|