Berurusan dengan gambar besar di kanvas HTML5

Saya memiliki gambar vektor berukuran 15000x15000 px yang ingin saya gunakan sebagai latar belakang untuk proyek kanvas. Saya harus dapat memotong sebagian gambar dan menggambarnya sebagai latar belakang dengan cepat dan sering (dalam requestAnimationFrame).

Untuk menggambar sektor gambar yang diperlukan saya menggunakan...

const xOffset = (pos.x - (canvas.width / 2)) + config.bgOffset + config.arenaRadius;
const yOffset = (pos.y - (canvas.height / 2)) + config.bgOffset + config.arenaRadius;
c.drawImage(image, xOffset, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);

Untuk menghitung luas latar belakang yang dibutuhkan dan menggambar gambar. Ini semua berfungsi dengan baik tetapi menggambar ulang lambat dan pengundian pertama bahkan lebih lambat.

Memuat gambar sebesar ini tampak konyol dan kinerjanya lamban. Bagaimana cara mengurangi ukuran file atau meningkatkan kinerja?

EDIT: Tidak ada solusi yang masuk akal untuk menggambar gambar sebesar itu dalam ukuran penuh. Karena latar belakangnya adalah pola yang berulang, solusi saya saat ini adalah mengambil satu "sel" pola dan menggambarnya beberapa kali.


person jolaleye    schedule 18.05.2018    source sumber
comment
Seberapa besar kanvas Anda? Juga, gambar vektor seperti di SVG?   -  person Máté Safranka    schedule 19.05.2018
comment
@ MátéSafranka Kanvas berskala sesuai ukuran layar. Ukuran asli yang saya desain adalah 1920x1080 (1920x974 untuk jendela sebenarnya). Saya membuat latar belakang di Affinity Designer sehingga saya bisa mengekspor sebagai SVG.   -  person jolaleye    schedule 19.05.2018
comment
Saya menjelajah sedikit keluar dari zona kepercayaan diri saya, tapi inilah dua sen saya. Menggambar sepotong bitmap seharusnya tidak menjadi operasi yang intensif CPU, karena pada dasarnya ini hanya menyalin sekumpulan byte dari satu buffer ke buffer lainnya. Satu-satunya sumber perlambatan yang dapat saya bayangkan adalah jumlah byte yang harus disalin, yaitu ukuran kanvas target. Namun -- dan di sinilah rasa percaya diri saya melemah -- menggambar dari SVG mungkin merupakan hal yang berbeda, karena dalam hal ini sumbernya bukan bitmap, artinya ada banyak perhitungan tambahan.   -  person Máté Safranka    schedule 19.05.2018
comment
Jadi, selain menggunakan kanvas yang lebih kecil, satu-satunya dugaan saya adalah menggunakan bitmap, bukan SVG sebagai sumber. Namun, pada ukuran 15k kali 15k piksel, hal itu mungkin akan menghabiskan banyak sekali memori.   -  person Máté Safranka    schedule 19.05.2018
comment
@ MátéSafranka Saya akan mengotak-atiknya sedikit lagi untuk melihat seberapa lambatnya seiring kemajuan saya dalam proyek ini. Ukuran file gambar adalah 9,23MB... Saya kira itu hanya menimbulkan beberapa tanda bahaya. Adapun poin terakhir Anda, gambar dirancang dengan program vektor tetapi dapat diekspor sebagai PNG (yang saya gunakan). Faktanya, ketika saya pertama kali mencoba, menggambar SVG bahkan tidak berhasil.   -  person jolaleye    schedule 19.05.2018


Jawaban (2)


15000px x 15000px memang besar.

GPU harus menyimpannya sebagai data RGB mentah di memorinya (saya tidak ingat persis Matematikanya tapi menurut saya itu seperti lebar x tinggi x 3 byte, yaitu 675MB dalam kasus Anda, yang lebih dari yang bisa dilakukan GPU pada umumnya. handle).
Tambahkan ke semua grafis lain yang mungkin Anda miliki, dan GPU Anda akan dipaksa untuk melepaskan gambar besar Anda dan mengambilnya lagi setiap frame.

Untuk menghindari hal tersebut, sebaiknya Anda membagi gambar besar menjadi beberapa gambar lebih kecil, dan memanggil beberapa kali drawImage per frame. Dengan cara ini, dalam kasus terburuk, GPU hanya perlu mengambil bagian yang dibutuhkan, dan dalam kasus terbaik, GPU sudah menyimpannya di memorinya.

Berikut adalah bukti kasar konsep, yang akan membagi gambar svg 5000*5000px menjadi ubin 250*250px. Tentu saja, Anda harus menyesuaikannya dengan kebutuhan Anda, tapi ini mungkin bisa memberi Anda gambaran.

console.log('generating image...');
var bigImg = new Image();
bigImg.src = URL.createObjectURL(generateBigImage(5000, 5000));
bigImg.onload = init;

function splitBigImage(img, maxSize) {
  if (!maxSize || typeof maxSize !== 'number') maxSize = 500;

  var iw = img.naturalWidth,
    ih = img.naturalHeight,
    tw = Math.min(maxSize, iw),
    th = Math.min(maxSize, ih),
    tileCols = Math.ceil(iw / tw), // how many columns we'll have
    tileRows = Math.ceil(ih / th), // how many rows we'll have
    tiles = [],
    r, c, canvas;

  // draw every part of our image once on different canvases
  for (r = 0; r < tileRows; r++) {
    for (c = 0; c < tileCols; c++) {
      canvas = document.createElement('canvas');
      // add a 1px margin all around for antialiasing when drawing at non integer
      canvas.width = tw + 2;
      canvas.height = th + 2;
      canvas.getContext('2d')
        .drawImage(img,
          (c * tw | 0) - 1, // compensate the 1px margin
          (r * tw | 0) - 1,
          iw, ih, 0, 0, iw, ih);
      tiles.push(canvas);
    }
  }

  return {
    width: iw,
    height: ih,
    // the drawing function, takes the output context and x,y positions
    draw: function drawBackground(ctx, x, y) {
      var cw = ctx.canvas.width,
        ch = ctx.canvas.height;
      // get our visible rectangle as rows and columns indexes
      var firstRowIndex = Math.max(Math.floor((y - th) / th), 0),
        lastRowIndex = Math.min(Math.ceil((ch + y) / th), tileRows),
        firstColIndex = Math.max(Math.floor((x - tw) / tw), 0),
        lastColIndex = Math.min(Math.ceil((cw + x) / tw), tileCols);

      var col, row;
      // loop through visible tiles and draw them
      for (row = firstRowIndex; row < lastRowIndex; row++) {
        for (col = firstColIndex; col < lastColIndex; col++) {
          ctx.drawImage(
            tiles[row * tileCols + col], // which part
            col * tw - x - 1, // x position
            row * th - y - 1 // y position
          );
        }
      }
    }
  };
}

function init() {
  console.log('image loaded');

  var bg = splitBigImage(bigImg, 250); // image_source, maxSize
  var ctx = document.getElementById('canvas').getContext('2d');
  var dx = 1,
    dy = 1,
    x = 150,
    y = 150;
  anim();
  setInterval(changeDirection, 2000);

  function anim() {
    // just to make the background position move...
    x += dx;
    y += dy;
    if (x < 0) {
      dx *= -1;
      x = 1;
    }
    if (x > bg.width - ctx.canvas.width) {
      dx *= -1;
      x = bg.width - ctx.canvas.width - 1;
    }
    if (y < 0) {
      dy *= -1;
      y = 1;
    }
    if (y > bg.height - ctx.canvas.height) {
      dy *= -1;
      y = bg.height - ctx.canvas.height - 1;
    }
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    if(chck.checked) {
      // that's how you call it
      bg.draw(ctx, x, y);
    }
    else {
      ctx.drawImage(bigImg, -x, -y);
    }
    requestAnimationFrame(anim);
  }

  function changeDirection() {
    dx = (Math.random()) * 5 * Math.sign(dx);
    dy = (Math.random()) * 5 * Math.sign(dy);
  }

  setTimeout(function() { console.clear(); }, 1000);
}
// produces a width * height pseudo-random svg image
function generateBigImage(width, height) {
  var str = '<svg width="' + width + '" height="' + height + '" xmlns="http://www.w3.org/2000/svg">',
    x, y;
  for (y = 0; y < height / 20; y++)
    for (x = 0; x < width / 20; x++)
      str += '<circle ' +
      'cx="' + ((x * 20) + 10) + '" ' +
      'cy="' + ((y * 20) + 10) + '" ' +
      'r="15" ' +
      'fill="hsl(' + (width * height / ((y * x) + width)) + 'deg, ' + (((width + height) / (x + y)) + 35) + '%, 50%)" ' +
      '/>';
  str += '</svg>';
  return new Blob([str], {
    type: 'image/svg+xml'
  });
}
<label>draw split <input type="checkbox" id="chck" checked></label>
<canvas id="canvas" width="800" height="800"></canvas>

Dan sebenarnya, dalam kasus spesifik Anda, saya pribadi bahkan akan menyimpan gambar terpisah di server (baik dalam svg karena memerlukan bandwidth lebih sedikit), dan menghasilkan ubin dari sumber yang berbeda. Namun saya akan menjadikannya sebagai latihan bagi pembaca.

person Kaiido    schedule 22.05.2018
comment
Saya sebenarnya menggunakan metode yang agak mirip dengan ini... karena latar belakang saya adalah pola berulang, saya mengambil satu sel pola dan menggambarnya beberapa kali. Terima kasih atas solusi Anda! - person jolaleye; 25.05.2018

Saya sangat merekomendasikan penggunaan EaselJS untuk merender beberapa/objek besar ke kanvas HTML. Perpustakaan ini mendukung caching vektor, webgl, dan banyak lagi.

Berikut ini contoh menyimpan beberapa objek vektor ke dalam bitmap (klik kotak centang untuk perbandingan): https://www.createjs.com/demos/easeljs/cache

Anda harus dapat memperlakukan vektor latar belakang Anda dengan cara yang sama. Cukup cache objeknya dan biarkan perangkat lunak menangani semua pekerjaan berat.

person doppler    schedule 18.05.2018
comment
Demo itu sangat mengesankan. Saya akan memeriksanya, terima kasih! - person jolaleye; 19.05.2018
comment
Tak satu pun dari demo ini menunjukkan bagaimana hal ini meningkatkan gambar gambar besar. Kasus OP adalah gambar yang sudah dirasterisasi berukuran 15000 * 15000 px. Strategi caching EaselJS adalah menghindari pemanggilan metode Path yang sama berkali-kali, dengan membuat versi rasternya. Itu tidak akan membantu OP. - person Kaiido; 22.05.2018
comment
@kaiido, berikut adalah contoh bagus penggunaan 'cacheCanvas' setelah vektor diubah menjadi bitmap: stackoverflow.com/a/35991855 /2510368. Secara teoritis, Anda dapat menyimpan cache vektor besar ke dalam bitmap besar, lalu menyimpan bitmap kecil dari objek kanvas tersebut. - person doppler; 22.05.2018
comment
@Kaiido, saya mengerti. Dengan asumsi gambar 15000x15000 ini (vektor atau bitmap) adalah statis, Anda dapat 'cacheCanvas' gambar menjadi sekitar 800x600 dan menggambar ulang pada dimensi yang jauh lebih kecil. - person doppler; 23.05.2018
comment
@doppler tetapi gambar harus ditampilkan pada skala sebenarnya. Bagaimanapun, saya akan berhenti berdebat dan tetap berpegang pada komentar pertama saya yang menyatakan bahwa jawaban ini tidak menargetkan pertanyaan yang diajukan (Berurusan dengan gambar besar di kanvas HTML5). - person Kaiido; 23.05.2018
comment
EaselJS tidak menyelesaikan masalah gambar asli, tapi saya akhirnya menggunakannya untuk manfaat lainnya, jadi terima kasih atas rekomendasinya. - person jolaleye; 25.05.2018