エレベーターメニューを作り直してみた。

使い方のmemo:

//optionは省略可
let option = {
  interval : setInterval のタイマー値,
  offset : new Point (x座標, y座標),
  accell : new Point (x座標における加速度, y座標における加速度),
  position: "top middle bottom left center right" //2つ指定すると吉
}

new Elevator (ターゲットとなる要素, option);

note

    • CSS1Compat の判定は必要か?
    • グローバルな window を直接指定してもよいのでは?
    • 要素の位置の値は整数値ではなかった。
    • move の中で、ターゲット要素の位置を保持していないので値を変化(縮小化)するのに考えを要した。
    • 2次元の値を扱うのにCGのコードで書いたものを応用してみた。簡潔になったかどうかは微妙。

code

{
  const
    abs = Math.abs,
    minimum = 1e-2,
    fx = (a, b, c, d) => 1 < abs (d = a - b) ? d / c: d;

  class Point {
    constructor (x = 0, y = 0)  { this.x = x ; this.y = y; }
    add ({x, y})       { this.x += x; this.y += y; return this; }
    sub ({x, y})       { this.x -= x; this.y -= y; return this; }
    multiply ({x, y})  { this.x *= x; this.y *= y; return this; }
    division ({x, y})  { this.x /= x; this.y /= y; return this; }
    copy ()            { return new Point (this.x, this.y); }
    reduction ({x, y}, {x:sx, y:sy}) {//縮小化
      this.x -= fx (this.x, x, sx);
      this.y -= fx (this.y, y, sy);
      return this;
    }
    equal ({x, y}) {
      return ! (minimum < abs (this.x - x) || minimum < abs (this.y - y));//&&を使いたくないので
    }
  }

  this.Point = Point;
}
//______________________________________________________________________

{
  const

    DEF_OPTIONS = {
      interval : 10,//ms @timerの間隔
      offset   : new Point (0, 0),
      accell   : new Point (25, 25), // 加速度(小さいほど高速)
      position : 'bottom right'
    },


    getIdealPoint =//向かうべき理想の位置
      function () {
        let
          e = this.target,
          d = e.ownerDocument,
          b = d.compatMode == 'CSS1Compat' ? d.documentElement: d.body;
          
        return new Point (b.clientWidth, b.clientHeight) //表示領域から
         .sub (new Point (e.offsetWidth, e.offsetHeight)) //要素のサイズを引き
         .multiply (this.matrix)//positionの位置に移動し
         .add (this.options.offset);//オフセット分だけさらに移動する
      },


    getScroll = //スクロール量を返す
      function () {
        let v = this.target.ownerDocument.defaultView;
        return new Point (v.pageXOffset, v.pageYOffset);
      },


    Locator = // 位置の設定
      function (p) {
        let {x, y} = p;
        this.left = x + 'px';
        this.top = y + 'px';
      },


    Decorator = // 透明度の設定
      function (alpha) {
        alpha = Math.min (Math.max (0, alpha), 100);
        this.visibility = alpha ? 'visible': 'hidden';
        this.opacity = String (alpha / 100);
      },


    Init = // 初期化
      function (target, arg_options) {
        this.target   = target;
        this.options  = Object.assign ({ }, DEF_OPTIONS, arg_options);
        this.timerId  = null;

        let
          x, y,
          win = target.ownerDocument.defaultView,
          style = target.style,
          pstr = this.options.position.split (' ');

        pstr.forEach (str => {
          switch (str) {
          case 'top'    : y = 0;   break;
          case 'middle' : y = 0.5; break;
          case 'bottom' : y = 1;   break;
          case 'left'   : x = 0;   break;
          case 'center' : x = 0.5; break;
          case 'right'  : x = 1;   break;
          }
        });

        this.matrix = new Point (x, y);

        style.cssText = 'margin: 0; position: absolute; white-space: nowrap;';

        Locator.call (style, getIdealPoint.call (this).add (getScroll.call(this)));
        win.addEventListener ('scroll', this, false);// イベント初期化
        win.addEventListener ('resize', this, false);// イベント初期化
      };


  //______________________________________________________________________


  class Elevator {

    constructor (target, arg_options) {
      if (1 > arguments.length)
        throw new Error ('対象となる要素がありません');

      Init.call (this, target, arg_options);
      this.start ();
    }


    start () {// スタート処理
      if (! this.timerId)
        this.timerId = setInterval (this.move.bind (this), this.options.interval);
    }


    stop () {// ストップ処理
      if (this.timerId) {
        clearInterval (this.timerId);
        this.timerId = null;
        Decorator.call (this.target.style, 0);
      }
    }


    handleEvent (event) {//スクロールイベントハンドラー
      this.start ();
    }


    move () {// 移動処理ループ
      let
        target  = this.target,
        style   = target.style,
        rect    = target.getBoundingClientRect (),
        scroll  = getScroll.call (this),//スクロール量
        current = new Point (rect.left, rect.top),//現在のターゲットの位置
        ideal   = getIdealPoint.call (this); //表示領域内での向かうべき位置

      current.reduction (ideal, this.options.accell);//currentをideialにaccell分だけ近づける
      
      if (current.equal (ideal))//ほぼ0なら止める
        this.stop ();

      Decorator.call (style, scroll.y);
      Locator.call (style, current.add (scroll));
    }
  }

  //______________________________________________________________________

  this.Elevator = Elevator;
}