pyfrilet 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,7 +85,7 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
85
85
  | `mouseReleased()` | Bouton souris relâché. |
86
86
  | `mouseDragged()` | Souris déplacée bouton enfoncé. |
87
87
  | `mouseMoved()` | Souris déplacée sans bouton (survol). |
88
- | `mouseWheel()` | Molette / pinch trackpad. `deltaY` est accessible via `p5py.deltaY` si besoin. |
88
+ | `mouseWheel()` | Molette / pinch trackpad. `deltaY` est accessible via `p5py._p.deltaY`. |
89
89
  | `doubleClicked()` | Double-clic. |
90
90
  | `keyPressed()` | Touche enfoncée. `keyCode` et `key` sont déjà à jour au moment de l'appel. |
91
91
  | `keyReleased()` | Touche relâchée. Utile pour les mouvements continus dans les jeux. |
@@ -103,6 +103,7 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
103
103
  | `smooth()` | Active l'antialiasing (par défaut). |
104
104
  | `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
105
105
  | `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
106
+ | `getCanvas()` | Retourne le `p5.Element` wrappant le canvas. Utile pour appeler des méthodes p5.Element comme `.drop()`. À appeler dans `setup()` ou après. |
106
107
 
107
108
  #### Propriétés dynamiques
108
109
 
@@ -161,6 +162,61 @@ def draw():
161
162
  text("page " + str(page), 160, 150)
162
163
  ```
163
164
 
165
+ ### Packages Python tiers
166
+
167
+ pyfrilet détecte automatiquement les imports du sketch et charge les packages disponibles dans la distribution Pyodide avant l'exécution. Il n'y a rien à faire :
168
+
169
+ ```python
170
+ from p5 import *
171
+ import numpy as np # chargé automatiquement
172
+ import networkx as nx # chargé automatiquement
173
+
174
+ def setup():
175
+ size(400, 400)
176
+
177
+ def draw():
178
+ background(20)
179
+ ```
180
+
181
+ Un message "Chargement des dépendances…" s'affiche pendant le téléchargement. Seuls les packages inclus dans la [distribution Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) sont supportés (numpy, scipy, pandas, networkx, pillow…). Les packages pip arbitraires ne sont pas supportés.
182
+
183
+ ### Glisser-déposer de fichiers
184
+
185
+ Pour recevoir des fichiers glissés sur le canvas, on utilise `getCanvas().drop()` avec `create_proxy` de Pyodide :
186
+
187
+ ```python
188
+ from p5 import *
189
+ from pyodide.ffi import create_proxy
190
+
191
+ img = None
192
+
193
+ def setup():
194
+ size(400, 400)
195
+ getCanvas().drop(create_proxy(on_drop))
196
+
197
+ def on_drop(file):
198
+ global img
199
+ if not file.type.startswith("image"):
200
+ return
201
+ if img:
202
+ img.remove()
203
+ img = createImg(file.data, "")
204
+ img.hide()
205
+
206
+ def draw():
207
+ background(40)
208
+ if img:
209
+ image(img, 0, 0, width, height)
210
+ ```
211
+
212
+ `create_proxy` est nécessaire pour que Pyodide maintienne la référence à la fonction Python active entre les appels JS. `file.type` contient le type MIME (`"image/png"`…), `file.data` la data URL base64.
213
+
214
+ ### Protection contre les boucles infinies
215
+
216
+ Si `draw()` met plus de 200 ms à s'exécuter, pyfrilet arrête le sketch automatiquement et affiche un message d'erreur. Cela protège le navigateur contre les calculs trop lourds qui bloqueraient l'interface. Pour des traitements lents, il est recommandé de les découper sur plusieurs frames via `frameCount`.
217
+
218
+ ---
219
+
164
220
  ### Note sur `smooth()` / `noSmooth()` et le texte
165
221
 
166
222
  Par défaut le canvas est rendu en mode antialiasé. `noSmooth()` bascule en mode pixel art — formes **et** texte sont pixelisés. Pour mélanger les deux dans le même `draw()` :
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -22,6 +22,7 @@
22
22
  'use strict';
23
23
 
24
24
  /* ═══════════════════════════ CDN URLS ═══════════════════════════════ */
25
+ let isCdn = false; /* set in DOMContentLoaded from data-sources attribute */
25
26
  const CDN = {
26
27
  p5 : 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js',
27
28
  pyodide : 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js',
@@ -259,7 +260,8 @@ document.addEventListener('DOMContentLoaded', function () {
259
260
  );
260
261
  const vp = vpRaw.replace(/\/?$/, '/'); /* ensure trailing slash */
261
262
 
262
- const URLS = sources === 'cdn' ? {
263
+ isCdn = sources === 'cdn';
264
+ const URLS = isCdn ? {
263
265
  p5 : CDN.p5,
264
266
  pyodide : CDN.pyodide,
265
267
  pyodideIndex: null,
@@ -480,7 +482,7 @@ function main(initialCode, starterCode, SK, URLS) {
480
482
  }
481
483
  /* Trigger p5's windowResized handler (which in turn calls the user's callback) */
482
484
  if (pInst && typeof pInst.windowResized === 'function') {
483
- try { pInst.windowResized(); } catch (_) {}
485
+ try { pInst.windowResized(); } catch (e) { showError(String(e)); }
484
486
  }
485
487
  if (aceInst) aceInst.resize();
486
488
  }
@@ -721,6 +723,19 @@ for _n in ('size', 'sketchTitle'):
721
723
  setattr(m, _n, _FW(_n))
722
724
  _p5_functions.add(_n) # keep __all__ consistent
723
725
 
726
+ # getCanvas() — returns the p5.Element wrapping the canvas,
727
+ # so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.
728
+ class _GetCanvasWrapper:
729
+ def __call__(self):
730
+ p = p5py._p
731
+ if p is None:
732
+ raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')
733
+ p.canvas.id = '__pf_canvas__'
734
+ return p.select('#__pf_canvas__')
735
+ def __repr__(self): return '<p5 function getCanvas>'
736
+ setattr(m, 'getCanvas', _GetCanvasWrapper())
737
+ _p5_functions.add('getCanvas')
738
+
724
739
  # mouseX / mouseY: override with our accurate coordinate calculator
725
740
  # (p5's own values are wrong when a CSS-transformed parent is used)
726
741
  _MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})
@@ -825,6 +840,15 @@ sys.modules["p5"] = m
825
840
  loaderEl.style.display = 'none';
826
841
 
827
842
  const code = aceInst ? aceInst.getValue() : initialCode;
843
+
844
+ /* Auto-load any Pyodide-bundled packages the sketch imports. */
845
+ try {
846
+ loaderMsg.textContent = 'Chargement des dépendances…';
847
+ loaderEl.style.display = 'flex';
848
+ await pyodide.loadPackagesFromImports(code, { messageCallback: () => {}, checkIntegrity: isCdn });
849
+ } catch (e) { console.warn('[pyfrilet] loadPackagesFromImports:', e); }
850
+ loaderEl.style.display = 'none';
851
+
828
852
  pyodide.globals.set('_USER_CODE', code);
829
853
 
830
854
  try {
@@ -872,8 +896,17 @@ sys.modules["p5"] = m
872
896
 
873
897
  preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
874
898
  setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
875
- drawProxy = create_proxy(() => {
876
- try { pyRefresh(pyNs); pyDraw(); } catch (e) { showError(String(e)); stopSketch(); }
899
+ const DRAW_TIMEOUT_MS = 200;
900
+ drawProxy = create_proxy(() => {
901
+ try {
902
+ pyRefresh(pyNs);
903
+ const t0 = performance.now();
904
+ pyDraw();
905
+ if (performance.now() - t0 > DRAW_TIMEOUT_MS) {
906
+ stopSketch();
907
+ showError(`draw() a mis plus de ${DRAW_TIMEOUT_MS} ms — sketch arrêté pour protéger le navigateur.`);
908
+ }
909
+ } catch (e) { showError(String(e)); stopSketch(); }
877
910
  });
878
911
  mousePressedProxy = mkProxy(pyMP);
879
912
  mouseReleasedProxy = mkProxy(pyMR);
@@ -932,7 +965,7 @@ sys.modules["p5"] = m
932
965
  }
933
966
 
934
967
  /* ─────────────────── DOWNLOAD ───────────────── */
935
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.2.2/pyfrilet.min.js';
968
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js';
936
969
 
937
970
  const STANDALONE_TEMPLATE = `<!doctype html>
938
971
  <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",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 q(){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){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,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 $=!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($)return;$=!0,h.classList.add("pf-running"),D(),H(),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=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)),$=!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)),$=!1,void h.classList.remove("pf-running")}if(!o)return A("Le script doit définir au moins une fonction draw()."),$=!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,Z=v(()=>{try{w(k),o()}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 L=x?v(()=>{try{x()}catch(e){A(String(e))}}):null;let C=!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(),C=!0},e.draw=()=>{C&&Z()},e.mousePressed=()=>{C&&ne&&ne()},e.mouseReleased=()=>{C&&te&&te()},e.mouseDragged=()=>{C&&oe&&oe()},e.mouseMoved=()=>{C&&ie&&ie()},e.mouseWheel=e=>{C&&se&&se()},e.doubleClicked=()=>{C&&re&&re()},e.keyPressed=()=>{C&&ae&&ae()},e.keyReleased=()=>{C&&de&&de()},e.touchStarted=()=>{C&&le&&le()},e.touchMoved=()=>{C&&ce&&ce()},e.touchEnded=()=>{C&&pe&&pe()},e.windowResized=()=>{"fullscreen"===F._mode?F.size("max"):W(),L&&L()}},c),$=!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.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function 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)})}();
1
+ !function(){"use strict";let e=!1;const n="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",t="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",a="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",d="\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",l='\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 c=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!c)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const p=(c.getAttribute("data-sources")||c.getAttribute("sources")||"local").toLowerCase().trim(),u=(c.getAttribute("data-vendor")||c.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");e="cdn"===p;const f=e?{p5:n,pyodide:t,pyodideIndex:null,ace:o,acePython:i,aceMonokai:s,aceLangTools:a,aceSearchbox:r}:{p5:u+"p5.min.js",pyodide:u+"pyodide/pyodide.js",pyodideIndex:u+"pyodide/",ace:u+"ace.min.js",acePython:u+"mode-python.min.js",aceMonokai:u+"theme-monokai.min.js",aceLangTools:u+"ext-language_tools.min.js",aceSearchbox:u+"ext-searchbox.min.js"},m=c.textContent.replace(/^\n/,""),h="pyfrilet:"+location.pathname,y=(()=>{try{return localStorage.getItem(h)}catch(e){return null}})();!function(n,t,o,i){const s=document.createElement("style");s.textContent=d,document.head.appendChild(s),document.body.innerHTML=l;const a=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),c=document.getElementById("pf-handle"),p=document.getElementById("pf-sketch"),u=document.getElementById("pf-viewport"),f=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),_=document.getElementById("pf-btn-code"),g=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-reset"),v=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),w=document.getElementById("pf-handle-hint");let k=!1,E=Math.round(.56*window.innerHeight);function C(){document.documentElement.style.setProperty("--pf-drawer-h",E+"px")}function L(){k=!0,r.classList.add("pf-open"),_.classList.add("pf-active"),setTimeout(()=>{Y(),V&&V.focus()},280)}function S(){k=!1,r.classList.remove("pf-open"),_.classList.remove("pf-active"),setTimeout(()=>{Y();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):a.focus()},280)}function P(){k?S():L()}C();let j=null;const R=5,z=120,I=document.createElement("div");function M(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:k?E:0,moved:!1},I.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function T(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>R&&(j.moved=!0),!j.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,j.h+t));o<z?(r.style.transition="none",r.style.height="32px"):(E=o,C(),k||L(),r.style.transition="none",r.style.height=E+"px"),Y()}function B(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,o=j.y-t,i=j.h+o;j=null,I.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(i<z?S():(E=Math.max(z,Math.min(window.innerHeight-50,i)),C(),k||L()),Y())}Object.assign(I.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(I),x.addEventListener("click",e=>{e.stopPropagation(),P()}),c.addEventListener("mousedown",M,!0),document.addEventListener("mousemove",T),document.addEventListener("mouseup",B),c.addEventListener("touchstart",M,{passive:!1}),document.addEventListener("touchmove",T,{passive:!0}),document.addEventListener("touchend",B);let O=0,A=0;function D(e){h.textContent=e,h.style.display="block",L()}function W(){h.textContent="",h.style.display="none"}function F(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=a.clientWidth,o=a.clientHeight,i=Math.min(t/e,o/n);u.style.transform=`scale(${i})`}function Y(){if("fullscreen"===H._mode?H.size("max"):F(),K&&"function"==typeof K.windowResized)try{K.windowResized()}catch(e){D(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{O=e.clientX,A=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(O=e.touches[0].clientX,A=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,o=H._h/n.height;return[(O-n.left)*t,(A-n.top)*o]},window.addEventListener("resize",Y);let K=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 o=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=a.clientWidth,this._h=a.clientHeight,void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),u.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),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){w.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(K){try{K.remove()}catch(e){}K=null}p.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,u.style.transform="scale(1)",w.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",ne&&(ne.destroy(),ne=null),Z&&(Z.destroy(),Z=null),ee&&(ee.destroy(),ee=null),te&&(te.destroy(),te=null),oe&&(oe.destroy(),oe=null),ie&&(ie.destroy(),ie=null),se&&(se.destroy(),se=null),ae&&(ae.destroy(),ae=null),re&&(re.destroy(),re=null),de&&(de.destroy(),de=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null),ue&&(ue.destroy(),ue=null)}window.p5py=H;let V=null;function X(){!i.ace.startsWith("vendor")&&i.ace.startsWith("http")||ace.config.set("basePath",i.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.session.setMode("ace/mode/python"),V.setTheme("ace/theme/monokai"),V.setValue(n,-1),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{fe()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:S}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:N}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe())}});let e=null;V.session.on("change",()=>{clearTimeout(e),e=setTimeout(N,350)})}function N(){try{localStorage.setItem(o,V?V.getValue():n)}catch(e){}}window.addEventListener("beforeunload",N);let J=null,G=null;async function $(){return G||(G=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),J=await loadPyodide(e),J.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# 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 * (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"),V){q(J.runPython("list(m.__all__)").toJs())}})(),G)}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,V.completers=[...V.completers||[],t]}let Q=!1,Z=null,ee=null,ne=null,te=null,oe=null,ie=null,se=null,ae=null,re=null,de=null,le=null,ce=null,pe=null,ue=null;async function fe(){if(Q)return;Q=!0,y.classList.add("pf-running"),W(),U(),J||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await $()}catch(e){return f.style.display="none",D("Erreur Pyodide : "+e),Q=!1,void y.classList.remove("pf-running")}f.style.display="none";const t=V?V.getValue():n;try{m.textContent="Chargement des dépendances…",f.style.display="flex",await J.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",J.globals.set("_USER_CODE",t);try{J.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return D(String(e)),Q=!1,void y.classList.remove("pf-running")}let o,i,s,a,r,d,l,c,u,h,_,g,b,v;try{r=J.runPython("_ns.get('preload')"),o=J.runPython("_ns.get('setup')"),i=J.runPython("_ns.get('draw')"),s=J.runPython("_ns.get('mousePressed')"),a=J.runPython("_ns.get('keyPressed')"),d=J.runPython("_ns.get('mouseDragged')"),l=J.runPython("_ns.get('mouseReleased')"),c=J.runPython("_ns.get('mouseMoved')"),u=J.runPython("_ns.get('mouseWheel')"),h=J.runPython("_ns.get('doubleClicked')"),_=J.runPython("_ns.get('keyReleased')"),g=J.runPython("_ns.get('touchStarted')"),b=J.runPython("_ns.get('touchMoved')"),v=J.runPython("_ns.get('touchEnded')")}catch(e){return D(String(e)),Q=!1,void y.classList.remove("pf-running")}if(!i)return D("Le script doit définir au moins une fonction draw()."),Q=!1,void y.classList.remove("pf-running");const{create_proxy:x}=J.pyimport("pyodide.ffi"),w=J.runPython("_ns.get('windowResized')"),k=J.globals.get("_pf_refresh"),E=J.globals.get("_ns"),C=e=>e?x(()=>{try{k(E),e()}catch(e){D(String(e))}}):null;ne=r?x(()=>{try{r()}catch(e){D(String(e))}}):null,Z=o?x(()=>{try{o()}catch(e){D(String(e))}}):null;const L=200;ee=x(()=>{try{k(E);const e=performance.now();i(),performance.now()-e>L&&(U(),D(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){D(String(e)),U()}}),te=C(s),oe=C(l),ie=C(d),se=C(c),ae=C(u),re=C(h),de=C(a),le=C(_),ce=C(g),pe=C(b),ue=C(v);const S=w?x(()=>{try{w()}catch(e){D(String(e))}}):null;let P=!1;K=new p5(e=>{H._setP(e),ne&&(e.preload=()=>{ne()}),e.setup=()=>{Z&&Z(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),P=!0},e.draw=()=>{P&&ee()},e.mousePressed=()=>{P&&te&&te()},e.mouseReleased=()=>{P&&oe&&oe()},e.mouseDragged=()=>{P&&ie&&ie()},e.mouseMoved=()=>{P&&se&&se()},e.mouseWheel=e=>{P&&ae&&ae()},e.doubleClicked=()=>{P&&re&&re()},e.keyPressed=()=>{P&&de&&de()},e.keyReleased=()=>{P&&le&&le()},e.touchStarted=()=>{P&&ce&&ce()},e.touchMoved=()=>{P&&pe&&pe()},e.touchEnded=()=>{P&&ue&&ue()},e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):F(),S&&S()}},p),Q=!1,y.classList.remove("pf-running")}const me='<!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.3.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function he(){const e=V?V.getValue():n,t=me.replace("FILLME-PYTHON",e),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)}y.addEventListener("click",()=>fe()),_.addEventListener("click",()=>{k?S():(E=window.innerHeight-32,C(),L())}),g.addEventListener("click",he);const ye="https://codeberg.org/nopid/pyfrilet";function _e(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)})}v.addEventListener("click",()=>window.open(ye,"_blank")),b.addEventListener("click",()=>{V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe())}),window.addEventListener("keydown",e=>{const n=k&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void fe();if("Escape"===e.key)return n?void setTimeout(()=>{k&&S()},0):(e.preventDefault(),e.stopPropagation(),void(k?S():L()));if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe()))):(e.preventDefault(),void N())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await _e(i.p5),await _e(i.ace),await _e(i.acePython),await _e(i.aceMonokai),await _e(i.aceLangTools),await _e(i.aceSearchbox),await _e(i.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}X(),await fe(),f.style.display="none"})()}(y&&y.trim()?y:m,m,h,f)})}();