fix(text): preserve requested font weights

This commit is contained in:
Danila Poyarkov 2026-05-17 20:40:39 +03:00
parent b46722a50b
commit 5d71e810b6
4 changed files with 57 additions and 15 deletions

View file

@ -208,8 +208,8 @@ function addStyledRuns(
),
fontSize: runFontSize,
fontStyle: {
weight: { value: 400 } as FontWeight,
slant: ck.FontSlant.Upright
weight: { value: s.fontWeight ?? node.fontWeight } as FontWeight,
slant: (s.italic ?? node.italic) ? ck.FontSlant.Italic : ck.FontSlant.Upright
},
letterSpacing: s.letterSpacing ?? (node.letterSpacing || 0),
decoration: textDecorationValue(ck, s.textDecoration ?? node.textDecoration),
@ -263,8 +263,8 @@ export function buildParagraph(
),
fontSize: baseFontSize,
fontStyle: {
weight: { value: 400 } as FontWeight,
slant: ck.FontSlant.Upright
weight: { value: node.fontWeight } as FontWeight,
slant: node.italic ? ck.FontSlant.Italic : ck.FontSlant.Upright
},
letterSpacing: node.letterSpacing || 0,
decoration: textDecorationValue(ck, node.textDecoration),

View file

@ -45,16 +45,12 @@ export function chooseLocalFontMatch<T extends LocalFontMatch>(
})
if (sameStyle) return sameStyle
const sameSlant = candidates.filter((x) => parseFontStyle(x.style).italic === requested.italic)
if (sameSlant.length > 0) {
return sameSlant.sort(
(a, b) =>
Math.abs(parseFontStyle(a.style).weight - requested.weight) -
Math.abs(parseFontStyle(b.style).weight - requested.weight)
)[0]
}
if (style) continue
if (!style && candidates.length > 0) return candidates[0]
const sameSlant = candidates.filter((x) => parseFontStyle(x.style).italic === requested.italic)
if (sameSlant.length > 0) return sameSlant[0]
if (candidates.length > 0) return candidates[0]
}
return undefined

View file

@ -11,6 +11,7 @@ import { initCanvasKit } from '#cli/headless'
import type { SkiaRenderer } from '#core/canvas/renderer'
import { renderText } from '#core/canvas/scene'
import type { SceneNode } from '#core/scene-graph'
import { buildParagraph } from '#core/canvas/text'
import { fontManager } from '#core/text/fonts'
import { expectDefined } from '#tests/helpers/assert'
@ -69,10 +70,27 @@ function textNode(overrides: Partial<SceneNode> = {}): SceneNode {
text: 'Hello 你好',
fontSize: 16,
fontFamily: 'Arial',
fontWeight: 400,
italic: false,
letterSpacing: 0,
lineHeight: null,
textAlignHorizontal: 'LEFT',
textAlignVertical: 'TOP',
textAutoResize: 'NONE',
textDecoration: 'NONE',
textDirection: 'AUTO',
styleRuns: [],
...overrides
} as SceneNode
}
async function createTextRenderer() {
const ck = await initCanvasKit()
const surface = expectDefined(ck.MakeSurface(400, 120), 'surface')
const renderer = new SkiaRendererClass(ck, surface)
return { renderer, surface }
}
describe('renderText', () => {
test('uses buildParagraph when fonts are loaded and node font is available', () => {
const r = createMockRenderer()
@ -150,6 +168,34 @@ describe('renderText', () => {
})
})
describe('paragraph font weights', () => {
test('bold Inter paragraph is wider than regular Inter', async () => {
const { renderer, surface } = await createTextRenderer()
await renderer.loadFonts()
const regular = await Bun.file(repoPath('public/Inter-Regular.ttf')).arrayBuffer()
const bold = await Bun.file(repoPath('public/Inter-Bold.ttf')).arrayBuffer()
fontManager.markLoaded('Inter', 'Regular', regular)
fontManager.markLoaded('Inter', 'Bold', bold)
const base = textNode({
text: 'World largest design',
fontFamily: 'Inter',
fontSize: 64,
width: 1000,
height: 100,
fontWeight: 400,
italic: false
})
const regularParagraph = buildParagraph(renderer, base)
const boldParagraph = buildParagraph(renderer, { ...base, fontWeight: 700 })
expect(boldParagraph.getLongestLine()).toBeGreaterThan(regularParagraph.getLongestLine())
regularParagraph.delete()
boldParagraph.delete()
surface.delete()
})
})
describe('renderText headless visual', () => {
test('detects base direction for Arabic and mixed text', () => {
expect(detectTextDirection('مرحبا')).toBe('RTL')

View file

@ -105,8 +105,8 @@ describe('chooseLocalFontMatch', () => {
expect(chooseLocalFontMatch(fonts, 'Inter', 'Regular')?.style).toBe('Regular')
})
test('does not fall back from upright requests to italic when upright faces exist', () => {
expect(chooseLocalFontMatch(fonts, 'Inter', 'Bold')?.style).toBe('Semi Bold')
test('does not substitute nearby weights for explicit style requests', () => {
expect(chooseLocalFontMatch(fonts, 'Inter', 'Bold')).toBeUndefined()
})
})