Canvas API備忘録(1)

HTML5のcanvas要素に2次元の図形を描画します

目次
  1. 図形の描画
  2. スタイルと色の適用
  3. アニメーション
    1. requestAnimationFrameについて
    2. Canvasに描画した図形のアニメーション
  4. マウスイベント
  5. 円と円の距離

図形の描画

スクリプトで描画コンテキストを取得します
*width属性とheight属性の初期値キャンバスは幅300ピクセル、高さ150ピクセル

<canvas style="border:solid 1px #ccc;"></canvas>
const canvas = document.querySelector("canvas");
const c = canvas.getContext("2d");

キャンバスのサイズをウィンドウのサイズにする

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

四角形の描画

  • fillRect(x, y, width, height)
    塗りつぶされた四角形を描きます
  • strokeRect(x, y, width, height)
    四角形の輪郭を描きます。
  • clearRect(x, y, width, height)
    指定された領域を消去し透明にします
const canvas = document.querySelector('canvas')
const c = canvas1.getContext("2d");
c.fillRect(10, 50, 50, 50)
c.strokeRect(110, 50, 50, 50)
c.fillRect(210, 50, 50, 50)
c.clearRect(220, 60, 30, 30)

パスを描く
beginPath():新しいパスを作成
moveTo(x, y): 始点を配置
lineTo(x, y): xと yで指定した位置に直線を描く
closePath():直線をパスに追加し現在のサブパスの開始地点につなぐ
パスを輪郭を描く(線を引く):stroke()
パスの内部を塗りつぶし:fill()

const canvas = document.querySelector('canvas')
const c = canvas1.getContext("2d");
 // 塗りつぶした三角形
 c.beginPath();
 c.moveTo(25, 25);
 c.lineTo(105, 25);
 c.lineTo(25, 105);
 c.fill();
 // 輪郭の三角形
 c.beginPath();
 c.moveTo(125, 125);
 c.lineTo(125, 45);
 c.lineTo(45, 125);
 c.closePath();
 c.stroke();

円と円弧(円周上の二点によって区切られた円周の部分)
arc(x, y, radius, startAngle, endAngle, true)
x, y:中心点
radius :半径
startAngle :開始角度
endAngle :終了角度
true:defaultで時計回り(falseは時計と反対)
startAngleが0endAngleがMath.PI * 2で円になる

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
 c.beginPath();
 c.arc(100, 75, 50, 0, Math.PI * 2)
 c.fill();
 c.beginPath();
 c.arc(220, 75, 50, 0, Math.PI * 2)
 c.stroke();

角度はラジアンで計算されます
度からラジアンへの変換:radians = (Math.PI/180)*degrees

ベジェ曲線
quadraticCurveTo(cp1x, cp1y, x, y)
x および y で指定した終端へ、 cp1x および cp1y で指定した制御点を使用して二次ベジェ曲線
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
x および y で指定した終端へ、(cp1x, cp1y) および (cp2x, cp2y) で指定した制御点を使用して三次ベジェ曲線

Path2DでSVG パスデータを使用してキャンバスのパスを初期化

const canvas = document.querySelector('canvas')
const c = canvas.getContext('2d');

let p = new Path2D('M10,30 A20,20,0,0,1,50,30 A20,20,0,0,1,90,30 Q90,60,50,90 Q10,60,10,30 Z');
c.stroke(p);

テキストの描画
fillText(text, x, y [, maxWidth])
x,yで指定した位置にテキストを塗りつぶして描画(任意で最大描画幅を指定)
strokeText(text, x, y [, maxWidth])
x,yで指定した位置にテキストの輪郭を描画(任意で最大描画幅を指定)

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
c.font = '48px serif';
c.fillText('Canvas', 10, 50);
c.strokeText('Canvas', 10, 125);
  • font = テキストのスタイル
    既定のフォントは10px sans-serif
  • textAlign = start、end、left、right、centerを指定
    既定値は start
  • textBaseline = value top、hanging、middle、alphabetic、ideographic、bottomを指定
    既定値は alphabetic
  • direction = ltr、rtl、inherit(書字方向)
    既定値は inherit
//canvasの中央
const canvas = document.querySelector('canvas')
const c = canvas15.getContext("2d");
c.font = '32px serif';
c.textAlign ='center';
c.textBaseline ='middle';
c.fillText('Canvas', canvas.width/2, canvas.height/2);

備考(便利な関数)

テキストは最大幅に収まるようにラップして表示する

<canvas style="width:100%"></canvas>
<script>
//最大幅に収まる長さのテキストを配列に格納する関数
function createTextArray(c, text, maxWidth) {
    const textArray = []
    const wordsArray = Array.from(text); //1文字ずつ格納配列にする
    const results = wordsArray.reduce((accu, word) => {
        // measureText()テキストの描画幅がmaxWidthより大きいとき
     if (c.measureText(accu).width > maxWidth && accu.includes("")) {
          //空白があれば空白で区切る
        let words = accu.split(" ");
        accu = words[words.length - 1]; //最後の空白以降の文字
        words.pop();
        if (words.join().replace(/,/g, " ") !== "") {
            textArray.push(words.join().replace(/,/g, " "));
            }
        }
     if (c.measureText(accu).width > maxWidth) {
         textArray.push(accu); //textArrayに格納して
        accu = ""; //空にする
       }
     return accu + word;
    }, '');
    textArray.push(results);
    return textArray;
}
//テキストの配列を描画する関数
function drawTextArray(c, array, textX, textY, lineHeight) {
     array.forEach((el, index) => {
       c.fillText(
         el,
         textX,
         textY -
           (lineHeight * array.length) / 2 +
           index * lineHeight +
           lineHeight / 2
       );
     });
}
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
const w = canvas.clientWidth
canvas.width = w;
canvas.height = 200;
c.font = '32px serif';
c.textAlign ='center';
c.textBaseline ='middle';
const textArray = createTextArray(c, 'HAPPY BIRTHDAY TO YOU CONGRATS!!', canvas.width*0.7)
drawTextArray(c, textArray, canvas.width/2, canvas.height/2, 32*1.2)
</script>

画像を貼り付ける
ctx.drawImage(image, dx, dy)
image:Image, Canvas, Videoのいずれかの要素
dx, dy:位置を指定(canvasの左上が0)

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");

const img = new Image();
img.src = "/image/....";  // 画像のURLを指定
img.onload = () => {
    c.drawImage(img, 0, 0);
  };

元画像を縮小・拡大してから貼り付ける
ctx.drawImage(image, dx, dy, dWidth, dHeight)
dWidth, dHeight:横幅、高さ
*アスペクト比が異なると歪む

 img.onload = () => {
    c.drawImage(img2, 0, 0, 300, 150);
  };

 img.addEventListener('load', () => {
    c.drawImage(img2, 0, 0, 300, 150)
 })

画像を切り抜く
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
image から左上の隅が (sx, sy)、幅と高さが sWidth および sHeight 領域を取得して
dx, dyで示した位置に配置してdWidthとdHeight で指定したサイズに拡大縮小

画像をbase64エンコードしたデータを読み込むこともできます

var img = new Image();  
img.src = 'data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn....';

スタイルと色の適用

色と透明度

  • fillStyle = color
    図形を塗りつぶす色
  • strokeStyle = color
    図形の輪郭の色
  • globalAlpha = 透明度
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
const colors = [
'#B1B2FF',
'#AAC4FF',
'#D2DAFF',
'#EEF1FF',
'#F9F9F9'
]
for(let i = 0; i <20; i++){
 let x = Math.random() * 300
 let y = Math.random() * 150
 c.beginPath();
 c.arc(x, y, 30, 0, Math.PI * 2)
 c.fillStyle = `hsl( ${ i * Math.floor(60/20) + 180}, 50% ,50%)`;
 //colorsの配列からランダムに選ぶとき
 // c.fillStyle = colors[Math.floor(Math.random()*colors.length)];
 c.fill();
}

RGBとHSLについて
*同系色や明暗をランダムに作りたいときはH(色相)を使います

  • RGBは赤・緑・青
    黒:0,0,0
    白:255,255,255
    赤:255,0,0
    緑:0,255,0
    青:0,0,255
  • HSLは色相(Hue)・彩度(Saturation)・輝度(Lightness)
    Hue:360度の角度で指定
    0(360)は赤・90は緑・180は青・270は紫
    Saturation:彩度(0%〜100%)で指定
    *0は灰色 100は純色
    Lightness:明度(0%〜100%)で指定
    *0は黒 50は原色 100は白

線のスタイル

  • lineWidth = 線の幅
  • lineCap=線の端点
    • butt:四角く切り落す
    • round:丸める
    • square:線の太さと同じ幅と半分の高さのボックスを追加して四角くする
  • lineJoin=2つ区間の結合
    • round:形状の角を丸くします(角の半径は線幅の半分と同じ)
    • bevel:三角形の領域を追加して埋める
    • miter:外側のエッジを延長し接続

save():canvasの描画情報を保存
restore():canvasの描画情報を復元
setTransform(伸縮x, 傾斜y, 傾斜x, 伸縮y, 移動x, 移動y):変形する

<canvas id="cv"></canvas>
<button type="button" onclick="restoreText()">復元</button>
<script>
  const ctx = document.getElementById("cv").getContext("2d");
  ctx.fillStyle ="pink";
  ctx.strokeStyle = "black";
  ctx.font = "32px Arial";
  ctx.textBaseline = "top";
for ( let i = 0; i < 3 ; i++ ){
  ctx.setTransform(1, 0, 0, 1, 0, 50*i);
  ctx.fillRect(0,0, 300, 40);
  ctx.save(); 
}
function restoreText(){
  ctx.restore();
  ctx.strokeText("復元", 40, 5); 
}  
</script>

アニメーション

requestAnimationFrameについて

requestAnimationFrame(callback)
引数に実行したい関数を渡すとその関数がブラウザにとって最適なタイミングで処理される
描画のタイミングに合わせて指定したコールバック関数を実行
*60FPS(FPSは1秒間の動画が何枚の画像で構成されているかを示すの単位)がデフォルト
60FPSの場合1000ms ÷ 60 = 16.6666… msに1回指定したコールバック関数が実行される

function draw() {
 window.requestAnimationFrame(draw);
}
draw()

requestAnimationFrameを止める
cancelAnimationFrame(id)

<div style="border:solid 1px #ccc;">
 <h1 id="countup" style="text-align:center;">1</h1>
 <button id="start-btn" type="button"">スタート</button>
</div>
<script>
const startBtn = document.querySelector('#start-btn')
let i = 0;
const loop = function() {
  let id = window.requestAnimationFrame(loop);
  const.textContent = i;
  i++;
 if (i === 100){ 
    window.cancelAnimationFrame(id)
   i = 0
  };
}
loop();
startBtn.addEventListener("click", loop)
</script>

1

タイムスタンプ(呼び出された時点の時刻)を引数に受け取れます
*経過時間だけアニメーションしたい場合などにタイムスタンプを引数にします
経過時間(elapsed):最初のタイムスタンプの差分
duration:アニメーションの開始からの経過時間(ミリ秒)
0.1 * elapsed
ついでにw1秒間に何回コールバック関数が呼び出されたかを表示
1秒間(1000 ミリ秒)動作の例

<p>経過時間 : <span id="elapsed"></span></p>
<p>duration : <span id="shaping"></p>
<p>コールバック回数 : <span id="fps"></p><
<button type="button" id="play">スタート</button>
<script>
let start;
let fps, prevTimestamap; //FPS用
let frameCount = 0;//FPS用
const e_val = document.getElementById('elapsed');
const s_val = document.getElementById('shaping');
const f_val = document.getElementById('fps');
let clearId
function count (timestamp){
  if (start === undefined) {
    start = timestamp;
  }
  let fps = prevTimestamap ? Math.floor((timestamp - prevTimestamap)*10000)/10000 : 0;
  prevTimestamap = timestamp; //FPS用
  frameCount++; //FPS用

  let elapsed = start ? timestamp - start : 0;
  let x = Math.min(0.1 * elapsed, 100);
  s_val.textContent = x;
  e_val.textContent = elapsed;
  f_val.textContent = fps
//コールバック回数の計算用
 if(elapsed > 1000) {
    fps = frameCount;
    frameCount = 0;
    start = timestamp;
  }
//1秒間だけ 経過時間とduration
  if (elapsed < 1000) { 
    clearID = window.requestAnimationFrame(count);
  } else{
    window.cancelAnimationFrame(clearID)
  }
}
const play =  document.getElementById('play');
play.addEventListener('click', (e)=> {
  start = undefined;
  frameCount = 0;
  count();
});
</script>

経過時間 :

duration :

コールバック回数 :

Canvasに描画した図形のアニメーション

アニメーションさせる場合にも移動する部分と以前に描いた部分をすべて再描画する必要があります

横(x軸)を移動する
clearRect()で以前に描画した図形をすべてクリアする必要があります

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
let x = 50
let dx = 2 // 速度(大きくすると速度が速くなる)
let radius = 30 //半径
function move() {
  const id= window.requestAnimationFrame(move);
  c.clearRect(0, 0, 300, 150);
  c.beginPath();
  c.arc(x, 75, 30, 0, Math.PI * 2);
  c.fillStyle = 'blue';
  c.fill();
  x += dx
 if(x+radius > 300 || x-radius < 0){
     dx = -dx
  }
 if (x-radius < 0) window.cancelAnimationFrame(id);
}

円をクラスにしてxとy方向への動きをつける

<canvas style="width:100%;"></canvas>
<div style="display:flex;">
   <button id="start-btn" type="button">スタート</button>
   <button id="stop-btn" type="button">ストップ</button>
</div>
const startBtn = document.querySelector('#start-btn')
const stopBtn = document.querySelector('#stop-btn')
const canvas = document.querySelector('canvas')
//幅を取得
const w = canvas.clientWidth
canvas.width=w;
canvas.height=200; //高さ200px
const c = canvas.getContext("2d");
//円を作成
class Circle{
constructor(x, y, dx, dy, radius){
  this.x=x;
  this.y=y;
  this.dx = dx;
  this.dy = dy;
  this.radius= radius;
}
draw(){
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
  c.fillStyle = 'blue';
  c.fill();
 }
move(){
 if(this.x+this.radius > canvas.width || this.x-this.radius < 0){
     this.dx = -this.dx
  }
  if(this.y+this.radius > canvas.height || this.y-this.radius < 0){
     this.dy = -this.dy
  }
   this.x += this.dx;
   this.y += this.dy;
   this.draw()
 }
}
//円の初期化
const circle = new Circle(20, 20, 3, 3,10)
let id;
//アニメーションする
function animation(){
 id= window.requestAnimationFrame(animation);
 c.clearRect(0, 0, canvas.width, canvas.height);
 circle.move();
}
//スタート
startBtn.addEventListener("click", animation)
//ストップ
stopBtn.addEventListener("click", function(e) {
  window.cancelAnimationFrame(id);
});

円の色をランダムに変更
複数の円インスタンスを描画する

const colors = [
'#B1B2FF',
'#AAC4FF',
'#D2DAFF',
'#EEF1FF',
'#F9F9F9'
]
class Circle{
constructor(x, y, dx, dy, radius){
 //省略
  this.color = colors[Math.floor(Math.random()*colors.length)];
}
draw(){
//省略
  c.strokeStyle = this.color;
  c.stroke();
 }
move(){
 //省略
 }
}
//インスタンスを格納する配列
const circleArray =[];
for(let i = 0; i < 100; i++){
   let radius = 20
   let x = Math.random() * (w - radius*2) + radius
   let y = Math.random() * (200 - radius*2)+ radius
   let dx = Math.random() 
   let dy = Math.random()  
   circleArray.push(new Circle2(x, y, dx, dy, radius))
}
function animation(){
 id = window.requestAnimationFrame(animation);
 c.clearRect(0, 0, canvas.width, canvas.height);
 circleArray.forEach(circle=>{
   circle.move();
 })
}

リサイズ時に対応する

let circleArray =[];
//図形オブジェクトを格納(初期化)
function init(){
 circleArray =[]
 for(let i = 0; i < 100; i++){
   let radius = 20
   let x = Math.random() * (w - radius*2) + radius
   let y = Math.random() * (200 - radius*2)+ radius
   let dx = Math.random() 
   let dy = Math.random()  
   circleArray.push(new Circle2(x, y, dx, dy, radius))
  }
}
init()
function animation(){
  id = window.requestAnimationFrame(animation);
  c.clearRect(0, 0, w, 200);
  circleArray.forEach(circle=>{
   circle.move();
  })
 }
//リサイズ時
window.addEventListener('resize', function(){
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  init() //図形オブジェクト格納を初期化する
})

縦(Y軸)方向に重力を表現する

move(){
  if(this.y+this.radius + this.dy > canvas.height){
     this.dy = -this.dy * 0.95
   } else{
    this.dy += 1
   }
  this.y += this.dy;
 }

便利な関数
minとmaxのあいだでランダムな数を生成
*注意「0 〜 10」であれば「11」が最大値

function randomRange(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min)
}

コードを見る

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>デモページ</title>
    <style>
    body {
        margin: 0;
        overflow: hidden;
        background: #3e4444;
    }
    </style>
</head>
<body>
<canvas></canvas>
<script>
const colors = [
'#B1B2FF',
'#AAC4FF',
'#D2DAFF',
'#EEF1FF',
'#F9F9F9'
]
const canvas = document.querySelector('canvas')
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext("2d");
function randomRange(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min)
}
class Circle{
constructor(x, y, dx, dy, radius){
  this.x=x;
  this.y=y;
  this.dx = dx;
  this.dy = dy;
  this.radius= radius;
  this.color = colors[Math.floor(Math.random()*colors.length)];
}
draw(){
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
  c.fillStyle = this.color;;
  c.fill();
 }
move(){
if(this.x + this.radius > canvas.width || this.x - this.radius < 0){
     this.dx = -this.dx
  }
   if(this.y + this.radius + this.dy > canvas.height){
     this.dy = -this.dy * 0.95
   } else{
    this.dy += 1
   }
   this.x += this.dx;
   this.y += this.dy;
   this.draw()
 }
}
let circleArray =[];
function init(){
circleArray =[];
for(let i = 0; i < 200; i++){
   let radius = randomRange(3, 30)
   let x = Math.random() * (canvas.width - radius*2) + radius
   let y = Math.random() * (canvas.height - radius*2)+ radius
   let dx = Math.random()*0.5
   let dy = Math.random()*0.5
   circleArray.push(new Circle(x, y, dx, dy, radius))
  }
}
init();
function animation(){
 window.requestAnimationFrame(animation);
 c.clearRect(0, 0, canvas.width, canvas.height);
 circleArray.forEach(circle=>{
   circle.move();
 })
}
animation()
window.addEventListener('resize', function(){
 canvas.width = window.innerWidth;
 canvas.height = window.innerHeight;
 init()
})
</script>
</body>
</html>

マウスイベント

マウスイベント備忘録

  • mousedown :ボタンが押されたとき
  • mouseup :ボタンが離されたとき
  • mouseover :ポインタが要素に乗ったとき
  • mouseout: ポインタが要素から離れたとき
  • mouseenter: ポインタが要素に乗ったとき( バブリングなし)
  • mouseleave :ポインタが要素から離れたとき (バブリングなし)
  • mousemove :ポインタが要素の上を移動したとき
  • offsetX / Y
    要素内での座標(マウスが載っている要素の左上を原点とした座標)を取得
  • clientX / Y
    ウィンドウ内でのカーソル座標を取得(ウィンドウの左上が (0, 0))

マウスでお絵描き

<canvas id="myCanvas" style="border:solid 1px #ccc;"></canvas>
<script>
let isDrawing = false;
let x = 0;
let y = 0;
const myCanvas = document.getElementById('myCanvas');
const c = myCanvas.getContext('2d');
function drawLine(c, x1, y1, x2, y2) {
  c.beginPath();
  c.strokeStyle = 'black';
  c.lineWidth = 1;
  c.moveTo(x1, y1);
  c.lineTo(x2, y2);
  c.stroke();
  c.closePath();
}
myCanvas.addEventListener('mousedown', e => {
  x = e.offsetX;
  y = e.offsetY;
  isDrawing = true;
});
myCanvas.addEventListener('mousemove', e => {
  if (isDrawing === true) {
    drawLine(c, x, y, e.offsetX, e.offsetY);
    x = e.offsetX;
    y = e.offsetY;
  }
});
window.addEventListener('mouseup', e => {
  if (isDrawing === true) {
    drawLine(c, x, y, e.offsetX, e.offsetY);
    x = 0;
    y = 0;
    isDrawing = false;
  }
});
</script>

円と円の距離

備考(便利な関数)

ピタゴラスの定理を利用して2つの円の距離円の中心からの距離)を計算する

function distance(x1, y1, x2, y2) {
  const xDist = x2 - x1
  const yDist = y2 - y1
  return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2))
}

マウスに追随する円が円弧に接触したら緑色に変化する
*2つの円の距離が2つの円の半径を足した長さより短くなれば接触している

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
function distance(x1, y1, x2, y2) {
  const xDist = x2 - x1
  const yDist = y2 - y1
  return Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2))
}
const radius1 = 10;
const radius2 = 30;
const mouse = {
  x:10,
  y:10
}
 c.beginPath();
 c.arc(220, 75, radius2, 0, Math.PI * 2)
 c.stroke();
canvas.addEventListener('mousemove', (e) => {
  mouse.x = e.offsetX
  mouse.y = e.offsetY
  c.clearRect(0, 0, 300, 150)
  c.beginPath();
  c.arc(mouse.x, mouse.y, radius1, 0, Math.PI * 2)
  c.fill();
  c.fillStyle='red';
  c.beginPath();
  c.arc(220, 75, radius2, 0, Math.PI * 2)
  c.stroke();
  if(distance(mouse.x, mouse.y, 220, 75) < radius1+radius2){
    c.fillStyle='green';
  }  
})

衝突した時に角度を変更して回避するための関数(便利な関数)
https://www.youtube.com/watchv=789weryntzM&list=PLpPnRKq7eNW3We9VdCfx9fprhqXHwTPXL&index=7(ありがとうございます💓)

function rotate(velocity, angle) {
    const rotatedVelocities = {
        x: velocity.x * Math.cos(angle) - velocity.y * Math.sin(angle),
        y: velocity.x * Math.sin(angle) + velocity.y * Math.cos(angle)
    };
    return rotatedVelocities;
}

function resolveCollision(particle, otherParticle) {
    const xVelocityDiff = particle.velocity.x - otherParticle.velocity.x;
    const yVelocityDiff = particle.velocity.y - otherParticle.velocity.y;
    const xDist = otherParticle.x - particle.x;
    const yDist = otherParticle.y - particle.y;
    if (xVelocityDiff * xDist + yVelocityDiff * yDist >= 0) {
        const angle = - Math.atan2(otherParticle.y - particle.y, otherParticle.x - particle.x);
        const m1 = particle.mass;
        const m2 = otherParticle.mass;
        const u1 = rotate(particle.velocity, angle);
        const u2 = rotate(otherParticle.velocity, angle);
        const v1 = { x: u1.x * (m1 - m2) / (m1 + m2) + u2.x * 2 * m2 / (m1 + m2), y: u1.y };
        const v2 = { x: u2.x * (m1 - m2) / (m1 + m2) + u1.x * 2 * m2 / (m1 + m2), y: u2.y };
        const vFinal1 = rotate(v1, -angle);
        const vFinal2 = rotate(v2, -angle);
        particle.velocity.x = vFinal1.x;
        particle.velocity.y = vFinal1.y;
        otherParticle.velocity.x = vFinal2.x;
        otherParticle.velocity.y = vFinal2.y;
    }
}

コードを見る

<div id="canvas-box" style="width:100%;">
<canvas></canvas>
</div>
const canvas = document.querySelector('canvas')
const w = document.querySelector('#canvas-box').clientWidth
canvas.width = w;
canvas.height = 200;
const c = canvas.getContext("2d");
const colors = [
'#B1B2FF',
'#AAC4FF',
'#D2DAFF',
'#EEF1FF'
]
//minとmaxの間でランダムな数を生成
function randomRange(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min)
}
class Partcles{
 constructor(x, y, radius){
   this.x = x;
   this.y = y;
   this.mass = 1; //resolveCollision関数で必要
   this.velocity = {
    x: Math.random(),
    y: Math.random()
   }; //resolveCollision関数ように(dx dy)はvelocityオブジェクトに
   this.radius= radius;
   this.color = colors[Math.floor(Math.random()*colors.length)];
  }
 draw(){
   c.beginPath();
   c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
   c.fillStyle = this.color;;
   c.fill();
  }
 update(lists){
    this.draw()
   //衝突した時の修正
    for(let i = 0; i < lists.length; i++){
     if(this===lists[i]) continue;
     if (distance(this.x, this.y, lists[i].x, lists[i].y) - this.radius*2 < 0){
      resolveCollision(this, lists[i])
     }
   }
  //横枠の範囲
  if(this.x + this.radius > canvas.width || this.x - this.radius < 0){
     this.velocity.x = -this.velocity.x
   }
  //縦枠の範囲
   if(this.y + this.radius > canvas.height || this.y - this.radius < 0){
     this.velocity.y = -this.velocity.y
   }
  //動き
   this.x += this.velocity.x;
   this.y += this.velocity.y;
  }
}

let partcles =[];
function init(){
  partcles =[];
  for(let i = 0; i < 10; i++){
    let radius = 30
    let x = randomRange(radius, canvas.width - radius )
    let y = randomRange(radius, canvas.height - radius)
    //円オブジェクト生成時のxとyが衝突しないようにする
    if(i !== 0){
     for(let j = 0; j < partcles.length; j++){
       if(distance(x, y, partcles[j].x,  partcles[j].y) - radius*2 < 0){
         x = randomRange(radius, canvas.width - radius )
         y = randomRange(radius, canvas.height - radius)
         j = -1;
        }
      }
     }
   partcles.push(new Partcles(x, y, radius))
  }
}

function animation(){
  window.requestAnimationFrame(animation);
  c.clearRect(0, 0, canvas.width, canvas.height);
  partcles.forEach(partcle=>{
    partcle.update(partcles);
  })
 }
init();
animation();
//画面リサイズ時の対応
window.addEventListener('resize', function(){
  canvas.width=w;
  canvas.height=200;
  init()
})