NeuralOS-Hypervisor / index.html
AEUPH's picture
Client: size canvas to viewport, 100% zoom, patch blit no longer clears the screen
6cfbb46 verified
Raw History Blame Contribute Delete
8.95 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>NeuralOS Hypervisor</title>
<style>
/* The ONLY styling in the whole OS: the terminal itself. No UI chrome here. */
html,body{margin:0;padding:0;height:100%;width:100%;background:#000;overflow:hidden}
#fb{display:block;position:fixed;inset:0;width:100vw;height:100vh;
image-rendering:pixelated;image-rendering:crisp-edges;cursor:none;touch-action:none}
</style>
</head>
<body>
<canvas id="fb"></canvas>
<script>
"use strict";
(function(){
var FB = 1024;
var canvas = document.getElementById("fb");
var ctx = canvas.getContext("2d", {alpha:false});
// Framebuffer lives OFF the DOM entirely (OffscreenCanvas, not an element node).
var off, octx;
if (typeof OffscreenCanvas !== "undefined"){
off = new OffscreenCanvas(FB, FB);
octx = off.getContext("2d", {alpha:false});
} else {
off = document.createElement("canvas"); off.width = FB; off.height = FB;
octx = off.getContext("2d", {alpha:false});
}
octx.fillStyle = "#3A6EA5"; octx.fillRect(0,0,FB,FB);
var view = {scale:1, ox:0, oy:0};
function layout(){
var vw = window.innerWidth, vh = window.innerHeight;
var dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(vw*dpr);
canvas.height = Math.round(vh*dpr);
canvas.style.width = vw+"px"; canvas.style.height = vh+"px";
// 100% zoom = one framebuffer pixel per CSS pixel (x dpr for device px).
// Never magnify past 100%; only shrink to fit a viewport smaller than the
// framebuffer. (Previously it always scaled up to fill the window, which
// is why the OS looked "zoomed in" / too big.)
var fit = Math.min(vw/FB, vh/FB);
view.scale = Math.min(fit, 1) * dpr;
view.ox = Math.floor((canvas.width - FB*view.scale)/2);
view.oy = Math.floor((canvas.height - FB*view.scale)/2);
paintAll();
}
// Full repaint of the VISIBLE canvas from the offscreen framebuffer.
function paintAll(){
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = "#000"; ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.drawImage(off, 0,0,FB,FB, view.ox, view.oy, FB*view.scale, FB*view.scale);
}
// Partial repaint: redraw ONLY the changed region. This must not clear the
// whole visible canvas -- doing so blacked out everything outside the patch
// until the next frame happened to touch it ("black tiles until mouseover").
function blit(x,y,w,h){
ctx.imageSmoothingEnabled = false;
var dx = Math.floor(view.ox + x*view.scale);
var dy = Math.floor(view.oy + y*view.scale);
var dx2 = Math.ceil(view.ox + (x+w)*view.scale);
var dy2 = Math.ceil(view.oy + (y+h)*view.scale);
ctx.drawImage(off, x,y,w,h, dx,dy, dx2-dx, dy2-dy);
}
function toFB(clientX, clientY){
return [Math.round((clientX*devicePixelRatio - view.ox)/view.scale),
Math.round((clientY*devicePixelRatio - view.oy)/view.scale)];
}
// ---- binary protocol: [MAGIC][type][u32 hlen][json][payload] ----
var MAGIC = 0x4E;
function decode(buf){
var dv = new DataView(buf);
if (dv.getUint8(0) !== MAGIC) return null;
var type = dv.getUint8(1);
var hlen = dv.getUint32(2);
var header = JSON.parse(new TextDecoder().decode(new Uint8Array(buf,6,hlen)));
var payload = buf.slice(6+hlen);
return {type:type, header:header, payload:payload};
}
var stats = {};
var banner = {text:"connecting...", t:performance.now()};
function applyPatch(p){
var blob = new Blob([p.payload], {type:"image/webp"});
return createImageBitmap(blob).then(function(bmp){
octx.imageSmoothingEnabled = false;
octx.drawImage(bmp, p.header.x, p.header.y);
bmp.close && bmp.close();
blit(p.header.x, p.header.y, p.header.w, p.header.h);
hud();
});
}
function applyFull(p){
var blob = new Blob([p.payload], {type:"image/webp"});
return createImageBitmap(blob).then(function(bmp){
octx.imageSmoothingEnabled = false;
octx.drawImage(bmp, 0, 0);
bmp.close && bmp.close();
paintAll();
hud();
});
}
// HUD is drawn INTO the framebuffer (pixels), not the DOM.
function hud(){
var m = stats;
var line = "v3 ZERO-DOM fb"+FB
+ " | fps~" + (m.fps||0)
+ " | build " + (m.build_ms!=null?m.build_ms:"-") + "ms"
+ " | frame " + (m.frame_ms!=null?m.frame_ms:"-") + "ms"
+ " | tiles " + (m.tiles!=null?m.tiles:"-")
+ " | kb " + (m.bytes!=null?(m.bytes/1024).toFixed(1):"-")
+ " | cache " + (m.cache?(m.cache.hits+"/"+(m.cache.hits+m.cache.misses)):"-")
+ " | wins " + (m.windows!=null?m.windows:"-")
+ " | " + (m.engine?m.engine.engine+"/"+m.engine.device:"-");
octx.fillStyle = "rgba(0,0,0,0.72)";
octx.fillRect(FB-820, FB-2*40-24, 816, 22);
octx.fillStyle = "#0ff";
octx.font = "13px monospace";
octx.fillText(line, FB-812, FB-2*40-8);
if (banner.text){
octx.fillStyle = "rgba(0,0,0,0.75)"; octx.fillRect(8, 8, 640, 24);
octx.fillStyle = "#7fff7f";
octx.fillText(banner.text, 16, 25);
}
var r = 0, w = canvas;
ctx.drawImage(off, FB-820, FB-2*40-24, 816, 22,
view.ox + (FB-820)*view.scale, view.oy + (FB-2*40-24)*view.scale, 816*view.scale, 22*view.scale);
ctx.drawImage(off, 8, 8, 640, 24,
view.ox + 8*view.scale, view.oy + 8*view.scale, 640*view.scale, 24*view.scale);
}
// ---- WebSocket ----
var ws = null, frames = 0, lastT = performance.now();
function connect(){
var proto = location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(proto + "://" + location.host + "/kernel");
ws.binaryType = "arraybuffer";
ws.onopen = function(){ banner = {text:"kernel connected", t:performance.now()}; };
ws.onmessage = function(ev){
if (typeof ev.data === "string") return;
var p = decode(ev.data);
if (!p) return;
if (p.type === 2){ banner = {text:p.header.text, t:performance.now()}; return; }
Object.assign(stats, p.header);
if (p.type === 1) applyPatch(p);
else if (p.type === 0) applyFull(p);
frames++;
};
ws.onclose = function(){ banner = {text:"disconnected - retrying", t:performance.now()}; setTimeout(connect, 800); };
ws.onerror = function(){ banner = {text:"socket error", t:performance.now()}; };
}
function send(o){ if (ws && ws.readyState === 1) ws.send(JSON.stringify(o)); }
// ---- input (all handled here; nothing lives in the DOM) ----
var down = null;
canvas.addEventListener("mousedown", function(e){
var p = toFB(e.clientX, e.clientY); down = p;
send({type:"click", x:p[0], y:p[1], button:e.button});
});
canvas.addEventListener("mousemove", function(e){
var p = toFB(e.clientX, e.clientY);
send({type:"move", x:p[0], y:p[1]});
});
canvas.addEventListener("mouseup", function(){
if (down){ send({type:"release"}); down = null; }
});
canvas.addEventListener("dblclick", function(e){
var p = toFB(e.clientX, e.clientY);
send({type:"dblclick", x:p[0], y:p[1]});
});
canvas.addEventListener("contextmenu", function(e){ e.preventDefault(); });
window.addEventListener("keydown", function(e){
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.key === "Enter" || e.key === "Backspace" || e.key.length === 1){
send({type:"key", key:e.key});
if (e.key === "Backspace" || e.key.length === 1) e.preventDefault();
}
});
window.addEventListener("resize", layout);
// ---- Text2IMG / Text2OS / LASM API from the console or other agents ----
window.NeuralOS = {
fb: FB,
send: send,
text2os: function(text){ var i=0; for(;i<text.length;i++) send({type:"key", key:text[i]}); send({type:"key", key:"Enter"}); },
wallpaper: function(prompt){ send({type:"wallpaper", prompt:prompt}); },
lasm: function(program){ send({type:"lasm", program:program}); },
stats: function(){ return stats; },
// debug/verification hook: returns the exact framebuffer as a data URL.
// Uses convertToBlob so no DOM node is ever created.
dump: function(){
if (off.convertToBlob){
return off.convertToBlob({type:"image/png"}).then(function(b){
return new Promise(function(res){
var fr = new FileReader();
fr.onload = function(){ res(fr.result); };
fr.readAsDataURL(b);
});
});
}
return Promise.resolve(null);
}
};
setInterval(function(){
var now = performance.now();
stats.fps = Math.round(frames*1000/(now-lastT)); frames = 0; lastT = now;
hud();
}, 1000);
// Size the backing store to the viewport BEFORE anything else. Without this
// the canvas keeps its 300x150 default and the browser stretches it to the
// window -- a massively zoomed-in, blocky desktop.
layout();
connect();
})();
</script>
</body>
</html>