pyfrilet 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Un sketch est un fichier HTML minimal avec son code Python embarqué.
15
15
  <head>
16
16
  <meta charset="utf-8">
17
17
  <title>Mon sketch</title>
18
- <script src="pyfrilet.js"></script>
18
+ <script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
19
19
  </head>
20
20
  <body>
21
21
  <script type="text/python" data-sources="cdn">
@@ -77,17 +77,28 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
77
77
 
78
78
  | Fonction | Rôle |
79
79
  |---|---|
80
+ | `preload()` | Appelée avant `setup()`. Utilisée pour charger des ressources (`loadImage`…) — p5 attend la fin du chargement avant de continuer. |
80
81
  | `setup()` | Appelée une seule fois au démarrage. Obligatoire pour appeler `size()` ou `frameRate()`. |
81
82
  | `draw()` | Appelée à chaque frame. **Obligatoire.** |
82
- | `mousePressed()` | Appelée à chaque clic sur le canvas. |
83
- | `keyPressed()` | Appelée à chaque pression de touche. `keyCode` et `key` sont déjà à jour au moment de l'appel. |
84
83
  | `windowResized()` | Appelée quand la zone d'affichage change (redimensionnement de la fenêtre ou du tiroir éditeur). |
84
+ | `mousePressed()` | Bouton souris enfoncé. |
85
+ | `mouseReleased()` | Bouton souris relâché. |
86
+ | `mouseDragged()` | Souris déplacée bouton enfoncé. |
87
+ | `mouseMoved()` | Souris déplacée sans bouton (survol). |
88
+ | `mouseWheel()` | Molette / pinch trackpad. `deltaY` est accessible via `p5py.deltaY` si besoin. |
89
+ | `doubleClicked()` | Double-clic. |
90
+ | `keyPressed()` | Touche enfoncée. `keyCode` et `key` sont déjà à jour au moment de l'appel. |
91
+ | `keyReleased()` | Touche relâchée. Utile pour les mouvements continus dans les jeux. |
92
+ | `touchStarted()` | Début de contact tactile. `touches[]` est accessible via p5. |
93
+ | `touchMoved()` | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
94
+ | `touchEnded()` | Fin de contact tactile. |
85
95
 
86
96
  #### Fonctions pyfrilet (hors p5.js standard)
87
97
 
88
98
  | Fonction | Description |
89
99
  |---|---|
90
100
  | `size(w, h)` | Crée ou redimensionne le canvas. Le canvas est mis à l'échelle CSS pour remplir l'écran sans déformation. |
101
+ | `size(w, h, WEBGL)` | Crée un canvas en mode WebGL 3D. `WEBGL` est une constante p5 importable avec `from p5 import *`. |
91
102
  | `size('max')` | Canvas plein écran, redimensionné dynamiquement. |
92
103
  | `smooth()` | Active l'antialiasing (par défaut). |
93
104
  | `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
@@ -192,20 +203,38 @@ Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque mod
192
203
 
193
204
  ### Télécharger
194
205
 
195
- Le bouton 💾 génère un fichier `sketch.html` autonome : pyfrilet.js y est embarqué sous forme de data URL base64, et le code Python est celui de l'éditeur au moment du clic. Le fichier produit charge p5.js et Pyodide depuis le CDN et n'a besoin d'aucun serveur pour fonctionner. On peut également télécharger l'export d'un export — le résultat est identique.
206
+ Le bouton 💾 génère un fichier `sketch.html` autonome : il référence pyfrilet depuis jsDelivr et embarque le code Python tel qu'il est dans l'éditeur au moment du clic. Le fichier produit charge p5.js et Pyodide depuis le CDN et n'a besoin d'aucun serveur pour fonctionner.
207
+
208
+ ---
209
+
210
+ ## Utiliser pyfrilet depuis le CDN
211
+
212
+ La façon la plus simple d'utiliser pyfrilet est de le charger depuis jsDelivr, sans rien installer :
213
+
214
+ ```html
215
+ <script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
216
+ ```
217
+
218
+ Pour épingler une version précise et éviter les surprises lors d'une montée de version :
219
+
220
+ ```html
221
+ <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.1.0/pyfrilet.min.js"></script>
222
+ ```
223
+
224
+ > **Note** : `@latest` est mis en cache jusqu'à 7 jours par les navigateurs — une version épinglée est préférable dès qu'on destine un sketch à un public.
196
225
 
197
226
  ---
198
227
 
199
228
  ## Déploiement local (mode `vendor/`)
200
229
 
201
- Par défaut (`data-sources="cdn"`), pyfrilet charge ses dépendances depuis des CDN publics, ce qui nécessite une connexion internet. Pour un déploiement entièrement local — intranet, usage hors ligne, ou simplement pour ne pas dépendre de services tiers — on peut héberger les dépendances soi-même.
230
+ Par défaut (`data-sources="cdn"`), pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics, ce qui nécessite une connexion internet. Pour un déploiement entièrement local — intranet, usage hors ligne, ou simplement pour ne pas dépendre de services tiers — on peut héberger les dépendances soi-même.
202
231
 
203
232
  ### Utiliser une copie locale de pyfrilet.js
204
233
 
205
- Dans une page déployée en local, on remplace la balise CDN par une référence locale :
234
+ On remplace la balise jsDelivr par une référence locale :
206
235
 
207
236
  ```html
208
- <!-- au lieu de : <script src="https://…/pyfrilet.js"></script> -->
237
+ <!-- au lieu de : <script src="https://cdn.jsdelivr.net/npm/pyfrilet@…/pyfrilet.min.js"></script> -->
209
238
  <script src="pyfrilet.js"></script>
210
239
  ```
211
240
 
@@ -279,6 +308,30 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
279
308
 
280
309
  ---
281
310
 
311
+ ## Build et publication
312
+
313
+ Le build est géré par `build.js` (Node.js + Terser). Il injecte automatiquement la version courante dans la constante `PYFRILET_CDN` du fichier source, puis génère `pyfrilet.min.js`.
314
+
315
+ ```bash
316
+ npm run build # génère pyfrilet.min.js
317
+ ```
318
+
319
+ Le hook `prepublishOnly` dans `package.json` déclenche le build automatiquement avant chaque `npm publish`. Le flux complet d'une release :
320
+
321
+ ```bash
322
+ git add .
323
+ git commit -m "feat: ..." # committer le travail
324
+
325
+ npm version patch # (ou minor / major) — modifie package.json, commit + tag
326
+ npm publish # build automatique puis publication sur npm
327
+
328
+ git push && git push --tags # pousser commits et tag sur Codeberg
329
+ ```
330
+
331
+ `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).
332
+
333
+ ---
334
+
282
335
  ## Licence
283
336
 
284
337
  pyfrilet.js est distribué sous **GNU Lesser General Public License v2.1** (LGPL-2.1).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -562,10 +562,20 @@ function main(initialCode, starterCode, SK, URLS) {
562
562
  p5Bridge._p = null; p5Bridge._mode = 'fit'; p5Bridge._w = 0; p5Bridge._h = 0;
563
563
  viewEl.style.transform = 'scale(1)';
564
564
  hintEl.textContent = 'Shift+Entrée → relancer \u00a0·\u00a0 Échap → ouvrir/fermer';
565
- if (setupProxy) { setupProxy.destroy(); setupProxy = null; }
566
- if (drawProxy) { drawProxy.destroy(); drawProxy = null; }
567
- if (mousePressedProxy) { mousePressedProxy.destroy(); mousePressedProxy = null; }
568
- if (keyPressedProxy) { keyPressedProxy.destroy(); keyPressedProxy = null; }
565
+ if (preloadProxy) { preloadProxy.destroy(); preloadProxy = null; }
566
+ if (setupProxy) { setupProxy.destroy(); setupProxy = null; }
567
+ if (drawProxy) { drawProxy.destroy(); drawProxy = null; }
568
+ if (mousePressedProxy) { mousePressedProxy.destroy(); mousePressedProxy = null; }
569
+ if (mouseReleasedProxy) { mouseReleasedProxy.destroy(); mouseReleasedProxy = null; }
570
+ if (mouseDraggedProxy) { mouseDraggedProxy.destroy(); mouseDraggedProxy = null; }
571
+ if (mouseMovedProxy) { mouseMovedProxy.destroy(); mouseMovedProxy = null; }
572
+ if (mouseWheelProxy) { mouseWheelProxy.destroy(); mouseWheelProxy = null; }
573
+ if (doubleClickedProxy) { doubleClickedProxy.destroy(); doubleClickedProxy = null; }
574
+ if (keyPressedProxy) { keyPressedProxy.destroy(); keyPressedProxy = null; }
575
+ if (keyReleasedProxy) { keyReleasedProxy.destroy(); keyReleasedProxy = null; }
576
+ if (touchStartedProxy) { touchStartedProxy.destroy(); touchStartedProxy = null; }
577
+ if (touchMovedProxy) { touchMovedProxy.destroy(); touchMovedProxy = null; }
578
+ if (touchEndedProxy) { touchEndedProxy.destroy(); touchEndedProxy = null; }
569
579
  }
570
580
 
571
581
  /* ─────────────────── ACE EDITOR ─────────────── */
@@ -785,7 +795,12 @@ sys.modules["p5"] = m
785
795
 
786
796
  /* ─────────────────── RUN CODE ───────────────── */
787
797
  let running = false;
788
- let setupProxy = null, drawProxy = null, mousePressedProxy = null, keyPressedProxy = null;
798
+ let setupProxy = null, drawProxy = null,
799
+ preloadProxy = null,
800
+ mousePressedProxy = null, mouseReleasedProxy = null, mouseDraggedProxy = null,
801
+ mouseMovedProxy = null, mouseWheelProxy = null, doubleClickedProxy = null,
802
+ keyPressedProxy = null, keyReleasedProxy = null,
803
+ touchStartedProxy = null, touchMovedProxy = null, touchEndedProxy = null;
789
804
 
790
805
  async function runCode() {
791
806
  if (running) return;
@@ -819,12 +834,23 @@ sys.modules["p5"] = m
819
834
  running = false; btnRun.classList.remove('pf-running'); return;
820
835
  }
821
836
 
822
- let pySetup, pyDraw, pyMP, pyKP;
837
+ let pySetup, pyDraw, pyMP, pyKP, pyPreload, pyMD, pyMR,
838
+ pyMM, pyMW, pyDC, pyKR, pyTS, pyTM, pyTE;
823
839
  try {
824
- pySetup = pyodide.runPython("_ns.get('setup')");
825
- pyDraw = pyodide.runPython("_ns.get('draw')");
826
- pyMP = pyodide.runPython("_ns.get('mousePressed')");
827
- pyKP = pyodide.runPython("_ns.get('keyPressed')");
840
+ pyPreload = pyodide.runPython("_ns.get('preload')");
841
+ pySetup = pyodide.runPython("_ns.get('setup')");
842
+ pyDraw = pyodide.runPython("_ns.get('draw')");
843
+ pyMP = pyodide.runPython("_ns.get('mousePressed')");
844
+ pyKP = pyodide.runPython("_ns.get('keyPressed')");
845
+ pyMD = pyodide.runPython("_ns.get('mouseDragged')");
846
+ pyMR = pyodide.runPython("_ns.get('mouseReleased')");
847
+ pyMM = pyodide.runPython("_ns.get('mouseMoved')");
848
+ pyMW = pyodide.runPython("_ns.get('mouseWheel')");
849
+ pyDC = pyodide.runPython("_ns.get('doubleClicked')");
850
+ pyKR = pyodide.runPython("_ns.get('keyReleased')");
851
+ pyTS = pyodide.runPython("_ns.get('touchStarted')");
852
+ pyTM = pyodide.runPython("_ns.get('touchMoved')");
853
+ pyTE = pyodide.runPython("_ns.get('touchEnded')");
828
854
  } catch (e) {
829
855
  showError(String(e));
830
856
  running = false; btnRun.classList.remove('pf-running'); return;
@@ -840,21 +866,41 @@ sys.modules["p5"] = m
840
866
  const pyRefresh = pyodide.globals.get('_pf_refresh');
841
867
  const pyNs = pyodide.globals.get('_ns');
842
868
 
843
- setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
844
- drawProxy = create_proxy(() => {
845
- try { pyRefresh(pyNs); pyDraw(); } catch (e) { showError(String(e)); stopSketch(); }
846
- });
847
- mousePressedProxy = pyMP ? create_proxy(() => {
848
- try { pyRefresh(pyNs); pyMP(); } catch (e) { showError(String(e)); }
849
- }) : null;
850
- keyPressedProxy = pyKP ? create_proxy(() => {
851
- try { pyRefresh(pyNs); pyKP(); } catch (e) { showError(String(e)); }
869
+ const mkProxy = (fn) => fn ? create_proxy(() => {
870
+ try { pyRefresh(pyNs); fn(); } catch (e) { showError(String(e)); }
852
871
  }) : null;
872
+
873
+ preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
874
+ setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
875
+ const DRAW_TIMEOUT_MS = 200;
876
+ drawProxy = create_proxy(() => {
877
+ try {
878
+ pyRefresh(pyNs);
879
+ const t0 = performance.now();
880
+ pyDraw();
881
+ if (performance.now() - t0 > DRAW_TIMEOUT_MS) {
882
+ stopSketch();
883
+ showError(`draw() a mis plus de ${DRAW_TIMEOUT_MS} ms — sketch arrêté pour protéger le navigateur.`);
884
+ }
885
+ } catch (e) { showError(String(e)); stopSketch(); }
886
+ });
887
+ mousePressedProxy = mkProxy(pyMP);
888
+ mouseReleasedProxy = mkProxy(pyMR);
889
+ mouseDraggedProxy = mkProxy(pyMD);
890
+ mouseMovedProxy = mkProxy(pyMM);
891
+ mouseWheelProxy = mkProxy(pyMW);
892
+ doubleClickedProxy = mkProxy(pyDC);
893
+ keyPressedProxy = mkProxy(pyKP);
894
+ keyReleasedProxy = mkProxy(pyKR);
895
+ touchStartedProxy = mkProxy(pyTS);
896
+ touchMovedProxy = mkProxy(pyTM);
897
+ touchEndedProxy = mkProxy(pyTE);
853
898
  const windowResizedProxy = pyWR ? create_proxy(() => { try { pyWR(); } catch (e) { showError(String(e)); } }) : null;
854
899
 
855
900
  let setupDone = false;
856
901
  pInst = new p5((p) => {
857
902
  p5Bridge._setP(p);
903
+ if (preloadProxy) p.preload = () => { preloadProxy(); };
858
904
  p.setup = () => {
859
905
  /* Let the user's setup() call size() to create the canvas — this
860
906
  allows WEBGL mode and custom dimensions to work correctly.
@@ -870,9 +916,18 @@ sys.modules["p5"] = m
870
916
  p.windowResized();
871
917
  setupDone = true;
872
918
  };
873
- p.draw = () => { if (setupDone) drawProxy(); };
874
- p.mousePressed = () => { if (setupDone && mousePressedProxy) mousePressedProxy(); };
875
- p.keyPressed = () => { if (setupDone && keyPressedProxy) keyPressedProxy(); };
919
+ p.draw = () => { if (setupDone) drawProxy(); };
920
+ p.mousePressed = () => { if (setupDone && mousePressedProxy) mousePressedProxy(); };
921
+ p.mouseReleased = () => { if (setupDone && mouseReleasedProxy) mouseReleasedProxy(); };
922
+ p.mouseDragged = () => { if (setupDone && mouseDraggedProxy) mouseDraggedProxy(); };
923
+ p.mouseMoved = () => { if (setupDone && mouseMovedProxy) mouseMovedProxy(); };
924
+ p.mouseWheel = (e) => { if (setupDone && mouseWheelProxy) mouseWheelProxy(); };
925
+ p.doubleClicked = () => { if (setupDone && doubleClickedProxy) doubleClickedProxy(); };
926
+ p.keyPressed = () => { if (setupDone && keyPressedProxy) keyPressedProxy(); };
927
+ p.keyReleased = () => { if (setupDone && keyReleasedProxy) keyReleasedProxy(); };
928
+ p.touchStarted = () => { if (setupDone && touchStartedProxy) touchStartedProxy(); };
929
+ p.touchMoved = () => { if (setupDone && touchMovedProxy) touchMovedProxy(); };
930
+ p.touchEnded = () => { if (setupDone && touchEndedProxy) touchEndedProxy(); };
876
931
  /* Called by p5 on actual window resize AND by notifyResize() */
877
932
  p.windowResized = () => {
878
933
  if (p5Bridge._mode === 'fullscreen') p5Bridge.size('max');
@@ -886,7 +941,7 @@ sys.modules["p5"] = m
886
941
  }
887
942
 
888
943
  /* ─────────────────── DOWNLOAD ───────────────── */
889
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.2.1/pyfrilet.min.js';
944
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js';
890
945
 
891
946
  const STANDALONE_TEMPLATE = `<!doctype html>
892
947
  <html lang="fr">
package/pyfrilet.min.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";const e="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",n="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",t="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",o="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",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",r="\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 {\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-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\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">\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 &nbsp;·&nbsp; 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)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</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)">&#8635;</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 l=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!l)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const c=(l.getAttribute("data-sources")||l.getAttribute("sources")||"local").toLowerCase().trim(),p=(l.getAttribute("data-vendor")||l.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/"),u="cdn"===c?{p5:e,pyodide:n,pyodideIndex:null,ace:t,acePython:i,aceMonokai:o,aceLangTools:s,aceSearchbox:a}:{p5:p+"p5.min.js",pyodide:p+"pyodide/pyodide.js",pyodideIndex:p+"pyodide/",ace:p+"ace.min.js",acePython:p+"mode-python.min.js",aceMonokai:p+"theme-monokai.min.js",aceLangTools:p+"ext-language_tools.min.js",aceSearchbox:p+"ext-searchbox.min.js"},f=l.textContent.replace(/^\n/,""),m="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(m)}catch(e){return null}})();!function(e,n,t,i){const o=document.createElement("style");o.textContent=r,document.head.appendChild(o),document.body.innerHTML=d;const s=document.getElementById("pf-app"),a=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),m=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),_=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-reset"),b=document.getElementById("pf-btn-help"),v=document.getElementById("pf-grip"),x=document.getElementById("pf-handle-hint");let w=!1,k=Math.round(.56*window.innerHeight);function E(){document.documentElement.style.setProperty("--pf-drawer-h",k+"px")}function L(){w=!0,a.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{K(),W&&W.focus()},280)}function C(){w=!1,a.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{K();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):s.focus()},280)}function S(){w?C():L()}E();let j=null;const z=5,R=120,P=document.createElement("div");function I(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:w?k:0,moved:!1},P.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function M(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>z&&(j.moved=!0),!j.moved)return;const i=Math.max(0,Math.min(window.innerHeight-50,j.h+t));i<R?(a.style.transition="none",a.style.height="32px"):(k=i,E(),w||L(),a.style.transition="none",a.style.height=k+"px"),K()}function T(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,i=j.y-t,o=j.h+i;j=null,P.style.display="none",document.body.style.userSelect="",a.style.transition="",a.style.height="",n&&(o<R?C():(k=Math.max(R,Math.min(window.innerHeight-50,o)),E(),w||L()),K())}Object.assign(P.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(P),v.addEventListener("click",e=>{e.stopPropagation(),S()}),l.addEventListener("mousedown",I,!0),document.addEventListener("mousemove",M),document.addEventListener("mouseup",T),l.addEventListener("touchstart",I,{passive:!1}),document.addEventListener("touchmove",M,{passive:!0}),document.addEventListener("touchend",T);let B=0,O=0;function A(e){m.textContent=e,m.style.display="block",L()}function D(){m.textContent="",m.style.display="none"}function Y(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=s.clientWidth,i=s.clientHeight,o=Math.min(t/e,i/n);p.style.transform=`scale(${o})`}function K(){if("fullscreen"===H._mode?H.size("max"):Y(),F&&"function"==typeof F.windowResized)try{F.windowResized()}catch(e){}W&&W.resize()}window.addEventListener("mousemove",e=>{B=e.clientX,O=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(B=e.touches[0].clientX,O=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=H._p?H._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=H._w/n.width,i=H._h/n.height;return[(B-n.left)*t,(O-n.top)*i]},window.addEventListener("resize",K);let F=null;const H=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const i=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=s.clientWidth,this._h=s.clientHeight,void 0===i&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,i),p.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===i&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,i),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){x.textContent=String(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 U(){if(F){try{F.remove()}catch(e){}F=null}c.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,p.style.transform="scale(1)",x.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",Q&&(Q.destroy(),Q=null),Z&&(Z.destroy(),Z=null),ee&&(ee.destroy(),ee=null),ne&&(ne.destroy(),ne=null)}window.p5py=H;let W=null;function V(){!i.ace.startsWith("vendor")&&i.ace.startsWith("http")||ace.config.set("basePath",i.ace.replace(/\/[^/]+$/,"/")),W=ace.edit("pf-ace"),W.session.setMode("ace/mode/python"),W.setTheme("ace/theme/monokai"),W.setValue(e,-1),W.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),W.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{te()}}),W.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:C}),W.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:X}),W.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te())}});let t=null;W.session.on("change",()=>{clearTimeout(t),t=setTimeout(X,350)})}function X(){try{localStorage.setItem(t,W?W.getValue():e)}catch(e){}}window.addEventListener("beforeunload",X);let N=null,J=null;async function q(){return J||(J=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),N=await loadPyodide(e),N.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 ('size', 'sketchTitle'):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\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 * (after all explicit additions)\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\n# ── _pf_refresh: called before every event callback ───────────────\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 if _k in _MOUSE_OVERRIDE:\n _v = mx if _k == 'mouseX' else my\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\nsys.modules[\"p5\"] = m\n"),W){G(N.runPython("list(m.__all__)").toJs())}})(),J)}function G(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,i,o,s){s(null,o.length>0?n:[])}},i=ace.require("ace/ext/language_tools");i&&Array.isArray(i.completers)&&(i.completers=i.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,W.completers=[...W.completers||[],t]}let $=!1,Q=null,Z=null,ee=null,ne=null;async function te(){if($)return;$=!0,h.classList.add("pf-running"),D(),U(),N||(f.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await q()}catch(e){return u.style.display="none",A("Erreur Pyodide : "+e),$=!1,void h.classList.remove("pf-running")}u.style.display="none";const n=W?W.getValue():e;N.globals.set("_USER_CODE",n);try{N.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return A(String(e)),$=!1,void h.classList.remove("pf-running")}let t,i,o,s;try{t=N.runPython("_ns.get('setup')"),i=N.runPython("_ns.get('draw')"),o=N.runPython("_ns.get('mousePressed')"),s=N.runPython("_ns.get('keyPressed')")}catch(e){return A(String(e)),$=!1,void h.classList.remove("pf-running")}if(!i)return A("Le script doit définir au moins une fonction draw()."),$=!1,void h.classList.remove("pf-running");const{create_proxy:a}=N.pyimport("pyodide.ffi"),r=N.runPython("_ns.get('windowResized')"),d=N.globals.get("_pf_refresh"),l=N.globals.get("_ns");Q=t?a(()=>{try{t()}catch(e){A(String(e))}}):null,Z=a(()=>{try{d(l),i()}catch(e){A(String(e)),U()}}),ee=o?a(()=>{try{d(l),o()}catch(e){A(String(e))}}):null,ne=s?a(()=>{try{d(l),s()}catch(e){A(String(e))}}):null;const p=r?a(()=>{try{r()}catch(e){A(String(e))}}):null;let m=!1;F=new p5(e=>{H._setP(e),e.setup=()=>{Q&&Q(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),m=!0},e.draw=()=>{m&&Z()},e.mousePressed=()=>{m&&ee&&ee()},e.keyPressed=()=>{m&&ne&&ne()},e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):Y(),p&&p()}},c),$=!1,h.classList.remove("pf-running")}const ie='<!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.2.1/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 oe(){const n=W?W.getValue():e,t=ie.replace("FILLME-PYTHON",n),i=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(i),s=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(o)}h.addEventListener("click",()=>te()),y.addEventListener("click",()=>{w?C():(k=window.innerHeight-32,E(),L())}),_.addEventListener("click",oe);const se="https://codeberg.org/nopid/pyfrilet";function ae(e){return new Promise((n,t)=>{const i=document.createElement("script");i.src=e,i.onload=n,i.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(i)})}b.addEventListener("click",()=>window.open(se,"_blank")),g.addEventListener("click",()=>{W&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te())}),window.addEventListener("keydown",e=>{const t=w&&W&&W.isFocused&&W.isFocused();if(t||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void te();if("Escape"===e.key)return t?void setTimeout(()=>{w&&C()},0):(e.preventDefault(),e.stopPropagation(),void(w?C():L()));if(!t)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(W&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te()))):(e.preventDefault(),void X())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",u.style.display="flex";try{await ae(i.p5),await ae(i.ace),await ae(i.acePython),await ae(i.aceMonokai),await ae(i.aceLangTools),await ae(i.aceSearchbox),await ae(i.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}V(),await te(),u.style.display="none"})()}(h&&h.trim()?h:f,f,m,u)})}();
1
+ !function(){"use strict";const e="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",n="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",t="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",a="\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 {\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-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\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">\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 &nbsp;·&nbsp; 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)">&#9654;</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</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)">&#8635;</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 l=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!l)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const c=(l.getAttribute("data-sources")||l.getAttribute("sources")||"local").toLowerCase().trim(),p=(l.getAttribute("data-vendor")||l.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/"),u="cdn"===c?{p5:e,pyodide:n,pyodideIndex:null,ace:t,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:r}:{p5:p+"p5.min.js",pyodide:p+"pyodide/pyodide.js",pyodideIndex:p+"pyodide/",ace:p+"ace.min.js",acePython:p+"mode-python.min.js",aceMonokai:p+"theme-monokai.min.js",aceLangTools:p+"ext-language_tools.min.js",aceSearchbox:p+"ext-searchbox.min.js"},f=l.textContent.replace(/^\n/,""),m="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(m)}catch(e){return null}})();!function(e,n,t,o){const i=document.createElement("style");i.textContent=a,document.head.appendChild(i),document.body.innerHTML=d;const s=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),m=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),_=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-reset"),b=document.getElementById("pf-btn-help"),v=document.getElementById("pf-grip"),x=document.getElementById("pf-handle-hint");let w=!1,k=Math.round(.56*window.innerHeight);function E(){document.documentElement.style.setProperty("--pf-drawer-h",k+"px")}function L(){w=!0,r.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{Y(),U&&U.focus()},280)}function C(){w=!1,r.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{Y();const e=F._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):s.focus()},280)}function S(){w?C():L()}E();let P=null;const j=5,R=120,z=document.createElement("div");function I(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;P={y:n,h:w?k:0,moved:!1},z.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function M(e){if(!P)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=P.y-n;if(Math.abs(t)>j&&(P.moved=!0),!P.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,P.h+t));o<R?(r.style.transition="none",r.style.height="32px"):(k=o,E(),w||L(),r.style.transition="none",r.style.height=k+"px"),Y()}function T(e){if(!P)return;const n=P.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??P.y,o=P.y-t,i=P.h+o;P=null,z.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(i<R?C():(k=Math.max(R,Math.min(window.innerHeight-50,i)),E(),w||L()),Y())}Object.assign(z.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(z),v.addEventListener("click",e=>{e.stopPropagation(),S()}),l.addEventListener("mousedown",I,!0),document.addEventListener("mousemove",M),document.addEventListener("mouseup",T),l.addEventListener("touchstart",I,{passive:!1}),document.addEventListener("touchmove",M,{passive:!0}),document.addEventListener("touchend",T);let B=0,O=0;function A(e){m.textContent=e,m.style.display="block",L()}function D(){m.textContent="",m.style.display="none"}function W(){if(!F._p||"fit"!==F._mode)return;const e=F._w,n=F._h;if(!e||!n)return;const t=s.clientWidth,o=s.clientHeight,i=Math.min(t/e,o/n);p.style.transform=`scale(${i})`}function Y(){if("fullscreen"===F._mode?F.size("max"):W(),K&&"function"==typeof K.windowResized)try{K.windowResized()}catch(e){}U&&U.resize()}window.addEventListener("mousemove",e=>{B=e.clientX,O=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(B=e.touches[0].clientX,O=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=F._p?F._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=F._w/n.width,o=F._h/n.height;return[(B-n.left)*t,(O-n.top)*o]},window.addEventListener("resize",Y);let K=null;const F=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=s.clientWidth,this._h=s.clientHeight,void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),p.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),W())},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){x.textContent=String(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(K){try{K.remove()}catch(e){}K=null}c.innerHTML="",F._p=null,F._mode="fit",F._w=0,F._h=0,p.style.transform="scale(1)",x.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",ee&&(ee.destroy(),ee=null),Q&&(Q.destroy(),Q=null),Z&&(Z.destroy(),Z=null),ne&&(ne.destroy(),ne=null),te&&(te.destroy(),te=null),oe&&(oe.destroy(),oe=null),ie&&(ie.destroy(),ie=null),se&&(se.destroy(),se=null),re&&(re.destroy(),re=null),ae&&(ae.destroy(),ae=null),de&&(de.destroy(),de=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null)}window.p5py=F;let U=null;function V(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),U=ace.edit("pf-ace"),U.session.setMode("ace/mode/python"),U.setTheme("ace/theme/monokai"),U.setValue(e,-1),U.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),U.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{ue()}}),U.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:C}),U.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:X}),U.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue())}});let t=null;U.session.on("change",()=>{clearTimeout(t),t=setTimeout(X,350)})}function X(){try{localStorage.setItem(t,U?U.getValue():e)}catch(e){}}window.addEventListener("beforeunload",X);let N=null,J=null;async function $(){return J||(J=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),N=await loadPyodide(e),N.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 ('size', 'sketchTitle'):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\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 * (after all explicit additions)\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\n# ── _pf_refresh: called before every event callback ───────────────\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 if _k in _MOUSE_OVERRIDE:\n _v = mx if _k == 'mouseX' else my\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\nsys.modules[\"p5\"] = m\n"),U){q(N.runPython("list(m.__all__)").toJs())}})(),J)}function q(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,o,i,s){s(null,i.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,U.completers=[...U.completers||[],t]}let G=!1,Q=null,Z=null,ee=null,ne=null,te=null,oe=null,ie=null,se=null,re=null,ae=null,de=null,le=null,ce=null,pe=null;async function ue(){if(G)return;G=!0,h.classList.add("pf-running"),D(),H(),N||(f.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await $()}catch(e){return u.style.display="none",A("Erreur Pyodide : "+e),G=!1,void h.classList.remove("pf-running")}u.style.display="none";const n=U?U.getValue():e;N.globals.set("_USER_CODE",n);try{N.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return A(String(e)),G=!1,void h.classList.remove("pf-running")}let t,o,i,s,r,a,d,l,p,m,y,_,g,b;try{r=N.runPython("_ns.get('preload')"),t=N.runPython("_ns.get('setup')"),o=N.runPython("_ns.get('draw')"),i=N.runPython("_ns.get('mousePressed')"),s=N.runPython("_ns.get('keyPressed')"),a=N.runPython("_ns.get('mouseDragged')"),d=N.runPython("_ns.get('mouseReleased')"),l=N.runPython("_ns.get('mouseMoved')"),p=N.runPython("_ns.get('mouseWheel')"),m=N.runPython("_ns.get('doubleClicked')"),y=N.runPython("_ns.get('keyReleased')"),_=N.runPython("_ns.get('touchStarted')"),g=N.runPython("_ns.get('touchMoved')"),b=N.runPython("_ns.get('touchEnded')")}catch(e){return A(String(e)),G=!1,void h.classList.remove("pf-running")}if(!o)return A("Le script doit définir au moins une fonction draw()."),G=!1,void h.classList.remove("pf-running");const{create_proxy:v}=N.pyimport("pyodide.ffi"),x=N.runPython("_ns.get('windowResized')"),w=N.globals.get("_pf_refresh"),k=N.globals.get("_ns"),E=e=>e?v(()=>{try{w(k),e()}catch(e){A(String(e))}}):null;ee=r?v(()=>{try{r()}catch(e){A(String(e))}}):null,Q=t?v(()=>{try{t()}catch(e){A(String(e))}}):null;const L=200;Z=v(()=>{try{w(k);const e=performance.now();o(),performance.now()-e>L&&(H(),A(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){A(String(e)),H()}}),ne=E(i),te=E(d),oe=E(a),ie=E(l),se=E(p),re=E(m),ae=E(s),de=E(y),le=E(_),ce=E(g),pe=E(b);const C=x?v(()=>{try{x()}catch(e){A(String(e))}}):null;let S=!1;K=new p5(e=>{F._setP(e),ee&&(e.preload=()=>{ee()}),e.setup=()=>{Q&&Q(),e.canvas||F.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),S=!0},e.draw=()=>{S&&Z()},e.mousePressed=()=>{S&&ne&&ne()},e.mouseReleased=()=>{S&&te&&te()},e.mouseDragged=()=>{S&&oe&&oe()},e.mouseMoved=()=>{S&&ie&&ie()},e.mouseWheel=e=>{S&&se&&se()},e.doubleClicked=()=>{S&&re&&re()},e.keyPressed=()=>{S&&ae&&ae()},e.keyReleased=()=>{S&&de&&de()},e.touchStarted=()=>{S&&le&&le()},e.touchMoved=()=>{S&&ce&&ce()},e.touchEnded=()=>{S&&pe&&pe()},e.windowResized=()=>{"fullscreen"===F._mode?F.size("max"):W(),C&&C()}},c),G=!1,h.classList.remove("pf-running")}const fe='<!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.2.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 me(){const n=U?U.getValue():e,t=fe.replace("FILLME-PYTHON",n),o=new Blob([t],{type:"text/html;charset=utf-8"}),i=URL.createObjectURL(o),s=Object.assign(document.createElement("a"),{href:i,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(i)}h.addEventListener("click",()=>ue()),y.addEventListener("click",()=>{w?C():(k=window.innerHeight-32,E(),L())}),_.addEventListener("click",me);const he="https://codeberg.org/nopid/pyfrilet";function ye(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)})}b.addEventListener("click",()=>window.open(he,"_blank")),g.addEventListener("click",()=>{U&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue())}),window.addEventListener("keydown",e=>{const t=w&&U&&U.isFocused&&U.isFocused();if(t||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void ue();if("Escape"===e.key)return t?void setTimeout(()=>{w&&C()},0):(e.preventDefault(),e.stopPropagation(),void(w?C():L()));if(!t)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(U&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue()))):(e.preventDefault(),void X())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",u.style.display="flex";try{await ye(o.p5),await ye(o.ace),await ye(o.acePython),await ye(o.aceMonokai),await ye(o.aceLangTools),await ye(o.aceSearchbox),await ye(o.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}V(),await ue(),u.style.display="none"})()}(h&&h.trim()?h:f,f,m,u)})}();