HTML 与 CSS
界面不必绘制在画布内。画布上的 HTML 与 CSS 可以使用浏览器文本渲染和布局、带输入和自动补全的表单、无障碍访问、CSS 动画及各种 Web 字体和表情。PlayCanvas 应用常将两者组合,例如 HTML 菜单和设置页搭配画布内 HUD。
何时使用 HTML
HTML 适合菜单、设置、表单、聊天、输入、长文本,以及需要无障碍访问的内容。画布界面适合 HUD、场景或 XR 界面,以及必须与场景同帧绘制、包含在画布捕获中或在编辑器构建的内容。详见两种方式的比较。
将 HTML 叠加到画布
将 HTML 放在与画布位置、尺寸相同的覆盖容器中:
- 引擎
- 编辑器
- React
- Web Components
// A full-window overlay, for a canvas that fills the window
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.innerHTML = `
<div class="score">Score: <span id="score">0</span></div>
<button id="pause">Pause</button>
`;
document.body.appendChild(overlay);
const style = document.createElement('style');
style.textContent = `
.overlay { position: fixed; inset: 0; pointer-events: none; font-family: sans-serif; color: white; }
.overlay .score { position: absolute; top: 16px; left: 16px; font-size: 24px; }
.overlay button { position: absolute; top: 16px; right: 16px; pointer-events: auto; }
`;
document.head.appendChild(style);
document.getElementById('pause').addEventListener('click', () => {
app.timeScale = app.timeScale === 0 ? 1 : 0;
});
上传 .html 与 .css 资源,再通过脚本添加到页面:
import { Asset, Script } from 'playcanvas';
export class HtmlOverlay extends Script {
static scriptName = 'htmlOverlay';
/**
* @attribute
* @type {Asset}
* @resource html
*/
html;
/**
* @attribute
* @type {Asset}
* @resource css
*/
css;
initialize() {
const style = document.createElement('style');
style.textContent = this.css.resource;
document.head.appendChild(style);
this.overlay = document.createElement('div');
this.overlay.className = 'overlay';
this.overlay.innerHTML = this.html.resource;
document.body.appendChild(this.overlay);
this.once('destroy', () => {
this.overlay.remove();
style.remove();
});
}
}
详见 HTML/CSS 界面教程。
将 HTML 与 <Application> 并列渲染,使它们填满同一容器:
import { useState } from 'react';
import { Application } from '@playcanvas/react';
export function Game() {
const [paused, setPaused] = useState(false);
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Application>
{/* the scene */}
</Application>
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', color: 'white' }}>
<button style={{ position: 'absolute', top: 16, right: 16, pointerEvents: 'auto' }}
onClick={() => setPaused(p => !p)}>
{paused ? 'Resume' : 'Pause'}
</button>
</div>
</div>
);
}
HTML 和场景共享普通 React 状态。HTML 位于 <Application> 外,不能调用 useApp(),因此应将共享状态提升到两者上方。
<div class="game">
<pc-app>
<!-- the scene -->
</pc-app>
<div class="overlay">
<button id="pause">Pause</button>
</div>
</div>
<style>
.game { position: relative; width: 100vw; height: 100dvh; }
.game pc-app { width: 100%; height: 100%; }
.overlay { position: absolute; inset: 0; pointer-events: none; }
.overlay button { position: absolute; top: 16px; right: 16px; pointer-events: auto; }
</style>
<script type="module">
import { whenReady } from '@playcanvas/web-components';
const { app } = await whenReady('pc-app');
document.getElementById('pause').addEventListener('click', () => {
app.timeScale = app.timeScale === 0 ? 1 : 0;
});
</script>
覆盖层使用 pointer-events: none,使空白处指针穿透到画布;控件使用 pointer-events: auto 接收输入。
隔离 HTML 与游戏输入
鼠标和键盘设备监听整个页面,因此 HTML 按钮点击也可能进入游戏,被 app.mouse.wasPressed() 和下方画布界面接收。文本框输入也会传到 app.keyboard,导致玩家输入姓名时角色同时前进。应在覆盖层停止事件传播:
// Clicks, wheel turns and key presses on the overlay's controls don't reach the game
for (const type of ['mousedown', 'mouseup', 'wheel', 'keydown', 'keyup']) {
overlay.addEventListener(type, event => event.stopPropagation());
}
覆盖层空白处仍通过 pointer-events: none 直接将事件交给画布。
定位 HTML
对齐三维点
摄像机 worldToScreen() 返回以画布左上角为原点的 CSS 像素,正是 HTML 所需坐标:
const onCanvas = new pc.Vec3();
app.on('update', () => {
camera.camera.worldToScreen(character.getPosition(), onCanvas);
// Hide the tag when the point is behind the camera
tag.style.display = onCanvas.z > 0 ? '' : 'none';
tag.style.transform = `translate(${onCanvas.x}px, ${onCanvas.y}px) translate(-50%, -100%)`;
});
这里假设标签容器覆盖画布,标签初始位于左上角。使用画布元素实现同类效果详见角色上方标签。引擎 annotations 脚本通过此方式为模型创建带标签热点:
对齐元素
canvasCorners 给出元素相对画布左上角的 CSS 像素角点,顺序为左下、右下、右上、左上。可精确将 HTML 覆盖到画布元素上,例如将文本框放到相应视觉元素上方:
function placeOver(element, html) {
const [bottomLeft, , topRight] = element.canvasCorners;
const canvasRect = app.graphicsDevice.canvas.getBoundingClientRect();
html.style.position = 'fixed';
html.style.left = `${canvasRect.left + bottomLeft.x}px`;
html.style.top = `${canvasRect.top + topRight.y}px`;
html.style.width = `${topRight.x - bottomLeft.x}px`;
html.style.height = `${bottomLeft.y - topRight.y}px`;
}
画布或元素尺寸变化时应重新调用。canvasCorners 适用于屏幕空间元素。界面文本输入教程用此方式构建完整文本框。
性能
浏览器独立于画布对 HTML 布局和绘制。每帧修改文字与样式会产生开销,尤其是宽高等触发布局的变化。跟随角色等每帧移动的 HTML 应只改 transform,避免重新布局。大面积半透明覆盖层也会增加移动设备合成成本。
无障碍访问
画布界面只是像素,屏幕阅读器无法读取,键盘用户也不能通过 Tab 访问控件。正确使用 <button>、<input> 和 <label> 等 HTML 元素时,可获得默认无障碍支持。菜单、设置等需要无障碍访问的内容宜采用 HTML,并为每个控件提供可见焦点状态。
画布中的 HTML
HTML-in-Canvas 是浏览器 API,按原文所述从 Chrome 157 起默认开启,可将 HTML 绘制到纹理用于世界空间面板等场景。其他浏览器尚不支持,需要保留回退方案。用法详见 HTML-in-Canvas:
用 PCUI 构建工具
构建编辑器和工具时,可以使用 PlayCanvas 编辑器自身采用的 HTML 组件库 PCUI,提供面板、检查器、树视图和输入控件等,支持普通 JavaScript 或 React。
另请参阅
- 用户界面:画布界面与 HTML 比较。
- 世界空间界面:角色标签与屏幕坐标。
- 安全区域:通过
env()避开刘海。 - HTML/CSS 界面:在编辑器中构建 HTML 界面。