pyfrilet 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -1339,12 +1339,31 @@ def _pf_safe_proxy(fn):
1339
1339
  setattr(m, 'safe_proxy', _pf_safe_proxy)
1340
1340
  _p5_functions.add('safe_proxy')
1341
1341
 
1342
+ def _pf_exec_user_code():
1343
+ _ns = {}
1344
+ try:
1345
+ exec(compile(_USER_CODE, '<string>', 'exec'), _ns, _ns)
1346
+ except Exception as _e:
1347
+ _tb = _e.__traceback__
1348
+ while _tb and _tb.tb_frame.f_code.co_filename != '<string>':
1349
+ _tb = _tb.tb_next
1350
+ if _tb: _e.__traceback__ = _tb
1351
+ _pf_rich_console.print_exception(show_locals=False)
1352
+ from js import _pfShowErrorTerminal
1353
+ _pfShowErrorTerminal()
1354
+ return None
1355
+ _ns_ref[0] = _ns
1356
+ return _ns
1357
+
1342
1358
  def _pf_draw_watchdog(fn, timeout_ms):
1343
1359
  _wdog_count[0] = 0
1344
1360
  _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001
1345
1361
  sys.settrace(_wdog_trace)
1346
1362
  try:
1347
1363
  _pf_safe_call(fn)
1364
+ except TimeoutError:
1365
+ from js import _pfShowWatchdogError
1366
+ _pfShowWatchdogError(timeout_ms)
1348
1367
  finally:
1349
1368
  sys.settrace(None)
1350
1369
 
@@ -1575,10 +1594,12 @@ async def _pf_run_terminal(source):
1575
1594
  hideTerminal(); /* make sure terminal hidden for p5 mode */
1576
1595
 
1577
1596
  pyodide.globals.set('_USER_CODE', code);
1597
+ const pfExecUser = pyodide.globals.get('_pf_exec_user_code');
1578
1598
 
1579
1599
  try {
1580
- pyodide.runPython('_ns = {}; exec(_USER_CODE, _ns, _ns)');
1581
- pyodide.runPython('_ns_ref[0] = _ns'); /* give size() access to current ns */
1600
+ const ns = pfExecUser();
1601
+ if (!ns) { running = false; btnRun.classList.remove('pf-running'); return; }
1602
+ pyodide.runPython('_ns = _ns_ref[0]');
1582
1603
  } catch (e) {
1583
1604
  showErrorTerminal(e.message || String(e));
1584
1605
  running = false; btnRun.classList.remove('pf-running'); return;
@@ -1631,13 +1652,8 @@ async def _pf_run_terminal(source):
1631
1652
  pyRefresh(pyNs);
1632
1653
  pfDrawWatchdog(pyDraw, WATCHDOG_MS);
1633
1654
  } catch (e) {
1634
- const msg = String(e);
1635
1655
  stopSketch();
1636
- if (msg.includes('TimeoutError') || msg.includes('watchdog')) {
1637
- showError(`draw() a dépassé ${WATCHDOG_MS}ms — sketch arrêté (watchdog).`);
1638
- } else {
1639
- showErrorTerminal('');
1640
- }
1656
+ showErrorTerminal(''); /* erreur JS inattendue uniquement */
1641
1657
  }
1642
1658
  });
1643
1659
  mousePressedProxy = mkProxy(pyMP);
@@ -1697,7 +1713,7 @@ async def _pf_run_terminal(source):
1697
1713
  }
1698
1714
 
1699
1715
  /* ─────────────────── DOWNLOAD ───────────────── */
1700
- const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.0/pyfrilet.min.js';
1716
+ const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@0.6.2/pyfrilet.min.js';
1701
1717
 
1702
1718
  const STANDALONE_TEMPLATE = `<!doctype html>
1703
1719
  <html lang="fr">
@@ -2060,6 +2076,11 @@ FILLME-SCRIPTS
2060
2076
  _xterm.focus();
2061
2077
  }
2062
2078
 
2079
+ window._pfShowWatchdogError = (ms) => {
2080
+ stopSketch();
2081
+ showError(`draw() a dépassé ${ms}ms — sketch arrêté (watchdog).`);
2082
+ };
2083
+
2063
2084
  window._pfShowErrorTerminal = () => {
2064
2085
  stopSketch();
2065
2086
  showErrorTerminal('');
package/pyfrilet.min.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="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",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"></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-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?m:null,mermaid:E?f:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let z;const R=L(S);let I=null;if(R)try{I=JSON.parse(R)}catch(e){I=null}if(I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;I.tabs.map(e).join(",")!==j.map(e).join(",")&&(I._stale=!0)}const T=!(!I||!I._stale);z=I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0?I.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=L(S+":"+t);if(a||"Code"!==e.label||1!==j.length||(a=L(S)),a&&a.trim())return{...e,code:a}}return e});const A=v.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),v=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let z=!1,R=Math.round(.56*window.innerHeight);function I(){document.documentElement.style.setProperty("--pf-drawer-h",R+"px")}function T(){z=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{G(),q&&q.focus()},280)}function A(){z=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{G();const e=V._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){z?A():T()}I();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:z?R:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function D(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(R=a,I(),z||T(),c.style.transition="none",c.style.height=R+"px"),G()}function N(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?A():(R=Math.max(O,Math.min(window.innerHeight-50,r)),I(),z||T()),G())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",D),document.addEventListener("mouseup",N),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",D,{passive:!0}),document.addEventListener("touchend",N);let U=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!V._p||"fit"!==V._mode)return;const e=V._w,n=V._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);f.style.transform=`scale(${r})`}function G(){if("fullscreen"===V._mode?V.size("max"):Y(),J&&"function"==typeof J.windowResized)try{J.windowResized()}catch(e){$(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=V._p?V._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=V._w/n.width,a=V._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",G);let J=null;const V=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),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){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function X(){if(Fe(),J){try{J.remove()}catch(e){}J=null}m.innerHTML="",V._p=null,V._mode="fit",V._w=0,V._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),ze&&(ze.destroy(),ze=null)}window.p5py=V;let q=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ae()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n\n _ns = {'input': _pf_async_input}\n exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)\n try:\n await _ns['programme']()\n except SystemExit:\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,q.completers=[...q.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,ze=null;const Re=300;function Ie(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,qe(),Xe(),H();try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ve(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,30))}ue=!0,y.classList.add("pf-running"),H(),Xe(),X(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await me()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Ie(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),ce.globals.set("_USER_CODE",t);try{ce.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),ce.runPython("_ns_ref[0] = _ns")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let a,r,i,s,d,l,c,p,f,h,b,g,w,v;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);d=e("preload","preload"),a=e("setup","setup"),r=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),l=e("mouseDragged","mouse_dragged"),c=e("mouseReleased","mouse_released"),p=e("mouseMoved","mouse_moved"),f=e("mouseWheel","mouse_wheel"),h=e("doubleClicked","double_clicked"),b=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),w=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!r)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:x}=ce.pyimport("pyodide.ffi"),k=ce.runPython("_ns.get('windowResized')"),E=ce.globals.get("_pf_refresh"),C=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),S=ce.globals.get("_ns"),j=ce.globals.get("_pf_safe_call"),L=e=>e?x(()=>{try{E(S),j(e)}catch(e){Ze("")}}):null;be=d?x(()=>{try{j(d)}catch(e){Ze("")}}):null,he=a?x(()=>{try{j(a)}catch(e){Ze("")}}):null,ye=x(()=>{try{E(S),C(r,Re)}catch(e){const n=String(e);X(),n.includes("TimeoutError")||n.includes("watchdog")?$(`draw() a dépassé ${Re}ms — sketch arrêté (watchdog).`):Ze("")}}),ge=L(i),we=L(c),ve=L(l),xe=L(p),ke=L(f),Ee=L(h),Ce=L(s),Se=L(b),je=L(g),Le=L(w),ze=L(v);const z=k?x(()=>{try{j(k)}catch(e){Ze("")}}):null;let R=!1;J=new p5(e=>{V._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||V.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),R=!0},e.draw=()=>{R&&ye()},e.mousePressed=()=>{R&&ge&&ge()},e.mouseReleased=()=>{R&&we&&we()},e.mouseDragged=()=>{R&&ve&&ve()},e.mouseMoved=()=>{R&&xe&&xe()},e.mouseWheel=e=>{R&&ke&&ke()},e.doubleClicked=()=>{R&&Ee&&Ee()},e.keyPressed=()=>{R&&Ce&&Ce()},e.keyReleased=()=>{R&&Se&&Se()},je&&(e.touchStarted=()=>{R&&je()}),Le&&(e.touchMoved=()=>{R&&Le()}),ze&&(e.touchEnded=()=>{R&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),z&&z()}},m),ue=!1,y.classList.remove("pf-running")}const Pe='<!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.6.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Pe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Be=null,Oe=[];function We(){const e=V._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{z?A():(R=window.innerHeight-32,I(),T())}),v.addEventListener("click",Me);const De="https://codeberg.org/nopid/pyfrilet";function Ne(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(De,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(z?A():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await Ne(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await Ne(r.marked),await Ne(r.katex),await Ne(r.markedKatex),await Ne(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await Ne(r.ace),await Ne(r.acePython),await Ne(r.aceMonokai),await Ne(r.aceLangTools),await Ne(r.aceSearchbox),await Ne(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await Ne(r.xterm),await Ne(r.xtermFit),await Ne(r.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ue=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Ge(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ue),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ue.style.display&&$e.fit()}).observe(Ue),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}function Je(e){Ke&&Ke.write(e)}function Ve(e){Ge(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function Xe(){Ke&&Ke.clear(),He=null,Ye=""}function qe(){f.style.display="none",Ue.style.display="block",Ue.classList.remove("pf-xterm-overlay"),Ge(),$e.fit(),Ke.focus()}function Ze(e){Ue.style.display="block",Ue.classList.add("pf-xterm-overlay"),Ge(),$e.fit(),e&&(Xe(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ue.style.display="none",Ue.classList.remove("pf-xterm-overlay"),f.style.display="",He){const e=He;He=null,Ye="",e("")}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=Je,window._pfTermWriteErr=Ve,window._pfShowErrorTerminal=()=>{X(),Ze("")}}(z,j,S,C,A,T)})}();
1
+ !function(){"use strict";const e=document.currentScript;let n=!1;const t="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",a="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",r="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",d="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",l="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js",c="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css",p="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js",m="https://cdn.jsdelivr.net/npm/marked-katex-extension@5.1.1/lib/index.umd.js",f="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js",u="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css",_="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js",h="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js",y="https://cdn.jsdelivr.net/npm/@xterm/addon-unicode11@0.8.0/lib/addon-unicode11.min.js",b="html, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app:focus { outline: none; }\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n display: flex;\n flex-direction: column;\n}\n#pf-ace { flex: 1; position: relative; min-height: 0; }\n\n/* ── tab bar ── */\n#pf-tabs {\n display: flex;\n flex-shrink: 0;\n background: #1a1b2e;\n border-bottom: 1px solid #414868;\n overflow-x: auto;\n scrollbar-width: none;\n}\n#pf-tabs:empty { display: none; }\n.pf-tab {\n padding: 5px 14px;\n font-size: 12px;\n background: transparent;\n border: none;\n border-bottom: 2px solid transparent;\n color: #737aa2;\n cursor: pointer;\n white-space: nowrap;\n transition: color .15s, border-color .15s;\n}\n.pf-tab:hover { color: #c0caf5; }\n.pf-tab.pf-tab-active { color: #c0caf5; border-bottom-color: #7aa2f7; }\n.pf-tab.pf-tab-readonly::after { content: ' 🔒'; font-size: 10px; opacity: .6; }\n.pf-tab.pf-tab-markdown::after { content: ' ✎'; font-size: 11px; opacity: .6; }\n\n/* ── markdown view ── */\n@import url('https://fonts.googleapis.com/css2?family=Alegreya+Sans:ital,wght@0,400;0,700;1,400&family=Fira+Code:wght@300..700&display=swap');\n\n#pf-markdown-view {\n flex: 1;\n overflow: auto;\n background: #f4f4f0;\n}\n\n#pf-markdown-view .pf-md-inner {\n width: 100%;\n max-width: 680px;\n margin: 0 auto;\n padding: 48px 48px 72px;\n box-sizing: border-box;\n font-family: 'Alegreya Sans', Georgia, serif;\n font-size: 17px;\n line-height: 1.8;\n color: #1c1c2e;\n}\n\n#pf-markdown-view h1 {\n font-size: 2.1em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 0 0 .3em;\n padding-bottom: .3em;\n border-bottom: 2px solid #d8d8e8;\n line-height: 1.2;\n}\n#pf-markdown-view h2 {\n font-size: 1.4em;\n font-weight: 700;\n color: #1c1c2e;\n margin: 2em 0 .5em;\n padding-bottom: .2em;\n border-bottom: 1px solid #e0e0ec;\n}\n#pf-markdown-view h3 {\n font-size: 1.1em;\n font-weight: 700;\n color: #2a2a4a;\n margin: 1.6em 0 .4em;\n}\n\n#pf-markdown-view p { margin: .75em 0; }\n#pf-markdown-view ul,\n#pf-markdown-view ol { padding-left: 1.6em; margin: .75em 0; }\n#pf-markdown-view li { margin: .3em 0; }\n#pf-markdown-view hr { border: none; border-top: 1px solid #dde; margin: 2em 0; }\n#pf-markdown-view blockquote {\n margin: 1em 0;\n padding: .5em 1em;\n border-left: 3px solid #aab;\n color: #555;\n background: #ededf5;\n border-radius: 0 4px 4px 0;\n}\n\n#pf-markdown-view code {\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n font-size: .84em;\n background: #e8e8f2;\n color: #3a3a6a;\n padding: .15em .45em;\n border-radius: 4px;\n}\n#pf-markdown-view pre {\n background: #1a1b2e;\n border-radius: 8px;\n padding: 1em 1.2em;\n overflow: auto;\n margin: 1.2em 0;\n box-shadow: 0 2px 8px rgba(0,0,0,.12);\n}\n#pf-markdown-view pre code {\n background: transparent;\n color: #c0caf5;\n font-size: .86em;\n padding: 0;\n line-height: 1.6;\n border-radius: 0;\n}\n\n#pf-markdown-view table {\n border-collapse: collapse;\n width: 100%;\n margin: 1.2em 0;\n font-size: .95em;\n}\n#pf-markdown-view th {\n background: #e4e4f0;\n color: #1c1c2e;\n font-weight: 700;\n text-align: left;\n padding: .55em .85em;\n border: 1px solid #d0d0e8;\n}\n#pf-markdown-view td {\n padding: .5em .85em;\n border: 1px solid #e0e0ee;\n vertical-align: top;\n}\n#pf-markdown-view tr:nth-child(even) td { background: #f0f0f8; }\n\n#pf-markdown-view a {\n color: #3a5fc8;\n text-decoration: none;\n border-bottom: 1px solid rgba(58,95,200,.3);\n transition: color .15s, border-color .15s;\n}\n#pf-markdown-view a:hover { color: #1a3fa0; border-bottom-color: #1a3fa0; }\n\n#pf-markdown-view .katex-display {\n overflow-x: auto;\n padding: .5em 0;\n margin: 1.2em 0;\n}\n#pf-markdown-view .mermaid {\n text-align: center;\n margin: 1.5em 0;\n background: #ededf5;\n border-radius: 8px;\n padding: 1em;\n}\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n/* ── xterm terminal ── */\n#pf-xterm {\n display: none;\n position: absolute;\n inset: 0;\n padding: 10px 12px;\n box-sizing: border-box;\n background: #000000;\n overflow: hidden;\n}\n\n#pf-xterm.pf-xterm-overlay {\n background: rgba(0, 0, 0, 0.82);\n}\n\n/* xterm interne : prendre toute la hauteur */\n#pf-xterm .xterm {\n height: 100%;\n}\n#pf-xterm .xterm-screen {\n height: 100% !important;\n}\n",g='<div id="pf-root">\n <div id="pf-app" tabindex="-1">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-xterm"></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-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-tabs"></div>\n <div id="pf-markdown-view" style="display:none"></div>\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>';document.addEventListener("DOMContentLoaded",function(){const w=[...document.querySelectorAll('script[type="text/python"], script[type="text/markdown"], python')];if(0===w.length)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const v=e||w[0],x=(v.getAttribute("data-sources")||v.getAttribute("sources")||"cdn").toLowerCase().trim(),k=(v.getAttribute("data-vendor")||v.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");n="cdn"===x;const E=w.some(e=>"text/markdown"===e.getAttribute("type")),C=n?{p5:t,pyodide:a,pyodideIndex:null,ace:r,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:d,marked:E?l:null,katexCss:E?c:null,katex:E?p:null,markedKatex:E?m:null,mermaid:E?f:null,xtermCss:u,xterm:_,xtermFit:h,xtermUni:y}:{p5:k+"p5.min.js",pyodide:k+"pyodide/pyodide.js",pyodideIndex:k+"pyodide/",ace:k+"ace.min.js",acePython:k+"mode-python.min.js",aceMonokai:k+"theme-monokai.min.js",aceLangTools:k+"ext-language_tools.min.js",aceSearchbox:k+"ext-searchbox.min.js",marked:E?k+"marked.min.js":null,katexCss:E?k+"katex.min.css":null,katex:E?k+"katex.min.js":null,markedKatex:E?k+"marked-katex-extension.js":null,mermaid:E?k+"mermaid.min.js":null,xtermCss:k+"xterm.min.css",xterm:k+"xterm.min.js",xtermFit:k+"xterm-addon-fit.min.js",xtermUni:k+"addon-unicode11.min.js"},S="pyfrilet:"+location.pathname,j=w.map((e,n)=>{const t="text/markdown"===e.getAttribute("type")?"markdown":"python",a=e.hasAttribute("data-hidden"),r=e.hasAttribute("data-readonly");let o=e.getAttribute("data-tab");null!==o||a||(o=1===w.length?"Code":`Bloc ${n+1}`);const i=e.textContent.replace(/^\n/,"");return{id:"tab-"+n,label:o,hidden:a,readonly:r,type:t,starterCode:i,code:i}}),L=e=>{try{return localStorage.getItem(e)}catch(e){return null}};let z;const R=L(S);let I=null;if(R)try{I=JSON.parse(R)}catch(e){I=null}if(I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0){const e=e=>`${e.label}|${e.type}|${e.hidden?1:0}|${e.readonly?1:0}`;I.tabs.map(e).join(",")!==j.map(e).join(",")&&(I._stale=!0)}const T=!(!I||!I._stale);z=I&&1===I.v&&Array.isArray(I.tabs)&&I.tabs.length>0?I.tabs.map((e,n)=>{const t=j.find(n=>n.label===e.label&&n.type===e.type)||null;return{id:"tab-"+n,label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,starterCode:t?t.starterCode:e.content,code:e.content}}):j.map((e,n)=>{if(!e.hidden&&!e.readonly&&"python"===e.type){const t=e.label?e.label.replace(/[^a-zA-Z0-9]/g,"_"):String(n);let a=L(S+":"+t);if(a||"Code"!==e.label||1!==j.length||(a=L(S)),a&&a.trim())return{...e,code:a}}return e});const A=v.hasAttribute("data-no-watchdog");!function(e,t,a,r,o,i){e=e.slice();let s=i;const d=document.createElement("style");d.textContent=b,document.head.appendChild(d),document.body.innerHTML=g;const l=document.getElementById("pf-app"),c=document.getElementById("pf-drawer"),p=document.getElementById("pf-handle"),m=document.getElementById("pf-sketch"),f=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),_=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),w=document.getElementById("pf-btn-code"),v=document.getElementById("pf-btn-dl"),x=document.getElementById("pf-btn-rec"),k=document.getElementById("pf-btn-reset"),E=document.getElementById("pf-btn-help"),C=document.getElementById("pf-grip"),S=document.getElementById("pf-handle-hint"),j=document.getElementById("pf-tabs"),L=document.getElementById("pf-markdown-view");let z=!1,R=Math.round(.56*window.innerHeight);function I(){document.documentElement.style.setProperty("--pf-drawer-h",R+"px")}function T(){z=!0,c.classList.add("pf-open"),w.classList.add("pf-active"),setTimeout(()=>{G(),q&&q.focus()},280)}function A(){z=!1,c.classList.remove("pf-open"),w.classList.remove("pf-active"),setTimeout(()=>{G();const e=V._p?.canvas;e&&e.removeAttribute("tabindex"),l.focus()},280)}function P(){z?A():T()}I();let M=null;const B=5,O=120,W=document.createElement("div");function F(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;M={y:n,h:z?R:0,moved:!1},W.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function N(e){if(!M)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=M.y-n;if(Math.abs(t)>B&&(M.moved=!0),!M.moved)return;const a=Math.max(0,Math.min(window.innerHeight-50,M.h+t));a<O?(c.style.transition="none",c.style.height="32px"):(R=a,I(),z||T(),c.style.transition="none",c.style.height=R+"px"),G()}function D(e){if(!M)return;const n=M.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??M.y,a=M.y-t,r=M.h+a;M=null,W.style.display="none",document.body.style.userSelect="",c.style.transition="",c.style.height="",n&&(r<O?A():(R=Math.max(O,Math.min(window.innerHeight-50,r)),I(),z||T()),G())}Object.assign(W.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(W),C.addEventListener("click",e=>{e.stopPropagation(),P()}),p.addEventListener("mousedown",F,!0),document.addEventListener("mousemove",N),document.addEventListener("mouseup",D),p.addEventListener("touchstart",F,{passive:!1}),document.addEventListener("touchmove",N,{passive:!0}),document.addEventListener("touchend",D);let U=0,K=0;function $(e){h.textContent=e,h.style.display="block",T()}function H(){h.textContent="",h.style.display="none"}function Y(){if(!V._p||"fit"!==V._mode)return;const e=V._w,n=V._h;if(!e||!n)return;const t=l.clientWidth,a=l.clientHeight,r=Math.min(t/e,a/n);f.style.transform=`scale(${r})`}function G(){if("fullscreen"===V._mode?V.size("max"):Y(),J&&"function"==typeof J.windowResized)try{J.windowResized()}catch(e){$(String(e))}q&&q.resize()}window.addEventListener("mousemove",e=>{U=e.clientX,K=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(U=e.touches[0].clientX,K=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=V._p?V._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=V._w/n.width,a=V._h/n.height;return[(U-n.left)*t,(K-n.top)*a]},window.addEventListener("resize",G);let J=null;const V=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const a=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=l.clientWidth,this._h=l.clientHeight,void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),f.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===a&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,a),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){S.textContent=String(e)},getItem(e){try{return localStorage.getItem(e)}catch(e){return null}},storeItem(e,n){try{localStorage.setItem(e,String(n))}catch(e){}},removeItem(e){try{localStorage.removeItem(e)}catch(e){}},clearStorage(){try{localStorage.clear()}catch(e){}}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function X(){if(Fe(),J){try{J.remove()}catch(e){}J=null}m.innerHTML="",V._p=null,V._mode="fit",V._w=0,V._h=0,f.style.transform="scale(1)",S.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",be&&(be.destroy(),be=null),he&&(he.destroy(),he=null),ye&&(ye.destroy(),ye=null),ge&&(ge.destroy(),ge=null),we&&(we.destroy(),we=null),ve&&(ve.destroy(),ve=null),xe&&(xe.destroy(),xe=null),ke&&(ke.destroy(),ke=null),Ee&&(Ee.destroy(),Ee=null),Ce&&(Ce.destroy(),Ce=null),Se&&(Se.destroy(),Se=null),je&&(je.destroy(),je=null),Le&&(Le.destroy(),Le=null),ze&&(ze.destroy(),ze=null)}window.p5py=V;let q=null,Z=null;const Q={},ee=new Set;function ne(){j.innerHTML="",Z=null;const n=e.filter(e=>!e.hidden);j.style.display=n.length<=1?"none":"",n.forEach(e=>{const n=document.createElement("button");n.className="pf-tab",n.dataset.tabId=e.id,n.textContent=e.label,e.readonly&&n.classList.add("pf-tab-readonly"),"markdown"===e.type&&n.classList.add("pf-tab-markdown"),n.addEventListener("click",()=>te(e)),j.appendChild(n)}),n.length>0&&te(n[0],!0)}function te(e,n){if(n||Z!==e)if(Z=e,j.querySelectorAll(".pf-tab").forEach(n=>{n.classList.toggle("pf-tab-active",n.dataset.tabId===e.id)}),"markdown"===e.type){if(document.getElementById("pf-ace").style.display="none",L.style.display="block",window.marked){let n=marked.parse(e.starterCode);window.mermaid&&(n=n.replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g,(e,n)=>`<div class="mermaid">${n.replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">")}</div>`)),L.innerHTML=`<div class="pf-md-inner">${n}</div>`}else L.innerHTML=`<div class="pf-md-inner"><pre>${e.starterCode}</pre></div>`;window.mermaid&&mermaid.run({nodes:L.querySelectorAll(".mermaid")})}else document.getElementById("pf-ace").style.display="block",L.style.display="none",q&&Q[e.id]&&(q.setSession(Q[e.id]),q.setReadOnly(e.readonly),q.focus())}function ae(){let n=1;e.filter(e=>"python"===e.type).forEach(e=>{e.hidden||e.readonly||!Q[e.id]?n+=e.code.split("\n").length:(Q[e.id].setOption("firstLineNumber",n),n+=Q[e.id].getLength())})}function re(){Object.keys(Q).forEach(e=>delete Q[e]),e.filter(e=>!e.hidden&&"python"===e.type).forEach(e=>{const n=ace.createEditSession(e.code,"ace/mode/python");if(n.setUseWorker(!1),n.setTabSize(4),Q[e.id]=n,!e.readonly){let e=null;n.on("change",()=>{null!==e&&(clearTimeout(e),ee.delete(e)),e=setTimeout(()=>{ee.delete(e),e=null,ie()},350),ee.add(e),ae(),de()})}});const n=e.find(e=>!e.hidden&&"python"===e.type);q&&n&&Q[n.id]&&(q.setSession(Q[n.id]),q.setReadOnly(n.readonly),q.renderer.updateFull(!0)),ae()}function oe(){!r.ace.startsWith("vendor")&&r.ace.startsWith("http")||ace.config.set("basePath",r.ace.replace(/\/[^/]+$/,"/")),q=ace.edit("pf-ace"),q.setTheme("ace/theme/monokai"),q.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),q.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{q.completer?.popup?.isOpen||Ae()}}),q.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:A}),q.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:se}),q.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}}),re(),ne(),de()}function ie(){const n={v:1,tabs:e.map(e=>({label:e.label,hidden:e.hidden,readonly:e.readonly,type:e.type,content:e.hidden||e.readonly||"python"!==e.type||!Q[e.id]?e.code:Q[e.id].getValue()}))};try{localStorage.setItem(a,JSON.stringify(n))}catch(e){}}function se(){ie()}function de(){const n=s||e.some(e=>!e.hidden&&!e.readonly&&"python"===e.type&&Q[e.id]&&Q[e.id].getValue()!==e.starterCode);k.classList.toggle("pf-dirty",n)}function le(){ee.forEach(e=>clearTimeout(e)),ee.clear();try{localStorage.removeItem(a)}catch(e){}e.forEach(e=>{if(e.label)try{localStorage.removeItem(a+":"+e.label.replace(/[^a-zA-Z0-9]/g,"_"))}catch(e){}});try{localStorage.removeItem(a+":Code")}catch(e){}s=!1,e=t.map((e,n)=>({...e,id:"tab-"+n,code:e.starterCode})),re(),ne(),de(),Ae()}window.addEventListener("beforeunload",se);let ce=null,pe=null;async function me(){return pe||(pe=(async()=>{const e={};if(r.pyodideIndex&&(e.indexURL=r.pyodideIndex),ce=await loadPyodide(e),await ce.loadPackage(["rich","pygments"]),ce.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\n# ── draw() watchdog via sys.settrace ──────────────────────────────\n# Trace is called on every Python line event. We only call time.monotonic()\n# every N events to minimize overhead — a tight loop still triggers within\n# a few microseconds, so detection latency is negligible.\nimport time as _time\n\n_WDOG_CHECK_EVERY = 100\n_wdog_deadline = [0.0]\n_wdog_count = [0]\n\ndef _wdog_trace(frame, event, arg):\n _wdog_count[0] += 1\n if _wdog_count[0] >= _WDOG_CHECK_EVERY:\n _wdog_count[0] = 0\n if _time.monotonic() > _wdog_deadline[0]:\n raise TimeoutError(\"draw() watchdog\")\n return _wdog_trace\n\nfrom rich.console import Console as _RichConsole\n_pf_rich_console = _RichConsole(stderr=True)\n\nclass _PfHandledError(Exception):\n \"\"\"Levée après que rich a déjà affiché le traceback vers xterm.\"\"\"\n pass\n\ndef _pf_safe_call(fn):\n try:\n fn()\n except (_PfHandledError, TimeoutError):\n raise\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename not in ('<string>', '<pyfrilet>'):\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n\ndef _pf_safe_proxy(fn):\n from pyodide.ffi import create_proxy as _cp\n def _wrapped(*args, **kwargs):\n _pf_safe_call(lambda: fn(*args, **kwargs))\n return _cp(_wrapped)\n\nsetattr(m, 'safe_proxy', _pf_safe_proxy)\n_p5_functions.add('safe_proxy')\n\ndef _pf_exec_user_code():\n _ns = {}\n try:\n exec(compile(_USER_CODE, '<string>', 'exec'), _ns, _ns)\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<string>':\n _tb = _tb.tb_next\n if _tb: _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n from js import _pfShowErrorTerminal\n _pfShowErrorTerminal()\n return None\n _ns_ref[0] = _ns\n return _ns\n\ndef _pf_draw_watchdog(fn, timeout_ms):\n _wdog_count[0] = 0\n _wdog_deadline[0] = _time.monotonic() + timeout_ms * 0.001\n sys.settrace(_wdog_trace)\n try:\n _pf_safe_call(fn)\n except TimeoutError:\n from js import _pfShowWatchdogError\n _pfShowWatchdogError(timeout_ms)\n finally:\n sys.settrace(None)\n\ndef _pf_draw_direct(fn, timeout_ms):\n _pf_safe_call(fn)\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),ce.runPython("\nimport asyncio as _asyncio, ast as _ast\nimport os as _os, sys as _sys\n_os.environ.setdefault('TERM', 'xterm-256color')\n_os.environ.setdefault('COLORTERM', 'truecolor')\n\n# Wrapper file-like qui écrit directement vers xterm via JS.\n# Rich écrit des strings sur sys.stdout.write() — il faut un vrai objet fichier.\nclass _PfStream:\n def __init__(self, js_fn):\n self._fn = js_fn\n self.encoding = 'utf-8'\n self.errors = 'replace'\n def write(self, s):\n if s:\n self._fn(s)\n return len(s)\n def writelines(self, lines):\n for l in lines: self.write(l)\n def flush(self): pass\n def isatty(self): return True\n @property\n def softspace(self): return 0\n\nfrom js import _pfTermWrite, _pfTermWriteErr\n_sys.stdout = _PfStream(_pfTermWrite)\n_sys.stderr = _PfStream(_pfTermWriteErr)\n\nasync def _pf_async_input(prompt=\"\"):\n from js import _pfTerminalInput\n result = await _pfTerminalInput(str(prompt) if prompt else \"\")\n return result\n\nasync def _pf_run_terminal(source):\n class _InputAwaiter(_ast.NodeTransformer):\n def visit_Call(self, node):\n self.generic_visit(node)\n if isinstance(node.func, _ast.Name) and node.func.id == 'input':\n return _ast.Await(value=node)\n return node\n\n tree = _ast.parse(source)\n tree = _InputAwaiter().visit(tree)\n\n wrapper = _ast.parse(\"async def programme(): pass\")\n wrapper.body[0].body = tree.body if tree.body else [_ast.Pass()]\n _ast.fix_missing_locations(wrapper)\n\n _ns = {'input': _pf_async_input}\n exec(compile(wrapper, '<pyfrilet>', 'exec'), _ns)\n try:\n await _ns['programme']()\n except SystemExit:\n pass\n except Exception as _e:\n _tb = _e.__traceback__\n while _tb and _tb.tb_frame.f_code.co_filename != '<pyfrilet>':\n _tb = _tb.tb_next\n if _tb:\n _e.__traceback__ = _tb\n _pf_rich_console.print_exception(show_locals=False)\n"),q){fe(ce.runPython("list(m.__all__)").toJs())}})(),pe)}function fe(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,a,r,o){o(null,r.length>0?n:[])}},a=ace.require("ace/ext/language_tools");a&&Array.isArray(a.completers)&&(a.completers=a.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,q.completers=[...q.completers||[],t]}let ue=!1,_e=!1,he=null,ye=null,be=null,ge=null,we=null,ve=null,xe=null,ke=null,Ee=null,Ce=null,Se=null,je=null,Le=null,ze=null;const Re=300;function Ie(e){return!/\bfrom\s+p5\s+import\b|\bimport\s+p5\b/.test(e)}async function Te(e){_e=!0,qe(),Xe(),H();try{const n=ce.globals.get("_pf_run_terminal");await n(e)}catch(e){const n=String(e);n.includes("SystemExit")||Ve(n+"\n")}finally{_e=!1}}async function Ae(){if(ue){if(!_e)return;Qe(),ue=!1,y.classList.remove("pf-running"),await new Promise(e=>setTimeout(e,30))}ue=!0,y.classList.add("pf-running"),H(),Xe(),X(),ce||(_.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await me()}catch(e){return u.style.display="none",$("Erreur Pyodide : "+(e.message||String(e))),ue=!1,void y.classList.remove("pf-running")}u.style.display="none";const t=e.filter(e=>"python"===e.type).map(e=>e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue()).join("\n");try{_.textContent="Chargement des dépendances…",u.style.display="flex",await ce.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:n})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}if(u.style.display="none",Ie(t))return y.classList.remove("pf-running"),await Te(t),void(ue=!1);Qe(),ce.globals.set("_USER_CODE",t);const a=ce.globals.get("_pf_exec_user_code");try{if(!a())return ue=!1,void y.classList.remove("pf-running");ce.runPython("_ns = _ns_ref[0]")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}let r,i,s,d,l,c,p,f,h,b,g,w,v,x;try{const e=(e,n)=>ce.runPython(`_ns.get('${e}') or _ns.get('${n}')`);l=e("preload","preload"),r=e("setup","setup"),i=e("draw","draw"),s=e("mousePressed","mouse_pressed"),d=e("keyPressed","key_pressed"),c=e("mouseDragged","mouse_dragged"),p=e("mouseReleased","mouse_released"),f=e("mouseMoved","mouse_moved"),h=e("mouseWheel","mouse_wheel"),b=e("doubleClicked","double_clicked"),g=e("keyReleased","key_released"),w=e("touchStarted","touch_started"),v=e("touchMoved","touch_moved"),x=e("touchEnded","touch_ended")}catch(e){return Ze(e.message||String(e)),ue=!1,void y.classList.remove("pf-running")}if(!i)return $("Le script doit définir au moins une fonction draw()."),ue=!1,void y.classList.remove("pf-running");const{create_proxy:k}=ce.pyimport("pyodide.ffi"),E=ce.runPython("_ns.get('windowResized')"),C=ce.globals.get("_pf_refresh"),S=ce.globals.get(o?"_pf_draw_direct":"_pf_draw_watchdog"),j=ce.globals.get("_ns"),L=ce.globals.get("_pf_safe_call"),z=e=>e?k(()=>{try{C(j),L(e)}catch(e){Ze("")}}):null;be=l?k(()=>{try{L(l)}catch(e){Ze("")}}):null,he=r?k(()=>{try{L(r)}catch(e){Ze("")}}):null,ye=k(()=>{try{C(j),S(i,Re)}catch(e){X(),Ze("")}}),ge=z(s),we=z(p),ve=z(c),xe=z(f),ke=z(h),Ee=z(b),Ce=z(d),Se=z(g),je=z(w),Le=z(v),ze=z(x);const R=E?k(()=>{try{L(E)}catch(e){Ze("")}}):null;let I=!1;J=new p5(e=>{V._setP(e),be&&(e.preload=()=>{be()}),e.setup=()=>{he&&he(),e.canvas||V.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),I=!0},e.draw=()=>{I&&ye()},e.mousePressed=()=>{I&&ge&&ge()},e.mouseReleased=()=>{I&&we&&we()},e.mouseDragged=()=>{I&&ve&&ve()},e.mouseMoved=()=>{I&&xe&&xe()},e.mouseWheel=e=>{I&&ke&&ke()},e.doubleClicked=()=>{I&&Ee&&Ee()},e.keyPressed=()=>{I&&Ce&&Ce()},e.keyReleased=()=>{I&&Se&&Se()},je&&(e.touchStarted=()=>{I&&je()}),Le&&(e.touchMoved=()=>{I&&Le()}),ze&&(e.touchEnded=()=>{I&&ze()}),e.windowResized=()=>{"fullscreen"===V._mode?V.size("max"):Y(),R&&R()}},m),ue=!1,y.classList.remove("pf-running")}const Pe='<!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.6.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\nFILLME-SCRIPTS\n\n</body>\n</html>';function Me(){const n=e.map((e,n)=>{let t;t="python"!==e.type||e.hidden||e.readonly||!Q[e.id]?e.code:Q[e.id].getValue();const a=[],r="markdown"===e.type?"text/markdown":"text/python";null!==e.label&&a.push(`data-tab="${e.label.replace(/"/g,"&quot;")}"`),e.hidden&&a.push("data-hidden"),e.readonly&&a.push("data-readonly");return`<script type="${r}"${a.length?" "+a.join(" "):""}>\n${t.replace(/<\/script>/gi,"<\\/script>")}\n<\/script>`}).join("\n\n"),t=Pe.replace("FILLME-SCRIPTS",n),a=new Blob([t],{type:"text/html;charset=utf-8"}),r=URL.createObjectURL(a),o=Object.assign(document.createElement("a"),{href:r,download:"sketch.html"});document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}let Be=null,Oe=[];function We(){const e=V._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();Be=new MediaRecorder(t,{mimeType:n}),Oe=[],Be.ondataavailable=e=>{e.data.size&&Oe.push(e.data)},Be.onstop=()=>{const e=new Blob(Oe,{type:n}),t=URL.createObjectURL(e),a=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${a}`}).click(),URL.revokeObjectURL(t),x.textContent="⏺",x.title="Enregistrer WebM",x.classList.remove("pf-recording"),Be=null},Be.start(),x.textContent="⏹",x.title="Arrêter l'enregistrement",x.classList.add("pf-recording")}function Fe(){Be&&"inactive"!==Be.state&&Be.stop()}x.addEventListener("click",()=>{Be?Fe():We()}),y.addEventListener("click",()=>Ae()),w.addEventListener("click",()=>{z?A():(R=window.innerHeight-32,I(),T())}),v.addEventListener("click",Me);const Ne="https://codeberg.org/nopid/pyfrilet";function De(e){return new Promise((n,t)=>{const a=document.createElement("script");a.src=e,a.onload=n,a.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(a)})}E.addEventListener("click",()=>window.open(Ne,"_blank")),k.addEventListener("click",()=>{confirm("Réinitialiser ? Les modifications seront perdues.")&&le()}),window.addEventListener("keydown",e=>{const n=z&&q&&q.isFocused&&q.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void Ae();if("Escape"===e.key){const t=document.querySelector(".ace_search");if(t&&"none"!==t.style.display)return e.preventDefault(),e.stopPropagation(),q.searchBox?q.searchBox.hide():t.style.display="none",void q.focus();if(n){const n=q.completer?.popup?.isOpen;if(n)return;return e.preventDefault(),e.stopPropagation(),void A()}return e.preventDefault(),e.stopPropagation(),void(z?A():T())}if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(confirm("Réinitialiser ? Les modifications seront perdues.")&&le())):(e.preventDefault(),void se())}else e.preventDefault()},!0),(async()=>{_.textContent="Chargement des dépendances…",u.style.display="flex";try{if(await De(r.p5),r.marked){const e=document.createElement("link");e.rel="stylesheet",e.href=r.katexCss,document.head.appendChild(e),await De(r.marked),await De(r.katex),await De(r.markedKatex),await De(r.mermaid),marked.use(markedKatex({throwOnError:!1})),mermaid.initialize({startOnLoad:!1,theme:"neutral"})}await De(r.ace),await De(r.acePython),await De(r.aceMonokai),await De(r.aceLangTools),await De(r.aceSearchbox),await De(r.pyodide);const e=document.createElement("link");e.rel="stylesheet",e.href=r.xtermCss,document.head.appendChild(e),await De(r.xterm),await De(r.xtermFit),await De(r.xtermUni)}catch(e){return _.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}oe(),await Ae(),u.style.display="none"})();const Ue=document.getElementById("pf-xterm");let Ke=null,$e=null,He=null,Ye="";function Ge(){if(Ke)return;Ke=new Terminal({theme:{background:"#000000",foreground:"#e8e8e8",cursor:"#ffffff",black:"#2a2a2a",brightBlack:"#555555",red:"#cc4444",brightRed:"#ff6666",green:"#44aa44",brightGreen:"#66cc66",yellow:"#aaaa00",brightYellow:"#dddd44",blue:"#4466cc",brightBlue:"#6688ff",magenta:"#aa44aa",brightMagenta:"#dd66dd",cyan:"#44aaaa",brightCyan:"#66cccc",white:"#cccccc",brightWhite:"#ffffff"},fontFamily:"'Fira Code', 'Consolas', 'Courier New', monospace",fontSize:15,lineHeight:1,letterSpacing:0,cursorBlink:!0,scrollback:2e3,convertEol:!0,allowProposedApi:!0}),$e=new FitAddon.FitAddon,Ke.loadAddon($e);const e=new Unicode11Addon.Unicode11Addon;Ke.loadAddon(e),Ke.unicode.activeVersion="11",Ke.open(Ue),$e.fit(),new ResizeObserver(()=>{Ke&&"none"!==Ue.style.display&&$e.fit()}).observe(Ue),Ke.onData(e=>{if(He)if("\r"===e){const e=Ye;Ye="",Ke.write("\r\n");const n=He;He=null,n(e)}else if(""===e)Ye.length>0&&(Ye=Ye.slice(0,-1),Ke.write("\b \b"));else if(""===e){Ye="",Ke.write("^C\r\n");const e=He;He=null,e(null)}else e.charCodeAt(0)>=32&&(Ye+=e,Ke.write(e))})}function Je(e){Ke&&Ke.write(e)}function Ve(e){Ge(),Ke.write(""),Ke.write(e.replace(/\n/g,"\r\n")),Ke.write("")}function Xe(){Ke&&Ke.clear(),He=null,Ye=""}function qe(){f.style.display="none",Ue.style.display="block",Ue.classList.remove("pf-xterm-overlay"),Ge(),$e.fit(),Ke.focus()}function Ze(e){Ue.style.display="block",Ue.classList.add("pf-xterm-overlay"),Ge(),$e.fit(),e&&(Xe(),Ke.write("── Erreur ──────────────────────────────────────\r\n\r\n"),Ke.write(e.replace(/\n/g,"\r\n")+"\r\n"))}function Qe(){if(Ue.style.display="none",Ue.classList.remove("pf-xterm-overlay"),f.style.display="",He){const e=He;He=null,Ye="",e("")}}window._pfTerminalInput=function(e){return new Promise(n=>{He=n,Ye="",e&&Ke.write(e),Ke.focus()})},window._pfTermWrite=Je,window._pfTermWriteErr=Ve,window._pfShowWatchdogError=e=>{X(),$(`draw() a dépassé ${e}ms — sketch arrêté (watchdog).`)},window._pfShowErrorTerminal=()=>{X(),Ze("")}}(z,j,S,C,A,T)})}();