スタートページJavascriptCANVAS

アニメーション(1)
setInterval()/clearInterval()

完成したグラフを一挙に表示する方法はxycoordinate.htmlで示しました。ここでは、xの増加に伴いyがどのように変化するかを、描画に遅延させる方法を示します。

その方法のうち、Javascriptの標準関数であるsetInterval()/clearInterval()を用いる方法を示します。

setInterval()/clearInterval()に似た機能をもつ関数に、setTimeout()/clearTimeout()がありますが、私にはその違いが理解できないので省略します。また、setInterval()/clearInterval()は記述面での制約があり、利用しにくい面があります。他言語ではsleep()やwait()など遅延機能の関数がありますが、Javascriptにはありません。疑似的にそれと似た機能を持たせる工夫を「アニメーション(2)」で示します。


実例とソースリスト

実例

まず[実行]をクリックしてください。

数式の指定
y=f(x)=
独立変数xの計算範囲とグラフの表示範囲
xmin= xmax= ymin= ymax=
遅延時間=(ms)(5~100に変えて再実行してください)

ソースリスト

SCRIPT部のJavascriptは次の通りです。外部ライブラリ「xycoordinate.js」に関しては「XY座標系の設定」を参照してください。
<script src="../xycoordinate.js"></script>
<scrip>
function graph() {
  var f, x,y, x0,y0, dx, interval;
  // フォーム入力の取得
  f = document.iform.f.value;
  xmin = eval(document.iform.xmin.value);
  xmax = eval(document.iform.xmax.value);
  ymin = eval(document.iform.ymin.value);
  ymax = eval(document.iform.ymax.value);
  interval = eval(document.iform.interval.value);
  // XY座標軸の設定
  cw = 400; ch = 400;
  setCanvas("canvas1", "black");
  drawLine(xmin,0, xmax,0, "aqua", 1);
  drawLine(0,ymin, 0,ymax, "aqua", 1);
  // 初期値設定
  dx = (xmax-xmin)/100;
  x = xmin;
  x0 = x;
  y0 = eval(f);
  // ループ
  var timerId = setInterval(function(){
    x = x + dx;                    ┐
    if(x>xmax) clearInterval(timerId); ・・・打ち切り │
    y = eval(f);                    │
    drawLine(x0,y0, x,y, "white", 4);         │繰り返し処理
    x0 = x;                      │
    y0 = y;                      ┘
  }, interval);
}
</script>

BODY部の「遅延時間=<input type="text" size="3" name="interval" value="20"></input>」で、interval=20を指定しています。20/1000秒間隔でループの部分が繰り返し処理されるので、グラフが伸びていくように見えるのです。intervalの値をにより速度を変えることができます。

遅延の方法:setInterval()/clearInterval()

事前説明

遅延して表示するのではなく一挙に表示するのであれば、「ループ」の部分は次のようになります(forを用いるほうが簡潔でしょうが、説明の都合で、whileを用います)。このループに入る前にxの初期値設定が必要ですが割愛しています。
  while(x<=xmax-dx) {
    x = x + dx;
    y = eval(f);
    drawLine(x0,y0, x,y, "white", 4);
    x0 = x;
    y0 = y;
  }

sleep()やwait()のような遅延関数があれば、簡単に実現できます。
  while(x<=xmax) {
    sleep(50);  ・・・システムが50ms停止する
    x = x + dx;
    y = eval(f);
    drawLine(x0,y0, x,y, "white", 4);
    x0 = x;
    y0 = y;
  }

ところが、、残念なことにJavascriptには、そのような遅延関数がありません(これはJavascriptの性質に基づくのだそうですが、私は理解していません)。

遅延の方法
  setInterval()  一定時間ごとに処理を行う
  clearInterval() setInterval()を止める

遅延関数に代わって   // ループ
  var timerId = setInterval(function(){
    x = x + dx;                    ┐
    if(x>xmax) clearInterval(timerId); ・・・打ち切り │
    y = eval(f);                    │
    drawLine(x0,y0, x,y, "white", 4);         │繰り返し処理
    x0 = x;                      │
    y0 = y;                      ┘
  }, interval);

setInterval()の一般形は、
     setInterval(処理処理内容), 一定時間)
です。「一定時間(ms)ごとに処理内容を繰り返して実行せよ」という意味です。
 通常は、処理内容を無名関数function()にして、{ }内に繰り返し処理を記述します。
     setInterval(function() {
       :
       :繰り返し処理
       :
     } , 一定時間);

setInterval()は、whileと異なり、ループを抜け出す条件を記述できません。setInterval()を止める関数clearInterval()を用います。一般形は、
     clearInterval(止めるsetIntervalの名称)>
ですので、setInterval()に名前を付ける必要があります。それで、「ループ」の一般形
     var timerId = setInterval(function(){
       :
       :if (打ち切り条件) clearInterval(timerId)
       :
       :繰り返し処理
       :
     } , 一定時間);

のようになります。timerIdは任意の名称でかまいません。

clearIntervalに関する留意点

複数個所に記述できる

「実例」のグラフの場合、「x>xmax」だけでなく、グラフがCANVASの上端に達したとき「y>ymax」も打切条件にするなど、「ループ」の中ならば、clearIntervalは複数個所に記述できます(ループの外では記述できません)。
  // ループ
  var timerId = setInterval(function(){
    x = x + dx;
    if(x>xmax) clearInterval(timerId);
    y = eval(f);
    if(y>ymax) clearInterval(timerId);
    drawLine(x0,y0, x,y, "white", 4);
    x0 = x;
    y0 = y;
  }, interval);

clearIntervalの機能は、returnやbreakと似ていますが、微妙に異なります。次の場合、 alertされるものは何でしょうか?
  var timerId = setInterval(function(){
    x = x + dx;
    if (x>xmax) alert("A");        ・・・A
    if(x>xmax) clearInterval(timerId);
    if (x>xmax) alert("B");        ・・・B
    y = eval(f);
    drawLine(x0,y0, x,y, "white", 4);
    x0 = x;
    y0 = y;
    if (x>xmax) alert("C");        ・・・C
  }, interval);
  alert("D");                ・・・D

正解は「A→B→C」です。
 最初のAは当然ですが、clearIntervalが発生しても、続くB~Cは実行されるのです。しかし、Cから先頭に戻って2度目のAが実行されることはないし、ループから脱出してDが実行されることもないのです。
 B~Cが実行されるため、もし x>xmax 直後のxの値により、y = eval(f)が定義されないような場合にはエラーになります。
 Dには到達しないのです。Dを実行する必要があるならば、次のように記述する必要があります。
    if(x>xmax) {
      alert("D");
      clearInterval(timerId);
    }

ここの「実例」では、こののような懸念はなく、clearInterval=returnと考えてもよい環境ですので、ソースリストのようにしました(厳密には「if (x > xmax-dx)」とすべきでしょうが)。

setIntervalに関する留意点

「var timerId = setInterval(…);」を複数回通ってはならない

私はこの理由を正確に知らないのですが、繰り返し処理がいつ実行されるのかが不明確だと考えると当然な気もします。例えば、次のような記述はできないのです。
   for(i=0; i<=imax; i++) {
     var timerId = setInterval(function(){
       :
       :
     }, interval}
   }

例えば、
  グラフ1:y1 = f(x) x*(x-1)*(x-2)
  グラフ2:y2 = g(x) = x*(x-1)
を同一CANVASに描画する場合を考えます。

2つのグラフを同時に描画するのであれば、xに対するyの値をyh=f(x)=eval(f)とyg=g(x)=eval(g)の二つにして、次のように記述すれば、setIntervalは1回ですみます。
  // 初期値設定
  f = "x*(x-1)*(x-2)";
  g = "x*(x-1)";
  dx = (xmax-xmin)/100;
  x = xmin;
  x0 = x;
  yf0 = eval(f);
  yg0 = eval(g);
  // ループ
  var timerId = setInterval(function(){
    x = x + dx;
    if(x>xmax) clearInterval(timerId);
    yf = eval(f);
    drawLine(x0,yf0, x,yf, "white", 4);
    yg = eval(g);
    drawLine(x0,yg0, x,yg, "yellow", 4);
    x0 = x;
    yf0 = yf;
    yg0 = yg;
  }, interval);

処理全体でsetIntervalは唯一でなければならない(のだろうか?)

ところが、グラフ1を描画した後で、グラフ2を付け加えるようにしようとすると、私はその方法を知りません。失敗1も失敗2もグラフ2しか表示されないのです。ご教示いただければ幸甚です。

失敗1
// グローバル変数
var f, x,y, x0,y0, dx, interval;

function setup() {
  // XY座標軸の設定
  cw = 400; ch = 400;
  xmin = -2; xmax = 4; ymin = -2; ymax = 4;
  setCanvas("canvas1", "black");
  drawLine(xmin,0, xmax,0, "aqua", 1);
  drawLine(0,ymin, 0,ymax, "aqua", 1);
}

function drow(phase) {
  // 初期値設定
  if (phase==1) { f = "x*(x-1)*(x-2)"; iro = "white"; }
  else      { f = "x*(x-1)";   iro = "yellow"; }
  interval = 20;
  dx = (xmax-xmin)/100;
  x = xmin;
  x0 = x;
  y0 = eval(f);
  // ループ
  var timerId = setInterval(function(){
    x = x + dx;
    if(x>xmax) {
      clearInterval(timerId);
      return;
    }
    y = eval(f);
    drawLine(x0,y0, x,y, iro, 4);
    x0 = x;
    y0 = y;
  }, interval);
}

function rei2() {
  setup();
  drow(1);
  drow(2);
}

失敗2
// グローバル変数
var f, x,y, x0,y0, dx, interval;

function setup() {
   ;   左と同じ
   ;
}

function drow1() {
  // 初期値設定
  f = "x*(x-1)*(x-2)";
    :
    :
  // ループ   var timerId1 = setInterval(function(){
    x = x + dx;
    if(x>xmax) {
       clearInterval(timerId1);
       return;
    }
    :
    :
  }, interval);
}

function drow2() {
  // 初期値設定
  f = "x*(x-1)";
    :
    :
  // ループ
  var timerId2 = setInterval(function(){
    x = x + dx;
    if(x>xmax) {
       clearInterval(timerId2);
       return;
    }
    :
    :
  }, interval);
}

function rei3() {
  setup();
  drow1();
  drow2();
}