
웹에서 프리 드로잉 밑바닥부터 구현하기
자연스러운 프리 드로잉 알고리즘을 완성해 가는과정 들어가며 콴다에서는 태블릿에서 문제를 풀고 피드백을 받는 솔루션을 제공하고 있습니다. 문제가 띄워진 대지 위에서 학생이 직접 풀이 과정을 적고 OMR에 답을 체크하며 태블릿에서도 실제로 문제를 푸는 듯한 경험을 제공합니다. 이때 iOS, Android, 웹을 포함한 다양한 환경에서 일관적이면서도 만족스러운 문제 풀이 경험을 제공하기 위해 웹의 Canvas API를사용합니다. 콴다과외 — 레벨테스트 처음에는 외부 라이브러리를 이용해 구현하였지만, 라이브러리에서 커스텀이 가능한 범위의 한계, 데이터가 많아졌을 때 발생하는 성능적인 이슈 등 의존성으로부터 오는 문제점이 조금씩 발견되었습니다. 추가로 더 많은 프로젝트에서 사용할 수 있도록 모듈화의 필요성이 제기되면서 의존성 없이 밑바닥부터 구현하기로 하였습니다. 그 과정에서 자연스러운 프리 드로잉을 구현하기 위해 제가 실제로 겪었던 기술적인 Challenge들과 이를 해결해 가는 과정을 공유해보려고합니다. 차근차근 정복해보기 가장 기본적인 프리드로잉 웹에서 유저의 터치 이벤트가 발생했을 때 가장 기본적으로 참고할 수 있는 데이터는 포인트 좌표입니다. 그리고 유저가 펜을 잡고 자유롭게 그린다면 다수의 터치 이벤트가 발생하게 됩니다. 그렇기 때문에 저희가 유저로부터 받은 데이터를 이용해 제일 먼저 생각해 볼 수 있는 방법은 입력받은 점들을 선으로 이어주는 것입니다. Pointer Move 이벤트를 통해 입력받은 점이 하나씩 추가될 때마다 이전 점과 현재 점을 이어주기만 하면 됩니다. 아래의 과정을 거치며 가장 기초적인 드로잉 알고리즘을 완성할 수있습니다. 점과 점을 직선으로잇는다. 하지만 아무래도 점들이 직선으로 이어지다 보니 아래와 같이 부드럽지않습니다. 점과 점을 직선으로 이었을 때의결과물 1. 곡선화하기 부드럽지 않다면 가장 첫 번째로 생각해 볼 수 있는 방법은 점들을 곡선으로 잇는 것일 겁니다. 그 중 프론트엔드 개발자가 CSS Animation을 다루며 한 번쯤 들어보았을 법한 베지어 곡선을알아봅시다. 위 사진과 같이 두 점이 있다고 가정해 봅시다. 왼쪽 점과 오른쪽 점을 각각 0.7과 0.3만큼 혼합한다면 아래와 같은 위치에 새로운 점을 찍을 수있습니다. 조금 더 보편적으로 두 점의 혼합 정도를 각각 1t, t (0 ≤ t ≤ 1)라고 한다면 아래와 같이 표현할 수도있겠네요! 이처럼 t를 일정하게 조절하며 두 점 사이의 점을 추정하는 것을 선형 보간 (lerping)이라고 합니다. 선형 보간 그리고 t를 0부터 1까지 조절하며 선형 보간의 결과를 자취로 남긴다면 이를 1차 베지어 곡선이라고 할 수있습니다. 1차 베지어곡선 하지만 이렇게 만들어진 1차 베지어 곡선은 흔히 우리가 곡선이라고 말하는 곡률이 전혀 없습니다. 그래서 1차 베지어 곡선 2개를 혼합하여 2차 베지어 곡선을 만들어줄 수있습니다. 2차 베지어곡선 더 나아가 2차 베지어 곡선 2개를 혼합하여 3차 베지어 곡선을, 점 n(n ≥ 2)개를 이용해 n-1차 베지어 곡선을 만들 수있습니다. 비로소 저희가 흔히 부드럽다고 얘기할 수 있는 곡선을 만들었습니다. 2차 베지어 곡선과 3차 베지어 곡선은 각각 Javascript의 Canvas API에서 제공하는 quadraticCurveTo, bezierCurveTo 함수를 이용해 만들 수 있으니 모든 준비는 끝났습니다. 이제 아래와 같이 드로잉 로직을 정리할 수있습니다. 1. 점이 3개 모일 때까지 기다린다. 2. 점 3개가 모이면 quadraticCurveTo를 이용해 2차 베지어 곡선을 그린다. 3. 12번을반복한다. 아래는 실제로 적용했을 때의 모습입니다. 생각보다 여전히 끊기는 느낌이 드는 것같습니다. 베지어 곡선을 적용했을 때의결과물 2. 스플라인 부드럽게하기 아래의 예시를 확인해봅시다. 하나의 베지어 곡선으로 이어준 점 3개에 대해서는 부드러울지 몰라도 각 곡선끼리는 부드럽게 이어지지 않는 것을 확인할 수 있습니다. 즉, 곡선끼리 부드럽게 이어주는 작업이 필요합니다. 이에 대한 해결책으로 먼저 n차 베지어 곡선을 생각해 볼 수 있습니다. 하지만 유저가 펜을 그리며 1000개의 점이 Input으로 들어왔고 999차 베지어 곡선으로 이어본다고 생각해 봅시다. 매우 비싼 작업이면서도 펜을 놓지 않았을 때 현재 찍힌 점에 의해 이전에 그렸던 선 또한 영향을 받게 됩니다. 저희가 원하는 작업은 가장 아름다운 곡선을 만들기보단 비싸지 않으면서도 Greedy하게 곡선을 그려주는 것이기 때문에 새로운 방법이 필요해보입니다. 다시 2차 베지어 곡선으로 이루어진 스플라인으로 넘어가 봅시다. 여기서 곡선과 곡선을 이어주는 접점에 초점을 맞추어보려고 합니다. 만약 저 접점이 그 이전 점과 다음 점의 중점에 위치한다면 어떻게될까요? 아래의 증명 과정을 거쳐 이전 곡선의 끝점 순간 기울기와 다음 곡선의 시작점 순간 기울기가 똑같아지는 것을 확인할 수있습니다. 증명 과정 그렇습니다! 접점의 위치를 교정해주면서 스플라인을 부드럽게 만들어주었습니다. 접점 교정 후의결과물 3. 즉각적으로 드로잉보여주기 이제 드로잉 결과물은 만족스러워졌습니다. 하지만 영상으로 본다면 아직 어색한 느낌이듭니다. 드로잉이 포인터를 따라간다는 느낌을준다. 아무래도 점 3개가 그려질 때까지는 어떠한 선도 보이지 않기 때문에 유저의 드로잉 이벤트에 느리게 반응하는 것처럼 보입니다. 이는 사용자 경험상 버벅이는 듯한 느낌을 주기 때문에 2차 베지어 곡선이 그려지기 전에 캔버스는 여전히 유저의 Input을 받고 있다는 것을 보여줘야 합니다. 따라서 점이 1개일 때 원, 점이 2개일 때 직선, 점이 3개일 때 2차 베지어 곡선을 그려주며 드로잉 이벤트에 즉각적으로 반응하도록 작업해 봅시다. 2번 방법(스플라인 부드럽게 하기)과 합쳐진다면 아래와 같은 Flow를타겠네요! 그 결과, 아래와 같이 즉각적으로 반응하면서도 부드러운 드로잉이완성되었습니다. 포인터 위치에 따라 즉각적으로 드로잉이그려진다. 4. Pen Event대응하기 지금까지 테스트한 환경은 PC Chrome + Mouse이었습니다. 위와 같은 일련의 알고리즘을 통해 만족스러운 결과물을 얻어냈지만 해당 드로잉 모듈이 가장 많이 사용될 Mobile Safari + Pen 환경에서는 막상 아래와 같이 천천히 그렸을 때 자글자글해지는 걸 볼 수있습니다. Mobile Safari + Pen 환경에서의결과물 일반적으로 터치 혹은 마우스 이벤트가 발생하면 포인트 좌표가 달라졌을 때 Pointer Move 이벤트가 발생합니다. 하지만 Pen의 경우 동일한 포인트라도 pressure, tilt 등 좌표 외적인 속성들이 달라질 수 있기 때문에 동일한 좌표에서 여러 개의 Pointer Move 이벤트가 발생할 수있습니다. 동일한 좌표임에도 펜압이 달라지니 연속적으로 Pointer Move 이벤트가발생한다. 결국 중복된 점에 곡선이 그려졌기 때문에 일련의 알고리즘이 속수무책이 되어버렸습니다. 하지만 저희는 펜의 감도를 포함한 전문적인 드로잉 기술을 필요로 하지 않기 때문에 동일한 좌표일 때는 이벤트를 재호출하지 않도록처리했습니다. if ( currentPoint.x === previousPoint.x && currentPoint.y === previousPoint.y ) ( return ) 5. 인접한 점간소화하기 그런데도 천천히 그렸을 때 Mobile Safari에서 여전히 자글자글하게 보이는 현상이 발생했습니다. 심지어 PC Safari에서도 동일한 현상이 발생했습니다. 이는 Chrome과 Safari에서 Pointer Move 이벤트를 서로 다르게 내려주는 데에서 원인을 찾을 수 있는데 아래와 같이 Chrome에서는 꽤 정밀한 좌표 데이터를 내려주지만, Safari에서는 좌표를 정수화하여 내려주는 것을 확인할 수있습니다. (좌: Safari, 우:Chrome) 그 결과, 아래와 같은 과정을 거치며 보정이 들어갔음에도 자글자글해지게됩니다. 대각선으로 그린 선이 자글자글해지는 과정 (: 실제 터치위치) 그래서 인접이라는 개념을 도입하였습니다. 사실 좌표가 정수화되었을 때 제일 큰 문제는 미시적인 관점에서 해당 좌표 데이터를 신뢰할 수 없다는 데에 있습니다. 즉, 직선을 의도하고 그렸더라도 인접한 점들은 반올림되어 직선의 형태가 나오지 않습니다. 따라서 두 점이 인접하다면 오차가 발생할 수 있다고 가정하고 그 중간 점을 생략하기로 했습니다. 이때 두 점 사이의 거리가 sqrt(10) (상하좌우로 1~3px)이내면 인접하다고 정의하였고 그 안에서의 보정은 사람이 인지하기 어렵다고판단했습니다. 인접한 점을 지운 이후 (: 실제 터치위치) 위와 같은 과정을 거치며 선들은 인접하지 않은 점들을 연결하고, 아래와 같이 천천히 그리더라도 자글거리지 않는 드로잉 결과물을 만들 수있었습니다. 인접한 점을 지웠을 때의결과물 6. 인접하지 않은 점쪼개기 이제 어느 플랫폼에서 어떻게 그리든 꽤 부드럽고 자연스럽게 그려지는 것 같습니다. 하지만 아래 영상과 같이 빠른 속도로 곡률이 크게 드로잉을 하면 보정 효과가 과하게 들어가는 듯한 느낌이보입니다. 원을 빠르게 그릴 때 보정 효과가 눈에띈다. 바로 2번 방법(스플라인 부드럽게 하기)과 3번 방법(즉각적으로 드로잉 보여주기)을 적용하며 생겨난 Side effect인데 곡률이 큰 경우 아래와 같은 과정을 거치며 사용자에게 잔상이 남아보였습니다. 이를 해결하기 위해 5번(인접한 점 간소화하기)에서 도입한 인접을 기준으로 두 가지 관점을 설정해보았습니다. 미시적: 자글거리지 않아야 하고 보정 효과가 들어가도 사람이 인지하기 어렵다. 거시적: 실제 그린 것과 유사해야 하고 보정 효과가 들어가면 사람이 인지하기쉽다. 5번 방법(인접한 점 간소화하기)을 적용했으니 이제 캔버스에 남는 점들은 적어도 인접하지 않습니다. 인접하지 않다는 것은 남아있는 점들을 선으로 이을 땐 거시적인 관점을 고려해야 한다는 것과 동일합니다. 이때 2번 방법(스플라인 부드럽게 하기)은 거시적인 관점에서 보정 효과가 크다고 생각했고 여기서는 실제 그린 것과 유사하게 보정이 들어가야 자연스럽다는 결론에 도달했습니다. 따라서 2번 방법에서 접점을 옮기는 대신 2번째 점과 3번째 점의 중점을 만들어 해당 중점을 접점으로 두고 2차 베지어 곡선을 그리는 방식으로 살짝 알고리즘을수정했습니다. 그 결과 아래와 같이 빠르게 원을 그렸을 때도 어색하지않아졌습니다. 원을 빠르게 그릴 때 보정 효과가 비교적덜해보인다. 마치며 지금까지 사용자의 드로잉 데이터가 위 6가지의 보정 과정을 거치면서 웹 캔버스에 자연스럽게 노출되는 과정을 소개했습니다. 개인별로 선호하는 필기감이 다르기에 가장 이상적인 드로잉 알고리즘을 적용하기란 꽤 어려운 일입니다. 그럼에도학생이 태블릿에서 문제를 푸는 과정이 자연스러워야 한다는 목적을 달성해 나가며 다양한 사람들의 의견을 듣고 수많은 테스트를 진행하는 과정은 흥미로웠습니다. 콴다 팀은 여전히 개선점을 찾고 있으며 더 자연스러운 드로잉을 제공하기 위해 지금도 다양한 방법을 시도해 보고있습니다. 글로벌 Top 교육 앱 QANDA(콴다)를 함께 만들어 갈 개발자들을 기다리고 있습니다! ➡️ 공고확인하기 웹에서 프리 드로잉 밑바닥부터 구현하기 was originally published in Team QANDA on Medium, where people are continuing the conversation by highlighting and responding to this story.
