Imitación del dibujo a mano alzada en el ejemplo de RoughJS

RoughJS es una pequeña biblioteca de gráficos JavaScript (< 9KB ) que le permite dibujar en un estilo incompleto y escrito a mano . Te permite dibujar sobre <canvas>y con SVG. En esta publicación quiero responder la pregunta más popular sobre RoughJS: ¿cómo funciona?


Un poco de historia


Fascinado por las imágenes de gráficos, diagramas y bocetos dibujados a mano, me pregunté, como un verdadero nerd: ¿puedo crear tales dibujos con la ayuda de un código, puedo imitar el dibujo con la mayor precisión posible a mano, sin dejar de preservar la posibilidad de implementación de software? Decidí centrarme en las primitivas (líneas, polígonos, elipses y curvas) para crear una biblioteca completa de gráficos 2D. En base a esto, puede crear bibliotecas y gráficos para dibujar gráficos y diagramas.

Después de examinar brevemente el problema, encontré un artículo de Joe Wood y sus colegas llamado Sketchy rendering para visualización de información . Las técnicas descritas en él se convirtieron en la base de la biblioteca, especialmente en el dibujo de líneas y elipses.

En 2017, escribí la primera versión de la biblioteca, que solo funcionaba en Canvas. Habiendo resuelto el problema, perdí interés en él. Un año después, trabajé mucho con SVG y decidí adaptar RoughJS para trabajar con SVG. También cambié la estructura de la API para hacerlo más simple y me centré en primitivas de gráficos vectoriales simples. Hablé sobre la versión 2.0 en Hacker News y de repente ganó una inmensa popularidad. En 2018, fue la segunda publicación más popular de ShowHN .

Desde entonces, otras personas han creado cosas más sorprendentes basadas en RoughJS, por ejemplo, Excalidraw , Why do Cats & Dogs ... , la biblioteca de gráficos roughViz .

Ahora hablemos de algoritmos ...

Desigualdad


La base fundamental para la imitación de figuras escritas a mano es el azar. Cuando dibujamos a mano, cualquiera de las dos formas será algo diferente. Nadie dibuja con precisión, por lo que cada punto espacial en RoughJS se ajusta para el desplazamiento aleatorio. La magnitud de la aleatoriedad viene dada por un parámetro numérico roughness.


Imagina un punto Ay un círculo a su alrededor. Ahora reemplace con un Apunto aleatorio dentro de este círculo. El área de este círculo de aleatoriedad está controlada por el valor roughness.

Líneas


Las líneas escritas a mano nunca son rectas y a menudo muestran curvatura en la curva (descrita aquí ). Aleatorizamos los dos puntos finales de la línea según la rugosidad. Luego seleccionamos dos puntos aleatorios más aproximadamente a una distancia del 50% y 75% desde el final del segmento. Al conectar estos puntos de la curva, obtenemos el efecto de flexión .


Al dibujar a mano, las personas a veces mueven el lápiz hacia adelante y hacia atrás a lo largo de la línea. Esto es necesario para hacer que la línea sea más brillante o simplemente para corregir la rectitud de la línea. Se ve algo como esto:


Para agregar un efecto incompleto, RoughJS dibuja una línea dos veces. En el futuro planeo hacer que este aspecto sea más personalizable.

Mira esta superficie del lienzo. El parámetro de rugosidad cambia la apariencia de las líneas:


En el artículo original sobre lienzo, puedes dibujarte a ti mismo.

Al dibujar a mano, las líneas largas generalmente se vuelven menos rectas y más curvas. Es decir, la aleatoriedad de las compensaciones para crear un efecto es una función de la longitud y el valor de la línea randomness. Sin embargo, escalar esta función no es adecuado para líneas muy largas. Por ejemplo, en la imagen a continuación, se dibujan cuadrados concéntricos con la misma semilla aleatoria, es decir de hecho, son una figura aleatoria, pero con una escala diferente.


Puede notar que los bordes de los cuadrados exteriores se ven un poco más desiguales que los interiores. Por lo tanto, también agregué un factor de amortiguación dependiendo de la longitud de la línea. El coeficiente de atenuación se utiliza como una función escalonada en varias longitudes.


Elipses (y círculos)


Tome una hoja de papel y dibuje algunos círculos lo más rápido posible en un movimiento continuo. Esto es lo que obtuve:


Tenga en cuenta que los puntos de inicio y finalización del bucle no siempre coinciden. RoughJS intenta imitar esto, mientras hace que la apariencia sea más completa (la técnica está adaptada del artículo giCenter ).

El algoritmo encuentra los npuntos de elipse donde nestá determinado por el tamaño de la elipse. Luego, cada punto se aleatoriza por su valor roughness. Luego se dibuja una curva a través de estos puntos. Para obtener el efecto de los extremos desconectados, los puntos del segundo al último no coinciden con el primer punto. En cambio, la curva conecta los puntos segundo y tercero.


También se dibuja una segunda elipse para que el bucle esté más cerrado y tenga un efecto de boceto adicional.

En el artículo original, puede dibujar elipses en una superficie de lienzo interactiva. Varíe la aspereza y observe cómo cambia la forma:


En el caso del dibujo lineal, algunos de estos artefactos se acentúan más si alguna forma se escala a diferentes tamaños. En una elipse, este efecto es más notable porque la relación es cuadrática. En la imagen a continuación, todos los círculos tienen la misma forma, pero los exteriores se ven más desiguales.


El algoritmo se ajusta automáticamente en función del tamaño de la forma, aumentando el número de puntos en el círculo ( n). A continuación se muestra el mismo conjunto de círculos generados mediante la sintonización automática.


Llenando


Las líneas punteadas se usan generalmente para rellenar formas dibujadas a mano . En el caso de los bocetos a mano alzada, las líneas no siempre permanecen dentro del contorno de las formas. También son aleatorizados. Densidad, ángulo, ancho de línea se puede ajustar.


Los cuadrados que se muestran arriba son fáciles de rellenar, pero en el caso de otras formas, pueden ocurrir todo tipo de problemas. Por ejemplo, los polígonos cóncavos (en los que los ángulos pueden exceder los 180 °) a menudo causan tales problemas:


De hecho, la imagen de arriba está tomada de un informe de error en una de las versiones anteriores de RoughJS. Desde entonces, he actualizado el algoritmo de relleno de trazo adaptando la versión del método de escaneo de cadenas .

El algoritmo de escaneo de cadenas se puede usar para llenar cualquier polígono. Su principio es escanear un polígono usando líneas horizontales (líneas de trama). Las líneas de trama van desde la parte superior del polígono hacia abajo. Para cada línea de trama, determinamos en qué puntos la línea se cruza con el polígono. Construimos estos puntos de intersección de izquierda a derecha.


Pasando de un punto a otro, cambiamos del modo de relleno al modo sin relleno; el cambio entre estados ocurre cuando cada punto de intersección en la línea de trama se encuentra. Aquí se debe tener mucho más en cuenta, en particular, los casos límite y los métodos de optimización de escaneo; Puede leer más sobre esto aquí: Rasterizar polígonos o implementar un spoiler con pseudocódigo.

Detalles de la implementación del algoritmo de escaneo de cadenas
() .

— (Edge Table, ET), , Ymin. Ymin, Xmin.

— (Active Edge Table, AET), , .

:

interface EdgeTableEntry {
  ymin: number;
  ymax: number;
  x: number; // Initialized to Xmin
  iSlope: number; // Inverse of the slope of the line: 1/m
}

interface ActiveEdgeTableEntry {
  scanlineY: number; // The y value of the scanline
  edge: EdgeTableEntry;
}

, :

1. y y ET. .

2. AET .

3. , AET, ET :

(a) ET y AET , ymin ≤ y.

(b) AET , y = ymax, AET x.

() y, x AET.

(d) y , , .. .

(e) , AET, x y (edge.x = edge.x + edge.iSlope)

En la imagen a continuación (en el artículo original interactivo), cada cuadrado denota un píxel. Puede mover los vértices para cambiar el polígono y observar qué píxeles se rellenarán tradicionalmente.


Al rellenar trazos, el incremento de las líneas de trama se realiza en incrementos dependiendo de la densidad dada de líneas de trazos, y cada línea se dibuja utilizando el algoritmo descrito anteriormente.

Sin embargo, este algoritmo es para líneas de trama horizontales. Para implementar diferentes ángulos de trazos, el algoritmo primero rota la forma en sí por el ángulo de trazos deseado. Luego se calculan las líneas de trama para la figura girada. Además, las líneas calculadas giran de vuelta al ángulo de los trazos en la dirección opuesta.


No solo rellenando trazos


RoughJS también admite otros estilos de relleno, pero todos se derivan del mismo algoritmo de eclosión. El sombreado cruzado consiste en dibujar líneas discontinuas en un ángulo angle, y luego otras líneas en un ángulo angle + 90°. Zigzag busca conectar una línea discontinua con la anterior. Para obtener un patrón de puntos , dibuje círculos pequeños a lo largo de las líneas punteadas.


Las curvas


Todo en RoughJS está normalizado a curvas: líneas, polígonos, elipses, etc. Por lo tanto, el desarrollo natural de esta idea es crear una curva de croquis. En RoughJS, pasamos un conjunto de puntos a una curva, después de lo cual usamos la aproximación de la curva para convertirlos en curvas de Bezier cúbicas .

Cada curva de Bezier tiene dos puntos finales y dos puntos de control. Al aleatorizarlos sobre la base roughness, también puede crear curvas "escritas a mano".


Llenado de curvas


Sin embargo, se requiere el proceso inverso para llenar las curvas. En lugar de normalizar todo a una curva, la curva se normaliza a un polígono. Después de obtener el polígono, puede usar el algoritmo de escaneo de línea para llenar la forma curva.

Puede muestrear puntos en la curva con la frecuencia deseada utilizando la ecuación de la curva cúbica de Bezier .


Si usamos la frecuencia de muestreo, que depende de la densidad de los trazos, obtenemos suficientes puntos para llenar la figura. Pero esto no es particularmente efectivo. Si parte de la curva es aguda, entonces necesitamos más puntos. Si parte de la curva es casi recta, entonces se necesitan menos puntos. Una solución puede ser determinar la curvatura / suavidad de la curva. Si es muy curva, dividimos la curva en dos curvas más pequeñas. Si es suave, lo consideraremos simplemente como una línea recta.

La suavidad de la curva se calcula utilizando el método descrito en esta publicación . El valor de suavidad se compara con el valor de tolerancia, después de lo cual se toma la decisión de dividir la curva o no.

Aquí está la misma curva con un nivel de tolerancia de 0.7:


Basado solo en la tolerancia, el algoritmo proporciona suficientes puntos para representar la curva. Sin embargo, no le permite deshacerse efectivamente de los puntos opcionales. Esto ayudará al segundo parámetro llamado distancia . Para reducir el número de puntos en este método, se utiliza el algoritmo Ramer-Douglas-Pecker .

Los siguientes muestra los puntos generados con valores de la distancia, que equivale a 0.15, 0.75, 1.5y 3.0.


Según la rugosidad de la forma, puede establecer el valor de distancia apropiado . Habiendo recibido todos los vértices del polígono, podemos rellenar bellamente las formas curvas:


Circuitos SVG


Los contornos SVG son una herramienta muy poderosa que se puede utilizar para crear todo tipo de imágenes impresionantes, pero debido a esto, es bastante difícil trabajar con ellas.

RoughJS analiza el camino y lo normaliza en solo tres operaciones: Mover , Línea y Curva cúbica . ( analizador de datos de ruta ). Después de la normalización, la figura se puede dibujar utilizando los métodos anteriores de dibujar líneas y curvas.

El paquete de puntos en ruta combina la normalización de rutas y el muestreo de puntos de curva para calcular los puntos de ruta correspondientes.

El siguiente es un ejemplo de cálculo de puntos para una ruta M240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100:


Otro ejemplo de SVG que me encanta mostrar es el mapa general de los Estados Unidos:


Prueba RoughJS


Consulte el sitio web o el repositorio en Github o la documentación de la API . Siga Twitter @RoughLib para obtener información del proyecto .

All Articles