왜 세 개의 하이라이터 지오메트리 엔진을 스탬프로 교체했는가
실제 Apple Pencil 데이터가 합성 테스트에서 놓친 결함을 드러내며,Lulucat Notes를 세 개의 외곽선 엔진에서 MaLiang 스탬프 모델로 이끌었다。
Lulucat Notes는 전형적인 하이라이터 렌더러로 시작했습니다。샘플링된 경로를 감싸는 기하학적 외곽선을 만든 다음,그 외곽선을 반투명한 색으로 한 번 채우는 방식이었습니다。한 번만 채우면 반투명한 세그먼트가 겹치는 곳에 나타나는 어두운 띠를 피할 수 있습니다。하지만 모든 연결부,자기 교차,캡,짧은 스트로크를 하나의 동일한 지오메트리 문제 안에서 다뤄야 하기도 합니다。
커스텀 지오메트리 세 세대는 각각 한 종류의 결함을 없애는 대신 다른 결함을 드러냈습니다。현재 배포하는 렌더러는 Harley-xk가 만든 MIT 라이선스 오픈 소스 드로잉 프레임워크 MaLiang을 바탕으로 한 부드러운 원형 스탬프를 사용합니다。외곽선이 실패하는 범주 하나를 통째로 없애는 대신,더 어두운 자기 교차와 압력에 따라 가늘어지는 끝부분을 받아들였습니다。

228%로 확대한 원본 기기 스크린샷。여러 스트로크의 양쪽 끝에 작은 훅이 나타납니다。
실제 필기에서는 합성 장면에 없던 훅이 생겼다
합성 장면에는 직선,호,짧은 탭,되돌아가기,압력 변화,급격한 방향 전환을 넣었고 모두 깨끗하게 렌더링되었습니다。그런데 실제 중국어 필기에서는 여전히 많은 스트로크의 시작과 끝에 길이 약 1–3포인트인 훅이 생겼습니다。
그래서 앱에 저장 버튼을 추가하고 완전한 스트로크 29개를 기록했습니다。각 샘플에는 위치,반지름,압력,방위각,고도,타임스탬프,UIKit 추정 플래그가 모두 보존되었습니다。오프라인 프로그램은 해당 JSON을 디코드하고,앱이 사용하는 것과 동일한 Stroke.swift,StrokeRenderer.swift,HighlighterStrokeBuilder.swift 파일을 컴파일했습니다。그 결과는 훅까지 포함해 기기 스크린샷과 스트로크 단위로 일치했습니다。
오프라인 렌더러가 기록된 입력만으로 결함을 재현했으므로 원인은 디스플레이 동작이 아니라 샘플과 지오메트리에 있었습니다。이제 렌더러에 들어간 정확한 샘플을 읽을 수 있었습니다。
처음에는 터치다운과 리프트오프 시점의 추정 Pencil 방향이 원인이라고 생각했습니다。데이터는 이 가설을 부정했습니다。29개 스트로크 전체에서 처음과 마지막 여덟 개 샘플 안의 방위각 변화는 최대 0.09라디안,고도 변화는 최대 0.02라디안이었고,해당 iPad에서는 모든 추정 플래그가 false였습니다。방향은 안정적으로 유지되는 동안 압력과 진행 방향만 급격히 바뀌었습니다。
압력 하한은 데이터의 뚜렷한 간격에서 나왔다
기록된 모든 스트로크의 마지막 샘플 압력은 0.000이었습니다。필기 중인 샘플은 0.01 이상이었지만,Pencil이 표면을 떠난 뒤 기록된 샘플은 0.002 이하였습니다。공중 샘플은 2–7포인트 동안 이어지는 경우가 많았고 0.8–9.0포인트를 이동했으며,방향이 필기한 스트로크에서 약 90도 벗어나 돌아가기도 했습니다。
스트로크 2가 이 메커니즘을 보여줍니다。본체는 143도와 180도 사이에서 끝났습니다。이후 저압 꼬리가 −90도에서 −67도까지 방향을 바꾸며 8.5포인트를 더 이동했습니다。외곽선 렌더러는 마지막 공중 샘플을 실제 끝점으로 간주했고,그 점을 향해 가느다란 지오메트리 조각을 늘였습니다。
측정된 압력은 필기 동작과 공중 이동 사이에 거의 한 자릿수에 해당하는 차이를 보였습니다。그래서 그 간격 안에 컷오프를 두었습니다。
private static let pressureFloor: CGFloat = 0.005
양쪽 끝에 있는 저압 샘플만 제거합니다。스트로크 중간의 저압 샘플은 스트로크의 일부로 남습니다。이 규칙은 측정된 이벤트 경계를 따르며,실제로 쓴 경로는 수정하지 않습니다。

트리밍 전에는 렌더러가 공중 샘플에 캡을 고정합니다。초록 점은 원시 샘플이고,빨간 선은 생성된 외곽선입니다。

0.005 미만의 샘플을 트리밍하면 캡은 마지막 필기 지점에서 끝납니다。이 압력 하한은 현재 스탬프 렌더러에도 남아 있습니다。
이 하한은 공중에서 길게 이어지는 훅을 제거했습니다。압력이 0.005 이상으로 유지된 짧은 터치다운 드래그와 리프트오프 호는 실제로 쓴 동작이므로 살아남았습니다。이 동작을 외곽선 모델 안에서 처리하려 한 과정이 다음 지오메트리 라운드로 이어졌습니다。
세 지오메트리 엔진은 결함의 위치만 옮겼다
첫 세대: 하나의 외곽선,한 번의 채움
첫 번째 압력 감지 엔진은 익숙한 파이프라인을 따랐습니다。중심선을 부드럽게 만들고 왼쪽과 오른쪽 오프셋을 계산한 뒤 하나의 폴리곤으로 연결하고,그 폴리곤을 한 번 채웠습니다。펜촉은 Apple Pencil의 방위각과 고도로 움직이는 타원이었으므로 횡단면이 필기 방향에 따라 달라졌습니다。짧은 탭에는 별도 모양을 사용했고,평평한 캡에는 와인딩이 0인 포켓을 피하기 위한 작은 비대칭을 넣었습니다。
한 번의 채움은 스트로크 내부에서 불투명도가 누적되는 문제를 해결했지만,모든 방향 전환에서 유효한 폴리곤이 필요했습니다。터치다운 드래그는 캡과 본체 사이에 쐐기 모양을 만들었습니다。되돌아가기는 흰색 초승달 포켓을 만들 수 있었습니다。급격한 방향 전환에서는 안쪽 외곽선이 자기 교차를 일으켜 채워지지 않은 홈을 남길 수 있었습니다。
두 번째 세대: 끝부분 근처의 외곽선을 수리하기
두 번째 엔진은 외곽선을 유지하면서 국소적인 수리를 추가했습니다。되돌아가기 트리밍,초기에 추정된 속성에 대한 감쇠,끝점 직선화,일시적 방향 트리밍,작은 캡 연장이었습니다。각 패치는 재현된 실패 하나를 해결했습니다。하지만 이들을 함께 적용하자 끝점 동작이 여러 임계값과 윈도우에 의존하게 되었습니다。
실제 필기에서는 그 윈도우 바깥의 사례가 나타났습니다。고정된 24포인트 직선화 윈도우는 짧은 스트로크에 의도적으로 넣은 훅을 지웠습니다。적응형 윈도우는 훅을 보존했지만,리프트오프 호가 여전히 눈에 보이는 모서리가 될 수 있었습니다。방향을 고려한 트리밍은 더 많은 일시적 움직임을 제거했지만,기록된 한 스트로크에서는 실제로 휘어진 시작 부분 약 2.5포인트를 버려야 했습니다。
각 패치는 목표로 삼은 사례를 고쳤습니다。그러나 그 아래의 모델은 잡음이 있고 휘어진 입력에서도 유효하게 유지되는 단단한 외곽선을 여전히 요구했습니다。
세 번째 세대: 잘린 정사각형 캡을 사용한 커버리지 합집합
세 번째 엔진은 하나의 외곽선을 만드는 일을 그만두었습니다。그레이스케일 마스크에 각 세그먼트마다 사다리꼴을 그리고 각 내부 샘플마다 원판을 그린 뒤,커버리지를 합집합하고 하이라이터 색을 한 번 적용했습니다。렌더러가 더 이상 폴리곤 와인딩에 의존하지 않게 되면서 되돌아가기 초승달,자기 교차로 인한 어두워짐,와인딩이 0인 구멍이 사라졌습니다。
합집합은 끝을 둥글게 만들었기 때문에,두 끝점 반평면에 맞춰 커버리지를 잘라 정사각형 캡을 복원했습니다。이 클립은 압력이 일정한 직선을 고쳤지만 더 심각한 결함을 만들었습니다。휘어진 스트로크의 본체는 끝점 반평면을 정상적으로 가로지를 수 있는데,캡 클립이 스트로크 중간의 일부까지 제거한 것입니다。라이브 입력 중에는 끝점 방향이 계속 바뀌므로 잘린 영역이 움직이고 스트로크가 깜빡였습니다。

전역 캡 제약이 휘어진 스트로크의 본체를 가로질러 잘라냅니다。

끝점에서 반 너비 이내의 프리미티브에만 캡 클리핑을 적용하면 이 스트로크가 복원됩니다。
호 길이 게이트는 캡 클리핑을 국소적으로 만들었고,기록된 모든 사례를 고쳤습니다。그래도 극단적인 합성 훅에서는 저압 끝이 잘린 캡과 만나는 지점에 작은 흰색 홈이 나타났습니다。세 번째 엔진 역시 한 지오메트리 실패를 다른 실패와 맞바꾼 셈입니다。
MaLiang은 단단한 경계를 없앤다
MaLiang은 하나의 닫힌 외곽선 대신 텍스처 스탬프를 반복해서 찍어 스트로크를 만듭니다。경로 생성기는 인접한 샘플 중점을 통과하는 이차 Bézier 세그먼트를 사용하고,선 렌더러는 그 경로를 따라 일정한 간격으로 스탬프를 배치합니다。스트로크를 따라 스탬프의 크기,회전,색상,불투명도를 모두 바꿀 수 있습니다。
부드러운 스탬프에는 별도의 캡,연결부,와인딩 규칙이 없습니다。방향이 급격히 바뀌어도 겹치는 표시의 연속으로 남습니다。모델에 끝점 클립이 없기 때문에 되돌아가기가 빈 폴리곤 포켓을 만들 수 없고,끝점 반평면이 스트로크 중간을 잘라낼 수도 없습니다。
우리는 코드를 가져온 것이 아니라 모델을 가져왔습니다。현재 HighlighterStrokeBuilder는 네 가지 작업을 수행합니다。
- 압력이
0.005미만인 끝점 샘플만 제거합니다。 - 중점 이차 Bézier 세그먼트로 위치를 평활화합니다。
- 호 길이의 각 지점마다 원형 스탬프 하나를 배치합니다。
- 정규화된 압력으로 스탬프 직경을 정하고 source-over 알파로 스탬프를 합성합니다。
빌더는 686줄에서 190줄로 줄었습니다。방위각,고도,외곽선 구성,합집합 마스크,끝점 방향 윈도우,캡 연장,캡 클리핑은 모두 하이라이터 경로에서 빠졌습니다。입력 모델이 이를 지원하기 때문에 Pencil 방향은 계속 기록하지만,하이라이터 자체는 더 이상 해당 필드를 읽지 않습니다。
MaLiang의 압력 공식에는 기기 보정이 필요했다
MaLiang은 다음과 동등한 공식으로 스탬프 크기를 정합니다。
이 공식은 force가 0에서 1 사이 범위의 유용한 부분을 사용한다고 가정합니다。우리 기기의 데이터는 그렇지 않았습니다。약 3,400개의 Apple Pencil 샘플에서 압력 중앙값은 0.047,최댓값은 0.178이었습니다。이 값을 공식에 그대로 넣으면 일반적인 필기가 명목 브러시 크기보다 훨씬 가늘어졌습니다。
그래서 관측된 중앙값을 기준으로 정규화한 다음 지수를 적용하고 결과를 클램프했습니다。
이제 필기 압력의 중앙값은 명목 12포인트 직경에 대응합니다。아주 가벼운 접촉도 명목 크기의 35%로 보이며,강한 압력은 120%에서 더 커지지 않습니다。
이 보정은 MaLiang이 아니라 입력 기기와 브러시 동작에 속합니다。우리 자신의 압력 분포를 측정하지 않고 원래 공식을 복사했다면 아키텍처는 유지했겠지만 잘못된 브러시를 만들었을 것입니다。
스탬프당 알파는 알려진 겹침 횟수에서 나왔다
MaLiang은 경험식 alpha ÷ overlapping × 2.5로 겹치는 반투명 스탬프를 보정합니다。우리 브러시는 명목 직경 12포인트와 스탬프 간격 1포인트로 고정되어 있으므로 중심선에는 약 12개 레이어가 쌓입니다。이 경우 겹침 횟수를 알고 있으므로 보정값을 추정하지 않고 계산할 수 있습니다。
목표 스트로크 불투명도가
따라서 스탬프 하나에는 다음이 필요합니다。

기록된 기기 경로를 재생하는 최종 렌더러。훅,대각선 절단,흰색 포켓이 없습니다。
우리가 받아들인 두 가지 비용
자기 교차가 더 어두워집니다。source-over 합성은 같은 영역을 이전에 통과하면서 찍힌 스탬프까지 포함해 모든 스탬프를 셉니다。커버리지 합집합 엔진은 이를 피했습니다。그래도 어두워지는 겹침은 예측 가능하고 스트로크의 형태를 온전히 유지하므로 받아들였습니다。
끝점은 압력에 따라 가늘어집니다。실제로 Pencil을 들어 올리면 압력이 줄고 스탬프 직경도 줄어들기 때문에,스트로크는 전체 너비의 정사각형 캡으로 끝나는 대신 가늘어집니다。세 지오메트리 엔진 모두에서 우리는 정사각형 캡을 요구 사항으로 취급했습니다。이를 포기하자 위에서 설명한 여러 실패의 배경에 있던 클리핑과 끝점 처리 장치를 없앨 수 있었으므로 가늘어진 끝을 받아들였습니다。
따라서 우리가 배포하는 모델은 이전 모델보다 하는 일이 적습니다。측정된 필기 이벤트를 보존하고,기기 보정 후 압력에 반응하며,전역적으로 유효한 외곽선을 요구하지 않고 곡선 경로와 자기 교차 경로를 렌더링합니다。끌 모양의 펜촉,완벽하게 정사각형인 캡,원시 샘플 사이의 정확한 폴리라인은 그 대가로 포기한 기능입니다。
압력 하한은 다른 프로젝트로 가장 직접적으로 옮길 수 있는 부분입니다。0.005라는 값은 실제 입력에서 두 군집 사이의 간격에서 나왔습니다。필기는 0.01 이상,공중 추적은 0.002 이하였습니다。같은 기록 스트로크가 각 지오메트리 수리가 어디에서 실패했는지도 보여주었습니다。그 실패들이 스탬프 모델과 그 모델이 지닌 두 가지 비용으로 이어졌습니다。