5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LIFFで描画(canvas)して、画像をLINEへ送信する。「お絵描きbot」

Last updated at Posted at 2024-02-20

LIFFで描画(canvas)して、画像をLINEへ送信する方法は、色々なHPに公開されていますが、何れを試してもうまくいきませんでした(自身のスキルが足りないので理解できないことが多いため)。
ChatGPTに質問を投げかけたところ、それなりのコードを教えてくれたものの、上手く動かず。しかし、試行錯誤している中で、やっと成功したので記事にします。

動作の仕組み

LIFF canvas 動作のコピー.png

canvasで描画したものは、直接LINEへは送信できない。一度サーバーにアップロードし、そこからダウンロードしLINEへ送信する方法が必要。
今回はImgBBというう画像共有サービスをサーバーとして利用。

動作の様子

20240220_2320006849.gif

作成の流れ

1. ImgBBのアカウント作成
2. LINEbot作成、LIFFを作成
3. 以下のコードへ差し替える
4. LIFFへ登録、リッチメニューに登録

実際の作成

1. ImgBBのアカウント作成

① サインイン(またはアカウント作成)

https://zenn.dev/protoout/articles/74-imgbb-setup
https://tcd-theme.com/2021/03/imgbb.html
などを参考に、「サインイン」、新規登録の方は「アカウント作成」を行う。
ImgBB アカウント作成、サインイン.png

② ImgBB APIキー発行

http://htfx.blog.fc2.com/blog-entry-625.html
などを参考に、
 左上の「詳細」をクリックしタグを表示。タグ内の「API」をクリック。
 上の方にある「Get API key」をクリックすると、API keyが発行される。

ImgBB API手順01.png

ImgBB API手順02.png

2. LINEbot作成、LIFFを作成

 仕組みとしては、LINEbotチャネル、LIFFチャネル をそれぞれ作成し、LINEbot上でLIFFのURLを読み込むことでLIFFを表示させます。

①LINEbotの作成
 LINEbotチャネルを作成

②LIFFの作成
 A. LIFFスターターアプリを作成
  動かしたいLIFFの中身を作成(Visual Studio Code)
 B. LIFFへの登録
  LIFFチャネルを作成し、中身(A.で作成しNetlifyへデプロイしたURL)を登録

① LINEbot作成

 LINE Developersサイトで、新規LINEbotチャネルを作成。
https://www.yukibnb.com/entry/linemessagingapi_account
などを参照。

 LINE Botを友だち追加する
https://qiita.com/21HideK/items/4005559995ac2f270528
の5.①などを参照。

② LIFFの作成

A. LIFFスターターアプリを作成

LINE Developers公式サイト 「LIFFスターターアプリを試してみる」
https://developers.line.biz/ja/docs/liff/trying-liff-app/#how-to-start-liff-starter-app
の通りに、LIFFを作成。
Netlifyへのデプロイまで。
私は、Visual Studio Code を使って作成しています。

ターミナル
git clone https://github.com/line/line-liff-v2-starter.git
cd line-liff-v2-starter/src/vanilla
yarn install
npm install -g netlify-cli
netlify login
yarn build
cd ../../
netlify deploy

netlify deploy --prod

B. LIFFへの登録

 LINE DevelopersサイトでLIFFを作成。
https://ayaoriko.com/coding/line/liff-app/
などを参照。

point!
●権限(scope)の profile、openid、chat_message.write はすべてチェックする。
●サイズは Full を選択。
●エンドポイントURLに上記 ② A. で作成したアプリのURLを記載する。
●LIFF ID と LIFF URLを控えておく。

LIFFチャネル作成手順01.png

LIFFチャネル作成手順03.png

LIFFチャネル作成手順04-1.png

LIFFチャネル作成手順04-2.png

LIFFチャネル作成手順04.png

LIFFチャネル作成手順05.png

3. 以下のコードへ差し替える

① 作成したLIFFスターターアプリ内のコードを

それぞれ(CSS, HTML, JS)、以下のコードに丸ごと書き換える。

YOUR_LIFF_ID(2.②B.でコピーしたもの)
YOUR_IMGBB_API_KEY(1.②で発行したもの)
は、ご自分のものへ差し換えてください。

canvasへの描画方法は、
https://tonamao.hatenablog.com/entry/2019/03/03/180739
を参照し作成しました。必要に応じてカスタマイズしてください。
(カスタマイズしたコード、機能の追加は、後日、下記に追記しました。)

index.css
index.css
* {
  box-sizing: border-box;
}

body{
  background: #CCCCCC;
  text-align: center;
  font-family: 'Courier New', sans-serif;
}

#board {
    padding: 0.2em 0.5em;
    margin: 2em auto;
    width: 320px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}

.title{
    padding: 0.2em 0.5em;
    margin: 2em auto;
    width: 300px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}
.title p {
    user-select: none;
  color: #636e72;
    margin: 0; 
    padding: 4px;
}

canvas{
  cursor: pointer;
  background: white;
  border-radius: 16px;
}

.tools{
  margin: 0px;
  background: orange;
  text-align: center;
}

.btn-circle-flat_black {
 display: inline-block;
 text-decoration: none;
 /*background: #87befd;*/
 background: #ffeaa7;
 width: 40px;
 height: 40px;
 line-height: 40px;
 border-radius: 50%;
 text-align: center;
 margin-right: 12px;
 margin-left: 12px;
 margin-top: 4px;
 margin-bottom: 4px;
 vertical-align: middle;
 overflow: hidden;
 transition: .4s;
}

.btn-circle-flat_black i {
 /*ボタン自体*/
 vertical-align: middle;
 color: #272828;
 top: 9px;
 border-radius: 50%;
 font-size: 24px;
}

.btn-circle-flat_red {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .btn-circle-flat_red i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ec313e;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .btn-circle-flat_blue {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .btn-circle-flat_blue i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #0b3fcf;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .btn-circle-flat_eraser {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .btn-circle-flat_eraser i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .btn-circle-flat_clear {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .btn-circle-flat_clear i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

  .button {
  width:100px;
  height:50px;
  font-family:Impact;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(245, 227, 123);
}

.button:hover{
opacity:0.8;
}
index.html
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>おえかきボード</title>
    <script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
</head>

<body>
    <div class="title">
      <p>おえかきボード</p>
    </div>
    <div id="board">
      <canvas id="canvas" width="300px" height="300px"></canvas>
    </div>
    <div id="palette">
      <a href="#" id="pen_black" class="btn-circle-flat_black" id="onoff">
        <i class="fas fa-paint-brush fa-3x"></i>
      </a>
      <a href="#" id="pen_red" class="btn-circle-flat_red" id="onoff">
        <i class="fas fa-paint-brush fa-3x"></i>
      </a>
      <a href="#" id="pen_blue" class="btn-circle-flat_blue" id="onoff">
        <i class="fas fa-paint-brush fa-3x"></i>
      </a>
      <a href="#" id="eraser" class="btn-circle-flat_eraser" id="onoff">
        <i class="fas fa-eraser fa-3x"></i>
      </a>
      <a href="#" id="clear" class="btn-circle-flat_clear" id="onoff">
        <i class="fas fa-undo-alt fa-3x"></i>
      </a><br><br>
    
    <button class="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
    
    <script>
    let canvas = document.getElementById('canvas');
    let ctx = canvas.getContext('2d');
    let drawing = false;

    window.onload = initializeLIFF;

    function initializeLIFF() {
        liff.init({
            liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
        })
        .then(() => {
            console.log("LIFF initialization successful");
        })
        .catch((err) => {
            console.error("LIFF initialization failed", err);
        });
    }

    function uploadAndSendToLINE() {
        // LIFFが初期化されているか確認
        if (!liff.isInClient()) {
            alert('This app must be viewed in the LINE app.');
            return;
        }

        // Canvasから画像データを取得
        var imageDataURL = canvas.toDataURL();
        // 画像データをBase64に変換
        var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

        // 画像をImgBBにアップロード
        fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
            method: 'POST',
            body: new URLSearchParams({ image: base64Image }),
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
        .then(response => response.json())
        .then(data => {
            // アップロード成功時にLINEに送信
            sendImageMessage(data.data.url);
        })
        .catch(error => {
            console.error('Error uploading image', error);
        });
    }

    function sendImageMessage(imageUrl) {
        // 画像のURLをLINEに送信
        liff.sendMessages([
            {
                type: 'image',
                originalContentUrl: imageUrl,
                previewImageUrl: imageUrl
            }
        ])
        .then(() => {
          liff.closeWindow();
        })
        .catch((err) => {
            console.error('Error sending image', err);
        });
    }
</script>
</body>
</html>
index.js
index.js
import './index.css';
import liff from '@line/liff'

liff.init({
    liffId: 'YOUR_LIFF_ID', // あなたのLIFFアプリのIDに置き換えてください
});

document.addEventListener("DOMContentLoaded", function() {
  liff
    .init({ liffId: process.env.LIFF_ID })
    .then(() => {
        console.log("Success! you can do something with LIFF API here.")
    })
    .catch((error) => {
        console.log(error)
    })
});

//背景を白に設定する。
document.addEventListener('DOMContentLoaded',()=> {
    // HTMLが読み込まれた時点で実行したい処理を記述する!
    var cnv = document.getElementById('canvas');
    var ctx = cnv.getContext('2d');
    ctx.beginPath();
    ctx.fillStyle = 'white';
    ctx.globalAlpha = 1.0;
    ctx.fillRect(0, 0, 500, 500);
    });


(function(){    

    var pen_black = document.getElementById('pen_black');
    var pen_red = document.getElementById('pen_red');
    var pen_blue = document.getElementById('pen_blue');
    var eraser = document.getElementById('eraser');
    var clear = document.getElementById('clear');
    
    var cnv = document.getElementById('canvas');
    var ctx = cnv.getContext('2d');
    
    var clickFlg = 0;
    var penMode = 0;//0->pen, 1->eraser
    
    cnv.addEventListener('mousedown', onClick, false);
    cnv.addEventListener('mousemove', draw, false);
    cnv.addEventListener('mouseup', mouseUp, false);
    cnv.addEventListener('mouseout', mouseUp, false);
    
    cnv.addEventListener('touchstart', onTouch, false);
    cnv.addEventListener('touchmove', drawByTouch, false);
    cnv.addEventListener('touchend', touchUp, false);
    
    pen_black.addEventListener('click', pen_blackClick, false);
    pen_red.addEventListener('click', pen_redClick, false);
    pen_blue.addEventListener('click', pen_blueClick, false);
    eraser.addEventListener('mousedown', erase, false);
    clear.addEventListener('mousedown', clearCanvas, false);
    
    function onClick(e){
      clickFlg = 1;
      ctx.beginPath();
      ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
    }

    
    function draw(e){ 
      if(clickFlg == 1){
        
        var rect = e.target.getBoundingClientRect();
        var x = e.clientX - rect.left;
        var y = e.clientY - rect.top;
  
        ctx.lineTo(x, y);
        if(penMode == 0){
            ctx.strokeStyle = '#2d3436';
            ctx.lineWidth = 5;
          }else if(penMode == 1){
            ctx.strokeStyle = 'red';
            ctx.lineWidth = 5;
          }else if(penMode == 2){
            ctx.strokeStyle = 'blue';
            ctx.lineWidth = 5;
          }else if(penMode == 3){
              ctx.strokeStyle = 'white';
              ctx.lineWidth = 20;
            }
        ctx.stroke();
      };
    }
    
    function mouseUp(){
      clickFlg = 0;
    }
    

    function onTouch(e){
      clickFlg = 1;
      ctx.beginPath();
      ctx.moveTo(e.changedTouches[0].pageX - rect.left, e.changedTouches[0].pageY - rect.top);
      
    }
    
    function drawByTouch(e){
      event.preventDefault();
      
      if(clickFlg == 1){
        var rect = e.target.getBoundingClientRect();
        var x = e.changedTouches[0].pageX - rect.left;
        var y = e.changedTouches[0].pageY - rect.top;
  
        ctx.lineTo(x, y);
        ctx.lineWidth = 5;
        if(penMode == 0){
            ctx.strokeStyle = '#2d3436';
            ctx.lineWidth = 5;
          }else if(penMode == 1){
            ctx.strokeStyle = 'red';
            ctx.lineWidth = 5;
          }else if(penMode == 2){
            ctx.strokeStyle = 'blue';
            ctx.lineWidth = 5;
          }else if(penMode == 3){
              ctx.strokeStyle = 'white';
              ctx.lineWidth = 20;
            }
        ctx.stroke();
      };
    }
    
    function touchUp(){
      clickFlg = 0;
    }

    function pen_blackClick(){
        penMode = 0;
    }

    function pen_redClick(){
        penMode = 1;
    }

    function pen_blueClick(){
        penMode = 2;
    }
      
    function erase(){
        penMode = 3;
    }
    
    function clearCanvas(){
      ctx.beginPath();
      ctx.fillStyle = 'white';
      ctx.globalAlpha = 1.0;
      ctx.fillRect(0, 0, 500, 500);
      
      penMode = 0;
    }      
  })();

② 再度、Netlifyへデプロイ

LINE公式サイト
https://developers.line.biz/ja/docs/liff/trying-liff-app/#how-to-start-liff-starter-app
の通りに、再度デプロイ。

ターミナル
netlify build
netlify deploy --prod

4. LIFFへ登録、リッチメニューに登録

https://qiita.com/21HideK/items/4005559995ac2f270528
の5.②を参考に、作成。

LIFF URL(2.②B.でコピーしたもの)を登録し、LINEbotのリッチメニューからLIFFを開けるようにする。

以上の工程で、はじめの動画の様なお絵描きLINEbotが完成したと思います。

コードの改変

(2024.02.22追記、2024.02.27改変)
 上記コードは問題なく動くと思いますが、色を増やすことが難しいこと、線の太さが変えられない とカスタマイズが難しいと分かりました。
 カスタマイズ可能なコードを作成し、メモ代わりに使えるように、以下の様に色々実装してみました。

  • 色の種類を増加
  • 太さを選択
  • 背景色の変更
  • 背景色の有無
  • 写真の読み込み

 また、ブラウザーで使用する方様に、ダウンロードボタンもコメントアウトに用意しました。

 以下のようなイメージになります。
Screenshot_20240227_181109_LINE.jpg

index.css
index.css
.color > a{
  display: inline-block;
  width: 40px;
  height: 40px;
}

* {
  box-sizing: border-box;
}

body{
  overflow: hidden; /* スクロールを無効にする */
  /* margin: 0; /* マージンをゼロにする */
  background: #CCCCCC;
  text-align: center;
  font-family: 'Courier New', sans-serif;
}

#board {
    padding: 0.2em 0.5em;
    margin: 2em auto;
    width: 320px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}

.title{
    padding: 0.0em 0.5em;
    margin: 0.5em auto;
    width: 300px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}
.title p {
    user-select: none;
  color: #636e72;
    margin: 0; 
    padding: 2px;
}

canvas{
  cursor: pointer;
  background: white;
  border-radius: 16px;
}

.tools{
  margin: 0px;
  background: orange;
  text-align: center;
}

.black {
 display: inline-block;
 text-decoration: none;
 /*background: #87befd;*/
 background: #ffeaa7;
 width: 40px;
 height: 40px;
 line-height: 40px;
 border-radius: 50%;
 text-align: center;
 margin-right: 4px;
 margin-left: 4px;
 margin-top: 0px;
 margin-bottom: 0px;
 vertical-align: middle;
 overflow: hidden;
 transition: .4s;
}

.black i {
 /*ボタン自体*/
 vertical-align: middle;
 color: #272828;
 top: 9px;
 border-radius: 50%;
 font-size: 24px;
}

.red {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .red i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ec313e;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .blue {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .blue i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #0b3fcf;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .yellow {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .yellow i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #bec103;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .green {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .green i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #1faa00;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .white {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .white i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ffffff;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .eraser {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .eraser i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .clear {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .clear i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

/* 色や太さを選択する部分のCSS */
.bold {
  display: flex;
  justify-content: center;
}

.bold a {width:84px; height:35px; margin:auto;list-style-type: none; border:1px solid #030d7d; border-radius:6px; margin:8px; display:block; float:left;}
#small {text-align:center; line-height:40px; font-size:100%;}
#middle {text-align:center; line-height:40px; font-size:125%;}
#large {text-align:center; line-height:40px; font-size:150%;}

.bgColor{
  margin-bottom: 4px;
}

.setting{
  display: flex;
  justify-content: center;
  margin-bottom: 8px;
}

.send{
display: flex;
justify-content: center;
}

.button {
  width:100px;
  height:50px;
  font-family:Impact;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(245, 227, 123);
}

.button:hover{
opacity:0.8;
}

.clearBgColor{
  width:80px;
  height:25px;
  margin-right: 5px;
  font-family:Impact;
  font-size:80%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(244, 173, 209);
}

.file{
  width:100px;
  height:30px;
  margin-bottom: 4px;
  font-family:Impact;
  font-size:70%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(123, 245, 227);
}

.fileinput {
  display: none;
}
index.html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>お絵描き</title>
      <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  </head>

  <body>
        <div class="title">
          <p>おえかきボード</p>
        </div>

        <div id="board">
          <canvas id="canvas" width="300px" height="300px"></canvas>
        </div>

        <div class="option">
          <div class="color">

            <a href="#" class="black" data-color="0, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="red" data-color="255, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="blue" data-color="0, 0, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="yellow" data-color="255, 255, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="green" data-color="0, 128, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="white" data-color="255, 255, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" id="eraser" class="eraser">
              <i class="fas fa-eraser fa-3x"></i>
            </a>
            <a href="#" id="clear" class="clear">
              <i class="fas fa-undo-alt fa-3x"></i>
            </a>
            <div id="bold" class="bold">
              <a href="#" id="small" class="small" data-bold="1">ほそい</a>
              <a href="#" id="middle" class="middle" data-bold="5">ふつう</a>
              <a href="#" id="large" class="large" data-bold="10">ふとい</a>
            </div>
          </div>
        </div>

        <div class="setting">
          <div class="bgColor">背景色:
            <input type="color" id="bgColorPicker" value="#ffffff">
            <button id="clearBgColor" class="clearBgColor">背景色無し</button>
          </div>
          <div>
            <!-- ファイル選択ボタン -->
            <button id="file" class="file">写真を読み込む</button>
            <input type="file" id="fileinput" class="fileinput" accept="image/*" onchange="handleFileSelect(this)" />
          </div>
        </div>

        <div class="send">
        <!-- このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
          このHTMLと下のJSのコメントアウトを外してください。
          <button id="download">download</button>
        -->
          <button class="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
        </div>


      <script>
        window.onload = initializeLIFF;

        function initializeLIFF() {
          liff.init({
              liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
          })
          .then(() => {
              console.log("LIFF initialization successful");
          })
          .catch((err) => {
              console.error("LIFF initialization failed", err);
          });
        }

          // canvas
          var canvas = document.getElementById('canvas');
          var ctx = canvas.getContext('2d');
      
          // 変数宣言
          const cnvWidth = 500;
          const cnvHeight = 500;
          var cnvColor = "0, 0, 0, 1";  // 線の色
          var cnvBold = 5;  // 線の太さ
          var drawing = false; // 描画中フラグ
          var bgColor = "#ffffff"; //開いた時の最初の背景色(白)
      
          // canvasの背景色を設定(指定がない場合にjpeg保存すると背景が黒になる)
          setBgColor();
      
          // canvas上でのイベント
          canvas.addEventListener('mousedown', startDrawing);
          canvas.addEventListener('mouseup', stopDrawing);
          canvas.addEventListener('mousemove', draw);
          canvas.addEventListener('touchstart', startDrawing);
          canvas.addEventListener('touchend', stopDrawing);
          canvas.addEventListener('touchmove', draw);
      
          // 描画処理
          function draw(e) {
            if (!drawing) return;
            var x, y;
            if (e.type.startsWith('touch')) {
              x = e.touches[0].clientX - canvas.offsetLeft;
              y = e.touches[0].clientY - canvas.offsetTop;
            } else {
              x = e.offsetX;
              y = e.offsetY;
            }
            ctx.lineWidth = cnvBold;
            ctx.strokeStyle = 'rgba(' + cnvColor + ')';
            ctx.lineTo(x, y);
            ctx.stroke();
          }
      
          // 描画開始
          function startDrawing(e) {
            drawing = true;
            var x, y;
            if (e.type.startsWith('touch')) {
              x = e.touches[0].clientX - canvas.offsetLeft;
              y = e.touches[0].clientY - canvas.offsetTop;
            } else {
              x = e.offsetX;
              y = e.offsetY;
            }
            ctx.beginPath();
            ctx.lineCap = "round";
            ctx.moveTo(x, y);
          }
      
          // 描画終了
          function stopDrawing() {
            drawing = false;
          }
      
          // 色の変更
          $(".color a").click(function () {
            cnvColor = $(this).data("color");
            ctx.globalCompositeOperation = 'source-over';
            return false;
          });
      
          // 線の太さ変更
          $(".bold a").click(function () {
            cnvBold = $(this).data("bold");
            return false;
          });

          // 消しゴム
          $("#eraser").click(function () {
            ctx.globalCompositeOperation = 'destination-out';
          });

          // 描画クリア
          $("#clear").click(function () {
            ctx.clearRect(0, 0, cnvWidth, cnvHeight);
            setBgColor();
          });

          // 背景色の変更
          $("#bgColorPicker").change(function() {
            ctx.globalCompositeOperation = 'source-over';
            bgColor = $(this).val();
            setBgColor();
          });

          // 背景色を無しにする
          $("#clearBgColor").click(function() {
            ctx.globalCompositeOperation = 'destination-out';
            setBgColor();
          });

      /*
       このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
       このJSと上のHTMLのコメントアウトを外してください。

          // canvasを画像で保存
          document.getElementById("download").onclick = (event) => {
            let canvas = document.getElementById("canvas");

            let link = document.createElement("a");
            link.href = canvas.toDataURL("image/png");
            link.download = "canvas.png";
            link.click();
          }
      */
          function setBgColor() {
            ctx.fillStyle = bgColor;
            ctx.fillRect(0, 0, cnvWidth, cnvHeight);
          }

          function uploadAndSendToLINE() {
              // LIFFが初期化されているか確認
              if (!liff.isInClient()) {
                  alert('This app must be viewed in the LINE app.');
                  return;
              }

              // Canvasから画像データを取得
              var imageDataURL = canvas.toDataURL();
              // 画像データをBase64に変換
              var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

              // 画像をImgBBにアップロード
              // YOUR_IMGBB_API_KEY をあなたのImgBB APIに置き換えてください
              fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
                  method: 'POST',
                  body: new URLSearchParams({ image: base64Image }),
                  headers: {
                      'Content-Type': 'application/x-www-form-urlencoded'
                  }
              })
              .then(response => response.json())
              .then(data => {
                  // アップロード成功時にLINEに送信
                  sendImageMessage(data.data.url);
              })
              .catch(error => {
                  console.error('Error uploading image', error);
              });
          }

          function sendImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                liff.closeWindow();
              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

        //id="file"のボタンを押したときに、
        //input type="file" id="fileinput" をクリックしたことと
        //同じ動作にする設定
        //input type="file" id="fileinput"は、CSSで表示させないように設定
        document.querySelector("#file").addEventListener("click", () => {
          document.querySelector("#fileinput").click();
        });

        //デバイス(スマホやPC)から写真をcanvasに読み込む
        //写真を、縦横比を維持し、
        //縦長の写真ならば高さを300px、横長の写真ならば幅を300px、
        //正方形ならば高さ300px、幅300pxの大きさに変えて、canvasに読み込ませる
        // ファイル選択時の処理
        function handleFileSelect(input) {
          const file = input.files[0];
          if (file) {
            const reader = new FileReader();

            reader.onload = function (e) {
              const image = new Image();
              image.src = e.target.result;

              image.onload = function () {
                // 新しいCanvasを作成し、指定したサイズに変更
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                if (image.width > image.height) {
                  // 横長の場合
                  canvas.width = 300;
                  canvas.height = (300 / image.width) * image.height;
                } else if (image.width < image.height) {
                  // 縦長の場合
                  canvas.width = (300 / image.height) * image.width;
                  canvas.height = 300;
                } else {
                  // 正方形の場合
                  canvas.width = 300;
                  canvas.height = 300;
                }

                // 画像を新しいCanvasに描画
                ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

                // 元のCanvasを取得し、新しいCanvasの内容を元のCanvasに描画
                const originalCanvas = document.getElementById('canvas');
                const originalCtx = originalCanvas.getContext('2d');
                originalCanvas.width = canvas.width;
                originalCanvas.height = canvas.height;
                originalCtx.drawImage(canvas, 0, 0);

                // 新しいCanvasは不要なので削除
                document.body.removeChild(canvas);
              };
            };

          reader.readAsDataURL(file);
        }
      }
      </script>
  </body>
</html>

index.html
index.js
import './index.css';
import liff from '@line/liff'

document.addEventListener("DOMContentLoaded", function() {
  liff
    .init({ liffId: process.env.LIFF_ID })
    .then(() => {
        console.log("Success! you can do something with LIFF API here.")
    })
    .catch((error) => {
        console.log(error)
    })
});

(2024.02.22追記ここまで)

さらに機能追加

(2024.2.28追加)
 自分のLINEでも使いたいというご要望があり、
「おえかき」とメッセージを送信すると、「下のURLをコピーして、ご自身のLINEへ送信してお使いください。」、「LIFFのURL」の2つのメッセージを返信する機能をLINEbotへ実装するGoogle Apps Script(GAS)のコードを作りました。
(といっても、ChatGPTに「あなたはLINE開発者です。「おえかき」とメッセージを送信すると、LIFFのURL、「上記URLをコピーしてご自分のLINEで送信してお使いください」という2つのメッセージを返信するLINEbotを動かすプログラムを、GASのコードで書いて下さい。」と入力し作って、あっという間にもらっただけですが)

 LINEbotへGASを実装する方法は、
下記事内で、画像付きでとても詳細に紹介されていますので参照しながら作成してください。
【シリーズ第1話】Google Driveに画像を自動保存するLINE BotをGASで作ろう (準備編)
https://www.yukibnb.com/entry/linemessagingapi_save_image
【シリーズ第2話】Google Driveに画像を自動保存するLINE BotをGASで作ろう (実践編)
https://www.yukibnb.com/entry/linemessagingapi_gas_save_image

ついでに、リッチメニューを増やして、押すと「おえかき」と送信するボタンを作っても良いかと思います。

 以下のようなイメージになります。
Screenshot_20240228_112858_LINE.jpg

Google Apps Script(GAS)
Google Apps Script(GAS)
// LINE Messaging APIのチャンネルアクセストークンとChannel Secretを設定
const CHANNEL_ACCESS_TOKEN = 'YOUR_CHANNEL_ACCESS_TOKEN';
const CHANNEL_SECRET = 'YOUR_CHANNEL_SECRET';

// LINE Messaging APIのエンドポイント
const LINE_API_ENDPOINT = 'https://api.line.me/v2/bot/message/reply';

// イベントハンドラー
function onMessage(event) {
  const replyToken = event.replyToken;
  const messageText = event.message.text;

  // "おえかき"というメッセージが送信された場合の処理
  if (messageText === 'おえかき') {
    const liffUrl = 'YOUR_LIFF_URL'; // おえかきLIFFのURLに置き換える
    const replyMessages = [
      { type: 'text', text: '下のURLをコピーして、ご自身のLINEへ送信してお使いください。' },
      { type: 'text', text: liffUrl }
    ];

    // LINEにメッセージを返信
    replyToLINE(replyToken, replyMessages);
  }
}

// LINEにメッセージを返信する関数
function replyToLINE(replyToken, messages) {
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };

  const payload = {
    replyToken: replyToken,
    messages: messages,
  };

  const options = {
    method: 'post',
    headers: headers,
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  };

  // LINE APIにリクエストを送信
  const response = UrlFetchApp.fetch(LINE_API_ENDPOINT, options);
  const responseCode = response.getResponseCode();
  if (responseCode !== 200) {
    console.error('LINE API request failed. Response code: ' + responseCode);
  }
}

// POSTリクエストを受け付けるエンドポイント
function doPost(e) {
  const body = JSON.parse(e.postData.contents);
  const events = body.events;

  // イベントごとの処理
  for (const event of events) {
    if (event.type === 'message' && event.message.type === 'text') {
      onMessage(event);
    }
  }

  // 正常に処理が終了したことを返す
  return ContentService.createTextOutput(JSON.stringify({ 'content': 'post request is received' })).setMimeType(ContentService.MimeType.JSON);
}

(2024.2.28追加ここまで)

更にコードの改変

(2024.03.04追記)
 canvasを調べてみると、様々なエフェクトが使えることが分かり、実装してみました。

 以下のようなイメージになります。
Screenshot_20240304_164855_LINE.jpg

index.css
index.css
.color > a{
  display: inline-block;
  width: 40px;
  height: 40px;
}

* {
  box-sizing: border-box;
}

body{
  overflow: hidden; /* スクロールを無効にする */
  /* margin: 0; /* マージンをゼロにする */
  background: #CCCCCC;
  text-align: center;
  font-family: 'Courier New', sans-serif;
}

#board {
    padding: 0.2em 0.5em;
    margin: 2em auto;
    width: 320px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}

.title{
    padding: 0.0em 0.5em;
    margin: 0.5em auto;
    width: 300px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}
.title p {
    user-select: none;
  color: #636e72;
    margin: 0; 
    padding: 2px;
}

canvas{
  cursor: pointer;
  background: white;
  border-radius: 16px;
}

.tools{
  margin: 0px;
  background: orange;
  text-align: center;
}

.black {
 display: inline-block;
 text-decoration: none;
 /*background: #87befd;*/
 background: #ffeaa7;
 width: 40px;
 height: 40px;
 line-height: 40px;
 border-radius: 50%;
 text-align: center;
 margin-right: 4px;
 margin-left: 4px;
 margin-top: 0px;
 margin-bottom: 0px;
 vertical-align: middle;
 overflow: hidden;
 transition: .4s;
}

.black i {
 /*ボタン自体*/
 vertical-align: middle;
 color: #272828;
 top: 9px;
 border-radius: 50%;
 font-size: 24px;
}

.red {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .red i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ec313e;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .blue {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .blue i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #0b3fcf;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .yellow {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .yellow i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #bec103;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .green {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .green i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #1faa00;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 
 .white {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .white i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ffffff;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .eraser {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .eraser i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .clear {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .clear i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

/* 色や太さを選択する部分のCSS */
.bold {
  display: flex;
  justify-content: center;
}

.bold a {width:84px; height:35px; margin:auto;list-style-type: none; border:1px solid #030d7d; border-radius:6px; margin:8px; display:block; float:left;}
#small {text-align:center; line-height:40px; font-size:100%;}
#middle {text-align:center; line-height:40px; font-size:125%;}
#large {text-align:center; line-height:40px; font-size:150%;}

.bgColor{
  margin-bottom: 4px;
}


.setting{
  display: flex;
  justify-content: center;
  margin-bottom: 8px;
}

.send{
display: flex;
justify-content: center;
}


  .button {
  width:100px;
  height:50px;
  font-family:Impact;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(245, 227, 123);
}

.button:hover{
opacity:0.8;
}

.clearBgColor{
  width:80px;
  height:25px;
  margin-right: 5px;
  font-family:Impact;
  font-size:80%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(244, 173, 209);
}

.file{
  width:100px;
  height:30px;
  margin-bottom: 4px;
  font-family:Impact;
  font-size:70%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(123, 245, 227);
}

.fileinput {
  display: none;
}

.particleMode{
  width:90px;
  height:30px;
  font-family:Impact;
  font-size:70%;
  color: #0b3fcf;
  font-weight: bold;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(123, 245, 164);
}

.particleMode:hover{
opacity:0.8;
}
index.html ver.1(2024.03.04)
index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>お絵描き</title>
      <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  </head>

  <body>
        <div class="title">
          <p>おえかきボード</p>
        </div>

        <div id="board">
          <canvas id="canvas" width="300px" height="300px"></canvas>
        </div>

        <div class="option">
          <div class="color">

            <a href="#" class="black" data-color="0, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="red" data-color="255, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="blue" data-color="0, 0, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="yellow" data-color="255, 255, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="green" data-color="0, 128, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="white" data-color="255, 255, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <button class="particleMode" id="particleMode">パーティクル</button>
            <a href="#" id="eraser" class="eraser">
              <i class="fas fa-eraser fa-3x"></i>
            </a>
            <a href="#" id="clear" class="clear">
              <i class="fas fa-undo-alt fa-3x"></i>
            </a>
            
            <div id="bold" class="bold">
              <a href="#" id="small" class="small" data-bold="1">ほそい</a>
              <a href="#" id="middle" class="middle" data-bold="5">ふつう</a>
              <a href="#" id="large" class="large" data-bold="10">ふとい</a>
            </div>
          </div>
        </div>

        <div class="setting">
          <div class="bgColor">背景色:
            <input type="color" id="bgColorPicker" value="#ffffff">
            <button id="clearBgColor" class="clearBgColor">背景色無し</button>
          </div>
          <div>
            <!-- ファイル選択ボタン -->
            <button id="file" class="file">写真を読み込む</button>
            <input type="file" id="fileinput" class="fileinput" accept="image/*" onchange="handleFileSelect(this)" />
          </div>
        </div>

        <div class="send">
        <!-- このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
          このHTMLと下のJSのコメントアウトを外してください。
          <button id="download">download</button>
        -->
          <button class="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
        </div>
        

      <script>

        window.onload = initializeLIFF;

        function initializeLIFF() {
          liff.init({
              liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
          })
          .then(() => {
              console.log("LIFF initialization successful");
          })
          .catch((err) => {
              console.error("LIFF initialization failed", err);
          });
        }

          // canvas
          var canvas = document.getElementById('canvas');
          var ctx = canvas.getContext('2d');
      
          // 変数宣言
          const cnvWidth = 500;
          const cnvHeight = 500;
          var cnvColor = "0, 0, 0, 1";  // 線の色
          var cnvBold = 5;  // 線の太さ
          var drawing = false; // 描画中フラグ
          var particleMode = false; //パーティクルエフェクトモードの選択
          var bgColor = "#ffffff"; //開いた時の最初の背景色(白)
      
          // canvasの背景色を設定(指定がない場合にjpeg保存すると背景が黒になる)
          setBgColor();
      
          // canvas上でのイベント
          canvas.addEventListener('mousedown', startDrawing);
          canvas.addEventListener('mouseup', stopDrawing);
          canvas.addEventListener('mousemove', draw);
          canvas.addEventListener('touchstart', startDrawing);
          canvas.addEventListener('touchend', stopDrawing);
          canvas.addEventListener('touchmove', draw);
      
      //ボタンをクリックしたときに、パーティクルエフェクトモードにする
      $("#particleMode").click(function () {
        particleMode = true;
        ctx.globalCompositeOperation = 'source-over';
      });


      // 描画処理
      function draw(e) {
        if (!drawing && !particleMode) return;

        var x, y;
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
        } else {
          x = e.offsetX;
          y = e.offsetY;
        }

        if (particleMode) {
          if (drawing) {
          generateParticle(x, y);
          }
        } else {
          if (drawing) {
            ctx.lineWidth = cnvBold;
            ctx.strokeStyle = 'rgba(' + cnvColor + ')';
            ctx.lineTo(x, y);
            ctx.stroke();
          }
        }
      }

      // 描画開始
      function startDrawing(e) {

          drawing = true;


        var x, y;
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
        } else {
          x = e.offsetX;
          y = e.offsetY;
        }

        if (!particleMode) {
          ctx.beginPath();
          ctx.lineCap = "round";
          ctx.moveTo(x, y);
        }
      }

      // 描画終了
      function stopDrawing() {
        drawing = false;
      }

      // 色の変更
      $(".color a").click(function () {
        cnvColor = $(this).data("color");
        ctx.globalCompositeOperation = 'source-over';
        particleMode = false;
        return false;
      });

      // 線の太さ変更
      $(".bold a").click(function () {
        cnvBold = $(this).data("bold");
        particleMode = false;
        return false;
      });

      // 消しゴム
      $("#eraser").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
      });

      // 描画クリア
      $("#clear").click(function () {
        ctx.clearRect(0, 0, cnvWidth, cnvHeight);
        setBgColor();
      });

      // 背景色の変更
      $("#bgColorPicker").change(function () {
        ctx.globalCompositeOperation = 'source-over';
        bgColor = $(this).val();
        setBgColor();
      });

      // 背景色を無しにする
      $("#clearBgColor").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
        setBgColor();
      });

      /*
       このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
       このJSと上のHTMLのコメントアウトを外してください。

          // canvasを画像で保存
          document.getElementById("download").onclick = (event) => {
            let canvas = document.getElementById("canvas");

            let link = document.createElement("a");
            link.href = canvas.toDataURL("image/png");
            link.download = "canvas.png";
            link.click();
          }
      */
      function setBgColor() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cnvWidth, cnvHeight);
      }

      function generateParticle(x, y) {
        const particleSize = Math.random() * 10 + 5;
        const particleColor = getRandomColor();
        const particleDirection = Math.random() * 2 * Math.PI;
        const particleSpeed = Math.random() * 3 + 1;
        
        setInterval(() => {
          ctx.beginPath();
          ctx.arc(x, y, particleSize, 0, 2 * Math.PI);
          ctx.fillStyle = particleColor;
          ctx.fill();

          x += particleSpeed * Math.cos(particleDirection);
          y += particleSpeed * Math.sin(particleDirection);

          particleSize *= 0.95;
          ctx.clearRect(x - particleSize, y - particleSize, particleSize * 2, particleSize * 2);

          if (particleSize < 0.5) {
            clearInterval();
          }
        }, 1000);
      }

      function getRandomColor() {
        const letters = '0123456789ABCDEF';
        let color = '#';
        for (let i = 0; i < 6; i++) {
          color += letters[Math.floor(Math.random() * 16)];
        }
        return color;
      }


      function generateParticle(x, y) {
        const particleSize = Math.random() * 10 + 5;
        const particleColor = getRandomColor();
        const particleDirection = Math.random() * 2 * Math.PI;
        const particleSpeed = Math.random() * 3 + 1;

        ctx.beginPath();
        ctx.arc(x, y, particleSize, 0, 2 * Math.PI);
        ctx.fillStyle = particleColor;
        ctx.fill();

        x += particleSpeed * Math.cos(particleDirection);
        y += particleSpeed * Math.sin(particleDirection);

        particleSize *= 0.95;
        ctx.clearRect(x - particleSize, y - particleSize, particleSize * 2, particleSize * 2);
      }

      function getRandomColor() {
        const letters = '0123456789ABCDEF';
        let color = '#';
        for (let i = 0; i < 6; i++) {
          color += letters[Math.floor(Math.random() * 16)];
        }
        return color;
      }          

          function uploadAndSendToLINE() {
              // LIFFが初期化されているか確認
              if (!liff.isInClient()) {
                  alert('This app must be viewed in the LINE app.');
                  return;
              }

              // Canvasから画像データを取得
              var imageDataURL = canvas.toDataURL();
              // 画像データをBase64に変換
              var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

              // 画像をImgBBにアップロード
              //YOUR_IMGG_API をあなたのImgBB APIに置き換えてください
              fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
                  method: 'POST',
                  body: new URLSearchParams({ image: base64Image }),
                  headers: {
                      'Content-Type': 'application/x-www-form-urlencoded'
                  }
              })
              .then(response => response.json())
              .then(data => {
                  // アップロード成功時にLINEに送信
                  sendImageMessage(data.data.url);
              })
              .catch(error => {
                  console.error('Error uploading image', error);
              });
          }

          function sendImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                liff.closeWindow();
              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

        //id="file"のボタンを押したときに、
        //input type="file" id="fileinput" をクリックしたことと
        //同じ動作にする設定
        //input type="file" id="fileinput"は、CSSで表示させないように設定
        document.querySelector("#file").addEventListener("click", () => {
          document.querySelector("#fileinput").click();
        });


        //デバイス(スマホやPC)から写真をcanvasに読み込む
        //写真を、縦横比を維持し、
        //縦長の写真ならば高さを300px、横長の写真ならば幅を300px、
        //正方形ならば高さ300px、幅300pxの大きさに変えて、canvasに読み込ませる
        // ファイル選択時の処理
        function handleFileSelect(input) {
          const file = input.files[0];
          if (file) {
            const reader = new FileReader();

            reader.onload = function (e) {
              const image = new Image();
              image.src = e.target.result;

              image.onload = function () {
                // 新しいCanvasを作成し、指定したサイズに変更
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                if (image.width > image.height) {
                  // 横長の場合
                  canvas.width = 300;
                  canvas.height = (300 / image.width) * image.height;
                } else if (image.width < image.height) {
                  // 縦長の場合
                  canvas.width = (300 / image.height) * image.width;
                  canvas.height = 300;
                } else {
                  // 正方形の場合
                  canvas.width = 300;
                  canvas.height = 300;
                }

                // 画像を新しいCanvasに描画
                ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

                // 元のCanvasを取得し、新しいCanvasの内容を元のCanvasに描画
                const originalCanvas = document.getElementById('canvas');
                const originalCtx = originalCanvas.getContext('2d');
                originalCanvas.width = canvas.width;
                originalCanvas.height = canvas.height;
                originalCtx.drawImage(canvas, 0, 0);

                // 新しいCanvasは不要なので削除
                document.body.removeChild(canvas);
              };
            };


          reader.readAsDataURL(file);
        }
      }
      </script>
  </body>
</html>

index.html ver.2(2024.03.06)
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>お絵描き</title>
      <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  </head>

  <body>
        <div class="title">
          <p>おえかきボード</p>
        </div>

        <div id="board">
          <canvas id="canvas" width="300px" height="300px"></canvas>
        </div>

        <div class="option">
          <div class="color">

            <a href="#" class="black" data-color="0, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="red" data-color="255, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="blue" data-color="0, 0, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="yellow" data-color="255, 255, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="green" data-color="0, 128, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="white" data-color="255, 255, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <button class="particleMode" id="particleMode" >パーティクル</button>
            <a href="#" id="eraser" class="eraser">
              <i class="fas fa-eraser fa-3x"></i>
            </a>
            <a href="#" id="clear" class="clear">
              <i class="fas fa-undo-alt fa-3x"></i>
            </a>
            
            <div id="bold" class="bold">
              <a href="#" id="small" class="small" data-bold="1">ほそい</a>
              <a href="#" id="middle" class="middle" data-bold="5">ふつう</a>
              <a href="#" id="large" class="large" data-bold="10">ふとい</a>
            </div>
          </div>
        </div>

        <div class="setting">
          <div class="bgColor">背景色:
            <input type="color" id="bgColorPicker" value="#ffffff">
            <button id="clearBgColor" class="clearBgColor">背景色無し</button>
          </div>
          <div>
            <!-- ファイル選択ボタン -->
            <button id="file" class="file">写真を読み込む</button>
            <input type="file" id="fileinput" class="fileinput" accept="image/*" onchange="handleFileSelect(this)" />
          </div>
        </div>

        <div class="send">
        <!-- このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
          このHTMLと下のJSのコメントアウトを外してください。
          <button id="download">download</button>
        -->
          <button class="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
        </div>
        

      <script>

        window.onload = initializeLIFF;

        function initializeLIFF() {
          liff.init({
              liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
          })
          .then(() => {
              console.log("LIFF initialization successful");
          })
          .catch((err) => {
              console.error("LIFF initialization failed", err);
          });
        }

          // canvas
          var canvas = document.getElementById('canvas');
          var ctx = canvas.getContext('2d');
      
          // 変数宣言
          const cnvWidth = 300;
          const cnvHeight = 300;
          var cnvColor = "0, 0, 0, 1";  // 線の色
          var cnvBold = 5;  // 線の太さ
          var drawing = false; // 描画中フラグ
          var particleMode = 0; //パーティクルエフェクトモードの選択
          var bgColor = "#ffffff"; //開いた時の最初の背景色(白)
      
          // canvasの背景色を設定(指定がない場合にjpeg保存すると背景が黒になる)
          setBgColor();
      
          // canvas上でのイベント
          canvas.addEventListener('mousedown', startDrawing);
          canvas.addEventListener('mouseup', stopDrawing);
          canvas.addEventListener('mousemove', draw);
          canvas.addEventListener('touchstart', startDrawing);
          canvas.addEventListener('touchend', stopDrawing);
          canvas.addEventListener('touchmove', draw);
      
      //ボタンをクリックしたときに、パーティクルエフェクトモードにする、押すたびにモード1と2を切り替える
      $("#particleMode").click(function () {
        if(particleMode == 0){
          particleMode = 1;
          document.getElementById("particleMode").style.color = "red";
          document.getElementById("particleMode").style.background= "blue";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 1){
          particleMode = 2;
          document.getElementById("particleMode").style.color = "yellow";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 2){
          particleMode = 3;
          document.getElementById("particleMode").style.color = "lightgreen";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 3){
          particleMode = 4;
          document.getElementById("particleMode").style.color = "purple";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 4){
          particleMode = 5;
          document.getElementById("particleMode").style.color = "white";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 5){
          particleMode = 6;
          document.getElementById("particleMode").style.color = "lightblue";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 6){
          particleMode = 1;
          document.getElementById("particleMode").style.color = "red";
          ctx.globalCompositeOperation = 'source-over';
        }
      });


      // 描画処理
      function draw(e) {
        if (!drawing && particleMode==0) return;

        var x, y;
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
        } else {
          x = e.offsetX;
          y = e.offsetY;
        }


        if (particleMode == 1) {
          if (drawing) {
            Particle_01(x, y);
          }

        }else if (particleMode == 2) {
          if (drawing) {
            Particles_02(e, lastX, lastY);
            }
              
        }else if (particleMode == 3) {
          if (drawing) {
            Particles_03();
            }
              
        }else if (particleMode == 4) {
          if (drawing) {
            Particles_04();
            }
              
        }else if (particleMode == 5) {
          if (drawing) {
            Particles_05();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else if (particleMode == 6) {
          if (drawing) {
            Particles_03();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else {
          if (drawing) {
            ctx.lineTo(x, y);
            ctx.stroke();
          }
        }
      }

      // 描画開始
      var lastX = 0;
      var lastY = 0;
      var x, y;

      function startDrawing(e) {
          drawing = true;
        
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
          lastX = e.touches[0].clientX - canvas.getBoundingClientRect().left;
          lastY = e.touches[0].clientY - canvas.getBoundingClientRect().top;

        } else {
          x = e.offsetX;
          y = e.offsetY;
          lastX = e.clientX - canvas.getBoundingClientRect().left;
          lastY = e.clientY - canvas.getBoundingClientRect().top;
        }
        
          ctx.lineWidth = cnvBold;
          ctx.strokeStyle = 'rgba(' + cnvColor + ')';

        if (!particleMode) {
          ctx.beginPath();
          ctx.lineCap = "round";
          ctx.moveTo(x, y);
        }
      }

      // 描画終了
      function stopDrawing() {
        drawing = false;
      }

      // 色の変更
      $(".color a").click(function () {
        cnvColor = $(this).data("color");
        ctx.strokeStyle = 'rgba(' + cnvColor + ')';
        ctx.globalCompositeOperation = 'source-over';
        document.getElementById("particleMode").style.color = "blue";
        document.getElementById("particleMode").style.background = "lightgreen";
        particleMode = 0;
        return false;
      });

      // 線の太さ変更
      $(".bold a").click(function () {
        cnvBold = $(this).data("bold");
        particleMode = 0;
        return false;
      });

      // 消しゴム
      $("#eraser").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
      });

      // 描画クリア
      $("#clear").click(function () {
        ctx.clearRect(0, 0, cnvWidth, cnvHeight);
        setBgColor();
      });

      // 背景色の変更
      $("#bgColorPicker").change(function () {
        ctx.globalCompositeOperation = 'source-over';
        bgColor = $(this).val();
        setBgColor();
      });

      // 背景色を無しにする
      $("#clearBgColor").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
        setBgColor();
      });

      /*
       このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
       このJSと上のHTMLのコメントアウトを外してください。

          // canvasを画像で保存
          document.getElementById("download").onclick = (event) => {
            let canvas = document.getElementById("canvas");

            let link = document.createElement("a");
            link.href = canvas.toDataURL("image/png");
            link.download = "canvas.png";
            link.click();
          }
      */
      function setBgColor() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cnvWidth, cnvHeight);
      }


      //ParticleMode=1 のエフェクト
          function Particle_01(x, y) {
            const particleSize = Math.random() * 10 + 5;
            const particleColor = getRandomColor();
            const particleDirection = Math.random() * 2 * Math.PI;
            const particleSpeed = Math.random() * 3 + 1;

            ctx.beginPath();
            ctx.arc(x, y, particleSize, 0, 2 * Math.PI);
            ctx.fillStyle = particleColor;
            ctx.fill();

            x += particleSpeed * Math.cos(particleDirection);
            y += particleSpeed * Math.sin(particleDirection);

            particleSize *= 0.95;
            ctx.clearRect(x - particleSize, y - particleSize, particleSize * 2, particleSize * 2);
          }

          function getRandomColor() {
            const letters = '0123456789ABCDEF';
            let color = '#';
            for (let i = 0; i < 6; i++) {
              color += letters[Math.floor(Math.random() * 16)];
            }
            return color;
          }          


      //ParticleMode=2 のエフェクト
          function Particles_02(e, lastX, lastY) {
              var x, y;
            if (e.type.startsWith('touch')) {
              x = e.touches[0].clientX - canvas.getBoundingClientRect().left;
              y = e.touches[0].clientY - canvas.getBoundingClientRect().top;
            } else {
              x = e.clientX - canvas.getBoundingClientRect().left;
              y = e.clientY - canvas.getBoundingClientRect().top;
            }
              drawLine(lastX, lastY, x, y);
              createParticle(x, y);
          }
      
          function drawLine(x1, y1, x2, y2) {
            ctx.lineWidth = cnvBold;
            ctx.strokeStyle = 'rgba(' + cnvColor + ')';
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
            }



          // パーティクルエフェクト用の変数
          var particles = [];

      //ParticleMode=3 のエフェクト          
          function Particles_03() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_03(); // パーティクル生成
          }

      //ParticleMode=4 のエフェクト          
          function Particles_04() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_04(); // パーティクル生成
          }

      //ParticleMode=5 のエフェクト
      function Particles_05() {
            animateParticles_03(); // パーティクルアニメーションを追加
      }

              // 0.1秒ごとにパーティクル生成
    setInterval(function () {
      if (drawing) {
        createParticle_03();
      }
    }, 100);
          // パーティクル生成関数
          function createParticle_03() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 3 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          function createParticle_04() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 30 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          // パーティクルアニメーション
          function animateParticles_03() {
            for (var i = 0; i < particles.length; i++) {
              var particle = particles[i];

              particle.x += Math.cos(particle.direction) * particle.speed;
              particle.y += Math.sin(particle.direction) * particle.speed;
              particle.size *= 0.9;
              particle.alpha *= 0.95;

              ctx.globalAlpha = particle.alpha;
              ctx.fillStyle = particle.color;
              ctx.beginPath();
              ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);
              ctx.fill();

              if (particle.size < 0.5) {
                particles.splice(i, 1);
                i--;
              }
            }
          }          



      //LINEへ描画したものを画像として送信する
          function uploadAndSendToLINE() {
              // LIFFが初期化されているか確認
              if (!liff.isInClient()) {
                  alert('This app must be viewed in the LINE app.');
                  return;
              }

              // Canvasから画像データを取得
              var imageDataURL = canvas.toDataURL();
              // 画像データをBase64に変換
              var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

              // 画像をImgBBにアップロード
              //YOUR_IMGG_API をあなたのImgBB APIに置き換えてください
              fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
                  method: 'POST',
                  body: new URLSearchParams({ image: base64Image }),
                  headers: {
                      'Content-Type': 'application/x-www-form-urlencoded'
                  }
              })
              .then(response => response.json())
              .then(data => {
                  // アップロード成功時にLINEに送信
                  sendImageMessage(data.data.url);
              })
              .catch(error => {
                  console.error('Error uploading image', error);
              });
          }

          function sendImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                liff.closeWindow();
              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

        //id="file"のボタンを押したときに、
        //input type="file" id="fileinput" をクリックしたことと
        //同じ動作にする設定
        //input type="file" id="fileinput"は、CSSで表示させないように設定
        document.querySelector("#file").addEventListener("click", () => {
          document.querySelector("#fileinput").click();
        });


        //デバイス(スマホやPC)から写真をcanvasに読み込む
        //写真を、縦横比を維持し、
        //縦長の写真ならば高さを300px、横長の写真ならば幅を300px、
        //正方形ならば高さ300px、幅300pxの大きさに変えて、canvasに読み込ませる
        // ファイル選択時の処理
        function handleFileSelect(input) {
          const file = input.files[0];
          if (file) {
            const reader = new FileReader();

            reader.onload = function (e) {
              const image = new Image();
              image.src = e.target.result;

              image.onload = function () {
                // 新しいCanvasを作成し、指定したサイズに変更
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                if (image.width > image.height) {
                  // 横長の場合
                  canvas.width = 300;
                  canvas.height = (300 / image.width) * image.height;
                } else if (image.width < image.height) {
                  // 縦長の場合
                  canvas.width = (300 / image.height) * image.width;
                  canvas.height = 300;
                } else {
                  // 正方形の場合
                  canvas.width = 300;
                  canvas.height = 300;
                }

                // 画像を新しいCanvasに描画
                ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

                // 元のCanvasを取得し、新しいCanvasの内容を元のCanvasに描画
                const originalCanvas = document.getElementById('canvas');
                const originalCtx = originalCanvas.getContext('2d');
                originalCanvas.width = canvas.width;
                originalCanvas.height = canvas.height;
                originalCtx.drawImage(canvas, 0, 0);

                // 新しいCanvasは不要なので削除
                document.body.removeChild(canvas);
              };
            };
            
          reader.readAsDataURL(file);
        }
      }
      </script>
  </body>
</html>

index.html ver.3(2024.03.07)アニメーション
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>お絵描き</title>
      <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  </head>

  <body>
        <div class="title">
          <p>おえかきボード</p>
        </div>

        <div id="board">
          <canvas id="canvas" width="300px" height="300px"></canvas>
        </div>

        <div class="option">
          <div class="color">

            <a href="#" class="black" data-color="0, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="red" data-color="255, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="blue" data-color="0, 0, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="yellow" data-color="255, 255, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="green" data-color="0, 128, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="white" data-color="255, 255, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <button class="particleMode" id="particleMode" >パーティクル</button>
            <a href="#" id="eraser" class="eraser">
              <i class="fas fa-eraser fa-3x"></i>
            </a>
            <a href="#" id="clear" class="clear">
              <i class="fas fa-undo-alt fa-3x"></i>
            </a>
            
            <div id="bold" class="bold">
              <a href="#" id="small" class="small" data-bold="1">ほそい</a>
              <a href="#" id="middle" class="middle" data-bold="5">ふつう</a>
              <a href="#" id="large" class="large" data-bold="10">ふとい</a>
            </div>
          </div>
        </div>

        <div class="setting">
          <div class="bgColor">背景色:
            <input type="color" id="bgColorPicker" value="#ffffff">
            <button id="clearBgColor" class="clearBgColor">背景色無し</button>
          </div>
          <div>
            <!-- ファイル選択ボタン -->
            <button id="file" class="file">写真を読み込む</button>
            <input type="file" id="fileinput" class="fileinput" accept="image/*" onchange="handleFileSelect(this)" />
          </div>
        </div>

        <div class="send">
        <!-- このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
          このHTMLと下のJSのコメントアウトを外してください。
          <button id="download">download</button>
        -->
          <button class="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
        </div>
        

      <script>

        window.onload = initializeLIFF;

        function initializeLIFF() {
          liff.init({
              liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
          })
          .then(() => {
              console.log("LIFF initialization successful");
          })
          .catch((err) => {
              console.error("LIFF initialization failed", err);
          });
        }

          // canvas
          var canvas = document.getElementById('canvas');
          var ctx = canvas.getContext('2d');
 
      //初期画面のアニメーション
          var animation = true;
          
          // ボールの初期値を設定する
          var x1 = 50; // ボール1の初期x座標
          var y1 = 50; // ボール1の初期y座標
          var dx1 = 2; // ボール1のx方向の移動量
          var dy1 = 4; // ボール1のy方向の移動量
          var radius1 = 21; // ボール1の半径
          var color1 = "red"; // ボール1の色
          
          var x2 = 100; // ボール2の初期x座標
          var y2 = 100; // ボール2の初期y座標
          var dx2 = 3; // ボール2のx方向の移動量
          var dy2 = 5; // ボール2のy方向の移動量
          var radius2 = 14; // ボール2の半径
          var color2 = "green"; // ボール2の色
          
          var x3 = 150; // ボール3の初期x座標
          var y3 = 150; // ボール3の初期y座標
          var dx3 = 4; // ボール3のx方向の移動量
          var dy3 = 6; // ボール3のy方向の移動量
          var radius3 = 7; // ボール3の半径
          var color3 = "blue"; // ボール3の色
          
          // ランダムな色を生成する関数(この行は変更しない)
          function randomColor() {
            var r = Math.floor(Math.random() * 256); // 赤の値を0から255の範囲でランダムに決める
            var g = Math.floor(Math.random() * 256); // 緑の値を0から255の範囲でランダムに決める
            var b = Math.floor(Math.random() * 256); // 青の値を0から255の範囲でランダムに決める
            return "rgb(" + r + "," + g + "," + b + ")"; // rgb形式の色を返す
          }
          
          // ボールを描画する関数
          function drawBall(x, y, radius, color) {
            ctx.beginPath();
            ctx.arc(x, y, radius, 0, Math.PI * 2);
            ctx.fillStyle = color;
            ctx.fill();
            ctx.closePath();
          }
          
          // ボールを動かす関数
          function moveBall(x, y, dx, dy, radius, color) {
            // ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア(この行を削除する)
            drawBall(x, y, radius, color); // ボールを描画
            x += dx; // x座標を更新
            y += dy; // y座標を更新
            // 壁に当たったら跳ね返る処理
            if (x + dx > canvas.width - radius || x + dx < radius) {
              dx = -dx;
              // ボールの色をランダムに変える処理(この行を追加する)
              color = randomColor();
            }
            if (y + dy > canvas.height - radius || y + dy < radius) {
              dy = -dy;
              // ボールの色をランダムに変える処理(この行を追加する)
              color = randomColor();
            }
            return [x, y, dx, dy, color]; // 更新した値を返す(colorも追加する)
          }
          
          // アニメーションを開始する関数
          function startAnimation() {
            ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア(この行は変更しない)
            // それぞれのボールに対してmoveBall関数を呼び出し、値を更新する
            [x1, y1, dx1, dy1, color1] = moveBall(x1, y1, dx1, dy1, radius1, color1); // color1も更新する
            [x2, y2, dx2, dy2, color2] = moveBall(x2, y2, dx2, dy2, radius2, color2); // color2も更新する
            [x3, y3, dx3, dy3, color3] = moveBall(x3, y3, dx3, dy3, radius3, color3); // color3も更新する
            requestID = window.requestAnimationFrame(startAnimation); // アニメーションを繰り返す
          }
          
          startAnimation(); // アニメーションを開始
          // アニメーションを開始する関数


          // 変数宣言
          const cnvWidth = 300;
          const cnvHeight = 300;
          var cnvColor = "0, 0, 0, 1";  // 線の色
          var cnvBold = 5;  // 線の太さ
          var drawing = false; // 描画中フラグ
          var particleMode = 0; //パーティクルエフェクトモードの選択
          var bgColor = "#ffffff"; //開いた時の最初の背景色(白)
      
          // canvasの背景色を設定(指定がない場合にjpeg保存すると背景が黒になる)
          setBgColor();
      
          // canvas上でのイベント
          canvas.addEventListener('mousedown', startDrawing);
          canvas.addEventListener('mouseup', stopDrawing);
          canvas.addEventListener('mousemove', draw);
          canvas.addEventListener('touchstart', startDrawing);
          canvas.addEventListener('touchend', stopDrawing);
          canvas.addEventListener('touchmove', draw);
      
      //ボタンをクリックしたときに、パーティクルエフェクトモードにする、押すたびにモード1と2を切り替える
      $("#particleMode").click(function () {
        if(particleMode == 0){
          particleMode = 1;
          document.getElementById("particleMode").style.color = "red";
          document.getElementById("particleMode").style.background= "blue";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 1){
          particleMode = 2;
          document.getElementById("particleMode").style.color = "yellow";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 2){
          particleMode = 3;
          document.getElementById("particleMode").style.color = "lightgreen";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 3){
          particleMode = 4;
          document.getElementById("particleMode").style.color = "purple";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 4){
          particleMode = 5;
          document.getElementById("particleMode").style.color = "white";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 5){
          particleMode = 6;
          document.getElementById("particleMode").style.color = "lightblue";
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 6){
          particleMode = 1;
          document.getElementById("particleMode").style.color = "red";
          ctx.globalCompositeOperation = 'source-over';
        }
      });


      // 描画処理
      function draw(e) {
        if (!drawing && particleMode==0) return;

        var x, y;
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
        } else {
          x = e.offsetX;
          y = e.offsetY;
        }


        if (particleMode == 1) {
          if (drawing) {
            Particle_01(x, y);
          }

        }else if (particleMode == 2) {
          if (drawing) {
            Particles_02(e, lastX, lastY);
            }
              
        }else if (particleMode == 3) {
          if (drawing) {
            Particles_03();
            }
              
        }else if (particleMode == 4) {
          if (drawing) {
            Particles_04();
            }
              
        }else if (particleMode == 5) {
          if (drawing) {
            Particles_05();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else if (particleMode == 6) {
          if (drawing) {
            Particles_03();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else {
          if (drawing) {
            ctx.lineTo(x, y);
            ctx.stroke();
          }
        }
      }

      // 描画開始
      var lastX = 0;
      var lastY = 0;
      var x, y;

      function startDrawing(e) {
          drawing = true;

          if(animation){
          animation = false;
          cancelAnimationFrame(requestID);
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          }
        
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
          lastX = e.touches[0].clientX - canvas.getBoundingClientRect().left;
          lastY = e.touches[0].clientY - canvas.getBoundingClientRect().top;

        } else {
          x = e.offsetX;
          y = e.offsetY;
          lastX = e.clientX - canvas.getBoundingClientRect().left;
          lastY = e.clientY - canvas.getBoundingClientRect().top;
        }
        
          ctx.lineWidth = cnvBold;
          ctx.strokeStyle = 'rgba(' + cnvColor + ')';

        if (!particleMode) {
          ctx.beginPath();
          ctx.lineCap = "round";
          ctx.moveTo(x, y);
        }
      }

      // 描画終了
      function stopDrawing() {
        drawing = false;
      }

      // 色の変更
      $(".color a").click(function () {
        cnvColor = $(this).data("color");
        ctx.strokeStyle = 'rgba(' + cnvColor + ')';
        ctx.globalCompositeOperation = 'source-over';
        document.getElementById("particleMode").style.color = "blue";
        document.getElementById("particleMode").style.background = "lightgreen";
        particleMode = 0;
        return false;
      });

      // 線の太さ変更
      $(".bold a").click(function () {
        cnvBold = $(this).data("bold");
        particleMode = 0;
        return false;
      });

      // 消しゴム
      $("#eraser").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
      });

      // 描画クリア
      $("#clear").click(function () {
        ctx.clearRect(0, 0, cnvWidth, cnvHeight);
        setBgColor();
      });

      // 背景色の変更
      $("#bgColorPicker").change(function () {
        ctx.globalCompositeOperation = 'source-over';
        bgColor = $(this).val();
        setBgColor();
      });

      // 背景色を無しにする
      $("#clearBgColor").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
        setBgColor();
      });

      /*
       このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
       このJSと上のHTMLのコメントアウトを外してください。

          // canvasを画像で保存
          document.getElementById("download").onclick = (event) => {
            let canvas = document.getElementById("canvas");

            let link = document.createElement("a");
            link.href = canvas.toDataURL("image/png");
            link.download = "canvas.png";
            link.click();
          }
      */
      function setBgColor() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cnvWidth, cnvHeight);
      }


      //ParticleMode=1 のエフェクト
          function Particle_01(x, y) {
            const particleSize = Math.random() * 10 + 5;
            const particleColor = getRandomColor();
            const particleDirection = Math.random() * 2 * Math.PI;
            const particleSpeed = Math.random() * 3 + 1;

            ctx.beginPath();
            ctx.arc(x, y, particleSize, 0, 2 * Math.PI);
            ctx.fillStyle = particleColor;
            ctx.fill();

            x += particleSpeed * Math.cos(particleDirection);
            y += particleSpeed * Math.sin(particleDirection);

            particleSize *= 0.95;
            ctx.clearRect(x - particleSize, y - particleSize, particleSize * 2, particleSize * 2);
          }

          function getRandomColor() {
            const letters = '0123456789ABCDEF';
            let color = '#';
            for (let i = 0; i < 6; i++) {
              color += letters[Math.floor(Math.random() * 16)];
            }
            return color;
          }          


      //ParticleMode=2 のエフェクト
          function Particles_02(e, lastX, lastY) {
              var x, y;
            if (e.type.startsWith('touch')) {
              x = e.touches[0].clientX - canvas.getBoundingClientRect().left;
              y = e.touches[0].clientY - canvas.getBoundingClientRect().top;
            } else {
              x = e.clientX - canvas.getBoundingClientRect().left;
              y = e.clientY - canvas.getBoundingClientRect().top;
            }
              drawLine(lastX, lastY, x, y);
              createParticle(x, y);
          }
      
          function drawLine(x1, y1, x2, y2) {
            ctx.lineWidth = cnvBold;
            ctx.strokeStyle = 'rgba(' + cnvColor + ')';
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
            }



          // パーティクルエフェクト用の変数
          var particles = [];

      //ParticleMode=3 のエフェクト          
          function Particles_03() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_03(); // パーティクル生成
          }

      //ParticleMode=4 のエフェクト          
          function Particles_04() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_04(); // パーティクル生成
          }

      //ParticleMode=5 のエフェクト
      function Particles_05() {
            animateParticles_03(); // パーティクルアニメーションを追加
      }

              // 0.1秒ごとにパーティクル生成
    setInterval(function () {
      if (drawing) {
        createParticle_03();
      }
    }, 100);
          // パーティクル生成関数
          function createParticle_03() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 3 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          function createParticle_04() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 30 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          // パーティクルアニメーション
          function animateParticles_03() {
            for (var i = 0; i < particles.length; i++) {
              var particle = particles[i];

              particle.x += Math.cos(particle.direction) * particle.speed;
              particle.y += Math.sin(particle.direction) * particle.speed;
              particle.size *= 0.9;
              particle.alpha *= 0.95;

              ctx.globalAlpha = particle.alpha;
              ctx.fillStyle = particle.color;
              ctx.beginPath();
              ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);
              ctx.fill();

              if (particle.size < 0.5) {
                particles.splice(i, 1);
                i--;
              }
            }
          }          



      //LINEへ描画したものを画像として送信する
          function uploadAndSendToLINE() {
              // LIFFが初期化されているか確認
              if (!liff.isInClient()) {
                  alert('This app must be viewed in the LINE app.');
                  return;
              }

              // Canvasから画像データを取得
              var imageDataURL = canvas.toDataURL();
              // 画像データをBase64に変換
              var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

              // 画像をImgBBにアップロード
              //YOUR_IMGG_API をあなたのImgBB APIに置き換えてください
              fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
                  method: 'POST',
                  body: new URLSearchParams({ image: base64Image }),
                  headers: {
                      'Content-Type': 'application/x-www-form-urlencoded'
                  }
              })
              .then(response => response.json())
              .then(data => {
                  // アップロード成功時にLINEに送信
                  sendImageMessage(data.data.url);
              })
              .catch(error => {
                  console.error('Error uploading image', error);
              });
          }

          function sendImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                liff.closeWindow();
              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

        //id="file"のボタンを押したときに、
        //input type="file" id="fileinput" をクリックしたことと
        //同じ動作にする設定
        //input type="file" id="fileinput"は、CSSで表示させないように設定
        document.querySelector("#file").addEventListener("click", () => {
          document.querySelector("#fileinput").click();
        });


        //デバイス(スマホやPC)から写真をcanvasに読み込む
        //写真を、縦横比を維持し、
        //縦長の写真ならば高さを300px、横長の写真ならば幅を300px、
        //正方形ならば高さ300px、幅300pxの大きさに変えて、canvasに読み込ませる
        // ファイル選択時の処理
        function handleFileSelect(input) {
          const file = input.files[0];
          if (file) {
            const reader = new FileReader();

            reader.onload = function (e) {
              const image = new Image();
              image.src = e.target.result;

              image.onload = function () {
                // 新しいCanvasを作成し、指定したサイズに変更
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                if (image.width > image.height) {
                  // 横長の場合
                  canvas.width = 300;
                  canvas.height = (300 / image.width) * image.height;
                } else if (image.width < image.height) {
                  // 縦長の場合
                  canvas.width = (300 / image.height) * image.width;
                  canvas.height = 300;
                } else {
                  // 正方形の場合
                  canvas.width = 300;
                  canvas.height = 300;
                }

                // 画像を新しいCanvasに描画
                ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

                // 元のCanvasを取得し、新しいCanvasの内容を元のCanvasに描画
                const originalCanvas = document.getElementById('canvas');
                const originalCtx = originalCanvas.getContext('2d');
                originalCanvas.width = canvas.width;
                originalCanvas.height = canvas.height;
                originalCtx.drawImage(canvas, 0, 0);

                // 新しいCanvasは不要なので削除
                document.body.removeChild(canvas);
              };
            };
            
          reader.readAsDataURL(file);
        }
      }
      </script>
  </body>
</html>

index.js
index.js
import './index.css';
import liff from '@line/liff'

document.addEventListener("DOMContentLoaded", function() {
  liff
    .init({ liffId: process.env.LIFF_ID })
    .then(() => {
        console.log("Success! you can do something with LIFF API here.")
    })
    .catch((error) => {
        console.log(error)
    })
});

(2024.03.04追記ここまで)

更に、更に、機能追加「お絵描きクイズ」

(2024.3.15追記)
 お絵描きbotに、お絵描きクイズを実装してみました。以下のようなイメージです。判定はランダムで適当です(笑)。皆様、お楽しみください。小さいお子様はこういう単純なゲームは好きだと思います。

bot.png

判定の時に同時に効果音が流れる仕様にしました。
効果音は、効果音らぼ(https://soundeffect-lab.info/)から適当にダウンロード(100種類)、一つのフォルダに入れて、1.mp3 ~ 100.mp3 までリネームし、フォルダをNetlifyへドラッグアンドドロップでアップロードし、URLをコピー(YOUR_NETLIFY_URL)。

音楽ファイルのアップロード.png

以下のHTMLコード内の、YOUR_LIFF_ID、YOUR_IMGG_API、YOUR_NETLIFY_URL を書き換えてください。

index.css
index.css
.color > a{
  display: inline-block;
  width: 40px;
  height: 40px;
}

* {
  box-sizing: border-box;
}

body{
  overflow: hidden; /* スクロールを無効にする */
  /* margin: 0; /* マージンをゼロにする */
  background: #CCCCCC;
  text-align: center;
  font-family: 'Courier New', sans-serif;
}

#board {
    padding: 0.2em 0.5em;
    margin: 2em auto;
    width: 320px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}

.title{
    padding: 0.0em 0.5em;
    margin: 0.5em auto;
    width: 300px;
    background: #ffda79;
    box-shadow: 0px 0px 0px 10px #ffda79;
    border: dashed 2px white;
}
.title p {
    user-select: none;
  color: #636e72;
    margin: 0; 
    padding: 2px;
}

canvas{
  cursor: pointer;
  background: white;
  border-radius: 16px;
}

.tools{
  margin: 0px;
  background: orange;
  text-align: center;
}

.black {
 display: inline-block;
 text-decoration: none;
 /*background: #87befd;*/
 background: #ffeaa7;
 width: 40px;
 height: 40px;
 line-height: 40px;
 border-radius: 50%;
 text-align: center;
 margin-right: 4px;
 margin-left: 4px;
 margin-top: 0px;
 margin-bottom: 0px;
 vertical-align: middle;
 overflow: hidden;
 transition: .4s;
}

.black i {
 /*ボタン自体*/
 vertical-align: middle;
 color: #272828;
 top: 9px;
 border-radius: 50%;
 font-size: 24px;
}

.red {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .red i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ec313e;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .blue {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .blue i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #0b3fcf;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .yellow {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .yellow i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #bec103;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .green {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .green i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #1faa00;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 
 .white {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 4px;
  margin-left: 4px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .white i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #ffffff;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .eraser {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 0px;
  margin-bottom: 0px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .eraser i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

 .clear {
  display: inline-block;
  text-decoration: none;
  /*background: #87befd;*/
  background: #ffeaa7;
  width: 40px;
  height: 40px;
  line-height: 40px;
  border-radius: 50%;
  text-align: center;
  margin-right: 12px;
  margin-left: 12px;
  margin-top: 4px;
  margin-bottom: 4px;
  vertical-align: middle;
  overflow: hidden;
  transition: .4s;
 }
 
 .clear i {
  /*ボタン自体*/
  vertical-align: middle;
  color: #636e72;
  top: 9px;
  border-radius: 50%;
  font-size: 24px;
 }

/* 色や太さを選択する部分のCSS */
.bold {
  display: flex;
  justify-content: center;
}

.bold a {width:84px; height:35px; margin:auto;list-style-type: none; border:1px solid #030d7d; border-radius:6px; margin:8px; display:block; float:left;}
#small {text-align:center; line-height:40px; font-size:100%;}
#middle {text-align:center; line-height:40px; font-size:125%;}
#large {text-align:center; line-height:40px; font-size:150%;}

.bgColor{
  margin-bottom: 4px;
}


.setting{
  display: flex;
  justify-content: center;
  margin-bottom: 8px;
}

.send{
display: flex;
justify-content: center;
}


  .button {
  width:100px;
  height:50px;
  font-family:Impact;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(245, 227, 123);
  margin-left: 10px;
}

.button:hover{
opacity:0.8;
}

.clearBgColor{
  width:80px;
  height:25px;
  margin-right: 5px;
  font-family:Impact;
  font-size:80%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(244, 173, 209);
}

.file{
  width:100px;
  height:30px;
  margin-bottom: 4px;
  font-family:Impact;
  font-size:70%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(123, 245, 227);
}

.fileinput {
  display: none;
}

.particleMode{
  width:90px;
  height:30px;
  font-family:Impact;
  font-size:70%;
  color: #0b3fcf;
  font-weight: bold;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:20px;
  background-color:rgb(123, 245, 164);
}

.particleMode:hover{
opacity:0.8;
}

.quiz{
  width:100px;
  height:50px;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:30px;
  background-color:rgb(130, 255, 108);
  margin-right: 20px;
}

.quiz:hover{
  opacity:0.8;
  }

/* 初めは隠れているボタン */
#hiddenMessage {
  display: none;
}

.hiddenMessage {
  width:200px;
  height:20px;
  font-size:100%;
  text-align:center;
  box-shadow:0 0 10px rgb(108, 108, 108);
  border-radius:30px;
  color:white;
  background-color:rgb(108, 83, 249);
  margin-right: 20px;
}
index.html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="index.css" />
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <title>お絵描き</title>
      <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  </head>

  <body>
        <div class="title">
          <p>おえかきボード</p>
        </div>

        <div id="board">
          <canvas id="canvas" width="300px" height="300px"></canvas>
        </div>

        <div class="option">
          <div class="color">

            <a href="#" class="black" data-color="0, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="red" data-color="255, 0, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="blue" data-color="0, 0, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="yellow" data-color="255, 255, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="green" data-color="0, 128, 0, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <a href="#" class="white" data-color="255, 255, 255, 1">
              <i class="fas fa-paint-brush fa-3x"></i>
            </a>
            <button class="particleMode" id="particleMode" >パーティクル</button>
            <a href="#" id="eraser" class="eraser">
              <i class="fas fa-eraser fa-3x"></i>
            </a>
            <a href="#" id="clear" class="clear">
              <i class="fas fa-undo-alt fa-3x"></i>
            </a>
            
            <div id="bold" class="bold">
              <a href="#" id="small" class="small" data-bold="1">ほそい</a>
              <a href="#" id="middle" class="middle" data-bold="5">ふつう</a>
              <a href="#" id="large" class="large" data-bold="10">ふとい</a>
            </div>
          </div>
        </div>

        <div class="setting">
          <div class="bgColor">背景色:
            <input type="color" id="bgColorPicker" value="#ffffff">
            <button id="clearBgColor" class="clearBgColor">背景色無し</button>
          </div>
          <div>
            <!-- ファイル選択ボタン -->
            <button id="file" class="file">写真を読み込む</button>
            <input type="file" id="fileinput" class="fileinput" accept="image/*" onchange="handleFileSelect(this)" />
          </div>
        </div>

        <div class="send">
        <!-- このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
          このHTMLと下のJSのコメントアウトを外してください。
          <button id="download">download</button>
        -->
         <button class="quiz" id="quiz" onclick="generateSentence()">Quiz!</button>
          <button class="button" id="button" onclick="uploadAndSendToLINE()">Send to LINE</button>
        </div>
        <div class="send">
        <a class="hiddenMessage" id="hiddenMessage">お題目の絵を書いて!</a>
      </div>

      <audio id="Sound_effect">
        <source id="Sound_kinds" class="Sound_kinds" src="" type="audio/mp3">
     </audio>

      <script>

        window.onload = initializeLIFF;

        function initializeLIFF() {
          liff.init({
              liffId: 'YOUR_LIFF_ID' // あなたのLIFFアプリのIDに置き換えてください
          })
          .then(() => {
              console.log("LIFF initialization successful");
          })
          .catch((err) => {
              console.error("LIFF initialization failed", err);
          });
        }

          // canvas
          var canvas = document.getElementById('canvas');
          var ctx = canvas.getContext('2d');

          // 変数宣言
          const cnvWidth = 300;
          const cnvHeight = 300;
          var cnvColor = "0, 0, 0, 1";  // 線の色
          var cnvBold = 5;  // 線の太さ
          var drawing = false; // 描画中フラグ
          var particleMode = 0; //パーティクルエフェクトモードの選択
          var bgColor = "#ffffff"; //開いた時の最初の背景色(白)
          var quiz = false;
          var drawing_quiz = false;
      
          // canvasの背景色を設定(指定がない場合にjpeg保存すると背景が黒になる)
          setBgColor();
      //初期画面のアニメーション
          var animation = true;
          
          // ボールの初期値を設定する
          var x1 = 50; // ボール1の初期x座標
          var y1 = 50; // ボール1の初期y座標
          var dx1 = 2; // ボール1のx方向の移動量
          var dy1 = 4; // ボール1のy方向の移動量
          var radius1 = 21; // ボール1の半径
          var color1 = "red"; // ボール1の色
          
          var x2 = 100; // ボール2の初期x座標
          var y2 = 100; // ボール2の初期y座標
          var dx2 = 3; // ボール2のx方向の移動量
          var dy2 = 5; // ボール2のy方向の移動量
          var radius2 = 14; // ボール2の半径
          var color2 = "green"; // ボール2の色
          
          var x3 = 150; // ボール3の初期x座標
          var y3 = 150; // ボール3の初期y座標
          var dx3 = 4; // ボール3のx方向の移動量
          var dy3 = 6; // ボール3のy方向の移動量
          var radius3 = 7; // ボール3の半径
          var color3 = "blue"; // ボール3の色
          
          // ランダムな色を生成する関数(この行は変更しない)
          function randomColor() {
            var r = Math.floor(Math.random() * 256); // 赤の値を0から255の範囲でランダムに決める
            var g = Math.floor(Math.random() * 256); // 緑の値を0から255の範囲でランダムに決める
            var b = Math.floor(Math.random() * 256); // 青の値を0から255の範囲でランダムに決める
            return "rgb(" + r + "," + g + "," + b + ")"; // rgb形式の色を返す
          }
          
          // ボールを描画する関数
          function drawBall(x, y, radius, color) {
            ctx.beginPath();
            ctx.arc(x, y, radius, 0, Math.PI * 2);
            ctx.fillStyle = color;
            ctx.fill();
            ctx.closePath();
          }
          
          // ボールを動かす関数
          function moveBall(x, y, dx, dy, radius, color) {
            // ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア(この行を削除する)
            drawBall(x, y, radius, color); // ボールを描画
            x += dx; // x座標を更新
            y += dy; // y座標を更新
            // 壁に当たったら跳ね返る処理
            if (x + dx > canvas.width - radius || x + dx < radius) {
              dx = -dx;
              // ボールの色をランダムに変える処理(この行を追加する)
              color = randomColor();
            }
            if (y + dy > canvas.height - radius || y + dy < radius) {
              dy = -dy;
              // ボールの色をランダムに変える処理(この行を追加する)
              color = randomColor();
            }
            return [x, y, dx, dy, color]; // 更新した値を返す(colorも追加する)
          }
          
          // アニメーションを開始する関数
          function startAnimation() {
            ctx.clearRect(0, 0, canvas.width, canvas.height); // キャンバスをクリア(この行は変更しない)
            // それぞれのボールに対してmoveBall関数を呼び出し、値を更新する
            [x1, y1, dx1, dy1, color1] = moveBall(x1, y1, dx1, dy1, radius1, color1); // color1も更新する
            [x2, y2, dx2, dy2, color2] = moveBall(x2, y2, dx2, dy2, radius2, color2); // color2も更新する
            [x3, y3, dx3, dy3, color3] = moveBall(x3, y3, dx3, dy3, radius3, color3); // color3も更新する
            requestID = window.requestAnimationFrame(startAnimation); // アニメーションを繰り返す
          }
          
          startAnimation(); // アニメーションを開始
          // アニメーションを開始する関数

          // canvas上でのイベント
          canvas.addEventListener('mousedown', startDrawing);
          canvas.addEventListener('mouseup', stopDrawing);
          canvas.addEventListener('mousemove', draw);
          canvas.addEventListener('touchstart', startDrawing);
          canvas.addEventListener('touchend', stopDrawing);
          canvas.addEventListener('touchmove', draw);
      
      //ボタンをクリックしたときに、パーティクルエフェクトモードにする、押すたびにモード1と2を切り替える
      $("#particleMode").click(function () {
        var particle = document.getElementById("particleMode")
        if(particleMode == 0){
          particleMode = 1;
          particle.style.color = "red";
          particle.style.background= "blue";
          particle.textContent = 'Part 1';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 1){
          particleMode = 2;
          particle.style.color = "yellow";
          particle.textContent = 'Part 2';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 2){
          particleMode = 3;
          particle.style.color = "lightgreen";
          particle.textContent = 'Part 3';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 3){
          particleMode = 4;
          particle.style.color = "gray";
          particle.textContent = 'Part 4';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 4){
          particleMode = 5;
          particle.style.color = "white";
          particle.textContent = 'Part 5';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 5){
          particleMode = 6;
          particle.style.color = "lightblue";
          particle.textContent = 'Part 6';
          ctx.globalCompositeOperation = 'source-over';
        }else if(particleMode == 6){
          particleMode = 1;
          particle.style.color = "red";
          particle.textContent = 'Part 1';
          ctx.globalCompositeOperation = 'source-over';
        }
      });


      // 描画処理
      function draw(e) {
        if (!drawing && particleMode==0) return;

        var x, y;
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
        } else {
          x = e.offsetX;
          y = e.offsetY;
        }

        if (quiz) {
          drawing_quiz = true;
        }

        if (particleMode == 1) {
          if (drawing) {
            Particle_01(x, y);
          }

        }else if (particleMode == 2) {
          if (drawing) {
            Particles_02(e, lastX, lastY);
            }
              
        }else if (particleMode == 3) {
          if (drawing) {
            Particles_03();
            }
              
        }else if (particleMode == 4) {
          if (drawing) {
            Particles_04();
            }
              
        }else if (particleMode == 5) {
          if (drawing) {
            Particles_05();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else if (particleMode == 6) {
          if (drawing) {
            Particles_03();
            ctx.lineTo(x, y);
            ctx.stroke();
            }
              
        }else {
          if (drawing) {
            ctx.lineTo(x, y);
            ctx.stroke();
          }
        }
      }

      // 描画開始
      var lastX = 0;
      var lastY = 0;
      var x, y;

      function startDrawing(e) {
          drawing = true;
          
          if(animation){
          animation = false;
          cancelAnimationFrame(requestID);
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          setBgColor();
          }
        
        if (e.type.startsWith('touch')) {
          x = e.touches[0].clientX - canvas.offsetLeft;
          y = e.touches[0].clientY - canvas.offsetTop;
          lastX = e.touches[0].clientX - canvas.getBoundingClientRect().left;
          lastY = e.touches[0].clientY - canvas.getBoundingClientRect().top;

        } else {
          x = e.offsetX;
          y = e.offsetY;
          lastX = e.clientX - canvas.getBoundingClientRect().left;
          lastY = e.clientY - canvas.getBoundingClientRect().top;
        }
        
          ctx.lineWidth = cnvBold;
          ctx.strokeStyle = 'rgba(' + cnvColor + ')';

        if (!particleMode) {
          ctx.beginPath();
          ctx.lineCap = "round";
          ctx.moveTo(x, y);
        }
      }

      // 描画終了
      function stopDrawing() {
        drawing = false;
      }

      // 色の変更
      $(".color a").click(function () {
        cnvColor = $(this).data("color");
        particleMode = 0;
        ctx.strokeStyle = 'rgba(' + cnvColor + ')';
        ctx.globalCompositeOperation = 'source-over';
        var particle = document.getElementById("particleMode")
        particle.style.color = "blue";
        particle.style.background = "lightgreen";
        particle.textContent = 'パーティクル';
        return false;
      });

      // 線の太さ変更
      $(".bold a").click(function () {
        cnvBold = $(this).data("bold");
        particleMode = 0;
        return false;
      });

      // 消しゴム
      $("#eraser").click(function () {
        ctx.globalCompositeOperation = 'destination-out';
      });

      // 描画クリア
      $("#clear").click(function () {
        ctx.clearRect(0, 0, cnvWidth, cnvHeight);
        setBgColor();
      });

      // 背景色の変更
      $("#bgColorPicker").change(function () {
        animation = false;
        cancelAnimationFrame(requestID);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        bgColor = $(this).val();
        setBgColor();
        ctx.globalCompositeOperation = 'source-over';
      });

      // 背景色を無しにする
      $("#clearBgColor").click(function () {
        animation = false;
        cancelAnimationFrame(requestID);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.globalCompositeOperation = 'destination-out';
        setBgColor();
      });

      /*
       このLIFFをブラウザーで使用し、デバイスへダウンロードする場合
       このJSと上のHTMLのコメントアウトを外してください。

          // canvasを画像で保存
          document.getElementById("download").onclick = (event) => {
            let canvas = document.getElementById("canvas");

            let link = document.createElement("a");
            link.href = canvas.toDataURL("image/png");
            link.download = "canvas.png";
            link.click();
          }
      */
      function setBgColor() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cnvWidth, cnvHeight);
      }


      //ParticleMode=1 のエフェクト
          function Particle_01(x, y) {
            const particleSize = Math.random() * 10 + 5;
            const particleColor = getRandomColor();
            const particleDirection = Math.random() * 2 * Math.PI;
            const particleSpeed = Math.random() * 3 + 1;

            ctx.beginPath();
            ctx.arc(x, y, particleSize, 0, 2 * Math.PI);
            ctx.fillStyle = particleColor;
            ctx.fill();

            x += particleSpeed * Math.cos(particleDirection);
            y += particleSpeed * Math.sin(particleDirection);

            particleSize *= 0.95;
            ctx.clearRect(x - particleSize, y - particleSize, particleSize * 2, particleSize * 2);
          }

          function getRandomColor() {
            const letters = '0123456789ABCDEF';
            let color = '#';
            for (let i = 0; i < 6; i++) {
              color += letters[Math.floor(Math.random() * 16)];
            }
            return color;
          }          


      //ParticleMode=2 のエフェクト
          function Particles_02(e, lastX, lastY) {
              var x, y;
            if (e.type.startsWith('touch')) {
              x = e.touches[0].clientX - canvas.getBoundingClientRect().left;
              y = e.touches[0].clientY - canvas.getBoundingClientRect().top;
            } else {
              x = e.clientX - canvas.getBoundingClientRect().left;
              y = e.clientY - canvas.getBoundingClientRect().top;
            }
              drawLine(lastX, lastY, x, y);
              createParticle(x, y);
          }
      
          function drawLine(x1, y1, x2, y2) {
            ctx.lineWidth = cnvBold;
            ctx.strokeStyle = 'rgba(' + cnvColor + ')';
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.stroke();
            }


          // パーティクルエフェクト用の変数
          var particles = [];

      //ParticleMode=3 のエフェクト          
          function Particles_03() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_03(); // パーティクル生成
          }

      //ParticleMode=4 のエフェクト          
          function Particles_04() {
            animateParticles_03(); // パーティクルアニメーションを追加
            createParticle_04(); // パーティクル生成
          }

      //ParticleMode=5 のエフェクト
      function Particles_05() {
            animateParticles_03(); // パーティクルアニメーションを追加
      }

              // 0.1秒ごとにパーティクル生成
    setInterval(function () {
      if (drawing) {
        createParticle_03();
      }
    }, 100);
          // パーティクル生成関数
          function createParticle_03() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 3 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          function createParticle_04() {
            var particle = {
              x: Math.random() * cnvWidth,
              y: Math.random() * cnvHeight,
              size: Math.random() * 10 + 5,
              color: 'rgba(' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() * 255 + ',' + Math.random() + ')',
              direction: Math.random() * 2 * Math.PI,
              speed: Math.random() * 30 + 1,
              alpha: 1
            };

            particles.push(particle);
          }

          // パーティクルアニメーション
          function animateParticles_03() {
            for (var i = 0; i < particles.length; i++) {
              var particle = particles[i];

              particle.x += Math.cos(particle.direction) * particle.speed;
              particle.y += Math.sin(particle.direction) * particle.speed;
              particle.size *= 0.9;
              particle.alpha *= 0.95;

              ctx.globalAlpha = particle.alpha;
              ctx.fillStyle = particle.color;
              ctx.beginPath();
              ctx.arc(particle.x, particle.y, particle.size, 0, 2 * Math.PI);
              ctx.fill();

              if (particle.size < 0.5) {
                particles.splice(i, 1);
                i--;
              }
            }
          }          



      //LINEへ描画したものを画像として送信する
          function uploadAndSendToLINE() {
              // LIFFが初期化されているか確認
              if (!liff.isInClient()) {
                  alert('This app must be viewed in the LINE app.');
                  return;
              }

              // Canvasから画像データを取得
              var imageDataURL = canvas.toDataURL();
              // 画像データをBase64に変換
              var base64Image = imageDataURL.replace(/^data:image\/(png|jpg);base64,/, '');

              // 画像をImgBBにアップロード
              //YOUR_IMGG_API をあなたのImgBB APIに置き換えてください
              fetch('https://api.imgbb.com/1/upload?key=YOUR_IMGBB_API_KEY', {
                  method: 'POST',
                  body: new URLSearchParams({ image: base64Image }),
                  headers: {
                      'Content-Type': 'application/x-www-form-urlencoded'
                  }
              })
              .then(response => response.json())
              .then(data => {
                  // アップロード成功時にLINEに送信
                  if(quiz){
                    if(drawing_quiz){
                      quiz = false;
                      drawing_quiz = false;
                      document.getElementById("hiddenMessage").style.display = "none";
                      document.getElementById("hiddenMessage").textContent = 'お題目の絵を書いて!';
                      document.getElementById("hiddenMessage").style.background= "blue";
                      sendTextImageMessage(data.data.url);
                      generateSentence_Re();
                      var randomNumber = Math.floor(Math.random() * 100) + 1;
                      //YOUR_NETLIFY_URL:Netlifyにアップロードした音声ファイルの入ったフォルダのURLを入れてください。
                      document.getElementById("Sound_kinds").src = "https://YOUR_NETLIFY_URL.netlify.app/" + randomNumber + ".mp3";
                      document.getElementById("Sound_effect").load(); 
                      document.getElementById("Sound_effect").play();
                    }else {
                      document.getElementById("hiddenMessage").textContent = 'お題目の絵を書いてから!';
                      document.getElementById("hiddenMessage").style.background= "red";
                    }
                  }else{
                    sendImageMessage(data.data.url);
                  }
              })
              .catch(error => {
                  console.error('Error uploading image', error);
              });
          }

          function sendTextImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                //liff.closeWindow();


              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

          function sendImageMessage(imageUrl) {
              // 画像のURLをLINEに送信
              liff.sendMessages([
                  {
                      type: 'image',
                      originalContentUrl: imageUrl,
                      previewImageUrl: imageUrl
                  }
              ])
              .then(() => {
                liff.closeWindow();
              })
              .catch((err) => {
                  console.error('Error sending image', err);
              });
          }

        //id="file"のボタンを押したときに、
        //input type="file" id="fileinput" をクリックしたことと
        //同じ動作にする設定
        //input type="file" id="fileinput"は、CSSで表示させないように設定
        document.querySelector("#file").addEventListener("click", () => {
          document.querySelector("#fileinput").click();
        });


        //デバイス(スマホやPC)から写真をcanvasに読み込む
        //写真を、縦横比を維持し、
        //縦長の写真ならば高さを300px、横長の写真ならば幅を300px、
        //正方形ならば高さ300px、幅300pxの大きさに変えて、canvasに読み込ませる
        // ファイル選択時の処理
        function handleFileSelect(input) {
          const file = input.files[0];
          if (file) {
            animation = false;
            cancelAnimationFrame(requestID);
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            const reader = new FileReader();

            reader.onload = function (e) {
              const image = new Image();
              image.src = e.target.result;

              image.onload = function () {
                // 新しいCanvasを作成し、指定したサイズに変更
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                if (image.width > image.height) {
                  // 横長の場合
                  canvas.width = 300;
                  canvas.height = (300 / image.width) * image.height;
                } else if (image.width < image.height) {
                  // 縦長の場合
                  canvas.width = (300 / image.height) * image.width;
                  canvas.height = 300;
                } else {
                  // 正方形の場合
                  canvas.width = 300;
                  canvas.height = 300;
                }

                // 画像を新しいCanvasに描画
                ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

                // 元のCanvasを取得し、新しいCanvasの内容を元のCanvasに描画
                const originalCanvas = document.getElementById('canvas');
                const originalCtx = originalCanvas.getContext('2d');
                originalCanvas.width = canvas.width;
                originalCanvas.height = canvas.height;
                originalCtx.drawImage(canvas, 0, 0);

                // 新しいCanvasは不要なので削除
                document.body.removeChild(canvas);
              };
            };
            
          reader.readAsDataURL(file);
        }
      }

        //クイズ
        function generateSentence() {
            if(animation){
            animation = false;
            cancelAnimationFrame(requestID);
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            drawing = false;
        }

            document.getElementById("hiddenMessage").style.display = "block";
            const quizBtn = document.getElementById('quiz');
            var button = document.getElementById("button")
            button.style.color = "white";
            button.style.background= "red";
            button.textContent = 'judge!';

            quiz = true;

          // 用意されたリストからランダムに単語を選択
          var wordList = ["太陽", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "自転車", "花火"];
//お好きなお題目を入れてください。

          var selectedWord = wordList[Math.floor(Math.random() * wordList.length)];

          // 文章の組み立て
          var sentence1 = '' + selectedWord + '';
          var sentence2 = 'を書いてください。';

          // Canvasに文章を表示

          ctx.clearRect(0, 0, canvas.width, canvas.height);
          setBgColor();
          // 文字のスタイルを変更
          ctx.font = "20px Arial";  // 文字のサイズを大きく

          // sentence1の文字を赤く描画
          ctx.fillStyle = "red";
          ctx.fillText(sentence1, 10, 30);

          // sentence2の文字を黒く描画
          ctx.fillStyle = "#000";
          ctx.font = "14px Arial";  // 文字のサイズを小さく
          ctx.fillText(sentence2, 20, 50);


      }

        //クイズの判定の返信
        function generateSentence_Re() {
            const quizBtn = document.getElementById('quiz');
            var button = document.getElementById("button")
            button.style.color = "black";
            button.style.background= "yellow";
            button.textContent = 'Send to LINE';

            quiz = false;

          // 用意されたリストからランダムに単語を選択
          var wordList = [
    "すばらしい!", "天才か‼", "才能あり!", "おしい!", "えぇぇぇーーーっ", "おっと!", "ふぅーん。", "そうか、なるほど。", "そうねぇ", "まあまあかな?"];
          var selectedWord = wordList[Math.floor(Math.random() * wordList.length)];

          // 文章の組み立て
          var sentence1 =  selectedWord;


          // Canvasに文章を表示

          ctx.clearRect(0, 0, canvas.width, canvas.height);
          setBgColor();
          // 文字のスタイルを変更
          ctx.font = "20px Arial";  // 文字のサイズを大きく

          // sentence1の文字を赤く描画
          ctx.fillStyle = "blue";
          ctx.font = "30px Arial"; 
          ctx.fillText(sentence1, 30, 90);
      }
      </script>
  </body>
</html>
index.js
index.js
import './index.css';
import liff from '@line/liff'

document.addEventListener("DOMContentLoaded", function() {
  liff
    .init({ liffId: process.env.LIFF_ID })
    .then(() => {
        console.log("Success! you can do something with LIFF API here.")
    })
    .catch((error) => {
        console.log(error)
    })
});

注意点

 canvasで描画した画像は、LIFFからデバイスへ直接ダウンロードは出来ないとのこと。
 canvasで描画した画像をデバイスへダウンロードしたい場合は、
① LINEへ送信した画像をダウンロードする
② canvasにダウンロードボタンを実装し、canvasをcromeなどのブラウザーから開いて使用する。この場合は直接ダウンロードできます。
(上記コードの、HTMLとJSのコメントアウトを外すと使用できます。)
 canvasの描画コードは、各自カスタマイズしてください(canvas画面の大きさ、線の太さやカラーの種類、など)。

苦労したポイント

● HTMLとJSしかわからないので、それでつくれるものを探す
⇒なかなか見つからず。

● jsではLIFF→LINEへ画像を送信できないこと、PHPを利用する必要があるとわかる
⇒スキルがなく、うまくいかず。

● Pythonを勉強すればよいかもと思ったが
⇒なかなか学習には至らず。

● 画像を取り扱うにあたり、様々な表示形式があることを知らず困惑
⇒ https://zenn.dev/daikuman/articles/df73908756bf04
にて学習し少し理解。

● どうしようもなくなり、ChatGPTに質問をしコードを書いてもらった。
⇒最初にImgBBを利用したコードを提示されたが、ImgBB自体を知らず、自分の知っているサービス内で作成したいと思った。

⇒LIFFはNetlifyでデプロイしているのだから、「Netlifyで出来ないか?」と質問したところ、Netlify formの機能を利用すれば出来るコードを提示された。Netlify formへは、テキストファイルは送信できるものの、画像は送信できなかった(おそらくスキル不足)。

⇒気を取り直して、ImgBBを登録し、ImgBBを利用したコードを使用。コピペでいけるかと思ったが出来ず。

⇒上記の様々な迷走の中で得た知識 ”画像を取り扱うにあたり様々な表示形式があり適宜変換し使用する” ことを少しはわかっていたことで、ChatGPTに提示されたコードの不足分を補うことが出来、無事に動かすことが出来た。

という感じで、どの方向で学習を進めていけばよいのかわからないまま迷走し、模索し、難渋したものの(3年間程(笑))、何とか作成することが出来ました。これは、とりあえず成功 という域だと思うので、もう少し検討してみたいと思います。

画像サーバーとして利用できるものは、ImgBBの他、Netlify form、GitHubなどを利用する方法もあるようです(ChatGPTによるものなので詳細は不明です)。ImgBBは、一定時間経過すると画像をサーバーから消去する機能(設定)があり、他のサーバーに比べ使用するメリットがあると思います。

その他補足

 LIFFスターターアプリを使うにあたり、私がつまづいたポイントを記載しておきます。
スターターアプリでの作成~Netlifyでの初回デプロイまでは、すんなり出来ると思います。

 私がつまづいたところは、Visual Studio Codeを終了(画面を閉じて)、再度起動、再度編集の後に、Netlifyへデプロイするところでした。
 再デプロイする時に、開いているディレクトリーを「line-liff-v2-starter」になっていないと、build(netlify build)の段階でエラーが出ます。そのあと無理やりデプロイは出来ますがコードは作動しません。
 Visual Studio Code再度起動時に開いているディレクトリーは大本のディレクトリーとなっていますので、ターミナルに、cd line-liff-v2-starter と入力し開いているディレクトリーを移動する必要があります。

 同様に、デプロイせずにローカルサーバ(http://localhost:3000)で開いて動作確認をする場合には、開いているディレクトリーは「vanilla」である必要があり、ターミナルに cd line-liff-v2-starter/src/vanilla と入力し開いているディレクトリーを移動してください。

お絵描き機能を応用した作成物

(2024.3.27追記)
この記事を応用し、以下のものを作成しました。合わせてお読みいただけると嬉しいです。

「文字練習bot」~canvasで文字を書き、OCRで読み取る~
https://qiita.com/21HideK/items/ce9f1487420b5ca4a4c3

参考にしたページ

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?