let panicImpl; let logImpl; function makeLogFunction(level) { return (length, pMessage) => { logImpl(level, length, pMessage); }; } let { instance: hakuInstance, module: hakuModule } = await WebAssembly.instantiateStreaming( fetch(HAKU_WASM_PATH), { env: { panic(length, pMessage) { panicImpl(length, pMessage); }, trace: makeLogFunction("trace"), debug: makeLogFunction("debug"), info: makeLogFunction("info"), warn: makeLogFunction("warn"), error: makeLogFunction("error"), }, }, ); let memory = hakuInstance.exports.memory; let w = hakuInstance.exports; let textEncoder = new TextEncoder(); function allocString(string) { let size = string.length * 3; let align = 1; let pString = w.haku_alloc(size, align); let buffer = new Uint8Array(memory.buffer, pString, size); let result = textEncoder.encodeInto(string, buffer); return { ptr: pString, length: result.written, size, align, }; } function freeString(alloc) { w.haku_free(alloc.ptr, alloc.size, alloc.align); } let textDecoder = new TextDecoder(); function readString(size, pString) { let buffer = new Uint8Array(memory.buffer, pString, size); return textDecoder.decode(buffer); } function readCString(pCString) { let memoryBuffer = new Uint8Array(memory.buffer); let pCursor = pCString; while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) { pCursor++; } let size = pCursor - pCString; return readString(size, pCString); } class Panic extends Error { name = "Panic"; } panicImpl = (length, pMessage) => { throw new Panic(readString(length, pMessage)); }; logImpl = (level, length, pMessage) => { console[level](readString(length, pMessage)); }; w.haku_init_logging(); export class Pixmap { #pPixmap = 0; constructor(width, height) { this.#pPixmap = w.haku_pixmap_new(width, height); this.width = width; this.height = height; } destroy() { w.haku_pixmap_destroy(this.#pPixmap); } clear(r, g, b, a) { w.haku_pixmap_clear(this.#pPixmap, r, g, b, a); } get ptr() { return this.#pPixmap; } getArrayBuffer() { return new Uint8ClampedArray( memory.buffer, w.haku_pixmap_data(this.#pPixmap), this.width * this.height * 4, ); } getImageData() { return new ImageData(this.getArrayBuffer(), this.width, this.height); } } // NOTE: This must be kept in sync with ContKind on the haku-wasm side. export const ContKind = { Scribble: 0, Dotter: 1, }; export class Haku { #pInstance = 0; #pBrush = 0; #brushCode = null; constructor(limits) { console.groupCollapsed("construct Haku"); let pLimits = w.haku_limits_new(); for (let name of Object.keys(limits)) { w[`haku_limits_set_${name}`](pLimits, limits[name]); } this.#pInstance = w.haku_instance_new(pLimits); this.#pBrush = w.haku_brush_new(); w.haku_limits_destroy(pLimits); console.groupEnd(); } destroy() { w.haku_brush_destroy(this.#pBrush); w.haku_instance_destroy(this.#pInstance); } setBrush(code) { w.haku_reset(this.#pInstance); // NOTE: Brush is invalid at this point, because we reset removes all defs and registered chunks. if (this.#brushCode != null) freeString(this.#brushCode); this.#brushCode = allocString(code); let statusCode = w.haku_compile_brush( this.#pInstance, this.#pBrush, this.#brushCode.length, this.#brushCode.ptr, ); if (!w.haku_is_ok(statusCode)) { if (w.haku_is_diagnostics_emitted(statusCode)) { let diagnostics = []; for (let i = 0; i < w.haku_num_diagnostics(this.#pBrush); ++i) { diagnostics.push({ start: w.haku_diagnostic_start(this.#pBrush, i), end: w.haku_diagnostic_end(this.#pBrush, i), message: readString( w.haku_diagnostic_message_len(this.#pBrush, i), w.haku_diagnostic_message(this.#pBrush, i), ), }); } return { status: "error", errorKind: "diagnostics", diagnostics, }; } else { return { status: "error", errorKind: "plain", message: readCString(w.haku_status_string(statusCode)), }; } } return { status: "ok" }; } #statusCodeToResultObject(statusCode) { if (!w.haku_is_ok(statusCode)) { if (w.haku_is_exception(statusCode)) { return { status: "error", errorKind: "exception", description: readCString(w.haku_status_string(statusCode)), message: readString( w.haku_exception_message_len(this.#pInstance), w.haku_exception_message(this.#pInstance), ), }; } else { return { status: "error", errorKind: "plain", message: readCString(w.haku_status_string(statusCode)), }; } } else { return { status: "ok" }; } } beginBrush() { return this.#statusCodeToResultObject(w.haku_begin_brush(this.#pInstance, this.#pBrush)); } expectedContKind() { return w.haku_cont_kind(this.#pInstance); } contScribble(pixmap, translationX, translationY) { return this.#statusCodeToResultObject( w.haku_cont_scribble(this.#pInstance, pixmap.ptr, translationX, translationY), ); } contDotter({ fromX, fromY, toX, toY, num }) { return this.#statusCodeToResultObject( w.haku_cont_dotter(this.#pInstance, fromX, fromY, toX, toY, num), ); } async evalBrush(options) { let { runDotter, runScribble } = options; let result; result = this.beginBrush(); if (result.status != "ok") return result; while (this.expectedContKind() != ContKind.Invalid) { switch (this.expectedContKind()) { case ContKind.Scribble: result = await runScribble((pixmap, translationX, translationY) => { return this.contScribble(pixmap, translationX, translationY); }); return result; case ContKind.Dotter: let dotter = await runDotter(); result = this.contDotter(dotter); if (result.status != "ok") return result; break; } } return { status: "ok" }; } }