blob: d3da38fbec7036ac4456b865439b58682b2cd68d [file] [log] [blame]
<!DOCTYPE html>
<html>
<!--
Copyright 2022 The IREE Authors
Licensed under the Apache License v2.0 with LLVM Exceptions.
See https://llvm.org/LICENSE.txt for license information.
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-->
<head>
<meta charset="utf-8" />
<title>IREE Static Web Sample</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- TODO(scotttodd): use local copy for CORS webserver / SharedArrayBuffer workarounds? -->
<script src="https://code.createjs.com/1.0.0/easeljs.min.js"></script>
</head>
<body style="background-color: #2b2c30; color: #ABB2BF">
<h1>IREE Static Web Sample</h1>
<canvas id="drawingCanvas" width="256" height="256"
style="border:2px solid #000000; background-color: #FFFFFF;"
oncontextmenu="return false;">
</canvas>
<canvas id="rescaledCanvas" width="28" height="28"
style="border:2px solid #000000; background-color: #FFFFFF;">
</canvas>
<br>
<div style="border:2px solid #000000; background-color: #CCCCCC; padding: 8px; color: #111111" width="400px" height="300px">
<button id="predictButton" disabled onclick="predictDigit()">Predict handwritten digit</button>
<br>
Prediction result: <div id="predictionResult"></div>
</div>
<script>
let setupNativeSample;
let cleanupNativeSample;
let runNativeSample;
let nativeState;
const predictionResultElement = document.getElementById("predictionResult");
const predictButtonElement = document.getElementById("predictButton");
let initialized = false;
const imagePixelCount = 28 * 28;
let imageBuffer;
var Module = {
print: function(text) {
console.log(text);
},
printErr: function(text) {
console.error(text);
},
onRuntimeInitialized: function() {
console.log("WebAssembly module onRuntimeInitialized()");
setupNativeSample = Module.cwrap("setup_sample", "number", []);
cleanupNativeSample = Module.cwrap("cleanup_sample", null, ["number"]);
runNativeSample = Module.cwrap("run_sample", "number", ["number", "number"]);
setupSample();
},
// https://emscripten.org/docs/api_reference/module.html#Module.noInitialRun
noInitialRun: true,
};
function setupSample() {
nativeState = setupNativeSample();
predictButtonElement.disabled = false;
imageBuffer = Module._malloc(imagePixelCount * Float32Array.BYTES_PER_ELEMENT);
initialized = true;
}
// TODO(scotttodd): call this on page suspend?
function cleanupSample() {
initialized = false;
Module._free(imageDataBuffer);
predictButtonElement.disabled = true;
cleanupNativeSample();
nativeState = null;
}
function predictDigit() {
const rawImageData = getRescaledCanvasData();
preprocessImageData(rawImageData);
result = runNativeSample(nativeState, imageBuffer);
if (result != -1) {
predictionResultElement.innerHTML = result;
} else {
predictionResultElement.innerHTML = "Error";
}
}
// https://becominghuman.ai/passing-and-returning-webassembly-array-parameters-a0f572c65d97
// https://developers.google.com/web/updates/2018/03/emscripting-a-c-library#get_an_image_from_javascript_into_wasm
function preprocessImageData(rawImageData) {
// * getImageData() returns a Uint8ClampedArray with RGBA image data
// * this MNIST model takes tensor<1x28x28x1xf32> with grayscale pixels
// in [0.0, 1.0]
// This conversion is terrible, but this is a toy demo with a small image
// Hopefully there aren't any logic / iteration order issues...
const typedArray = new Float32Array(imagePixelCount);
for (let y = 0; y < 28; ++y) {
for (let x = 0; x < 28; ++x) {
const typedIndex = y * 28 + x;
const rawIndex = 4 * (y * 28 + x) + 3; // Assume colorSpace srgb
typedArray[typedIndex] = rawImageData.data[rawIndex] / 255.0;
}
}
// Copy into Wasm heap.
// Note: we could have done the conversion in-place, but this is demo code
Module.HEAPF32.set(typedArray, imageBuffer >> 2);
}
</script>
<script src="sample-web-static-sync.js"></script>
<!-- <script src="sample-web-static-multithreaded.js"></script> -->
<script>
// Forked from:
// https://createjs.com/demos/easeljs/curveto
// https://github.com/CreateJS/EaselJS/blob/master/examples/CurveTo.html
let drawingCanvasElement;
let rescaledCanvasElement, rescaledCanvasContext;
let stage;
let drawingCanvasShape;
let oldPt, oldMidPt;
let titleText;
const primaryColor = "#000000";
const eraseColor = "#FFFFFF";
const stroke = 32;
function initDrawing() {
drawingCanvasElement = document.getElementById("drawingCanvas");
rescaledCanvasElement = document.getElementById("rescaledCanvas");
rescaledCanvasContext = rescaledCanvasElement.getContext("2d");
rescaledCanvasContext.imageSmoothingEnabled = false;
rescaledCanvasContext.mozImageSmoothingEnabled = false;
rescaledCanvasContext.webkitImageSmoothingEnabled = false;
rescaledCanvasContext.msImageSmoothingEnabled = false;
stage = new createjs.Stage(drawingCanvasElement);
stage.autoClear = false;
stage.enableDOMEvents(true);
createjs.Touch.enable(stage);
createjs.Ticker.framerate = 24;
stage.addEventListener("stagemousedown", handleMouseDown);
stage.addEventListener("stagemouseup", handleMouseUp);
drawingCanvasShape = new createjs.Shape();
stage.addChild(drawingCanvasShape);
// Add instruction text.
titleText = new createjs.Text("Click and Drag to draw", "18px Arial", "#000000");
titleText.x = 30;
titleText.y = 100;
stage.addChild(titleText);
stage.update();
}
function handleMouseDown(event) {
if (!event.primary && !event.secondary) { return; }
if (stage.contains(titleText)) {
stage.clear();
stage.removeChild(titleText);
}
oldPt = new createjs.Point(stage.mouseX, stage.mouseY);
oldMidPt = oldPt.clone();
stage.addEventListener("stagemousemove", handleMouseMove);
}
function handleMouseMove(event) {
if (!event.primary && !event.secondary) { return; }
const midPt = new createjs.Point(
oldPt.x + stage.mouseX >> 1, oldPt.y + stage.mouseY >> 1);
const color = event.nativeEvent.which == 1 ? primaryColor : eraseColor;
drawingCanvasShape.graphics.clear()
.setStrokeStyle(stroke, 'round', 'round')
.beginStroke(color).moveTo(midPt.x, midPt.y)
.curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);
oldPt.x = stage.mouseX;
oldPt.y = stage.mouseY;
oldMidPt.x = midPt.x;
oldMidPt.y = midPt.y;
stage.update();
updateRescaledCanvas();
if (initialized) {
// TODO(scotttodd): debounce / rate limit this
predictDigit();
}
}
function handleMouseUp(event) {
if (!event.primary && !event.default) { return; }
stage.removeEventListener("stagemousemove", handleMouseMove);
}
function updateRescaledCanvas() {
rescaledCanvasContext.drawImage(
drawingCanvasElement,
/*sx=*/0, /*sy=*/0,
/*sWidth=*/256, /*sHeight=*/256,
/*dx=*/0, /*dy=*/0,
/*dWidth=*/28, /*dHeight=*/28);
}
function getRescaledCanvasData() {
return rescaledCanvasContext.getImageData(0, 0, 28, 28);
}
initDrawing();
</script>
</body>
</html>