🔖

モダンなJavaScriptを勉強したいので湯婆婆作成に挑戦する

2022/12/14に公開約7,300字

モダンなJavaScriptを勉強する!!

モダンなJavaScriptの勉強にぴったりなネタがないか探しているkaiyuと言います。
久々にプログラミングやってて浦島状態なのです。

先日待望のネタに出会いました

令和のHello Worldに入門する

https://qiita.com/Nemesis/items/c7192a7c510788d2cba2
面白すぎる!!!!
これは作らざるをえない。
しかし何か従来のに手を加えないと記事にする意味もないと考えた。
よってそれっぽくアニメーションしてもらうことにした。

html

まず土台のhtmlを作る

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <script src = "util.js"></script>
        <script src = "canvas.js"></script>
        <style>
            canvas {
                border: 1px solid rgb(0, 0, 0);
                box-sizing: border-box;
            }
        </style>
    </head>
    <body>
        <p>
            契約書だよ。
            <br />
            そこに名前を書きな。
        </p>
        <p>
            <input type="text" id="contractor_name" value="">
            <button onclick="drawContract('canvas', 'contractor_name');">契約する</button>
        </p>
        <canvas id="canvas" width="800" height="800"></canvas>
    </body>
</html>

canvasのサイズは800*800とした


JavaScript

jsの中身に取り組む
方針としては

  1. モダンな書き方を極力する(ES5, ES2015以降)
  2. 入力された名前は縦書きで表示する
  3. 1文字ごとに透過処理,移動処理をほどこす
  4. 文字はランダムに移動して消える

文字クラス

ということでまず文字クラスを作る

canvas.js
class Character {
  constructor(char, isMove, x, y) {
    this.char = char;
    this.isMove = isMove;
    this.x = x;
    this.y = y;

    this.vx = getRandomIntInclusive(1, 9) / 10;
    this.vx *= [-1, 1][getRandomIntInclusive(0, 1)];

    this.vy = getRandomIntInclusive(1, 9) / 10;
    this.vy *= [-1, 1][getRandomIntInclusive(0, 1)];
    
    this.opacity = 1.0;
    this.fontSize = 32;
  }

  drawContractorNameV = (ctx) => {
    if (this.isMove) {
      if (this.fontSize > 0) {
        this.opacity = mathRound2(this.opacity - 0.02, 2);
        this.x = mathRound2(this.x + this.vx, 1);
        this.y = mathRound2(this.y + this.vy, 1);
        this.fontSize = mathRound2(this.fontSize - 0.1, 2);
      }
    }
    ctx.font = `${this.fontSize}pt sans-serif`;
    ctx.fillStyle = 'rgba(0, 0, 0, ' + this.opacity + ')';
    ctx.fillText(this.char, this.x, this.y);
  };
}

getRandomIntInclusiveなるものは別途util.jsを作成して
その中にMDNのコード(下記ページ参照)をそのままコピペしたものを使用している
min maxを引数にminからmaxまでを含むランダムな整数値が返ってくる
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random


this.vx,this.vy は文字の移動速度としている。(小数点下1桁)
この移動速度もランダムにしてみた
ただプラスとマイナスもランダムにしたかったのだが、うまいやり方がよくわからず
[-1, 1]の配列を用意してやった。ちょっとブサイクかもしれない


drawContractorNameVはアロー関数にした
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions
drawContractorNameVの中身はマジックナンバでゴリゴリ書いていて申し訳ない
JavaScriptには桁を指定した四捨五入がないそうなので
望みの桁数になるように10のn乗をかけて10のn乗で割る。ということをmathRound2で行っている
こちらはユーティリティとして自作した
べき乗演算子を使用している。これはES2016で追加されたようだ。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Exponentiation

util.js
function mathRound2(num, specifiedDigit) {
  return Math.round(num * (10 ** specifiedDigit)) / (10 ** specifiedDigit);
}

ctx.font = `${this.fontSize}pt sans-serif`;のところは
テンプレートリテラルを使用している。ES2015で追加
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals


というかモダンなJavaScriptの書き方の勉強中なのでこんなclass作るのって
もしかして"らしく"なかったりするのかもしれないが作っちゃったのでこのままいくとする
一応ES2015で追加された内容なのでヨシッ!!
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes


契約書の文言と名前の縦書き

そしてメインの文字のアニメーションを含む関数を作る

canvas.js
//キャンバスに文字を描く
const drawContract = (canvas_id, text_id) => {
  let canvas = document.getElementById(canvas_id);
  let ctx = canvas.getContext("2d");
  let contractorName = document
    .getElementById(text_id)
    .value.replaceAll(/\s+/g, "");

  //縦書き表示のため入力された名前を1文字ずつに分解する
  let contractorNameV = [...contractorName].map(
    (char, index) => new Character(char, true, 400, 300 + 45 * index)
  );

  //お前の名前は1文字になるんだよ!
  let stolenName = contractorNameV[getRandomInt(0, contractorName.length)];
  stolenName.isMove = false;

  const drawContractorNameV = () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.font = "24pt serif";
    ctx.fillStyle = "rgba(0, 0, 0, 1)";
    ctx.fillText(`フン。「${contractorName}」というのかい。`, 100, 50 + 40 * 0);
    ctx.fillText(`贅沢な名だねえ。`, 100, 50 + 40 * 1);
    ctx.fillText(
      `今からお前の名前は「${stolenName.char}」だ`,
      100,
      50 + 40 * 2
    );
    ctx.fillText(`いいかい、「${stolenName.char}」だよ`, 100, 50 + 40 * 3);
    ctx.fillText(
      `わかったら返事をするんだ、「${stolenName.char}」!!`,
      100,
      50 + 40 * 4
    );

    contractorNameV.map((element) => {
      element.drawContractorNameV(ctx);
    });
    
    //ここにアニメーション処理(後述)
}

入力された文字は空白を除去しておいた。


縦書き表示はArray.map()を記述
MDN眺めながら書いたがスタイリッシュですね
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map

またスプレッド構文を使用。こちらはES2016で追加
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax
めっちゃ便利でいい


new Character()のところはとりあえず800*800のcanvasサイズの真ん中らへんになるように
x座標を指定して、Y座標はフォントサイズが32なので余裕をもって40ぐらい開けて指定している。なんだか疲れたのでこのままである。
またisMoveはとりあえず配列全ての要素をtrueで初期化しておいて
直後にランダムな1文字を取得してその1文字だけをfalseに指定する方法をとった
getRandomIntは先述のMDN参照 (minとmaxを引数にとりminからmaxを含まない範囲でのランダムな整数値を返す)


ctx.clearRect(0, 0, canvas.width, canvas.height);
この一文はcanvasアニメーションをさせるときの定番
canvasの画面クリア→描画→canvasの画面クリア→描画→(以下続く)
という処理でいわゆるパラパラ漫画の理屈でアニメーションをさせている


あとは湯婆婆のセリフを強引に表示!!
ここらへん面倒になってきておりマジックナンバハードコーディング!!!

湯婆婆のセリフ自体はアニメーションしなくても結果として毎フレームごとに
画面がクリアされているのでここの描画処理は毎フレームごとに必要となる


文字が消えるアニメーション

後はアニメーションをさせる。
アニメーションを止める処理も含める

canvas.js
    let id = window.requestAnimationFrame(drawContractorNameV);
    let isAnimeStop = false;
    for( element of contractorNameV ){
        if( element.isMove ){
            if(element.fontSize < 0){
                isAnimeStop = true;
            }
        }
    }
    if(isAnimeStop){
        console.log("isAnimeStop");
        cancelAnimationFrame(id);
    }
    
  }
  drawContractorNameV();

for...ofを使用!シンプルに書けてとてもいい。(ES2015で追加)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for...of


fontSizeが0になったら画面上で表示されなくなったということで
ここでアニメーションを停止させる。
透過度が0(element.opacity=0)でもよいとは思うのだが、
どうにもアニメーションが不安定になり
原因がつかめないのでfontSizeで判定させている。


アニメーションは前はsetInterval()を使用していたが
今回はrequestAnimationFrame()を使用した。html5から導入されたようだ
https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame

ただ指定した時間ごとに実行してほしい。という要望のときは
従来通りsetInterval()を使用するという使い分けが必要みたいだ。
MDNでも

、ディスプレイのリフレッシュレートに合わせて行われます。
コールバックの確率は、バックグラウンドのタブや隠れた <iframe> では、パフォーマンス向上>やバッテリー消費を減らすために低くなるでしょう。

とある。


アニメーションを止める箇所がもっとうまい書き方があるのだろうが
どうにも1フレーム描画が残ったり、アニメーションが止まらなかったりで上記の書き方になった


完成した湯婆婆

これで好きなだけ契約できるようになった!!!


参考

  • 偉大なる先駆者(再掲)

https://qiita.com/Nemesis/items/c7192a7c510788d2cba2

  • アニメーションを参考にさせていただいた

https://qiita.com/wakwak/items/b00372ab0eddeeac3e4c

  • 自分の知らなかった言語や実装方法などが知れたランキング記事

https://qiita.com/torifukukaiou/items/c8361231cdc56e493245

感想

  • モダンな書き方みんなスタイリッシュでかっこいい!
  • canvas上で文字をアニメーションってムズイ
  • 画像化して個別に加工すればもっと"ふわあ"っとしたアニメーションができたのだろうが思いつかなかった
  • 関数型言語やりたい
  • 実は映画見たことない

クラッシュ湯婆婆

クラッシュしません。すみません。
そのかわり作成途中の画面で許してください

くお〜!!! undefined〜!!! 湯婆婆を右に!!!

Discussion

ログインするとコメントできます