폰트 로딩과 text metrics
텍스트는 renderer보다 layout 문제가 먼저 옵니다. 폰트가 로드되기 전후의 width가 달라지면 selection bounds도 달라집니다.
font readiness와 metrics를 관리하는 코드
폰트가 준비되기 전에는 bounds를 확정하지 말고, 로드 후 text layout을 다시 계산합니다.
async function ensureFontLoaded(node) {
const descriptor = `${node.fontWeight ?? 400} ${node.fontSize}px ${node.fontFamily}`;
await document.fonts.load(descriptor, node.text || "A");
await document.fonts.ready;
}
function measureTextLines(ctx, node) {
ctx.font = `${node.fontWeight ?? 400} ${node.fontSize}px ${node.fontFamily}`;
return node.lines.map((line) => {
const metrics = ctx.measureText(line);
return {
text: line,
width: metrics.width,
ascent: metrics.actualBoundingBoxAscent,
descent: metrics.actualBoundingBoxDescent
};
});
}
조심할 것
font loading
fallback font
Canvas measureText
DOM layout metrics
selection bounds
Canvas와 DOM의 text measurement가 완전히 같다고 가정하면 줄바꿈과 cursor 위치가 어긋날 수 있습니다.
font load 후 bounds를 다시 계산한다
async function updateTextLayout(editor, nodeId) {
const node = editor.getNode(nodeId);
await ensureFontLoaded(node);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const lines = measureTextLines(ctx, node);
editor.dispatch({
type: "updateMeasuredText",
id: nodeId,
metrics: lines,
bounds: linesToBounds(lines, node.lineHeight)
});
}
폰트 로딩은 renderer의 세부 구현이 아니라 document layout을 바꾸는 사건입니다. 그래서 load 완료 후 selection bounds와 text texture를 모두 dirty 처리해야 합니다.
오늘의 핵심
텍스트 노드는 렌더링 대상이면서 layout 대상입니다. font readiness를 editor state 흐름에 포함해야 합니다.