Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui-spacetimechart: fixes blurry graduations #914

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open

Conversation

jacomyal
Copy link
Contributor

This commit fixes OpenRailAssociation/osrd#10662.

It ensures vertical and horizontal graduation lines are as thin as possible, so that they are as crisp as they can be.

@jacomyal jacomyal requested review from emersion and Yohh February 19, 2025 09:19
@jacomyal jacomyal requested a review from a team as a code owner February 19, 2025 09:19
Comment on lines 324 to 325
const centerOffset = (Math.ceil(lineWidth) % 2) / 2;
return Math.round(rawCoordinate - centerOffset) + centerOffset;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took me a bit to double-check this formula, it seems to work perfectly fine on LoDPI screens :)

I'm not super sure about HiDPI though. On HiDPI screens, the numbers will be implicitly multiplied by the scale before drawing on screen. Ideally, on a devicePixelRatio = 2 setup, a 0.5 large line at rawCoordinate 0 would be painted at coordinate 0.25, right? Also, on a HiDPI screen it's not necessary to align a 2px large line between two pixels: painting at x = 0.5 will result in a crisp image, I think?

Should we read window.devicePixelRatio here, and change our behavior based on that number? (Mainly how rounding is performed)

Or should we give up and passthrough any number which isn't an integer? Or do you see another solution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point indeed... Let me check that again :)

Copy link
Contributor Author

@jacomyal jacomyal Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I ran some tests on my 3.5 DPR Android phone, and:

  • I couldn't get pixel perfect lines (when I check my screen captures zoomed on GIMP at least)
  • They appear very acceptable on screen to me, I wouldn't have guessed it wasn't pixel perfect
  • The resolution is such that I couldn't see any difference from afar, in all the tests I ran

To give a bit more context, the solution that made the most sense to me was to update the formula as:

export function getCrispLineCoordinate(rawCoordinate: number, lineWidth: number): number {
  const centerOffset = (Math.ceil(lineWidth * devicePixelRatio) % 2) / 2;
  return Math.round(rawCoordinate - centerOffset) + centerOffset;
}

Then, for a 2px wide line, on my phone, it would be 7px wide effectively, and thus with a centerOffset of 0.5. I checked with console.log, and that's what I got. And here is how it was rendered:

image

Zoomed in, it's clearly not crisp, and it's not even symmetric. But on my phone, with the actual screen, it looks very crisp. So, at that point, I really don't know how to do better 😅 Also, it looks like that last formula breaks with lower DPIs.

I'd say we go with this for now, at least if we agree that the lines are crisp enough on high DPIs.

Copy link
Member

@emersion emersion Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've written a bunch of tests to more easily play with the code and ensure it does what I'd expect:

describe('getCrispLineCoordinate', () => {
  it.each([1, 3, 99])('should align a %ipx line on a LoDPI device', (lineWidth) => {
    const devicePixelRatio = 1;
    expect(getCrispLineCoordinate(0, lineWidth, devicePixelRatio)).toEqual(0.5);
    expect(getCrispLineCoordinate(0.1, lineWidth, devicePixelRatio)).toEqual(0.5);
    expect(getCrispLineCoordinate(0.5, lineWidth, devicePixelRatio)).toEqual(0.5);
    expect(getCrispLineCoordinate(0.9, lineWidth, devicePixelRatio)).toEqual(0.5);
    expect(getCrispLineCoordinate(1, lineWidth, devicePixelRatio)).toEqual(1.5);
    expect(getCrispLineCoordinate(42, lineWidth, devicePixelRatio)).toEqual(42.5);
    expect(getCrispLineCoordinate(-0.4, lineWidth, devicePixelRatio)).toEqual(-0.5);
    expect(getCrispLineCoordinate(-0.7, lineWidth, devicePixelRatio)).toEqual(-0.5);
  });

  it.each([2, 4, 64])('should align a 2px line on a LoDPI device', (lineWidth) => {
    const devicePixelRatio = 1;
    expect(getCrispLineCoordinate(-0.4, lineWidth, devicePixelRatio)).toEqual(0);
    expect(getCrispLineCoordinate(0, lineWidth, devicePixelRatio)).toEqual(0);
    expect(getCrispLineCoordinate(0.4, lineWidth, devicePixelRatio)).toEqual(0);
    expect(getCrispLineCoordinate(0.5, lineWidth, devicePixelRatio)).toEqual(1);
    expect(getCrispLineCoordinate(1.5, lineWidth, devicePixelRatio)).toEqual(2);
    expect(getCrispLineCoordinate(42, lineWidth, devicePixelRatio)).toEqual(42);
    expect(getCrispLineCoordinate(-0.7, lineWidth, devicePixelRatio)).toEqual(-1);
  });

  it('should align a 2px line on a HiDPI device', () => {
    const devicePixelRatio = 2;
    const lineWidth = 2;
    expect(getCrispLineCoordinate(0.5, lineWidth, devicePixelRatio)).toEqual(0.5);
  });

  it('should align a 0.5px line on a HiDPI device', () => {
    const devicePixelRatio = 2;
    const lineWidth = 0.5;
    expect(getCrispLineCoordinate(0, lineWidth, devicePixelRatio)).toEqual(0.25);
  });
});

I've simplified your code to the following:

export function getCrispLineCoordinate(rawCoordinate: number, lineWidth: number): number {
  const centerOffset = lineWidth / 2;
  return Math.round(rawCoordinate - centerOffset) + centerOffset;
}

and then I've extended it to handle HiDPI:

export function getCrispLineCoordinate(
  rawCoordinate: number,
  lineWidth: number,
  devicePixelRatio = window.devicePixelRatio || 1
): number {
  const centerOffset = lineWidth / 2;
  return (
    Math.round((rawCoordinate - centerOffset) * devicePixelRatio) / devicePixelRatio + centerOffset
  );
}

Do you think this is a version we could use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it, and it looks good to me. I pushed your modifications to the PR.

This commit fixes OpenRailAssociation/osrd#10662.

It ensures vertical and horizontal graduation lines are as thin as
possible, so that they are as crisp as they can be.

Signed-off-by: Alexis Jacomy <[email protected]>
Co-authored-by: Simon Ser <[email protected]>
Copy link
Member

@emersion emersion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

front: different resolution for space time chart and manchette
2 participants