HTML-in-Canvas: Technical concepts

HTML-in-Canvas API به شما امکان می‌دهد زیردرخت‌های DOM تعاملی و دردسترس را مستقیماً در زمینه‌های بوم 2D،‏ WebGL، یا WebGPU ارائه دهید.

این راهنما مفاهیم فنی HTML-in-Canvas را که در همه زمینه‌های Canvas مشترک است شرح می‌دهد:

آماده کردن بوم برای پرداز HTML

برای ارائه عناصر HTML در <canvas>، دو مشخصه زیر را پیکربندی کنید:

  • تنظیم content="drawable" در <canvas>: این کار باعث می‌شود عنصر بوم گره‌های 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 (Subtree B) سطح و نوشتار دکمه از زیردرخت #parentElement مستثنی شده است؛ در تماس drawElementImage(nestedChildElement, ...) جداگانه‌ای ارائه شده است
دکمه drawable در کارت drawable تودرتو شده است و هرکدام با فراخوانی drawElementImage() خودشان ترسیم شده‌اند.

تودرتو کردن عناصر قابل‌رسم به شما امکان می‌دهد روابط معنایی HTML را حفظ کنید و درعین‌حال آزادی عمل داشته باشید تا عناصر فرزند را به‌طور مستقل روی صحنه بوم خود لایه‌بندی، موقعیت‌یابی، یا پویانمایی کنید.

آشنایی با رویداد paint

HTML-in-Canvas API رویداد paint را معرفی می‌کند. هرگاه چیدمان، نوشتار، یا سبک‌های CSS در هریک از فرزندان drawable تغییر کند، مرورگر به‌طور خودکار رویداد paint را در <canvas> ارسال می‌کند.

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`);
  }
});
هرگاه رندر کردن فرزند قابل‌رسم تغییر کند، مرورگر رنگ را روی بوم اعمال می‌کند.

چرخه حیات قاب

فرمان‌های طراحی Canvas که در رویداد paint اجرا می‌شوند در به‌روزرسانی رندرینگ فعلی نشان داده می‌شوند، اما اصلاحات DOM که در رویداد paint انجام می‌شوند تا فریم بعدی نشان داده نمی‌شوند:

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"> تودرتو باشد، مرورگر رویدادهای paint را به‌ترتیب معکوس درخت DOM ارسال می‌کند (فرزندان قبل‌از اجداد فعال می‌شوند). این کار تضمین می‌کند که عناصر بوم فرزند قبل‌از اینکه عناصر بوم والد آن‌ها را ترکیب کنند، رندر شوند.

فرض کنید در مثال زیر عنصر داخلی nestedElement باید ازنظر بصری تغییر کند (برای مثال، رنگ پس‌زمینه CSS آن وقتی کاربر نشانگر را روی آن نگه می‌دارد تغییر می‌کند). ابتدا، رویداد paint در بوم nestedCanvas فعال می‌شود. شنونده آن میان‌گیر فرزند را پاک می‌کند و ctx.drawElementImage(nestedElement, ...) را برای ارائه وضعیت به‌روزشده فراخوانی می‌کند. سپس رویداد paint در parentCanvas فعال می‌شود. شنونده آن 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

برای زمان‌بندی یک رویداد paint در قاب بعدی، requestPaint() را فراخوانی کنید، حتی اگر هیچ سبک یا نوشتار DOM تغییر نکرده باشد. از این ویژگی می‌توان برای رندر کردن صحنه‌های تعاملی با عناصر متحرک استفاده کرد، برای مثال:

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()، برمی‌گرداند.
  • مرزهای عنصر را به فناوری‌های کمکی، مانند ذره‌بین‌های صفحه‌نمایش، گزارش دهید.

در حالت دوبعدی، drawElementImage() به‌طور پیش‌فرض هندسه را به‌طور خودکار به‌روز می‌کند. آرگومان‌هایی که به آن ارسال می‌کنید، همراه با سبک محاسبه‌شده عنصر و تبدیل فعلی بافت، برای تعیین محل نمایش عنصر کافی هستند.

در زمینه‌های WebGL و WebGPU، مرورگر نمی‌تواند هندسه را تعیین کند. یک عنصر HTML در یک بافت کپی می‌شود و سپس برنامه سایه‌زن شما می‌تواند آن بافت را به هر روش دلخواهی روی صحنه نگاشت کند. برای اینکه به مرورگر بگویید عنصر کجا نشان داده می‌شود، تبدیلی را به updateElementGeometry() ارسال کنید.

وقتی کد دیگر updateElementGeometry() را فراخوانی نمی‌کند، مرورگر دیگر نمی‌داند عناصر HTML در کارت سه‌بعدی کجا هستند. کارت همچنان می‌چرخد، اما کلیک‌ها همچنان در جایی که قبلاً بود فرود می‌آیند.

برای به‌روزرسانی هندسه، یک تبدیل فضایی را به روش ارسال کنید. با نحوه ساختن تبدیل برای زمینه‌های مختلف آشنا شوید:

مدیریت ترتیب دسته‌بندی آزمایش ضربه

به‌طور پیش‌فرض، هربار که هندسه عنصری به‌روز می‌شود، مرورگر آن را به بالای پشته تعامل می‌آورد (آخرین ترسیم = بالاترین هدف کلیک). می‌توانید این رفتار را بااستفاده از preserveHitTestOrder کنترل کنید:

// Updates the backdrop without stealing clicks from foreground elements
canvas.updateElementGeometry(backgroundElement, {
  canvasTransform: customTransform,
  preserveHitTestOrder: true
});
گزینه رفتار کاربرد
preserveHitTestOrder: false (پیش‌فرض) عنصر را به بالای پشته hit-test منتقل می‌کند. عناصر پیش‌زمینه که باید ابتدا کلیک‌ها را دریافت کنند.
preserveHitTestOrder: true عنصر را در اولویت پشته فعلی‌اش رها می‌کند. عناصر پس‌زمینه که باید بعداز عناصر پیش‌زمینه به‌روز شوند.

بازنشانی هندسه

برای لغو ترسیم عنصر بدون برداشتن آن از DOM، با canvas.clearElementGeometry() تماس بگیرید. این کار تبدیل بوم عنصر را بازنشانی می‌کند و آن را به‌عنوان خارج از صفحه در درخت دسترس‌پذیری علامت‌گذاری می‌کند:

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

اندازه و واحدها

وقتی HTML را در بوم می‌کشید، مرورگر به‌طور هم‌زمان در سه سیستم مختصات عمل می‌کند: پیکسل‌های CSS، پیکسل‌های دستگاه، و پیکسل‌های شبکه بوم.

از پیکسل‌های CSS برای اندازه‌گیری و جای‌گذاری محتوا درطول چیدمان استفاده می‌شود. سپس مرورگر آن‌ها را با devicePixelRatio مقیاس‌بندی می‌کند تا صفحه را به پیکسل‌های فیزیکی دستگاه تبدیل کند. چون پیکسل‌های CSS واحدی منطقی هستند، موقعیت‌ها و اندازه‌ها اغلب به‌صورت کسری از پیکسل دستگاه به‌دست می‌آیند (برای مثال، ۱۰٫۵ پیکسل CSS × نسبت پیکسل دستگاه ۱٫۵ = ۱۵٫۷۵ پیکسل دستگاه). صفحه‌نمایش فقط می‌تواند پیکسل‌های کامل را رنگ کند، بنابراین مرورگر باید هر چیزی را که بین آن‌ها قرار می‌گیرد حل کند. این کار را به دو روش انجام می‌دهد:

  • چسباندن: مرزهای چارگوش، حاشیه‌ها، و جابه‌جایی‌های لایه به نزدیک‌ترین پیکسل کامل دستگاه گرد می‌شوند تا واضح باقی بمانند.
  • هموارسازی: برای نویسه‌نگاشت‌های نوشتاری، منحنی‌ها، و لبه‌های تبدیل‌شده، پوشش جزئی با پیکسل‌های مجاور ترکیب می‌شود تا همچنان هموار و در جای درست به‌نظر برسند.

<canvas> شبکه پیکسل خودش را دارد که با مشخصه‌های width و height آن تنظیم می‌شود و از پیکسل‌های CSS و پیکسل‌های دستگاه جدا است. برای نمایش آن، مرورگر نقشه بیتی بوم را روی شبکه پیکسل فیزیکی نمونه‌برداری مجدد می‌کند و پیکسل‌های بوم مجاور را برای پر کردن هر پیکسل دستگاه ترکیب می‌کند. برای آشنایی با نحوه نگاشت مرورگر بین CSS، بوم، و پیکسل‌های دستگاه، بازپرداخت Pixel-perfect با devicePixelContentBox را ببینید.

هر عنصر اچ‌تی‌ام‌ال drawable در پیکسل‌های CSS چیده شده است. ازآنجایی‌که محتوای ترسیم‌شده در بوم می‌تواند دو تبدیل را طی کند، از پیکسل‌های CSS به شبکه بوم و سپس از شبکه بوم به پیکسل‌های فیزیکی، هر مرحله‌ای که دقیقاً تراز نشود، دور دیگری از ترکیب را اضافه می‌کند و نتیجه می‌تواند تار به‌نظر برسد. برای جلوگیری از این اتفاق، شبکه بوم را با شبکه پیکسل فیزیکی مطابقت دهید. این کار دو هدف دارد:

  • پرداخت سازگار: اگر بوم ۱:۱ روی پیکسل‌های فیزیکی نگاشت شود، تبدیل پیکسل‌های CSS به پیکسل‌های بوم همان نتیجه تبدیل پیکسل‌های CSS به پیکسل‌های فیزیکی برای HTML معمولی را در مرورگر تولید می‌کند.
  • بدون ترکیب اضافی: اگر بوم به‌صورت ۱:۱ روی پیکسل‌های فیزیکی نگاشت شود، تبدیل آن به پیکسل‌های دستگاه هیچ محوشدگی‌ای اضافه نمی‌کند.

تصویر زیر تبدیل پیکسل را نشان می‌دهد که منجر به محتوای تار می‌شود. این سه شبکه باهم تراز نیستند: عنصر بوم ۳۶۰ × ۱۸۰ پیکسل CSS است و اندازه بوم ۳۰۰ × ۱۵۰ پیکسل شبکه بوم است. در نسبت پیکسل دستگاه ۱٫۵، بوم ۵۴۰ × ۲۷۰ پیکسل دستگاه را پوشش می‌دهد. یک پیکسل CSS شامل ۱٫۵ پیکسل دستگاه است و یک پیکسل بوم شامل ۱٫۲ پیکسل CSS یا ۱٫۸ پیکسل دستگاه است (پانل ۱). وقتی دایره رسم می‌شود، موقعیت و شعاع آن برحسب پیکسل‌های بوم است، نه پیکسل‌های CSS، بنابراین مرورگر آن را در نقشه بیتی بوم ۳۰۰ × ۱۵۰ رسم می‌کند و لبه را هموارسازی می‌کند (پانل ۲). سپس مرورگر برای نمایش بوم، این نقشه بیتی را روی ۵۴۰ × ۲۷۰ پیکسل دستگاه می‌کشد، بنابراین مرورگر باید درون‌یابی کند: هر پیکسل دستگاه ترکیبی وزنی از نزدیک‌ترین پیکسل‌های بوم را می‌گیرد که ازقبل حاوی لبه هموارشده است. لبه برای بار دوم ترکیب می‌شود و در چند پیکسل دستگاه پخش می‌شود، بنابراین دایره تار به‌نظر می‌رسد (پانل ۳). اگر شبکه بوم با پیکسل‌های دستگاه ۱:۱ مطابقت داشته باشد، می‌توان از این مشکل جلوگیری کرد. سپس مرورگر لبه را فقط یک‌بار، مستقیماً روی شبکه دستگاه، هموارسازی می‌کند و نقشه بیتی را بدون نمونه‌برداری مجدد نمایش می‌دهد، بنابراین لبه واضح می‌ماند (پانل ۴):

نمودار چهارپانلی که شبکه‌های پیکسلی را برای لبه خمیده مقایسه می‌کند: ۱. شبکه‌های CSS (۳۶۰ در ۱۸۰)، بوم (۳۰۰ در ۱۵۰)، و دستگاه (۵۴۰ در ۲۷۰) تراز نشده‌اند. ۲. The curve anti-aliased onto the coarse 300 by 150 canvas grid. ۳. آن نقشه بیتی روی شبکه دستگاه ۵۴۰ در ۲۷۰ درون‌یابی می‌شود، برای بار دوم ضدپلگی می‌شود، و لبه را محو می‌کند. ۴. بوم ۵۴۰ در ۲۷۰ که با شبکه دستگاه ۱ به ۱ مطابقت دارد، یک‌بار برای لبه واضح ضدنام‌گذاری شده است.
چگونه CSS، بوم، و شبکه‌های پیکسل دستگاه ترازنشده باعث ایجاد هموارسازی مضاعف می‌شوند (پانل‌های ۱ تا ۳)، درمقایسه با شبکه بوم به دستگاه ۱:۱ (پانل ۴).

برای رندر کردن واضح، شبکه بوم باید با تعداد پیکسل‌های دستگاهی که بوم پوشش می‌دهد مطابقت داشته باشد: ۵۴۰ × ۲۷۰ در مثال قبلی.

مقایسه پهلوبه‌پهلو کارت HTML با متن «سلام از DOM! این کارت HTML است که در بوم کشیده شده است.' سمت راست: روی شبکه بوم پیش‌فرض ۳۰۰ در ۱۵۰ کشیده شده است و تار به‌نظر می‌رسد. سمت راست: روی شبکه بوم ۸۰۰ در ۴۰۰ که با ResizeObserver اندازه آن تغییر کرده است کشیده شده است و واضح به‌نظر می‌رسد.
همان بوم، ۴۰۰ در ۲۰۰ پیکسل CSS، در صفحه‌نمایشی با DPR‏ ۱٫۵. سمت راست: شبکه بوم ۴۰۰ در ۲۰۰. سمت راست: با نسبت پیکسل دستگاه (DPR) تنظیم شده است، یک شبکه ۶۰۰ در ۳۰۰.

اندازه شبکه بوم را با ناظر تغییر اندازه تنظیم کنید

اندازه مناسب توری به دستگاه بستگی دارد و ممکن است با تغییر چیدمان، بزرگ‌نمایی صفحه توسط کاربر، یا انتقال پنجره به نمایشگر دیگر توسط کاربر تغییر کند. بنابراین به‌جای کدنویسی سخت اندازه شبکه، اجازه دهید مرورگر به شما بگوید که اندازه آن چقدر باید باشد.

برای همگام نگه داشتن شبکه بوم با بوم روی صفحه، همیشه اندازه آن را با 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 مشخص می‌شود. برای مثال، پارامترهای dx،‏ dy،‏ dwidth، و dheight از روش‌های طراحی drawElementImage()، یا دیگر پارامترهای میانای برنامه‌سازی کاربردی بوم، مانند fillRect() و translate().
  • پیکسل‌های CSS صفحه را توصیف می‌کنند. اندازه‌گیری‌های DOM مانند offsetWidth و clientWidth، و مختصات رویداد اشاره‌گر مانند offsetX، برحسب پیکسل‌های CSS هستند.

در اینجا نمای کلی واحدهای استفاده‌شده توسط پارامترهای مختلف API آورده شده است:

مقدار واحدها
‫canvas.width و canvas.height پیکسل‌های جدول بوم
پارامترهای طراحی دوبعدی، ازجمله dx،‏ dy،‏ dwidth، و dheight از drawElementImage() پیکسل‌های جدول بوم
‫width و height برگشت از canvas.getElementImageDefaultSize() پیکسل‌های جدول بوم
مستطیل‌های منبع: sx، sy، swidth، و sheight از drawElementImage() و texElementSubImage2D()؛ sourceX، sourceY، sourceWidth، و sourceHeight از drawElementImageToTexture() پیکسل‌های CSS
اندازه‌های بافت: گزینه‌های width و height از texElementSubImage2D()؛ size از drawElementImageToTexture() پیکسل‌های بافت. به‌طور پیش‌فرض، اندازه عنصر در پیکسل‌های شبکه بوم، گردشده به بالا
هندسه عنصر: getElementTransform()، و گزینه‌های canvasTransform و canvasClip از updateElementGeometry() پیکسل‌های 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() جعبه حاشیه عنصر را ترسیم می‌کند، از box-sizing: border-box استفاده کنید تا لایه داخلی و حاشیه را در width و height کارت بگنجانید تا کارت با اندازه‌ای که تنظیم کرده‌اید ترسیم شود:

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

آشنایی با دسترس‌پذیری HTML-in-Canvas

به‌طور کلی، محتوای HTML مربوط به <canvas content="drawable"> به همان روش DOM معمولی درمعرض دسترس‌پذیری قرار می‌گیرد، یعنی در درخت دسترس‌پذیری نشان داده می‌شود، مگراینکه صریحاً بااستفاده از مشخصه‌هایی مثل aria-hidden="true" یا inert پنهان شده باشد. بااین‌حال، یک استثنای کلیدی وجود دارد: عنصر drawable که هندسه رندرشده آن هنوز ایجاد یا به‌روزرسانی نشده است.

عنصر drawable داخل <canvas content="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';
}

اگر از میانای برنامه‌سازی کاربردی پشتیبانی نمی‌شود، بسته به اهداف پیاده‌سازی‌تان، می‌توانید یکی از جایگزین‌های زیر را انتخاب کنید:

مراحل بعدی