| <!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> |