diff --git a/packages/core/src/canvas/text.ts b/packages/core/src/canvas/text.ts index b7f020b70..b272be5a5 100644 --- a/packages/core/src/canvas/text.ts +++ b/packages/core/src/canvas/text.ts @@ -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), diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 8638dd059..9ba767c5a 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -45,16 +45,12 @@ export function chooseLocalFontMatch( }) 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 diff --git a/tests/engine/render/canvas/text.test.ts b/tests/engine/render/canvas/text.test.ts index ec795bec0..2293767dd 100644 --- a/tests/engine/render/canvas/text.test.ts +++ b/tests/engine/render/canvas/text.test.ts @@ -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 { 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') diff --git a/tests/engine/text/fonts/loading.test.ts b/tests/engine/text/fonts/loading.test.ts index f53ddacad..65c359d6a 100644 --- a/tests/engine/text/fonts/loading.test.ts +++ b/tests/engine/text/fonts/loading.test.ts @@ -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() }) })