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

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

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


Html 캔버스/Html 캔버스 에니메이션

Html Canvas circle agenda, Html 캔버스 원형 시간표(circle schedule, 방학 시간표) 제작기

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

 

HTML5가 등장하면서 웹에서 별의별것(?)을 다 하니 클라이언트의 요구사항 수준이 점점 높아지고 있는 시대입니다.

특별한 기능을 제작하고 만드는 경우에는 수학적 사고가 많이 필요로 하는데..

학창시절에 배웠던 수학을 떠올리기에는 머리가 이미 너무 굳어버린건 아닌지 모르겠습니다...;ㅁ;

 

이번에 제작한 기능은 원형 시간표 입니다.

원형 시간표는 초등학생(요즘은 배우나 모르겠습니다..) 때 방학숙제에서 자주 하던 원형 표 입니다.

동그라미 원 안에 자기가 해야될 일을 채우고 원 주변은 시간이 쓰여있는 모양 입니다.

구글에서 검색하면 이렇게 나오네요.

 

해당 기능을 Html 에서 제작하려면 부채꼴 모양이 그려져야 하므로 캔버스(canvas)이외의 방법으로는 구현하기가 어렵습니다.

먼저 기능 구현을 위해서 어떠한 기능이 필요한지 기초 기능을 정리를 하여 봅니다.

1. 원을 그리는 기능
2. 부채꼴을 그리는 기능
3. 시간표를 채우기 위해 마우스의 움직임을 판별하는 기능
4. 마우스가 원 안으로 들어왔는지 판별하는 기능
5. 마우스의 움직임에서 각도(degree)를 알아내는 기능
6. 시작 각도부터 마지막 각도까지의 중간각(degree)을 알아낸 뒤에 좌표로 바꾸는 기능

 

원, 부채꼴을 그리는 함수는 arc를 사용하면 되고, 마우스가 움직이는 것은 이벤트를 부여하면 됩니다.

그리고 마우스의 움직임은 addEventListener 같은 함수를 사용하면 됩니다.

그러므로 1번과 2번, 3번은 가장 나중에 고민하도록 합니다.

 

제일 먼저 고민할 것은 마우스가 원 내부에 접근하였는지를 알아내기 위한 방법입니다.

원을 그릴 때 우리는 원의 중심점과 반지름을 알 수 있습니다.

그리고 마우스의 위치로 2번째 지점을 알 수 있습니다.

원의 중심점에서 마우스가 위치한 곳의 x와 y좌표값을 빼준뒤에 제곱근을 구하면 원의 중심점으로부터 떨어진 길이가 나오게 됩니다. 

* 삼각함수를 의미합니다!

그리고 계산한 x와 y좌표값의 아크탄젠트를 구하면 기울어진 각도가 나오게 됩니다.

이를 함수로 제작하여 봅니다.

    //내부 판별용 함수입니다. x1과 y1은 마우스에서 가져온 위치 값 입니다.
    function isInsideArc(x1, y1){
        var result = false
        var x = width/2 - x1
        var y = height/2 - y1
        var my_len = Math.sqrt(Math.abs(x * x) + Math.abs(y * y)) //삼각함수
        if(radius >= my_len){
            result = true
        }            
        var rad = Math.atan2(y, x)
        rad = (rad*180)/Math.PI  //-180 ~ 180의 값이 나옵니다.
        rad += 180  //캔버스의 각도로 변경하여 줍니다.
        return {result:result, degree : rad}
    }

 

해당 함수는 원 내부에 들어온 유무를 result값으로 알려주고, 얼마나 기울어져있는지를 degree로 알려주는 함수 입니다.

이를 캔버스에 붙이는 것은 쉽습니다.

const canvas = document.getElementById('캔버스아이디')
const ctx = canvas.getContext('2d')
const width = canvas.clientWidth
const height = canvas.clientHeight
const radius = width*0.3  //반지름 값 입니다.

ctx.addEventListener('mousemove', function (event) {
    var x1 = event.clientX - eventer.parentElement.offsetLeft  //빼주는 값은 나중에 설명 합니다.
    var y1 = event.clientY - eventer.parentElement.offsetTop //빼주는 값은 나중에 설명 합니다.
    var inn = isInsideArc(x1, y1)
    console.log(inn)
})


function drawCircle(targets, first, last){
    targets.save()
    targets.beginPath()
    targets.arc(width/2, height/2, radius,(Math.PI / 180) * first, (Math.PI / 180) * last, false)
    targets.lineWidth = 3
    targets.strokeStyle='#dfdfdf'
    targets.stroke()
    targets.closePath()
    targets.restore()
}

drawCircle(ctx, 0, 360, true)

 

캔버스의 arc 함수를 이용하여 원을 그리고나면 일단 원 안에 이벤트 부여까지는 성공한 셈 입니다.

원 내부로가니 true, 외부로가니 false가 나옵니다.

 

원을 그리고 보니 조금 밋밋한 감이 있어 내부에 선과 시간을 그려주도록 합니다.

이를 위해 시간정보를 담을 배열을 만들어 줍니다.

    const canvas = document.getElementById('캔버스아이디')
    const ctx = canvas.getContext('2d')
    const width = canvas.clientWidth
    const height = canvas.clientHeight
    const radius = width*0.3
    const hour12 = [
        {start:0, end:30, display:'04'},
        {start:30, end:60, display:'05'},
        {start:60, end:90, display:'06'},
        {start:90, end:120, display:'07'},
        {start:120, end:150, display:'08'},
        {start:150, end:180, display:'09'},
        {start:180, end:210, display:'10'},
        {start:210, end:240, display:'11'},
        {start:240, end:270, display:'12'},
        {start:270, end:300, display:'01'},
        {start:300, end:330, display:'02'},
        {start:330, end:360, display:'03'}
    ]

 

hour12 배열은 시간에 따른 각도를 담고있는 배열 입니다.

html캔버스는 0도가 맨 위쪽이 아니라 가로형태로 되어 있습니다.

0도가 맨 위가 아닙니다.

 

그러므로 반복문을 통해서 숫자를 표기하려면 위의 배열처럼 0도 ~ 30도 사이의 값은 숫자 4가 와야 합니다.

그러면 중심점으로부터 기울어진 각도의 선을 추가하려면 사인(sine)과 코사인(cosine)을 활용하여 좌표값을 확인한 뒤에 그려주도록 합니다.

//내부 선
hour12.forEach(function(data){
    //내부 실선 표시
    ctx.save()
    ctx.beginPath()
    ctx.strokeStyle='#dfdfdf'
    ctx.lineWidth = 0.3
    ctx.moveTo(width/2, height/2)
    var xx = Math.cos(degreesToRadians(data.end)) * radius + width / 2
    var yy = Math.sin(degreesToRadians(data.end)) * radius + height / 2
    ctx.lineTo(xx,yy)
    ctx.stroke()
    //텍스트(시간) 표시
    xx = Math.cos(degreesToRadians(data.end)) * radius*1.2 + width / 2
    yy = Math.sin(degreesToRadians(data.end)) * radius*1.2 + height / 2
    var minus = ctx.measureText(data.display).width / 2;
    ctx.fillText(data.display, xx-minus, yy)  //텍스트의 길이 빼 주기
    ctx.closePath()       
    ctx.restore()         
})

//각도를 라디안으로
function degreesToRadians(degrees) {
    const pi = Math.PI
    return degrees * (pi / 180)
}

 

텍스트를 표시하는 것은 원형 바로 위에 위치하는 것 보다 원의 살짝 위에 위치하는 것이 좀 더 보기 좋으므로 1.2를 곱하여 살짝 위로 뜨게 하였습니다.

이제 숫자도있고, 원 내부의 실선도 있고...이벤트도 생겼습니다.

 

기초 기능을 제작하고 자주 사용할 것 같은 기능은 함수로 분리하여 놓습니다.

그리고 이제 고민해야되는 기능은 스케줄을 추가하고, 삭제하고 결과를 받는 기능입니다.

1. 드래그 기능을 통해 스케줄을 추가 합니다.
2. 더블 클릭을 통해 스케줄을 제거 합니다.
3. 여러 스케줄을 등록 할 수 있습니다.
4. 한번 등록된 스케줄은 영향을 받지 않습니다.

 

스케줄을 추가하는 것은 드래그 기능, 스케줄을 빼는 것은 더블클릭으로 기능을 정의 하였습니다.

드래그 기능과, 더블클릭 기능을 구현하기 위해서는 조금 단순한 방법을 사용 하여야 합니다.

먼저 드래그 중인지, 더블클릭 중인지를 판별하기 위해서 move, up, down에 대한 이벤트를 정의하여 줍니다.

    eventer.addEventListener('mousemove', function (event) {
        var x1 = event.clientX - eventer.parentElement.offsetLeft
        var y1 = event.clientY - eventer.parentElement.offsetTop
        var inn = isInsideArc(x1, y1)
    })
    
    eventer.addEventListener('mousedown', function (event) {
        isUp = true
        setTimeout(function(){
            doubleClickedCnt++
        },100)        
    })    
    
    eventer.addEventListener('mouseup', function (event) {
        isUp = false
        //더블클릭
        setTimeout(function(){
            if(doubleClickedCnt >1){
                var x1 = event.clientX - eventer.parentElement.offsetLeft
                var y1 = event.clientY - eventer.parentElement.offsetTop
                var inn = isInsideArc(x1, y1)
            }
            doubleClickedCnt = 0
            
        },250)    
    })    

 

setTimeout함수를 통해 고전적(?)으로 더블클릭 이벤트에 대해서 동작하게 하였습니다.

그리고 잘 만들어놓은 내부에 마우스가 있는지를 판별하여 주는 isInsideArc 함수를 사용하여 마우스를 누른 다음에 움직 일 때 마다 그림을 그려주도록 합니다.

그림을 그릴때는 기존에 만들어둔 배열인 hour12 배열에서의 start와 end값을 가져와서 arc함수를 동작시켜주면 됩니다.

이벤트 기준으로 설명하면, 마우스를 down한 뒤에 move하면 내 mouse 위치에서 isInsideArc함수에서의 각도를 가져온 뒤에 hour12 배열에서의 각도 위치를 가지고 부채꼴을 그립니다.

마우스를 down한 뒤에 move하면 hour12 배열에서의 각도를 가지고 부채꼴을 그립니다.

 

위 방법처럼 더블클릭을 하면 마우스 위치값이 나오므로 해당 색을 제거하도록 합니다.

이제 필요한 것은 바로 "기록" 입니다.

행위에 대해서 기억을 하게 하려면 기존의 housr12 배열에서 해당 부채꼴이 그려졌는지를 추가해 주도록 합니다.

그리고 여러 스케줄이 사용 되어야 하므로 스케줄을 선택하면 선택된 정보가 포함되도록 합니다.

이런식으로 스케줄을 더할 노드를 추가하고,

 

아래 코드처럼 위 엘리먼트가 선택되면 선택된 인덱스(고유 값) 값과, 본인의 색상이 추가되도록 합니다.

    //스케줄 아이템 클릭 기능
    var clickedIndex
    var clickedColor
    $('.sch_item').each(function(){
        $(this).click(function(){
            $('.sch_item').removeClass('imClicked')
            $(this).addClass('imClicked')
            clickedIndex = $(this).attr('clickedIndex')
            clickedColor = $(this).css('color')
        })
    })

 

위 개념을 토대로 코드를 완성하면 아래모양의 제법 그럴싸한 기능이 완성 됩니다.

여기서 마우스 down과 move 이벤트가 발생하여 스케줄이 추가 또는 제거 되면 housr12 배열에 상태값이 변경되야 하는 것 입니다.

가령 아래처럼 배열이 변경되었다고 가정하여 봅니다.

    const hour12 = [
        {start:0, end:30, display:'04', dataFilled : true, index:'A', filledColor : 'red'},
        {start:30, end:60, display:'05', dataFilled : false, index:'', filledColor : ''},
        {start:60, end:90, display:'06', dataFilled : true, index:'B', filledColor : 'blue'},
        ...
    }

 

위 배열의 내용은, 04시의 값은 A가 포함되어있다는 내용이고, 05시는 비어있다는 내용 입니다.

그리고 06시의 내용은 B가 포함되어 있다는 내용을 의미합니다.

이러한 방식으로 마우스의 움직임에 대해서 배열의 값을 변화주면서 그림을 지웠다 그렸다를 반복하여 줍니다.

스케줄이 1시간단위로 기록되는 모양입니다.

 

스케줄의 내용이 멋지게 채워지면서 동작하는 것을 볼 수 있습니다.

여기서 조금 더 욕심을 내 보면, 만들어진 부채꼴 중앙에 이름을 그리고 싶어 집니다.

중앙에 이름을 표기하려면 hour12에서 고유의 index가 있는지를 조사하고, 인덱스가 같음 또는 다름에 의해서 서로의 값을 1개로 묶어(grouping, mapping)주어야 합니다.

 

hour12 배열은 1시간 단위의 정보가 존재 하므로 해당 정보에 대해서 반복문을 동작시켜 텍스트를 단순하게 추가하면 모든 부채꼴에 글씨가 표기되어 버리기 때문 입니다.

아..난감한 UI입니다..

 

가장 처음값을 기준으로 데이터를 매핑하여 줍니다.

고유의 인덱스가 같다, 다르다를 통해서 데이터를 1개의 꾸러미로 만들어 줍니다.

그리고나서 가장 첫번째 배열값과 마지막 배열값의 인덱스를 비교하여 봅니다.

가장 첫번째 배열값과 마지막 배열의 인덱스가 같으면 마지막 배열에서의 시작값을 첫번째 배열의 시작 값으로 교체를 하도록 합니다.

    function textMaker(){
        var copyArr = Object.assign([], hour12)  //배열 복사
        var summery = []
        var eq = ''
        var start = -1
        var colors = ''
        copyArr.forEach(function(data, idx){
            if(idx == 0){  //가장 처음이면
                eq = data.index
                start = data.start
                colors = data.filledColor
            } else if(eq != data.index){   //인덱스가 다르면             
                summery.push({start : start, end : data.start, index : eq, filledColor: colors})
                eq = data.index
                start = data.start
                colors =  data.filledColor
                if(idx == copyArr.length-1){  //인덱스가 다르면서 마지막이면
                    summery.push({start : start, end : data.end, index : eq, filledColor: data.filledColor})
                }                
            } else if(idx == copyArr.length-1){ //마지막이면
                summery.push({start : start, end : data.start, index : eq, filledColor: data.filledColor})
            }
        })

        var lastCheck = summery[0]
        var lastCheck2 = summery[summery.length-1]
        
        if(lastCheck.index == lastCheck2.index) {  //마지막 스타트가 처음 스타트로 오도록 변경 합니다.
            summery[0].start = lastCheck2.start
            summery.pop()
        }      
    }

 

첫번째와 마지막 배열의 값을 위 코드처럼 교체와 삭제를 하는 이유는 360이 넘어가는 값의 형태이기 때문 입니다.

조립된 글씨를 표기하기위한 배열에서 텍스트를 그리려면 시작과 끝값의 차이르 빼준 다음에 2로 나누어 처음 값에 추가하여 줍니다.

마지막 각도가 0도가 넘어간 경우에는 360에서 시작값을 빼 주고 난 뒤의 마지막 값을 더한뒤 2로 나누어 줍니다.

    function textMaker(){
        var copyArr = Object.assign([], hour12)  //배열 복사
        var summery = []
        var eq = ''
        var start = -1
        var colors = ''
        copyArr.forEach(function(data, idx){
            if(idx == 0){  //가장 처음이면
                eq = data.index
                start = data.start
                colors = data.filledColor
            } else if(eq != data.index){   //인덱스가 다르면             
                summery.push({start : start, end : data.start, index : eq, filledColor: colors})
                eq = data.index
                start = data.start
                colors =  data.filledColor
                if(idx == copyArr.length-1){  //인덱스가 다르면서 마지막이면
                    summery.push({start : start, end : data.end, index : eq, filledColor: data.filledColor})
                }                
            } else if(idx == copyArr.length-1){ //마지막이면
                summery.push({start : start, end : data.start, index : eq, filledColor: data.filledColor})
            }
        })

        var lastCheck = summery[0]
        var lastCheck2 = summery[summery.length-1]
        
        if(lastCheck.index == lastCheck2.index) {  //마지막 스타트가 처음 스타트로 오도록 변경 합니다.
            summery[0].start = lastCheck2.start
            summery.pop()
        }      
        
        summery.forEach(function(data){
            if(data.index != ''){  //스케줄이 등록된 경우만 그리게 합니다.
                var half = Math.abs(data.end - data.start) / 2  //부채꼴 크기의 절반입니다.
                
                if(data.start > data.end ){ //마지막 각도가 0도가 넘어간 경우 입니다.
                    half = (360-data.start + data.end) / 2
                }
                var degg = data.start + half;
                var xx = Math.cos(degreesToRadians(degg)) * radius * 0.7 + width / 2;
                var yy = Math.sin(degreesToRadians(degg)) * radius * 0.7 + height / 2;
                event_ctx.save()
                event_ctx.beginPath()
                event_ctx.fillStyle = 'white'
                event_ctx.fillText(data.index, xx, yy)
                event_ctx.restore()
            }
        })           
    }

 

위 내용을 각각 필요에 따라 함수에 추가한뒤에 불필요한 코드를 제거합니다.

기능을 정리하고 난 뒤에 실행하여 봅니다.

다양한 형태로 추가가 잘 됩니다.

 

스케줄이 그려지면서 변경된 hour12 배열의 값을 반복문으로 출력하면 시간대별 값을 데이터로 출력하는 것도 어렵지 않습니다.

해당 시간 값도 스케줄 텍스트를 그린 방법을 사용한다면 어렵지 않게 그룹(mapping, groupping)할 수 있을 것 입니다.

구간별 시간값이 잘 나옵니다.

 

위 내용은 단순하게 12시간 단위로 표기를 하였습니다.

하지만, hour12 배열의 값을 절반으로 나누면 24시간 표기도 가능 합니다.

이것저것 기능을 붙여본 모습 입니다.

이미지 출력기능도 달아보았습니다~ 나름 괜찮네요..:)

천천히 살펴보세요! 마지막에 이미지도 출력됩니다! :)

 

 

좀 더 자세한 구동모습을 보시려면 아래 깃주소를 통해 확인하여주세요. :)

https://taeseungryu.github.io/sample/sampleView/circleSch/index.html

 

위 모든 내용이 웹(web)에서 동작하는 캔버스(canvas)로 이루어진 내용 입니다.

 * 플래쉬, 액션스크립트, 유니티 같은 응용 프로그램이 아닙니다~

이상으로 Html 캔버스를 활용한 원형 스케줄 기능에 대해서 정리하여 보았습니다.

 

해당 포스팅은 코드를 전부 공개하지 않고 개념만 정리한 내용입니다.

문의사항이나 궁금한 점은 언제든 댓글 또는 메일로 연락주세요~ :)

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

댓글