Javascript 関数を繰り返して呼び出す

callBack 関数が true を返す間、定期的に関数を呼び出す

class Repeater {
  
  #cnt = null;
  #timerId = null;

  #loop = function () {
    if (this.#cnt && this.cbFunc (--this.#cnt) && this.#cnt)
      this.#timerId = setTimeout (this.#loop.bind (this), this.interval);
    else
      this.stop ();
  }

  //_______

  constructor (cbFunc, count, interval) {
    this.cbFunc = cbFunc;
    this.count = this.#cnt = count;
    this.interval = interval;
  }

  start (force = false) {
    this.stop ();
    if (force)
      this.#cnt = this.count;
    this.#loop ();
    return this;
  }

  stop (force = false) {
    if (this.#timerId)
      this.#timerId = clearTimeout (this.#timerId) || null;
    if (force)
      this.#cnt = 0;
    return this;
  }

  //_______

  static create (cbFunc, count = 1, interval = 1000) {
    if ('function' !== typeof cbFunc)
      throw new Error ('関数ではありません');

    let obj = new this (cbFunc, count, interval);
    return obj.start ();
  }
}

callBack 関数が true を返す間、定期的に関数を呼び出す。その2
勉強のため promise を使ってみた。

class AsyncRepeater {
  
  #cnt = null;
  #timerId = null;

  #loop = function () {
    if (this.#cnt && this.cbFunc (--this.#cnt) && this.#cnt) //マイナスに対し ―∞ へ
      this.#timerId = setTimeout (this.#loop.bind (this), this.interval);
    else
      this.end ();
  };

  //_______

  constructor (cbFunc, count, interval, resolve, reject) {
    this.cbFunc = cbFunc;
    this.count = this.#cnt = count;
    this.interval = interval;
    this.resolve = resolve;
    this.reject = reject;
  }

  start (force = false) {
    this.stop ();
    if (force)
      this.#cnt = this.count;
    this.#loop ();
    return this;
  }

  restart () {
    this.#loop ();
    return this;
  }

  stop () {//あくまでも中断
    if (this.#timerId)
      this.#timerId = clearTimeout (this.#timerId) || null;
    return this;
  }

  end (force = false) {
    this.stop ();
    if (force && this.reject)//強制中断
      this.reject ();
    else if (this.resolve)
      this.resolve ();
    return this;
  }

  //_______

  static create (cbFunc, count = 1, interval = 1000, async = true) {
    if ('function' !== typeof cbFunc)
      throw new Error ();

    let obj, resolve, reject;
    let p =  async
      ? new Promise ((a, b)=> {resolve = a; reject = b})
      : null;

    obj = new this (cbFunc, count, interval, resolve, reject);
    Object.assign (obj, {promise: p});
    obj.start ();
    return obj;
  }
}

応用して、画面をフェードイン・フェードアウトで切り替えられるチェンジャーを作ってみた
css のアニメーションが楽だったような感じ。
LI 要素に data-change_time を設け、以下のようにセットする
構文は、[フェードインまでの時間, 表示し続ける時間, フェードアウトまでの時間, 空白の時間] とする
停止・再開機能を付けようと思ったが中止。

フリーソフトの exeUSB なるものを見つけた。
USBメモリを装着すると、自動で指定のアプリを起動できる
そこで Chrome --kiosk のモードで起動すれば、電子看板のように使える

html

<ul id="SAMPLE">
  <li data-change_time="1000,1500,1000,1000"> 
  <li ...>
  ...

Script

DisplayChanger.create (SAMPLE);

class DisplayChanger {

  #skip = function (n = 1) { // - !?
    for (let i = 0, I = n % this.children.length; i < I; i++)
      this.parent.appendChild (this.current);
  };

  #getStateTimes (e = this.current) {
    let state = /^(?:\s*([+-]?\d*)(?:,\s*([+-]?\d*)(?:,\s*([+-]?\d*)(?:,\s*([+-]?\d*)?)?)?)?)?(?:,.*)?$/.exec (e.dataset.change_time);
    if (null == state) console.error (e, e.dataset.change_time);
    return state.slice (1).map ((a, i, _, val = parseInt (a))=> isNaN (val) ? this.default[i]: val);
  };

  #next = function () {
    this.#skip ();
    this.#loop ();
  };

  #loop = async function () {
    let e = this.current;
    let [t0, t1, t2, t3] = this.#getStateTimes (e);
    let i = this.interval;

    await (AsyncRepeater.create (this.#fadeIn (e, t0, i), -1, i)).promise;
    await this.#keep (t1);
    await (AsyncRepeater.create (this.#fadeOut (e, t2, i), -1, i)).promise;
    await this.#keep (t3);

    this.#next ();
  };

  #keep = function (a) {
    return new Promise (c=> setInterval (c, a));
  }

  #fadeIn = function (e, time, interval) {
    let max = Math.round (time / interval);
    let step = 1 / max;
    let opacity = 0;

    return function () {
      opacity += step;
      e.style.opacity = (1 < opacity) ? 1: opacity;
      return opacity < 1;
    };
  }

  #fadeOut = function (e, time, interval) {
    let max = Math.round (time / interval);
    let step = 1 / max;
    let opacity = 1;

    return function () {
      opacity -= step;
      e.style.opacity = (opacity < 0) ? 0: opacity;
      return 0 < opacity;
    };
  }


  //_____

  constructor (a, b, c, d) {
    this.parent   = a;
    this.begin    = b;
    this.interval = c;
    this.default  = d;//[fadeInTime, displayTime, fadeOutTime, blankTime]
    this.#skip (b);
    this.isInOpration = false; //作動中か?
  }

  start () {
    this.isInOpration = true;
    this.#loop ();
  }

  //_____

  get current () {
    return this.parent.firstElementChild;
  }

  get children () {
    return [...this.parent.children];
  }

  //_____
  
  static create (parent, begin = 0, interval = 20, fadeInTime = 1000, displayTime = 5000, fadeOutTime = 500, blankTime = 300) {
    if (1 > arguments.length)
      throw new Error ('引数がありません');

    [...parent.children].forEach (e=> e.style.opacity = 0);

    let tm = parent.dataset;
    interval   = Math.max (1, parseFloat (tm.interval || interval));
    fadeInTime = Math.max (1, parseFloat (tm.fadeInTime || fadeInTime));
    displayTime= Math.max (1, parseFloat (tm.displayTime || displayTime));
    fadeOutTime= Math.max (1, parseFloat (tm.fadeOutTime || fadeOutTime));
    blankTime  = Math.max (1, parseFloat (tm.blankTime || blankTime));

    let obj = new this (parent, begin, interval, [fadeInTime, displayTime, fadeOutTime, blankTime]);
    obj.start ();
    return obj;
  }
}