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