I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts. Follow me if you're interested in what I've learned along the way about building web apps with React, Tailwind CSS, Firebase, Apollo/GraphQL, three.js and TypeScript.
Today's episode is about one particular problem I've encountered and how I've fixed it:
The Problem
In the previous episodes, I've made a mind map using the 3D graphics library three.js (part I, part II, part III, part IV).
Here's the code of the previous version that has the problem:
Now, if you take a look at this with a regular monitor with, for example, full HD resolution (1920x1080), this will look like this:
Looks OK, however, when I look at it on my iPhone, it looks like this:
Ugh, you can hardly decipher the label texts!
The Cause
My phone has more pixels per inch (PPI) than my monitor. Apple calls this a “Retina” screen, but Android phones and tablets have this, as well.
My mind map is rendered on an HTML canvas element.
While the high PPI rate on my phone makes sure that the regular text on web pages is rendered more sharply (less pixelated), with HTML canvases, the effect is the opposite – the browser renders the content on the canvas as if the screen were regular, low PPI. Actually, even worse, it seems to additionally blur the content. It does not matter which browser, my mind map looks equally bad on Chrome and Safari.
The Solution
I've found the solution on this page:
The trick is as follows:
- Detect if your canvas is being rendered on a device with high PPI
- If the device has high PPI, increase the canvas size
- To make sure that the bigger canvas looks to be the same size, use CSS styling to make it smaller
The Code
I've come up with this function that creates a canvas suitable to each device:
function createCanvas(width, height, set2dTransform = true) {
const ratio = Math.ceil(window.devicePixelRatio);
const canvas = document.createElement('canvas');
canvas.width = width * ratio;
canvas.height = height * ratio;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
if (set2dTransform) {
canvas.getContext('2d').setTransform(ratio, 0, 0, ratio, 0, 0);
}
return canvas;
}
My createCanvas
function accepts three arguments:
-
width
,height
: the dimensions of the canvas, the way it looks on the screen -
set2dTransform
: this toggles a specific line of code that uses setTransform to make sure 2D rendering on the canvas works correctly –true
by default
Why the set2DTransform
argument? I've discovered that the 3D graphics library three.js, which I'm using, throws an error if you set 2D transform, that's why I made it optional.
The function calls window.devicePixelRatio to determine if we have a regular or high PPI screen. On my monitor, the value is 1
, on my iPhone, it's 2
.
Some devices have fractional device pixel ratios, which can cause rendering issues, so I'm using Math.ceil to round up to the next integer.
We then set the width and height of the canvas by multiplying the input width and height with the device pixel ratio. Given width 800 and height 600, on my monitor, the canvas dimensions will be 800x600, on my phone, they will be 1600x1200.
We then set the width and height at which the canvas appears on the screen using style.width
and style.height
(in my example, this will be 800x600, regardless of device pixel ratio).
The result
Here's my Retina-compatible mind map:
Screenshot from my phone:
👍🏻 Looks great!
Acknowledgements
❤️ Thanks to Reddit user SydBal for pointing out the problem (see discussion)!
❤️ Thanks to StackOverflow user mynameisko for their post on SO that is the base for my solution!
To Be Continued…
I'm planning to turn my mind map into a social media network and collaboration tool and will continue to blog about my progress in follow-up articles. Stay tuned!
Top comments (0)