Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,6 @@ llvm-project-llvmorg-*
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.claude
.claude

*.code-workspace
Binary file added __test__/snapshots/direction-align.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __test__/snapshots/direction-letter-spacing.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions __test__/text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,110 @@ test('font-variant-caps-shorthand-vs-property-equality', async (t) => {

t.deepEqual(buffer1, buffer2, 'font shorthand small-caps should produce identical output as fontVariantCaps property')
})

test('direction-all-values', (t) => {
const { ctx } = t.context
const validValues = [
'ltr',
'rtl',
'inherit',
] as const

validValues.forEach((value) => {
ctx.direction = value
if (value === 'inherit') {
t.is(ctx.direction, 'ltr')
} else {
t.is(ctx.direction, value)
}
})
})

// MDN example: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/direction
test('direction-letter-spacing', async (t) => {
const canvas = createCanvas(500, 150)
const ctx = canvas.getContext('2d')!
GlobalFonts.registerFromPath(join(__dirname, 'fonts', 'ScienceGothic-VariableFont.ttf'), 'Science Gothic')
const x = canvas.width / 2

// 1. Fill background first
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, canvas.width, canvas.height)

// 2. Draw center line
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.strokeStyle = 'green'
ctx.stroke()

ctx.font = '48px Science Gothic'
ctx.fillStyle = 'black'
ctx.letterSpacing = '20px'

// First line: default direction (should be ltr)
ctx.fillText('Hi!', x, 50)
// Second line: rtl direction - "Hi!" should become "!Hi" visually
ctx.direction = 'rtl'
ctx.letterSpacing = '20px'
ctx.fillText('Hi!', x, 130)

await snapshotImage(t, { canvas, ctx })
})

test('direction-align', async (t) => {
const canvas = createCanvas(500, 580)
const ctx = canvas.getContext('2d')!
GlobalFonts.registerFromPath(join(__dirname, 'fonts', 'ScienceGothic-VariableFont.ttf'), 'Science Gothic');
const x = canvas.width / 2

ctx.fillStyle = 'white'
ctx.fillRect(0, 0, canvas.width, canvas.height)

// Draw center line
ctx.beginPath()
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.strokeStyle = 'green'
ctx.stroke()

ctx.font = '48px Science Gothic'
ctx.fillStyle = 'black'
ctx.letterSpacing = '3px'

// ltr align
// start = left in ltr
ctx.direction = 'ltr'
ctx.fillText('ltr start!', 0, 50)
ctx.direction = 'inherit' // inherit = ltr
ctx.textAlign = 'left'
ctx.fillText('ltr left!', 0, 100)

ctx.textAlign = 'center'
ctx.fillText('ltr center!', x, 150)

// end = right in ltr
ctx.textAlign = 'end'
ctx.fillText('ltr end!', canvas.width, 200)
ctx.textAlign = 'right'
ctx.fillText('ltr right!', canvas.width, 250)

// rtl align
// start = right in rtl
ctx.direction = 'rtl'
ctx.textAlign = 'start'
ctx.fillText('rtl start!', canvas.width, 350)
ctx.textAlign = 'right'
ctx.fillText('rtl right!', canvas.width, 400)

ctx.textAlign = 'center'
ctx.fillText('rtl center!', x, 450)

// end = left in rtl
ctx.textAlign = 'end'
ctx.fillText('rtl end!', 0, 500)
ctx.textAlign = 'left'
ctx.fillText('rtl left!', 0, 550)

await snapshotImage(t, { canvas, ctx })
})
30 changes: 21 additions & 9 deletions skia-c/skia_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ void skiac_canvas_get_line_metrics_or_draw_text(
for (auto& box : text_box) {
line_width += box.rect.width();
}

for (size_t i = 1; i <= glyphs_size - 1; ++i) {
auto char_bounds = bounds[i];
auto char_bottom = char_bounds.fBottom;
Expand Down Expand Up @@ -640,35 +641,47 @@ void skiac_canvas_get_line_metrics_or_draw_text(
auto line_center = line_width / 2.0f;
float paint_x;
float offset_x = 0.0;
// When RTL, Skia lays out text starting from the right edge of the layout
// width (MAX_LAYOUT_WIDTH). We need to compensate for this by subtracting the
// RTL offset.
float rtl_offset = (text_direction == TextDirection::kRtl)
? (MAX_LAYOUT_WIDTH - line_width)
: 0.0f;
// Skia Paragraph adds letter_spacing / 2 before the first character and after
// the last character.
// For LTR: we need to shift left by letter_spacing / 2 to compensate for the
// spacing before the first character.
float letter_spacing_offset =
(text_direction == TextDirection::kLtr) ? -letter_spacing / 2 : 0.0f;
switch ((TextAlign)align) {
case TextAlign::kLeft:
paint_x = x;
paint_x = x - rtl_offset + letter_spacing_offset;
break;
case TextAlign::kCenter:
paint_x = x - line_center;
paint_x = x - line_center - rtl_offset + letter_spacing_offset;
offset_x = line_center;
break;
case TextAlign::kRight:
paint_x = x - line_width;
paint_x = x - line_width - rtl_offset + letter_spacing_offset;
offset_x = line_width;
break;
// Unreachable
case TextAlign::kJustify:
paint_x = x;
paint_x = x - rtl_offset + letter_spacing_offset;
break;
case TextAlign::kStart:
if (text_direction == TextDirection::kLtr) {
paint_x = x;
paint_x = x + letter_spacing_offset;
} else {
paint_x = x - line_width;
paint_x = x - line_width - rtl_offset;
offset_x = line_width;
}
break;
case TextAlign::kEnd:
if (text_direction == TextDirection::kRtl) {
paint_x = x;
paint_x = x - rtl_offset;
} else {
paint_x = x - line_width;
paint_x = x - line_width + letter_spacing_offset;
offset_x = line_width;
}
break;
Expand All @@ -682,7 +695,6 @@ void skiac_canvas_get_line_metrics_or_draw_text(
CANVAS_CAST->scale(ratio, 1.0);
}
auto paint_y = y + baseline_offset;
paint_x = paint_x - letter_spacing / 2;
paragraph->paint(
CANVAS_CAST,
need_scale ? (paint_x + (1 - ratio) * offset_x) / ratio : paint_x,
Expand Down
4 changes: 2 additions & 2 deletions src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,12 +1400,12 @@ impl CanvasRenderingContext2D {
}

#[napi(getter)]
pub fn get_text_direction(&self) -> String {
pub fn get_direction(&self) -> String {
self.context.state.text_direction.as_str().to_owned()
}

#[napi(setter, return_if_invalid)]
pub fn set_text_direction(&mut self, direction: String) {
pub fn set_direction(&mut self, direction: String) {
if let Ok(d) = direction.parse() {
self.context.state.text_direction = d;
};
Expand Down