Billiard 03
당구공의 속도가 자연스럽게 감쇠하는 시뮬레이션
테스트 환경: HTML5, Eclipse, Google Chrome, Javascript
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>Canvas를 이용한 애니메이션</title>
<style type="text/css">
#MyCanvas { border:1px dotted black;}
</style>
<script type="text/javascript" src="Vector2D.js"></script>
<script type="text/javascript" src="Ball.js"></script>
<script type="text/javascript">
var ctx = null;
var redBall = null;
var greenBall = null;
var radius = 30;
var timer = 0;
var prevtime = 0;
var currtime = 0;
window.onload = function() {
var canvas = document.getElementById("MyCanvas");
ctx = canvas.getContext("2d");
if(!ctx){
alert("캔바스로부터 Context를 구할 수가 없었습니다");
return;
}
init();
gameLoop();
timer = setInterval("gameLoop()",10);
}
function init() {
redBall = new Ball(ctx,"rgba(255,0,0,0.4)",new Vector2D(ctx.canvas.width/2, ctx.canvas.height/2),radius, new Vector2D(0,0));
greenBall = new Ball(ctx,"rgba(0,255,0,0.4)",new Vector2D(ctx.canvas.width/2-50, ctx.canvas.height-30),radius, new Vector2D(0,-30));
}
function gameLoop() {
currtime = new Date().getTime();
if(prevtime==0) prevtime = currtime;
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
redBall.update(currtime);
greenBall.update(currtime);
redBall.draw(ctx);
greenBall.draw(ctx);
checkCollision();
prevtime = currtime;
}
function print(msg, vec) {
document.write(msg+"("+vec.x+", "+vec.y+"), 크기("+vec.length()+")<br/>");
}
/* 서로 충돌한 공은 서로의 운동 에너지를 상대방에게 주고 받게 되므로 벡터연산을 두번 중복해야 한다 */
function checkCollision() {
var tmpVec = redBall.pos.sub(greenBall.pos);
var colVec1 = null, colVec2 = null;
var distance = tmpVec.length();
if(distance<=radius*2) {
colVec1 = tmpVec;
while(redBall.pos.sub(greenBall.pos).length()<radius*2){
redBall.pos.x -= redBall.speed.x;
redBall.pos.y -= redBall.speed.y;
greenBall.pos.x -= greenBall.speed.x;
greenBall.pos.y -= greenBall.speed.y;
}
// 초록 공의 속도와 방향은 충돌시 적색공의 속도와 방향에 영향을 미친다
colVec1.normalize();
var greenSpeedLen = greenBall.speed.length();
var cosVal = 0;
if(greenSpeedLen!=0) {
cosVal = greenBall.speed.dot(colVec1)/greenSpeedLen;
colVec1.mul(cosVal*greenSpeedLen); // 초록공의 충돌 에너지를 표현한 벡터
}
// 적색 공의 속도와 방향은 충돌시 초록공의 속도와 방향에 영향을 미친다.
var redSpeedLen = redBall.speed.length();
if(redSpeedLen!=0) {
colVec2 = greenBall.pos.sub(redBall.pos);
colVec2.normalize();
cosVal = redBall.speed.dot(colVec2)/redSpeedLen;
colVec2.mul(cosVal*redSpeedLen); // 적색공의 충돌 에너지를 표현한 벡터
}
if(colVec1 != null) {
redBall.speed = redBall.speed.add(colVec1);
greenBall.speed = greenBall.speed.sub(colVec1);
}
if(colVec2 != null) {
redBall.speed = redBall.speed.sub(colVec2);
greenBall.speed = greenBall.speed.add(colVec2);
}
}
}
</script>
</head>
<body>
<canvas id="MyCanvas" width="400" height="300">
Canvas를 지원하지 않는 브라우저입니다
</canvas>
</body>
</html>
Vector2D.js
function Vector2D(x,y) {
this.x = x;
this.y = y;
this.RAD = Math.PI/180;
this.DEG = 180/Math.PI;
Vector2D.prototype.add = add;
Vector2D.prototype.sub = sub;
Vector2D.prototype.length = length;
Vector2D.prototype.normalize = normalize;
Vector2D.prototype.mul = mul;
Vector2D.prototype.dot = dot;
Vector2D.prototype.angle = angle;
}
function add(vec){
var x = this.x + vec.x;
var y = this.y + vec.y;
return new Vector2D(x,y);
}
function sub(vec) {
var x = this.x-vec.x;
var y = this.y-vec.y;
return new Vector2D(x,y);
}
function length(){
return Math.sqrt(this.x*this.x + this.y*this.y);
}
function normalize() {
var len = this.length();
this.x /= len;
this.y /= len;
}
function mul(len) {
this.x *= len;
this.y *= len;
}
function dot(vec) {
return (this.x*vec.x) + (this.y*vec.y);
}
function angle(vec) {
var cosVal = this.dot(vec)/ (this.length()*vec.length());
var rad = Math.acos(cosVal);
//return rad * this.DEG;
return rad;
}
function copy() {
return new Vector2D(this.x, this.y);
}
Ball.js
function Ball(ctx, color, pos, r, speed) {
this.ctx = ctx;
this.color = color;
this.pos = pos;
this.r = r;
this.speed = speed;
this.prevtime = 0;
/* 이동량을 초당 최대 200으로 제한하는 경우 */
this.movRate = 200/1000;
/* 밀리초당 감쇠량 */
this.decRate = 1/1900;
Ball.prototype.update = update;
Ball.prototype.draw = draw;
};
function update(currtime){
if(this.prevtime==0) {
this.prevtime = currtime;
return;
}
/* 업데이트 간 경과시간에 따른 이동량 및 감쇠량 계산 */
var move = (currtime-this.prevtime) * this.movRate;
var decrement = (currtime-this.prevtime) * this.decRate;
//console.log("decrement:"+decrement);
this.prevtime = currtime;
/* 속도가 무한히 작아지도록 두지 않고 적당한 시점에서 속도를 0으로 설정함 */
if(this.speed.length()<0.01) {
this.speed.x = 0;
this.speed.y = 0;
return;
}
// 속도의 자연감쇠 효과
this.speed.x *= this.speed.x==0 ? 0 : (1-decrement);
this.speed.y *= this.speed.y==0 ? 0 : (1-decrement);
this.pos.x += this.speed.x*(move/10);
this.pos.y += this.speed.y*(move/10);
// 캔바스의 테두리에 닿으면 반대 방향으로 반사한다
if(this.pos.x+this.r >= this.ctx.canvas.width){
this.speed.x *= -1;
this.pos.x = this.ctx.canvas.width-this.r;
}
if(this.pos.y+this.r >= this.ctx.canvas.height){
this.speed.y *= -1;
this.pos.y = this.ctx.canvas.height-this.r;
}
if(this.pos.x-this.r <= 0) {
this.speed.x *= -1;
this.pos.x = this.r;
}
if(this.pos.y-this.r <= 0) {
this.speed.y *= -1;
this.pos.y = this.r;
}
};
function draw(){
this.ctx.beginPath();
this.ctx.fillStyle = this.color;
this.ctx.arc(this.pos.x, this.pos.y, this.r, 0, 2 * Math.PI);
this.ctx.fill();
this.ctx.lineWidth = 1;
this.ctx.strokeStyle = this.speed.length()==0 ? "black" : "rgba(0,0,0,0)";
this.ctx.stroke();
};