(완성 예제 : 크롬, 파이어폭스, IE9에서 볼 수 있습니다)


글 : 인지심리 매니아


지각심리학은 자극의 움직임이 어떻게 뉴런의 발화로 이어지는지 연구해왔다. 이런 실험은 보통 두 가지 유형의 자극을 사용한다. 하나는 기울어진 막대처럼 생긴 자극이고, 다른 하나는 무선적인 방향으로 움직이는 점 자극이다.

 

오늘은 무선적으로 움직이는 점들을 구현한 예제를 소개하고자 한다이 예제는 Codeflow라는 블로그에서 소개되어 있으며필자가 목적에 맞게 약간의 수정을 가했다.


 


구조도 


이 코드는 네 개의 object를 갖고 있다. 먼저 main system을 불러온다. System은 여러 개의 점(particle)을 불러내서 해당 좌표에 그린다. Particle은 각각 고유한 좌표와 속도(velocity)를 갖고 있다. 각각의 좌표는 Vector로 표현된다. Vectorxy좌표를 변수로 가지고 있으며, 좌표를 계산할 때 사용할 사칙연산 메소드를 가지고 있다.

 


소스 코드


index.html

<!DOCTYPE HTML>

<!-- http://codeflow.org/entries/2010/aug/22/html5-canvas-and-the-flying-dots/ -->

<html lang="en"><head>

<style type="text/css">

html,body{width:200px;height:200px;margin:0;padding:0;border:0;outline:0}

body{background-color:#fff}

span{position:absolute;left:5px;top:2px}

a{font-family:sans-serif;color:#88ca0c;text-decoration:none;font-weight:bold;cursor:pointer}</style>

<script type="text/javascript" src="gravity.js"></script>

</head>


<body onload="main()">

<canvas id="particles" width="200" height="200"></canvas></a>

</body>

</html>


 

gravity.js

var Vector = function(x, y){

    this.x = x;

    this.y = y;


    this.sub = function(other){

        return new Vector(

            this.x - other.x,

            this.y - other.y

        );

    }

    this.isub = function(other){

        this.x -= other.x;

        this.y -= other.y;

    }

    this.iadd = function(other){

        this.x += other.x;

        this.y += other.y;

    }

    this.length = function(){

        return Math.sqrt(this.x*this.x + this.y*this.y);

    }

    this.idiv = function(scalar){

        this.x /= scalar;

        this.y /= scalar;

    }

    this.zero = function(){

        this.x = 0;

        this.y = 0;

    }

    this.validate = function(){

        if(isNaN(this.x+this.y)){

            this.x = 0;

            this.y = 0;

        }

    }

}

            

var Particle = function(canvas){

    var x_speed = Math.random()*2;

    var y_speed = Math.sqrt(16-x_speed^2); // y 값을 계산해서 속력이 4가 되도록 한다.


if(Math.random() <0.5){

x_speed=-x_speed;

}

if(Math.random() <0.5){

y_speed=-y_speed;

}

    var bounce_damping = 1;

    this.velocity = new Vector(x_speed, y_speed)

    this.position = new Vector(

        Math.random() * canvas.width,

        Math.random() * canvas.height

        

    )


    this.step = function(){

        this.position.iadd(this.velocity);


        // border bounce

        if(this.position.x < 0){

            this.position.x = 0;

            this.velocity.x *= -bounce_damping;

        }

        else if(this.position.x > canvas.width){

            this.position.x = canvas.width;

            this.velocity.x *= -bounce_damping;

        }


        if(this.position.y < 0){

            this.position.y = 0;

            this.velocity.y *= -bounce_damping;

        }

        else if(this.position.y > canvas.height){

            this.position.y = canvas.height;

            this.velocity.y *= -bounce_damping;

        }


    }

    this.draw = function(context){

        context.beginPath();

        context.arc(this.position.x, this.position.y, 2.5, 0, Math.PI*2, false);

        context.fill();

    }

}


var System = function(amount, milliseconds){

    var canvas = document.getElementById('particles');

    var context = canvas.getContext('2d');

        

    var particles = [];

    for(var i=0; i<amount; i++){

        particles.push(new Particle(canvas));

    }

       

    setInterval(function(){

       

        // dot drawing style

context.clearRect(0, 0, canvas.width, canvas.height);

        context.globalCompositeOperation = 'lighter'; 

        context.fillStyle = 'rgba(0,0,0,0.9)';


        // nbody code acceleration accumulation

        for(var i=0, il=amount; i<il; i++){

            var a = particles[i];

            a.step();

            a.draw(context);

        }

    }, milliseconds);

}


var main = function(){

    var system = new System(15, 40);

};





설명

 

참고: 이 자바스크립트 코드는 객체를 활용하고 있다. 자바스크립트에서 객체를 활용하는 방법은 인터넷에 많이 소개되어 있으므로 이를 참고하기 바란다.

 

Vector

var Vector = function(x, y){

    this.x = x;

    this.y = y;

 

    this.sub = function(other){

        return new Vector(

            this.x - other.x,

            this.y - other.y

        );

    }

    this.isub = function(other){

        this.x -= other.x;

        this.y -= other.y;

    }

    this.iadd = function(other){

        this.x += other.x;

        this.y += other.y;

    }

    this.length = function(){

        return Math.sqrt(this.x*this.x + this.y*this.y);

    }

    this.idiv = function(scalar){

        this.x /= scalar;

        this.y /= scalar;

    }

    this.zero = function(){

        this.x = 0;

        this.y = 0;

    }

    this.validate = function(){

        if(isNaN(this.x+this.y)){

            this.x = 0;

            this.y = 0;

        }

    }

}

 

Vector xy좌표라는 두 개의 변수를 가지고 있으며, 좌표를 계산할 때 사용할 사칙연산 메소드를 갖고 있다. 예를 들어, 아래와 같은 경우 vec3=vec1-vec2 (-2,-2)가 된다.

var vec1 = new Vector(1, 2);

var vec2 = new Vector(3, 4);

var vec3 = vec1.sub(vec2);

 


Particle

var Particle = function(canvas){

    var x_speed = Math.random()*2;

    var y_speed = Math.sqrt(16-x_speed^2);          // y 값을 계산해서 속력이 4가 되도록 한다.

 

           if(Math.random() <0.5){

                     x_speed=-x_speed;

                                }

           if(Math.random() <0.5){

                     y_speed=-y_speed;

                                }

          

    var bounce_damping = 1;

    this.velocity = new Vector(x_speed, y_speed)

    this.position = new Vector(

        Math.random() * canvas.width,

        Math.random() * canvas.height

       

    )

 

    this.step = function(){

        this.position.iadd(this.velocity);

 

        // border bounce

        if(this.position.x < 0){

            this.position.x = 0;

            this.velocity.x *= -bounce_damping;

        }

        else if(this.position.x > canvas.width){

            this.position.x = canvas.width;

            this.velocity.x *= -bounce_damping;

        }

 

        if(this.position.y < 0){

            this.position.y = 0;

            this.velocity.y *= -bounce_damping;

        }

        else if(this.position.y > canvas.height){

            this.position.y = canvas.height;

            this.velocity.y *= -bounce_damping;

        }

 

    }

    this.draw = function(context){

        context.beginPath();

        context.arc(this.position.x, this.position.y, 2.5, 0, Math.PI*2, false);

        context.fill();

    }

}

 

Particle() x_speed y_speed라는 변수를 가진다. 변수들의 값은 무선적으로 결정되지만, sqrt(x_speed^2+y_speed^2)=4. , 속도(velocity) 4가 되도록 만들었다. , 점의 최초 좌표를 생성한다.

 

Step 메소드는 좌표에 velocity를 더해줘서 점이 한 프레임당 4만큼 이동하도록 만든다. 점이 화면 바깥으로 나가는 것을 방지하기 위해 경계선에 부딪힐 때 velocitybounce_damping(=-1 or +1)변수를 곱해줘서 방향이 반대로 향하도록 만들었다.

 

Draw 메소드는 해당 좌표에 작은 원을 그린다.

 


System

var System = function(amount, milliseconds){

    var canvas = document.getElementById('particles');

    var context = canvas.getContext('2d');

        

    var particles = [];

    for(var i=0; i<amount; i++){

        particles.push(new Particle(canvas));

    }

       

    setInterval(function(){

        // fading

/*

        context.globalCompositeOperation = 'source-out'; //source-out으로 바꿔서 점들의 꼬리를 제거했다

        context.fillStyle = 'rgba(100,100,100)';

        context.fillRect(0, 0, canvas.width, canvas.height);

*/

        // dot drawing style

context.clearRect(0, 0, canvas.width, canvas.height);

        context.globalCompositeOperation = 'lighter'; 

        context.fillStyle = 'rgba(0,0,0,0.9)';


        // nbody code acceleration accumulation

        for(var i=0, il=amount; i<il; i++){

            var a = particles[i];

            a.step();

            a.draw(context);

        }

    }, milliseconds);

}


System은 캔버스에 particle을 그린다. 먼저 particle이라는 배열을 만들고 particle instance들을 모두 저장한다. 그 다음 setinterval 명령어로 각 점들의 step draw 메소드를 실행시켜서 점들을 화면에 그린다.

 

 

Main 

var main = function(){

    var system = new System(15, 40);

};


Main system의 인스턴스를 만든다. 15는 점의 개수, 40 setinterval 명령어가 실행되는 시간 간격을 말한다. 예제의 경우 각 점들은 40 밀리세컨드 단위로 움직이게 될 것이다.

 


글: 인지심리 매니아


필자는 실험 자극을 만들 때 주로 Matlab을 사용해왔다. 하지만 HTML5의 도입으로 인해 이제는 웹에서도 실험 자극을 쉽게 구현할 수 있게 되었다.  그래서 HTML5로 간단한 자극을 만드는 법을 소개하고자 한다. 


오늘은 원운동을 하는 작은 점을 만들고자 한다(필자가 실제로 실험에 사용했던 자극이다).




완성된 자극(IE8 이하 버젼에서는 예제가 보이지 않을 수 있습니다)



소스 코드

<!doctype html>

<html>

  <head>

    <meta charset="UTF-8" />

    <title>Canvas Test</title>

  </head>

<body>

  <section>


    <div>

        <canvas id="canvas" width="800" height="600">

         This text is displayed if your browser 

         does not support HTML5 Canvas.

        </canvas>

    </div>


<script type="text/javascript">

var canvas;  

var ctx;

var x = 100;

var y = 200;

var WIDTH = 400;

var HEIGHT = 400; 

var r=1;                               // 원운동 궤적의 반지름

var i=Math.PI/180;              // (라디안으로 변환한) 1도


function init() {

  canvas = document.getElementById("canvas");

  ctx = canvas.getContext("2d");

  return setInterval(draw, 20);

}



function draw() {

  clear();

  ctx.fillStyle = "#444444";

  circle(x, y, 10);


  x -= r*Math.cos(i);

  y -= r*Math.sin(i);

  i += Math.PI/180;

}


function clear() {

  ctx.clearRect(0, 0, WIDTH, HEIGHT);

}


function circle(x,y,r) {

  ctx.beginPath();

  ctx.arc(x, y, r, 0, Math.PI*2, true);

  ctx.fill();

}


init();


</script>

  </section>

</body>

</html>



이 코드는 Unknown Kadath에 소개된 코드를 필자가 수정한 것이다. 원래는 무선적으로 움직이는 점을 구현하고 있지만, 위 코드에선 점이 원운동을 하도록 수정되었다. 



이 코드의 개요는 다음과 같다.


먼저 init 함수가 실행되면 이 함수가 다시 draw 함수를 호출한다. 호출된 draw 함수는 이전 화면을 지운 다음 새 화면 위에 점을 그린다. Init 함수가 10밀리세컨드 단위로 draw 함수를 반복해서 호출하고, draw 함수는 매번 다른 위치에 점을 그리기 때문에 마치 점이 움직이는는 것처럼 보이게 된다. 




캔버스 지정하기

<canvas id="canvas" width="800" height="600">

This text is displayed if your browser does not support HTML5 Canvas.

</canvas>

먼저 도형을 그릴 캔버스를 지정한다. width는 넓이, height는 높이를 말한다.



초기 변수 지정

var canvas;  

var ctx;

var x = 100;

var y = 200;

var WIDTH = 400;

var HEIGHT = 400; 

var r=1;                               // 원운동 궤적의 반지름

var i=Math.PI/180;              // (라디안으로 변환한) 1도

x,y: 좌표를 의미한다

WIDTH와 HEIGHT: 캔버스의 크기를 의미한다

r: 원운동 궤적의 반지름을 의미한다.

i: 1도를 의미한다



init 함수

function init() {

  canvas = document.getElementById("canvas");

  ctx = canvas.getContext("2d");

  return setInterval(draw, 10);

}

Init 함수는 먼저 “canvas”의 element를 가져온다. 

그 다음 컨텍스트를 정의한다. 모든 캔버스는 컨텍스트를 가지고 있다. 우리가 그릴 그림이나 애니메이션은 모두 컨텍스트 안에서 표현된다. 여기서는 2d context를 선택했다. 

setInterval 함수는 draw 함수를 10 밀리세컨드 간격으로 반복 실행시킨다. 



draw 함수

function draw() {

  clear();

ctx.fillStyle = "#444444";

  circle(x, y, 10);


  i += Math.PI/90;

  x -= r*Math.cos(i);

  y -= r*Math.sin(i);

 

}


Draw 함수는 먼저 Clear 함수를 호출해서 이전 화면(점)을 지운다. 

function clear() {

  ctx.clearRect(0, 0, WIDTH, HEIGHT);

}


그 다음, 점의 색상을 #444444로 지정해 준다. 

그 다음, circle 함수로 작은 점을 그린다. x,y는 원의 좌표, 10은 반지름을 의미한다.

function circle(x,y,r) {

  ctx.beginPath();

  ctx.arc(x, y, r, 0, Math.PI*2, true);

  ctx.fill();

}


그 다음 각도 i의 값을 증가시킨다. 그리고 그 각도를 토대로 새 좌표 x,y를 만든다. 


여기까지가 한 사이클이다. 이후 10밀리세컨드가 지나면 init 함수가 draw 함수를 다시 호출하고, draw 함수는 점을 지운 다음 새 좌표에 점을 찍는다. 


근데, x, y를 계산하는 저 복잡한 공식은 무얼 의미할까?

우리의 목표는 점이 원모양으로 빙글빙글 돌게 만드는 것이다. 점이 원운동을 하려면 좌표가 원모양으로 변하도록 만들어야 한다.  그러려면 삼각 함수를 이용해서 좌표를 구해야 한다. 



단위원. 사진출처-위키피디아




따라서 x=cos(i), y=sin(i)가 된다. 


그럼 i를 구하는 저 복잡한 공식은 무얼 의미할까?

x와 y좌표가 원모양을 따라 변화하려면 각도 i가 증가해야 한다. 문제는 자바스크립트가 호도법의 각도 대신 ‘라디안’을 쓴다는 점이다. 따라서 i가 1도씩 늘어나게 해주려면 Math.PI/180(라디안으로 표현한 1도)을 더해주면 된다. 

+ Recent posts