Generative art adalah gambar yang dibuat oleh aturan yang kamu tulis, dengan angka acak mengisi detailnya. Bayangin Raka bilang ke asistennya: "tata 140 biji kopi di kain, sepuluh per baris; tiap biji boleh geser sedikit, miring ke mana saja, warnanya pilih dari tiga tinta". Aturannya dari Raka, detailnya dari asisten. Hasilnya beda tiap kali, kecuali asistennya mengikuti satu buku angka acak yang sama, dibuka di halaman yang sama. Nomor halaman itu namanya seed.
Lesson ini membuat Pola Biji, motif untuk kaos Raka berikutnya: biji kopi dari sablon Kopi, ditata kode di area sablon A4, lalu diekspor jadi file cetak 300 DPI.
🧪 Tebakan cepat: Raka menyetujui pola di layar, lalu kode yang sama menggambarnya ulang di canvas besar untuk file cetak. Kalau angka acaknya dari
Math.random(), apakah file cetaknya sama dengan pola yang dia setujui? Tebak dulu, jawabannya ada di bagian "Seed: acak yang bisa diulang".
Aturan dan keacakan
Kita menggambar dalam satuan milimeter: area sablon A4 adalah 210 × 297. Aturannya:
| Aturan (selalu sama) | Keacakan (beda tiap biji) |
|---|---|
| 10 kolom × 14 baris = 140 biji, jarak 21 mm | geser x dan y, sampai 4 mm |
| bentuk biji: lonjong dengan belahan di tengah | jari-jari 6 sampai 8 mm |
| latar hitam kaos | sudut miring |
| warna dari palet tinta Raka | warna mana dari palet |
rand() di sini adalah fungsi yang mengembalikan angka pecahan dari 0 sampai di bawah 1, seperti Math.random(). Dari situ semua rentang dibuat:
const KAOS = '#1c1917', KREM = '#f5e6c8', COKLAT = '#c47a3d', HIJAU = '#7fb069';
// coklat 3 dari 6, krem 2 dari 6, hijau 1 dari 6
const PALET = [COKLAT, COKLAT, COKLAT, KREM, KREM, HIJAU];
const dx = (rand() * 2 - 1) * 4; // -4 sampai di bawah 4 mm
const r = 6 + rand() * 2; // 6 sampai di bawah 8 mm
const warna = PALET[Math.floor(rand() * PALET.length)]; // indeks 0 sampai 5
min + rand() * (max - min)memberi angka di antara min dan max.rand() * 2 - 1memberi -1 sampai 1.Math.floor(rand() * 6)memberi bilangan bulat 0 sampai 5, pas untuk indeks array. Warna yang muncul lebih dari sekali di palet lebih sering terpilih: begitu cara membuat coklat dominan dan hijau jarang.- Contoh lain:
for (let i = 0; i < 5; i++) ctx.fillRect(Math.random() * 400, 200, 10, 10)menggambar 5 kotak 10 × 10 di x acak antara 0 dan 400, semuanya di y = 200: satu baris mendatar dengan jarak yang gak rata.
Seed: acak yang bisa diulang
Math.random() gak bisa diberi seed. Tiap panggilan mengambil angka baru dari generator milik browser, dan gak ada cara memutar ulang urutannya. Jadi ketika kode yang sama menggambar ulang pola untuk file cetak, semua angkanya baru: file cetaknya pola lain, bukan yang disetujui Raka. Itu jawaban tebakannya.
Solusinya PRNG (pseudo-random number generator) yang ditulis sendiri. Angkanya terlihat acak, tapi dihitung dari seed, jadi seed yang sama selalu memberi urutan yang sama. mulberry32 cukup 8 baris:
function mulberry32(seed) {
return function rand() {
seed |= 0;
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; // 0 sampai di bawah 1
};
}
rand() ke-1 |
ke-2 | ke-3 | |
|---|---|---|---|
mulberry32(42) |
0,6011 | 0,4483 | 0,8525 |
mulberry32(42) lagi, besok, di HP lain |
0,6011 | 0,4483 | 0,8525 |
mulberry32(7) |
0,0117 | 0,0620 | 0,9769 |
- Fungsi luar menyimpan
seed, danranddi dalamnya mengubahnya tiap dipanggil (closure).Math.imul(perkalian bilangan bulat 32-bit),>>>(geser bit) dan^(XOR) gak perlu dihafal; yang penting hasilnya bisa diulang. - Seed 42 = cetakan yang Raka kirim ke tukang sablon. Bulan depan dia mau cetak ulang 50 kaos? Simpan seed-nya, bukan cuma gambarnya.
Noise: acak yang mulus
Angka dari rand() gak saling kenal: biji di sebelahnya bisa miring ke arah mana saja. Noise memberi angka acak yang mulus: dua titik yang berdekatan dapat nilai yang mirip, jadi biji-biji bertetangga miring searah seperti ikut arus.
Yang kita tulis di sini adalah value noise: angka acak dipasang di titik-titik bilangan bulat sebuah kisi, lalu nilai di antaranya dihaluskan. Perlin noise (Ken Perlin, 1983) memasang arah acak (gradient), bukan angka, di titik kisi, dan hasilnya lebih alami; simplex noise adalah penerusnya. Idenya sama: tetangga dekat, nilai mirip.
function buatNoise(rand) {
const N = 16;
const kisi = [];
for (let i = 0; i < N * N; i++) kisi.push(rand()); // 256 angka acak di titik kisi
const v = (x, y) => kisi[(y % N) * N + (x % N)];
const halus = (t) => t * t * (3 - 2 * t); // smoothstep: pelan di ujung
return function noise(x, y) {
const x0 = Math.floor(x), y0 = Math.floor(y);
const tx = halus(x - x0), ty = halus(y - y0);
const atas = v(x0, y0) + (v(x0 + 1, y0) - v(x0, y0)) * tx;
const bawah = v(x0, y0 + 1) + (v(x0 + 1, y0 + 1) - v(x0, y0 + 1)) * tx;
return atas + (bawah - atas) * ty; // 0 sampai 1
};
}
Satu dimensi dulu: titik kisi x = 2 bernilai 0,2 dan x = 3 bernilai 0,8. Di x = 2,25, t = 0,25 dan halus(0,25) = 0,156, jadi noise = 0,2 + 0,6 × 0,156 = 0,29. Garis lurus biasa akan memberi 0,35; smoothstep membuat nilai berbelok pelan di dekat titik kisi, jadi gak ada sudut tajam.
Kalau noise dipakai sebagai sudut di setiap titik, namanya flow field: peta arah arus. Tiap biji tinggal mengikuti arah di posisinya.
Pola Biji, lengkap
biji() adalah fungsi dari lesson Canvas API Dasar, sekarang dengan jari-jari dan warna sebagai parameter. ctx harus sudah diatur supaya 1 satuan = 1 mm (lihat bagian berikutnya).
function biji(ctx, x, y, sudut, r, warna) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(sudut);
ctx.scale(1, 0.62); // lingkaran jadi lonjong
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.fillStyle = warna;
ctx.fill();
ctx.beginPath(); // belahan biji
ctx.moveTo(-r * 0.7, 0);
ctx.lineTo(r * 0.7, 0);
ctx.strokeStyle = KAOS;
ctx.lineWidth = r * 0.25;
ctx.stroke();
ctx.restore();
}
function gambarPola(ctx, seed, acak) { // acak: 0 = grid kaku, 1 = geser penuh
const rand = mulberry32(seed);
const noise = buatNoise(rand); // memakai 256 angka pertama
ctx.fillStyle = KAOS;
ctx.fillRect(0, 0, 210, 297);
for (let baris = 0; baris < 14; baris++) {
for (let kolom = 0; kolom < 10; kolom++) {
const dx = (rand() * 2 - 1) * 4 * acak;
const dy = (rand() * 2 - 1) * 4 * acak;
const r = 6 + rand() * 2;
const sudutAcak = rand() * Math.PI * 2; // selalu dipanggil, lihat Jebakan
const warna = PALET[Math.floor(rand() * PALET.length)];
const x = 10.5 + kolom * 21 + dx;
const y = 12 + baris * 21 + dy; // 14 × 21 = 294 mm, sisa 1,5 mm atas dan bawah
const sudut = noise(x / 30, y / 30) * Math.PI * 2;
biji(ctx, x, y, sudut, r, warna);
}
}
}
- Urutan
rand()selalu sama: 256 angka untuk kisi noise, lalu tepat 5 per biji, 140 × 5 = 700. x / 30: biji berjarak 21 mm, jadi di kisi noise jaraknya 0,7. Tetangga jatuh di kotak kisi yang sama atau sebelahnya, makanya sudutnya mirip. PakaisudutAcaksebagai sudut untuk versi yang tiap bijinya miring sendiri-sendiri.
Ekspor 300 DPI untuk sablon
DPI (dots per inch) adalah jumlah pixel per inci kain. Tukang sablon biasanya minta 300. 1 inci = 25,4 mm:
| mm | inci | pixel di 300 DPI | |
|---|---|---|---|
| lebar A4 | 210 | 8,27 | 2.480 |
| tinggi A4 | 297 | 11,69 | 3.508 |
Ini trik setTransform dari lesson Canvas API Dasar: di layar 1 mm = 1 px CSS (setTransform(dpr, 0, 0, dpr, 0, 0)), untuk cetak 1 mm = 11,81 pixel. Fungsi gambarnya sama, seed-nya sama, urutan rand()-nya sama, jadi polanya sama persis, cuma lebih tajam.
function eksporCetak(seed, acak) {
const pxPerMm = 300 / 25.4; // 11,81
const cetak = document.createElement('canvas');
cetak.width = Math.round(210 * pxPerMm); // 2480
cetak.height = Math.round(297 * pxPerMm); // 3508
const ctx = cetak.getContext('2d');
ctx.setTransform(pxPerMm, 0, 0, pxPerMm, 0, 0);
gambarPola(ctx, seed, acak);
cetak.toBlob((file) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = `pola-biji-seed-${seed}.png`;
a.click();
}, 'image/png');
}
- 2.480 × 3.508 = 8.699.840 pixel. Hasilnya PNG sekitar 0,9 MB (diukur di Chrome, Firefox dan Safari).
toBlobmembuat file dari isi canvas.a.downloadmembuat klik link menyimpan file, bukan membukanya.- PNG dari canvas gak membawa keterangan "300 DPI". Yang dipakai tukang sablon adalah jumlah pixelnya, jadi sebutkan ukuran cetaknya: 21 × 29,7 cm.
⚠️ Jebakan yang sering ketemu
Math.random()untuk file cetak. Pola di layar dan pola di file jadi beda. Pakai PRNG dengan seed, dan buat generator baru (mulberry32(seed)) tiap kali menggambar dari awal.- Menambah atau melewatkan satu
rand(). Semua angka sesudahnya bergeser, jadi seed 42 berubah jadi pola lain. Itu sebabnyasudutAcaktetap dipanggil walau sudutnya dari noise. - Noise dengan langkah terlalu besar.
noise(x, y)dengan x dalam mm melompati 21 titik kisi antar biji, jadi hasilnya kembali terlihat acak. Bagi dulu (x / 30). - Canvas ekspor terlalu besar. Safari di iPhone menolak canvas di atas 16.777.216 pixel. A3 di 300 DPI (3.508 × 4.961 = 17,4 juta) melewatinya, dan hasilnya gambar kosong tanpa error.
TL;DR: Generative art = aturan yang kamu tulis + angka acak untuk detailnya. min + rand() * (max - min) memberi rentang, Math.floor(rand() * n) memberi indeks. Math.random() gak bisa diulang; PRNG seperti mulberry32 dengan seed yang sama selalu memberi urutan yang sama (seed 42: 0,6011, 0,4483, 0,8525), selama urutan panggilan rand() gak berubah. Random biasa melompat-lompat; noise (value noise di sini, Perlin dan simplex lebih halus) memberi nilai mirip untuk titik yang berdekatan, dan noise sebagai sudut adalah flow field. Untuk cetak 300 DPI, A4 210 × 297 mm = 2.480 × 3.508 pixel: gambar dalam mm, lalu setTransform(300 / 25.4, 0, 0, 300 / 25.4, 0, 0), toBlob, dan simpan seed-nya.