Generative Art — Creative Web

Generative Art Generative art adalah seni yang dibuat dengan bantuan algoritma dan keacakan terkontrol. Kamu menulis "aturan" dan komputer menghasilkan…

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

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

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);
    }
  }
}

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');
}

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari