Skip to content

Commit 32c05a1

Browse files
Avoid sub-pixel rendering in sequence display (GMOD#739)
1 parent e1dd393 commit 32c05a1

File tree

2 files changed

+34
-14
lines changed

2 files changed

+34
-14
lines changed

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/glyphs/util.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,20 @@ export function getLeftPx(
1717
featureLeftBpDistanceFromBlockLeftBp / bpPerPx
1818
return blockLeftPx + featureLeftPxDistanceFromBlockLeftPx
1919
}
20+
21+
/**
22+
* Perform a canvas strokeRect, but have the stroke be contained within the
23+
* given rect instead of centered on it.
24+
*/
25+
export function strokeRectInner(
26+
ctx: CanvasRenderingContext2D,
27+
left: number,
28+
top: number,
29+
width: number,
30+
height: number,
31+
color: string,
32+
) {
33+
ctx.strokeStyle = color
34+
ctx.lineWidth = 1
35+
ctx.strokeRect(left + 0.5, top + 0.5, width - 1, height - 1)
36+
}

packages/jbrowse-plugin-apollo/src/LinearApolloReferenceSequenceDisplay/drawSequenceTrack.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { defaultCodonTable, getFrame, revcom } from '@jbrowse/core/util'
22
import { type BlockSet } from '@jbrowse/core/util/blockTypes'
33
import { type Theme } from '@mui/material'
44

5+
import { strokeRectInner } from '../LinearApolloDisplay/glyphs/util'
56
import { type ApolloSessionModel } from '../session'
67
import { codonColorCode, colorCode } from '../util/displayUtils'
78

@@ -16,7 +17,7 @@ function drawLetter(
1617
seqTrackctx.fillStyle = '#000'
1718
seqTrackctx.font = `${fontSize}px`
1819
const textWidth = seqTrackctx.measureText(letter).width
19-
const textX = left + (width - textWidth) / 2
20+
const textX = Math.round(left + (width - textWidth) / 2)
2021
seqTrackctx.fillText(letter, textX, top + 10)
2122
}
2223

@@ -39,19 +40,19 @@ function drawTranslationFrameBackgrounds(
3940
const top = idx * sequenceRowHeight
4041
const { offsetPx } = dynamicBlocks
4142
const left = Math.max(0, -offsetPx)
42-
const width = dynamicBlocks.totalWidthPx
43+
const width = Math.round(dynamicBlocks.totalWidthPx)
4344
ctx.fillStyle = highContrast ? theme.palette.background.default : frameColor
4445
ctx.fillRect(left, top, width, sequenceRowHeight)
4546
if (highContrast) {
4647
// eslint-disable-next-line prefer-destructuring
47-
ctx.strokeStyle = theme.palette.grey[200]
48-
ctx.strokeRect(left, top, width, sequenceRowHeight)
48+
const strokeStyle = theme.palette.grey[200]
49+
strokeRectInner(ctx, left, top, width, sequenceRowHeight, strokeStyle)
4950
}
5051
}
5152
// allows inter-region padding lines to show through
5253
for (const block of dynamicBlocks.getBlocks()) {
5354
if (block.type === 'InterRegionPaddingBlock') {
54-
const left = block.offsetPx - dynamicBlocks.offsetPx
55+
const left = Math.round(block.offsetPx - dynamicBlocks.offsetPx)
5556
ctx.clearRect(left, 0, block.widthPx, canvas.height)
5657
}
5758
}
@@ -66,20 +67,21 @@ function drawBase(
6667
rowHeight: number,
6768
theme: Theme,
6869
) {
69-
const width = 1 / bpPerPx
70-
if (width < 1) {
70+
if (1 / bpPerPx < 1) {
7171
return
7272
}
73-
const left = leftPx + index / bpPerPx
73+
const left = Math.round(leftPx + index / bpPerPx)
74+
const nextLeft = Math.round(leftPx + (index + 1) / bpPerPx)
75+
const width = nextLeft - left
7476
const strands = [-1, 1] as const
7577
for (const strand of strands) {
7678
const top = (strand === 1 ? 3 : 4) * rowHeight
7779
const baseCode = strand === 1 ? base : revcom(base)
7880
ctx.fillStyle = colorCode(baseCode, theme)
7981
ctx.fillRect(left, top, width, rowHeight)
8082
if (1 / bpPerPx >= 12) {
81-
ctx.strokeStyle = theme.palette.text.disabled
82-
ctx.strokeRect(left, top, width, rowHeight)
83+
const strokeStyle = theme.palette.text.disabled
84+
strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle)
8385
drawLetter(ctx, left, top, width, baseCode)
8486
}
8587
}
@@ -109,7 +111,8 @@ function drawCodon(
109111
continue
110112
}
111113
const left = Math.round(leftPx + index / bpPerPx)
112-
const width = Math.round(3 / bpPerPx)
114+
const nextLeft = Math.round(leftPx + (index + 3) / bpPerPx)
115+
const width = nextLeft - left
113116
const codonCode = strand === 1 ? codon : revcom(codon)
114117
const aminoAcidCode =
115118
defaultCodonTable[codonCode as keyof typeof defaultCodonTable]
@@ -123,8 +126,8 @@ function drawCodon(
123126
ctx.fillRect(left, top, width, rowHeight)
124127
}
125128
if (1 / bpPerPx >= 4) {
126-
ctx.strokeStyle = theme.palette.text.disabled
127-
ctx.strokeRect(left, top, width, rowHeight)
129+
const strokeStyle = theme.palette.text.disabled
130+
strokeRectInner(ctx, left, top, width, rowHeight, strokeStyle)
128131
drawLetter(ctx, left, top, width, aminoAcidCode)
129132
}
130133
}
@@ -170,7 +173,7 @@ export function drawSequenceTrack(
170173
}
171174
seq = seq.toUpperCase()
172175
const baseOffsetPx = (block.start - roundedStart) / bpPerPx
173-
const seqLeftPx = Math.round(block.offsetPx - offsetPx - baseOffsetPx)
176+
const seqLeftPx = block.offsetPx - offsetPx - baseOffsetPx
174177
for (let i = 0; i < seq.length; i++) {
175178
const bp = roundedStart + i
176179
const codon = seq.slice(i, i + 3)

0 commit comments

Comments
 (0)