Uint8ClampedArrayに小数を代入すると詰まる


先に結論だけ

Uint8ClampedArrayに小数を代入すると、0.5以下が切り捨てられます。複合代入演算子を利用しても同様です。

この記事の想定する環境

この記事は以下の環境で動作することを想定しています。

  • Google Chrome 78.0.3904.108

記事を読む前に、お手元の環境をご確認ください。

Uint8ClampedArrayとは

Uint8ClampedArrayは、8ビット符号なし整数値の配列です。JavaScriptのNumberは、浮動小数点を扱いますがこの配列は0 ~ 255の整数値に値が制限されます。Uint8ClampedArrayなどのTypedArrayは型付きの要素の配列です。

どのような操作で必要となるのか

Canvas要素のImageDataオブジェクトが、ピクセルデータをUint8ClampedArrayで格納しています。TypedArrayは一般的に、バイナリデータをJavaScriptで操作する際のインターフェイスです。

小数を代入してみる

このUint8ClampedArrayに、小数を代入するとどのような振る舞いをするでしょう?情報がなかったため、Codepenでテストコードを書いてみました。

const array = new Uint8ClampedArray(1);

for( let i = 1; i < 101; i++){
  const val =  1.0 - (i / 100)
  array[0] = val;
  console.log( val , array[0] )
}

このコードの出力は以下の通りです。

0.99 1
pen.js:7 0.98 1
pen.js:7 0.97 1
...(中略)...
pen.js:7 0.51 1
pen.js:7 0.5 0
pen.js:7 0.49 0
...(中略)...
pen.js:7 0.020000000000000018 0
pen.js:7 0.010000000000000009 0
pen.js:7 0 0

0.5以下が切り捨て、それ以上では1に丸め込みが行われていました。四捨五入ではなく0.5で切り捨てが行われているのは意外でした。

どんなときに困るのか

JavaScriptのNumber型は浮動小数点なので、うっかり以下のようなコードを書いてしまうとバグを生みます。

array[0] *= 0.8;

このコードではarray[0]が2以下になると、数値がそれ以上減りません。丸め込み処理が行われるためです。例としてCanvasのピクセルを徐々に暗くしたいのに、いつまでもわずかな灰色が残ります。

array[0] = ( array[0] * 0.8 ) | 0;

Math.floorやビット演算で明示的に小数を切り捨てると、この問題は解決します。

参考記事 : HTML5 canvas のパフォーマンスの改善 / 浮動小数点座標の使用を控える

整数型を持った言語では当然問題になるコードですが、JavaScriptでも同様の注意が必要となる場合があります。
とくにCanvasを操作する場合はご注意ください。

以上、ありがとうございました。