# 曼德勃罗集示例

使用 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.htmlfetch 本地资源,但可以使用本地服务器作为解决方法

npm install --save-dev http-server
http-server . -o -c-1