거의 알고리즘 일기장
svg drawing 구현과 문제, 그리고 simplify 방법 본문
https://kunkunwoo.tistory.com/307
최근 오늘의 그림일기라는 프로젝트를 리뉴얼하면서 기존의 webview를 이용한 raster drawing 방식에서 svg drawing 방식으로 바꾸었다. react-native-draw라는 모듈을 이용하였는데, 몇몇 문제가 있어 모듈을 살펴볼 일이 좀 있었다.
살펴보다보니 이 svg drawing의 방식 자체의 흥미가 들어서 간단하게 작은 웹 프로젝트를 만들어 실험해보았다.
실습 (간단한 drawing)
svg drawing canvas 자체를 만드는 건 간단하다!! html canvas에서 drawing을 만들때와 동일하다!
(물론, 이벤트의 좌표를 svg 좌표로 매핑하는 과정이 있기는 하다.)
//...
//mouse down
this.originSvgCanvas.addEventListener(
'mousedown',
this.mouseDown.bind(this),
);
//mouse move
this.originSvgCanvas.addEventListener(
'mousemove',
this.mouseMove.bind(this),
);
//mouse up
this.originSvgCanvas.addEventListener('mouseup', this.mouseUp.bind(this));
//...
이런 형태로
mouse down -> drawing 시작
mouse move -> drawing
mouse up -> drawing 끝
이런 로직만 짜주면 된다.
문제?
이 방식에는 문제가 있다.
바로 정점의 수가 너무 많다 (퍼포먼스의 문제)
내가 그린 간단한 선 그림을 한번 보자. 시각적으로 보기 편하게 각 정점마다 빨간 점을 추가적으로 그려주었다.
엄청나게 많은 정점수가 보일것이다. 이게 퍼포먼스에 문제를 끼친다.
그래서 이 문제를 해결하기위해 chatgpt 군에게 물어보았다.
Laplacian smoothing -> 곡선을 부드럽게
Ramer-Douglas-Peucker -> 정점의 수를 제거
Visvalingam -> 정점의 수를 제거
필자는 Laplacian smoothing + Ramer-Douglas-Peucker를 조합해 정점의 수를 제거하고 곡선을 부드럽게 하기로 했다.
Laplacian smoothing 알고리즘 적용
Laplacian smoothing 방법은 gpt 군이 설명한대로 제일 간단하게 곡선을 부드럽게 만드는 방법이다.
가중치와 얼마나 반복할지를 정하고
(인접한 점 (prev, next) 의 점의 합 - 현재의 점 *2 ) * 가중치를 더해주는 방식이다.
// laplacian smoothing
const laplacianSmooth = (
points: Point[],
iterations: number,
weight: number,
): Point[] => {
for (let i = 0; i < iterations; i++) {
// 첫번째 점은 고정
const newPoints: Point[] = [points[0]!];
for (let j = 1; j < points.length - 1; j++) {
const prev = points[j - 1]!;
const curr = points[j]!;
const next = points[j + 1]!;
const newX = curr.x + weight * (prev.x + next.x - 2 * curr.x);
const newY = curr.y + weight * (prev.y + next.y - 2 * curr.y);
newPoints.push({ x: newX, y: newY });
}
newPoints.push(points[points.length - 1]!);
points = newPoints;
}
return points;
};
적용하면 이런식으로 선이 부드러워진다.
하지만, 정점의 수의 차이는 없다.
이제 퍼포먼스에서 가장 중요한 정점의 수를 줄여보자.
Ramer-Douglas-Peucker 알고리즘 적용
이 방법은 허용치를 정해놓고, 허용치보다 큰 거리를 가진 점을 유지하며 다른 점들을 제거함으로써 주어진 점들을 단순화한다.
재귀적으로 배열을 두 부분으로 나눕니다.
각 부분의 시작점과 끝점 사이에 있는 점들 중에서 선분과 가장 멀리 떨어진 점을 찾습니다.
거리가 epsilon보다 크면, 해당 점을 기준으로 배열을 두 부분으로 나누고, 이 과정을 각 부분에 대해 반복합니다.
거리가 epsilon보다 작으면, 해당 점을 제거하고 시작점과 끝점만 남깁니다.
// 라머-도플러스-피커 알고리즘
const ramerDouglasPeucker = (points: Point[], epsilon: number): Point[] => {
if (points.length < 3) return points;
let maxDist = 0;
let index = 0;
for (let i = 1; i < points.length - 1; i++) {
const dist = distanceToSegment(
points[i]!,
points[0]!,
points[points.length - 1]!,
);
if (dist > maxDist) {
maxDist = dist;
index = i;
}
}
if (maxDist > epsilon) {
const firstHalf = points.slice(0, index + 1);
const secondHalf = points.slice(index);
const result1 = ramerDouglasPeucker(firstHalf, epsilon);
const result2 = ramerDouglasPeucker(secondHalf, epsilon);
return [...result1.slice(0, -1), ...result2];
} else {
return [points[0]!, points[points.length - 1]!];
}
};
적용하게 되면 점이 꽤나 줄어든다.
자세한 내용을 적지는 않겠지만, 여기서 L으로 그렸던 path들을 Q(2차 베지어)로 바꾸면 곡선에 대한 표현들이 괜찮아진다.
그러면 그림일기에서 쓴 라이브러리(react-native-draw)에서는 무슨 알고리즘을 썼을까??
여기서는 graphics gem 1권에 나온 AN ALGORITHM FOR AUTOMATICALLY FITTING DIGITIZED CURVES(612 ~ 625) 의 알고리즘을 사용하였다.
추가적인 휴리스틱 과정이 있고 꽤나 복잡하다.
관심이 있으면 아래의 link와 책을 보기 바란다. (책은 graphics gem 1 pdf 라고 구글에 치면 바로나온다.)
https://github.com/luncheon/simplify-svg-path/blob/main/index.ts
reference
https://github.com/BenJeau/react-native-draw
https://www.youtube.com/watch?v=jvPPXbo87ds&t=2891s
+ chatgpt
'web' 카테고리의 다른 글
기존 프로젝트를 git history를 유지한 상태로 monorepo에 프로젝트 합치기 (git history 유지한채로 repo 두개 합치기) (0) | 2023.08.29 |
---|---|
circle ci에서 ssh key가 있음에도 repo를 가져오지 못하는 현상 (0) | 2023.08.11 |
github issue template 추가하기 (0) | 2023.03.15 |
react context rerender 방지하기 (with: constate, use-context-selector) (0) | 2023.03.14 |
cra 환경에서 배포환경에 따라 .env 나누기 & 동작에 대해 톺아보기(with: env-cmd) (0) | 2023.03.13 |