본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Html 캔버스/Html 캔버스 튜토리얼 (내가만든 차트!)

Html Canvas (Html 캔버스) 튜토리얼 (차트만들기!) - 10 : 이벤트 관리1

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2020. 2. 7.

 

캔버스에 이벤트를 주는방법 첫번째 시간이다.   * 3번째 시간까지 있슴돠~ ><

Javascript로 이루어진 차트들은 대부분 마우스로 클릭하거나, 마우스가 해당 차트 위에 위치할 때 특정 행위를 한다.

그런데 캔버스는 그러한 기능을 아주 쉽게 제공하지를 않는다.

 

구글링을 하면 Path2D를 사용한 방법이 존재하는데.. Path2D는 익스플로러에서(!?!) 동작하지 않는다.

조금 구린(버전이낮은?) 브라우저에서도 잘 동작을 안하기 때문에 Path2D를 활용해서 차트를 만들었다면 대략 낭패를 보고 만다.

아직 대한민국의 절반 이상이 익스플로러를 사용하는 현실에서는 눈물을 머금고 Path2D를 포기 하여야 한다..ㅠ

뭐..익스플로러를 안쓰면 상관없지만..

 

그러면, 어떻게 해야하는지 한번 살펴보도록 하자.

먼저 아래 코드를 붙여넣어보자.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Canvas</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<style>
    body{padding: 5%;text-align: center;}
    canvas{border: 1px solid gray;border-radius: 3px;}
</style>
<body>
    <canvas width='400' height='400' id='canvas'></canvas>
</body>
</html>

<script>    
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
   
    var position = [  //위치정보를 가지고 있는 객체
        {x:50,y:50, width:50,height:50}
    ];
</script>        

 

요렇게 기본 구성을 하여보았다.

position이라는 객체는 사각형을 그리기위한 정보를 지닌 대상이다.

그러면 사각형을 그려보자.

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
   
    var position = [  //위치정보를 가지고 있는 객체
        {x:50,y:50, width:50,height:50}
    ];
   
    function areaSimple(){ //그리기용 함수
        ctx.beginPath();
        position.forEach( (element,idx)=> {
            ctx.fillRect(element.x, element.y, element.width, element.height);
        });
    }
    areaSimple()

사각형이 그려졌다.

 

맨 첫장에서 설명하였듯 캔버스는 단일 객체이다.

즉, 다른 html 테그처럼 부모노드 > 자식노드 의 관계가 아니다. 1개의 테그로만 되어 있다.

그러므로 이벤트를 부여하려면 직접 캔버스에 해 주어야 하며, 그림을 그리는 데이터를 계산식을 통해서 계산을 해 주어야 한다.

 

일단..캔버스에 클릭이벤트를 부여하여보자.

스크립트를 수정하자.

    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
   


    var position = [  //위치정보를 가지고 있는 객체
        {x:50,y:50, width:50,height:50}
    ];
   

    function areaSimple(){   //그리기용 함수
        ctx.beginPath();
        position.forEach( (element,idx)=> {
            ctx.fillRect(element.x, element.y, element.width, element.height);
        });
    }
    areaSimple();

    canvas.addEventListener('click', function (event) {  //클릭 이벤트
        console.log(event);
    });     

 

캔버스를 클릭하면 해당 이벤트를 콘솔로 출력하게 하였다.

그러면 캔버스를 클릭 할 때마다 이벤트가 콘솔로 출력이 된다.

 

그런데 사용자가 원하는 것은 캔버스에 대한 이벤트가 아니라 그림이 그려진 사각형에 대한 이벤트 이다.

아래 사진을 보자.

요 관계 그림을 잘 보자!

 

위 그림처럼 현재 사각형이 그려졌다.

그리고 사용자는 캔버스 전체에 클릭 이벤트를 부여하였다.

사용자가 캔버스 지역을 클릭하였다고 가정하여보자.

클릭은 빨강색 점으로 표현 하였다.

 

캔버스 테그가 만약에 HTML테그에서 0,0 지역에 위치한다면 해당 내용은 사실 불필요하다.

그러나 대부분의 웹 화면에서 좌측 최 상단에 캔버스가 존재하는 경우는 거의 드믈다.

사용자가 캔버스를 클릭하였을때의 좌표값은 HTML에서의 좌표값을 의미하기 때문에 캔버스가 HTML 테그에서 떠 있는 곳 만큼을 빼 주어야 한다.

다시말해 캔버스가 Html 내부에서 좌측 및 위로 떨어진 거리값을 빼주어야 한다는 이야기 이다.

위 사진 개념처럼 말이다.

 

해당 개념을 적용해서 소스코드를 수정하여보자.

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

var position = [  //위치정보를 가지고 있는 객체
    {x:50,y:50, width:50,height:50}
];

function areaSimple(){   //그리기용 함수
    ctx.beginPath();
    position.forEach( (element,idx)=> {
        ctx.fillRect(element.x, element.y, element.width, element.height);
    });
}

areaSimple();

canvas.addEventListener('click', function (event) {  //클릭 이벤트
    var x1 = event.clientX - canvas.offsetLeft;//offsetLeft는 html테그 안에서 좌>우로 이동한 값
    var y1 = event.clientY - canvas.offsetTop;//offsetTop은 html테그 안에서 위>아래로 내려간 값        
    console.log(x1, y1);
});     

 

요렇게 해주면 캔버스의 내부의 좌표값이 정확하게 나오게 된다.

여기까지 이해가 안되면 안되는데..ㅠㅠ

위 내용이 어렵다면 여러번 살펴보자. 위 내용이 이해가 안되면 아래 내용은 더욱 이해가 안된다..

 

 

정확한 캔버스의 위치값을 얻었으므로 이제 사각형 영역안에 들어왔나 들어오지 않았나를 판별하는 부분에 대해서 살펴보자.

마찬가지로 클릭은 빨강색 점으로 표현하였다.

우앗..!

 

자, 사용자가 캔버스를 클릭하면 클릭한 X좌표와 Y좌표가 나온다.

그러면 사각형 영역에 들어온 것을 판단하려면 내가 클릭한 X좌표는 사각형 시작점 X보다 크거나 같아야한다.

또한 시작점 X값 + 사각형 넓이만큼보다는 작거나 같아야 한다.

구분 사각형이 그려지기 위한 시작점X 시작점X + 사각형 넓이
내가클릭한 X값(왼쪽) 크거나 같아야 한다. 작거나 같아햐 한다.

 

내가 클릭한 Y값은 사각형 시작점 Y값보다 크거나 같아야 한다.

마찬가지로 시작점 Y값 + 사각형 높이보다는 작거나 같아야 한다.

구분 사각형이 그려지기 위한 시작점Y 시작점Y + 사각형 높이
내가클릭한 Y값(왼쪽) 크거나 같아야 한다. 작거나 같아햐 한다.

 

위 내용을 함수로 표현하면 아래처럼 표현이 가능하다.

    function isInsideRect(x1, y1){  //x1은 클릭한 X값, y1은 클릭한 y값
        var result = false; 
        var index = -1;
        for(var i=0; i < position.length;i++){ //사각형이 담긴 배열
            var data = position[i];
            var start_x = data.x;  //x시작값
            var end_x = data.x + data.width;  //x시작값 + 넓이
            var start_y = data.y; //y시작값
            var end_y = data.y + data.height;  //y시작값 + 높이
            if(x1 >= start_x && x1 <= end_x){  
                if(y1 >= start_y && y1 <= end_y){
                    result = true;  //들어왔다.
                    index = i;  //해당 배열 인덱스 번호
                    break;
                }
            } 
        }
        return {result:result, index:index};
    }

 

그러면 소스코드를 수정하여보자.

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

var position = [  //위치정보를 가지고 있는 객체
    {x:50,y:50, width:50,height:50}
];

function areaSimple(){   //그리기용 함수
    ctx.beginPath();
    position.forEach( (element,idx)=> {
        ctx.fillRect(element.x, element.y, element.width, element.height);
    });
}

areaSimple();

canvas.addEventListener('click', function (event) {  //클릭 이벤트
    var x1 = event.clientX - canvas.offsetLeft;//offsetLeft는 html테그 안에서 좌>우로 이동한 값
    var y1 = event.clientY - canvas.offsetTop;//offsetTop은 html테그 안에서 위>아래로 내려간 값        
    var result = isInsideRect(x1, y1);
    console.log(result);
});     

function isInsideRect(x1, y1){  //x1은 클릭한 X값, y1은 클릭한 y값
    var result = false; 
    var index = -1;
    for(var i=0; i < position.length;i++){ //사각형이 담긴 배열
        var data = position[i];
        var start_x = data.x;  //x시작값
        var end_x = data.x + data.width;  //x시작값 + 넓이
        var start_y = data.y; //y시작값
        var end_y = data.y + data.height;  //y시작값 + 높이
        if(x1 >= start_x && x1 <= end_x){  
            if(y1 >= start_y && y1 <= end_y){
                result = true;  //들어왔다.
                index = i;  //해당 배열 인덱스 번호
                break;
            }
        } 
    }
    return {result:result, index:index};
}

 

위 코드를 실행하면 사각형 영역이라고 판별이 되면 result 값이 true 상태를 나타내게 된다.

오...뭔가 된다!

요기까지 하면 큰 개념이 잡힌 것과 같다.

그러면 이제 응용하여보자!

클릭해서 해당 영역에 들어오면 색깔변화를 주어보자.

만약 클릭해서 영역에 잘 들어오면 파랑색을주고 아니면 그대로 있게 하자.

 

작업개념은,

1. 클릭하여 영역에 들어온다.

2. 클릭하여 대상이 있으면 캔버스를 전부 지운다. (초기화 개념)

3. 클릭한 대상은 파랑색, 대상이 아닌 내용은 그대로 모양을 그린다.

4. 시간이 지나면 캔버스를 지우고 원래의 색으로 되돌린다.

 

4단계로 나누어 한번 해보자.

캔버스가 영역에 들어온 것은 result 값이 true인 경우이므로 조건문을 작성하자.

    canvas.addEventListener('click', function (event) {
        var x1 = event.clientX - canvas.offsetLeft;
        var y1 = event.clientY - canvas.offsetTop;
        
        var inn = isInsideRect(x1, y1);

        if (inn.result) {  //만약 들어온 것 이라면
            ctx.clearRect(0, 0, 400, 400);  //초기화
        }
    }); 

 

1번과 2번단계가 끝이 났다. 다음단계인 3번을 위해서 캔버스 내부를 다시 그려보자.

isInsideRect 함수에서 결과값으로 배열의 인덱스도 리턴하여 주었으므로 해당 인덱스를 키값으로 활용하자.

    canvas.addEventListener('click', function (event) {
        var x1 = event.clientX - canvas.offsetLeft;
        var y1 = event.clientY - canvas.offsetTop;
        
        var inn = isInsideRect(x1, y1);

        if (inn.result) {  //만약 들어온 것 이라면
            ctx.clearRect(0, 0, 400, 400);  //초기화
            position.forEach( (element,idx)=> {
                ctx.save();  // 상태 저장
                if(inn.index == idx){  //결과값과 같은 인덱스 값이면
                    ctx.fillStyle = 'blue';     //파랑색 처리
                }
                ctx.fillRect(element.x, element.y, element.width, element.height);  //그리기
                ctx.restore();  //기본값 복원
            });            
        }
    }); 

 

3번단계에서의 핵심은 save와 restore이다. 해당 기능을 사용하지 않으면 캔버스는 동일한 fill 스타일을 적용해 버린다.

마지막으로 시간이 지나면 원래의 색으로 되돌리는 4번기능을 구현하여보자.

    canvas.addEventListener('click', function (event) {
        var x1 = event.clientX - canvas.offsetLeft;
        var y1 = event.clientY - canvas.offsetTop;
        
        var inn = isInsideRect(x1, y1);

        if (inn.result) {  //만약 들어온 것 이라면
            ctx.clearRect(0, 0, 400, 400);  //초기화
            position.forEach( (element,idx)=> {
                ctx.save();  // 상태 저장
                if(inn.index == idx){  //결과값과 같은 인덱스 값이면
                    ctx.fillStyle = 'blue';     //파랑색 처리
                }
                ctx.fillRect(element.x, element.y, element.width, element.height);  //그리기
                ctx.restore();  //기본값 복원
            });  
            setTimeout(() => {  //0.1초뒤 실행
                ctx.clearRect(0, 0, 400, 400);  //전부 지운다.
                areaSimple();  //기본을 그린다.
            }, 100);            
        }
    }); 

 

자, 드디어 완성하였다.

배열로 그릴 대상을 준 것은 여러개의 모양을 그릴때를 대비하기 위해서 한 것이다.

기왕 한 김에 position 값에 여러개의 사각형을 넣고서 한번 실행하여보자.

전체 소스코드이다.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Canvas</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<style>
    body{padding: 5%;text-align: center;}
    canvas{border: 1px solid gray;border-radius: 3px;}
</style>
<body>
    <canvas width='400' height='400' id='canvas'></canvas>
</body>
</html>

<script>    
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
   
    var position = [
        {x:50,y:50, width:50,height:50},
        {x:85,y:200, width:100, height:30}, 
        {x:105,y:100, width:30, height:30}, 
        {x:255,y:30, width:30, height:130}, 
    ];
   
    function areaSimple(){
        ctx.beginPath();
        position.forEach( (element,idx)=> {
            ctx.fillRect(element.x, element.y, element.width, element.height);
        });
    }
    areaSimple();  //디폴트로 그려주기!

    canvas.addEventListener('click', function (event) {
        var x1 = event.clientX - canvas.offsetLeft;
        var y1 = event.clientY - canvas.offsetTop;        
        var inn = isInsideRect(x1, y1);

        if (inn.result) {
            ctx.clearRect(0, 0, 400, 400);
            position.forEach( (element,idx)=> {
                ctx.save();
                if(inn.index == idx){
                    ctx.fillStyle = 'blue';   
                }
                ctx.fillRect(element.x, element.y, element.width, element.height);
                ctx.restore();
            });
            setTimeout(() => {
                ctx.clearRect(0, 0, 400, 400);
                areaSimple();
            }, 100);
        }
    }); 

    function isInsideRect(x1, y1){
        var result = false;
        var index = -1;
        for(var i=0; i < position.length;i++){
            var data = position[i];
            var start_x = data.x;
            var end_x = data.x + data.width;
            var start_y = data.y;
            var end_y = data.y + data.height;
            if(x1 >= start_x && x1 <= end_x){  
                if(y1 >= start_y && y1 <= end_y){
                    result = true;
                    index = i;
                    break;
                }
            } 
        }
        return {result:result, index:index};
    }
               
</script>

 

아래 사진처럼 동작하는 것을 볼 수 있다.

잘 나온닷!

 

 

여기까지 캔버스에 이벤트를 부여하는 첫번째 방법에 대해 살펴 보았다.

캔버스는 html 다른 테그와 달리 이벤트를 위 방식처럼 입력된 또는 그림을 그렸던 데이터를 기반으로 계산을 해 주어야 한다.

물론 Path2D라는 객체를 사용해서 위 내용을 구현하여도 상관없지만 버전이 낮은 브라우저, 인터넷 익스플로러(전부)에서는 동작하지 않는다.

 

결국의 캔버스에서의 특정 그림이 변하고 움직이는 이벤트는 쉼없이 그리고 지우고가 반복되는 것임을 알 수 있다.

그리는시간과 지우는시간이 너무나도 빨라서 움직이는 것 처럼 보이는 것 뿐이다.

 

다음시간도 이어서 이벤트를 부여하는 방법에 대해서 알아보겠다.

 

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글