Demostraremos un caso de uso típico mediante una función simple para convertir una imagen a escala de grises.
Aunque el cálculo del color gris no es muy exigente, demuestra claramente el uso real de WebAssembly en la web: tareas de cálculo intensivo.
Tiempo de ejecución en el browser
A diferencia de nuestros experimentos anteriores con AssemblyScript, esta vez ejecutaremos nuestro módulo Wasm en un navegador. Es posible que primero recuerde cómo usar un navegador web como tiempo de ejecución de Wasm, pero el código es bastante sencillo
WebAssembly
.instantiateStreaming(fetch('grayscale.wasm'), {})
.then(({ instance }) => {
…
}
Para que la API de Fetch funcione, debe servir la página web a través de HTTP (S).
El segundo argumento de instantiateStreaming
es un objeto con importaciones Wasm. Todas las importaciones de entorno para los módulos Wasm, que se compilaron a partir de AssemblyScript, se incluyen en un objeto env:
Cuando se ejecuta en el navegador, debemos importar una función de devolución de llamada de error abortar:
WebAssembly
.instantiateStreaming(fetch('grayscale.wasm'), {
env: {
abort: (_msg, _file, line, column) =>
console.error(Error at ${line}:${column}
)
}
})
En Node.js, el cargador de AssemblyScript ya los proporciona.
Canvas
Para mostrar la imagen y su transformación en un navegador, usaremos el canvaslienzo HTML:
<canvas id="canvas" width="500" height="500"></canvas>
Y su contexto 2D
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const [width, height] = [canvas.width, canvas.height];
Primero, dibujaremos la imagen original en el canvas:
const img = new Image();
img.src = './dibujo-de-un-gato.png';
img.crossOrigin = 'anonymous';
img.onload = () =>
ctx.drawImage(img, 0, 0, width, height);
Luego, obtendremos los datos de imagen con los que trabajaremos:
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
Los datos de la imagen se representan como un Uint8ClampedArray, una matriz unidimensional en el orden RGBA, con valores enteros entre 0 y 255 (inclusive).
Memoria
Debido a que trabajaremos extensamente con la memoria, debemos recordar los fundamentos.
Podemos crear una instancia de memoria a partir de JavaScript e importarla al módulo Wasm o dejar que el módulo inicialice la memoria y exporte su instancia a JavaScript.
Un beneficio del primer enfoque es la posibilidad de inicializar la memoria al tamaño necesario.
Al exportar memoria desde Wasm, el tamaño inicial es una página (64 KB). Para aumentar su tamaño, tenemos que llamar a memory.grow () mediante programación, lo que podría ser poco práctico, al menos en los casos en los que el tamaño se conoce de antemano.
Nuestra imagen puede tener más de 64 KB, por lo que será mejor que creemos una instancia de memoria lo suficientemente grande:
const arraySize = (width * height * 4) >>> 0;
const nPages = ((arraySize + 0xffff) & ~0xffff) >>> 16;
const memory = new WebAssembly.Memory({ initial: nPages });
Aquí viene la trampa. Para poder importar memoria a un módulo Wasm, tenemos que compilar nuestro código AssemblyScript con el indicador --importMemory
:
npm run asbuild:optimized -- --importMemory
Esto generará la siguiente línea en el módulo Wasm compilado:
(import "env" "memory" (memory $0 1))
Ahora, podemos importar nuestro objeto de memoria desde JavaScript a Wasm:
WebAssembly
.instantiateStreaming(fetch('grayscale.wasm'), {
env:{ memory }
})
El siguiente paso es inicializar la memoria con los datos de la imagen:
const bytes = new Uint8ClampedArray(memory.buffer);
for (let i = 0; i < data.length; i++)
bytes[i] = data[i];
Como habrás notado, usamos la misma matriz escrita, Uint8ClampedArray, como datos de imagen para formatear y acceder a los bytes de memoria.
Cuando se llenan los bytes de memoria, podemos ejecutar nuestra función Wasm:
instance.exports
.convertToGrayscale (ancho, alto);
Y almacene el resultado de la memoria nuevamente en la imagen:
for (let i = 0; i < bytes.length; i++)
data[i] = bytes[i];
ctx.putImageData(imageData, 0, 0);
Manipulación De Imágenes
Ahora, cuando tengamos todo lo que necesitamos para ejecutar una función de manipulación de imágenes en un navegador, de hecho escribiremos una. Como se mencionó anteriormente, crearemos una función que convierte una imagen a escala de grises:
export function convertToGrayscale(width: i32, height: i32): void {
const len = width * height * 4;
for (let i = 0; i < len; i += 4 /rgba/) {
const r = load(i);
const g = load(i + 1);
const b = load(i + 2);
const gray = u8(r * 0.2126 + g * 0.7152 + b * 0.0722);
store<u8>(i, gray);
store<u8>(i + 1, gray);
store<u8>(i + 2, gray);
}
}
Como los datos de la imagen contienen cuadriplicados RGBA lineales de enteros con signo de 8 bits (sujetos al rango de valores de 0 a 255), iteramos la matriz de datos en 4 pasos incrementales.
Cargamos los valores RGB de la memoria, calculamos el color gris y almacenamos los valores de color nuevamente en la memoria.