Catatan Teknis – Baby Coloring Book

Setiap kali saya membuat aplikasi dengan teknologi yang baru, biasanya saya mendapati banyak tantangan teknis. Dalam konteks ini “baru” bisa berarti teknologinya benar-benar baru, atau saya yang baru saja mengenal teknologi tersebut. Kali ini saya ingin membahas mengenai aplikasi saya di appworld, Baby Coloring Book.

Ide dari aplikasi ini sangat sederhana: buku mewarnai untuk bayi (terutama di bawah 3 tahun), hanya perlu menyentuh saja untuk mewarnai, tidak perlu menggosok-gosok seperti memakai krayon. Jika saya menggunakan teknologi lain untuk membuat ini (misalnya flash atau C++), saya akan menggunakan pendekatan sederhana: buat gambar tidak berwarna, gunakan flood fill untuk mengisi area. Tapi saya menggunakan HTML5 untuk membuat aplikasi ini. Alasan utamanya adalah untuk belajar mengenal lebih jauh HTML5. Saya tidak menggunakan library selain selain JQuery.

Contoh Gambar

Di Stack Overflow sudah ada yang menjawab bagaimana mengimplementasikan flood fill dengan JavaScript menggunakan Canvas:

How can I perform flood fill with HTML Canvas?

Jadi saya coba itu di PC menggunakan browser Google Chrome. Manipulasi piksel di Google Chrome sangat cepat, tapi saya melihat ada sedikit delay. Saya langsung curiga: jangan-jangan jika saya coba di PlayBook akan sangat lambat. Ternyata benar: sangat lambat.

Ada beberapa pendekatan yang terpikir oleh saya supaya aplikasi ini bisa dibuat dengan HTML5 tapi tetap cepat, tapi saya memiliki beberapa requirement:

  1. Saya tidak ingin “membatik” mendefinisikan setiap area gambar yang bisa diwarnai. Jika saya punya gambar, saya ingin langsung bisa memakai gambar itu tanpa edit manual. Dengan pendekatan flood fill, algoritma tersebut bisa otomatis mewarnai sebuah area yang dibatasi pixel tertentu (seperti mewarnai dengan “ember” di Ms Paint).
  2. Saya ingin bisa membuat gambar yang warnanya seperti diwarnai dengan krayon, jadi tidak polos.


Pendekatan yang terpikir oleh saya:

  1. Menggambar langsung menggunakan method-method canvas (line, circle, dsb). Tapi menerjemahkan ini langsung dari bitmap (PNG) tidak mudah
  2. Menggunakan SVG. Ini akan sangat bagus, seharusnya akan sangat cepat. Masalahnya adalah: gambar saya harus dalam format SVG, dan setiap area yang ingin diwarnai harus didefinisikan dulu. Perlu trik supaya warna bisa seperti krayon.
  3. Menggunakan WebGL. Ini juga seharusnya akan sangat cepat, tapi lebih rumit, perlu definisi region dalam 3D, walaupun hanya untuk objek 2D
  4. Memotong dan menempelkan potongan gambar, tapi pemotongan dilakukan “offline”.

Saya sedikit bereksperiman menggunakan SVG, tapi terdapat cukup banyak hambatan. Saya coba melakukan trace otomatis sebuah gambar sederhanamenggunakan InkScape, tapi hasilnya adalah satu objek besar (bukan banyak objek terpisah per wilayah). Gambar perlu dipotong-potong dulu menjadi beberapa region supaya bisa menjadi SVG yang terpisah.

Saya putuskan cara yang termudah adalah pendekatan terakhir. Saya tidak perlu memikirkan segala macam hubungan antara SVG dan HTML Canvas. Mungkin dalam aplikasi berikutnya, saya akan mempertimbangkan kembali pemakaian SVG.

Memotong Gambar

Saya menggunakan Python untuk pemotongan gambar. Tapi dalam artikel ini kode python hanya dalam bentuk pseudo code saja. Pendekatan pemotongan gambar yang saya lakukan cukup sederhana:

  1. Periksa semua piksel dari kiri atas menuju kanan bawah
  2. Jika ketemu warna piksel putih di x, y, maka warnai dengan flood fill menggunakan warna (c,c,c)
  3. tambahkan 1 ke c

Saya akan memakai pseudo-code untuk menjelaskan algoritmanya. Anggap saja Anda bisa mengakses piksel dengan pixel[x][y]

var c = 1;
for (var x = 0; x < width; x++) {
    for (var y = 0; y < height; y++) {
   	   if (pixel[x][y]==WHITE) {
	      floodFill(x, y, c);
	      c++;
	   }
     }
}

Region pertama yang ketemu saya beri warna rgb(1,1,1), region putih kedua rgb(2,2,2), dst. Lalu apa gunanya punya region yang sudah diwarnai semuanya? Sekarang saatnya algoritma pemotongan gambar. Perhatikan kita punya region sejumlah c – 1; Kita bisa mengekstraksi region ini ke file (satu region adalah semua yang warna pikselnya sama):

for (var region = 1;  region < c; region++) {
    var image = new Image(width, height)
    for (var x = 0; x < width; x++) {
    	for (var y = 0; y < height; y++) {
   	   if (pixel[x][y]==rgb(region))) {
	      image[x][y] = rgba(0,0,0,255);
	   }
     	}
    }
    saveImage(image, "region" + region + ".png")
}

Warna rgb(0,0,0) adalah hitam, dan warna itu saya pakai untuk garis dalam gambar, jadi tidak saya pakai, saya mulai dari rgb(1,1,1)

Nah sekarang kita punya banyak file region, masing-masing berisi sepotong saja bagian gambar. Perhatikan bahwa cara di atas kurang efisien, setiap region adalah file yang relatif besar banyak bagian yang blank (putih). Supaya lebih efisien, kita hanya perlu membuat gambar yang kecil saja, yang berwarna hitam, plus informasi kordinat bahwa nantinya itu akan digambar di posisi tertentu.

Catatan: dalam menyimpan gambar, saya menyimpan sebagai “transparent PNG (RGBA)”. Ini hanya untuk optimasi pewarnaan gambar saja.

Menyusun Gambar dalam Canvas

Semua potongan gambar saya load di canvas terpisah. Gambar lengkah saya load di canvas pertama (index 0). Cara meload satu image kira-kira begini (lengkapnya silakan lihat bagian pembahasan mengenai Loading dan Progress):

var img = new Image();
img.src = image_url;

Setelah semua gambar terload:

var ww = images[i].naturalWidth;
var hh =  images[i].naturalHeight;
canvaslist[i].width = ww;
canvaslist[i].height = hh;
var ctx = canvaslist[i].getContext('2d');
ctx.drawImage(images[i],0,0);
/*nanti ini akan digunakan untuk mewarnai*/
var myImageData = ctx.getImageData(0, 0, ww, hh);
canvaslist_imagedata[i] = myImageData;

Untuk menggambar ke Canvas utama:

function renderAll()
{
	var main = document.getElementById('canvas').getContext('2d');
	var ctx = canvaslist[0].getContext('2d');

	main.clearRect(0, 0, canvas.width, canvas.height);

	main.drawImage(ctx.canvas, 0, 0);
	for (var i =1; i < canvaslist_visible.length; i++) {
		if (canvaslist_visible[i]) {
			var cpos = canvas_position[i-1];
			var x = cpos[0];
			var y = cpos[1];

			main.drawImage(canvaslist[i], x, y);
		}
	}
}

Perhatikan saya punya canvas_position yang menyatakan potongan gambar ke i harus digambar di posisi mana. Saya juga punya variabel canvaslist_visible, karena potongan gambar hanya akan ditampilkan jika bagian itu sudah diwarnai oleh pengguna.

Mewarnai Gambar

Persoalan berikutnya adalah: wah kok gambarnya hitam semua? bagaimana dengan warnanya? Kita bisa menggunakan manipulasi piksel. Ini bisa dilakukan relatif cepat karena area yang diwarnai kecil. Karena saya menggunakan PNG dengan RGBA, saya hanya mengecek jika A (alpha) channel tidak nol (artinya: tidak transparan), maka ganti warnanya menjadi warna yang kita mau:

function setColor(layer, color)
{
    var imageData = canvaslist_imagedata[layer];
    var data = imageData.data;
    for (var i =0; i < data.length; i+=4) {
	if (data[i+3]) {
	    data[i] = color[0];
	    data[i + 1] = color[1];
	    data[i + 2] = color[2];
	}
    }

    var ctx = canvaslist[layer].getContext('2d');
    ctx.putImageData(imageData, 0, 0);
}

Manipulasi ini dilakukan menggunakan array biasa (untyped) karena meski PlayBook mendukung typed array, tapi Canvas di Playbook belum mengembalikan Uint8ClampedArray, masih mengembalikan CanvasPixelArray (Buat yang belum tahu bedanya: http://hacks.mozilla.org/2011/12/faster-canvas-pixel-manipulation-with-typed-arrays/).

Efek krayon

Menggunakan format RGBA ternyata memiliki keuntungan tambahan: bisa menambahkan efek krayon dengan mudah. Pertama saya punya goresan krayon hitam putih seperti ini.

Nah ketika membuat potongan gambar, saya tidak membuat pikselnya menjadi hitam saja, tapi saya set alpha nya dengan nilai kehitaman krayon di posisi (x,y). Artinya: di titik itu transparansinya berubah sesuai dengan pola krayon.

for (var region = 1;  region < c; region++) {
    var image = new Image(width, height)
    for (var x = 0; x < width; x++) {
    	for (var y = 0; y < height; y++) {
   	   if (pixel[x][y]==rgb(region))) {
	      image[x][y] = rgba(0,0, 0, crayon[x][y]);
	   }
     	}
    }
    saveImage(image, "region" + region + ".png")
}

Efek ini lebih terlihat di aplikasi saya yang lain (Four Colors):

Four Colors

Ini pola yang dipakai di four colors:

Dengan algoritma pewarnaan gambar yang sama di JavaScript, maka potongan gambar itu bisa diwarnai apa saja.

Thumbnail

Berikutnya adalah tampilan thumbnail: gambar yang sudah diwarnai semestinya punya thumbnail di halaman depan. Teorinya sih mudah membuat thumbnail ini, tapi sepertinya ada bug di PlayBook.

Baby Coloring Book

Ada beberapa kode di Internet, misalnya di

Resizing an image in an HTML5 canvas

Tapi karena cara drawImage biasa tidak berhasil, dan pemrosesan piksel terlalu lambat, saya menggunakan pendekatan lain:

  1. pertama saya set agar canvasthumbnail memiliki scale 0.2 (ini skala yang dibutuhkan di app saya)
  2. saya gambarkan isi canvas utama ke kanvas tersebut. Teorinya di sini saya tinggal menggunakan toDataURL, tapi hasilnya imagenya blank, jadi saya teruskan lagi
  3. saya ambil datanya (dengan getImageData)
  4. saya taruh datanya ke canvasthumbnail2
  5. baru saya ambil datanya dengan toDataURL dari canvasthumbnail2
//bug in playbook: can not get thumbnail the easy way
function getThumbnail() {
    var canvas = document.getElementById('canvasthumbnail');
    var main = document.getElementById('canvas').getContext('2d');

    var x = canvas.getContext("2d");
    x.save();
    x.clearRect(0, 0, canvas.width, canvas.height);
    x.scale(0.2, 0.2);
    x.drawImage(main.canvas, 0, 0);
    var myImageData = x.getImageData(0, 0, 140, 100);
    var canvas2 = document.getElementById('canvasthumbnail2');
    var x2 = canvas2.getContext("2d");
    x2.clearRect(0, 0, canvas2.width, canvas2.height);
    x2.putImageData(myImageData, 0, 0);
    var result = canvas2.toDataURL("image/jpeg");
    x.restore();
    return result;
}

Loading dan progress

Masalah dengan pendekatan yang saya ambil adalah: jumlah image yang harus diload jadi banyak. Meload resource yang banyak (dalam hal ini gambar) akan memakan waktu yang lama, jadi sebaiknya ini dilakukan dari JavaScript secara asinkron

    still_loading = false;
    for (var i  = 0; i < file_list.length; i++) {
	var img = new Image();
	img.onload = function(e){
	    inc_counter(e);
	}
	img.src = file_list[i];
	preloaded_images[file_list[i]] = img;
    }

dalam contoh tersebut, saya memanggil inc_counter setiap kali gambar berhasil diload. Total image ada sebanyak file_list.length

function inc_counter(e)
{
    img_loaded++;

    var percent = Math.floor(img_loaded*100.0/total_image);

    $("#play").text("Loading " + percent  + "%");

    if (img_loaded>=total_image) {
	$("#play").text("Play");
	still_loading = false;
    }
}

Tips tambahan

Kita bisa menggunakan web inspector untuk mendebug JavaScript yang di browser playbook via browser di desktop. Kita juga bisa mengeset ketika mempackage supaya bisa memakai web inspector. Jika tidak bisa koneksi ke PlayBook melalui Chrome, gunakan browser webkit lain, misalnya Safari.

Untuk memudahkan testing di desktop, kita bisa mendeteksi apakah sedang dalam PlayBook atau tidak. Misalnya ini fungsi untuk open URL. Dengan ini saya bisa memastikan URL yang saya masukkan sudah benar di desktop.

function open_url(url)
{

    if (typeof window.blackberry === 'undefined') {
	window.open(url);
	return false;
    }

    var args = new blackberry.invoke.BrowserArguments(url);    
    blackberry.invoke.invoke(blackberry.invoke.APP_BROWSER, args);
    return false;
}

Dan tips terakhir adalah ini: Untuk membuat versi yang lite dan bukan, saya menggunakan satu file javascript, tapi menggunakan dua file HTML. Semua logika ada di JavaScript ini. Di file HTML yang full version ada “var LITE_VERSION=false” dan yang lite version “var LITE_VERSION=true”. File HTML keduanya memang sedikit berbeda (misalnya logo Lite dan Full version berbeda). Lalu hal yang berbeda berikutnya adalah file config.xml (nama aplikasinya berbeda, dan file HTML utamanya berbeda).

Saya membuat layout direktori seperti ini (file config.xml untuk lite ada di dalam direktori lite).


main.html
main-lite.html
config.xml
lite/config.xml

Saya membuat batch file, yang satu bernama make-full.bat dan yang lain make-lite.bat.

cd C:\wamp\www\baby-color-book
del babycolorbook.zip
zip -r babycolorbook.zip config.xml *.css *.js *.html *.png *.mp3 bg.jpg slice thumbs
path c:\webworks-tablet\jre\bin;%PATH%
\webworks-tablet\bbwp\bbwp babycolorbook.zip -o c:\wamp\www\baby-color-book -gcsk passwordsaya -gp12 passwordsaya -v -buildId  1

Sedangkan versi make-lite:

cd C:\wamp\www\baby-color-book
del babycolorbooklite.zip
zip -r babycolorbooklite.zip config.xml *.css *.js *.html *.png *.mp3 bg.jpg slice thumbs
zip -j babycolorbooklite.zip lite\config.xml
path c:\webworks-tablet\jre\bin;%PATH%
\webworks-tablet\bbwp\bbwp babycolorbooklite.zip -o c:\wamp\www\baby-color-book -gcsk passwordsaya -gp12 passwordsaya -v -buildId  1

Kuncinya ada di:

zip -j babycolorbooklite.zip lite\config.xml

Jadi setelah file zip terbentuk dengan config.xml yang berasal dari “full”, saya timpa dengan config.xml dari “lite”, dan menggunakan -j akan membuat pathnya (“lite”) tidak disertakan dalam file zip.

Happy Hacking.

3 thoughts on “Catatan Teknis – Baby Coloring Book”

  1. Kalau membuat aplikasi macam ini tapi pake bb eclipse, dimana ya menempatkan asset www-nya ?

    kalau di android kan folder www cukup tempatkan didalam folder asset ….

    thanks infonya.

    1. Kalo di playbook sih, di mana aja boleh (nggak harus di direktori bernama tertentu). Belum nyoba yang Smartphone BB.

Leave a Reply

Your email address will not be published. Required fields are marked *