محتوای معمولی <canvas> درمعرض ویژگیهای مرورگر مانند صفحهخوانها، نمایهسازی، ابزارهای ترجمه، ابزارهای کمکی دسترسپذیری، یافتن در صفحه، چاپ، و غیره قرار نمیگیرد. با HTML in canvas، میتوانید DOM واقعی را مستقیماً در عنصر بوم ارائه دهید. افزودن مشخصه content="drawable" به عنصر اچتیامال <canvas>، عناصر اچتیامال فرزند آن را طرحبندی میکند و مشخصه drawable عناصری را که میتوانید در بافت پرداز بوم پرداز کنید علامتگذاری میکند. میتوانید از آن برای سبکبندی و چیدمان نوشتار در بوم، نمایش محتوای بوم در ویژگیهای مرورگر (مانند دسترسپذیری، ترجمه، یا یافتن در صفحه)، و اعمال جلوههای دوبعدی و سهبعدی در HTML استفاده کنید.
نحوه پیادهسازی
بررسی کنید که آیا HTML-in-Canvas در مرورگر پشتیبانی میشود یا نه:
if ('content' in HTMLCanvasElement.prototype) { // Use HTML in Canvas API } else { // Use fallback strategy }مشخصه
content="drawable"را به عنصر HTML<canvas>اضافه کنید.محتوای HTML خود را داخل عنصر
<canvas>قرار دهید و ویژگیdrawableرا به هر عنصری که ترسیم میکنید اضافه کنید. عنصری باdrawableزیردرخت خود را ضبط میکند، بهجز نسلهای تودرتو که آنها همdrawableهستند.<canvas id="canvas" content="drawable"> <div id="html-content" drawable></div> </canvas>برای جلوگیری از تار شدن، شبکه بوم نقاشیتان را با ضریب مقیاس دستگاه مطابقت دهید:
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);محتوای HTML را در بوم داخل کنترلکننده رویداد
canvas.onpaintرندر کنید. عنصر فقط پساز اینکه بوم آن را رنگآمیزی کرد میتواند ترسیم شود: برای درخواست رویدادpaint،canvas.requestPaint()را فراخوانی کنید.
- در بافت دوبعدی، از روش
drawElementImageاستفاده کنید. این ویژگی آخرین عکس آنی از عنصرdrawableو زیردرخت قابلترسیم آن را، از کادر حاشیه عنصر شروع میکند، قبلاز اینکه CSS تبدیل شود:
canvas.onpaint = () => {
ctx.reset();
// Draw the form element at x:0, y:0
ctx.drawElementImage(form_element, 0, 0);
};
- در زمینههای WebGL و WebGPU، عنصر را در یک بافت کپی میکنید. اندازه بافت را از
canvas.captureElementImage(element)تغییر دهید. widthوheightآن در پیکسلهای شبکه بوم (ذخیرهسازی پشتیبان) هستند، بنابراین آنها را باMath.ceil()گرد کنید.captureElementImage()اگر عنصر هنوز سابقه رنگآمیزی نداشته باشد، خطا میدهد: باrequestPaint()تماس بگیرید و در رویدادpaintبعدی دوباره امتحان کنید.
// Returns the element's size in canvas grid pixels, or null if the element
// has no paint record yet.
function getElementImageSize(canvas, element) {
let elementImage;
try {
elementImage = canvas.captureElementImage(element);
} catch (err) {
canvas.requestPaint(); // No paint record yet: retry on the next paint event.
return null;
}
const width = Math.ceil(elementImage.width);
const height = Math.ceil(elementImage.height);
elementImage.close();
return width > 0 && height > 0 ? { width, height } : null;
}
- در بافت WebGL، ابتدا بااستفاده از روش
texImage2D، پشتیبان بافت را ازپیش اختصاص دهید. قطعههای این راهنما از بافت WebGL 2 (canvas.getContext("webgl2")) استفاده میکنند. فضای ذخیرهسازی بافت را با فراخوانیtexImage2D()باnullبهجای دادههای پیکسل اختصاص دهید، که حافظه GPU را با اندازه و قالب دادهشده بدون بارگذاری هیچ پیکسلی رزرو میکند. این قوانین را دنبال کنید:- بافت را از
captureElementImage()اندازه کنید، همانطور که در بالا نشان داده شده است. - سطح ۰ را فقط در اولین بارگذاری یا زمانی که اندازه تغییر میکند اختصاص دهید، زیرا تخصیص مجدد بافت را پاک میکند. استفاده از فضای ذخیرهسازی تغییرپذیر: اندازه فضای ذخیرهسازی تغییرناپذیر
texStorage2D()را نمیتوان هنگام تغییر اندازه عنصر تغییر داد.
- بافت را از
// Binds the texture, and allocates its storage on the first upload, and when
// the element size changes. `state` stores the allocated size.
function allocateElementTexture(gl, texture, state, width, height) {
gl.bindTexture(gl.TEXTURE_2D, texture);
if (state.width === width && state.height === height) return;
gl.texImage2D(
gl.TEXTURE_2D,
0, // Mipmap level
gl.RGBA8, // Internal format: 8 bits per RGBA channel
width, // Size in canvas grid pixels
height,
0, // Border: must be 0
gl.RGBA, // Format and type that match RGBA8
gl.UNSIGNED_BYTE,
null // null reserves GPU memory without uploading pixels
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
state.width = width;
state.height = height;
}
const texture = gl.createTexture();
const textureState = { width: 0, height: 0 };
- سپس، در بافت WebGL، محتوای HTML را بااستفاده از روش
texElementSubImage2Dدر بافت رندر کنید. این قوانین را دنبال کنید:{ width, height }را در پیکربندی رد کنید. این گزینه باعث میشود که کپی، بافت اختصاصدادهشده را بدون توجه به نحوه مقیاسبندی بوم پر کند.- اگر اندازه تغییر کرد، بافت را دوباره اختصاص دهید، همانطور که در بالا نشان داده شده است.
// Uploads the element into the texture. Returns false if the element has no
// paint record yet. In that case, it's uploaded on the next paint event.
function uploadElementWebGL(gl, canvas, texture, element, state) {
const size = getElementImageSize(canvas, element);
if (!size) return false;
const { width, height } = size;
// Bind the texture, and reallocate it if the size changed (see above).
allocateElementTexture(gl, texture, state, width, height);
gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, element, { width, height });
return true;
}
canvas.onpaint = () => {
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
};
- در بافت WebGPU، از روش
drawElementImageToTextureاستفاده کنید. این قوانین را دنبال کنید:- بافت را بااستفاده از
COPY_DSTوRENDER_ATTACHMENTایجاد کنید وTEXTURE_BINDINGرا اضافه کنید تا در سایهزنها نمونهبرداری شود. استفاده نادرست بهعنوان خطای اعتبارسنجی WebGPU گزارش میشود، نه بهعنوان استثنا. - اندازه بافت را بهعنوان
sizeارسال کنید. بدونsize، عنصر بزرگتر به بافت برش داده میشود. باsize، عنصر برای پر کردن آن مقیاسبندی میشود. - اندازه بافتهای WebGPU را نمیتوان تغییر داد. وقتی اندازه عنصر تغییر میکند، بافت را دوباره ایجاد کنید و گروههای پیوند را که به آن ارجاع میدهند بازسازی کنید.
- بافت را بااستفاده از
// WebGPU textures can't be resized. Create the texture once, and recreate it
// when the element size changes.
function ensureElementTexture(device, state, width, height) {
if (state.texture?.width === width && state.texture?.height === height) {
return state.texture;
}
state.texture?.destroy();
state.texture = device.createTexture({
size: { width, height },
format: "rgba8unorm",
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
return state.texture; // Recreate the bind groups that used the previous texture.
}
const textureState = { texture: null };
canvas.onpaint = () => {
const size = getElementImageSize(canvas, uiElement);
if (!size) return;
const previousTexture = textureState.texture;
const texture = ensureElementTexture(device, textureState, size.width, size.height);
if (texture !== previousTexture) {
// New texture: create, or rebuild, the bind groups that reference it.
}
try {
device.queue.drawElementImageToTexture({ source: uiElement }, { texture, size });
} catch (err) {
console.error('drawElementImageToTexture copy failed:', err);
}
};
هنگام استفاده از حلقه requestAnimationFrame برای رندر کردن صحنه، canvas.requestPaint() را در حلقه فراخوانی کنید تا مطمئن شوید محتوای HTML در بوم رندر میشود. event.changedElements عناصری را که پردازش آنها تغییر کرده است فهرست میکند. مطمئن شوید که فقط درصورت بهروزرسانی عناصر اچتیامال فرزند، عنصر را دوباره ترسیم کنید یا بافت آن را دوباره بارگذاری کنید:
function render() {
// Request to update the canvas
canvas.requestPaint();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
canvas.onpaint = (event) => {
if (event.changedElements && event.changedElements.length > 0) {
// Redraw the element, or update its texture, as shown above
}
// In WebGL and WebGPU, render the scene, and sync the element geometry as shown in step 6
};
- هندسه عنصر را همگامسازی کنید.
مرورگر باید بداند عنصر اچتیامال کجا ترسیم شده است تا موقعیت DOM آن با پیکسلهای ترسیمشدهاش مطابقت داشته باشد. آزمایش ضربه، برای مثال، برای کلیکها و انتخاب نوشتار، و دسترسپذیری از این هندسه استفاده میکند: عنصر drawable که هندسه آن بهروز نشده است همچنان در درخت دسترسپذیری نمایش داده میشود، اما بدون اطلاعات هندسه. بوم فقط عناصری را آزمایش میکند که با روش drawElementImage() دوبعدی ترسیم شدهاند یا با updateElementGeometry() ثبت شدهاند.
برای مورد بافت دوبعدی، مرورگر عنصر را بهطور خودکار همگامسازی میکند.
drawElementImage()مرزهای آزمایش برخورد DOM و حلقههای تمرکز صفحهخوان را با مختصات ترسیمشده تراز میکند، بنابراین نیازی به تنظیمstyle.transformندارید. برای مدیریت دستی هندسه، مثلاً وقتی یک عنصر را بیشاز یک بار میکشید،{ preserveElementGeometry: true }را بهdrawElementImage()منتقل کنید و تبدیلها را باcanvas.updateElementGeometry()اعمال کنید.برای مورد سهبعدی با WebGL یا WebGPU، بوم نمیتواند تشخیص دهد که عنصر HTML کجا ترسیم شده است، بنابراین باید به آن بگویید. یک
DOMMatrixرا محاسبه کنید که جعبه حاشیه عنصر را، برحسب پیکسلهای CSS، به بوم، برحسب پیکسلهای CSS، نگاشت میکند. مرورگر باید بااستفاده از تبدیل نمای دید، از فضای مختصات سهبعدی به فضای مختصات CSS نگاشت کند. برای تسهیل این کار، موارد زیر را انجام دهید:- عنصر HTML را نرمالسازی کنید. اندازه عناصر HTML برحسب پیکسل است (برای مثال، عرض ۲۰۰ پیکسل). بااینحال، WebGL معمولاً اشیاء را بهعنوان «مربعهای واحد» درنظر میگیرد، برای مثال، از ۰٫۵- تا ۰٫۵. اگر نرمالسازی نکنید، دکمه ۲۰۰ پیکسلی شما ۲۰۰ برابر بزرگتر بهنظر خواهد رسید. این مرحله همچنین محور Y را برعکس میکند، زیرا در CSS، پایین مثبت است، اما در WebGL، بالا مثبت است.
- تبدیل ماتریس WebGL MVP به ماتریس DOM.
- به ناحیه نمایش بوم نگاشت کنید. این مرحله فاز «مقیاسبندی مجدد» است: این مرحله ریاضیات فضای واحد را دوباره گسترش میدهد تا با ابعاد پیکسل CSS عنصر
<canvas>شما در صفحه مطابقت داشته باشد و محور Y را برمیگرداند. - محاسبه تبدیل نهایی. ماتریسها را بهترتیب ضرب کنید: Viewport * MVP * Normalization. ترکیب آنها در یک تبدیل نهایی، «نقشهای» تولید میکند که به مرورگر میگوید دقیقاً آن عنصر HTML کجا باید قرار گیرد تا با طراحی سهبعدی همتراز شود. مرورگر تقسیم پرسپکتیو را انجام میدهد.
// Maps the element's border box (CSS pixels) to the canvas (CSS pixels), for a // quad that spans -0.5 to 0.5 in model space and is drawn with the `mvp` matrix. function computeCanvasTransform(canvas, element, mvp) { const width = element.offsetWidth; const height = element.offsetHeight; // 1. Normalize the HTML element (CSS pixels -> WebGL Model Space) const toGLModel = new DOMMatrix() // Scale pixels to 1 unit, flip Y (as in CSS it points down, and in WebGL it points up) .scale(1 / width, -1 / height, 1 / height) // Center the origin: (0,0) becomes (-width/2, -height/2) before scaling .translate(-width / 2, -height / 2); // 2. Convert WebGL MVP Matrix to DOM Matrix const mvpDOM = new DOMMatrix(Array.from(mvp)); // 3. Map to the canvas viewport, in CSS pixels const clipToCanvasViewport = new DOMMatrix() // Move center (0,0) to center of canvas .translate(canvas.clientWidth / 2, canvas.clientHeight / 2) // Scale normalized clip (-1..1) to viewport size, and flip Y back .scale(canvas.clientWidth / 2, -canvas.clientHeight / 2, canvas.clientHeight / 2); // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square) return clipToCanvasViewport.multiply(mvpDOM).multiply(toGLModel); }
سپس تبدیل را بهعنوان canvasTransform به canvas.updateElementGeometry() منتقل کنید. این کار عنصر HTML را جابهجا میکند تا مستقیماً روی پیکسلهای پردازششده آن قرار گیرد. این کار تضمین میکند که وقتی کاربر روی دکمهای کلیک میکند یا نوشتاری را انتخاب میکند، درواقع روی عنصر HTML واقعی ضربه میزند. مرورگر همچنین از تبدیل برای حدود دسترسپذیری عنصر استفاده میکند، برای مثال، برای حلقههای تمرکز صفحهخوان:
canvas.updateElementGeometry(targetHTMLElement, {
canvasTransform: computeCanvasTransform(canvas, targetHTMLElement, htmlElementMVP),
});
از این گزینهها برای کنترل آزمایش ضربه استفاده کنید:
- هر تماس
updateElementGeometry()عنصر را به بالای ترتیب آزمایش ضربه منتقل میکند. مجوز{ preserveHitTestOrder: true }در جایگاه خود باقی میماند. - برای برداشتن عنصر از آزمایش ضربه، مثلاً وقتی آن را پنهان میکنید،
canvas.clearElementGeometry(element)را فراخوانی کنید.
کد نمونه
بوم نقاشی دوبعدی
<canvas id="canvas" content="drawable" style="width: 400px; height: 200px;">
<div id="ui-element" drawable>
<p>
This text is rendered inside the canvas but is present in the DOM tree.
</p>
<input type="email" name="email" placeholder="enter your email" />
<button type="button">Submit</button>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const uiElement = document.getElementById("ui-element");
canvas.onpaint = () => {
ctx.reset();
// Draw the HTML element at x:0, y:0. The browser syncs the DOM location
// with the drawn location.
ctx.drawElementImage(uiElement, 0, 0);
};
// Handle resizing to match device pixels
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);
canvas.requestPaint();
});
const supportsDevicePixelContentBox =
typeof ResizeObserverEntry !== "undefined" &&
"devicePixelContentBoxSize" in ResizeObserverEntry.prototype;
const options = supportsDevicePixelContentBox
? { box: "device-pixel-content-box" }
: {};
observer.observe(canvas, options);
</script>
بوم WebGL
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGL UI Element</p>
<button>Action</button>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const gl = canvas.getContext("webgl2");
const uiElement = document.getElementById("ui-element");
// getElementImageSize(), allocateElementTexture(), and uploadElementWebGL()
// are defined in step 5.
// computeCanvasTransform() is defined in step 6.
// Setup WebGL texture...
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const textureState = { width: 0, height: 0 };
canvas.onpaint = () => {
// 1. Update texture with HTML content
try {
uploadElementWebGL(gl, canvas, texture, uiElement, textureState);
} catch (err) {
console.error('texElementSubImage2D copy failed:', err);
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds with 3D scene
canvas.updateElementGeometry(uiElement, {
canvasTransform: computeCanvasTransform(canvas, uiElement, htmlElementMVP),
});
};
</script>
بوم WebGPU
<canvas id="canvas" content="drawable" style="width: 400px; height: 400px;">
<div id="ui-element" drawable>
<p>WebGPU UI Element</p>
</div>
</canvas>
<script>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("webgpu");
const uiElement = document.getElementById("ui-element");
// getElementImageSize() and ensureElementTexture() are defined in step 5.
// computeCanvasTransform() is defined in step 6.
// Setup WebGPU...
// const device = ...
const textureState = { texture: null };
canvas.onpaint = () => {
// 1. Copy HTML content to a texture that matches the element size
const size = getElementImageSize(canvas, uiElement);
if (size) {
const previousTexture = textureState.texture;
const texture = ensureElementTexture(device, textureState, size.width, size.height);
if (texture !== previousTexture) {
// New texture: create, or rebuild, the bind groups that reference it.
}
try {
device.queue.drawElementImageToTexture({ source: uiElement }, { texture, size });
} catch (err) {
console.error('drawElementImageToTexture copy failed:', err);
}
}
// ... Render your 3D scene here, calculating htmlElementMVP matrix ...
// 2. Sync DOM position, hit testing, and accessibility bounds (same matrix math as WebGL)
canvas.updateElementGeometry(uiElement, {
canvasTransform: computeCanvasTransform(canvas, uiElement, htmlElementMVP),
});
};
</script>
روالهای مطلوب
- الزامی: قبلاز استفاده از HTML-in-Canvas API، پشتیبانی مرورگر از آن را بررسی کنید.
- اجباری: همیشه مشخصه
content="drawable"را به عنصر<canvas>اضافه کنید. - اجباری: مشخصه
drawableرا به هر عنصری که ترسیم میکنید، ازجمله فرزندان مستقیم بوم، اضافه کنید. - اجباری: از یک کنترلکننده رویداد
onpaintبرای رندر کردن محتوای HTML در بوم استفاده کنید. - اجباری: از روش
drawElementImageدر 2D یاdrawElementImageToTextureدر WebGPU برای رندر کردن محتوای HTML در بوم استفاده کنید. - اجباری: در WebGL، میانگیر بافت را یکبار با
texImage2D()پیشاختصاص دهید (وقتی مقداردهی اولیه میشود یا وقتی اندازه عنصر تغییر میکند). بافت را فقط زمانی که اندازه تغییر میکند بازتخصیص دهید وTEXTURE_MIN_FILTERرا رویLINEARتنظیم کنید. - اجباری: در WebGL، از روش
texElementSubImage2Dبرای رندر کردن محتوای HTML به بافتی که ازقبل باtexImage2D()اختصاص دادهاید و اندازه آن ازcaptureElementImage()است استفاده کنید. - اجباری: در WebGPU، اندازه بافت را از
captureElementImage()تعیین کنید و فقط زمانی آن را بازسازی کنید که اندازه تغییر کند. بافت را بااستفاده ازCOPY_DSTوRENDER_ATTACHMENTایجاد کنید و اندازه بافت را بهصورتsizeارسال کنید. - اجباری: هندسه عنصر HTML را با محتوای رندرشده همگامسازی کنید. در حالت دوبعدی،
drawElementImageاین کار را بهطور خودکار انجام میدهد. در WebGL و WebGPU، باcanvas.updateElementGeometry(element, { canvasTransform })تماس بگیرید. - اجباری: از
ResizeObserverبرای مشاهده اندازه صفحه و بهروزرسانی اندازه بوم برای مطابقت با پیکسلهای دستگاه استفاده کنید. - محتوای میانمبدئی را در بوم نقاشی جاسازی نکنید، زیرا پشتیبانی نمیشود.
- DO NOT initialize
ResizeObserverwithin theonpaintevent handler, as it may lead to memory leaks.
راهبردهای جایگزین
HTML-in-Canvas API درحالحاضر در همه مرورگرهای مدرن پشتیبانی نمیشود، بنابراین معمولاً به یک استراتژی جایگزین نیاز است.
بااینحال، با توجه به مزایای بهبود عملکرد این API، اگر مرورگر از HTML-in-Canvas پشتیبانی میکند، باید از آن استفاده شود.
راهبرد جایگزین به مورد استفاده بستگی دارد. برای مثال، برای محتوای HTML تعاملی در بوم، اگر HTML-in-Canvas پشتیبانی نمیشود، محتوای HTML را بااستفاده از CSS در بالای بوم قرار دهید.
شبیهسازهای مرورگر HTML-in-Canvas
از اسکریپت شبیهساز مرورگر زیر برای تقلید HTML-in-Canvas API در مرورگرهایی که از آن پشتیبانی نمیکنند استفاده کنید.
کتابخانه را نصب یا جاسازی کنید:
# Install npm install three-html-render# Embed <script src="https://cdn.jsdelivr.net/npm/three-html-render/dist/polyfill.js"></script>برای ترجمه HTML-in-Canvas، روش
installHtmlInCanvasPolyfill()را اجرا کنید.