2点間のイージングであれば比較的簡単に実装できますが、任意の複数点からなる範囲の移動をイージングを使って移動したかったので仕組みを作ってみました。
きっともっといい方法があるような気がしますが、とりあえず動くものが作れたのでよしとしますw
実際の動作サンプルはjsdo.itに上げてあります。
※ サンプル内で使用しているvec2
は、自作の数学用ライブラリを使っています。
解説
大まかな流れは以下のようにして計算しています。
- 全頂点の「距離」を計算する
- 各頂点間の「距離」を配列に保持
-
update
内にて、現在の経過時間から「全体の距離」の中でどの位置にいるかをイージング関数を使って計算する - 求めた位置から、どの頂点間にいるかを計算
- さらにその頂点間のどの位置かの「比率」を出す
- 求めた比率と該当の頂点間の差分ベクトルから現在いる地点を求める
- (6)で求めたベクトルを該当頂点間の始点となる点に足して最終的な実際の位置を求める
というような流れで計算しています。
本当は$f(x) = y$みたいな式に変換できれば求めやすいのですが、マイナス方向だったり線が交差したり、みたいなこともあるので今回の実装にしました。
(というかそういう式の算出ができないとも言う)
ということで、該当部分を抜き出しつつコード解説
情報を集める
まずは渡された全頂点から、計算に必要な情報を計算・取得します。
setup: {
enumerable: true,
value: function (vertices) {
var next = vec2(this.origin);
this.vertices.push(vec2(this.origin));
for (var i = 0, l = vertices.length; i < l; i++) {
var vertex = vertices[i];
next = vec2.add(next, vertex);
this.vertices.push(vec2(next));
}
/////////////////////////////////////////////////////////////////////////////
var lengthArray = [];
var max = 0;
for (var i = 0, l = vertices.length; i < l; i++) {
var length = vec2.norm(vertices[i]);
max += length;
lengthArray.push(length);
}
this.lengthArray = lengthArray;
this.currentPosition = 0;
this.maxLength = max;
},
},
やっていることは単純で、引数で渡された頂点の配列を、始点からの絶対座標に変換し、それを配列に保持しています。
※ 引数で渡される頂点はすべて「ひとつ前の」頂点からの相対値を指定する想定で作っています。
ふたつめのfor
文では、引数で渡された頂点配列からすべての「長さ」を計算し、各頂点間の距離を配列に入れ、さらに全頂点を合わせた合計距離を計算しています。
現在の進捗をシンプルに求める
全体の距離を求めているのは、「現在の位置が全体のどの位置か」というのを1次元で考えられるようにするためです。
総距離が500
で現在地が300
なら、今は60%
の位置にいる、という情報が手にはいります。
以上で必要な情報が集まりました。
あとは時間を進め、現在地点をupdateしていき現在の位置を計算してやれば点が動くようになります。
位置の更新処理
位置の更新は普通のイージング関数を使います。
アニメーションさせたい時間と、現在の経過時間から比率としての経過時間を求めます。
その時間と総距離を利用して「現在の位置」を1次元的に求めます。
var now = Date.now();
this.time += now - this.previousTime;
this.previousTime = now;
var duration = this.time / this.interval;
if (duration > 1.0) {
this.stop();
return;
}
// 現在のポイントの位置を更新
this.currentPosition = this.easing(0, this.maxLength, duration);
ここまでは普通のイージングの処理と変わりません。
今回はsin波を利用したものになっていますが、他のイージング関数を使っても大丈夫です。
そして求めた位置情報を使って、最終的な2次元の位置を求めます。それが以下です。
var cur = 0;
var index = -1;
var prev = 0;
var p1 = 0;
var p2 = 0;
for (var i = 0, l = this.lengthArray.length; i < l; i++) {
var len = this.lengthArray[i];
prev = cur;
cur += len;
if (this.currentPosition <= cur) {
index = i;
p1 = prev;
p2 = cur;
break;
}
}
var delta = this.currentPosition - p1;
var range = p2 - p1;
var ratio = (range === 0) ? 0 : (delta / range);
var v1 = vec2(this.vertices[index + 0]);
var v2 = vec2(this.vertices[index + 1]);
var deltaVector = vec2.sub(v2, v1);
deltaVector = vec2.multiplyScalar(deltaVector, ratio);
v1 = vec2.add(v1, deltaVector);
vec2.copy(v1, this.point);
図にすると以下のイメージです。
(0, 0)
からスタートして点をつないでいきます。
それぞれの頂点は前の頂点からの相対位置です。
図では分かりやすさのためにすべてX-Y軸に対して平行ですが、もちろん斜めのラインでも基本は同じです。
例
上記の図は現在の移動位置が250
のときを想定しています。
つまり赤のラインになっているところが進んだ距離ですね。
そして求めるべきは全体の進んだ進捗度と、今どの点にいるかの頂点間、そして頂点間内での進捗度です。
上記例で言えば進んだ進捗度は250 / 511 = 0.489
、頂点は(90, 0) <-> (0, 106)
の間です。
該当頂点間の進捗度は以下の式で求まります。
$\frac{現在地点から始点の距離を引いた距離}{頂点間の距離}$
で求まります。
これで求めた比率を、頂点間の差分ベクトルにかけてやることで求めた頂点間の始点からどれくらい進んでいるか、が分かります。
あとは求めたベクトルと始点を足してやれば今の位置が求まる、というわけです。
まとめ
最後に、該当クラスの全体のコードを載せておきます。
実際に動いているものはjsdo.itにあるのでそちらで動作確認もできます。
(function () {
'use strict';
function Line(vertices) {
// 初期化時はアニメーションをオフに
this.running = false;
this.origin = vec2(0, 0);
this.vertices = [];
this.setup(vertices);
// Create a light point texture.
this.point = vec2(0, 0);
}
Line.prototype = Object.create({}, {
constructor: {
value: Line
},
/**
* アニメーションのインターバル
*/
interval: {
writable: true,
enumerable: true,
value: 5000
},
/**
* アニメーションなどに必要なデータを集める
*/
setup: {
enumerable: true,
value: function (vertices) {
var next = vec2(this.origin);
this.vertices.push(vec2(this.origin));
for (var i = 0, l = vertices.length; i < l; i++) {
var vertex = vertices[i];
next = vec2.add(next, vertex);
this.vertices.push(vec2(next));
}
/////////////////////////////////////////////////////////////////////////////
var lengthArray = [];
var max = 0;
for (var i = 0, l = vertices.length; i < l; i++) {
var length = vec2.norm(vertices[i]);
max += length;
lengthArray.push(length);
}
this.lengthArray = lengthArray;
this.currentPosition = 0;
this.maxLength = max;
},
},
/**
* アニメーションスタート
*/
start: {
configurable: true,
enumerable: true,
value: function () {
this.running = true;
this.time = 0;
this.previousTime = Date.now();
this.currentPosition = 0;
},
},
/**
* アニメーションストップ
*/
stop: {
enumerable: true,
value: function () {
this.running = false;
},
},
easing: {
enumerable: true,
value: function (a, b, x) {
var f;
f = (1.0 - Math.cos(x * Math.PI)) * 0.5;
return a * (1.0 - f) + b * f;
},
},
draw: {
enumerable: true,
value: function (ctx) {
ctx.beginPath();
var radius = 5;
ctx.arc(this.point.x, this.point.y, radius, 0, Math.PI * 2, true);
ctx.fill();
},
},
/**
* アニメーションアップデート
*/
update: {
enumerable: true,
value: function () {
var now = Date.now();
this.time += now - this.previousTime;
this.previousTime = now;
var duration = this.time / this.interval;
if (duration > 1.0) {
this.stop();
return;
}
// 現在のポイントの位置を更新
this.currentPosition = this.easing(0, this.maxLength, duration);
var cur = 0;
var index = -1;
var prev = 0;
var p1 = 0;
var p2 = 0;
for (var i = 0, l = this.lengthArray.length; i < l; i++) {
var len = this.lengthArray[i];
prev = cur;
cur += len;
if (this.currentPosition <= cur) {
index = i;
p1 = prev;
p2 = cur;
break;
}
}
var delta = this.currentPosition - p1;
var range = p2 - p1;
var ratio = (range === 0) ? 0 : (delta / range);
var v1 = vec2(this.vertices[index + 0]);
var v2 = vec2(this.vertices[index + 1]);
var deltaVector = vec2.sub(v2, v1);
deltaVector = vec2.multiplyScalar(deltaVector, ratio);
v1 = vec2.add(v1, deltaVector);
vec2.copy(v1, this.point);
},
}
});
// Export
window.Line = Line;
}());
ちなみに生成はこんな感じ。
var num = 30;
for (var i = 0; i < num; i++) {
var dir = Math.random() < 0.2 ? -1 : 1;
var x = Math.random() * cv.width / num * dir;
var y = Math.random() * cv.height / num * dir;
var v = vec2(x, y);
vertices.push(v);
}
var line = new Line(vertices);