Manuale di riferimento MALDA™

Il linguaggio di programmazione AI-First - Versione 1.0.11

26. Backend UI JavaScript nel browser

MALDA supporta anche un modello UI ospitato nel browser in cui il sorgente MALDA viene transpile in JavaScript e gira con il runtime JS di MALDA. È distinto dai flussi server-side @PAGE, @AIPAGE e dai componenti server.

Parti da qui: Leggi 23. Panoramica Web UI per confrontare i modelli UI. Questo capitolo copre l'esecuzione MALDA lato browser, gli helper DOM, il template mode e l'API canvas/game. Per la UI renderizzata sul server, vedi Componenti server Web UI o HttpServer e generazione UI HTML.

26.1 Mappa del capitolo

26.2 Cosa offre questa modalità

26.2.1 Decoratori target nell'output JS

Quando compili verso JavaScript, i decoratori target servono a tenere le preoccupazioni browser e server in un unico file sorgente:

I decoratori di route restano orientati al server. Una dichiarazione come @GET(...) è trattata come codice server e non viene emessa nell'output JavaScript.

26.3 Comandi di compile

# API mode (.malda source)
malda compile Examples/Web/js/hello_dom.malda --mode js -o Examples/Web/js/hello_dom.js

# Alias form (equivalent)
malda compile Examples/Web/js/counter.malda --target js -o Examples/Web/js/counter.js

# PWA output directory
malda compile Examples/Web/js/hello_dom.malda --target pwa -o dist/hello-dom

# Template mode (.malda.html source)
malda compile Examples/Web/js/hello_template.malda.html --mode js -o Examples/Web/js/hello_template.generated.js

--target js è un alias di --mode js. --target pwa è un alias di --mode pwa e produce una directory che contiene l'app shell, il runtime e il JavaScript transpile.

Il ciclo interno per i giochi canvas è malda play. Compila --mode js, scrive una pagina host e serve un URL locale così non devi scrivere HTML. Il packaging per itch.io resta --mode pwa.

malda new game my-game
cd my-game
malda play app.malda

Canvas più API punteggi è malda new game --fullstack. Emette @client() / @GET / @POST e schema Score in un solo file. Compila con --mode fullstack; malda play rifiuta quelle sorgenti.

malda new game my-scores --fullstack
cd my-scores
malda compile app.malda --mode fullstack -o dist

26.4 Ordine obbligatorio di caricamento degli script

La pagina host deve includere un contenitore come #app, poi caricare gli script in quest'ordine. Stesso file: Examples/Web/js/host.html.

  1. Qualsiasi dipendenza browser richiesta per il namespace scelto, per esempio three.js per three.*
  2. malda-js-runtime.js
  3. Lo script MALDA compilato
  4. La chiamata di bootstrap, come MaldaApp.main()
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MALDA JS host</title>
</head>
<body>
  <div id="app"></div>
  <script src="../wwwroot/malda-js-runtime.js"></script>
  <script src="./counter.js"></script>
  <script>
    if (window.MaldaApp && typeof window.MaldaApp.main === "function") {
      window.MaldaApp.main();
    }
  </script>
</body>
</html>

println è un helper di console del backend JavaScript. L'interprete non ha println (usa io.print / print — vedi 12. Input/Output).

26.5 Helper DOM in API mode

Il runtime JavaScript espone helper browser sotto dom per la manipolazione diretta del DOM. Stesso programma: Examples/Web/js/counter.malda (compila con il comando in 26.3, poi apri la pagina host di 26.4).

var root = dom.query("#app");

if (root == null) {
    println("No #app container found.");
} else {
    dom.clear(root);

    var count = 0;

    var title = dom.create("h1");
    dom.setText(title, "Counter example");
    dom.append(root, title);

    var valueLabel = dom.create("p");
    dom.append(root, valueLabel);

    function renderCount() {
        dom.setText(valueLabel, "Count: " + string(count));
    }

    var button = dom.create("button");
    dom.setText(button, "Increment");
    dom.append(root, button);

    function onIncrementClick() {
        count = count + 1;
        renderCount();
    }

    dom.on(button, "click", onIncrementClick);
    renderCount();
}

26.6 Sum type e pattern matching in JS mode

Puoi usare lo stesso modello di sum type e pattern matching in JavaScript backend mode come in modalità interpretata.

type Result = Ok(value) | Err(message);

function divide(a, b) {
    if (b == 0) return Err("divide by zero");
    return Ok(a / b);
}

var outcome = divide(10, 2);
var text = match outcome {
    case Ok(v): "ok: " + v;
    case Err(msg): "error: " + msg;
};
println(text);

Tutte le famiglie di pattern sono supportate in JS mode: letterali, identificatori, wildcard, payload delle variant, pattern di array con ...rest e pattern di oggetto nidificati.

26.7 Actor in JS mode

Il backend JavaScript include il supporto al runtime actor locale tramite mlRuntime.actors. Offre concorrenza actor lato browser o Node-local con semantica di mailbox allineata al comportamento actor MALDA.

Confine: è solo supporto al runtime actor locale. La comunicazione trasparente diretta tra actor JS nel browser e actor lato server non è abilitata di default.

26.8 Sintassi del template mode

Il template mode usa file .malda.html. Usa {{ ... }} per l'interpolazione e {% ... %} per le istruzioni.

<div class="hello-template">
  <h1>Hello from template mode</h1>
  <p>User: {{ "Andrea" }}</p>
  {% var count = 2; %}
  <p>Count value: {{ count }}</p>
</div>

Vedi Examples/Web/js/host.html per una pagina statica minima che mostra il caricamento del runtime e il bootstrap dello script compilato.

26.9 API Game Canvas

Il backend JavaScript include un'API canvas/game nel browser sotto game.* per loop interattivi 2D semplici.

Indicazioni su immagini e camera: game.loadImage restituisce subito un handle; la decode è asincrona. I draw sono no-op finché game.imageIsReady è true (i file mancanti restano unready, senza throw). game.setCamera sottrae i draw successivi nel mondo (fillRect, drawImage, linee, testo). Non sposta setPixel / blitPixels. Le coordinate del mouse restano in pixel del canvas. Vedi Examples/Games/game_sprite_smoke.malda.

Indicazioni sull'input: I fronti di tasti e gamepad sono snapshot all'inizio di update e sono false in render. Leggi wasKeyPressed / wasKeyReleased / wasGamepadButtonPressed solo in update (stessi nomi di isKeyDown). getTouches() restituisce { id, x, y } in pixel del canvas (vuoto se nessuno). Il primo tocco attivo continua ad aliasare il pulsante mouse 0. API Gamepad assente o pad scollegati: disconnected / asse 0. game.stop() azzera tasti e fronti dei pulsanti. Vedi Examples/Games/game_input_smoke.malda.

Indicazioni sulla collisione: Gli helper di overlap sono inclusivi (i bordi che si toccano contano) e restituiscono false se larghezza, altezza o raggio sono ≤ 0. Sono funzioni pure: niente canvas, e non sottraggono setCamera. Non sono AABB swept né fisica. Vedi Examples/Games/game_collision_smoke.malda.

Indicazioni sul loop: updateFn riceve dtMs. Scala il movimento per dtMs così l'animazione resta indipendente dal frame-rate. game.startFixed(updateFn, renderFn?, tickMs?) usa di default tickMs = 1000 / 60, accumula il tempo wall e chiama update(tickMs) zero o più volte per frame (max 5), poi render una volta. Non può girare insieme a game.start. Vedi Examples/Games/game_fixed_save_smoke.malda.

Indicazioni sul save: game.save / game.load / game.removeSave salvano JSON in localStorage scoped all'origine sotto il prefisso malda.game. (non file). Storage assente, quota o JSON corrotto: load restituisce null e save è no-op (senza throw).

Indicazioni sull'audio: chiama game.audioInit() dopo un gesto dell'utente per soddisfare le policy di autoplay del browser. game.audioPlaySample(url, volume?, options?) decodifica un WAV/OGG una volta per URL e lo mette in cache (volume default 1, clamp [0, 1]; { loop: true } ripete). I one-shot possono sovrapporsi. game.audioStopSample(url?) ferma solo i sample — non ferma la track v1, il pattern o i toni. game.stop() non ferma implicitamente l'audio. I sample condividono il cap di 32 nodi con i toni. Vedi Examples/Games/game_audio_sample_smoke.malda.

Per un avvio rapido completo e le best practice, vedi docs/javascript-backend.md. Per un sample completo, vedi Examples/Games/game_bounce.malda. Per blit di un atlas PNG e una camera che scorre, vedi Examples/Games/game_sprite_smoke.malda. Per fronti di tasti, tocchi e gamepad, vedi Examples/Games/game_input_smoke.malda. Per overlap AABB e cerchi, vedi Examples/Games/game_collision_smoke.malda. Per one-shot WAV sovrapposti accanto a un pattern in loop, vedi Examples/Games/game_audio_sample_smoke.malda. Per un timestep fisso e un high score che sopravvive al reload, vedi Examples/Games/game_fixed_save_smoke.malda. Per un side-scroller breve che usa il kit G1–G5 insieme (tile da atlas, camera, AABB, fronti di tasti, sample SFX, startFixed), vedi Examples/Games/malda_platform.malda. Per un client canvas più schema / validate sui punteggi, usa malda new game --fullstack. Per una cave in stile Boulder Dash (terra, massi con gravità, diamanti, lucciole), vedi Examples/Games/maldadash.malda. Per un ray tracer CPU che scrive i pixel in un buffer ImageData e fa blit una volta per frame, vedi Examples/Games/ray_tracer.malda.

26.10 API scene three.js

Il backend JavaScript include un'API scene 3D curata sotto three.*. Usala quando vuoi una scena nel browser in MALDA senza scrivere chiamate THREE.* grezze. Il namespace è solo JavaScript mode; l'interprete e il transpile C# non eseguono questi helper. Le chiamate mappano su mlRuntime.three.* nello script generato.

Costruisci una scena in questo ordine: renderer, scene, camera, poi mesh e luci, poi three.start. three.createRenderer(width, height, mountSelector?) crea il canvas WebGL e lo appende all'elemento host (default "#app"). Chiamalo una volta, prima di three.render o three.start. Se ti serve una nuova dimensione o un secondo canvas, chiama prima three.stop() — non creare un altro renderer mentre il loop è in esecuzione.

Posizioni, scale e near/far della camera sono in unità mondo. Le rotazioni sono in radianti (un pavimento sul piano XZ è circa -1.5708 su X). Le opzioni di standard-material che il wrapper capisce includono "color" (#rrggbb), "roughness", "metalness" e "map" (un handle di three.createTexture). createTexture restituisce subito; la decode è asincrona e i file mancanti restano unready (senza throw). Il materiale lascia map unset finché l'handle è ready. three.loadGLTF(url) restituisce un group che puoi add subito; i children compaiono quando three.modelIsReady(handle) è true (JSON .gltf o .glb; i fallimenti restano unready). Il runtime possiede il loader — le pagine host non aggiungono un quarto <script>. three.lookAt(object, x, y, z) richiede lookAt sull'oggetto three.js. I group parentano le mesh così puoi muovere un insieme con un solo setPosition / setRotation. I tasti di input usano gli stessi nomi dell'API canvas ("arrowleft", "arrowright", …).

Una prima scena con un cubo che gira, una key light e un loop su dtMs:

var width = 800;
var height = 500;
var aspect = width / height;

var renderer = three.createRenderer(width, height, "#app");
three.setClearColor(renderer, "#101722");
three.setRendererSize(renderer, width, height);

var scene = three.createScene();
var camera = three.createPerspectiveCamera(70, aspect, 0.1, 100.0);
three.setCameraAspect(camera, aspect);
three.setPosition(camera, 0, 0, 5);

var geometry = three.createBoxGeometry(1, 1, 1);
var material = three.createStandardMaterial({ "color": "#44aaff" });
var cube = three.createMesh(geometry, material);
three.add(scene, cube);

var light = three.createDirectionalLight("#ffffff", 1.1);
three.setPosition(light, 2, 3, 4);
three.add(scene, light);

var angle = 0.0;

function update(dtMs) {
    angle = angle + ((dtMs * 1.2) / 1000);
    three.setRotation(cube, angle, angle * 0.8, 0.0);
}

function render() {
    three.render(renderer, scene, camera);
}

three.start(update, render);

Indicazioni sul loop: updateFn riceve dtMs, come game.start. Scala rotazione e movimento per dtMs / 1000 così l'animazione resta indipendente dal frame-rate. Limita i valori enormi di dtMs dopo che un tab è stato in background se un singolo step saltrebbe troppo. Metti il disegno in renderFn (oppure omettilo e fai render alla fine di update).

Ordine di caricamento: la pagina host deve caricare un bundle browser compatibile che definisce globalThis.THREE prima di malda-js-runtime.js e prima dello script MALDA compilato, poi chiamare MaldaApp.main(). Il repository include Examples/Web/wwwroot/vendor/three.min.js a questo scopo.

  1. Examples/Web/wwwroot/vendor/three.min.js
  2. malda-js-runtime.js
  3. compiled MALDA script
  4. MaldaApp.main()

Indicazioni sul resize: se la pagina host cambia il viewport di render dopo il setup, aggiorna sia il renderer sia la camera: three.setRendererSize(renderer, width, height) e three.setCameraAspect(camera, width / height).

Scope MVP: orbit control e interop JS grezzo generico restano fuori da questa API. Texture e caricamento glTF ci sono: vedi Examples/Games/three_textured.malda. Le mesh illuminate usano three.createStandardMaterial. Il GLSL custom usa three.createShaderMaterial e three.setUniform — vedi 26.10.1.

Compila il demo del cubo con:

malda compile Examples/Games/three_cube.malda --mode js -o Examples/Games/three_cube.js

Vedi Examples/Games/three_cube.malda e Examples/Games/three_runtime_smoke_test.html per la pagina host end-to-end. Per una map PNG, un cubo glTF e lookAt, vedi Examples/Games/three_textured.malda. Un listing più breve è in 33.15 Backend JavaScript (API scena three.js). Note operative: docs/javascript-backend.md.

26.10.1 Kernel shader (MALDA verso GLSL)

Quando game.setPixel sulla CPU è troppo lento, sposta il lavoro per-pixel sulla GPU. Questo è un sottoinsieme a compile-time JavaScript, non un quarto backend di esecuzione. Il MALDA host continua a possedere il loop three.* e gli aggiornamenti delle uniform. Il kernel possiede i raggi.

Marca le funzioni GPU con @shader() e raccoglile in una stringa GLSL con glsl.compile({ ... }). Quelle funzioni non vengono emesse come JavaScript o C#. Non chiamarle dal MALDA host. L'interprete e il transpile C# le saltano.

@shader()
function vertexMain() {
    vUv = uv;
    gl_Position = vec4(position.xy, 0.0, 1.0);
}

var vert = glsl.compile({
    varyings: ["vec2 vUv"],
    functions: ["vertexMain"],
    main: "vertexMain"
});

var material = three.createShaderMaterial({
    "vertexShader": vert,
    "fragmentShader": frag,
    "uniforms": { "uTime": 0 }
});
three.setUniform(material, "uTime", t);

glsl.compile accetta un solo object literal. Chiavi:

Passa le stringhe risultanti a three.createShaderMaterial come "vertexShader" / "fragmentShader". I valori uniform dal MALDA host sono wrappati come { value }. Gli array di lunghezza 2/3/4 diventano Vector2 / Vector3 / Vector4. Le stringhe #rrggbb diventano Color. Flag opzionali: "depthWrite": false, "depthTest": false, "transparent": true.

Per un pass a schermo intero usa three.createOrthographicCamera(-1, 1, 1, -1, 0, 1) e three.createPlaneGeometry(2, 2).

Tipi nei kernel

vec2, vec3, vec4 e gli altri nomi vettore/matrice GLSL non sono tipi MALDA. Sono nomi GLSL memorizzati come type hint e copiati nello shader. Fuori da @shader(), var p: vec3 = ... è un Error di hint sconosciuto. I nomi che esistono già in entrambi i linguaggi — float, int, bool, void — restano hint MALDA ordinari.

Parametri e locali dello shader richiedono un type hint GLSL. I parametri che in GLSL si scriverebbero con out usano un hint di due parole: tHit: out float. Le uniform host restano array MALDA come [camX, camY, camZ], non valori vec3.

Sottoinsieme del linguaggio

Ammessi: if / else, while, for, return, aritmetica e confronti, and / or / not (emessi come && / || / !), swizzle di membro come position.xy, costruttori e builtin GLSL (vec3, dot, normalize, mix, clamp, …), e math.sqrt → GLSL sqrt (stesso per gli altri nomi math.* mappati).

Non ammessi nei kernel: actor, prompt, match, stringhe, foreach, await, classi e I/O MALDA. Un costrutto che l'emettitore GLSL non conosce fa fallire il compile JS con un errore GLSL transpile alla riga MALDA.

IDE

Poiché i kernel sono dichiarazioni function MALDA ordinarie, nel Desktop IDE e nell'LSP rename, trova riferimenti, highlight e outline si applicano agli identificatori nel kernel — a differenza del GLSL grezzo in una stringa """, che è un solo token. Rename continua a matchare il lessema in tutto il file (come nel resto di MALDA). Non riscrive le stringhe in glsl.compile (functions: ["hitSphere"], uniforms: ["vec3 uCamPos"], main: "fragmentMain"). Dopo aver rinominato un identificatore di kernel o di uniform, aggiorna quelle stringhe. I builtin GLSL (dot, gl_FragColor, nome di tipo vec3) non hanno una dichiarazione MALDA, quindi vai alla definizione non ha dove andare.

GLSL grezzo

Le stringhe GLSL tra triple quotes restano valide quando ti serve sintassi che il sottoinsieme non emette. Usa """, non $""", così le graffe restano letterali. Preferisci @shader() quando il kernel sta nel sottoinsieme, così rename e outline possono vederlo.

Compila il tracer GPU in tempo reale di sfere con:

malda compile Examples/Games/three_shader_raytracer.malda --mode js -o Examples/Games/three_shader_raytracer.js
malda compile Examples/Games/three_shader_path_tunnel.malda --mode js -o Examples/Games/three_shader_path_tunnel.js

Carica three.min.js per primo, come negli altri demo three.*. Un listing breve è in 33.15.1 Materiali shader. Note operative: docs/javascript-backend.md. Il sample path-tunnel è CC-BY-NC-SA-4.0 (Frostbyte).

26.11 Errori comuni e indicazioni

Vedi anche