HTML-ইন-ক্যানভাস: প্রযুক্তিগত ধারণা

HTML-in-Canvas API আপনাকে সরাসরি 2D, WebGL বা WebGPU ক্যানভাস প্রসঙ্গে ইন্টার‍্যাক্টিভ, অ্যাক্সেসযোগ্য DOM সাবট্রি রেন্ডার করতে দেয়।

এই নির্দেশিকায় HTML-ইন-ক্যানভাস টেকনিক্যাল কনসেপ্টের রূপরেখা দেওয়া হয়েছে যা সব ক্যানভাস কনটেক্সট জুড়ে সাধারণ:

HTML রেন্ডার করার জন্য আপনার ক্যানভাস প্রস্তুত করুন

<canvas>-এর মধ্যে HTML এলিমেন্ট রেন্ডার করতে, নিম্নলিখিত দুটি অ্যাট্রিবিউট কনফিগার করুন:

  • <canvas>-এ content="drawable" সেট করুন: এটি ক্যানভাস এলিমেন্টকে তার ডিসেন্ডেন্ট DOM নোডগুলিকে শুধুমাত্র লুকানো কন্টেন্ট হিসেবে বিবেচনা করার পরিবর্তে লেআউট করার জন্য বেছে নেয়।
  • টার্গেট এলিমেন্টে drawable সেট করুন: এটি কোনও এলিমেন্টকে ক্যানভাস রেন্ডারিংয়ের জন্য উপযুক্ত একটি স্বতন্ত্র পেইন্ট রুট হিসেবে চিহ্নিত করে।
<canvas id="mainCanvas" width="600" height="400" content="drawable">
  <div id="parentElement" class="card" drawable>
    <h2>Container Title</h2>
    <p>Descriptive text inside the parent element.</p>
    <!-- Nested drawable: excluded from parentElement and drawn independently -->
    <button id="childElement" class="btn" drawable>Action Button</button>
  </div>
</canvas>

ড্রয়েবল সাবট্রি বোঝা

ক্যানভাসে রেন্ডার করার জন্য HTML প্রস্তুত করার সময়, আপনাকে সিদ্ধান্ত নিতে হবে যে কোন এলিমেন্টগুলি স্বাধীনভাবে আঁকা উচিত। প্রতিটি drawable এলিমেন্ট একটি স্বতন্ত্র পেইন্ট রুট এবং এর আঁকা যায় এমন সাবট্রিকে উপস্থাপন করে।

ড্রয়েবল সাবট্রি drawable অ্যাট্রিবিউট সহ একটি এলিমেন্ট এবং এর সব ডিসেন্ডেন্ট নিয়ে গঠিত হয়, বাদ দিয়ে যেকোনও নেস্টেড ডিসেন্ডেন্ট যা drawable-ও বহন করে, যেমন:

<canvas content="drawable">
  <!-- Subtree A (Renders parent container and text) -->
  <div id="parentElement" drawable>
    <h2>Parent Card Heading</h2>
    <p>Card body text content.</p>
    <!-- Subtree B (Excluded from Subtree A and drawn separately) -->
    <button id="nestedChildElement" drawable>
      Clickable Button
    </button>
  </div>
</canvas>
এলিমেন্ট পেন্ট বাউন্ডারি আচরণ
#parentElement (সাবট্রি ক) ব্যাকগ্রাউন্ড প্যানেল, শিরোনাম ও অনুচ্ছেদ drawElementImage(parentElement, ...)-কে কল করার সময় রেন্ডার করা হয়েছে
#nestedChildElement (সাবট্রি B) বোতামের সারফেস ও টেক্সট #parentElement সাবট্রি থেকে বাদ দেওয়া হয়েছে; আলাদা drawElementImage(nestedChildElement, ...) কলে রেন্ডার করা হয়েছে
drawable বোতামটি drawable কার্ডের মধ্যে নেস্ট করা আছে এবং প্রত্যেকটি নিজস্ব drawElementImage() কলের মাধ্যমে ড্র করা হয়।

নেস্টিং ড্রয়েবল এলিমেন্ট আপনাকে সিম্যান্টিক HTML সম্পর্ক বজায় রাখতে সাহায্য করে এবং একই সাথে আপনার ক্যানভাস দৃশ্যে চাইল্ড এলিমেন্টকে স্বাধীনভাবে লেয়ার, পজিশন বা অ্যানিমেট করার স্বাধীনতা দেয়।

paint ইভেন্ট সম্পর্কে বোঝা

HTML-in-Canvas API, paint ইভেন্ট নিয়ে আসে। ব্রাউজার অটোমেটিক paint ইভেন্ট <canvas>-এ পাঠায় যখনই লেআউট, টেক্সট বা CSS স্টাইল এর যেকোনও drawable ডিসেন্ডেন্টে পরিবর্তন হয়।

canvas.addEventListener("paint", (event) => {
  // Clear the viewport
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Redraw elements individually
  ctx.drawElementImage(parentElement, 20, 20);
  ctx.drawElementImage(childElement, 40, 90);
});

paint ইভেন্ট CanvasPaintEvent-এর একটি ইনস্ট্যান্স ডিসপ্যাচ করে, যা স্ট্যান্ডার্ড DOM Event-কে এক্সটেন্ড করে এবং এতে একটি changedElements প্রপার্টি থাকে যাতে শুধুমাত্র সেইসব drawable ডিসেন্ডেন্ট থাকে যেগুলি শেষবার পেইন্ট করার পর থেকে ভিজ্যুয়ালি পরিবর্তিত হয়েছে (যেমন, স্টাইল পরিবর্তন, লেআউট শিফ্ট, টেক্সট এডিট বা ফোকাস স্টেট পরিবর্তন)।

canvas.addEventListener("paint", (event) => {
  // Array of changed elements
  for (const el of event.changedElements) {
    console.log(`Element #${el.id} visually changed and needs repainting`);
  }
});
ড্র করা যায় এমন ডিসেন্ডেন্টের রেন্ডারিং পরিবর্তন হলে ব্রাউজার ক্যানভাসে পেইন্ট ফায়ার করে।

ফ্রেম লাইফসাইকেল

paint ইভেন্টের মধ্যে এক্সিকিউট করা ক্যানভাস ড্রয়িং কমান্ড বর্তমান রেন্ডারিং আপডেটে দেখা যায়, কিন্তু paint ইভেন্টের মধ্যে করা DOM পরিবর্তন পরবর্তী ফ্রেমের আগে দেখা যায় না:

canvas.addEventListener("paint", () => {
  // Renders in the CURRENT rendering update:
  ctx.drawElementImage(parentElement, 20, 20);

  // Modifying DOM styles inside paint schedules a new layout cycle;
  // changes will appear in the NEXT frame:
  parentElement.style.backgroundColor = "#e0f2fe";
});

নেস্ট করা ক্যানভাস

আপনার পৃষ্ঠায় নেস্টেড <canvas content="drawable"> এলিমেন্ট থাকলে, ব্রাউজার রিভার্স DOM ট্রি অর্ডারে (পূর্বপুরুষের আগে বংশধররা ফায়ার করে) paint ইভেন্ট ডিসপ্যাচ করে। এটি নিশ্চিত করে যে চাইল্ড ক্যানভাস এলিমেন্টগুলি পেরেন্ট ক্যানভাস এলিমেন্টগুলি কম্পোজিট করার আগে রেন্ডার করে।

ধরুন, নিম্নলিখিত উদাহরণে ভিতরের nestedElement এলিমেন্টটি অবশ্যই দৃশ্যত পরিবর্তন হতে হবে (যেমন, ব্যবহারকারী যখন এটির উপর পয়েন্টার ধরে রাখেন তখন এর CSS ব্যাকগ্রাউন্ডের রঙ পরিবর্তিত হয়)। প্রথমে, paint ইভেন্ট nestedCanvas ক্যানভাসে ফায়ার করে। এর লিসেনার চাইল্ড বাফার মুছে দেয় এবং আপডেট করা স্ট্যাটাস রেন্ডার করার জন্য ctx.drawElementImage(nestedElement, ...)-কে কল করে। তারপর parentCanvas-এ paint ইভেন্ট ফায়ার হয়। এর লিসেনার দৃশ্যে wrapperElement (যার মধ্যে আগে থেকে আপডেট করা nestedCanvas ক্যানভাস আছে) আঁকে।

<!-- Outer parent canvas -->
<canvas id="parentCanvas" width="800" height="600" content="drawable">
  <!-- Wrapper container for nested content -->
  <div id="wrapperElement" drawable>

    <!-- Nested child canvas -->
    <canvas id="nestedCanvas" width="300" height="200" content="drawable">
      <div id="nestedElement" class="badge" drawable>
        Inner Child Content
      </div>
    </canvas>

  </div>
</canvas>

<script>
const parentCanvas = document.getElementById("parentCanvas");
const parentCtx = parentCanvas.getContext("2d");

const nestedCanvas = document.getElementById("nestedCanvas");
const nestedCtx = nestedCanvas.getContext("2d");

const nestedElement = document.getElementById("nestedElement");
const wrapperElement = document.getElementById("wrapperElement");

// ============================================================================
// 1. Child Canvas Listener (Fires FIRST)
// ============================================================================
nestedCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the child canvas with the updated inner element
  nestedCtx.clearRect(0, 0, nestedCanvas.width, nestedCanvas.height);
  nestedCtx.drawElementImage(nestedElement, 10, 10);

  console.log("1. Child nested canvas rendered fresh pixels.");
});

// ============================================================================
// 2. Parent Canvas Listener (Fires SECOND)
// ============================================================================
parentCanvas.addEventListener("paint", (event) => {
  // Clear and redraw the parent canvas scene
  parentCtx.clearRect(0, 0, parentCanvas.width, parentCanvas.height);

  // Draws the wrapper container (including the freshly painted nestedCanvas)
  parentCtx.drawElementImage(wrapperElement, 50, 50);

  console.log("2. Parent canvas composited the complete scene.");
});
</script>
<canvas content="drawable"> এলিমেন্ট নেস্ট করা থাকলে, পেইন্ট ইভেন্ট রিভার্স ট্রি অর্ডারে ফায়ার করে: নেস্ট করা ক্যানভাস প্রথমে পেইন্ট করে, যাতে পেরেন্ট ক্যানভাস তার আপ-টু-ডেট পিক্সেল আঁকতে পারে।

paint ইভেন্ট ট্রিগার করুন

কোনও DOM টেক্সট বা স্টাইল পরিবর্তন না হলেও, পরবর্তী ফ্রেমে একটি paint ইভেন্ট শিডিউল করতে requestPaint() কল করুন। এটি মুভিং এলিমেন্ট সহ ইন্টার‍্যাক্টিভ দৃশ্য রেন্ডার করার জন্য ব্যবহার করা যেতে পারে, যেমন:

let animatedX = 0;

canvas.addEventListener("paint", () => {
  animatedX += 1.5; // Move the element horizontally
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.drawElementImage(staticElement, 20, 20);
  ctx.drawElementImage(movingElement, animatedX, 200);
  // Request canvas paint on each rendering update for smooth animation
  canvas.requestPaint();
});

canvas.requestPaint();

কোনও এলিমেন্টের জ্যামিতি আপডেট করুন

আপনি যখন ক্যানভাসে কোনও HTML এলিমেন্ট আঁকেন, তখন আপনার ড্রয়িং কোড ঠিক করে যে এলিমেন্টটি কোথায় দেখা যাবে এবং এটি কতটা বড় হবে। সাধারণ HTML কন্টেন্টের মতো, এলিমেন্টের অন-স্ক্রিন পজিশন এবং সাইজ, এর জিওমেট্রি, শুধুমাত্র এর কম্পিউটেড স্টাইল থেকে নির্ধারণ করা যায় না। নিম্নলিখিত কাজগুলি করার জন্য ব্রাউজারের একটি এলিমেন্টের জ্যামিতি প্রয়োজন:

  • সঠিক এলিমেন্টে ক্লিক, ট্যাপ ও পয়েন্টার মুভমেন্টের মতো ইনপুট ইভেন্ট পাঠান। একে হিট টেস্টিং বলা হয়।
  • লেআউট কোয়েরি থেকে সঠিক ফলাফল দেখান, যেমন getBoundingClientRect()।
  • স্ক্রিন ম্যাগনিফায়ারের মতো সহায়ক প্রযুক্তিতে এলিমেন্টের বাউন্ড রিপোর্ট করুন।

2D-তে, drawElementImage() ডিফল্ট হিসেবে অটোমেটিক জিওমেট্রি আপডেট করে। আপনি যে আর্গুমেন্ট পাস করেন, তার সাথে এলিমেন্টের কম্পিউট করা স্টাইল এবং কনটেক্সটের বর্তমান ট্রান্সফর্ম, এলিমেন্ট কোথায় দেখা যাবে তা নির্ধারণ করার জন্য যথেষ্ট।

WebGL ও WebGPU-এর ক্ষেত্রে, ব্রাউজার জিওমেট্রি নির্ধারণ করতে পারে না। একটি HTML এলিমেন্ট টেক্সচারে কপি করা হয় এবং আপনার শেডার প্রোগ্রাম যেকোনও ইচ্ছামতো উপায়ে সেই টেক্সচারকে দৃশ্যে ম্যাপ করতে পারে। এলিমেন্টটি কোথায় দেখা যাবে তা ব্রাউজারকে জানাতে, updateElementGeometry()-এ একটি ট্রান্সফর্ম পাস করুন।

কোড যখন updateElementGeometry() কল করা বন্ধ করে দেয়, তখন ব্রাউজার আর জানতে পারে না যে 3D কার্ডে HTML এলিমেন্ট কোথায় আছে। কার্ডটি ঘুরতে থাকে, কিন্তু ক্লিকগুলি এখনও যেখানে ছিল সেখানেই ল্যান্ড করে।

জ্যামিতি আপডেট করতে, পদ্ধতিতে একটি স্থানিক রূপান্তর পাস করুন। বিভিন্ন প্রসঙ্গের জন্য কীভাবে ট্রান্সফর্ম তৈরি করতে হয় তা জানুন:

হিট-টেস্ট স্ট্যাকিং অর্ডার ম্যানেজ করুন

ডিফল্ট হিসেবে, প্রতিবার কোনও এলিমেন্টের জ্যামিতি আপডেট করা হলে, ব্রাউজার এটিকে ইন্টার‍্যাকশন স্ট্যাকের সবচেয়ে উপরে নিয়ে আসে (সবচেয়ে শেষে আঁকা = সবচেয়ে উপরে থাকা ক্লিক টার্গেট)। preserveHitTestOrder ব্যবহার করে আপনি এই আচরণ নিয়ন্ত্রণ করতে পারবেন:

// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
  canvasTransform: customTransform,
  preserveHitTestOrder: true
});
Option আচরণ ব্যবহারিক ক্ষেত্র
preserveHitTestOrder: false (ডিফল্ট) এলিমেন্টকে হিট-টেস্ট স্ট্যাকের সবচেয়ে উপরে নিয়ে যায়। ফোরগ্রাউন্ড এলিমেন্ট যাতে প্রথমে ক্লিক করা উচিত।
preserveHitTestOrder: true এলিমেন্টটিকে তার বর্তমান স্ট্যাকের অগ্রাধিকার অনুযায়ী রেখে দেয়। ব্যাকগ্রাউন্ড এলিমেন্ট যা ফোরগ্রাউন্ড এলিমেন্টের পরে আপডেট করতে হবে।

জিওমেট্রি রিসেট করুন

DOM থেকে কোনও এলিমেন্ট না সরিয়েই সেটি আন-ড্র করতে, canvas.clearElementGeometry() কল করুন। এটি এলিমেন্টের ক্যানভাস ট্রান্সফর্ম রিসেট করে এবং অ্যাক্সেসিবিলিটি ট্রিতে এটিকে অফস্ক্রিন হিসেবে চিহ্নিত করে:

// Remove from canvas hit-testing and mark as offscreen
canvas.clearElementGeometry(childElement);

সাইজ ও ইউনিট

আপনি ক্যানভাসে HTML আঁকলে, ব্রাউজার একসাথে তিনটি কোঅর্ডিনেট সিস্টেমে কাজ করে: CSS পিক্সেল, ডিভাইস পিক্সেল এবং ক্যানভাস গ্রিড পিক্সেল।

লেআউট চলাকালীন কন্টেন্ট পরিমাপ ও পজিশন করার জন্য CSS পিক্সেল ব্যবহার করা হয়। তারপর, ব্রাউজার সেগুলিকে devicePixelRatio দিয়ে স্কেল করে যাতে ফিজিক্যাল ডিভাইস পিক্সেলে পৃষ্ঠাটি রাস্টারাইজ করা যায়। CSS পিক্সেল হল একটি লজিক্যাল ইউনিট, তাই অবস্থান এবং সাইজ প্রায়ই ডিভাইসের পিক্সেলের ভগ্নাংশ হিসেবে শেষ হয় (যেমন, ১০.৫ CSS পিক্সেল × ১.৫ DPR = ১৫.৭৫ ডিভাইস পিক্সেল)। স্ক্রিন শুধুমাত্র সম্পূর্ণ পিক্সেল কালার করতে পারে, তাই ব্রাউজারকে তাদের মধ্যে থাকা যেকোনও কিছু সমাধান করতে হবে। এটি দুটি উপায়ে এই কাজ করে:

  • স্ন্যাপিং: বক্সের সীমানা, বর্ডার এবং লেয়ার অফসেটগুলি নিকটতম সম্পূর্ণ ডিভাইস পিক্সেলে রাউন্ড করা হয় যাতে সেগুলি ক্রিস্প থাকে।
  • অ্যান্টি-অ্যালাইজিং: টেক্সট গ্লিফ, কার্ভ এবং রূপান্তরিত প্রান্তের জন্য, আংশিক কভারেজ আশেপাশের পিক্সেলের সাথে মিশে যায় যাতে সেগুলি এখনও মসৃণ এবং সঠিক জায়গায় দেখায়।

<canvas>-এর নিজস্ব পিক্সেল গ্রিড আছে, যা এর width এবং height অ্যাট্রিবিউট দ্বারা সেট করা হয়, যা CSS পিক্সেল এবং ডিভাইস পিক্সেল উভয় থেকে আলাদা। এটি দেখানোর জন্য, ব্রাউজার ফিজিক্যাল পিক্সেল গ্রিডে ক্যানভাস বিটম্যাপ রিস্যাম্পেল করে, প্রতিটি ডিভাইস পিক্সেল পূরণ করতে প্রতিবেশী ক্যানভাস পিক্সেল ব্লেন্ড করে। CSS, ক্যানভাস ও ডিভাইস পিক্সেলের মধ্যে ব্রাউজার কীভাবে ম্যাপ করে তা জানতে, devicePixelContentBox-এর মাধ্যমে পিক্সেল-পারফেক্ট রেন্ডারিং দেখুন।

যেকোনও drawable HTML এলিমেন্ট CSS পিক্সেলে লে-আউট করা হয়। ক্যানভাসে আঁকা কন্টেন্ট দুটি কনভার্সনের মাধ্যমে যেতে পারে, CSS পিক্সেল থেকে ক্যানভাস গ্রিড এবং তারপর ক্যানভাস গ্রিড থেকে ফিজিক্যাল পিক্সেল, প্রতিটি ধাপ যা ঠিকভাবে সারিবদ্ধ হয় না তা ব্লেন্ডিংয়ের আরেকটি রাউন্ড যোগ করে এবং ফলাফলটি অস্পষ্ট দেখাতে পারে। এটি এড়াতে, ক্যানভাস গ্রিডকে ফিজিক্যাল পিক্সেল গ্রিডের সাথে ম্যাচ করান। এটি দুটি উদ্দেশ্য পূরণ করে:

  • একই রকম রেন্ডারিং: ক্যানভাস যদি ফিজিক্যাল পিক্সেলে ১:১ ম্যাপ করে, তাহলে CSS পিক্সেলকে ক্যানভাস পিক্সেলে কনভার্ট করলে, রেগুলার HTML-এর জন্য CSS পিক্সেল থেকে ফিজিক্যাল পিক্সেলে ব্রাউজারের কনভার্সনের মতো একই ফলাফল পাওয়া যায়।
  • অতিরিক্ত ব্লেন্ডিং নেই: ক্যানভাস যদি ফিজিক্যাল পিক্সেলে ১:১ ম্যাপ করে, তাহলে এটিকে ডিভাইসের পিক্সেলে কনভার্ট করলে কোনও অস্পষ্টতা যোগ হয় না।

নিচের ছবিতে পিক্সেল কনভার্সন দেখানো হয়েছে যার ফলে কন্টেন্ট অস্পষ্ট হয়ে গেছে। তিনটি গ্রিড সারিবদ্ধ নয়: ক্যানভাস এলিমেন্ট হল ৩৬০ × ১৮০ CSS পিক্সেল এবং ক্যানভাস সাইজ হল ৩০০ × ১৫০ ক্যানভাস গ্রিড পিক্সেল। ১.৫ ডিভাইস পিক্সেল অনুপাতে, ক্যানভাস ৫৪০ × ২৭০ ডিভাইস পিক্সেল কভার করে। একটি CSS পিক্সেল ১.৫ ডিভাইস পিক্সেল জুড়ে থাকে এবং একটি ক্যানভাস পিক্সেল ১.২ CSS পিক্সেল বা ১.৮ ডিভাইস পিক্সেল জুড়ে থাকে (প্যানেল ১)। বৃত্তটি আঁকা হলে, এর অবস্থান এবং ব্যাসার্ধ ক্যানভাস পিক্সেলে থাকে, CSS পিক্সেলে নয়, তাই ব্রাউজার এটিকে ৩০০ × ১৫০ ক্যানভাস বিটম্যাপে আঁকে এবং প্রান্তটিকে অ্যান্টি-অ্যালাইজ করে (প্যানেল ২)। ক্যানভাস দেখানোর জন্য, ব্রাউজার তখন এই বিটম্যাপকে ৫৪০ × ২৭০ ডিভাইস পিক্সেল পর্যন্ত প্রসারিত করে, তাই ব্রাউজারকে ইন্টারপোলেট করতে হয়: প্রতিটি ডিভাইস পিক্সেল নিকটতম ক্যানভাস পিক্সেলের একটি ওয়েটেড মিক্স নেয়, যার মধ্যে ইতিমধ্যেই অ্যান্টি-অ্যালাইজড এজ রয়েছে। দ্বিতীয়বার প্রান্তটি ব্লেন্ড করা হয় এবং একাধিক ডিভাইস পিক্সেল জুড়ে ছড়িয়ে পড়ে, তাই বৃত্তটি অস্পষ্ট দেখায় (প্যানেল ৩)। ক্যানভাস গ্রিড ডিভাইসের পিক্সেলের সাথে ১:১ অনুপাতে ম্যাচ করলে এটি এড়ানো যেতে পারে। ব্রাউজারটি তখন সরাসরি ডিভাইসের গ্রিডে একবারই প্রান্তটিকে অ্যান্টি-অ্যালাইজ করবে এবং রিস্যাম্পলিং ছাড়াই বিটম্যাপ প্রদর্শন করবে, যাতে প্রান্তটি ক্রিস্প থাকে (প্যানেল ৪):

কার্ভড এজের জন্য পিক্সেল গ্রিডের তুলনা করে চার-প্যানেলের ডায়াগ্রাম: ১. অ্যালাইন না করা CSS (৩৬০ x ১৮০), ক্যানভাস (৩০০ x ১৫০) এবং ডিভাইস (৫৪০ x ২৭০) গ্রিড। ২. কার্ভটি ৩০০ বাই ১৫০ ক্যানভাস গ্রিডে অ্যান্টি-অ্যালাইজ করা হয়েছে। ৩. সেই বিটম্যাপ ৫৪০ বাই ২৭০ ডিভাইস গ্রিডে ইন্টারপোলেট করা হয়েছে, দ্বিতীয়বার অ্যান্টি-অ্যালাইজিং করা হয়েছে এবং প্রান্তটি অস্পষ্ট করা হয়েছে। ৪. ৫৪০ বাই ২৭০ ক্যানভাস যা ডিভাইসের গ্রিডের সাথে ১ থেকে ১ পর্যন্ত মেলে, একবার অ্যান্টি-অ্যালাইজড করা হয় যাতে প্রান্তটি স্পষ্ট হয়।
১:১ ক্যানভাস-টু-ডিভাইস গ্রিডের (প্যানেল ৪) তুলনায়, কীভাবে অ্যালাইন না করা CSS, ক্যানভাস এবং ডিভাইস পিক্সেল গ্রিড ডবল অ্যান্টি-অ্যালাইজিং (প্যানেল ১–৩) ঘটায়।

ক্রিস্প রেন্ডারিংয়ের জন্য, ক্যানভাস গ্রিড অবশ্যই ক্যানভাস কভার করা ডিভাইস পিক্সেলের সংখ্যার সাথে মিলতে হবে: পূর্ববর্তী উদাহরণে ৫৪০ × ২৭০।

HTML কার্ডের পাশাপাশি তুলনা, যাতে লেখা আছে 'DOM-এর তরফ থেকে হ্যালো! এই কার্ডটি হল ক্যানভাসে আঁকা HTML।' বাঁদিকে: ডিফল্ট ৩০০ বাই ১৫০ ক্যানভাস গ্রিডে আঁকা, অস্পষ্ট দেখাচ্ছে। ডানদিকে: ResizeObserver-এর মাধ্যমে ৮০০ বাই ৪০০ ক্যানভাস গ্রিড সাইজে আঁকা হয়েছে, যা দেখতে শার্প লাগছে।
একই ক্যানভাস, ৪০০ x ২০০ CSS পিক্সেল, ১.৫ DPR সহ স্ক্রিনে। বাঁদিকে: ৪০০ x ২০০ ক্যানভাস গ্রিড। ডানদিকে: ডিপিআর (DPR) অনুযায়ী অ্যাডজাস্ট করা, একটি ৬০০ x ৩০০ গ্রিড।

রিসাইজ অবজারভারের সাহায্যে ক্যানভাস গ্রিডের সাইজ পরিবর্তন করুন

সঠিক গ্রিড সাইজ ডিভাইসের উপর নির্ভর করে এবং লেআউট পরিবর্তন হলে, ব্যবহারকারী পৃষ্ঠা জুম করলে অথবা ব্যবহারকারী উইন্ডোটি অন্য ডিসপ্লেতে সরালে এটি পরিবর্তিত হতে পারে। তাই গ্রিডের সাইজ হার্ডকোড করার পরিবর্তে, ব্রাউজারকে বলতে দিন যে এটি কী হওয়া উচিত।

স্ক্রিনে ক্যানভাসের সাথে ক্যানভাস গ্রিড সিঙ্ক করে রাখতে, সবসময় ResizeObserver দিয়ে এটির সাইজ নির্ধারণ করুন এবং ক্যানভাস গ্রিড devicePixelContentBoxSize-এ অ্যাডজাস্ট করুন:

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);
  canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

CSS পিক্সেলকে ক্যানভাস গ্রিড কোঅর্ডিনেটে কনভার্ট করুন

paint হ্যান্ডলার প্রায়শই ড্রয়িং কলের সাথে DOM পরিমাপকে একত্রিত করে—যেমন, কোনও এলিমেন্টের সাইজের উপর ভিত্তি করে সেটির পজিশন নির্ধারণ করা। দুটি আলাদা ইউনিট ব্যবহার করে:

  • ক্যানভাস পিক্সেল ক্যানভাস গ্রিড বর্ণনা করে, যার সাইজ canvas.width এবং canvas.height দ্বারা নির্ধারিত হয়। যেমন, drawElementImage() ড্রয়িং পদ্ধতির dx, dy, dwidth এবং dheight প্যারামিটার অথবা অন্যান্য ক্যানভাস API প্যারামিটার, যেমন fillRect() এবং translate()।
  • CSS পিক্সেল পৃষ্ঠাটি বর্ণনা করে। offsetWidth ও clientWidth-এর মতো DOM পরিমাপ এবং offsetX-এর মতো পয়েন্টার ইভেন্ট কোঅর্ডিনেট CSS পিক্সেলে থাকে।

বিভিন্ন API প্যারামিটার দ্বারা ব্যবহৃত ইউনিটের ওভারভিউ এখানে দেওয়া হল:

ভ্যালু একক
canvas.width ও canvas.height ক্যানভাস গ্রিড পিক্সেল
drawElementImage()-এর dx, dy, dwidth ও dheight সহ 2D ড্রয়িং প্যারামিটার ক্যানভাস গ্রিড পিক্সেল
width ও height, canvas.getElementImageDefaultSize()-এর মাধ্যমে ফেরত দিয়েছেন ক্যানভাস গ্রিড পিক্সেল
সোর্স আয়তক্ষেত্র: drawElementImage() এবং texElementSubImage2D()-এর sx, sy, swidth এবং sheight; drawElementImageToTexture()-এর sourceX, sourceY, sourceWidth এবং sourceHeight CSS পিক্সেল
টেক্সচারের সাইজ: texElementSubImage2D()-এর মধ্যে width এবং height বিকল্প; drawElementImageToTexture()-এর মধ্যে size টেক্সচার পিক্সেল। ডিফল্ট হিসেবে, ক্যানভাস গ্রিড পিক্সেলে এলিমেন্টের সাইজ, রাউন্ড আপ করা হয়েছে
এলিমেন্ট জিওমেট্রি: getElementTransform() এবং updateElementGeometry()-এর canvasTransform ও canvasClip বিকল্প CSS পিক্সেল
DOM পরিমাপ, যেমন offsetWidth, clientWidth, getBoundingClientRect(), পয়েন্টার ইভেন্ট কোঅর্ডিনেট এবং রিসাইজ অবজারভার এন্ট্রির contentBoxSize CSS পিক্সেল
devicePixelContentBoxSize-এর একটি রিসাইজ অবজারভার এন্ট্রি ডিভাইস পিক্সেল

আপনি ভুল ইউনিট ব্যবহার করলে, ক্যানভাসে আঁকা HTML কন্টেন্টের সাইজ আপনার প্রত্যাশা পূরণ নাও করতে পারে। ক্যানভাসে HTML আঁকার সময় আপনাকে CSS পিক্সেল ভ্যালু ক্যানভাস গ্রিড পিক্সেলে কনভার্ট করতে হবে। এটি করতে, ক্যানভাস গ্রিড সাইজ ও ক্যানভাস CSS সাইজের মধ্যে অনুপাত গণনা করুন: অনুভূমিকভাবে canvas.width / cssContentWidth এবং উল্লম্বভাবে canvas.height / cssContentHeight। আপনার রিসাইজ অবজারভার হল নিম্নলিখিত অনুপাত গণনা করার জন্য একটি সুবিধাজনক জায়গা:

// The number of canvas grid pixels per CSS pixel
let scaleX = 1;
let scaleY = 1;

const observer = new ResizeObserver(([entry]) => {
  const dpc = entry.devicePixelContentBoxSize;
  const cssContentWidth = entry.contentRect.width;
  const cssContentHeight = entry.contentRect.height;

  // Best practice: make the canvas grid match the physical pixel grid
  if (dpc) {
    canvas.width = dpc[0].inlineSize;
    canvas.height = dpc[0].blockSize;
  } else {
    canvas.width = Math.round(cssContentWidth * window.devicePixelRatio);
    canvas.height = Math.round(cssContentHeight * window.devicePixelRatio);
  }

  scaleX = canvas.width / cssContentWidth;
  scaleY = canvas.height / cssContentHeight;
});

const supportsDevicePixelContentBox =
  typeof ResizeObserverEntry !== 'undefined' &&
  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};
observer.observe(canvas, options);

তারপর, ড্রয়িং পদ্ধতিতে পাস করার আগে CSS পিক্সেল ভ্যালুকে scaleX এবং scaleY দিয়ে গুণ করুন। যেমন, ক্যানভাসে আঁকা যায় এমন এলিমেন্টকে কেন্দ্রে রাখতে:

const ctx = canvas.getContext('2d');
const card = document.getElementById('card');

canvas.onpaint = () => {
  ctx.reset();

  // offsetWidth and offsetHeight are in CSS pixels.
  // Convert them to canvas grid coordinates.
  const width = card.offsetWidth * scaleX;
  const height = card.offsetHeight * scaleY;
  const x = (canvas.width - width) / 2;
  const y = (canvas.height - height) / 2;

  // drawElementImage() takes canvas grid coordinates.
  ctx.drawElementImage(card, x, y);
};

শেষে, যেহেতু drawElementImage() এলিমেন্টের বর্ডার বক্স আঁকে, তাই কার্ডের width এবং height-এ প্যাডিং এবং বর্ডার অন্তর্ভুক্ত করতে box-sizing: border-box ব্যবহার করুন, যাতে কার্ডটি আপনার সেট করা সাইজে আঁকা হয়:

#card {
  /* Make width and height include padding and border */
  box-sizing: border-box;
  width: 200px;
  padding: 16px;
}

HTML-in-Canvas-এর অ্যাক্সেসিবিলিটি সম্পর্কে বোঝা

সাধারণত, <canvas content="drawable">-এর HTML কন্টেন্ট অ্যাক্সেসিবিলিটির জন্য সাধারণ DOM-এর মতোই দেখানো হয়, যার অর্থ হল, অ্যাক্সেসিবিলিটি ট্রি-তে এটি দেখানো হয়, যদি না aria-hidden="true" বা inert-এর মতো অ্যাট্রিবিউট ব্যবহার করে স্পষ্টভাবে লুকানো হয়। তবে, একটি ব্যতিক্রম আছে: একটি drawable এলিমেন্ট যার রেন্ডার করা জ্যামিতি এখনও প্রতিষ্ঠিত বা আপডেট করা হয়নি।

<canvas content="drawable">-এর মধ্যে থাকা drawable এলিমেন্টে রেন্ডার করা জিওমেট্রি থাকে না, যদি:

  • এটি এখনও drawElementImage()-এর সাথে রেন্ডার করা হয়নি যা ডিফল্ট হিসেবে অটোমেটিক জিওমেট্রি আপডেট করে।
  • updateElementGeometry()-এর সাথে এর জ্যামিতি স্পষ্টভাবে আপডেট করা হয়নি।

কোনও drawable এলিমেন্টে রেন্ডার করা জ্যামিতি না থাকলে, এই drawable এলিমেন্ট এবং এটির drawable সাবট্রি জ্যামিতি সংক্রান্ত তথ্য ছাড়াই অ্যাক্সেসিবিলিটি ট্রিতে রিপোর্ট করা হয়। এটি সহায়ক হয়, বিশেষত, স্ক্রিন রিডারকে সিম্যান্টিক কন্টেন্ট অ্যাক্সেস করার অনুমতি দিতে, যখন ভিজ্যুয়াল অ্যাসিস্টিভ টুল (যেমন ম্যাগনিফায়ার) আঁকা হয়নি এমন এলিমেন্ট ফিল্টার করে।

নিম্নলিখিত উদাহরণে ছবি ও ফিগার আলাদাভাবে আঁকার সময় ফিগার, ছবি ও ক্যাপশনের সিম্যান্টিক বজায় রাখা হয়েছে:

<canvas id="canvas" width="400" height="300" content="drawable">
  <figure id="figure">
    <img drawable id="image" src="..." alt="image alt text" />
    <figcaption drawable id="caption">A caption</figcaption>
  </figure>
</canvas>
<script>
  const ctx = canvas.getContext('2d');

  canvas.onpaint = () => {
    ctx.reset();
    // Note: Because `preserveElementGeometry` defaults to false, the
    // `drawElementImage` call automatically updates geometry, ensuring
    // the image and caption have geometry information for accessibility.
    ctx.drawElementImage(image, 0, 0);
    ctx.drawElementImage(caption, 0, 200);
  };
</script>

ফলব্যাক স্ট্র্যাটেজি

HTML-in-Canvas API, Chrome 157-এ শিপ করা হয়েছে এবং এটি সব ব্রাউজারে কাজ নাও করতে পারে। আপনার সমাধান যাতে সকলের জন্য মসৃণভাবে কাজ করে তা নিশ্চিত করতে, ব্রাউজার API-কে সাপোর্ট করে কিনা তা চেক করুন এবং প্রয়োজন হলে বিকল্প সমাধান ব্যবহার করুন:

export function supportsHtmlInCanvas() {
  if (typeof document === 'undefined') return false;
  const canvas = document.createElement('canvas');
  canvas.setAttribute('content', 'drawable');
  return canvas.content === 'drawable';
}

API কাজ না করলে, আপনার প্রয়োগের লক্ষ্যের উপর নির্ভর করে, আপনি নিম্নলিখিত বিকল্পগুলির মধ্যে একটি বেছে নিতে পারেন:

পরবর্তী ধাপ