営業の枠から飛び出せ!
営業の枠から飛び出せ!
2018.12.20
#172
それいけ!フロントエンド

数学の時間はいつも寝ていたフロントエンドエンジニアが使うささやかなスニペット

ヒガ

どうも、フロントエンドエンジニアのヒガです。

中学生のころまでは数学が好きでした。しかし、気づけば高校に入学した年の夏以降、数学の授業はだいたい寝るようになっていました。それ以外もですが。あまりの眠気に2問だけ解いた数学のテスト、ふたつとも間違えて0点だったのは未だに記憶に残っています。

あれから十数年。いまでは、フロントエンドエンジニアとして働く上で、数学の愛しさと切なさと心強さを感じています。直接的な数学の話ではないのですが、いくつかのスニペットを、どのような場合に使ったかの話を交えてお伝えします。

範囲と停止と繰り返し(JavaScript)

たとえばスライダーなどのオプションには、たいていループの設定があります。これは範囲を超えた場合にどう処理するかとういことです。基本的には2つの処理があります。

  1. 範囲を超えさせない(ループしない)
  2. 範囲を超えると反対の端点にいく(ループする)

このような単体で考えることのできる処理は、大抵の場合は分けて実装することができます。実際に僕がスライダーなどを作る際にはそうしています。そのときのスニペットです。

範囲を超えさせない

/**
 * @param {number} num - 指定値
 * @param {number} min - 最小値
 * @param {number} max - 最大値
 * @return {number}
 */
const clamp = (num, min, max) => {
  return min > num ? min : max < num ? max : num;
};

関数名になっている clamp(クランプ)とは締め具のことです。つまり範囲の端点まで数値が達するとそこで止めますよってこと。これは一般的な関数名で、GLSL などではビルトイン関数として用意されています。ワンライナーになっているので見づらいですが、単純な条件分のみによって構成されています。Math.max() と Math.min() を合わせてもできますね。

範囲を超えると反対の端点にする

/**
 * @param {number} num - 指定値
 * @param {number} min - 最小値
 * @param {number} max - 最大値
 * @return {number}
 */
const hoop = (num, min, max) => {
  const range = max - min + 1;
  let mod = (num - min) % range;
  if (0 > mod) {
    mod = range + mod;
  }
  return mod + min;
};

関数名になっている hoop(フープ)とは輪のことです。つまり範囲の端点を超えると反対の端点になるってこと。マリオブラザーズを想像するとわかりやすかも。これは調べても良くわからなかったので clamp(クランプ)っぽく適当に命名しました。一般的な関数名があれば教えてほしいです。 肝としては剰余算ですかね。あとは調整って感じですが、正直エレファントにも思えます。エレガントな書き方があったら教えてほしいです。

位置と順番と色情報(Canvas)

Canvasの getImageData の返り値には data, width, height があります。

data はピクセルごとの色情報を RGBA(0〜255)にして一次元配列で返します。分かりやすく以下の画像で説明してみます。

小さくて存在にすら気づかないかもですが、この画像は幅が2pxで高さが2pxの2×2サイズの画像です。小さすぎるので100倍に拡大します。

この画像を縦横2分割して考え、先程の2×2サイズの画像として考えて進めます。
1pxごとのRBGA(0〜255)は以下です。

  • 左上(赤)→ R:210, G:73, B:115, A:255
  • 右上(緑)→ R:64, G:195, B:138, A:255
  • 左下(黄)→ R:224, G:178, B:80, A:255
  • 右下(青)→ R:73, G:121, B:210, A:255

getImageData は引数で取得する範囲を設定するのですが、今回は画像全体を取得するとして考えます。そして、data の値は左上起点で、Z型に右下へと順番に並んでいます。上記のリストと同じ順番ですね。

// こんな感じです
['左上(赤)', '右上(緑)', '左下(黄)', '右下(青)']

// つまりこれ
[
  210, 73, 115, 255, // 左上(赤)
  64, 195, 138, 255, // 右上(緑)
  224, 178, 80, 255, // 左下(黄)
  73, 121, 210, 255, // 右下(青)
]

何を説明したいのかというと、data は画像の色情報を左上起点として一次元配列になっているということです。

では本題です。getImageData で取得したデータを元に、特定の情報を取得することがあります。僕の場合は WebGL で頂点データを作成するときに必要に迫られました。そのときのスニペットです。

※今回は深く言及しませんが、Canvas と WebGL は座標系が違うので、そのままだと上下反転して表示されます。

座標からインデックスを取得する

WebGL で頂点を作る際に、先に座標を決めて、その位置の色情報を取得するときに使いました。

/**
 * @param {number} col - getImageData の返り値 width
 * @param {number} x - x座標(0から始まる)
 * @param {number} y - y座標(0から始まる)
 * @return {number}
 */
const getIndex = (col, x, y) => {
  return col * y + x;
};

y座標分を埋めて、x座標分を足す。シンプルですね。

インデックスが分かれば data から色情報を取得できます。気をつけてほしいのは、RGBAも並列になっているので、取得するときは4倍にする必要があるという点です。

const index = getIndex(width, 2, 1); // x座標が2、y座標が1のインデックス
const r = data[index * 4]; // red
const g = data[index * 4 + 1]; // green
const b = data[index * 4 + 2]; // bule
const a = data[index * 4 + 3]; // alpha

インデックスから座標を取得する

WebGLですべてのピクセルの頂点データを作る際に、getImageData で取得したデータには座標情報がないので、data のインデックスから座標を求めるときに使いました。

/**
 * @param {number} col - getImageData の返り値 width
 * @param {number} index - 求めたい座標のインデックス
 * @return {Array}
 */
const getPosition = (col, index) => {
  const x = index % col;
  const y = (index - x) / col;
  return [x, y];
};

先にx座標を調べて、それを元にy座標を調べています。やり方は色々あって、たとえば Math.floor() を使えば、x座標がわからなくてもy座標を求めることもできます。返り値は分かりやすく連想配列にしても良いかも。

すべてのピクセルを取得する時は for などを使います。気をつけるのは、RGBAも並列になっているので、4つおきに取得する必要があります。

for (let i = 0; data.length > i; i += 4) {
  const position = getPosition(width, i);
  const x = position[0]; // x座標
  const y = position[1]; // y座標
}

時間と変化と繰り返し(GLSL)

WebGLを使って表現するときに、繰り返しの動きをする事があります。JavaScript側で演算して渡すこともできますが、高速なGLSL内で演算したいときもあります。僕の場合は大量の頂点を、バラバラの時間で繰り返し動かすことがありました。そのときのスニペットです。

常に変化し続けながら繰り返す

attribute float duration; // 変化時間(ms)
attribute float time; // 経過時間(ms)

void main(void) {
  float progress = fract(time / duration);
}

これは凄く単純です。fract というビルトイン関数は小数点部分を返します。ほぼコレがすべてです。気をつける点としては、1.0 は返ってこないです。まあ、繰り返すことを考えるとたいして問題にならない気はしますが。

間をおきながら変化を繰り返す

attribute vec3 animation;
// animation.x -> 繰り返す迄の時間(ms)
// animation.y -> 変化開始時間の割合位置(0.0〜1.0)
// animation.z -> 変化時間の割合(0.0〜1.0)
attribute float time; // 経過時間(ms)

void main(void) {
  float interval = fract(time / animation.x);
  float progress = min(max(interval - animation.y, 0.) / animation.z, 1.);
}

これは前述したコードにインターバルの仕様を追加したものです。変化開始時間と変化時間を 0.0 〜 1.0 の浮動小数点数で指定するのが少し分かりづらいかも。ms 指定もできないことはないのですが、高速化したいのが前提なので無駄な処理を減らす為にこのようにしています。図の例で説明してみます。

  • 繰り返す迄の時間(animation.x) = 5000(ms)
  • 変化開始時間の割合位置(animation.y) = 0.2
  • 変化時間の割合(animation.z) = 0.6

変化開始時間の割合位置 は 5000ms の 0.2 の位置で 1000ms です。
変化時間の割合 は 5000ms の 0.6 の時間で 3000ms です。
つまり、1000ms から 4000ms の間に 0.0 から 1.0 に変化します。

気をつける点は、前述したコードと同様に 1.0 は返ってこないです。また、「変化時間の割合」が「全体の時間(1.0) – 変化開始時間の割合位置」を超えると最後まで変化しません。

所感

大したものではありませんが結構時間の掛かったのもあり、数学の大切さを身にしみて感じました。学生の頃にきちんと勉強しておけばよかったと。

また、GLSLを書いていると基本やることが計算なので、偉大なる数学者たちには尊敬の念を禁じ得ないです。眠っていたあの時間は戻ってきませんが、今からでも彼らの証明してきたものを少しでも学んで、この仕事に、人生に活かしていこうと考えています。

LIGにWeb制作について相談してみる!