# 曼德勃罗集示例
使用 JS 端计算的 2048 个离散颜色值,将 曼德勃罗集 (在新窗口中打开) 渲染到画布。
# 内容
- 从 WebAssembly 模块导出函数。
- 调用从 WebAssembly 导出的函数。
- 在 JavaScript 中实例化模块的内存,并使用
--importMemory
导入它。 - 通过
--use Math=JSMath
利用 JavaScript 的Math
,而不是本机 libm,以减少模块大小。 - 最后:从 WebAssembly 内存读取和转换数据到渲染到画布的颜色。
# 示例
#!optimize=speed&runtime=stub&importMemory&use=Math=JSMath
/** Number of discrete color values on the JS side. */
const NUM_COLORS = 2048;
/** Updates the rectangle `width` x `height`. */
export function update(width: u32, height: u32, limit: u32): void {
var translateX = width * (1.0 / 1.6);
var translateY = height * (1.0 / 2.0);
var scale = 10.0 / min(3 * width, 4 * height);
var realOffset = translateX * scale;
var invLimit = 1.0 / limit;
var minIterations = min(8, limit);
for (let y: u32 = 0; y < height; ++y) {
let imaginary = (y - translateY) * scale;
let yOffset = (y * width) << 1;
for (let x: u32 = 0; x < width; ++x) {
let real = x * scale - realOffset;
// Iterate until either the escape radius or iteration limit is exceeded
let ix = 0.0, iy = 0.0, ixSq: f64, iySq: f64;
let iteration: u32 = 0;
while ((ixSq = ix * ix) + (iySq = iy * iy) <= 4.0) {
iy = 2.0 * ix * iy + imaginary;
ix = ixSq - iySq + real;
if (iteration >= limit) break;
++iteration;
}
// Do a few extra iterations for quick escapes to reduce error margin
while (iteration < minIterations) {
let ixNew = ix * ix - iy * iy + real;
iy = 2.0 * ix * iy + imaginary;
ix = ixNew;
++iteration;
}
// Iteration count is a discrete value in the range [0, limit] here, but we'd like it to be
// normalized in the range [0, 2047] so it maps to the gradient computed in JS.
// see also: http://linas.org/art-gallery/escape/escape.html
let colorIndex = NUM_COLORS - 1;
let distanceSq = ix * ix + iy * iy;
if (distanceSq > 1.0) {
let fraction = Math.log2(0.5 * Math.log(distanceSq));
colorIndex = <u32>((NUM_COLORS - 1) * clamp<f64>((iteration + 1 - fraction) * invLimit, 0.0, 1.0));
}
store<u16>(yOffset + (x << 1), colorIndex);
}
}
}
/** Clamps a value between the given minimum and maximum. */
function clamp<T>(value: T, minValue: T, maxValue: T): T {
return min(max(value, minValue), maxValue);
}
#!html
<canvas id="canvas" style="width: 100%; height: 100%"></canvas>
<script type="module">
// Set up the canvas with a 2D rendering context
const canvas = document.getElementById("canvas");
const boundingRect = canvas.getBoundingClientRect();
const ctx = canvas.getContext("2d");
// Compute the size of the viewport
const ratio = window.devicePixelRatio || 1;
const width = (boundingRect.width | 0) * ratio;
const height = (boundingRect.height | 0) * ratio;
const size = width * height;
const byteSize = size << 1; // discrete color indices in range [0, 2047] (2 bytes per pixel)
canvas.width = width;
canvas.height = height;
ctx.scale(ratio, ratio);
// Compute the size (in pages) of and instantiate the module's memory.
// Pages are 64kb. Rounds up using mask 0xffff before shifting to pages.
const memory = new WebAssembly.Memory({ initial: ((byteSize + 0xffff) & ~0xffff) >>> 16 });
const buffer = new Uint16Array(memory.buffer);
const imageData = ctx.createImageData(width, height);
const argb = new Uint32Array(imageData.data.buffer);
const colors = computeColors();
const exports = await instantiate(await compile(), {
env: {
memory
}
})
// Update state
exports.update(width, height, 40);
// Translate 16-bit color indices to colors
for (let y = 0; y < height; ++y) {
const yx = y * width;
for (let x = 0; x < width; ++x) {
argb[yx + x] = colors[buffer[yx + x]];
}
}
// Render the image buffer.
ctx.putImageData(imageData, 0, 0);
/** Computes a nice set of colors using a gradient. */
function computeColors() {
const canvas = document.createElement("canvas");
canvas.width = 2048;
canvas.height = 1;
const ctx = canvas.getContext("2d");
const grd = ctx.createLinearGradient(0, 0, 2048, 0);
grd.addColorStop(0.00, "#000764");
grd.addColorStop(0.16, "#2068CB");
grd.addColorStop(0.42, "#EDFFFF");
grd.addColorStop(0.6425, "#FFAA00");
grd.addColorStop(0.8575, "#000200");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 2048, 1);
return new Uint32Array(ctx.getImageData(0, 0, 2048, 1).data.buffer);
}
</script>
注意
示例做出了一些假设。例如,在这个示例中将程序的整个内存用作图像缓冲区,之所以可以这样做,是因为我们知道不会创建任何干扰的静态内存段,这可以通过以下方式实现:
- 使用 JavaScript 的 Math 而不是本机 libm(通常会添加查找表),
- 不使用更复杂的运行时(通常会添加簿记),以及
- 示例的其余部分相对简单(即没有字符串或类似的东西)。
一旦这些条件不再满足,就可以通过指定合适的 --memoryBase
预留一些空间,或者导出动态实例化的内存块,比如 Uint16Array
,并在 WebAssembly 和 JavaScript 中都将其用作颜色索引缓冲区。
# 本地运行
按照 入门 中的说明设置一个新的 AssemblyScript 项目,并将 module.ts
复制到 assembly/index.ts
,并将 index.html
复制到项目的顶级目录。编辑 package.json
中的构建命令以包括
--runtime stub --use Math=JSMath --importMemory
现在可以使用以下命令编译示例:
npm run asbuild
要查看示例,可以将 index.html
中的实例化从
loader.instantiate(module_wasm, {
env: {
memory
}
}).then(({ exports }) => {
修改为
WebAssembly.instantiateStreaming(fetch('./build/optimized.wasm'), {
env: {
memory
},
Math
}).then(({ exports }) => {
因为这里最终不需要使用 加载器(没有交换托管对象)。如果使用加载器,它会自动提供 JavaScript 的 Math
。
一些浏览器可能会限制仅打开 index.html
时 fetch
本地资源,但可以使用本地服务器作为解决方法
npm install --save-dev http-server
http-server . -o -c-1