text.mjs raw
1 // Moxie jsruntime - Browser Text Rendering Bridge
2 // Renders text using browser's native canvas 2D API.
3 // Uses a shared hidden canvas for synchronous measurement and rendering.
4
5 let _canvas = null;
6 let _ctx = null;
7 let _wasm = null;
8
9 export function SetWASM(instance) {
10 _wasm = instance;
11 }
12
13 function ensureCanvas(w, h) {
14 if (!_canvas) {
15 _canvas = document.createElement('canvas');
16 _canvas.style.display = 'none';
17 document.body.appendChild(_canvas);
18 _ctx = _canvas.getContext('2d');
19 }
20 if (_canvas.width < w) _canvas.width = w;
21 if (_canvas.height < h) _canvas.height = h;
22 }
23
24 function readStr(ptr, len) {
25 const uptr = ptr >>> 0;
26 if (!uptr || len <= 0 || !_wasm) return '';
27 return new TextDecoder().decode(new Uint8Array(_wasm.exports.memory.buffer, uptr, len));
28 }
29
30 function setI32(ptr, val) {
31 if (!_wasm) return;
32 const view = new DataView(_wasm.exports.memory.buffer);
33 view.setInt32(ptr >>> 0, val, true);
34 }
35
36 function fontCSS(family, size) {
37 // family format: "weight [italic] font-name" e.g. "700 sans-serif"
38 // CSS font shorthand: weight [style] size family
39 const parts = (family || 'sans-serif').split(' ');
40 const weight = parts[0];
41 const rest = parts.slice(1).join(' ') || 'sans-serif';
42 return `${weight} ${size}px ${rest}`;
43 }
44
45 // Measure returns pixel width and height of the given text string.
46 // Writes two int32 values at wPtr and hPtr (physical pixels).
47 export function Measure(fontPtr, fontLen, size, textPtr, textLen, wPtr, hPtr) {
48 const family = readStr(fontPtr, fontLen);
49 const text = readStr(textPtr, textLen);
50 ensureCanvas(1, 1);
51 _ctx.font = fontCSS(family, size);
52 const m = _ctx.measureText(text);
53 // Add +4 to match Render's padding - WASM allocates based on Measure's output.
54 const w = Math.ceil(m.width) + 4;
55 // Use actual bounding box if available (modern browsers), else estimate.
56 let ascent = size;
57 let descent = size * 0.25;
58 if (m.actualBoundingBoxAscent !== undefined) {
59 ascent = m.actualBoundingBoxAscent;
60 descent = m.actualBoundingBoxDescent;
61 }
62 const h = Math.ceil(ascent + descent + 2); // +2px padding
63 setI32(wPtr, w);
64 setI32(hPtr, h);
65 }
66
67 // Render renders text into WASM memory as RGBA pixels.
68 // Clears the canvas, draws white text, reads back pixels.
69 // maxW: canvas width (0 = auto from text width).
70 // dataPtr: WASM memory pointer, dataCap: buffer capacity.
71 // Returns actual bytes written (w * h * 4), or 0 if buffer too small.
72 export function Render(fontPtr, fontLen, size, textPtr, textLen, maxW, dataPtr, dataCap) {
73 const family = readStr(fontPtr, fontLen);
74 const text = readStr(textPtr, textLen);
75 _ctx.font = fontCSS(family, size);
76
77 // Measure first.
78 const m = _ctx.measureText(text);
79 const textW = Math.ceil(m.width);
80 let ascent = size;
81 let descent = size * 0.25;
82 if (m.actualBoundingBoxAscent !== undefined) {
83 ascent = m.actualBoundingBoxAscent;
84 descent = m.actualBoundingBoxDescent;
85 }
86 const h = Math.ceil(ascent + descent + 2);
87 const w = maxW > 0 ? Math.min(maxW, textW + 4) : textW + 4;
88
89 if (w * h * 4 > dataCap) return 0; // buffer too small
90
91 ensureCanvas(w, h);
92 _ctx.font = fontCSS(family, size); // re-set after canvas resize
93 _ctx.clearRect(0, 0, w, h);
94 _ctx.fillStyle = 'black';
95 _ctx.textBaseline = 'top';
96 _ctx.fillText(text, 1, 1); // 1px inset for sub-pixel safety
97
98 const imageData = _ctx.getImageData(0, 0, w, h);
99 const bytes = imageData.data; // Uint8ClampedArray, length = w*h*4
100 const n = w * h * 4;
101 new Uint8Array(_wasm.exports.memory.buffer, dataPtr >>> 0, n).set(bytes.subarray(0, n));
102 return n;
103 }
104