Tiruan gambar tangan bebas pada contoh RoughJS

RoughJS adalah perpustakaan grafik JavaScript kecil (< 9KB ) yang memungkinkan Anda menggambar dengan gaya tulisan tangan yang samar . Ini memungkinkan Anda untuk menggambar <canvas>dan dengan SVG. Dalam posting ini saya ingin menjawab pertanyaan paling populer tentang RoughJS: bagaimana cara kerjanya?


Sedikit sejarah


Terpesona oleh gambar grafik tulisan tangan, diagram, dan sketsa, saya, seperti kutu buku sejati, bertanya pada diri sendiri: dapatkah saya membuat gambar seperti itu dengan bantuan kode, meniru gambar seakurat mungkin dengan tangan, sambil tetap menjaga kemungkinan implementasi perangkat lunak? Saya memutuskan untuk fokus pada primitif - garis, poligon, elips, dan kurva - untuk membuat seluruh perpustakaan grafik 2D. Berdasarkan itu, Anda dapat membuat perpustakaan dan grafik untuk menggambar grafik dan diagram.

Setelah memeriksa masalah ini sebentar, saya menemukan sebuah artikel oleh Joe Wood dan rekan-rekannya yang disebut Sketchy rendering untuk visualisasi informasi . Teknik yang dijelaskan di dalamnya menjadi dasar perpustakaan, terutama dalam menggambar garis dan elips.

Pada 2017, saya menulis versi pertama perpustakaan, yang hanya berfungsi pada Canvas. Setelah memecahkan masalah, saya kehilangan minat. Setahun kemudian, saya banyak bekerja dengan SVG, dan memutuskan untuk mengadaptasi RoughJS untuk bekerja dengan SVG. Saya juga mengubah struktur API agar lebih sederhana, dan berfokus pada primitif grafis vektor sederhana. Saya berbicara tentang versi 2.0 di Hacker News dan tiba-tiba mendapatkan popularitas yang luar biasa. Pada tahun 2018, itu adalah posting ShowHN paling populer kedua .

Sejak itu, orang lain telah menciptakan hal-hal yang lebih menakjubkan berdasarkan RoughJS, misalnya, Excalidraw , Why Cats & Dogs ... , perpustakaan grafis roughViz .

Sekarang mari kita bicara tentang algoritma ...

Ketidakrataan


Dasar fundamental untuk meniru figur tulisan tangan adalah kebetulan. Saat kita menggambar dengan tangan, dua bentuk akan sedikit berbeda. Tidak ada yang menggambar dengan tepat, sehingga setiap titik spasial di RoughJS disesuaikan untuk perpindahan acak. Besarnya keacakan diberikan oleh parameter numerik roughness.


Bayangkan sebuah titik Adan sebuah lingkaran di sekitarnya. Sekarang ganti dengan Atitik acak di dalam lingkaran ini. Area lingkaran keacakan ini dikendalikan oleh nilai roughness.

Garis


Garis tulisan tangan tidak pernah lurus dan sering menunjukkan kelengkungan di tikungan (dijelaskan di sini ). Kami mengacak dua titik akhir garis berdasarkan kekasaran. Kemudian kami memilih dua titik acak lagi pada jarak 50% dan 75% dari akhir segmen. Dengan menghubungkan titik-titik kurva ini, kita mendapatkan efek tekukan .


Saat menggambar dengan tangan, orang kadang-kadang menggerakkan pensil maju dan mundur sepanjang garis. Ini diperlukan baik untuk membuat garis lebih cerah, atau hanya untuk memperbaiki kelurusan garis. Itu terlihat seperti ini:


Untuk menambahkan efek samar, RoughJS menarik garis dua kali. Di masa depan saya berencana untuk membuat aspek ini lebih dapat disesuaikan.

Lihatlah permukaan kanvas ini. Parameter roughness mengubah tampilan garis:


Di artikel asli di kanvas, Anda bisa menggambar diri sendiri.

Saat menggambar dengan tangan, garis panjang biasanya menjadi kurang lurus dan lebih melengkung. Artinya, keacakan dari offset untuk membuat efek adalah fungsi dari panjang dan nilai garis randomness. Namun, penskalaan fungsi ini tidak cocok untuk garis yang sangat panjang. Misalnya, pada gambar di bawah ini, kotak konsentris digambar dengan seed acak yang sama, mis. sebenarnya, mereka adalah satu tokoh acak, tetapi dengan skala yang berbeda.


Anda mungkin memperhatikan bahwa tepi kotak luar terlihat sedikit lebih tidak rata daripada bagian dalam. Karena itu, saya juga menambahkan faktor redaman tergantung pada panjang garis. Koefisien atenuasi digunakan sebagai fungsi langkah pada berbagai panjang.


Elips (dan lingkaran)


Ambil selembar kertas dan gambar beberapa lingkaran secepat mungkin dalam satu gerakan terus menerus. Inilah yang saya dapatkan:


Perhatikan bahwa titik awal dan akhir dari loop tidak selalu cocok. RoughJS mencoba untuk meniru ini, sambil membuat penampilan lebih lengkap (teknik ini diadaptasi dari artikel giCenter ).

Algoritma menemukan ntitik elips di mana ia nditentukan oleh ukuran elips. Kemudian setiap titik diacak berdasarkan nilainya roughness. Kemudian kurva ditarik melalui titik-titik ini. Untuk mendapatkan efek dari ujung yang terputus, poin dari yang kedua ke yang terakhir tidak sesuai dengan poin pertama. Sebagai gantinya, kurva menghubungkan titik kedua dan ketiga.


Elips kedua juga digambar sehingga loop lebih tertutup dan memiliki efek sketsa tambahan.

Di artikel asli, Anda bisa menggambar elips di permukaan kanvas interaktif. Variasikan kekasaran dan perhatikan bagaimana bentuknya berubah:


Dalam kasus gambar garis, beberapa artefak ini menjadi lebih ditekankan jika beberapa bentuk diskalakan ke ukuran yang berbeda. Dalam elips, efek ini lebih terlihat karena rasionya kuadratik. Pada gambar di bawah ini, semua lingkaran memiliki bentuk yang sama, tetapi yang luar terlihat lebih tidak rata.


Algoritma secara otomatis menyesuaikan berdasarkan ukuran bentuk, meningkatkan jumlah titik dalam lingkaran ( n). Di bawah ini adalah kumpulan lingkaran yang sama yang dihasilkan menggunakan penyetelan otomatis.


Mengisi


Garis putus - putus biasanya digunakan untuk mengisi bentuk yang digambar tangan . Dalam kasus sketsa tangan bebas, garis tidak selalu tetap berada dalam garis besar bentuk. Mereka juga diacak. Kepadatan, sudut, lebar garis dapat disesuaikan.


Kotak yang ditunjukkan di atas mudah untuk diisi, tetapi dalam kasus bentuk lain, semua jenis masalah dapat terjadi. Misalnya, poligon cekung (di mana sudut bisa melebihi 180 °) sering menyebabkan masalah seperti:


Bahkan, gambar di atas diambil dari laporan kesalahan di salah satu versi RoughJS sebelumnya. Sejak itu, saya telah memperbarui algoritma pengisian goresan dengan mengadaptasi versi metode pemindaian string .

Algoritma pemindaian string dapat digunakan untuk mengisi poligon apa pun. Prinsipnya adalah memindai poligon menggunakan garis horizontal (garis raster). Garis raster bergerak dari atas poligon ke bawah. Untuk setiap garis raster, kami menentukan pada titik apa garis tersebut bersilangan dengan poligon. Kami membangun titik persimpangan ini dari kiri ke kanan.


Bergerak dari titik ke titik, kami beralih dari mode pengisian ke mode tidak mengisi; pergantian antar negara terjadi ketika setiap titik persimpangan pada garis raster bertemu. Di sini, banyak lagi yang perlu dipertimbangkan, khususnya kasus batas dan metode optimisasi pemindaian; Anda dapat membaca lebih lanjut tentang ini di sini: Meraster poligon , atau menggunakan spoiler dengan pseudo-code.

Detail implementasi algoritma pemindaian string
() .

— (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)

Pada gambar di bawah ini (dalam artikel asli interaktif), setiap kotak menunjukkan piksel. Anda dapat memindahkan titik untuk mengubah poligon dan mengamati piksel mana yang akan diisi secara tradisional.


Saat mengisi goresan, penambahan garis raster dilakukan secara bertahap tergantung pada kepadatan garis goresan yang diberikan, dan setiap garis digambar menggunakan algoritme yang dijelaskan di atas.

Namun, algoritma ini untuk garis raster horizontal. Untuk menerapkan berbagai sudut goresan, algoritma pertama-tama memutar bentuk itu sendiri dengan sudut goresan yang diinginkan. Kemudian garis raster untuk gambar yang diputar dihitung. Selanjutnya, garis yang dihitung berputar kembali ke sudut sapuan dalam arah yang berlawanan.


Bukan hanya mengisi stroke


RoughJS juga mendukung gaya isian lainnya, tetapi semuanya berasal dari algoritma penetasan yang sama. Penetasan silang terdiri dari menggambar garis putus-putus pada suatu sudut angle, dan kemudian garis lain pada suatu sudut angle + 90°. Zigzag berupaya menghubungkan satu garis putus-putus dengan yang sebelumnya. Untuk mendapatkan pola titik , gambar lingkaran kecil di sepanjang garis putus-putus.


Kurva


Semua yang ada di RoughJS dinormalisasi menjadi kurva - garis, poligon, elips, dll. Oleh karena itu, pengembangan alami dari ide ini adalah membuat kurva sketsa. Dalam RoughJS, kami melewati satu set poin ke kurva, setelah itu kami menggunakan pendekatan kurva untuk mengubahnya menjadi kurva Bezier kubik .

Setiap kurva Bezier memiliki dua titik akhir dan dua titik kontrol. Dengan mengacak mereka berdasarkan roughness, Anda juga bisa membuat kurva "tulisan tangan".


Pengisian Kurva


Namun, proses terbalik diperlukan untuk mengisi kurva. Alih-alih menormalkan semuanya menjadi kurva, kurva menormalkan menjadi poligon. Setelah mendapatkan poligon, Anda dapat menggunakan algoritma pemindaian garis untuk mengisi bentuk melengkung.

Anda dapat mengambil sampel titik pada kurva dengan frekuensi yang diinginkan menggunakan persamaan kurva Bezier kubik .


Jika kita menggunakan frekuensi pengambilan sampel, yang tergantung pada kepadatan stroke, maka kita mendapatkan poin yang cukup untuk mengisi gambar. Tetapi ini tidak terlalu efektif. Jika bagian dari kurva tajam, maka kita perlu lebih banyak poin. Jika bagian dari kurva hampir lurus, maka lebih sedikit poin yang dibutuhkan. Salah satu solusinya adalah menentukan kelengkungan / kehalusan kurva. Jika sangat melengkung, maka kita membagi kurva menjadi dua kurva yang lebih kecil. Jika halus, maka kami akan menganggapnya hanya sebagai garis lurus.

Kelancaran kurva dihitung menggunakan metode yang dijelaskan dalam posting ini . Nilai kelancaran dibandingkan dengan nilai toleransi, setelah itu keputusan dibuat apakah akan membagi kurva atau tidak.

Berikut adalah kurva yang sama dengan tingkat toleransi 0,7:


Berdasarkan toleransi saja, algoritma menyediakan cukup poin untuk mewakili kurva. Namun, itu tidak memungkinkan Anda untuk secara efektif menyingkirkan poin opsional. Ini akan membantu parameter kedua yang disebut jarak . Untuk mengurangi jumlah poin dalam metode ini, algoritma Ramer-Douglas-Pecker digunakan .

Berikut ini menunjukkan poin yang dihasilkan dengan nilai-nilai jarak, sama dengan 0.15, 0.75, 1.5dan 3.0.


Berdasarkan kekasaran bentuk, Anda dapat mengatur nilai jarak yang sesuai . Setelah menerima semua simpul poligon, kita dapat dengan indah mengisi bentuk melengkung:


Sirkuit SVG


Kontur SVG adalah alat yang sangat kuat yang dapat digunakan untuk membuat semua jenis gambar yang menakjubkan, tetapi karena ini, cukup sulit untuk bekerja dengannya.

RoughJS mem-parsing path dan menormalkannya hanya dalam tiga operasi: Move , Line dan Cubic Curve . ( path-data-parser ). Setelah normalisasi, gambar dapat ditarik menggunakan metode menggambar garis dan kurva di atas.

Paket points-on-path menggabungkan normalisasi jalur dan pengambilan sampel titik kurva untuk menghitung titik jalur yang sesuai.

Berikut ini adalah perhitungan titik contoh untuk jalur 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:


Contoh SVG lain yang saya suka tunjukkan adalah peta garis besar Amerika Serikat:


Coba RoughJS


Lihatlah situs web atau repositori di Github atau dokumentasi API . Ikuti Twitter @RoughLib untuk informasi proyek .

All Articles