Canvasで赤いLEDが残光を曳くアニメーション

# Canvasで赤いLEDが残光を曳くアニメーション

夏休みに電子工作の気分を味わいたかったので、canvas と JavaScript で電飾を作ってみました。効果音が必要な方は脳内再生で。
Windows10 上の Edge と Chrome と Firefox で動作を確認しています。

# 実行画面

実行するとこんな感じです。

![2000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513949/47a74d9a-40ae-7060-f282-6682cb9dafbe.png)

![3000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513949/78666d1c-f98f-20d5-aca2-79f4c7e5737d.png)


# プログラムの構成

## 単体の赤LEDを表すクラス Lamp

現在の輝度を100段階で保持します。
インスタンスを18個生成して、横一列に並べることとします。

## 点灯パターンを保持するクラス LightingPattern

どのランプをどの輝度で光らせるかを2次元配列で保持します。
動作開始時と動作継続時の点灯パターンを別にするため、配列の添え字をマイナス方向にも伸ばしています。
下記のような感じで６の字型で添え字をカウントアップします。

1. マイナス方向の終端からスタート
2. プラス側の終端までいったら、0に戻る
3. 0とプラス側の終端の間でループ

## Animation オブジェクト

アニメーションの開始、ループ、停止の制御です。
LightingPattern クラスから一定時間ごとに次の点灯パターンを取り出して再描画します。
デバッグ用にコマ送りの機能があります。


#ソース

UTF-8 でローカルに保存してください。

```html:red_led_scanner.html
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>RED LED SCANNER</title>
<script>
'use strict';

let $ = e => document.getElementById(e);

class Lamp {
	constructor() {
		this.width = 15;
		this.height = 25;
		this.x = 0;
		this.y = 0;
		this.brightness = 100;		//0 <= brightness <= 100
	}
	color() {
		const alpha = ((this.brightness == 0) ? 0 : 1);
		return {r:Math.round(255 * this.brightness / 100), g:0, b:0, a:alpha};
		//return {r:Math.round(255 * this.brightness / 100), g:Math.round(255 * this.brightness / 128), b:0, a:alpha};
	}
	drawon(context) {
		const mycolor = this.color();
		context.fillStyle = "rgba(" + mycolor.r + "," + mycolor.g + "," + mycolor.b + "," + mycolor.a + ")";
		context.fillRect(this.x, this.y, this.width, this.height);
	}
}

const LEDArray = {
	vLamp: [],
	length: 18,

	init() {
		const gap = 0;
		let x = 68;
		for (let i = 0; i < this.length; i++) {
			this.vLamp[i] = new Lamp();
			Object.assign(this.vLamp[i], {x:x, y:35, brightness:0});
			x += (this.vLamp[i].width + gap);
		}
	}
}

class LightingPattern {
	constructor() {
		this.pattern = [];
		this.patternLength = 1;
		this.counter = 0;
		this.frequency = 50;	//msec
		this.version = 2000;
	}
	init(version) {
		this.version = version;
		(this.version == 2000) ? this.initPattern_2000() : this.initPattern_3000();
	}
	pattern_off() {
		return [  0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,  0];
	}
	initPattern_2000() {
		this.pattern = [];

		let i = -25;
		this.pattern[i++] = [  0,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  0];
		this.pattern[i++] = [  0,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  6,  0];
		this.pattern[i++] = [  0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,  0];
		this.pattern[i++] = [  0, 18, 18, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,  0];
		this.pattern[i++] = [  0, 12, 12, 18, 18, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 78, 78, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 84, 84, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 84, 84, 90, 90,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 84, 84, 96, 96,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 42, 42, 54, 54, 72, 72, 84, 84, 96, 96,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 30, 30, 42, 42, 54, 54, 72, 72, 96, 96,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 24, 24, 30, 30, 42, 42, 84, 84,100,100,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 24, 24, 24, 24, 30, 30, 76, 76, 96, 96,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 24, 24, 24, 24, 30, 30, 54, 54,100,100, 93, 93,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 24, 24, 30, 30, 30, 30,100,100, 93, 93,  0];

		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18,  0];
		this.pattern[i++] = [  0, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 24, 24,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0,100,100, 76, 76, 66, 66, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0,100,100, 93, 93, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0,100,100, 93, 93, 39, 39, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 93, 93,100,100, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 93, 93,100,100, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 76, 76,100,100, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 76, 76,100,100, 48, 48, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24, 18, 18,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100, 24, 24,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48, 66, 66, 76, 76,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48, 93, 93,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 39, 39, 93, 93,100,100,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48,100,100, 93, 93,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48,100,100, 93, 93,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48,100,100, 93, 93,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48,100,100, 76, 76,  0];
		this.pattern[i++] = [  0, 18, 18, 18, 18, 18, 18, 18, 18, 24, 24, 48, 48,100,100, 76, 76,  0];

		this.patternLength = i;
		this.counter = -23;
		this.frequency = 52;
	}
	initPattern_3000() {
		this.pattern = [];
		this.pattern[0]  = [  6,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6,  6];
		this.pattern[1]  = [  6,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6,  6];
		this.pattern[2]  = [ 42,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6, 42];
		this.pattern[3]  = [ 66,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6, 66];
		this.pattern[4]  = [ 93, 18,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6, 18, 93];
		this.pattern[5]  = [100, 93, 18,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6, 18, 93,100];
		this.pattern[6]  = [ 96,100, 93, 18,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6, 18, 93,100, 96];
		this.pattern[7]  = [ 93, 96,100, 93, 18,  6,  6,  6,  0,  0,  6,  6,  6, 18, 93,100, 96, 93];
		this.pattern[8]  = [ 93, 93, 96,100, 93, 18,  6,  6,  0,  0,  6,  6, 18, 93,100, 96, 93, 93];
		this.pattern[9]  = [ 93, 93, 93, 96,100, 93, 18,  6,  0,  0,  6, 18, 93,100, 96, 93, 93, 93];
		this.pattern[10] = [ 93, 93, 93, 93, 96,100, 93, 18,  0,  0, 18, 93,100, 96, 93, 93, 93, 93];
		this.pattern[11] = [ 96, 96, 96, 96, 96, 96,100, 96,  0,  0, 96,100, 96, 96, 96, 96, 96, 96];
		this.pattern[12] = [100,100,100,100,100,100,100,100,  0,  0,100,100,100,100,100,100,100,100];
		this.pattern[13] = [ 93, 96,100,100,100,100,100,100,  0,  0,100,100,100,100,100,100, 96, 93];
		this.pattern[14] = [ 93, 93, 96,100,100,100,100,100,  0,  0,100,100,100,100,100, 96, 93, 93];
		this.pattern[15] = [ 78, 93, 93, 96,100,100,100,100,  0,  0,100,100,100,100, 96, 93, 93, 78];
		this.pattern[16] = [ 66, 78, 93, 93, 96,100,100,100,  0,  0,100,100,100, 96, 93, 93, 78, 66];
		this.pattern[17] = [ 54, 66, 78, 93, 93, 96,100,100,  0,  0,100,100, 96, 93, 93, 78, 66, 54];
		this.pattern[18] = [ 42, 54, 66, 78, 93, 93, 96,100,  0,  0,100, 96, 93, 93, 78, 66, 54, 42];
		this.pattern[19] = [ 30, 42, 54, 66, 78, 93, 96,100,  0,  0,100, 96, 93, 78, 66, 54, 42, 30];
		this.pattern[20] = [ 18, 30, 42, 54, 66, 78, 93, 96,  0,  0, 96, 93, 78, 66, 54, 42, 30, 18];
		this.pattern[21] = [  6, 18, 30, 42, 54, 66, 78, 93,  0,  0, 93, 78, 66, 54, 42, 30, 18,  6];
		this.pattern[22] = [  6,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6,  6];
		this.pattern[23] = [  6,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6,  6];

		this.pattern[24] = [  6,  6,  6,  6,  6,  6, 42, 18,  0,  0, 18, 42,  6,  6,  6,  6,  6,  6];
		this.pattern[25] = [  6,  6,  6,  6,  6,  6, 66, 42,  0,  0, 42, 66,  6,  6,  6,  6,  6,  6];
		this.pattern[26] = [  6,  6,  6,  6,  6, 18, 93, 78,  0,  0, 78, 93, 18,  6,  6,  6,  6,  6];
		this.pattern[27] = [  6,  6,  6,  6, 18, 93,100, 93,  0,  0, 93,100, 93, 18,  6,  6,  6,  6];
		this.pattern[28] = [  6,  6,  6, 18, 93,100, 96, 93,  0,  0, 93, 96,100, 93, 18,  6,  6,  6];
		this.pattern[29] = [  6,  6, 18, 93,100, 96, 93, 93,  0,  0, 93, 93, 96,100, 93, 18,  6,  6];
		this.pattern[30] = [  6, 18, 93,100, 96, 93, 93, 93,  0,  0, 93, 93, 93, 96,100, 93, 18,  6];
		this.pattern[31] = [ 18, 93,100, 96, 93, 93, 93, 93,  0,  0, 93, 93, 93, 93, 96,100, 93, 18];
		this.pattern[32] = [ 93,100, 96, 93, 93, 93, 93, 93,  0,  0, 93, 93, 93, 93, 93, 96,100, 93];
		this.pattern[33] = [100, 96, 96, 96, 96, 96, 96, 96,  0,  0, 96, 96, 96, 96, 96, 96, 96,100];
		this.pattern[34] = [100,100,100,100,100,100,100,100,  0,  0,100,100,100,100,100,100,100,100];
		this.pattern[35] = [100,100,100,100,100, 96, 93, 78,  0,  0, 78, 93, 96,100,100,100,100,100];
		this.pattern[36] = [100,100,100,100, 96, 93, 93, 78,  0,  0, 78, 93, 93, 96,100,100,100,100];
		this.pattern[36] = [100,100,100, 96, 93, 93, 78, 66,  0,  0, 66, 78, 93, 93, 96,100,100,100];
		this.pattern[37] = [100,100, 96, 93, 93, 78, 66, 54,  0,  0, 54, 66, 78, 93, 93, 96,100,100];
		this.pattern[38] = [100, 96, 93, 93, 78, 66, 54, 42,  0,  0, 42, 54, 66, 78, 93, 93, 96,100];
		this.pattern[39] = [ 96, 93, 93, 78, 66, 54, 42, 30,  0,  0, 30, 42, 54, 66, 78, 93, 93, 96];
		this.pattern[40] = [ 96, 93, 78, 66, 54, 42, 30, 18,  0,  0, 18, 30, 42, 54, 66, 78, 93, 96];
		this.pattern[41] = [ 93, 78, 66, 54, 42, 30, 18,  6,  0,  0,  6, 18, 30, 42, 54, 66, 78, 93];
		this.pattern[42] = [ 78, 66, 54, 42, 30, 18,  6,  6,  0,  0,  6,  6, 18, 30, 42, 54, 66, 78];
		this.pattern[43] = [ 54, 42, 30, 18,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6, 18, 30, 42, 54];
		this.pattern[44] = [  6,  6,  6,  6,  6,  6,  6,  6,  0,  0,  6,  6,  6,  6,  6,  6,  6,  6];

		this.patternLength = this.pattern.length;
		this.counter = 0;
		this.frequency = 60;
	}
	countUP() {
		this.counter = (this.counter < 0) ? (this.counter + 1) : (this.counter + 1) % this.patternLength;
		return this.counter;
	}
	currentPattern() {
		const n = this.countUP();
		return this.pattern[n];
	}
}

const Animation = {
	callbackId: 0,
	context: 0,
	AnimationPattern: new LightingPattern(),
	lastTime: 0,

	loop(timestamp) {
		const nowTime = performance.now(), time = nowTime - this.lastTime;
		if (time > this.AnimationPattern.frequency) {
			this.lastTime = nowTime;
			this.redraw();
		}
		this.callbackId = window.requestAnimationFrame((ts) => this.loop(ts));
	},
	start() {
		if (this.callbackId) { this.stop(); }

		this.AnimationPattern.init(($("form1").rdoVersion.value == 2000) ? 2000 : 3000);

		const canvas = $("scanner");
		this.context.fillStyle = "rgba(17,17,17,1)";
		this.context.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);

		this.lastTime = performance.now();
		window.requestAnimationFrame((ts) => this.loop(ts));
	},
	stop() {
		cancelAnimationFrame(this.callbackId);
		this.callbackId = 0;
	},
	step() {
		const p = this.AnimationPattern.currentPattern();
		for (let i = 0; i < LEDArray.length; i++) {
			Object.assign(LEDArray.vLamp[i], {brightness:p[i]});
			LEDArray.vLamp[i].drawon(this.context);
		}
	},
	redraw() {
		const p = (this.callbackId) ? this.AnimationPattern.currentPattern() 
									: this.AnimationPattern.pattern_off();
		for (let i = 0; i < LEDArray.length; i++) {
			Object.assign(LEDArray.vLamp[i], {brightness:p[i]});
			LEDArray.vLamp[i].drawon(this.context);
		}
	}
}

window.onload = function() {
	$("btnStart").onclick = () => {
		$("btnStart").disabled = true;
		$("btnStep").disabled = true;
		Animation.start();
	}
	$("btnStop").onclick = () => {
		$("btnStart").disabled = false;
		$("btnStep").disabled = false;
		Animation.stop();
	}
	$("btnStep").onclick = () => {
		Animation.step();
	}

	LEDArray.init();

	const canvas = $("scanner");
	if (!canvas.getContext) { return; }
	Animation.context = canvas.getContext("2d");
	Animation.redraw();
}

</script>
</head>
<body>

<form id="form1" name="form1">

赤いLEDが残光を曳くアニメーション

<input type="button" id="btnStart" value="start" />
<input type="button" id="btnStop" value="stop" />
<input type="button" id="btnStep" value="step" disabled style="display:inline-block;" /><br />


点灯パターン
<input type="radio" name="rdoVersion" value="2000" checked />往復
<input type="radio" name="rdoVersion" value="3000" />両側から
<br />

<canvas id="scanner" width="410" height="95" style="background-color:#111111;">
canvas
</canvas>

</form>

</body>
</html>
```