D
P
0
← Semua artikel Read in English

Shopify, Liquid & Theme CLI

Canvas Frame Sequence Kosong Sampai Pengguna Menggulir? Frame Pertama Sudah Termuat, Tapi Tidak Ada yang Menyuruhnya Digambar

· · 5 menit baca
Canvas Frame Sequence Kosong Sampai Pengguna Menggulir? Frame Pertama Sudah Termuat, Tapi Tidak Ada yang Menyuruhnya Digambar

Animator mengirim seratus dua puluh frame hero sebagai PNG. Saya konversi semuanya jadi WebP lossless dengan cwebp, lalu menaruhnya di folder assets tema toko yang sedang saya kerjakan, dari seq-001.webp sampai seq-120.webp. Penamaannya sengaja datar, prefiks ditambah indeks berimbuhan nol, supaya urutan alfabetis di dalam folder itu sama dengan urutan framenya.

Loader yang sudah saya punya berdiri di atas anggapan yang berbeda. Di sana alamat sebuah frame dirakit dari dua potong, satu direktori dan satu nomor berpadding empat digit, jadi indeks ketujuh berubah jadi 0007 dan alamatnya tinggal disambung: .../img_0007.jpg. Di folder assets sebuah tema Shopify, resep itu putus di dua tempat sekaligus.

Yang pertama, folder assets di Shopify itu datar. Tidak ada subdirektori di dalamnya, jadi tidak ada direktori yang bisa saya pegang sebagai potongan pertama. Yang kedua, asset_url tidak mengembalikan alamat polos. Dia mengembalikan URL CDN dengan parameter versi sebagai penangkal cache, dan nilainya bisa berbeda dari satu berkas ke berkas lain:

{{ 'seq-001.webp' | asset_url }}

menghasilkan sesuatu yang bentuknya kira-kira begini:

https://cdn.shopify.com/.../seq-001.webp?v=12345

Jadi tidak ada direktori yang bisa saya sambung dengan nomor, dan membuat URL per frame di atas folder yang datar seperti ini memang canggung.

Seratus dua puluh URL di dalam HTML

Jalan paling lurus adalah memanggil asset_url seratus dua puluh kali lewat loop Liquid, lalu menumpahkan hasilnya sebagai satu daftar JSON di sebuah atribut. Taksiran kasar saya waktu itu sekitar dua puluh kilobyte atribut, angka yang tidak pernah saya ukur betulan, dan seluruhnya menggembungkan DOM untuk isi yang sebenarnya cuma satu pola dengan nomor yang berganti.

Satu URL, satu penanda

Yang saya butuhkan cuma satu URL yang benar dan satu cara untuk menukar bagian nomornya. Liquid bisa melakukan itu tanpa loop sama sekali, dan blok yang sama sekalian menyediakan jalan keluar kalau kolom override di editor tema diisi:

{%- if section.settings.sequence_url != blank -%}
  {%- assign sequence_url = section.settings.sequence_url -%}
{%- else -%}
  {%- assign base_url = 'seq-001.webp' | asset_url -%}
  {%- assign sequence_url = base_url | replace: 'seq-001.webp', 'seq-{N}.webp' -%}
{%- endif -%}

Filter replace bekerja di tengah URL yang sudah jadi, jadi host CDN, jalurnya, dan ekor ?v= tetap menempel. Yang berubah cuma nama berkas di tengah, yang sekarang membawa penanda {N} untuk ditukar JavaScript nanti.

Kolom override-nya sendiri cuma sebuah kolom teks di schema, dengan catatan bahwa isinya opsional dan dibiarkan kosong kalau memakai aset bawaan. Gunanya supaya kliennya bisa pindah ke CDN eksternal seperti Cloudinary atau Bunny suatu saat tanpa mengubah kode, cukup menempel URL yang memuat {N}.

Markup-nya jadi satu elemen pembungkus yang membawa tiga atribut data, dengan canvas di dalamnya:

<div data-sequence
     data-sequence-url="{{ sequence_url }}"
     data-sequence-count="{{ sequence_count }}"
     data-sequence-pad="{{ sequence_pad | default: 3 }}">
  <canvas data-sequence-canvas></canvas>
</div>

Padding ikut jadi atribut karena keluaran animator bisa tiga, empat, atau lima digit. Set ini kebetulan tiga digit, tapi selama nilainya jadi konstanta di dalam JavaScript, tiap set baru berarti menyentuh kode lagi. Di schema, kontrol paddingnya berentang satu sampai enam digit dengan default tiga, dan jumlah framenya berentang tiga puluh sampai dua ratus empat puluh dengan langkah sepuluh dan default seratus dua puluh.

Di sisi JavaScript, pembangun alamatnya menyimpan kedua lingkungan tadi dalam satu fungsi:

const frameSrc = (i) => {
  const idx = String(i).padStart(pad, '0');
  if (url.includes('{N}')) return url.replace(/\{N\}/g, idx);
  // fallback lawas: anggap url sebuah folder
  return `${url.replace(/\/$/, '')}/img_${String(i).padStart(4, '0')}.jpg`;
};

Cabang pertama untuk tema, tempat alamatnya sudah utuh dan cuma perlu ditukar penandanya. Cabang kedua adalah fallback lawas yang memperlakukan URL sebagai folder lalu menempelkan nama berkas berpadding empat digit yang dipatok di kode.

Kanvasnya kosong sampai orang menggulir

Urusan URL beres, dan kanvasnya masih kosong waktu halaman dimuat. Frame pertamanya tidak pernah tergambar. Begitu pengguna menggulir, gambarnya muncul.

Akar masalahnya

Preload gambar itu asinkron. Tanpa pendengar load di tiap gambar, tidak ada yang memicu gambar pertama begitu frame awal selesai didekode, sementara jalur menggambar yang terpasang dipicu oleh posisi scroll. Sebelum orang menggulir, memang tidak pernah ada yang menyuruh kanvasnya menggambar.

Di kepala saya barisnya terbaca berurutan, seolah preload selesai otomatis berarti frame pertama tampil. Padahal preload cuma menyiapkan bahan. Menampilkan bahan itu pekerjaan lain yang harus dipanggil sendiri.

Perbaikannya

Satu pendengar load di tiap gambar, yang menjadwalkan gambar ulang begitu ada frame yang selesai:

for (let i = 1; i <= count; i++) {
  const img = new Image();
  img.decoding = 'async';
  img.addEventListener('load', queueDraw, { once: true });
  img.src = frameSrc(i);
  frames.push(img);
}

queueDraw menggambar lewat requestAnimationFrame supaya tidak memblokir, dan hasilnya frame pertama tergambar begitu selesai didekode, tanpa menunggu scroll. Pendengarnya dipasang di tiap gambar, bukan cuma yang pertama, dan opsi { once: true } menjaga tiap gambar cuma memicu sekali seumur hidupnya.

Mengisi penuh, bukan memuat seluruhnya

Perhitungan skalanya memakai Math.max atas rasio lebar dan tinggi:

const ratio = Math.max(canvas.width / img.naturalWidth, canvas.height / img.naturalHeight);

Math.min di posisi itu bukan cover melainkan contain: seluruh gambar muat, dan sisi yang tidak pas menyisakan pita kosong yang menembuskan latar. Math.max membuat kanvasnya selalu penuh.

Yang saya relakan

Filter replace membawa parameter versi milik frame pertama ke semua URL yang dihasilkannya. Frame kedua mungkin punya nilai v= sendiri yang berbeda, tapi berkas di jalur CDN itu tetap termuat, dan yang mungkin muncul cuma cache miss ringan. Saya menerimanya sebagai kompromi yang disengaja, bukan bug yang tertunda, karena tandingannya adalah mencetak seratus dua puluh URL lengkap ke dalam HTML.

Belakangan pemilik tokonya memutuskan hero-nya cukup satu gambar diam, jadi urutan frame ini tidak bertahan sebagai hasil akhir. Pola alamatnya sendiri tetap utuh: satu URL aset, satu penanda, satu loader yang membacanya, dan satu kolom override yang membuka jalan ke CDN eksternal tanpa menyentuh kode lagi.