Skip to content

Commit 79c0cc3

Browse files
Move rules logic to multilingual
1 parent 10359d8 commit 79c0cc3

7 files changed

Lines changed: 312 additions & 65 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public/*.wasm
2020
public/*.wat
2121
public/main.ml
2222
public/automate_elementaire.ml
23-
23+
public/gallery-fragment.html
24+
public/gallery/

public/index.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,8 @@ <h1>Cellcosmos</h1>
129129

130130
<section id="gallery-panel" class="gallery-panel" hidden>
131131
<div class="gallery-header">
132-
<p>Les 256 r&egrave;gles sont rendues c&ocirc;t&eacute; navigateur avec acc&eacute;l&eacute;ration WASM quand
133-
elle est valide, sinon via le
134-
moteur JavaScript int&eacute;gr&eacute;.</p>
132+
<p>Les 256 r&egrave;gles sont pr&eacute;calcul&eacute;es au build puis charg&eacute;es depuis un fragment
133+
statique s&eacute;par&eacute;, afin de garder `index.html` compact.</p>
135134
</div>
136135
<div id="gallery-grid" class="gallery-grid"></div>
137136
</section>

public/style.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,17 @@ input[type="color"] {
398398
cursor: pointer;
399399
}
400400

401+
.gallery-item:focus-visible {
402+
outline: 2px solid rgba(255, 209, 102, 0.9);
403+
outline-offset: 2px;
404+
}
405+
401406
.gallery-thumb {
402407
display: block;
403408
width: 100%;
404409
aspect-ratio: 1;
405410
background: #061018;
411+
object-fit: cover;
406412
}
407413

408414
.gallery-label {
@@ -541,4 +547,4 @@ input[type="color"] {
541547
.rule-bar {
542548
padding: 14px;
543549
}
544-
}
550+
}

public/ui.js

Lines changed: 93 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,6 @@ const DEFAULTS = {
1212
direction: "ltr",
1313
};
1414

15-
const RULE_NOTE_LABELS = {
16-
0: "",
17-
1: "Chaos pseudo al\u00e9atoire",
18-
2: "Triangle de Sierpinski",
19-
3: "Calcul universel",
20-
4: "XOR avec auto-r\u00e9f\u00e9rence",
21-
5: "Mod\u00e8le de trafic",
22-
6: "Fronti\u00e8res seulement",
23-
};
24-
2515
const fallbackDomain = {
2616
transition(ruleNumber, left, center, right) {
2717
const index = left * 4 + center * 2 + right;
@@ -36,14 +26,14 @@ const fallbackDomain = {
3626
if ([54, 106, 110, 137, 193].includes(ruleNumber)) return 4;
3727
return 2;
3828
},
39-
noteId(ruleNumber) {
40-
if (ruleNumber === 30) return 1;
41-
if (ruleNumber === 90) return 2;
42-
if (ruleNumber === 110) return 3;
43-
if (ruleNumber === 150) return 4;
44-
if (ruleNumber === 184) return 5;
45-
if (ruleNumber === 254) return 6;
46-
return 0;
29+
noteLabel(ruleNumber) {
30+
if (ruleNumber === 30) return "Chaos pseudo al\u00e9atoire";
31+
if (ruleNumber === 90) return "Triangle de Sierpinski";
32+
if (ruleNumber === 110) return "Calcul universel";
33+
if (ruleNumber === 150) return "XOR avec auto-r\u00e9f\u00e9rence";
34+
if (ruleNumber === 184) return "Mod\u00e8le de trafic";
35+
if (ruleNumber === 254) return "Fronti\u00e8res seulement";
36+
return "";
4737
},
4838
interpolateComponent(start, end, progressScaled) {
4939
return Math.round(start + (end - start) * (progressScaled / 1000));
@@ -53,7 +43,9 @@ const fallbackDomain = {
5343
let state = structuredClone(DEFAULTS);
5444
let wasm = null;
5545
let wasmAvailable = false;
56-
let galleryKey = "";
46+
const textDecoder = new TextDecoder();
47+
let galleryLoaded = false;
48+
let galleryLoading = null;
5749

5850
function clamp(value, min, max) {
5951
return Math.max(min, Math.min(max, value));
@@ -170,6 +162,7 @@ function validateWasmExports(exports) {
170162
|| typeof exports.sortie_motif !== "function"
171163
|| typeof exports.classe_wolfram !== "function"
172164
|| typeof exports.note_regle !== "function"
165+
|| typeof exports.etiquette_note_regle !== "function"
173166
|| typeof exports.composante_interpolee !== "function"
174167
) {
175168
return false;
@@ -211,6 +204,15 @@ function validateWasmExports(exports) {
211204
}
212205
}
213206

207+
const labelPtr = Number(exports.etiquette_note_regle(90));
208+
const labelLength = Number(exports.__ml_str_len());
209+
const labelBytes = new Uint8Array(exports.memory.buffer, labelPtr, labelLength);
210+
const label = textDecoder.decode(labelBytes.slice());
211+
exports.__ml_reset();
212+
if (label !== "Triangle de Sierpinski") {
213+
return false;
214+
}
215+
214216
if (Number(exports.sortie_motif(90, 4)) !== 1) {
215217
return false;
216218
}
@@ -258,19 +260,22 @@ function wolframClass(ruleNumber) {
258260
return fallbackDomain.wolframClass(ruleNumber);
259261
}
260262

261-
function ruleNoteId(ruleNumber) {
262-
if (wasmAvailable && wasm && typeof wasm.note_regle === "function") {
263+
function ruleNoteLabel(ruleNumber) {
264+
if (
265+
wasmAvailable
266+
&& wasm
267+
&& typeof wasm.etiquette_note_regle === "function"
268+
&& typeof wasm.__ml_str_len === "function"
269+
&& typeof wasm.__ml_reset === "function"
270+
&& wasm.memory instanceof WebAssembly.Memory
271+
) {
263272
try {
264-
return Number(wasm.note_regle(ruleNumber));
273+
return callWasmString(wasm.etiquette_note_regle, ruleNumber);
265274
} catch (error) {
266275
disableWasmRuntime(error);
267276
}
268277
}
269-
return fallbackDomain.noteId(ruleNumber);
270-
}
271-
272-
function ruleNoteLabel(ruleNumber) {
273-
return RULE_NOTE_LABELS[ruleNoteId(ruleNumber)] ?? "";
278+
return fallbackDomain.noteLabel(ruleNumber);
274279
}
275280

276281
function interpolateComponent(start, end, progressScaled) {
@@ -284,6 +289,15 @@ function interpolateComponent(start, end, progressScaled) {
284289
return fallbackDomain.interpolateComponent(start, end, progressScaled);
285290
}
286291

292+
function callWasmString(fn, ...args) {
293+
const ptr = Number(fn(...args));
294+
const length = Number(wasm.__ml_str_len());
295+
const bytes = new Uint8Array(wasm.memory.buffer, ptr, length);
296+
const value = textDecoder.decode(bytes.slice());
297+
wasm.__ml_reset();
298+
return value;
299+
}
300+
287301
function disableWasmRuntime(error) {
288302
wasm = null;
289303
wasmAvailable = false;
@@ -503,37 +517,6 @@ function renderGradientPickers() {
503517
});
504518
}
505519

506-
function galleryStateKey() {
507-
return JSON.stringify(state);
508-
}
509-
510-
function buildGallery() {
511-
const key = galleryStateKey();
512-
if (key === galleryKey) return;
513-
galleryKey = key;
514-
const grid = document.getElementById("gallery-grid");
515-
grid.innerHTML = "";
516-
for (let rule = 0; rule < 256; rule += 1) {
517-
const item = document.createElement("article");
518-
item.className = "gallery-item";
519-
const canvas = document.createElement("canvas");
520-
canvas.className = "gallery-thumb";
521-
renderToCanvas(canvas, rule, 50, 50, 2);
522-
const label = document.createElement("div");
523-
label.className = "gallery-label";
524-
label.innerHTML = `<strong>R\u00e8gle ${rule}</strong><span>${ruleNoteLabel(rule) || `Classe ${wolframClass(rule)}`}</span>`;
525-
item.append(canvas, label);
526-
item.addEventListener("click", () => {
527-
state.rule = rule;
528-
syncRuleControls();
529-
renderRuleDiagram();
530-
switchTab("explorer");
531-
scheduleRender();
532-
});
533-
grid.appendChild(item);
534-
}
535-
}
536-
537520
function switchTab(tab) {
538521
const explorer = tab === "explorer";
539522
const gallery = tab === "gallery";
@@ -544,14 +527,63 @@ function switchTab(tab) {
544527
document.getElementById("tab-explorer").classList.toggle("active", explorer);
545528
document.getElementById("tab-gallery").classList.toggle("active", gallery);
546529
document.getElementById("tab-source").classList.toggle("active", source);
547-
if (gallery) buildGallery();
530+
if (gallery) loadGalleryFragment();
548531
}
549532

550533
const scheduleRender = debounce(() => {
551534
renderMainView();
552-
if (!document.getElementById("gallery-panel").hidden) buildGallery();
553535
}, 100);
554536

537+
function selectGalleryRule(rule) {
538+
state.rule = clamp(rule, 0, 255);
539+
syncRuleControls();
540+
renderRuleDiagram();
541+
switchTab("explorer");
542+
scheduleRender();
543+
}
544+
545+
function bindGallery() {
546+
const grid = document.getElementById("gallery-grid");
547+
grid.addEventListener("click", (event) => {
548+
const item = event.target.closest(".gallery-item");
549+
if (!item) return;
550+
selectGalleryRule(Number.parseInt(item.dataset.rule, 10) || 0);
551+
});
552+
grid.addEventListener("keydown", (event) => {
553+
if (event.key !== "Enter" && event.key !== " ") return;
554+
const item = event.target.closest(".gallery-item");
555+
if (!item) return;
556+
event.preventDefault();
557+
selectGalleryRule(Number.parseInt(item.dataset.rule, 10) || 0);
558+
});
559+
}
560+
561+
async function loadGalleryFragment() {
562+
if (galleryLoaded) return;
563+
if (galleryLoading) return galleryLoading;
564+
const grid = document.getElementById("gallery-grid");
565+
grid.innerHTML = '<p class="muted">Chargement de la galerie...</p>';
566+
galleryLoading = fetch("gallery-fragment.html")
567+
.then((response) => {
568+
if (!response.ok) {
569+
throw new Error(`Fragment galerie indisponible (${response.status}).`);
570+
}
571+
return response.text();
572+
})
573+
.then((markup) => {
574+
grid.innerHTML = markup;
575+
galleryLoaded = true;
576+
})
577+
.catch((error) => {
578+
grid.innerHTML = '<p class="muted">Impossible de charger la galerie statique.</p>';
579+
console.error(error);
580+
})
581+
.finally(() => {
582+
galleryLoading = null;
583+
});
584+
return galleryLoading;
585+
}
586+
555587
function bindControls() {
556588
const presets = document.getElementById("presets");
557589

@@ -715,6 +747,7 @@ function bindControls() {
715747
async function init() {
716748
loadFromURL();
717749
bindControls();
750+
bindGallery();
718751
renderGradientPickers();
719752
syncRuleControls();
720753
renderRuleDiagram();

0 commit comments

Comments
 (0)