Canvas API備忘録 (2)

目次
  1. sinとcos
  2. CSSのfilterを利用する
  3. 基本のテンプレート
  4. Canvasを回転させる

sinとcos

波線を描く

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
c.beginPath();
c.moveTo(0, canvas.height/2)
//1pxごとにlineTo
for(let i = 0; i < canvas.width; i++){
  c.lineTo(i, canvas.height/2 + Math.sin(i * 0.05) * 50)
}
c.stroke()

Math.sin(radians)を使って波を動かし、色を変化させる(「-1」~「1」の間の数)
*clearRectのかわりにfillRectでcanvasをクリア(半透明に)する

const w = document.querySelector('#canvas-box').clientWidth
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
canvas.width = w;
canvas.height = 200;
const wave ={
  y:canvas.height/2,
  amplitude : 75, //振幅
  length: 0.02, //幅
  frequency:0.01 //周波数
}

let inc = wave.frequency;
function animation(){
 window.requestAnimationFrame(animation);
//背景は透明度のある黒
 c.fillStyle ='rgba(0,0,0,0.01)'
 c.fillRect(0, 0, canvas.width, canvas.height);
 c.beginPath();
 c.moveTo(0, wave.y)
 for(let i = 0; i < canvas.width; i++){
   c.lineTo(i, wave.y + Math.sin(i * wave.length + inc ) * wave.amplitude * Math.sin(inc))
 }
//Math.abs()で絶対値を取得(マイナス時の対策)
 c.strokeStyle=`hsl(${ Math.abs(Math.sin(inc)* 360)}, 50%, 50%)`
 c.stroke()
 inc += wave.frequency //加算する
}
animation()

Math.cos(radians)とMath.sin(radians)
x座標:cos(0) =1 cos(90) = 0
y座標:sin(0) = 0 sin(90) = 1
(1,0)は(cos(0), sin(0))
(0, 1)は(cos(90), sin(90))

<canvas width="300" height="300"></canvas>
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
let x = canvas.width/2
let y = 50
let radius = 10
//速度(数字が小さいと円が大きくなる)
let velocity = 0.05
//角度ラジアン
let radians = 0 
function move() {
  window.requestAnimationFrame(move);
  c.clearRect(0, 0, 300, 300);
  c.beginPath();
  c.arc(x, y, radius, 0, Math.PI * 2);
  c.fillStyle = 'blue';
  c.fill();
  radians += velocity;
  x = x + Math.cos(radians)* 5;
  y = y + Math.sin(radians)* 5;
}
move()

複数の円をMath.cos(radians)Math.sin(radians)を利用して動かす

const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d");
class Particle{
constructor(x, y, radius, color, velocity){
  this.x = x;
  this.y = y;
  this.radius= radius;
  this.color = color;
  this.velocity = velocity;
}
draw(){
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
  c.fillStyle = this.color;
  c.fill();
  c.closePath()
 }
update(){
   this.draw()
   this.x += this.velocity.x
   this.y += this.velocity.y
   if(this.x  > canvas.width || this.x < 0){
     this.velocity.x = -this.velocity.x
   }
   if(this.y  > canvas.height || this.y < 0){
     this.velocity.y = -this.velocity.y
   }
 }
}
const particleCount = 30
// 360度を円の数で割る
const angleInc = Math.PI * 2 / particleCount
let particles=[];
function init(){
  for(let i = 0; i < particleCount; i++){
   particles.push(new Particle(150, 150, 10, `hsl(${360/particleCount * i}, 100%, 50%)`,
     { x : Math.cos(angleInc * i), y : Math.sin(angleInc * i)}
    ))
  }
}

function animation(){
  window.requestAnimationFrame(animation);
  c.clearRect(0, 0, canvas.width, canvas.height);
  particles.forEach( particle=>{
    particle.update();
  })
}
init()
animation()

応用
花火を作る(ロード時とクリック時に表示される)
ランダムな粒子にして、大きく広がり最後に粒子を消す

コードを見る

<!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: black;
    }
    canvas{
     position: absolute;
     top:0;
     left: 0;
    }
    </style>
</head>
<body>
<canvas></canvas>
<script>
const canvas = document.querySelector('canvas')
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext("2d");

const gravity = 0.005 //重力
const power = 0.95 //広がり
class Particle{
//x,y中心点  ・粒子の半径・色・velocityはオブジェクト {x,y}
constructor(x, y, radius, color, velocity){
  this.x=x;
  this.y=y;
  this.radius= radius;
  this.color = color;
  this.velocity = velocity;
  this.alpha = 1 //
}
draw(){
  c.save() //陰影を消すため
  c.globalAlpha = this.alpha
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
  c.fillStyle = this.color;;
  c.fill();
  c.restore()//陰影を消すため
 }
update(){
   this.draw()
   this.velocity.x *= power //x方向の広がり
   this.velocity.y *= power //y方向の広がり
   this.velocity.y += gravity//y方向重力
   this.x += this.velocity.x
   this.y += this.velocity.y
   this.alpha -= 0.005 //粒子を消す為
 }
}
let particles;
function init(x,y){
particles =[];
const particleCount = 1000
const angleInc = Math.PI * 2 / particleCount
const power = 40
let px = x;
let py = y;
for(let i = 0; i < particleCount; i++){
   particles.push(new Particle(px, py, 3, `hsl(${Math.random()*360}, 100%, 50%)`,
    {
     x:Math.cos(angleInc * i) * Math.random()*power,
     y:Math.sin(angleInc * i) * Math.random()*power
    }
    ))
  }
}
function animation(){
 window.requestAnimationFrame(animation);
 c.fillStyle ='rgba(0,0,0,0.05)'
 c.fillRect(0, 0, canvas.width, canvas.height);
 particles.forEach( (particle, index)=>{
    if(particle.alpha > 0){
       particle.update();
    }else{
      particles.splice(index, 1) //粒子を消す
    }
 })
}
//ロード時は中心
init(canvas.width/2, canvas.height/2);
window.addEventListener('load', animation)
//クリック時
canvas.addEventListener('click', (e)=>{
    init(e.clientX, e.clientY,)
})
//リサイズ時
window.addEventListener('resize', ()=>{
 canvas.width = window.innerWidth;
 canvas.height = window.innerHeight;
 init(canvas.width/2, canvas.height/2);
})
</script>
</body>
</html>

CSSのfilterを利用する

スライムの動きはCSSでfilterを利用します
*canvasがblurでぼやけるので親要素にはbackground-colorとoverflow:hidden

<div style="background:black; width:300px; height:300px; overflow:hidden;" >
<canvas width="300" height="300"></canvas>
</div>
<style>
canvas{
  background:black;
  filter:blur(10px) contrast(20);
}
</style>
<script>
class Setting{
  constructor(width, height){
     this.instanceArray=[]
     this.width = width
     this.height = height
  }
  init(number){
   for(let i = 0; i < number; i++){
     this.instanceArray.push(new Ball(this))
    }
  }
  render(c){
   this.instanceArray.forEach(ball=>ball.update(c)) 
  }
}
class Ball{
  constructor(setting){
  this.setting = setting
  this.radius = Math.random()* 30 + 3
  this.x = this.x = this.radius *2 +(Math.random()*(this.setting.width -this.radius *4))
  this.y = this.radius*-1
  this.speedX = Math.random()*0.2-0.1
  this.speedY = Math.random()*0.5+0.2
  this.g =  Math.random()*0.005
  this.vy =0
  }
 update(c){
  if(this.x < this.radius || this.x > this.setting.width - this.radius) this.speedX *= -1
  if( this.y > this.setting.height - this.radius){
    this.radius = Math.random()* 30 + 3
    this.x = this.radius *2 +(Math.random()*(this.setting.width -this.radius *4))
    this.y = this.radius*-1
    this.vy=0
    this.speedY = Math.random()*0.5+0.2
  }
  if(this.y > this.radius*2){
    this.vy += this.g
    this.speedY +=this.vy
  }
  this.x += this.speedX
  this.y += this.speedY
  this.draw(c)
 }
 draw(c){
  c.beginPath()
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
  c.fill()
 }
}

const canvas = document.querySelector('canvas')
const c = canvas1.getContext("2d")
const setting = new Setting(canvas.width, canvas.height)
setting.init(15)

function animation(){
  c.clearRect(0, 0, canvas.width, canvas.height);
  setting.render(c)
  requestAnimationFrame(animation);
}
animation()
</script>

フィルターあり

フィルターなし

基本のテンプレート

初期化とレンダリング用のSettingクラスを作る
図形クラスのコンストラクタにSettingを渡す

class Setting{
  constructor(width, height){
     this.instanceArray=[]
     this.width = width
     this.height = height
  }
  init(number){
   for(let i = 0; i < number; i++){
     this.instanceArray.push(new Objects(this))
    }
  }
  draw(c){
   this.instanceArray.forEach(object=>object.draw(c)) 
  }
  render(c){
   this.instanceArray.forEach(object=>object.update(c)) 
  }
}
class Objects{
  constructor(setting){
  this.setting = setting
  this.radius = 50
  this.x = 100
  this.y = 100
  this.speedX = 1
  this.speedY = 1
  }
 update(c){
  this.draw(c)//今回はupdateでdrawメソッドを呼び出している
  this.x += this.speedX
  this.y += this.speedY
 }
 draw(c){
  c.beginPath()
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
  c.fill()
 }
}
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d")
canvas.width = window.innerWidth
canvas.height = window.innerHeight
const setting = new Setting(canvas.width, canvas.height)
function animation(){
  c.clearRect(0, 0, canvas.width, canvas.height)
  setting.render(c)
  requestAnimationFrame(animation)
}
setting.init(1)
animation()
//リサイズ
window.addEventListener("resize", () => {
  canvas.width = window.innerWidth
  canvas.height = window.innerHeight
  setting.init(1)
});

Canvasを回転させる

updateメソッドは不要

Canvasを回転させる
*粒子の移動はしないのでupdateメソッドは不要
save()で記録した時点をrestore()で復元する
rotate(radians)は左上が軸になるので
まずtranslate(canvas.width/2, canvas.height/2)で中心に
*描画範囲もズレるので、最初のxとyを四等分した左上の境界が最大値になるようにする
*画面の縦横比に対応するにはCanvasを大きめで星の数も多めに見積もる

const colors = [
'white',
'blue',
'yellow',
'orange',
]
class Setting{
  constructor(width, height){
     this.instanceArray=[]
     this.width = width
     this.height = height
  }
  init(number){
   for(let i = 0; i < number; i++){
   //四等分した左上の境界が最大値になる
     const x = (Math.random() * (this.width+100)) -  (this.width+100)/2 
     const y =( Math.random() * (this.height+100)) -  (this.width+100) /2
     const radius = Math.random() * 1
     const color = colors[Math.floor(Math.random()*colors.length)]
     this.instanceArray.push(new Objects(this, x, y, radius, color))
    }
  }
//今回はdrawだけでいい
  draw(c){
   this.instanceArray.forEach(object=>object.draw(c)) 
  }
}
class Objects{
  constructor(setting, x, y, radius, color){
  this.setting = setting
  this.radius = radius
  this.x = x
  this.y = y
  this.color = color
  }
//update不要
 draw(c){
  c.beginPath()
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2)
  c.shadowColor = this.color //線や図形に影を付ける
  c.shadowBlur = 50
  c.fillStyle=this.color
  c.fill()
 }
}
const canvas = document.querySelector('canvas')
const c = canvas.getContext("2d")
const setting = new Setting(canvas.width, canvas.height)
let radians = 0
//Canvasを回転させる
function animation(){
  c.fillStyle='rgba(10,10,10,1)' //rgba(10,10,10,0.03)陰影が残る
  c.fillRect(0, 0, canvas.width, canvas.height)
  c.save()
  c.translate(canvas.width/2, canvas.height/2) 
 c.rotate(radians)
  setting.draw(c)
  c.restore()
  radians += 0.005
 requestAnimationFrame(animation)
}
setting.init(400)
animation()

rgba(10,10,10,1)

rgba(10,10,10,0.03)