pyfrilet 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/pyfrilet.js +71 -2
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -242,7 +242,8 @@ La barre de contrôle est collée en bas de l'écran.
|
|
|
242
242
|
| ▶ | Relance l'exécution du code (sans fermer l'éditeur) |
|
|
243
243
|
| ✏️ | Ouvre l'éditeur en **plein écran** ; referme si déjà ouvert |
|
|
244
244
|
| 💾 | Télécharge la page en HTML autonome (voir ci-dessous) |
|
|
245
|
-
|
|
|
245
|
+
| ⏺ | Démarre l'enregistrement WebM du canvas ; devient ⏹ pendant l'enregistrement — cliquer pour arrêter et télécharger |
|
|
246
|
+
| ↻ | 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
247
|
|
|
247
248
|
### Raccourcis clavier
|
|
248
249
|
|
package/package.json
CHANGED
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)">▶</button>
|
|
226
240
|
<button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>
|
|
227
241
|
<button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>
|
|
242
|
+
<button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</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)">↻</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
|
|
|
@@ -719,10 +738,21 @@ for _n in _p5_functions:
|
|
|
719
738
|
# them — but our Proxy overrides them to also toggle CSS image-rendering.
|
|
720
739
|
# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,
|
|
721
740
|
# so introspection misses them — add them explicitly.
|
|
722
|
-
for _n in ('
|
|
741
|
+
for _n in ('sketchTitle',):
|
|
723
742
|
setattr(m, _n, _FW(_n))
|
|
724
743
|
_p5_functions.add(_n) # keep __all__ consistent
|
|
725
744
|
|
|
745
|
+
# size() calls _pf_refresh after resizing so width/height are immediately
|
|
746
|
+
# correct in setup() — consistent with p5.js JS behaviour.
|
|
747
|
+
class _SizeWrapper:
|
|
748
|
+
def __call__(self, *a):
|
|
749
|
+
p5py.size(*a)
|
|
750
|
+
_pf_refresh(_ns_ref[0])
|
|
751
|
+
def __repr__(self): return '<p5 function size>'
|
|
752
|
+
setattr(m, 'size', _SizeWrapper())
|
|
753
|
+
_p5_functions.add('size')
|
|
754
|
+
_ns_ref = [{}] # filled in by runCode before each exec
|
|
755
|
+
|
|
726
756
|
# getCanvas() — returns the p5.Element wrapping the canvas,
|
|
727
757
|
# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.
|
|
728
758
|
class _GetCanvasWrapper:
|
|
@@ -853,6 +883,7 @@ sys.modules["p5"] = m
|
|
|
853
883
|
|
|
854
884
|
try {
|
|
855
885
|
pyodide.runPython('_ns = {}; exec(_USER_CODE, _ns, _ns)');
|
|
886
|
+
pyodide.runPython('_ns_ref[0] = _ns'); /* give size() access to current ns */
|
|
856
887
|
} catch (e) {
|
|
857
888
|
showError(String(e));
|
|
858
889
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
@@ -1010,7 +1041,45 @@ FILLME-PYTHON
|
|
|
1010
1041
|
}
|
|
1011
1042
|
});
|
|
1012
1043
|
|
|
1013
|
-
|
|
1044
|
+
/* ── WebM recording ─────────────────────────────────────────────── */
|
|
1045
|
+
let mediaRecorder = null;
|
|
1046
|
+
let recChunks = [];
|
|
1047
|
+
|
|
1048
|
+
function startRecording() {
|
|
1049
|
+
const canvas = p5Bridge._p?.canvas;
|
|
1050
|
+
if (!canvas) return;
|
|
1051
|
+
const mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']
|
|
1052
|
+
.find(m => MediaRecorder.isTypeSupported(m)) || 'video/webm';
|
|
1053
|
+
const stream = canvas.captureStream();
|
|
1054
|
+
mediaRecorder = new MediaRecorder(stream, { mimeType });
|
|
1055
|
+
recChunks = [];
|
|
1056
|
+
mediaRecorder.ondataavailable = e => { if (e.data.size) recChunks.push(e.data); };
|
|
1057
|
+
mediaRecorder.onstop = () => {
|
|
1058
|
+
const blob = new Blob(recChunks, { type: mimeType });
|
|
1059
|
+
const url = URL.createObjectURL(blob);
|
|
1060
|
+
const ext = mimeType.includes('webm') ? 'webm' : 'mp4';
|
|
1061
|
+
Object.assign(document.createElement('a'), { href: url, download: `sketch.${ext}` }).click();
|
|
1062
|
+
URL.revokeObjectURL(url);
|
|
1063
|
+
btnRec.textContent = '⏺';
|
|
1064
|
+
btnRec.title = 'Enregistrer WebM';
|
|
1065
|
+
btnRec.classList.remove('pf-recording');
|
|
1066
|
+
mediaRecorder = null;
|
|
1067
|
+
};
|
|
1068
|
+
mediaRecorder.start();
|
|
1069
|
+
btnRec.textContent = '⏹';
|
|
1070
|
+
btnRec.title = 'Arrêter l\'enregistrement';
|
|
1071
|
+
btnRec.classList.add('pf-recording');
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function stopRecording() {
|
|
1075
|
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
btnRec.addEventListener('click', () => {
|
|
1079
|
+
mediaRecorder ? stopRecording() : startRecording();
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
btnDl.addEventListener('click', download);
|
|
1014
1083
|
const HELP_URL = 'https://codeberg.org/nopid/pyfrilet';
|
|
1015
1084
|
btnHelp.addEventListener('click', () => window.open(HELP_URL, '_blank'));
|
|
1016
1085
|
|
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",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",d="\nhtml, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n}\n#pf-ace { position: absolute; inset: 0; }\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n",l='\n<div id="pf-root">\n <div id="pf-app">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-ace"></div>\n </div>\n <pre id="pf-err"></pre>\n </div>\n</div>\n';document.addEventListener("DOMContentLoaded",function(){const c=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!c)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const p=(c.getAttribute("data-sources")||c.getAttribute("sources")||"local").toLowerCase().trim(),u=(c.getAttribute("data-vendor")||c.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/");e="cdn"===p;const f=e?{p5:n,pyodide:t,pyodideIndex:null,ace:o,acePython:i,aceMonokai:s,aceLangTools:a,aceSearchbox:r}:{p5:u+"p5.min.js",pyodide:u+"pyodide/pyodide.js",pyodideIndex:u+"pyodide/",ace:u+"ace.min.js",acePython:u+"mode-python.min.js",aceMonokai:u+"theme-monokai.min.js",aceLangTools:u+"ext-language_tools.min.js",aceSearchbox:u+"ext-searchbox.min.js"},m=c.textContent.replace(/^\n/,""),h="pyfrilet:"+location.pathname,y=(()=>{try{return localStorage.getItem(h)}catch(e){return null}})();!function(n,t,o,i){const s=document.createElement("style");s.textContent=d,document.head.appendChild(s),document.body.innerHTML=l;const a=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),c=document.getElementById("pf-handle"),p=document.getElementById("pf-sketch"),u=document.getElementById("pf-viewport"),f=document.getElementById("pf-loader"),m=document.getElementById("pf-loader-msg"),h=document.getElementById("pf-err"),y=document.getElementById("pf-btn-run"),_=document.getElementById("pf-btn-code"),g=document.getElementById("pf-btn-dl"),b=document.getElementById("pf-btn-reset"),v=document.getElementById("pf-btn-help"),x=document.getElementById("pf-grip"),w=document.getElementById("pf-handle-hint");let k=!1,E=Math.round(.56*window.innerHeight);function C(){document.documentElement.style.setProperty("--pf-drawer-h",E+"px")}function L(){k=!0,r.classList.add("pf-open"),_.classList.add("pf-active"),setTimeout(()=>{Y(),V&&V.focus()},280)}function S(){k=!1,r.classList.remove("pf-open"),_.classList.remove("pf-active"),setTimeout(()=>{Y();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):a.focus()},280)}function P(){k?S():L()}C();let j=null;const R=5,z=120,I=document.createElement("div");function M(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;j={y:n,h:k?E:0,moved:!1},I.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function T(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>R&&(j.moved=!0),!j.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,j.h+t));o<z?(r.style.transition="none",r.style.height="32px"):(E=o,C(),k||L(),r.style.transition="none",r.style.height=E+"px"),Y()}function B(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,o=j.y-t,i=j.h+o;j=null,I.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(i<z?S():(E=Math.max(z,Math.min(window.innerHeight-50,i)),C(),k||L()),Y())}Object.assign(I.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(I),x.addEventListener("click",e=>{e.stopPropagation(),P()}),c.addEventListener("mousedown",M,!0),document.addEventListener("mousemove",T),document.addEventListener("mouseup",B),c.addEventListener("touchstart",M,{passive:!1}),document.addEventListener("touchmove",T,{passive:!0}),document.addEventListener("touchend",B);let O=0,A=0;function D(e){h.textContent=e,h.style.display="block",L()}function W(){h.textContent="",h.style.display="none"}function F(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=a.clientWidth,o=a.clientHeight,i=Math.min(t/e,o/n);u.style.transform=`scale(${i})`}function Y(){if("fullscreen"===H._mode?H.size("max"):F(),K&&"function"==typeof K.windowResized)try{K.windowResized()}catch(e){D(String(e))}V&&V.resize()}window.addEventListener("mousemove",e=>{O=e.clientX,A=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(O=e.touches[0].clientX,A=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=H._p?H._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=H._w/n.width,o=H._h/n.height;return[(O-n.left)*t,(A-n.top)*o]},window.addEventListener("resize",Y);let K=null;const H=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const o=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=a.clientWidth,this._h=a.clientHeight,void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),u.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),F())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){w.textContent=String(e)}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function U(){if(K){try{K.remove()}catch(e){}K=null}p.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,u.style.transform="scale(1)",w.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",ne&&(ne.destroy(),ne=null),Z&&(Z.destroy(),Z=null),ee&&(ee.destroy(),ee=null),te&&(te.destroy(),te=null),oe&&(oe.destroy(),oe=null),ie&&(ie.destroy(),ie=null),se&&(se.destroy(),se=null),ae&&(ae.destroy(),ae=null),re&&(re.destroy(),re=null),de&&(de.destroy(),de=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null),ue&&(ue.destroy(),ue=null)}window.p5py=H;let V=null;function X(){!i.ace.startsWith("vendor")&&i.ace.startsWith("http")||ace.config.set("basePath",i.ace.replace(/\/[^/]+$/,"/")),V=ace.edit("pf-ace"),V.session.setMode("ace/mode/python"),V.setTheme("ace/theme/monokai"),V.setValue(n,-1),V.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),V.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{fe()}}),V.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:S}),V.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:N}),V.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe())}});let e=null;V.session.on("change",()=>{clearTimeout(e),e=setTimeout(N,350)})}function N(){try{localStorage.setItem(o,V?V.getValue():n)}catch(e){}}window.addEventListener("beforeunload",N);let J=null,G=null;async function $(){return G||(G=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),J=await loadPyodide(e),J.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('size', 'sketchTitle'):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# getCanvas() — returns the p5.Element wrapping the canvas,\n# so the user can call .drop(create_proxy(fn)), .mouseOver(), etc. directly like in JS.\nclass _GetCanvasWrapper:\n def __call__(self):\n p = p5py._p\n if p is None:\n raise RuntimeError('getCanvas() doit être appelé dans setup() ou après')\n p.canvas.id = '__pf_canvas__'\n return p.select('#__pf_canvas__')\n def __repr__(self): return '<p5 function getCanvas>'\nsetattr(m, 'getCanvas', _GetCanvasWrapper())\n_p5_functions.add('getCanvas')\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * (after all explicit additions)\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\n# ── _pf_refresh: called before every event callback ───────────────\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k == 'mouseX' else my\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n\nsys.modules[\"p5\"] = m\n"),V){q(J.runPython("list(m.__all__)").toJs())}})(),G)}function q(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,o,i,s){s(null,i.length>0?n:[])}},o=ace.require("ace/ext/language_tools");o&&Array.isArray(o.completers)&&(o.completers=o.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,V.completers=[...V.completers||[],t]}let Q=!1,Z=null,ee=null,ne=null,te=null,oe=null,ie=null,se=null,ae=null,re=null,de=null,le=null,ce=null,pe=null,ue=null;async function fe(){if(Q)return;Q=!0,y.classList.add("pf-running"),W(),U(),J||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await $()}catch(e){return f.style.display="none",D("Erreur Pyodide : "+e),Q=!1,void y.classList.remove("pf-running")}f.style.display="none";const t=V?V.getValue():n;try{m.textContent="Chargement des dépendances…",f.style.display="flex",await J.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",J.globals.set("_USER_CODE",t);try{J.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return D(String(e)),Q=!1,void y.classList.remove("pf-running")}let o,i,s,a,r,d,l,c,u,h,_,g,b,v;try{r=J.runPython("_ns.get('preload')"),o=J.runPython("_ns.get('setup')"),i=J.runPython("_ns.get('draw')"),s=J.runPython("_ns.get('mousePressed')"),a=J.runPython("_ns.get('keyPressed')"),d=J.runPython("_ns.get('mouseDragged')"),l=J.runPython("_ns.get('mouseReleased')"),c=J.runPython("_ns.get('mouseMoved')"),u=J.runPython("_ns.get('mouseWheel')"),h=J.runPython("_ns.get('doubleClicked')"),_=J.runPython("_ns.get('keyReleased')"),g=J.runPython("_ns.get('touchStarted')"),b=J.runPython("_ns.get('touchMoved')"),v=J.runPython("_ns.get('touchEnded')")}catch(e){return D(String(e)),Q=!1,void y.classList.remove("pf-running")}if(!i)return D("Le script doit définir au moins une fonction draw()."),Q=!1,void y.classList.remove("pf-running");const{create_proxy:x}=J.pyimport("pyodide.ffi"),w=J.runPython("_ns.get('windowResized')"),k=J.globals.get("_pf_refresh"),E=J.globals.get("_ns"),C=e=>e?x(()=>{try{k(E),e()}catch(e){D(String(e))}}):null;ne=r?x(()=>{try{r()}catch(e){D(String(e))}}):null,Z=o?x(()=>{try{o()}catch(e){D(String(e))}}):null;const L=200;ee=x(()=>{try{k(E);const e=performance.now();i(),performance.now()-e>L&&(U(),D(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){D(String(e)),U()}}),te=C(s),oe=C(l),ie=C(d),se=C(c),ae=C(u),re=C(h),de=C(a),le=C(_),ce=C(g),pe=C(b),ue=C(v);const S=w?x(()=>{try{w()}catch(e){D(String(e))}}):null;let P=!1;K=new p5(e=>{H._setP(e),ne&&(e.preload=()=>{ne()}),e.setup=()=>{Z&&Z(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),P=!0},e.draw=()=>{P&&ee()},e.mousePressed=()=>{P&&te&&te()},e.mouseReleased=()=>{P&&oe&&oe()},e.mouseDragged=()=>{P&&ie&&ie()},e.mouseMoved=()=>{P&&se&&se()},e.mouseWheel=e=>{P&&ae&&ae()},e.doubleClicked=()=>{P&&re&&re()},e.keyPressed=()=>{P&&de&&de()},e.keyReleased=()=>{P&&le&&le()},e.touchStarted=()=>{P&&ce&&ce()},e.touchMoved=()=>{P&&pe&&pe()},e.touchEnded=()=>{P&&ue&&ue()},e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):F(),S&&S()}},p),Q=!1,y.classList.remove("pf-running")}const me='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.3.0/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function he(){const e=V?V.getValue():n,t=me.replace("FILLME-PYTHON",e),o=new Blob([t],{type:"text/html;charset=utf-8"}),i=URL.createObjectURL(o),s=Object.assign(document.createElement("a"),{href:i,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(i)}y.addEventListener("click",()=>fe()),_.addEventListener("click",()=>{k?S():(E=window.innerHeight-32,C(),L())}),g.addEventListener("click",he);const ye="https://codeberg.org/nopid/pyfrilet";function _e(e){return new Promise((n,t)=>{const o=document.createElement("script");o.src=e,o.onload=n,o.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(o)})}v.addEventListener("click",()=>window.open(ye,"_blank")),b.addEventListener("click",()=>{V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe())}),window.addEventListener("keydown",e=>{const n=k&&V&&V.isFocused&&V.isFocused();if(n||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void fe();if("Escape"===e.key)return n?void setTimeout(()=>{k&&S()},0):(e.preventDefault(),e.stopPropagation(),void(k?S():L()));if(!n)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(V&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(V.setValue(t,-1),fe()))):(e.preventDefault(),void N())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await _e(i.p5),await _e(i.ace),await _e(i.acePython),await _e(i.aceMonokai),await _e(i.aceLangTools),await _e(i.aceSearchbox),await _e(i.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}X(),await fe(),f.style.display="none"})()}(y&&y.trim()?y:m,m,h,f)})}();
|
|
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-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 · Shift+Entrée → relancer</span>\n <div id="pf-handle-btns">\n <button class="pf-btn" id="pf-btn-run" title="Relancer (Shift+Entrée)">▶</button>\n <button class="pf-btn" id="pf-btn-code" title="Éditeur plein écran">✏️</button>\n <button class="pf-btn" id="pf-btn-dl" title="Télécharger HTML autonome">💾</button>\n <button class="pf-btn" id="pf-btn-rec" title="Enregistrer WebM">⏺</button>\n <button class="pf-btn" id="pf-btn-help" title="Aide">?</button>\n <button class="pf-btn" id="pf-btn-reset" title="Réinitialiser le code (Ctrl+R)">↻</button>\n </div>\n </div>\n <div id="pf-editor-wrap">\n <div id="pf-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,y=(()=>{try{return localStorage.getItem(h)}catch(e){return null}})();!function(n,t,o,i){const s=document.createElement("style");s.textContent=d,document.head.appendChild(s),document.body.innerHTML=l;const 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"),y=document.getElementById("pf-btn-run"),_=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,L=Math.round(.56*window.innerHeight);function C(){document.documentElement.style.setProperty("--pf-drawer-h",L+"px")}function S(){E=!0,a.classList.add("pf-open"),_.classList.add("pf-active"),setTimeout(()=>{Y(),X&&X.focus()},280)}function P(){E=!1,a.classList.remove("pf-open"),_.classList.remove("pf-active"),setTimeout(()=>{Y();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):r.focus()},280)}function R(){E?P():S()}C();let z=null;const j=5,I=120,M=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;z={y:n,h:E?L:0,moved:!1},M.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function B(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<I?(a.style.transition="none",a.style.height="32px"):(L=o,C(),E||S(),a.style.transition="none",a.style.height=L+"px"),Y()}function O(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,M.style.display="none",document.body.style.userSelect="",a.style.transition="",a.style.height="",n&&(i<I?P():(L=Math.max(I,Math.min(window.innerHeight-50,i)),C(),E||S()),Y())}Object.assign(M.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(M),w.addEventListener("click",e=>{e.stopPropagation(),R()}),c.addEventListener("mousedown",T,!0),document.addEventListener("mousemove",B),document.addEventListener("mouseup",O),c.addEventListener("touchstart",T,{passive:!1}),document.addEventListener("touchmove",B,{passive:!0}),document.addEventListener("touchend",O);let A=0,W=0;function D(e){h.textContent=e,h.style.display="block",S()}function U(){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){D(String(e))}X&&X.resize()}window.addEventListener("mousemove",e=>{A=e.clientX,W=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(A=e.touches[0].clientX,W=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[(A-n.left)*t,(W-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){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 V(){if(ve(),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)",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),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),fe&&(fe.destroy(),fe=null)}window.p5py=H;let X=null;function J(){!i.ace.startsWith("vendor")&&i.ace.startsWith("http")||ace.config.set("basePath",i.ace.replace(/\/[^/]+$/,"/")),X=ace.edit("pf-ace"),X.session.setMode("ace/mode/python"),X.setTheme("ace/theme/monokai"),X.setValue(n,-1),v.classList.toggle("pf-dirty",n!==t),X.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),X.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{me()}}),X.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:P}),X.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:N}),X.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(X.setValue(t,-1),me())}});let e=null;X.session.on("change",()=>{clearTimeout(e),e=setTimeout(N,350),v.classList.toggle("pf-dirty",X.getValue()!==t)})}function N(){try{localStorage.setItem(o,X?X.getValue():n)}catch(e){}}window.addEventListener("beforeunload",N);let G=null,$=null;async function q(){return $||($=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),G=await loadPyodide(e),G.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"),X){Q(G.runPython("list(m.__all__)").toJs())}})(),$)}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,X.completers=[...X.completers||[],t]}let Z=!1,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,fe=null;async function me(){if(Z)return;Z=!0,y.classList.add("pf-running"),U(),V(),G||(m.textContent="Initialisation de Pyodide…",f.style.display="flex");try{await q()}catch(e){return f.style.display="none",D("Erreur Pyodide : "+e),Z=!1,void y.classList.remove("pf-running")}f.style.display="none";const t=X?X.getValue():n;try{m.textContent="Chargement des dépendances…",f.style.display="flex",await G.loadPackagesFromImports(t,{messageCallback:()=>{},checkIntegrity:e})}catch(e){console.warn("[pyfrilet] loadPackagesFromImports:",e)}f.style.display="none",G.globals.set("_USER_CODE",t);try{G.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)"),G.runPython("_ns_ref[0] = _ns")}catch(e){return D(String(e)),Z=!1,void y.classList.remove("pf-running")}let o,i,s,r,a,d,l,c,u,h,_,g,b,v;try{a=G.runPython("_ns.get('preload')"),o=G.runPython("_ns.get('setup')"),i=G.runPython("_ns.get('draw')"),s=G.runPython("_ns.get('mousePressed')"),r=G.runPython("_ns.get('keyPressed')"),d=G.runPython("_ns.get('mouseDragged')"),l=G.runPython("_ns.get('mouseReleased')"),c=G.runPython("_ns.get('mouseMoved')"),u=G.runPython("_ns.get('mouseWheel')"),h=G.runPython("_ns.get('doubleClicked')"),_=G.runPython("_ns.get('keyReleased')"),g=G.runPython("_ns.get('touchStarted')"),b=G.runPython("_ns.get('touchMoved')"),v=G.runPython("_ns.get('touchEnded')")}catch(e){return D(String(e)),Z=!1,void y.classList.remove("pf-running")}if(!i)return D("Le script doit définir au moins une fonction draw()."),Z=!1,void y.classList.remove("pf-running");const{create_proxy:x}=G.pyimport("pyodide.ffi"),w=G.runPython("_ns.get('windowResized')"),k=G.globals.get("_pf_refresh"),E=G.globals.get("_ns"),L=e=>e?x(()=>{try{k(E),e()}catch(e){D(String(e))}}):null;te=a?x(()=>{try{a()}catch(e){D(String(e))}}):null,ee=o?x(()=>{try{o()}catch(e){D(String(e))}}):null;const C=200;ne=x(()=>{try{k(E);const e=performance.now();i(),performance.now()-e>C&&(V(),D(`draw() a mis plus de ${C} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){D(String(e)),V()}}),oe=L(s),ie=L(l),se=L(d),re=L(c),ae=L(u),de=L(h),le=L(r),ce=L(_),pe=L(g),ue=L(b),fe=L(v);const S=w?x(()=>{try{w()}catch(e){D(String(e))}}):null;let P=!1;K=new p5(e=>{H._setP(e),te&&(e.preload=()=>{te()}),e.setup=()=>{ee&&ee(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),P=!0},e.draw=()=>{P&&ne()},e.mousePressed=()=>{P&&oe&&oe()},e.mouseReleased=()=>{P&&ie&&ie()},e.mouseDragged=()=>{P&&se&&se()},e.mouseMoved=()=>{P&&re&&re()},e.mouseWheel=e=>{P&&ae&&ae()},e.doubleClicked=()=>{P&&de&&de()},e.keyPressed=()=>{P&&le&&le()},e.keyReleased=()=>{P&&ce&&ce()},e.touchStarted=()=>{P&&pe&&pe()},e.touchMoved=()=>{P&&ue&&ue()},e.touchEnded=()=>{P&&fe&&fe()},e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):F(),S&&S()}},p),Z=!1,y.classList.remove("pf-running")}const he='<!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.2/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function ye(){const e=X?X.getValue():n,t=he.replace("FILLME-PYTHON",e),o=new Blob([t],{type:"text/html;charset=utf-8"}),i=URL.createObjectURL(o),s=Object.assign(document.createElement("a"),{href:i,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(i)}y.addEventListener("click",()=>me()),_.addEventListener("click",()=>{E?P():(L=window.innerHeight-32,C(),S())});let _e=null,ge=[];function be(){const e=H._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();_e=new MediaRecorder(t,{mimeType:n}),ge=[],_e.ondataavailable=e=>{e.data.size&&ge.push(e.data)},_e.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"),_e=null},_e.start(),b.textContent="⏹",b.title="Arrêter l'enregistrement",b.classList.add("pf-recording")}function ve(){_e&&"inactive"!==_e.state&&_e.stop()}b.addEventListener("click",()=>{_e?ve():be()}),g.addEventListener("click",ye);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",()=>{X&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(X.setValue(t,-1),me())}),window.addEventListener("keydown",e=>{const n=E&&X&&X.isFocused&&X.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&&P()},0):(e.preventDefault(),e.stopPropagation(),void(E?P():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(X&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(X.setValue(t,-1),me()))):(e.preventDefault(),void N())}else e.preventDefault()},!0),(async()=>{m.textContent="Chargement des dépendances…",f.style.display="flex";try{await we(i.p5),await we(i.ace),await we(i.acePython),await we(i.aceMonokai),await we(i.aceLangTools),await we(i.aceSearchbox),await we(i.pyodide)}catch(e){return m.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}J(),await me(),f.style.display="none"})()}(y&&y.trim()?y:m,m,h,f)})}();
|