Lulucat

Por qué reemplazamos tres motores geométricos del resaltador por sellos

Gaoge ZhangGaoge Zhang

Los datos reales del Apple Pencil revelaron fallas que las pruebas sintéticas no detectaron y llevaron a Lulucat Notes de tres motores de contorno a un modelo de sellos de MaLiang.

Actualizado

Lulucat Notes comenzó con un renderizador convencional de resaltador: construir un contorno geométrico alrededor de la trayectoria muestreada y rellenarlo una sola vez con un color translúcido. Un solo relleno evita las bandas más oscuras que aparecen donde se superponen segmentos translúcidos. También hace que cada unión, autointersección, remate y trazo corto forme parte del mismo problema geométrico.

Cada una de las tres generaciones de geometría personalizada eliminó una clase de falla y expuso otra. El renderizador que distribuimos ahora utiliza sellos circulares suaves basados en MaLiang, un framework de dibujo de código abierto con licencia MIT creado por Harley-xk. A cambio de eliminar toda una categoría de fallas del contorno, aceptamos autointersecciones más oscuras y extremos afinados por la presión.

Escritura azul de resaltador capturada en un iPad, con ganchos delgados visibles al principio y al final de varios trazos.

La captura de pantalla original del dispositivo con un zoom del 228 %. Aparecen pequeños ganchos en ambos extremos de varios trazos.

La escritura real produjo ganchos que las escenas sintéticas no producían

Nuestras escenas sintéticas cubrían líneas rectas, arcos, toques breves, retrocesos, cambios de presión y giros bruscos, y todos se renderizaban correctamente. La escritura china real aún producía ganchos de aproximadamente 1–3 puntos al principio y al final de muchos trazos.

Por eso agregamos un botón para guardar en la app y registramos 29 trazos completos. Cada muestra conservó su posición, radio, presión, acimut, altitud, marca de tiempo y banderas de estimación de UIKit. Un programa sin conexión decodificó ese JSON y compiló los mismos archivos Stroke.swift, StrokeRenderer.swift y HighlighterStrokeBuilder.swift que usa la app. Su salida coincidió trazo por trazo con la captura del dispositivo, incluidos los ganchos.

Como el renderizador sin conexión reprodujo la falla únicamente a partir de la entrada registrada, la causa estaba en las muestras y la geometría, no en el comportamiento de la pantalla. Ahora podíamos leer las muestras exactas que habían entrado al renderizador.

Nuestra primera hipótesis culpó a la orientación estimada del Pencil al hacer contacto y despegarse. Los datos la rechazaron. En los 29 trazos, el acimut cambió como máximo 0.09 radianes y la altitud como máximo 0.02 radianes dentro de las primeras y últimas ocho muestras, y todas las banderas de estimación eran falsas en ese iPad. La orientación se mantuvo estable mientras la presión y la dirección cambiaban bruscamente.

Un umbral de presión surgió de una separación clara en los datos

La última muestra de cada trazo registrado tenía una presión de 0.000. Las muestras durante la escritura estaban en 0.01 o por encima, mientras que las muestras registradas después de que el Pencil había abandonado la superficie estaban en 0.002 o por debajo. Esas muestras en el aire a menudo continuaban durante 2–7 puntos y recorrían 0.8–9.0 puntos, y su dirección podía girar aproximadamente 90 grados alejándose del trazo escrito.

El trazo 2 muestra el mecanismo. Su cuerpo terminaba entre 143 y 180 grados. La cola de baja presión luego giraba de −90 a −67 grados y recorría otros 8.5 puntos. El renderizador de contornos trataba el último punto en el aire como el punto final de referencia, así que estiraba una pieza estrecha de geometría hacia ese punto.

Las presiones medidas muestran una diferencia de casi un orden de magnitud entre la escritura y el movimiento en el aire, así que colocamos el umbral dentro de esa separación:

private static let pressureFloor: CGFloat = 0.005

Solo se eliminan las muestras de baja presión de los dos extremos. Una muestra de baja presión en medio de un trazo sigue formando parte de él. La regla sigue el límite medido entre eventos y deja intacta la trayectoria escrita.

Acercamiento instrumentado antes del recorte: las muestras de entrada verdes se alejan del trazo mientras el contorno rojo forma un gancho estrecho.

Antes del recorte, el renderizador ancla el remate a las muestras en el aire. Los puntos verdes son muestras sin procesar; la línea roja es el contorno generado.

Acercamiento instrumentado después del recorte: el relleno azul termina en las últimas muestras escritas y los puntos verdes de baja presión quedan fuera.

Después de recortar las muestras por debajo de 0.005, el remate termina en el último punto escrito. Este umbral de presión se mantiene en el renderizador actual basado en sellos.

El umbral eliminó los ganchos largos en el aire. Los arrastres breves al hacer contacto y los arcos al despegarse cuya presión se mantuvo por encima de 0.005 sobrevivieron, porque son movimientos escritos. Manejar esos movimientos dentro de un modelo de contorno llevó a las siguientes rondas de geometría.

Tres motores de geometría movieron las fallas de lugar

Generación uno: un contorno, un relleno

El primer motor sensible a la presión seguía un flujo conocido. Suavizaba una línea central, calculaba desplazamientos a izquierda y derecha, los unía en un solo polígono y rellenaba ese polígono una vez. La punta era una elipse controlada por el acimut y la altitud del Apple Pencil, por lo que su sección transversal cambiaba con la dirección de escritura. Los toques breves tenían una forma aparte, y los remates planos usaban pequeñas asimetrías para evitar cavidades de winding cero.

El relleno único resolvía la acumulación de opacidad dentro de un trazo y exigía un polígono válido en cada giro. Los arrastres al hacer contacto creaban cuñas entre el remate y el cuerpo. Los retrocesos podían crear cavidades blancas en forma de media luna. Los giros bruscos podían hacer que el contorno interior se autointersectara y dejara una muesca sin relleno.

Generación dos: reparar el contorno cerca de sus extremos

El segundo motor conservó el contorno y agregó reparaciones locales: recorte de retrocesos, amortiguación para las propiedades estimadas iniciales, enderezamiento de los extremos, recorte de la dirección transitoria y una pequeña extensión del remate. Cada parche abordaba una falla reproducida. Juntos hicieron que el comportamiento de los extremos dependiera de varios umbrales y ventanas.

La escritura real produjo entonces casos fuera de esas ventanas. Una ventana fija de enderezamiento de 24 puntos borraba los ganchos intencionales en trazos cortos. Una ventana adaptativa conservaba esos ganchos, y los arcos al despegarse aún podían convertirse en esquinas visibles. El recorte consciente de la dirección eliminaba más movimiento transitorio, a costa de descartar aproximadamente 2.5 puntos de un inicio realmente curvo en un trazo registrado.

Cada parche corregía el caso al que estaba dirigido. El modelo subyacente aún requería un contorno rígido que siguiera siendo válido con entradas curvas y ruidosas.

Generación tres: unión de cobertura con remates cuadrados recortados

El tercer motor dejó de construir un contorno único. Dibujaba un trapecio por cada segmento y un disco en cada muestra interior dentro de una máscara en escala de grises, tomaba la unión de sus coberturas y luego aplicaba el color del resaltador una sola vez. Las medias lunas de los retrocesos, el oscurecimiento de las autointersecciones y los agujeros de winding cero desaparecieron, porque el renderizador ya no dependía del winding del polígono.

La unión redondeaba los extremos, así que restauramos los remates cuadrados recortando la cobertura contra dos semiplanos de los extremos. Ese recorte corregía las líneas rectas con presión constante e introducía una falla más grave: el cuerpo de un trazo curvo puede cruzar legítimamente un semiplano del extremo, y entonces el recorte del remate eliminaba parte del centro del trazo. Durante la entrada en vivo, la dirección del extremo seguía cambiando, así que el área recortada se movía y el trazo parpadeaba.

Un trazo azul curvo antes de corregir el recorte del remate, con su cuerpo superior cortado a lo largo de una línea diagonal; los puntos verdes muestran toda la trayectoria muestreada.

La restricción global del remate atraviesa el cuerpo de un trazo curvo.

El mismo trazo azul curvo después de agregar un filtro local de longitud de arco, con ambas ramas restauradas alrededor de la trayectoria muestreada en verde.

Limitar el recorte del remate a las primitivas que están dentro de una semianchura del extremo restaura este trazo.

Un filtro de longitud de arco hizo local el recorte del remate y corrigió todos los casos registrados. Un gancho sintético extremo aún mostraba una pequeña muesca blanca donde una punta de baja presión se unía al remate recortado. El tercer motor había vuelto a intercambiar una falla geométrica por otra.

MaLiang elimina el límite rígido

MaLiang construye un trazo a partir de sellos de textura repetidos en lugar de un contorno cerrado. Su generador de trayectorias usa segmentos cuadráticos de Bézier que pasan por los puntos medios de muestras adyacentes, y su renderizador de líneas coloca sellos a intervalos regulares a lo largo de esa trayectoria. El tamaño, la rotación, el color y la opacidad del sello pueden variar a lo largo del trazo.

Un sello suave no tiene un remate, una unión ni una regla de winding independientes. Un cambio brusco de dirección sigue siendo una secuencia de marcas superpuestas. Los retrocesos no pueden crear una cavidad vacía en un polígono, y un semiplano del extremo no puede cortar el centro de un trazo, porque el modelo no contiene ningún recorte del extremo.

Adoptamos el modelo, no el código. El HighlighterStrokeBuilder actual realiza cuatro operaciones:

  1. Eliminar solo las muestras de los extremos cuya presión sea inferior a 0.005.
  2. Suavizar las posiciones con segmentos cuadráticos de Bézier en los puntos medios.
  3. Colocar un sello circular por cada punto de longitud de arco.
  4. Establecer el diámetro del sello a partir de la presión normalizada y componer los sellos con alfa de fuente sobre destino (source-over).

El constructor pasó de 686 líneas a 190. El acimut, la altitud, la construcción del contorno, las máscaras de unión, las ventanas de dirección de los extremos, la extensión del remate y el recorte del remate dejaron de formar parte de la trayectoria del resaltador. Aún registramos la orientación del Pencil porque el modelo de entrada la admite, pero el resaltador ya no lee esos campos.

La fórmula de presión de MaLiang necesitó calibración del dispositivo

MaLiang dimensiona un sello con una fórmula equivalente a

que supone que force usa una parte útil del intervalo entre 0 y 1. Nuestros datos del dispositivo no lo hacían. Entre unas 3,400 muestras del Apple Pencil, la presión mediana era 0.047 y la máxima era 0.178. Introducir esos valores directamente en la fórmula hacía que la escritura normal fuera mucho más delgada que el tamaño nominal del pincel.

Por eso normalizamos alrededor de la mediana observada, aplicamos el exponente y limitamos el resultado:

La presión mediana de escritura ahora se asigna al diámetro nominal de 12 puntos. Un contacto muy ligero sigue siendo visible al 35 % del tamaño nominal, mientras que la presión fuerte deja de aumentar al 120 %.

Esta calibración corresponde al dispositivo de entrada y al comportamiento del pincel, no a MaLiang. Copiar la fórmula original sin medir nuestra propia distribución de presión habría conservado la arquitectura y producido el pincel equivocado.

El alfa por sello surgió del número conocido de superposiciones

MaLiang compensa los sellos translúcidos superpuestos con la expresión empírica alpha ÷ overlapping × 2.5. Nuestro pincel tiene un diámetro nominal fijo de 12 puntos y un intervalo entre sellos de 1 punto, así que su línea central recibe aproximadamente 12 capas. En nuestro caso, el número de superposiciones es conocido y la compensación se puede resolver en lugar de estimar.

Para una opacidad objetivo y capas iguales de fuente sobre destino, una opacidad de sello produce

por lo que un sello requiere

Con y , cada sello usa una opacidad de aproximadamente . El centro alcanza el color previsto después de la composición repetida, mientras que el borde recibe menos sellos y conserva una transición difuminada de uno o dos puntos.

Una hoja con diversos trazos azules de resaltador renderizados por el modelo final de sellos suaves, incluidos bucles, ganchos, marcas cortas y autointersecciones.

El renderizador final reproduce las trayectorias registradas del dispositivo. No hay ganchos, cortes diagonales ni cavidades blancas.

Los dos costos que aceptamos

Las autointersecciones se vuelven más oscuras. La composición de fuente sobre destino cuenta cada sello, incluidos los sellos colocados por un recorrido anterior sobre la misma área, y el motor de unión de cobertura evitaba eso. Aceptamos la superposición más oscura porque es predecible y conserva la forma del trazo.

Los extremos se estrechan con la presión. Un despegue real reduce la presión, lo que reduce el diámetro del sello, así que el trazo se afina en lugar de terminar con un remate cuadrado de ancho completo. Habíamos tratado ese remate cuadrado como un requisito durante los tres motores de geometría. Abandonarlo eliminó el recorte y la maquinaria de los extremos detrás de varias de las fallas anteriores, así que aceptamos el extremo afinado.

Por lo tanto, el modelo que distribuimos hace menos que los anteriores. Conserva el evento de escritura medido, responde a la presión después de calibrar el dispositivo y renderiza trayectorias curvas y que se cruzan a sí mismas sin exigir un contorno globalmente válido. Una punta en forma de cincel, un remate perfectamente cuadrado y la polilínea exacta entre las muestras sin procesar son las características a las que renunciamos por ello.

El umbral de presión es lo que se transfiere más directamente a otros proyectos. El valor 0.005 surgió de la separación entre dos grupos en la entrada real: escritura en 0.01 o por encima y seguimiento en el aire en 0.002 o por debajo. Esos mismos trazos registrados mostraron dónde fallaba cada reparación geométrica. Esos fallos llevaron al modelo de sellos y a los dos costos que conlleva.