pyfrilet 0.3.1 → 0.4.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
@@ -44,6 +44,11 @@ Le module `p5` expose **toute l'API p5.js** via introspection dynamique : au dé
44
44
 
45
45
  L'autocomplétion dans l'éditeur intégré connaît l'ensemble de ces symboles.
46
46
 
47
+ > **Noms snake_case** : pyfrilet accepte les noms de fonctions, callbacks et attributs en style Python et les convertit silencieusement en camelCase. Les deux styles sont équivalents :
48
+ > - fonctions : `create_graphics`, `load_image`, `no_stroke`…
49
+ > - callbacks : `key_pressed`, `mouse_pressed`, `mouse_dragged`…
50
+ > - attributs dynamiques : `key_is_pressed`, `frame_count`, `mouse_x`, `mouse_y`…
51
+
47
52
  Les fonctions ci-dessous sont un aperçu des plus couramment utilisées, à titre d'illustration :
48
53
 
49
54
  | Fonction | Description |
@@ -97,9 +102,10 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
97
102
 
98
103
  | Fonction | Description |
99
104
  |---|---|
100
- | `size(w, h)` | Crée ou redimensionne le canvas. Le canvas est mis à l'échelle CSS pour remplir l'écran sans déformation. |
105
+ | `size(w, h)` | Crée ou redimensionne le canvas. Le canvas est mis à l'échelle CSS pour remplir l'écran sans déformation. Retourne le `p5.Element` du canvas. |
101
106
  | `size(w, h, WEBGL)` | Crée un canvas en mode WebGL 3D. `WEBGL` est une constante p5 importable avec `from p5 import *`. |
102
107
  | `size('max')` | Canvas plein écran, redimensionné dynamiquement. |
108
+ | `createCanvas(w, h)` | Alias de `size()` — accepte les mêmes arguments et retourne le canvas. Permet de copier des exemples p5.js sans adaptation. |
103
109
  | `smooth()` | Active l'antialiasing (par défaut). |
104
110
  | `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
105
111
  | `sketchTitle(s)` | Affiche un texte dans la barre de contrôle. À appeler dans `setup()`. |
@@ -242,7 +248,8 @@ La barre de contrôle est collée en bas de l'écran.
242
248
  | ▶ | Relance l'exécution du code (sans fermer l'éditeur) |
243
249
  | ✏️ | Ouvre l'éditeur en **plein écran** ; referme si déjà ouvert |
244
250
  | 💾 | Télécharge la page en HTML autonome (voir ci-dessous) |
245
- | | Réinitialise le code à la version d'origine (confirmation demandée) |
251
+ | | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement cliquer pour arrêter et télécharger |
252
+ | ↻ | Réinitialise le code à la version d'origine (confirmation demandée). Un point orange apparaît sur le bouton quand le code a été modifié. |
246
253
 
247
254
  ### Raccourcis clavier
248
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyfrilet",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "pyfrilet.js",
6
6
  "files": [
package/pyfrilet.js CHANGED
@@ -181,8 +181,22 @@ html, body {
181
181
  #pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }
182
182
  #pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }
183
183
 
184
+ #pf-btn-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }
185
+ #pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }
186
+ #pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }
187
+ @keyframes pf-blink { 50% { opacity: .4; } }
188
+
184
189
  #pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }
185
190
  #pf-btn-reset:hover { background: #3d4166; color: #ffc777; }
191
+ #pf-btn-reset.pf-dirty::after {
192
+ content: '●';
193
+ position: absolute;
194
+ top: 2px; right: 3px;
195
+ font-size: 7px;
196
+ color: #e0af68;
197
+ line-height: 1;
198
+ }
199
+ #pf-btn-reset { position: relative; }
186
200
 
187
201
  /* ── editor area inside drawer ── */
188
202
  #pf-editor-wrap {
@@ -225,6 +239,7 @@ const MARKUP = `
225
239
  <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">&#9654;</button>
226
240
  <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">&#9999;&#xFE0F;</button>
227
241
  <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">&#128190;</button>
242
+ <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">&#9210;</button>
228
243
  <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>
229
244
  <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">&#8635;</button>
230
245
  </div>
@@ -312,6 +327,7 @@ function main(initialCode, starterCode, SK, URLS) {
312
327
  const btnRun = document.getElementById('pf-btn-run');
313
328
  const btnCode = document.getElementById('pf-btn-code');
314
329
  const btnDl = document.getElementById('pf-btn-dl');
330
+ const btnRec = document.getElementById('pf-btn-rec');
315
331
  const btnReset = document.getElementById('pf-btn-reset');
316
332
  const btnHelp = document.getElementById('pf-btn-help');
317
333
  const gripEl = document.getElementById('pf-grip');
@@ -559,6 +575,7 @@ function main(initialCode, starterCode, SK, URLS) {
559
575
  window.p5py = p5Bridge;
560
576
 
561
577
  function stopSketch() {
578
+ stopRecording();
562
579
  if (pInst) { try { pInst.remove(); } catch (e) {} pInst = null; }
563
580
  sketchEl.innerHTML = '';
564
581
  p5Bridge._p = null; p5Bridge._mode = 'fit'; p5Bridge._w = 0; p5Bridge._h = 0;
@@ -593,6 +610,7 @@ function main(initialCode, starterCode, SK, URLS) {
593
610
  aceInst.session.setMode('ace/mode/python');
594
611
  aceInst.setTheme('ace/theme/monokai');
595
612
  aceInst.setValue(initialCode, -1);
613
+ btnReset.classList.toggle('pf-dirty', initialCode !== starterCode);
596
614
  aceInst.setOptions({
597
615
  fontSize : '15px',
598
616
  showPrintMargin: false,
@@ -634,6 +652,7 @@ function main(initialCode, starterCode, SK, URLS) {
634
652
  aceInst.session.on('change', () => {
635
653
  clearTimeout(saveTimer);
636
654
  saveTimer = setTimeout(saveCode, 350);
655
+ btnReset.classList.toggle('pf-dirty', aceInst.getValue() !== starterCode);
637
656
  });
638
657
  }
639
658
 
@@ -729,9 +748,12 @@ class _SizeWrapper:
729
748
  def __call__(self, *a):
730
749
  p5py.size(*a)
731
750
  _pf_refresh(_ns_ref[0])
751
+ return _GetCanvasWrapper()()
732
752
  def __repr__(self): return '<p5 function size>'
733
753
  setattr(m, 'size', _SizeWrapper())
754
+ setattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)
734
755
  _p5_functions.add('size')
756
+ _p5_functions.add('createCanvas')
735
757
  _ns_ref = [{}] # filled in by runCode before each exec
736
758
 
737
759
  # getCanvas() — returns the p5.Element wrapping the canvas,
@@ -759,18 +781,37 @@ for _n in _p5_attributes:
759
781
  else:
760
782
  setattr(m, _n, _attr_init.get(_n, 0))
761
783
 
762
- # Build __all__ for import * (after all explicit additions)
763
- m.__all__ = sorted(_p5_functions | _p5_attributes)
784
+ # Build __all__ for import * — done later, after snake_case aliases are added
764
785
 
765
786
  # ── _pf_refresh: called before every event callback ───────────────
787
+ import re as _re
788
+
789
+ # Pre-compute snake_case alias for each attribute — None if identical
790
+ _attr_snake = {
791
+ _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)
792
+ for _k in _p5_attributes
793
+ }
794
+ _attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}
795
+
796
+ # Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them
797
+ for _k, _sk in list(_attr_snake.items()):
798
+ if _sk:
799
+ _p5_attributes.add(_sk)
800
+ setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase
801
+ _attr_snake[_sk] = None # snake name has no further alias
802
+
766
803
  def _pf_refresh(ns):
767
804
  # accurate mouse coords (bypasses p5's stale CSS-transform offset)
768
805
  mx, my = _pfMouse()
769
806
 
770
807
  # update all known scalar attributes from live instance
771
808
  for _k in _p5_attributes:
809
+ _sk = _attr_snake.get(_k)
772
810
  if _k in _MOUSE_OVERRIDE:
773
- _v = mx if _k == 'mouseX' else my
811
+ _v = mx if _k in ('mouseX', 'mouse_x') else my
812
+ elif _sk is None and _k not in _attr_snake:
813
+ # pure snake_case entry — skip, updated via its camelCase counterpart
814
+ continue
774
815
  else:
775
816
  try:
776
817
  _v = getattr(p5py, _k)
@@ -779,8 +820,37 @@ def _pf_refresh(ns):
779
820
  setattr(m, _k, _v)
780
821
  if _k in ns:
781
822
  ns[_k] = _v
823
+ if _sk:
824
+ setattr(m, _sk, _v)
825
+ if _sk in ns:
826
+ ns[_sk] = _v
782
827
 
783
828
  sys.modules["p5"] = m
829
+
830
+ def _snake_to_camel(name):
831
+ parts = name.split('_')
832
+ return parts[0] + ''.join(p.capitalize() for p in parts[1:])
833
+
834
+ # Pre-populate snake_case aliases so "from p5 import no_fill" works
835
+ for _camel in list(vars(m).keys()):
836
+ _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)
837
+ if _snake != _camel and not hasattr(m, _snake):
838
+ setattr(m, _snake, getattr(m, _camel))
839
+ if _camel in _p5_functions:
840
+ _p5_functions.add(_snake)
841
+
842
+ # Rebuild __all__ now that snake_case aliases are included
843
+ m.__all__ = sorted(_p5_functions | _p5_attributes)
844
+
845
+ def _p5_getattr(name):
846
+ camel = _snake_to_camel(name)
847
+ if camel != name:
848
+ val = getattr(m, camel, None)
849
+ if val is not None:
850
+ return val
851
+ raise AttributeError(f"module 'p5' has no attribute '{name}'")
852
+
853
+ m.__getattr__ = _p5_getattr
784
854
  `);
785
855
 
786
856
  /* Inject p5 symbols into ACE autocomplete */
@@ -873,20 +943,21 @@ sys.modules["p5"] = m
873
943
  let pySetup, pyDraw, pyMP, pyKP, pyPreload, pyMD, pyMR,
874
944
  pyMM, pyMW, pyDC, pyKR, pyTS, pyTM, pyTE;
875
945
  try {
876
- pyPreload = pyodide.runPython("_ns.get('preload')");
877
- pySetup = pyodide.runPython("_ns.get('setup')");
878
- pyDraw = pyodide.runPython("_ns.get('draw')");
879
- pyMP = pyodide.runPython("_ns.get('mousePressed')");
880
- pyKP = pyodide.runPython("_ns.get('keyPressed')");
881
- pyMD = pyodide.runPython("_ns.get('mouseDragged')");
882
- pyMR = pyodide.runPython("_ns.get('mouseReleased')");
883
- pyMM = pyodide.runPython("_ns.get('mouseMoved')");
884
- pyMW = pyodide.runPython("_ns.get('mouseWheel')");
885
- pyDC = pyodide.runPython("_ns.get('doubleClicked')");
886
- pyKR = pyodide.runPython("_ns.get('keyReleased')");
887
- pyTS = pyodide.runPython("_ns.get('touchStarted')");
888
- pyTM = pyodide.runPython("_ns.get('touchMoved')");
889
- pyTE = pyodide.runPython("_ns.get('touchEnded')");
946
+ const _get = (camel, snake) => pyodide.runPython(`_ns.get('${camel}') or _ns.get('${snake}')`);
947
+ pyPreload = _get('preload', 'preload');
948
+ pySetup = _get('setup', 'setup');
949
+ pyDraw = _get('draw', 'draw');
950
+ pyMP = _get('mousePressed', 'mouse_pressed');
951
+ pyKP = _get('keyPressed', 'key_pressed');
952
+ pyMD = _get('mouseDragged', 'mouse_dragged');
953
+ pyMR = _get('mouseReleased', 'mouse_released');
954
+ pyMM = _get('mouseMoved', 'mouse_moved');
955
+ pyMW = _get('mouseWheel', 'mouse_wheel');
956
+ pyDC = _get('doubleClicked', 'double_clicked');
957
+ pyKR = _get('keyReleased', 'key_released');
958
+ pyTS = _get('touchStarted', 'touch_started');
959
+ pyTM = _get('touchMoved', 'touch_moved');
960
+ pyTE = _get('touchEnded', 'touch_ended');
890
961
  } catch (e) {
891
962
  showError(String(e));
892
963
  running = false; btnRun.classList.remove('pf-running'); return;
@@ -1022,7 +1093,45 @@ FILLME-PYTHON
1022
1093
  }
1023
1094
  });
1024
1095
 
1025
- btnDl.addEventListener('click', download);
1096
+ /* ── WebM recording ─────────────────────────────────────────────── */
1097
+ let mediaRecorder = null;
1098
+ let recChunks = [];
1099
+
1100
+ function startRecording() {
1101
+ const canvas = p5Bridge._p?.canvas;
1102
+ if (!canvas) return;
1103
+ const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']
1104
+ .find(m => MediaRecorder.isTypeSupported(m)) || 'video/webm';
1105
+ const stream = canvas.captureStream();
1106
+ mediaRecorder = new MediaRecorder(stream, { mimeType });
1107
+ recChunks = [];
1108
+ mediaRecorder.ondataavailable = e => { if (e.data.size) recChunks.push(e.data); };
1109
+ mediaRecorder.onstop = () => {
1110
+ const blob = new Blob(recChunks, { type: mimeType });
1111
+ const url = URL.createObjectURL(blob);
1112
+ const ext = mimeType.includes('webm') ? 'webm' : 'mp4';
1113
+ Object.assign(document.createElement('a'), { href: url, download: `sketch.${ext}` }).click();
1114
+ URL.revokeObjectURL(url);
1115
+ btnRec.textContent = '⏺';
1116
+ btnRec.title = 'Enregistrer WebM';
1117
+ btnRec.classList.remove('pf-recording');
1118
+ mediaRecorder = null;
1119
+ };
1120
+ mediaRecorder.start();
1121
+ btnRec.textContent = '⏹';
1122
+ btnRec.title = 'Arrêter l\'enregistrement';
1123
+ btnRec.classList.add('pf-recording');
1124
+ }
1125
+
1126
+ function stopRecording() {
1127
+ if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
1128
+ }
1129
+
1130
+ btnRec.addEventListener('click', () => {
1131
+ mediaRecorder ? stopRecording() : startRecording();
1132
+ });
1133
+
1134
+ btnDl.addEventListener('click', download);
1026
1135
  const HELP_URL = 'https://codeberg.org/nopid/pyfrilet';
1027
1136
  btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
1028
1137
 
package/pyfrilet.min.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";let e=!1;const n="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",t="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",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",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",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:r,aceSearchbox:a}:{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,_=(()=>{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 r=document.getElementById("pf-app"),a=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"),_=document.getElementById("pf-btn-run"),y=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,a.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{Y(),V&&V.focus()},280)}function S(){k=!1,a.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{Y();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):r.focus()},280)}function P(){k?S():L()}C();let z=null;const j=5,R=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;z={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(!z)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=z.y-n;if(Math.abs(t)>j&&(z.moved=!0),!z.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,z.h+t));o<R?(a.style.transition="none",a.style.height="32px"):(E=o,C(),k||L(),a.style.transition="none",a.style.height=E+"px"),Y()}function B(e){if(!z)return;const n=z.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??z.y,o=z.y-t,i=z.h+o;z=null,I.style.display="none",document.body.style.userSelect="",a.style.transition="",a.style.height="",n&&(i<R?S():(E=Math.max(R,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 W(e){h.textContent=e,h.style.display="block",L()}function D(){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=r.clientWidth,o=r.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){W(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=r.clientWidth,this._h=r.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),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),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:J}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe())}});let e=null;V.session.on("change",()=>{clearTimeout(e),e=setTimeout(J,350)})}function J(){try{localStorage.setItem(o,V?V.getValue():n)}catch(e){}}window.addEventListener("beforeunload",J);let N=null,G=null;async function $(){return G||(G=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),N=await loadPyodide(e),N.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('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 def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\n_p5_functions.add('size')\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 * (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(N.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,re=null,ae=null,de=null,le=null,ce=null,pe=null,ue=null;async function fe(){if(Q)return;Q=!0,_.classList.add("pf-running"),D(),U(),N||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await $()}catch(e){return f.style.display="none",W("Erreur Pyodide : "+e),Q=!1,void _.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 N.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",N.globals.set("_USER_CODE",t);try{N.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),N.runPython("_ns_ref[0] = _ns")}catch(e){return W(String(e)),Q=!1,void _.classList.remove("pf-running")}let o,i,s,r,a,d,l,c,u,h,y,g,b,v;try{a=N.runPython("_ns.get('preload')"),o=N.runPython("_ns.get('setup')"),i=N.runPython("_ns.get('draw')"),s=N.runPython("_ns.get('mousePressed')"),r=N.runPython("_ns.get('keyPressed')"),d=N.runPython("_ns.get('mouseDragged')"),l=N.runPython("_ns.get('mouseReleased')"),c=N.runPython("_ns.get('mouseMoved')"),u=N.runPython("_ns.get('mouseWheel')"),h=N.runPython("_ns.get('doubleClicked')"),y=N.runPython("_ns.get('keyReleased')"),g=N.runPython("_ns.get('touchStarted')"),b=N.runPython("_ns.get('touchMoved')"),v=N.runPython("_ns.get('touchEnded')")}catch(e){return W(String(e)),Q=!1,void _.classList.remove("pf-running")}if(!i)return W("Le script doit définir au moins une fonction draw()."),Q=!1,void _.classList.remove("pf-running");const{create_proxy:x}=N.pyimport("pyodide.ffi"),w=N.runPython("_ns.get('windowResized')"),k=N.globals.get("_pf_refresh"),E=N.globals.get("_ns"),C=e=>e?x(()=>{try{k(E),e()}catch(e){W(String(e))}}):null;ne=a?x(()=>{try{a()}catch(e){W(String(e))}}):null,Z=o?x(()=>{try{o()}catch(e){W(String(e))}}):null;const L=200;ee=x(()=>{try{k(E);const e=performance.now();i(),performance.now()-e>L&&(U(),W(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){W(String(e)),U()}}),te=C(s),oe=C(l),ie=C(d),se=C(c),re=C(u),ae=C(h),de=C(r),le=C(y),ce=C(g),pe=C(b),ue=C(v);const S=w?x(()=>{try{w()}catch(e){W(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&&re&&re()},e.doubleClicked=()=>{P&&ae&&ae()},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,_.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.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function 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)}_.addEventListener("click",()=>fe()),y.addEventListener("click",()=>{k?S():(E=window.innerHeight-32,C(),L())}),g.addEventListener("click",he);const _e="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)})}v.addEventListener("click",()=>window.open(_e,"_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 J())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await ye(i.p5),await ye(i.ace),await ye(i.acePython),await ye(i.aceMonokai),await ye(i.aceLangTools),await ye(i.aceSearchbox),await ye(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"})()}(_&&_.trim()?_:m,m,h,f)})}();
1
+ !function(){"use strict";let e=!1;const n="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",t="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",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",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-rec { background: #2a2c3e; color: #f7768e; font-size: 13px; }\n#pf-btn-rec:hover { background: #3d4166; color: #ff9e9e; }\n#pf-btn-rec.pf-recording { background: #6b1a1a; color: #f7768e; animation: pf-blink .8s step-end infinite; }\n@keyframes pf-blink { 50% { opacity: .4; } }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n#pf-btn-reset.pf-dirty::after {\n content: '●';\n position: absolute;\n top: 2px; right: 3px;\n font-size: 7px;\n color: #e0af68;\n line-height: 1;\n}\n#pf-btn-reset { position: relative; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n}\n#pf-ace { position: absolute; inset: 0; }\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n",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-rec" title="Enregistrer WebM">&#9210;</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:a,aceMonokai:i,aceLangTools:s,aceSearchbox:r}:{p5:u+"p5.min.js",pyodide:u+"pyodide/pyodide.js",pyodideIndex:u+"pyodide/",ace:u+"ace.min.js",acePython:u+"mode-python.min.js",aceMonokai:u+"theme-monokai.min.js",aceLangTools:u+"ext-language_tools.min.js",aceSearchbox:u+"ext-searchbox.min.js"},m=c.textContent.replace(/^\n/,""),_="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(_)}catch(e){return null}})();!function(n,t,o,a){const i=document.createElement("style");i.textContent=d,document.head.appendChild(i),document.body.innerHTML=l;const s=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),c=document.getElementById("pf-handle"),p=document.getElementById("pf-sketch"),u=document.getElementById("pf-viewport"),f=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),_=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),g=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-rec"),v=document.getElementById("pf-btn-reset"),x=document.getElementById("pf-btn-help"),w=document.getElementById("pf-grip"),k=document.getElementById("pf-handle-hint");let E=!1,C=Math.round(.56*window.innerHeight);function L(){document.documentElement.style.setProperty("--pf-drawer-h",C+"px")}function S(){E=!0,r.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{F(),V&&V.focus()},280)}function z(){E=!1,r.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{F();const e=K._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):s.focus()},280)}function R(){E?z():S()}L();let j=null;const I=5,M=120,P=document.createElement("div");function T(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;j={y:n,h:E?C:0,moved:!1},P.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function B(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>I&&(j.moved=!0),!j.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,j.h+t));o<M?(r.style.transition="none",r.style.height="32px"):(C=o,L(),E||S(),r.style.transition="none",r.style.height=C+"px"),F()}function A(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,o=j.y-t,a=j.h+o;j=null,P.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(a<M?z():(C=Math.max(M,Math.min(window.innerHeight-50,a)),L(),E||S()),F())}Object.assign(P.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(P),w.addEventListener("click",e=>{e.stopPropagation(),R()}),c.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",B),document.addEventListener("mouseup",A),c.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",B,{passive:!0}),document.addEventListener("touchend",A);let O=0,W=0;function D(e){_.textContent=e,_.style.display="block",S()}function U(){_.textContent="",_.style.display="none"}function N(){if(!K._p||"fit"!==K._mode)return;const e=K._w,n=K._h;if(!e||!n)return;const t=s.clientWidth,o=s.clientHeight,a=Math.min(t/e,o/n);u.style.transform=`scale(${a})`}function F(){if("fullscreen"===K._mode?K.size("max"):N(),Y&&"function"==typeof Y.windowResized)try{Y.windowResized()}catch(e){D(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{O=e.clientX,W=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(O=e.touches[0].clientX,W=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=K._p?K._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=K._w/n.width,o=K._h/n.height;return[(O-n.left)*t,(W-n.top)*o]},window.addEventListener("resize",F);let Y=null;const K=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const o=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=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),u.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),N())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){k.textContent=String(e)}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function H(){if(ve(),Y){try{Y.remove()}catch(e){}Y=null}p.innerHTML="",K._p=null,K._mode="fit",K._w=0,K._h=0,u.style.transform="scale(1)",k.textContent="Shift+Entrée → relancer  ·  Échap → ouvrir/fermer",te&&(te.destroy(),te=null),ee&&(ee.destroy(),ee=null),ne&&(ne.destroy(),ne=null),oe&&(oe.destroy(),oe=null),ae&&(ae.destroy(),ae=null),ie&&(ie.destroy(),ie=null),se&&(se.destroy(),se=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),fe&&(fe.destroy(),fe=null)}window.p5py=K;let V=null;function X(){!a.ace.startsWith("vendor")&&a.ace.startsWith("http")||ace.config.set("basePath",a.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.session.setMode("ace/mode/python"),V.setTheme("ace/theme/monokai"),V.setValue(n,-1),v.classList.toggle("pf-dirty",n!==t),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{me()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:z}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:J}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me())}});let e=null;V.session.on("change",()=>{clearTimeout(e),e=setTimeout(J,350),v.classList.toggle("pf-dirty",V.getValue()!==t)})}function J(){try{localStorage.setItem(o,V?V.getValue():n)}catch(e){}}window.addEventListener("beforeunload",J);let $=null,G=null;async function q(){return G||(G=(async()=>{const e={};if(a.pyodideIndex&&(e.indexURL=a.pyodideIndex),$=await loadPyodide(e),$.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('sketchTitle',):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# size() calls _pf_refresh after resizing so width/height are immediately\n# correct in setup() — consistent with p5.js JS behaviour.\nclass _SizeWrapper:\n def __call__(self, *a):\n p5py.size(*a)\n _pf_refresh(_ns_ref[0])\n return _GetCanvasWrapper()()\n def __repr__(self): return '<p5 function size>'\nsetattr(m, 'size', _SizeWrapper())\nsetattr(m, 'createCanvas', m.size) # alias — createCanvas(...) == size(...)\n_p5_functions.add('size')\n_p5_functions.add('createCanvas')\n_ns_ref = [{}] # filled in by runCode before each exec\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * — done later, after snake_case aliases are added\n\n# ── _pf_refresh: called before every event callback ───────────────\nimport re as _re\n\n# Pre-compute snake_case alias for each attribute — None if identical\n_attr_snake = {\n _k: (_re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _k) or None)\n for _k in _p5_attributes\n}\n_attr_snake = {_k: (_s if _s != _k else None) for _k, _s in _attr_snake.items()}\n\n# Add snake_case names to _p5_attributes so __all__ and _pf_refresh cover them\nfor _k, _sk in list(_attr_snake.items()):\n if _sk:\n _p5_attributes.add(_sk)\n setattr(m, _sk, getattr(m, _k, 0)) # initial value mirrors camelCase\n _attr_snake[_sk] = None # snake name has no further alias\n\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n _sk = _attr_snake.get(_k)\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k in ('mouseX', 'mouse_x') else my\n elif _sk is None and _k not in _attr_snake:\n # pure snake_case entry — skip, updated via its camelCase counterpart\n continue\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n if _sk:\n setattr(m, _sk, _v)\n if _sk in ns:\n ns[_sk] = _v\n\nsys.modules[\"p5\"] = m\n\ndef _snake_to_camel(name):\n parts = name.split('_')\n return parts[0] + ''.join(p.capitalize() for p in parts[1:])\n\n# Pre-populate snake_case aliases so \"from p5 import no_fill\" works\nfor _camel in list(vars(m).keys()):\n _snake = _re.sub(r'([A-Z])', lambda x: '_' + x.group(1).lower(), _camel)\n if _snake != _camel and not hasattr(m, _snake):\n setattr(m, _snake, getattr(m, _camel))\n if _camel in _p5_functions:\n _p5_functions.add(_snake)\n\n# Rebuild __all__ now that snake_case aliases are included\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\ndef _p5_getattr(name):\n camel = _snake_to_camel(name)\n if camel != name:\n val = getattr(m, camel, None)\n if val is not None:\n return val\n raise AttributeError(f\"module 'p5' has no attribute '{name}'\")\n\nm.__getattr__ = _p5_getattr\n"),V){Z($.runPython("list(m.__all__)").toJs())}})(),G)}function Z(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,o,a,i){i(null,a.length>0?n:[])}},o=ace.require("ace/ext/language_tools");o&&Array.isArray(o.completers)&&(o.completers=o.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,V.completers=[...V.completers||[],t]}let Q=!1,ee=null,ne=null,te=null,oe=null,ae=null,ie=null,se=null,re=null,de=null,le=null,ce=null,pe=null,ue=null,fe=null;async function me(){if(Q)return;Q=!0,h.classList.add("pf-running"),U(),H(),$||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await q()}catch(e){return f.style.display="none",D("Erreur Pyodide : "+e),Q=!1,void h.classList.remove("pf-running")}f.style.display="none";const t=V?V.getValue():n;try{m.textContent="Chargement des dépendances…",f.style.display="flex",await $.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",$.globals.set("_USER_CODE",t);try{$.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),$.runPython("_ns_ref[0] = _ns")}catch(e){return D(String(e)),Q=!1,void h.classList.remove("pf-running")}let o,a,i,s,r,d,l,c,u,_,y,g,b,v;try{const e=(e,n)=>$.runPython(`_ns.get('${e}') or _ns.get('${n}')`);r=e("preload","preload"),o=e("setup","setup"),a=e("draw","draw"),i=e("mousePressed","mouse_pressed"),s=e("keyPressed","key_pressed"),d=e("mouseDragged","mouse_dragged"),l=e("mouseReleased","mouse_released"),c=e("mouseMoved","mouse_moved"),u=e("mouseWheel","mouse_wheel"),_=e("doubleClicked","double_clicked"),y=e("keyReleased","key_released"),g=e("touchStarted","touch_started"),b=e("touchMoved","touch_moved"),v=e("touchEnded","touch_ended")}catch(e){return D(String(e)),Q=!1,void h.classList.remove("pf-running")}if(!a)return D("Le script doit définir au moins une fonction draw()."),Q=!1,void h.classList.remove("pf-running");const{create_proxy:x}=$.pyimport("pyodide.ffi"),w=$.runPython("_ns.get('windowResized')"),k=$.globals.get("_pf_refresh"),E=$.globals.get("_ns"),C=e=>e?x(()=>{try{k(E),e()}catch(e){D(String(e))}}):null;te=r?x(()=>{try{r()}catch(e){D(String(e))}}):null,ee=o?x(()=>{try{o()}catch(e){D(String(e))}}):null;const L=200;ne=x(()=>{try{k(E);const e=performance.now();a(),performance.now()-e>L&&(H(),D(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){D(String(e)),H()}}),oe=C(i),ae=C(l),ie=C(d),se=C(c),re=C(u),de=C(_),le=C(s),ce=C(y),pe=C(g),ue=C(b),fe=C(v);const S=w?x(()=>{try{w()}catch(e){D(String(e))}}):null;let z=!1;Y=new p5(e=>{K._setP(e),te&&(e.preload=()=>{te()}),e.setup=()=>{ee&&ee(),e.canvas||K.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),z=!0},e.draw=()=>{z&&ne()},e.mousePressed=()=>{z&&oe&&oe()},e.mouseReleased=()=>{z&&ae&&ae()},e.mouseDragged=()=>{z&&ie&&ie()},e.mouseMoved=()=>{z&&se&&se()},e.mouseWheel=e=>{z&&re&&re()},e.doubleClicked=()=>{z&&de&&de()},e.keyPressed=()=>{z&&le&&le()},e.keyReleased=()=>{z&&ce&&ce()},e.touchStarted=()=>{z&&pe&&pe()},e.touchMoved=()=>{z&&ue&&ue()},e.touchEnded=()=>{z&&fe&&fe()},e.windowResized=()=>{"fullscreen"===K._mode?K.size("max"):N(),S&&S()}},p),Q=!1,h.classList.remove("pf-running")}const _e='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.4.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=_e.replace("FILLME-PYTHON",e),o=new Blob([t],{type:"text/html;charset=utf-8"}),a=URL.createObjectURL(o),i=Object.assign(document.createElement("a"),{href:a,download:"sketch.html"});document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(a)}h.addEventListener("click",()=>me()),y.addEventListener("click",()=>{E?z():(C=window.innerHeight-32,L(),S())});let ye=null,ge=[];function be(){const e=K._p?.canvas;if(!e)return;const n=["video/webm;codecs=vp9","video/webm;codecs=vp8","video/webm"].find(e=>MediaRecorder.isTypeSupported(e))||"video/webm",t=e.captureStream();ye=new MediaRecorder(t,{mimeType:n}),ge=[],ye.ondataavailable=e=>{e.data.size&&ge.push(e.data)},ye.onstop=()=>{const e=new Blob(ge,{type:n}),t=URL.createObjectURL(e),o=n.includes("webm")?"webm":"mp4";Object.assign(document.createElement("a"),{href:t,download:`sketch.${o}`}).click(),URL.revokeObjectURL(t),b.textContent="⏺",b.title="Enregistrer WebM",b.classList.remove("pf-recording"),ye=null},ye.start(),b.textContent="⏹",b.title="Arrêter l'enregistrement",b.classList.add("pf-recording")}function ve(){ye&&"inactive"!==ye.state&&ye.stop()}b.addEventListener("click",()=>{ye?ve():be()}),g.addEventListener("click",he);const xe="https://codeberg.org/nopid/pyfrilet";function we(e){return new Promise((n,t)=>{const o=document.createElement("script");o.src=e,o.onload=n,o.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(o)})}x.addEventListener("click",()=>window.open(xe,"_blank")),v.addEventListener("click",()=>{V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me())}),window.addEventListener("keydown",e=>{const n=E&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void me();if("Escape"===e.key)return n?void setTimeout(()=>{E&&z()},0):(e.preventDefault(),e.stopPropagation(),void(E?z():S()));if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),me()))):(e.preventDefault(),void J())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await we(a.p5),await we(a.ace),await we(a.acePython),await we(a.aceMonokai),await we(a.aceLangTools),await we(a.aceSearchbox),await we(a.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}X(),await me(),f.style.display="none"})()}(h&&h.trim()?h:m,m,_,f)})}();