
이번 글에서 다룰 내용
- 브라우저 창에 마우스 호버하면(mouseover) 슬로우모션으로 애니메이션 실행
 - 마우스로 움직이는 공 잡아서 드래그
 
- mousedown, mousemove, mouseup, mouseout
 
mouseover 로 호버했을 때 공들의 속도를 늦추기mouseout 으로 마우스가 브라우저 밖으로 나갔을 때 속도 원상복귀//app.js
constructor(){
  ...
  this.slowDown();
}
...
slowDown() {
  this.canvas.addEventListener('mouseover', () => {
    this.balls.forEach((ball) => {
      ball.vx /= 20;
      ball.vy /= 20;
    });
  });
  this.canvas.addEventListener('mouseout', () => {
    this.balls.forEach((ball) => {
      ball.vx *= 20;
      ball.vy *= 20;
    });
  });
}
- 마우스로 움직이는 공을 클릭
 - 클릭한 공은 마우스가 움직이는 대로 드래그될 것
 - 공을 놓는 순간 원래 움직이던 방향으로 이어서 이동
 
//ball.js
...
draw() {
  this.interaction();
}
interaction() {
  this.canvas = document.getElementById('canvas');
  
  this.canvas.addEventListener('mousedown', this.onMouseDown);
  this.canvas.addEventListener('mouseup', this.onMouseUp);
  this.canvas.addEventListener('mouseout', this.onMouseOut);
}
onMouseDown = (e) => {
  console.log("mouse down");
}
onMouseUp = (e) => {
  console.log("mouse up");
}
onMouseOut = (e) => {
  console.log("mouse out");
}
//ball.js
mousedown() {
    this.offsetX = e.clientX - this.x;
    this.offsetY = e.clientY - this.y;
    if (
      Math.abs(this.offsetX) <= this.radius &&
      Math.abs(this.offsetY) <= this.radius
    ) {
      this.canvas.addEventListener('mousemove', this.onMouseMove);
    }
//ball.js
onMouseMove = (e) => {
  this.x = e.clientX - this.offsetX;
  this.y = e.clientY - this.offsetY;
  this.vx = 0;
  this.vy = 0;
};
//ball.js
constructor(stageWidth, stageHeight, radius, speedX, speedY, src) {
  ...
  this.speedX = speedX;
  this.speedY = speedY;
  this.vx = this.speedX;
  this.vy = this.speedY;
  ...
}
//ball.js
onMouseUp = (e) => {
  this.vx = this.speedX / 20;
  this.vy = this.speedY / 20;
  this.canvas.removeEventListener('mousedown', this.onMouseDown);
  this.canvas.removeEventListener('mousemove', this.onMouseMove);
};


🥊 따라서 mouseout 설정과 기존 방향을 기억하는 기능이 필요
//ball.js
onMouseOut = (e) => {
  this.vx = this.speedX;
  this.vy = this.speedY;
  this.canvas.removeEventListener('mousedown', this.onMouseDown);
  this.canvas.removeEventListener('mousemove', this.onMouseMove);
};
//ball.js
constructor() {
  ...
  this.vx_minus = this.speedX < 0 ? -1 : 1;
  this.vy_minus = this.speedY < 0 ? -1 : 1;
  ...
}
//ball.js
...
setMinus = () => {
  this.vx_minus = this.vx < 0 ? -1 : 1;
  this.vy_minus = this.vy < 0 ? -1 : 1;
};
//ball.js
onMouseDown = (e) => {
  this.setMinus();
  ...
}
//ball.js
applyMinus = () => {
  if (this.vx_minus * this.speedX < 0) {
    this.vx *= -1;
  }
  if (this.vy_minus * this.speedY < 0) {
    this.vy *= -1;
  }
};
//ball.js
onMouseUp = (e) => {
  //vx, vy 슬로모션 속도로 원상복귀
  this.applyMinus();
  //이벤트 제거
}
onMouseOut = (e) => {
  //vx, vy 속도 원상복귀
  this.applyMinus();
  //이벤트 제거
}
//app.js
import Ball from './velogballs.js';
import {logos} from './consts.js';
class App {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.canvas.setAttribute('id', 'canvas');
    this.ctx = this.canvas.getContext('2d');
    document.body.appendChild(this.canvas);
    window.addEventListener('resize', this.resize.bind(this), false);
    this.resize();
    this.balls = [];
    this.createBall();
    this.slowDown();
    this.animate();
  }
  resize() {
    this.stageWidth = document.body.clientWidth;
    this.stageHeight = document.body.clientHeight;
    this.canvas.width = this.stageWidth * 2;
    this.canvas.height = this.stageHeight * 2;
    this.ctx.scale(2, 2);
  }
  slowDown() {
    this.canvas.addEventListener('mouseover', () => {
      this.balls.forEach((ball) => {
        ball.vx /= 20;
        ball.vy /= 20;
      });
    });
    this.canvas.addEventListener('mouseout', () => {
      this.balls.forEach((ball) => {
        ball.vx *= 20;
        ball.vy *= 20;
      });
    });
  }
  createBall() {
    for (let i = 0; i < logos.length; i++) {
      let radius = Math.ceil(Math.random() * 30) + 10;
      let speedX = Math.ceil(Math.random() * 50) + 5;
      let speedY = Math.ceil(Math.random() * 50) + 5;
      let signX = speedX % 2 === 0 ? -1 : 1;
      let signY = speedY % 2 === 0 ? -1 : 1;
      this.balls.push(
        new Ball(
          this.stageWidth,
          this.stageHeight,
          radius,
          speedX * signX,
          speedY * signY,
          `./srcs/${logos[i]}.png`
        )
      );
    }
  }
  animate() {
    window.requestAnimationFrame(this.animate.bind(this));
    this.ctx.clearRect(0, 0, this.stageWidth, this.stageHeight);
    this.balls.forEach((ball) => {
      ball.draw(this.ctx, this.stageWidth, this.stageHeight);
    });
  }
}
new App();
//ball.js
export default class Ball {
  constructor(stageWidth, stageHeight, radius, speedX, speedY, src) {
    this.img = new Image();
    this.radius = radius;
    this.diameter = this.radius * 2;
    this.speedX = speedX;
    this.speedY = speedY;
    this.vx_minus = this.speedX < 0 ? -1 : 1;
    this.vy_minus = this.speedY < 0 ? -1 : 1;
    this.vx = this.speedX;
    this.vy = this.speedY;
    this.src = src;
    this.x = this.radius + Math.random() * (stageWidth - this.diameter);
    this.y = this.radius + Math.random() * (stageHeight - this.diameter);
  }
  draw(ctx, stageWidth, stageHeight) {
    this.interaction();
    this.x += this.vx;
    this.y += this.vy;
    this.bounceWindow(stageWidth, stageHeight);
    ctx.save();
    ctx.beginPath();
    ctx.fillStyle = 'white';
    ctx.shadowColor = '#dee2e6';
    ctx.shadowBlur = 10;
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 3;
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.fill();
    ctx.closePath();
    ctx.clip();
    ctx.drawImage(
      this.img,
      this.x - this.radius,
      this.y - this.radius,
      this.diameter,
      this.diameter
    );
    this.img.src = this.src;
    ctx.restore();
  }
  bounceWindow(stageWidth, stageHeight) {
    if (this.x <= this.radius || this.x >= stageWidth - this.radius) {
      this.vx *= -1;
      this.x += this.vx;
    }
    if (this.y <= this.radius || this.y >= stageHeight - this.radius) {
      this.vy *= -1;
      this.y += this.vy;
    }
  }
  interaction() {
    this.canvas = document.getElementById('canvas');
    this.canvas.addEventListener('mousedown', this.onMouseDown);
    this.canvas.addEventListener('mouseup', this.onMouseUp);
    this.canvas.addEventListener('mouseout', this.onMouseOut);
  }
  setMinus = () => {
    this.vx_minus = this.vx < 0 ? -1 : 1;
    this.vy_minus = this.vy < 0 ? -1 : 1;
  };
  applyMinus = () => {
    if (this.vx_minus * this.speedX < 0) {
      this.vx *= -1;
    }
    if (this.vy_minus * this.speedY < 0) {
      this.vy *= -1;
    }
  };
  onMouseDown = (e) => {
    this.setMinus();
    this.offsetX = e.clientX - this.x;
    this.offsetY = e.clientY - this.y;
    if (
      Math.abs(this.offsetX) <= this.radius &&
      Math.abs(this.offsetY) <= this.radius
    ) {
      this.canvas.addEventListener('mousemove', this.onMouseMove);
    }
  };
  onMouseMove = (e) => {
    this.x = e.clientX - this.offsetX;
    this.y = e.clientY - this.offsetY;
    this.vx = 0;
    this.vy = 0;
  };
  onMouseUp = (e) => {
    this.vx = this.speedX / 20;
    this.vy = this.speedY / 20;
    this.applyMinus();
    this.canvas.removeEventListener('mousedown', this.onMouseDown);
    this.canvas.removeEventListener('mousemove', this.onMouseMove);
  };
  onMouseOut = (e) => {
    this.vx = this.speedX;
    this.vy = this.speedY;
    this.applyMinus();
    this.canvas.removeEventListener('mousedown', this.onMouseDown);
    this.canvas.removeEventListener('mousemove', this.onMouseMove);
  };
}
//consts.js
export const logos = [
  'c',
  'csharp',
  'c++',
  'java',
  'js',
  'mongoDB',
  'MySQL',
  'oracle3',
  'php',
  'PostgreSQL',
  'python',
  'r',
  'SQLite',
  'SQLServer',
  'angular',
  'backbone',
  'django',
  'ember',
  'flask',
  'laravel',
  'node',
  'preact',
  'rails',
  'react',
  'spring',
  'svelte',
  'vue',
];
/*
  stylesheet.css
*/
html {
  width: 100%;
  height: 100%;
}
body {
  width: 100%;
  height: 100%;
}
canvas {
  width: 100%;
  height: 100%;
}