pyfrilet 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -7
- package/package.json +1 -1
- package/pyfrilet.js +78 -23
- package/pyfrilet.min.js +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Un sketch est un fichier HTML minimal avec son code Python embarqué.
|
|
|
15
15
|
<head>
|
|
16
16
|
<meta charset="utf-8">
|
|
17
17
|
<title>Mon sketch</title>
|
|
18
|
-
<script src="pyfrilet.js"></script>
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
|
21
21
|
<script type="text/python" data-sources="cdn">
|
|
@@ -77,17 +77,28 @@ Ces fonctions sont définies par l'utilisateur et appelées automatiquement par
|
|
|
77
77
|
|
|
78
78
|
| Fonction | Rôle |
|
|
79
79
|
|---|---|
|
|
80
|
+
| `preload()` | Appelée avant `setup()`. Utilisée pour charger des ressources (`loadImage`…) — p5 attend la fin du chargement avant de continuer. |
|
|
80
81
|
| `setup()` | Appelée une seule fois au démarrage. Obligatoire pour appeler `size()` ou `frameRate()`. |
|
|
81
82
|
| `draw()` | Appelée à chaque frame. **Obligatoire.** |
|
|
82
|
-
| `mousePressed()` | Appelée à chaque clic sur le canvas. |
|
|
83
|
-
| `keyPressed()` | Appelée à chaque pression de touche. `keyCode` et `key` sont déjà à jour au moment de l'appel. |
|
|
84
83
|
| `windowResized()` | Appelée quand la zone d'affichage change (redimensionnement de la fenêtre ou du tiroir éditeur). |
|
|
84
|
+
| `mousePressed()` | Bouton souris enfoncé. |
|
|
85
|
+
| `mouseReleased()` | Bouton souris relâché. |
|
|
86
|
+
| `mouseDragged()` | Souris déplacée bouton enfoncé. |
|
|
87
|
+
| `mouseMoved()` | Souris déplacée sans bouton (survol). |
|
|
88
|
+
| `mouseWheel()` | Molette / pinch trackpad. `deltaY` est accessible via `p5py.deltaY` si besoin. |
|
|
89
|
+
| `doubleClicked()` | Double-clic. |
|
|
90
|
+
| `keyPressed()` | Touche enfoncée. `keyCode` et `key` sont déjà à jour au moment de l'appel. |
|
|
91
|
+
| `keyReleased()` | Touche relâchée. Utile pour les mouvements continus dans les jeux. |
|
|
92
|
+
| `touchStarted()` | Début de contact tactile. `touches[]` est accessible via p5. |
|
|
93
|
+
| `touchMoved()` | Contact tactile déplacé. Utile pour le multi-touch (pinch zoom…). |
|
|
94
|
+
| `touchEnded()` | Fin de contact tactile. |
|
|
85
95
|
|
|
86
96
|
#### Fonctions pyfrilet (hors p5.js standard)
|
|
87
97
|
|
|
88
98
|
| Fonction | Description |
|
|
89
99
|
|---|---|
|
|
90
100
|
| `size(w, h)` | Crée ou redimensionne le canvas. Le canvas est mis à l'échelle CSS pour remplir l'écran sans déformation. |
|
|
101
|
+
| `size(w, h, WEBGL)` | Crée un canvas en mode WebGL 3D. `WEBGL` est une constante p5 importable avec `from p5 import *`. |
|
|
91
102
|
| `size('max')` | Canvas plein écran, redimensionné dynamiquement. |
|
|
92
103
|
| `smooth()` | Active l'antialiasing (par défaut). |
|
|
93
104
|
| `noSmooth()` | Désactive l'antialiasing (pixel art). Affecte formes **et** texte. |
|
|
@@ -192,20 +203,38 @@ Le code est **sauvegardé automatiquement** dans le `localStorage` à chaque mod
|
|
|
192
203
|
|
|
193
204
|
### Télécharger
|
|
194
205
|
|
|
195
|
-
Le bouton 💾 génère un fichier `sketch.html` autonome :
|
|
206
|
+
Le bouton 💾 génère un fichier `sketch.html` autonome : il référence pyfrilet depuis jsDelivr et embarque le code Python tel qu'il est dans l'éditeur au moment du clic. Le fichier produit charge p5.js et Pyodide depuis le CDN et n'a besoin d'aucun serveur pour fonctionner.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Utiliser pyfrilet depuis le CDN
|
|
211
|
+
|
|
212
|
+
La façon la plus simple d'utiliser pyfrilet est de le charger depuis jsDelivr, sans rien installer :
|
|
213
|
+
|
|
214
|
+
```html
|
|
215
|
+
<script src="https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js"></script>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Pour épingler une version précise et éviter les surprises lors d'une montée de version :
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.1.0/pyfrilet.min.js"></script>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
> **Note** : `@latest` est mis en cache jusqu'à 7 jours par les navigateurs — une version épinglée est préférable dès qu'on destine un sketch à un public.
|
|
196
225
|
|
|
197
226
|
---
|
|
198
227
|
|
|
199
228
|
## Déploiement local (mode `vendor/`)
|
|
200
229
|
|
|
201
|
-
Par défaut (`data-sources="cdn"`), pyfrilet charge
|
|
230
|
+
Par défaut (`data-sources="cdn"`), pyfrilet charge p5.js, Pyodide et ACE depuis des CDN publics, ce qui nécessite une connexion internet. Pour un déploiement entièrement local — intranet, usage hors ligne, ou simplement pour ne pas dépendre de services tiers — on peut héberger les dépendances soi-même.
|
|
202
231
|
|
|
203
232
|
### Utiliser une copie locale de pyfrilet.js
|
|
204
233
|
|
|
205
|
-
|
|
234
|
+
On remplace la balise jsDelivr par une référence locale :
|
|
206
235
|
|
|
207
236
|
```html
|
|
208
|
-
<!-- au lieu de : <script src="https
|
|
237
|
+
<!-- au lieu de : <script src="https://cdn.jsdelivr.net/npm/pyfrilet@…/pyfrilet.min.js"></script> -->
|
|
209
238
|
<script src="pyfrilet.js"></script>
|
|
210
239
|
```
|
|
211
240
|
|
|
@@ -279,6 +308,30 @@ Puis ouvrir `http://localhost:8000/mon-sketch.html`.
|
|
|
279
308
|
|
|
280
309
|
---
|
|
281
310
|
|
|
311
|
+
## Build et publication
|
|
312
|
+
|
|
313
|
+
Le build est géré par `build.js` (Node.js + Terser). Il injecte automatiquement la version courante dans la constante `PYFRILET_CDN` du fichier source, puis génère `pyfrilet.min.js`.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm run build # génère pyfrilet.min.js
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Le hook `prepublishOnly` dans `package.json` déclenche le build automatiquement avant chaque `npm publish`. Le flux complet d'une release :
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
git add .
|
|
323
|
+
git commit -m "feat: ..." # committer le travail
|
|
324
|
+
|
|
325
|
+
npm version patch # (ou minor / major) — modifie package.json, commit + tag
|
|
326
|
+
npm publish # build automatique puis publication sur npm
|
|
327
|
+
|
|
328
|
+
git push && git push --tags # pousser commits et tag sur Codeberg
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`npm version` incrémente le numéro de version selon les conventions semver : `patch` pour `z`, `minor` pour `y` (remet `z` à 0), `major` pour `x` (remet `y` et `z` à 0).
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
282
335
|
## Licence
|
|
283
336
|
|
|
284
337
|
pyfrilet.js est distribué sous **GNU Lesser General Public License v2.1** (LGPL-2.1).
|
package/package.json
CHANGED
package/pyfrilet.js
CHANGED
|
@@ -562,10 +562,20 @@ function main(initialCode, starterCode, SK, URLS) {
|
|
|
562
562
|
p5Bridge._p = null; p5Bridge._mode = 'fit'; p5Bridge._w = 0; p5Bridge._h = 0;
|
|
563
563
|
viewEl.style.transform = 'scale(1)';
|
|
564
564
|
hintEl.textContent = 'Shift+Entrée → relancer \u00a0·\u00a0 Échap → ouvrir/fermer';
|
|
565
|
-
if (
|
|
566
|
-
if (
|
|
567
|
-
if (
|
|
568
|
-
if (
|
|
565
|
+
if (preloadProxy) { preloadProxy.destroy(); preloadProxy = null; }
|
|
566
|
+
if (setupProxy) { setupProxy.destroy(); setupProxy = null; }
|
|
567
|
+
if (drawProxy) { drawProxy.destroy(); drawProxy = null; }
|
|
568
|
+
if (mousePressedProxy) { mousePressedProxy.destroy(); mousePressedProxy = null; }
|
|
569
|
+
if (mouseReleasedProxy) { mouseReleasedProxy.destroy(); mouseReleasedProxy = null; }
|
|
570
|
+
if (mouseDraggedProxy) { mouseDraggedProxy.destroy(); mouseDraggedProxy = null; }
|
|
571
|
+
if (mouseMovedProxy) { mouseMovedProxy.destroy(); mouseMovedProxy = null; }
|
|
572
|
+
if (mouseWheelProxy) { mouseWheelProxy.destroy(); mouseWheelProxy = null; }
|
|
573
|
+
if (doubleClickedProxy) { doubleClickedProxy.destroy(); doubleClickedProxy = null; }
|
|
574
|
+
if (keyPressedProxy) { keyPressedProxy.destroy(); keyPressedProxy = null; }
|
|
575
|
+
if (keyReleasedProxy) { keyReleasedProxy.destroy(); keyReleasedProxy = null; }
|
|
576
|
+
if (touchStartedProxy) { touchStartedProxy.destroy(); touchStartedProxy = null; }
|
|
577
|
+
if (touchMovedProxy) { touchMovedProxy.destroy(); touchMovedProxy = null; }
|
|
578
|
+
if (touchEndedProxy) { touchEndedProxy.destroy(); touchEndedProxy = null; }
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
/* ─────────────────── ACE EDITOR ─────────────── */
|
|
@@ -785,7 +795,12 @@ sys.modules["p5"] = m
|
|
|
785
795
|
|
|
786
796
|
/* ─────────────────── RUN CODE ───────────────── */
|
|
787
797
|
let running = false;
|
|
788
|
-
let setupProxy = null, drawProxy = null,
|
|
798
|
+
let setupProxy = null, drawProxy = null,
|
|
799
|
+
preloadProxy = null,
|
|
800
|
+
mousePressedProxy = null, mouseReleasedProxy = null, mouseDraggedProxy = null,
|
|
801
|
+
mouseMovedProxy = null, mouseWheelProxy = null, doubleClickedProxy = null,
|
|
802
|
+
keyPressedProxy = null, keyReleasedProxy = null,
|
|
803
|
+
touchStartedProxy = null, touchMovedProxy = null, touchEndedProxy = null;
|
|
789
804
|
|
|
790
805
|
async function runCode() {
|
|
791
806
|
if (running) return;
|
|
@@ -819,12 +834,23 @@ sys.modules["p5"] = m
|
|
|
819
834
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
820
835
|
}
|
|
821
836
|
|
|
822
|
-
let pySetup, pyDraw, pyMP, pyKP
|
|
837
|
+
let pySetup, pyDraw, pyMP, pyKP, pyPreload, pyMD, pyMR,
|
|
838
|
+
pyMM, pyMW, pyDC, pyKR, pyTS, pyTM, pyTE;
|
|
823
839
|
try {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
840
|
+
pyPreload = pyodide.runPython("_ns.get('preload')");
|
|
841
|
+
pySetup = pyodide.runPython("_ns.get('setup')");
|
|
842
|
+
pyDraw = pyodide.runPython("_ns.get('draw')");
|
|
843
|
+
pyMP = pyodide.runPython("_ns.get('mousePressed')");
|
|
844
|
+
pyKP = pyodide.runPython("_ns.get('keyPressed')");
|
|
845
|
+
pyMD = pyodide.runPython("_ns.get('mouseDragged')");
|
|
846
|
+
pyMR = pyodide.runPython("_ns.get('mouseReleased')");
|
|
847
|
+
pyMM = pyodide.runPython("_ns.get('mouseMoved')");
|
|
848
|
+
pyMW = pyodide.runPython("_ns.get('mouseWheel')");
|
|
849
|
+
pyDC = pyodide.runPython("_ns.get('doubleClicked')");
|
|
850
|
+
pyKR = pyodide.runPython("_ns.get('keyReleased')");
|
|
851
|
+
pyTS = pyodide.runPython("_ns.get('touchStarted')");
|
|
852
|
+
pyTM = pyodide.runPython("_ns.get('touchMoved')");
|
|
853
|
+
pyTE = pyodide.runPython("_ns.get('touchEnded')");
|
|
828
854
|
} catch (e) {
|
|
829
855
|
showError(String(e));
|
|
830
856
|
running = false; btnRun.classList.remove('pf-running'); return;
|
|
@@ -840,21 +866,41 @@ sys.modules["p5"] = m
|
|
|
840
866
|
const pyRefresh = pyodide.globals.get('_pf_refresh');
|
|
841
867
|
const pyNs = pyodide.globals.get('_ns');
|
|
842
868
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
try { pyRefresh(pyNs); pyDraw(); } catch (e) { showError(String(e)); stopSketch(); }
|
|
846
|
-
});
|
|
847
|
-
mousePressedProxy = pyMP ? create_proxy(() => {
|
|
848
|
-
try { pyRefresh(pyNs); pyMP(); } catch (e) { showError(String(e)); }
|
|
849
|
-
}) : null;
|
|
850
|
-
keyPressedProxy = pyKP ? create_proxy(() => {
|
|
851
|
-
try { pyRefresh(pyNs); pyKP(); } catch (e) { showError(String(e)); }
|
|
869
|
+
const mkProxy = (fn) => fn ? create_proxy(() => {
|
|
870
|
+
try { pyRefresh(pyNs); fn(); } catch (e) { showError(String(e)); }
|
|
852
871
|
}) : null;
|
|
872
|
+
|
|
873
|
+
preloadProxy = pyPreload ? create_proxy(() => { try { pyPreload(); } catch (e) { showError(String(e)); } }) : null;
|
|
874
|
+
setupProxy = pySetup ? create_proxy(() => { try { pySetup(); } catch (e) { showError(String(e)); } }) : null;
|
|
875
|
+
const DRAW_TIMEOUT_MS = 200;
|
|
876
|
+
drawProxy = create_proxy(() => {
|
|
877
|
+
try {
|
|
878
|
+
pyRefresh(pyNs);
|
|
879
|
+
const t0 = performance.now();
|
|
880
|
+
pyDraw();
|
|
881
|
+
if (performance.now() - t0 > DRAW_TIMEOUT_MS) {
|
|
882
|
+
stopSketch();
|
|
883
|
+
showError(`draw() a mis plus de ${DRAW_TIMEOUT_MS} ms — sketch arrêté pour protéger le navigateur.`);
|
|
884
|
+
}
|
|
885
|
+
} catch (e) { showError(String(e)); stopSketch(); }
|
|
886
|
+
});
|
|
887
|
+
mousePressedProxy = mkProxy(pyMP);
|
|
888
|
+
mouseReleasedProxy = mkProxy(pyMR);
|
|
889
|
+
mouseDraggedProxy = mkProxy(pyMD);
|
|
890
|
+
mouseMovedProxy = mkProxy(pyMM);
|
|
891
|
+
mouseWheelProxy = mkProxy(pyMW);
|
|
892
|
+
doubleClickedProxy = mkProxy(pyDC);
|
|
893
|
+
keyPressedProxy = mkProxy(pyKP);
|
|
894
|
+
keyReleasedProxy = mkProxy(pyKR);
|
|
895
|
+
touchStartedProxy = mkProxy(pyTS);
|
|
896
|
+
touchMovedProxy = mkProxy(pyTM);
|
|
897
|
+
touchEndedProxy = mkProxy(pyTE);
|
|
853
898
|
const windowResizedProxy = pyWR ? create_proxy(() => { try { pyWR(); } catch (e) { showError(String(e)); } }) : null;
|
|
854
899
|
|
|
855
900
|
let setupDone = false;
|
|
856
901
|
pInst = new p5((p) => {
|
|
857
902
|
p5Bridge._setP(p);
|
|
903
|
+
if (preloadProxy) p.preload = () => { preloadProxy(); };
|
|
858
904
|
p.setup = () => {
|
|
859
905
|
/* Let the user's setup() call size() to create the canvas — this
|
|
860
906
|
allows WEBGL mode and custom dimensions to work correctly.
|
|
@@ -870,9 +916,18 @@ sys.modules["p5"] = m
|
|
|
870
916
|
p.windowResized();
|
|
871
917
|
setupDone = true;
|
|
872
918
|
};
|
|
873
|
-
p.draw
|
|
874
|
-
p.mousePressed
|
|
875
|
-
p.
|
|
919
|
+
p.draw = () => { if (setupDone) drawProxy(); };
|
|
920
|
+
p.mousePressed = () => { if (setupDone && mousePressedProxy) mousePressedProxy(); };
|
|
921
|
+
p.mouseReleased = () => { if (setupDone && mouseReleasedProxy) mouseReleasedProxy(); };
|
|
922
|
+
p.mouseDragged = () => { if (setupDone && mouseDraggedProxy) mouseDraggedProxy(); };
|
|
923
|
+
p.mouseMoved = () => { if (setupDone && mouseMovedProxy) mouseMovedProxy(); };
|
|
924
|
+
p.mouseWheel = (e) => { if (setupDone && mouseWheelProxy) mouseWheelProxy(); };
|
|
925
|
+
p.doubleClicked = () => { if (setupDone && doubleClickedProxy) doubleClickedProxy(); };
|
|
926
|
+
p.keyPressed = () => { if (setupDone && keyPressedProxy) keyPressedProxy(); };
|
|
927
|
+
p.keyReleased = () => { if (setupDone && keyReleasedProxy) keyReleasedProxy(); };
|
|
928
|
+
p.touchStarted = () => { if (setupDone && touchStartedProxy) touchStartedProxy(); };
|
|
929
|
+
p.touchMoved = () => { if (setupDone && touchMovedProxy) touchMovedProxy(); };
|
|
930
|
+
p.touchEnded = () => { if (setupDone && touchEndedProxy) touchEndedProxy(); };
|
|
876
931
|
/* Called by p5 on actual window resize AND by notifyResize() */
|
|
877
932
|
p.windowResized = () => {
|
|
878
933
|
if (p5Bridge._mode === 'fullscreen') p5Bridge.size('max');
|
|
@@ -886,7 +941,7 @@ sys.modules["p5"] = m
|
|
|
886
941
|
}
|
|
887
942
|
|
|
888
943
|
/* ─────────────────── DOWNLOAD ───────────────── */
|
|
889
|
-
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@
|
|
944
|
+
const PYFRILET_CDN = 'https://cdn.jsdelivr.net/npm/pyfrilet@latest/pyfrilet.min.js';
|
|
890
945
|
|
|
891
946
|
const STANDALONE_TEMPLATE = `<!doctype html>
|
|
892
947
|
<html lang="fr">
|
package/pyfrilet.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(){"use strict";const e="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",n="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",t="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",a="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",r="\nhtml, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n}\n#pf-ace { position: absolute; inset: 0; }\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n",d='\n<div id="pf-root">\n <div id="pf-app">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · 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 l=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!l)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const c=(l.getAttribute("data-sources")||l.getAttribute("sources")||"local").toLowerCase().trim(),p=(l.getAttribute("data-vendor")||l.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/"),u="cdn"===c?{p5:e,pyodide:n,pyodideIndex:null,ace:t,acePython:i,aceMonokai:o,aceLangTools:s,aceSearchbox:a}:{p5:p+"p5.min.js",pyodide:p+"pyodide/pyodide.js",pyodideIndex:p+"pyodide/",ace:p+"ace.min.js",acePython:p+"mode-python.min.js",aceMonokai:p+"theme-monokai.min.js",aceLangTools:p+"ext-language_tools.min.js",aceSearchbox:p+"ext-searchbox.min.js"},f=l.textContent.replace(/^\n/,""),m="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(m)}catch(e){return null}})();!function(e,n,t,i){const o=document.createElement("style");o.textContent=r,document.head.appendChild(o),document.body.innerHTML=d;const s=document.getElementById("pf-app"),a=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),m=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),_=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-reset"),b=document.getElementById("pf-btn-help"),v=document.getElementById("pf-grip"),x=document.getElementById("pf-handle-hint");let w=!1,k=Math.round(.56*window.innerHeight);function E(){document.documentElement.style.setProperty("--pf-drawer-h",k+"px")}function L(){w=!0,a.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{K(),W&&W.focus()},280)}function C(){w=!1,a.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{K();const e=H._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):s.focus()},280)}function S(){w?C():L()}E();let j=null;const z=5,R=120,P=document.createElement("div");function I(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;j={y:n,h:w?k:0,moved:!1},P.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function M(e){if(!j)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=j.y-n;if(Math.abs(t)>z&&(j.moved=!0),!j.moved)return;const i=Math.max(0,Math.min(window.innerHeight-50,j.h+t));i<R?(a.style.transition="none",a.style.height="32px"):(k=i,E(),w||L(),a.style.transition="none",a.style.height=k+"px"),K()}function T(e){if(!j)return;const n=j.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??j.y,i=j.y-t,o=j.h+i;j=null,P.style.display="none",document.body.style.userSelect="",a.style.transition="",a.style.height="",n&&(o<R?C():(k=Math.max(R,Math.min(window.innerHeight-50,o)),E(),w||L()),K())}Object.assign(P.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(P),v.addEventListener("click",e=>{e.stopPropagation(),S()}),l.addEventListener("mousedown",I,!0),document.addEventListener("mousemove",M),document.addEventListener("mouseup",T),l.addEventListener("touchstart",I,{passive:!1}),document.addEventListener("touchmove",M,{passive:!0}),document.addEventListener("touchend",T);let B=0,O=0;function A(e){m.textContent=e,m.style.display="block",L()}function D(){m.textContent="",m.style.display="none"}function Y(){if(!H._p||"fit"!==H._mode)return;const e=H._w,n=H._h;if(!e||!n)return;const t=s.clientWidth,i=s.clientHeight,o=Math.min(t/e,i/n);p.style.transform=`scale(${o})`}function K(){if("fullscreen"===H._mode?H.size("max"):Y(),F&&"function"==typeof F.windowResized)try{F.windowResized()}catch(e){}W&&W.resize()}window.addEventListener("mousemove",e=>{B=e.clientX,O=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(B=e.touches[0].clientX,O=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=H._p?H._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=H._w/n.width,i=H._h/n.height;return[(B-n.left)*t,(O-n.top)*i]},window.addEventListener("resize",K);let F=null;const H=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const i=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=s.clientWidth,this._h=s.clientHeight,void 0===i&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,i),p.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===i&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,i),Y())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){x.textContent=String(e)}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function U(){if(F){try{F.remove()}catch(e){}F=null}c.innerHTML="",H._p=null,H._mode="fit",H._w=0,H._h=0,p.style.transform="scale(1)",x.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",Q&&(Q.destroy(),Q=null),Z&&(Z.destroy(),Z=null),ee&&(ee.destroy(),ee=null),ne&&(ne.destroy(),ne=null)}window.p5py=H;let W=null;function V(){!i.ace.startsWith("vendor")&&i.ace.startsWith("http")||ace.config.set("basePath",i.ace.replace(/\/[^/]+$/,"/")),W=ace.edit("pf-ace"),W.session.setMode("ace/mode/python"),W.setTheme("ace/theme/monokai"),W.setValue(e,-1),W.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),W.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{te()}}),W.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:C}),W.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:X}),W.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te())}});let t=null;W.session.on("change",()=>{clearTimeout(t),t=setTimeout(X,350)})}function X(){try{localStorage.setItem(t,W?W.getValue():e)}catch(e){}}window.addEventListener("beforeunload",X);let N=null,J=null;async function q(){return J||(J=(async()=>{const e={};if(i.pyodideIndex&&(e.indexURL=i.pyodideIndex),N=await loadPyodide(e),N.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('size', 'sketchTitle'):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * (after all explicit additions)\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\n# ── _pf_refresh: called before every event callback ───────────────\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k == 'mouseX' else my\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n\nsys.modules[\"p5\"] = m\n"),W){G(N.runPython("list(m.__all__)").toJs())}})(),J)}function G(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,i,o,s){s(null,o.length>0?n:[])}},i=ace.require("ace/ext/language_tools");i&&Array.isArray(i.completers)&&(i.completers=i.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,W.completers=[...W.completers||[],t]}let $=!1,Q=null,Z=null,ee=null,ne=null;async function te(){if($)return;$=!0,h.classList.add("pf-running"),D(),U(),N||(f.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await q()}catch(e){return u.style.display="none",A("Erreur Pyodide : "+e),$=!1,void h.classList.remove("pf-running")}u.style.display="none";const n=W?W.getValue():e;N.globals.set("_USER_CODE",n);try{N.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return A(String(e)),$=!1,void h.classList.remove("pf-running")}let t,i,o,s;try{t=N.runPython("_ns.get('setup')"),i=N.runPython("_ns.get('draw')"),o=N.runPython("_ns.get('mousePressed')"),s=N.runPython("_ns.get('keyPressed')")}catch(e){return A(String(e)),$=!1,void h.classList.remove("pf-running")}if(!i)return A("Le script doit définir au moins une fonction draw()."),$=!1,void h.classList.remove("pf-running");const{create_proxy:a}=N.pyimport("pyodide.ffi"),r=N.runPython("_ns.get('windowResized')"),d=N.globals.get("_pf_refresh"),l=N.globals.get("_ns");Q=t?a(()=>{try{t()}catch(e){A(String(e))}}):null,Z=a(()=>{try{d(l),i()}catch(e){A(String(e)),U()}}),ee=o?a(()=>{try{d(l),o()}catch(e){A(String(e))}}):null,ne=s?a(()=>{try{d(l),s()}catch(e){A(String(e))}}):null;const p=r?a(()=>{try{r()}catch(e){A(String(e))}}):null;let m=!1;F=new p5(e=>{H._setP(e),e.setup=()=>{Q&&Q(),e.canvas||H.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),m=!0},e.draw=()=>{m&&Z()},e.mousePressed=()=>{m&&ee&&ee()},e.keyPressed=()=>{m&&ne&&ne()},e.windowResized=()=>{"fullscreen"===H._mode?H.size("max"):Y(),p&&p()}},c),$=!1,h.classList.remove("pf-running")}const ie='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.2.1/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function oe(){const n=W?W.getValue():e,t=ie.replace("FILLME-PYTHON",n),i=new Blob([t],{type:"text/html;charset=utf-8"}),o=URL.createObjectURL(i),s=Object.assign(document.createElement("a"),{href:o,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(o)}h.addEventListener("click",()=>te()),y.addEventListener("click",()=>{w?C():(k=window.innerHeight-32,E(),L())}),_.addEventListener("click",oe);const se="https://codeberg.org/nopid/pyfrilet";function ae(e){return new Promise((n,t)=>{const i=document.createElement("script");i.src=e,i.onload=n,i.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(i)})}b.addEventListener("click",()=>window.open(se,"_blank")),g.addEventListener("click",()=>{W&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te())}),window.addEventListener("keydown",e=>{const t=w&&W&&W.isFocused&&W.isFocused();if(t||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void te();if("Escape"===e.key)return t?void setTimeout(()=>{w&&C()},0):(e.preventDefault(),e.stopPropagation(),void(w?C():L()));if(!t)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(W&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(W.setValue(n,-1),te()))):(e.preventDefault(),void X())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",u.style.display="flex";try{await ae(i.p5),await ae(i.ace),await ae(i.acePython),await ae(i.aceMonokai),await ae(i.aceLangTools),await ae(i.aceSearchbox),await ae(i.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}V(),await te(),u.style.display="none"})()}(h&&h.trim()?h:f,f,m,u)})}();
|
|
1
|
+
!function(){"use strict";const e="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.min.js",n="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js",t="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ace.min.js",o="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/mode-python.min.js",i="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/theme-monokai.min.js",s="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-language_tools.min.js",r="https://cdnjs.cloudflare.com/ajax/libs/ace/1.36.5/ext-searchbox.min.js",a="\nhtml, body {\n height: 100%; margin: 0; overflow: hidden;\n background: #111;\n}\n#pf-root {\n position: fixed; inset: 0;\n display: flex; flex-direction: column;\n font-family: ui-monospace, 'Cascadia Code', 'Fira Code', monospace;\n}\n\n/* ── app area ── */\n#pf-app {\n flex: 1; min-height: 0;\n position: relative;\n background: #111;\n display: flex; align-items: center; justify-content: center;\n overflow: hidden;\n}\n#pf-viewport {\n transform-origin: 50% 50%;\n will-change: transform;\n}\n#pf-viewport canvas {\n display: block;\n outline: none;\n}\n#pf-loader {\n position: absolute; inset: 0;\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n gap: 14px;\n background: #111;\n color: #565f89;\n font-size: 13px;\n z-index: 50;\n pointer-events: none;\n}\n#pf-loader-bar {\n width: 160px; height: 2px;\n background: #2a2c3e;\n border-radius: 2px;\n overflow: hidden;\n}\n#pf-loader-bar::after {\n content: '';\n display: block;\n height: 100%;\n width: 40%;\n background: #7aa2f7;\n border-radius: 2px;\n animation: pf-slide 1.2s ease-in-out infinite;\n}\n@keyframes pf-slide {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(350%); }\n}\n\n/* ── drawer (slide-up editor panel) ── */\n#pf-drawer {\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n background: #1a1b26;\n height: 32px; /* collapsed = handle only */\n transition: height 0.26s cubic-bezier(.4, 0, .2, 1);\n overflow: hidden;\n /* shadow cast upward onto the app */\n box-shadow: 0 -4px 20px rgba(0,0,0,.55);\n}\n#pf-drawer.pf-open {\n height: var(--pf-drawer-h, 56vh);\n}\n\n/* ── handle bar ── */\n#pf-handle {\n height: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n padding: 0 8px 0 6px;\n background: #24283b;\n border-top: 1px solid #3d4166;\n cursor: ns-resize;\n user-select: none;\n gap: 6px;\n flex-shrink: 0;\n}\n/* grip zone: clickable to toggle, draggable to resize */\n#pf-grip {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 5px 6px;\n flex-shrink: 0;\n opacity: .5;\n border-radius: 4px;\n transition: opacity .15s, background .15s;\n cursor: pointer;\n}\n#pf-grip:hover { opacity: .85; background: rgba(255,255,255,.06); }\n#pf-grip span {\n display: block;\n width: 16px; height: 2px;\n background: #a9b1d6;\n border-radius: 1px;\n}\n#pf-handle-hint {\n flex: 1;\n color: #565f89;\n font-size: 10px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n#pf-handle-btns {\n display: flex;\n gap: 4px;\n flex-shrink: 0;\n}\n.pf-btn {\n height: 26px;\n min-width: 26px;\n padding: 0 5px;\n border: 0; border-radius: 5px;\n cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n font-size: 13px; line-height: 1;\n white-space: nowrap;\n transition: background .15s, transform .1s, opacity .15s;\n outline: none;\n box-sizing: border-box;\n}\n.pf-btn:active { transform: scale(.88); }\n.pf-btn:focus-visible { outline: 2px solid #7aa2f7; outline-offset: 1px; }\n\n#pf-btn-run { background: #1a6b3a; color: #9ece6a; font-size: 11px; }\n#pf-btn-run:hover { background: #1f8447; color: #b9f27a; }\n#pf-btn-run.pf-running { opacity: .5; cursor: not-allowed; }\n\n#pf-btn-code { background: #2a2c3e; color: #7aa2f7; font-size: 14px; }\n#pf-btn-code:hover { background: #3d4166; color: #c0caf5; }\n#pf-btn-code.pf-active { background: #3d4166; color: #e0af68; }\n\n#pf-btn-dl { background: #2a2c3e; color: #9d7cd8; font-size: 14px; }\n#pf-btn-dl:hover { background: #3d4166; color: #bb9af7; }\n\n#pf-btn-reset { background: #2a2c3e; color: #e0af68; font-size: 16px; }\n#pf-btn-reset:hover { background: #3d4166; color: #ffc777; }\n\n/* ── editor area inside drawer ── */\n#pf-editor-wrap {\n flex: 1;\n min-height: 80px;\n position: relative;\n}\n#pf-ace { position: absolute; inset: 0; }\n\n/* ── error panel (below editor, never overlaps ACE) ── */\n#pf-err {\n flex-shrink: 0;\n max-height: 120px;\n overflow: auto;\n margin: 0; padding: 8px 13px;\n font-size: 11.5px; line-height: 1.45;\n background: rgba(13, 3, 3, .95);\n color: #f7768e;\n white-space: pre-wrap;\n display: none;\n border-top: 1px solid rgba(247, 118, 142, .35);\n}\n",d='\n<div id="pf-root">\n <div id="pf-app">\n <div id="pf-viewport"><div id="pf-sketch"></div></div>\n <div id="pf-loader">\n <span id="pf-loader-msg">Chargement…</span>\n <div id="pf-loader-bar"></div>\n </div>\n </div>\n <div id="pf-drawer">\n <div id="pf-handle">\n <div id="pf-grip" title="Clic → ouvrir/fermer"><span></span><span></span><span></span></div>\n <span id="pf-handle-hint">Clic ☰ → ouvrir/fermer · 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 l=document.querySelector('script[type="text/python"]')||document.querySelector("python");if(!l)return void console.warn('[pyfrilet] No <script type="text/python"> or <python> tag found.');const c=(l.getAttribute("data-sources")||l.getAttribute("sources")||"local").toLowerCase().trim(),p=(l.getAttribute("data-vendor")||l.getAttribute("vendor")||"vendor/").replace(/\/?$/,"/"),u="cdn"===c?{p5:e,pyodide:n,pyodideIndex:null,ace:t,acePython:o,aceMonokai:i,aceLangTools:s,aceSearchbox:r}:{p5:p+"p5.min.js",pyodide:p+"pyodide/pyodide.js",pyodideIndex:p+"pyodide/",ace:p+"ace.min.js",acePython:p+"mode-python.min.js",aceMonokai:p+"theme-monokai.min.js",aceLangTools:p+"ext-language_tools.min.js",aceSearchbox:p+"ext-searchbox.min.js"},f=l.textContent.replace(/^\n/,""),m="pyfrilet:"+location.pathname,h=(()=>{try{return localStorage.getItem(m)}catch(e){return null}})();!function(e,n,t,o){const i=document.createElement("style");i.textContent=a,document.head.appendChild(i),document.body.innerHTML=d;const s=document.getElementById("pf-app"),r=document.getElementById("pf-drawer"),l=document.getElementById("pf-handle"),c=document.getElementById("pf-sketch"),p=document.getElementById("pf-viewport"),u=document.getElementById("pf-loader"),f=document.getElementById("pf-loader-msg"),m=document.getElementById("pf-err"),h=document.getElementById("pf-btn-run"),y=document.getElementById("pf-btn-code"),_=document.getElementById("pf-btn-dl"),g=document.getElementById("pf-btn-reset"),b=document.getElementById("pf-btn-help"),v=document.getElementById("pf-grip"),x=document.getElementById("pf-handle-hint");let w=!1,k=Math.round(.56*window.innerHeight);function E(){document.documentElement.style.setProperty("--pf-drawer-h",k+"px")}function L(){w=!0,r.classList.add("pf-open"),y.classList.add("pf-active"),setTimeout(()=>{Y(),U&&U.focus()},280)}function C(){w=!1,r.classList.remove("pf-open"),y.classList.remove("pf-active"),setTimeout(()=>{Y();const e=F._p?.canvas;e?(e.setAttribute("tabindex","0"),e.focus()):s.focus()},280)}function S(){w?C():L()}E();let P=null;const j=5,R=120,z=document.createElement("div");function I(e){if(e.target.closest(".pf-btn"))return;if(e.target.closest("#pf-grip"))return;const n=e.touches?e.touches[0].clientY:e.clientY;P={y:n,h:w?k:0,moved:!1},z.style.display="block",document.body.style.userSelect="none",e.cancelable&&e.preventDefault(),e.stopPropagation()}function M(e){if(!P)return;const n=e.touches?e.touches[0].clientY:e.clientY,t=P.y-n;if(Math.abs(t)>j&&(P.moved=!0),!P.moved)return;const o=Math.max(0,Math.min(window.innerHeight-50,P.h+t));o<R?(r.style.transition="none",r.style.height="32px"):(k=o,E(),w||L(),r.style.transition="none",r.style.height=k+"px"),Y()}function T(e){if(!P)return;const n=P.moved,t=(e.changedTouches?e.changedTouches[0].clientY:e.clientY)??P.y,o=P.y-t,i=P.h+o;P=null,z.style.display="none",document.body.style.userSelect="",r.style.transition="",r.style.height="",n&&(i<R?C():(k=Math.max(R,Math.min(window.innerHeight-50,i)),E(),w||L()),Y())}Object.assign(z.style,{position:"fixed",inset:"0",zIndex:"9999",cursor:"ns-resize",display:"none"}),document.body.appendChild(z),v.addEventListener("click",e=>{e.stopPropagation(),S()}),l.addEventListener("mousedown",I,!0),document.addEventListener("mousemove",M),document.addEventListener("mouseup",T),l.addEventListener("touchstart",I,{passive:!1}),document.addEventListener("touchmove",M,{passive:!0}),document.addEventListener("touchend",T);let B=0,O=0;function A(e){m.textContent=e,m.style.display="block",L()}function D(){m.textContent="",m.style.display="none"}function W(){if(!F._p||"fit"!==F._mode)return;const e=F._w,n=F._h;if(!e||!n)return;const t=s.clientWidth,o=s.clientHeight,i=Math.min(t/e,o/n);p.style.transform=`scale(${i})`}function Y(){if("fullscreen"===F._mode?F.size("max"):W(),K&&"function"==typeof K.windowResized)try{K.windowResized()}catch(e){}U&&U.resize()}window.addEventListener("mousemove",e=>{B=e.clientX,O=e.clientY},{passive:!0}),window.addEventListener("touchmove",e=>{e.touches.length>0&&(B=e.touches[0].clientX,O=e.touches[0].clientY)},{passive:!0}),window._pfMouse=()=>{const e=F._p?F._p.canvas:null;if(!e)return[0,0];const n=e.getBoundingClientRect(),t=F._w/n.width,o=F._h/n.height;return[(B-n.left)*t,(O-n.top)*o]},window.addEventListener("resize",Y);let K=null;const F=new Proxy({_p:null,_mode:"fit",_w:0,_h:0,_setP(e){this._p=e},size(e,n,t){if(!this._p)return;const o=t??void 0;"max"===e||null==e?(this._mode="fullscreen",this._w=s.clientWidth,this._h=s.clientHeight,void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),p.style.transform="scale(1)"):(this._mode="fit",this._w=Math.max(1,0|e),this._h=Math.max(1,0|n),void 0===o&&this._p.canvas?this._p.resizeCanvas(this._w,this._h):this._p.createCanvas(this._w,this._h,o),W())},noSmooth(){this._p?.noSmooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="pixelated")},smooth(){this._p?.smooth(),this._p?.canvas&&(this._p.canvas.style.imageRendering="auto")},sketchTitle(e){x.textContent=String(e)}},{get(e,n){if(n in e)return"function"==typeof e[n]?e[n].bind(e):e[n];if(e._p&&n in e._p){const t=e._p[n];return"function"==typeof t?t.bind(e._p):t}},set:(e,n,t)=>n.startsWith("_")?(e[n]=t,!0):(e._p&&(e._p[n]=t),!0)});function H(){if(K){try{K.remove()}catch(e){}K=null}c.innerHTML="",F._p=null,F._mode="fit",F._w=0,F._h=0,p.style.transform="scale(1)",x.textContent="Shift+Entrée → relancer · Échap → ouvrir/fermer",ee&&(ee.destroy(),ee=null),Q&&(Q.destroy(),Q=null),Z&&(Z.destroy(),Z=null),ne&&(ne.destroy(),ne=null),te&&(te.destroy(),te=null),oe&&(oe.destroy(),oe=null),ie&&(ie.destroy(),ie=null),se&&(se.destroy(),se=null),re&&(re.destroy(),re=null),ae&&(ae.destroy(),ae=null),de&&(de.destroy(),de=null),le&&(le.destroy(),le=null),ce&&(ce.destroy(),ce=null),pe&&(pe.destroy(),pe=null)}window.p5py=F;let U=null;function V(){!o.ace.startsWith("vendor")&&o.ace.startsWith("http")||ace.config.set("basePath",o.ace.replace(/\/[^/]+$/,"/")),U=ace.edit("pf-ace"),U.session.setMode("ace/mode/python"),U.setTheme("ace/theme/monokai"),U.setValue(e,-1),U.setOptions({fontSize:"15px",showPrintMargin:!1,wrap:!1,useWorker:!1,tabSize:4,enableBasicAutocompletion:!0,enableLiveAutocompletion:!0,enableSnippets:!0}),U.commands.addCommand({name:"pfRun",bindKey:{win:"Shift-Enter",mac:"Shift-Enter"},exec:()=>{ue()}}),U.commands.addCommand({name:"pfClose",bindKey:{win:"Escape",mac:"Escape"},exec:C}),U.commands.addCommand({name:"pfSave",bindKey:{win:"Ctrl-S",mac:"Command-S"},exec:X}),U.commands.addCommand({name:"pfReset",bindKey:{win:"Ctrl-R",mac:"Command-R"},exec:()=>{confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue())}});let t=null;U.session.on("change",()=>{clearTimeout(t),t=setTimeout(X,350)})}function X(){try{localStorage.setItem(t,U?U.getValue():e)}catch(e){}}window.addEventListener("beforeunload",X);let N=null,J=null;async function $(){return J||(J=(async()=>{const e={};if(o.pyodideIndex&&(e.indexURL=o.pyodideIndex),N=await loadPyodide(e),N.runPython("\nimport sys, types, js\nfrom js import p5py, _pfMouse\nfrom pyodide.ffi import JsProxy\n\n# ── Python builtins that must NOT be shadowed ──────────────────────\n_BLACKLIST = frozenset({\n 'abs','all','any','bin','bool','bytes','callable','chr','compile',\n 'delattr','dict','dir','divmod','enumerate','eval','exec',\n 'filter','float','format','frozenset','getattr','globals','hasattr',\n 'hash','help','hex','id','input','int','isinstance','issubclass',\n 'iter','len','list','locals','map','max','min','next','object',\n 'oct','open','ord','pow','print','property','range','repr',\n 'reversed','round','set','setattr','slice','sorted','staticmethod',\n 'str','sum','super','tuple','type','vars','zip',\n # p5 lifecycle hooks — user defines these, we don't import them\n 'setup','draw','preload',\n})\n\n# ── Introspect a hidden dummy p5 instance ─────────────────────────\n_dummy_node = js.document.createElement('div')\n_dummy = js.p5.new(lambda _: None, _dummy_node)\n\n_p5_functions = set() # names of callable JS members\n_p5_attributes = set() # names of scalar/readable members\n\nfor _n in dir(_dummy):\n if _n.startswith('_') or _n in _BLACKLIST:\n continue\n _v = getattr(_dummy, _n)\n if isinstance(_v, JsProxy):\n if callable(_v):\n _p5_functions.add(_n)\n # non-callable JsProxy (canvas, pixels…) → skip\n else:\n _p5_attributes.add(_n)\n\n# Read real initial values now, while dummy is still alive\n_attr_init = {}\nfor _n in _p5_attributes:\n try:\n _attr_init[_n] = getattr(_dummy, _n)\n except Exception:\n _attr_init[_n] = 0\n\n_dummy.remove()\ndel _dummy, _dummy_node\n\n# ── Build module ───────────────────────────────────────────────────\nm = types.ModuleType(\"p5\")\n\n# Generic function wrapper: delegates to live p5Bridge instance\nclass _FW:\n __slots__ = ('_n',)\n def __init__(self, n): self._n = n\n def __call__(self, *a): return getattr(p5py, self._n)(*a)\n def __repr__(self): return f'<p5 function {self._n}>'\n\nfor _n in _p5_functions:\n setattr(m, _n, _FW(_n))\n\n# ── Special overrides (our bridge has custom behaviour) ────────────\n# smooth/noSmooth exist on a real p5 instance so introspection finds\n# them — but our Proxy overrides them to also toggle CSS image-rendering.\n# size and sketchTitle are pyfrilet-only: NOT on a real p5 instance,\n# so introspection misses them — add them explicitly.\nfor _n in ('size', 'sketchTitle'):\n setattr(m, _n, _FW(_n))\n _p5_functions.add(_n) # keep __all__ consistent\n\n# mouseX / mouseY: override with our accurate coordinate calculator\n# (p5's own values are wrong when a CSS-transformed parent is used)\n_MOUSE_OVERRIDE = frozenset({'mouseX', 'mouseY'})\n\n# Initial values from the dummy instance — constants like WEBGL, DEGREES,\n# LEFT_ARROW… are correct from the very first setup() call.\nfor _n in _p5_attributes:\n if _n in _MOUSE_OVERRIDE:\n setattr(m, _n, 0.0)\n else:\n setattr(m, _n, _attr_init.get(_n, 0))\n\n# Build __all__ for import * (after all explicit additions)\nm.__all__ = sorted(_p5_functions | _p5_attributes)\n\n# ── _pf_refresh: called before every event callback ───────────────\ndef _pf_refresh(ns):\n # accurate mouse coords (bypasses p5's stale CSS-transform offset)\n mx, my = _pfMouse()\n\n # update all known scalar attributes from live instance\n for _k in _p5_attributes:\n if _k in _MOUSE_OVERRIDE:\n _v = mx if _k == 'mouseX' else my\n else:\n try:\n _v = getattr(p5py, _k)\n except Exception:\n continue\n setattr(m, _k, _v)\n if _k in ns:\n ns[_k] = _v\n\nsys.modules[\"p5\"] = m\n"),U){q(N.runPython("list(m.__all__)").toJs())}})(),J)}function q(e){const n=e.map(e=>({caption:e,value:e,meta:"p5",score:1e3})),t={getCompletions(e,t,o,i,s){s(null,i.length>0?n:[])}},o=ace.require("ace/ext/language_tools");o&&Array.isArray(o.completers)&&(o.completers=o.completers.filter(e=>!0!==e._pyfrilet)),t._pyfrilet=!0,U.completers=[...U.completers||[],t]}let G=!1,Q=null,Z=null,ee=null,ne=null,te=null,oe=null,ie=null,se=null,re=null,ae=null,de=null,le=null,ce=null,pe=null;async function ue(){if(G)return;G=!0,h.classList.add("pf-running"),D(),H(),N||(f.textContent="Initialisation de Pyodide…",u.style.display="flex");try{await $()}catch(e){return u.style.display="none",A("Erreur Pyodide : "+e),G=!1,void h.classList.remove("pf-running")}u.style.display="none";const n=U?U.getValue():e;N.globals.set("_USER_CODE",n);try{N.runPython("_ns = {}; exec(_USER_CODE, _ns, _ns)")}catch(e){return A(String(e)),G=!1,void h.classList.remove("pf-running")}let t,o,i,s,r,a,d,l,p,m,y,_,g,b;try{r=N.runPython("_ns.get('preload')"),t=N.runPython("_ns.get('setup')"),o=N.runPython("_ns.get('draw')"),i=N.runPython("_ns.get('mousePressed')"),s=N.runPython("_ns.get('keyPressed')"),a=N.runPython("_ns.get('mouseDragged')"),d=N.runPython("_ns.get('mouseReleased')"),l=N.runPython("_ns.get('mouseMoved')"),p=N.runPython("_ns.get('mouseWheel')"),m=N.runPython("_ns.get('doubleClicked')"),y=N.runPython("_ns.get('keyReleased')"),_=N.runPython("_ns.get('touchStarted')"),g=N.runPython("_ns.get('touchMoved')"),b=N.runPython("_ns.get('touchEnded')")}catch(e){return A(String(e)),G=!1,void h.classList.remove("pf-running")}if(!o)return A("Le script doit définir au moins une fonction draw()."),G=!1,void h.classList.remove("pf-running");const{create_proxy:v}=N.pyimport("pyodide.ffi"),x=N.runPython("_ns.get('windowResized')"),w=N.globals.get("_pf_refresh"),k=N.globals.get("_ns"),E=e=>e?v(()=>{try{w(k),e()}catch(e){A(String(e))}}):null;ee=r?v(()=>{try{r()}catch(e){A(String(e))}}):null,Q=t?v(()=>{try{t()}catch(e){A(String(e))}}):null;const L=200;Z=v(()=>{try{w(k);const e=performance.now();o(),performance.now()-e>L&&(H(),A(`draw() a mis plus de ${L} ms — sketch arrêté pour protéger le navigateur.`))}catch(e){A(String(e)),H()}}),ne=E(i),te=E(d),oe=E(a),ie=E(l),se=E(p),re=E(m),ae=E(s),de=E(y),le=E(_),ce=E(g),pe=E(b);const C=x?v(()=>{try{x()}catch(e){A(String(e))}}):null;let S=!1;K=new p5(e=>{F._setP(e),ee&&(e.preload=()=>{ee()}),e.setup=()=>{Q&&Q(),e.canvas||F.size(200,200),"function"==typeof e._updateMouseCoords&&e._updateMouseCoords({clientX:0,clientY:0}),e.windowResized(),S=!0},e.draw=()=>{S&&Z()},e.mousePressed=()=>{S&&ne&&ne()},e.mouseReleased=()=>{S&&te&&te()},e.mouseDragged=()=>{S&&oe&&oe()},e.mouseMoved=()=>{S&&ie&&ie()},e.mouseWheel=e=>{S&&se&&se()},e.doubleClicked=()=>{S&&re&&re()},e.keyPressed=()=>{S&&ae&&ae()},e.keyReleased=()=>{S&&de&&de()},e.touchStarted=()=>{S&&le&&le()},e.touchMoved=()=>{S&&ce&&ce()},e.touchEnded=()=>{S&&pe&&pe()},e.windowResized=()=>{"fullscreen"===F._mode?F.size("max"):W(),C&&C()}},c),G=!1,h.classList.remove("pf-running")}const fe='<!doctype html>\n<html lang="fr">\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>export</title>\n <script src="https://cdn.jsdelivr.net/npm/pyfrilet@0.2.3/pyfrilet.min.js"><\/script>\n</head>\n<body>\n\n<script type="text/python" data-sources="cdn">\nFILLME-PYTHON\n<\/script>\n\n</body>\n</html>';function me(){const n=U?U.getValue():e,t=fe.replace("FILLME-PYTHON",n),o=new Blob([t],{type:"text/html;charset=utf-8"}),i=URL.createObjectURL(o),s=Object.assign(document.createElement("a"),{href:i,download:"sketch.html"});document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(i)}h.addEventListener("click",()=>ue()),y.addEventListener("click",()=>{w?C():(k=window.innerHeight-32,E(),L())}),_.addEventListener("click",me);const he="https://codeberg.org/nopid/pyfrilet";function ye(e){return new Promise((n,t)=>{const o=document.createElement("script");o.src=e,o.onload=n,o.onerror=()=>t(new Error("Impossible de charger : "+e)),document.head.appendChild(o)})}b.addEventListener("click",()=>window.open(he,"_blank")),g.addEventListener("click",()=>{U&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue())}),window.addEventListener("keydown",e=>{const t=w&&U&&U.isFocused&&U.isFocused();if(t||!["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(e.key)){if("Enter"===e.key&&e.shiftKey)return e.preventDefault(),void ue();if("Escape"===e.key)return t?void setTimeout(()=>{w&&C()},0):(e.preventDefault(),e.stopPropagation(),void(w?C():L()));if(!t)return"s"!==e.key&&"S"!==e.key||!e.ctrlKey&&!e.metaKey?"r"!==e.key&&"R"!==e.key||!e.ctrlKey&&!e.metaKey||e.altKey?void 0:(e.preventDefault(),void(U&&confirm("Réinitialiser le code ? Les modifications seront perdues.")&&(U.setValue(n,-1),ue()))):(e.preventDefault(),void X())}else e.preventDefault()},!0),(async()=>{f.textContent="Chargement des dépendances…",u.style.display="flex";try{await ye(o.p5),await ye(o.ace),await ye(o.acePython),await ye(o.aceMonokai),await ye(o.aceLangTools),await ye(o.aceSearchbox),await ye(o.pyodide)}catch(e){return f.textContent="⚠ "+e.message,void(document.getElementById("pf-loader-bar").style.display="none")}V(),await ue(),u.style.display="none"})()}(h&&h.trim()?h:f,f,m,u)})}();
|