Canvasでマスクをかけたカルーセルをつくる


デモ：https://mo49.github.io/qiita/20170329/

単純な四角形ではない形でカルーセルをつくる場合、
Canvasでマスクをかけることで簡単に実現できる。

![スクリーンショット 2017-03-29 1.46.52.png](https://qiita-image-store.s3.amazonaws.com/0/136286/7f7cd6f1-b608-5e03-66b3-84d30bb5672a.png "スクリーンショット 2017-03-29 1.46.52.png")

上記のようなマスク画像を用いることで、
赤い部分にマスクがかかった状態にする（白いところは透過）

`destination-out`：赤い部分のみが表示されないようになる
`destination-in`：赤い部分のみが表示されるようになる

以下、`destination-out`を適用させた一例

![スクリーンショット 2017-03-29 2.05.06.png](https://qiita-image-store.s3.amazonaws.com/0/136286/7fcd275b-a83b-a86f-869f-53696fbce3c4.png "スクリーンショット 2017-03-29 2.05.06.png")



```javascript:CanvasCarousel.js
class CanvasCarousel {

  constructor(opts={}) {
    // パラメータ
    this.DURATION = isNaN(opts.DURATION) ? 3000 : opts.DURATION;
    this.FADE_DURATION = isNaN(opts.FADE_DURATION) ? 500 : opts.FADE_DURATION;
    this.INTERVAL = isNaN(opts.INTERVAL) ? 50 : opts.INTERVAL; // loop
    this.DELAY = isNaN(opts.DELAY) ? 0 : opts.DELAY;
    this.isSP = opts.isSP || false;
    this.RATIO = this.isSP ? 2 : 1;

    // canvas
    this.$canvas = opts.$canvas;
    this.ctx = this.$canvas.get(0).getContext('2d');
    // 表示する画像
    this.SRC = opts.SRC || [];
    this.images = opts.images || [];
    // マスク
    this.MASK_SRC = opts.MASK_SRC || {};
    this.mask = new Image();
    this.mask.src = this.isSP ? this.MASK_SRC.sp : this.MASK_SRC.pc;
    this.maskPosY = opts.maskPosY || 'bottom'; // 上寄せ or 下寄せ

    this.init();
  }

  init() {
    if (!this.$canvas.length) {
      return;
    }
    this.load();
  }

  load() {
    // load image
    let loadingCount = 0;
    $.each(this.SRC, (index, src) => {
      loadingCount++;
      const img = new Image();
      $(img).on('load', () => {
        this.images[index] = img;
        loadingCount--;
        if (!loadingCount) {
          setTimeout(() => this.startLoop(), this.DELAY)
        }
      });
      img.src = src;
    });
  }

  draw (img, elWidth, elHeight) {
    const imgWidth = img.width;
    const imgHeight = img.height;

    let width, height;
    let x = 0, y = 0;
    // 全画面表示
    if (imgWidth / imgHeight > elWidth / elHeight) {
      height = elHeight;
      width = imgWidth * elHeight / imgHeight;
      x = (elWidth - width) / 2;
    } else {
      width = elWidth;
      height = imgHeight * elWidth / imgWidth;
      y = (elHeight - height) / 2;
    }

    this.ctx.drawImage(img, x, y, width, height);
  }

  startLoop() {
    this.startTime = Date.now();
    this.loop();
  }

  loop() {
    const now = Date.now();
    while (now - this.startTime > this.DURATION) {
      this.startTime += this.DURATION;
      this.images.push(this.images.shift()); // 先頭を最後尾に移動
    }
    const time = now - this.startTime;

    const elWidth = this.$canvas.width() * this.RATIO;
    const elHeight = this.$canvas.height() * this.RATIO;

    this.$canvas.attr({
      width: elWidth,
      height: elHeight
    });

    this.draw(this.images[0], elWidth, elHeight);

    // 次の画像を上に重ねる
    if (time > this.DURATION - this.FADE_DURATION) {
      let fade = (time - (this.DURATION - this.FADE_DURATION)) / this.FADE_DURATION;
      this.ctx.save();
      this.ctx.globalAlpha = fade;
      this.draw(this.images[1], elWidth, elHeight);
      this.ctx.restore();
    }

    if (this.mask.width && this.mask.height) {
      const diff = 2; // 白い線が出ていたので対策
      const maskHeight = this.mask.height * (elWidth / this.mask.width);
      const posY = (this.maskPosY === 'top') ? 0 : elHeight - maskHeight ;
      this.ctx.save();
      // マスクがかかった部分を消す
      this.ctx.globalCompositeOperation = 'destination-out';
      this.ctx.drawImage(this.mask, 0, posY - diff, elWidth, maskHeight);
      this.ctx.restore();
    }

    setTimeout(() => {
      this.loop();
    }, this.INTERVAL);
  }


}


export function initCarousel() {

  const isSP = false; // ここでは仮にPC環境とする
  const DURATION = 3000;

  new CanvasCarousel({
    isSP: isSP,
    $canvas: $('.fv__canvas--top'),
    SRC: [
      './img/img01.jpg',
      './img/img02.jpg',
      './img/img03.jpg',
      './img/img04.jpg',
      './img/img05.jpg'
    ],
    MASK_SRC: {
      sp: './img/mask_top.png',
      pc: './img/mask_top.png'
    },
  })

  new CanvasCarousel({
    isSP: isSP,
    $canvas: $('.fv__canvas--left'),
    SRC: [
      './img/img02.jpg',
      './img/img03.jpg',
      './img/img04.jpg',
      './img/img05.jpg',
      './img/img01.jpg'
    ],
    MASK_SRC: {
      sp: './img/mask_left.png',
      pc: './img/mask_left.png'
    },
    DELAY: DURATION/2,
  })

  new CanvasCarousel({
    isSP: isSP,
    $canvas: $('.fv__canvas--right'),
    SRC: [
      './img/img03.jpg',
      './img/img04.jpg',
      './img/img05.jpg',
      './img/img01.jpg',
      './img/img02.jpg'
    ],
    MASK_SRC: {
      sp: './img/mask_right.png',
      pc: './img/mask_right.png'
    },
    DELAY: DURATION/4,
  })

  new CanvasCarousel({
    isSP: isSP,
    $canvas: $('.fv__canvas--bottom'),
    SRC: [
      './img/img04.jpg',
      './img/img05.jpg',
      './img/img01.jpg',
      './img/img02.jpg',
      './img/img03.jpg'
    ],
    MASK_SRC: {
      sp: './img/mask_bottom.png',
      pc: './img/mask_bottom.png'
    },
    DELAY: DURATION/3,
    maskPosY: 'top',
  })

}
```