ニコニコ動画でよく見える弾幕機能はどうやって実装するかが気になっていました。
ライブラリを使ったら、すぐにできると思いますが、サイトも重くなるリスクもあります。
今回はブログでライブラリとフレームワークを使わず、弾幕を実装できるテクニックを紹介します。
フレームアニメーション
フレームにより、アニメーションを作る論理はゲーム開発でよく使っています。
基本のフレームアニメーションの手順は下記です。
- htmlのcavansのキャッシュをクリアする
- フレームの状態により、フレームを描画する
- 次のフレームの状態更新する
- 1~3を振り返りする
web開発で固定時間で振り返りする方法は二つがあります。今回にrequestAnimationFrameを使います。
実装必要なクラス
ロジックを理解しやすいように、今回実装するクラスの役割を紹介します。
- LineManager: 画面の弾幕行数を設定できる
- Line: 弾幕発射頻度、位置管理できる
-
TextEnemy:弾幕の位置、スピード、サイズ、削除を設定できる
Step1: htmlを設定する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="style.css"> <title>Document</title> </head> <body> <canvas id="canvans_text_animation"></canvas> <script src="script.js"></script> </body> </html> |
Step2: CSSを設定する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#canvans_text_animation { position:absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); background-color: black; max-height: 100%; max-width: 100%; } |
Step3: Javascriptを設定する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
window.addEventListener('load', function(){ const canvas = document.getElementById('canvans_text_animation'); canvas.height= 820 canvas.width= 1800 const ctx = canvas.getContext('2d'); const commands = [ '頑張れ!😀', 'いつもお世話になってありがとうございます。', '意外な結果が............', 'こんにちは〜今日は天気がいいですね!', 'おめでとうございます。', '長い文が失礼しました。長い文が失礼しました。長い文が失礼しました。長い文が失礼しました。長い文が失礼しました。長い文が失礼しました。', '🍑😁🐲🍑😁🐲🍑😁🐲✊✋✊✋🍑🐲✊✋🍑😁🐲✊✋😁🐲🍑😁🐲🍑😁🐲🍑😁🐲🍑😁🐲🍑😁🐲✊✋', '最大何人使いますか?', 'Liveアンケート!Liveアンケート!Liveアンケート!Liveアンケート!Liveアンケート!', '最高!', ] //LineManager実装 class LineManager { constructor(maxLines){ this.lines = []; this.texts = []; for(let i = 0; i<maxLines;i++){ this.lines.push(new Line(i)) } } push(text){ if(text){ this.texts.push(text); } } update(){ if(this.texts.length > 0){ for(let i = 0; i<this.texts.length; i++){ // 使えるLineオブジェクトを取得する const line = this.getPushEnableLine() // 使えるLineオブジェクトにテキストをプッシュする if(line){ line.push(this.texts[i]); this.texts.splice(i,1); i--; } } } for(const line of this.lines){ line.update(); } } draw(context){ for(const line of this.lines){ line.draw(context); } } getPushEnableLine(){ for(const line of this.lines){ if(line.getPushEnable()){ return line; } } return undefined; } } //Lineクラス実装 class Line { constructor(index){ //Lineの行番 this.index = index; //Lineの高さ this.lineHeight = 40; this.textEnemys = []; } push(text){ const textEnemy = new TextEnemy(text,(this.index + 1)*(this.lineHeight + 30), canvas.width,canvas.height) this.textEnemys.push(textEnemy); } update(){ this.textEnemys = this.textEnemys.filter(e=>!e.markForDeletion); for(const textEnemy of this.textEnemys){ textEnemy.update(); } } draw(context){ for(const textEnemy of this.textEnemys){ textEnemy.draw(context); } } getPushEnable(){ if(this.textEnemys.length>0){ return this.textEnemys[this.textEnemys.length-1].hasFinishedShowed(); } return true; } } //弾幕クライス実装する class TextEnemy { constructor(text, y, screenWidth, screenHeight){ //弾幕開始の位置設定する this.x= screenWidth; this.y=y; //長さと高さ、記録するように変数を宣言する this.height=0; this.width=0; //screenのサイズ記録する this.screenWidth = screenWidth; this.screenHeight = screenHeight; //この弾幕スピードを設定する this.speed = -2; //文字サイズ this.font = "28px serif"; //弾幕内容 this.text = text; //削除用フラグ this.markForDeletion = false; //次の弾幕とスペース this.nextSpace = 50; //弾幕色 this.color = 'white'; }; hasFinishedShowed(){ return this.x < this.screenWidth - this.width - this.nextSpace; } update(){ this.x += this.speed if(this.x < -this.width){ this.markForDeletion = true; } } draw(context){ context.save(); context.font = this.font; context.fillStyle = this.color; context.fillText(this.text,this.x,this.y); if(this.text&&this.width===0){ let metrics = context.measureText(this.text); this.width = metrics.width; // let fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; // let actualHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; // console.log(`『${this.text}』のwidthは`,metrics.width) // console.log(`『${this.text}』のheightは`,fontHeight) // console.log(`『${this.text}』のactualHeightは`,actualHeight) } context.restore(); } } //LineManagerのオブジェクトを新規する 最大行数を3を設定する const lineManager = new LineManager(3); let lastTime = 0; //弾幕発射スード let createTextInternal = 500 function animate(timeStamp){ if(timeStamp-lastTime > createTextInternal){ lineManager.push(commands[Math.floor(Math.random() * 11)]); lastTime = timeStamp } ctx.clearRect(0,0,canvas.width,canvas.height); lineManager.update(); lineManager.draw(ctx); requestAnimationFrame(animate); } animate(0); });<br /><br /> |
もっと面白くする
- 弾幕の色をランダムにする
- 最大行列を8列にする
- 発射スードを10倍にする
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
.... class TextEnemy { constructor(text, y, screenWidth, screenHeight){ .... //弾幕色ランダムにする this.color = 'rgb('+Math.random()*255+','+Math.random()*255+','+Math.random()*255+')'; }; .... } .... //LineManagerのオブジェクトを新規する 最大行数を8を設定する const lineManager = new LineManager(8); let lastTime = 0; //弾幕発射スードを10倍にする let createTextInternal = 50 ....<br /><br /> |
いい感じになりますね!笑
皆さんが普段に開発する時に参考になれば、幸いです。
最後まで読んでいただきありがとうございます!