1

Let's put some text on a HTML5 <canvas> with

var canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.font = '14px sans-serif';
ctx.fillText('Bonjour', 10, 10);

When zooming the canvas on text, one can see pixelation.

Is there a way of zooming on a canvas without having pixelation on text ?

Basj
  • 41,386
  • 99
  • 383
  • 673
  • How are you zooming the canvas? Are you talking about the build-in zoom-feature of the web browser (Ctrl+mousewheel)? Or did you program your own zooming feature? When you did the latter, how did you implement it? Are you drawing at a larger scale or are you resizing the canvas with CSS? – Philipp Jan 14 '14 at 20:24
  • I tried with CTRL+moueswheel, is there a cleaner way of implementing canvas zooming ? @Philipp – Basj Jan 14 '14 at 20:35
  • To reduce the pixilation you will have to listen for resizing events. When resizing you must scale the ctx.font (up/down) and redraw the canvas. – markE Jan 14 '14 at 20:47
  • Do you mean @markE that I must scale the `ctx.font` of each textbox ? How to do this ? Should I keep the list of textboxes in an array, and then do a loop on all these textboxes and manually change their font size ? – Basj Jan 14 '14 at 20:57
  • Yes, var texts=[]; texts.push({text:"Hello", baseFontSize:14, x:10, y:10, fontFace:"sans-serif", currentScaleFactor:1.00}) On resize you change currentScaleFactor by the scaling factor you determine using the mousewheel delta (you must decide how much you want to scale for each mousewheel delta). Then redraw each text in the array using the font size = baseFontSize*currentScaleFactor. Cross-browser mousewheel events are non-standard, so consider using a mousewheel lib like this for jquery: https://github.com/brandonaaron/jquery-mousewheel – markE Jan 14 '14 at 21:20

2 Answers2

1

When you fillText on the canvas, it stops being letters and starts being a letter-shaped collection of pixels. When you zoom in on it, the pixels become bigger. That's how a canvas works.

When you want the text to scale as a vector-based font and not as pixels, don't draw them on the canvas. You could create <span> HTML elements instead and place them on top of the canvas using CSS positioning. That way the rendering engine will render the fonts in a higher resolution when you zoom in and they will stay sharp. But anything you draw on the canvas will zoom accordingly.

Alternatively, you could override the browsers zoom feature and create your own zooming algorithm, but this will be some work.

When the user zooms in or out of the window, the window.onresize event handler is triggered. You can use this trigger to adjust the width and the height of the canvas css styling accordingly (not the properties of the canvas. That's the internal rendering resolution. Change the width and height attributes of the style which is the resolution it is scaled to on the website).

Now you effectively disabled the users web browser from resizing the canvas, and also have a place where you can react on the scaling input events. You can use this to adjust the context.scale of your canvas to change the size of everything you draw, including fonts.

Here is an example:

<!DOCTYPE html>
<html>
<head>

    <script type="application/javascript">

        "use strict"

        var canvas;
        var context;

        function redraw() {
            // clears the canvas and draws a text label
            context.clearRect(0, 0, context.canvas.width, context.canvas.height);
            context.font = "60pt sans-serif";
            context.fillText("Hello World!", 100, 100);
        }

        function adjustSize() {
            var width = window.innerWidth;
            var height = window.innerHeight;

            // resize the canvas to fill the whole screen
            var style = canvas.style;
            style.width = width + "px";
            style.height = height + "px";

            // backup the old current scaling factor
            context.save();
            // change the scaling according to the new zoom factor
            context.scale(1000 / width, 1000 / height);
            // redraw the canvas
            redraw();
            // restore the original scaling (important because multiple calls to scale are relative to the current scale factor)
            context.restore();
        }

        window.onload = function() {
            canvas = document.getElementById("myCanvas");
            context = canvas.getContext("2d");
            adjustSize();
        }

        window.onresize = adjustSize;
    </script>
</head>

<body>

<canvas id ="myCanvas" width = 1000 height = 1000 ></canvas>

</body>

</html>
Philipp
  • 67,764
  • 9
  • 118
  • 153
  • I tried with before, but the problem is : when I have a big number of textboxes, scrolling in the canvas (with CLICK+DRAG) is very slow... So I thought it would be better with ... – Basj Jan 14 '14 at 20:55
  • Would you have an example @Philipp with `canvas`'s `scale`, I couldn't make it work... ? – Basj Jan 14 '14 at 20:56
  • Resizing canvas with CSS style is not recommended because the pixels will "stretch" rather than having pixels added to the canvas element. The result is distorted canvas drawings. Instead do canvas.width=width and canvas.height=height. – markE Jan 14 '14 at 23:55
  • @markE Yes, the stretching effect is *exactly the reason* why I said to use CSS in this case. The browsers zoom function *stretches* it in the one direction and changing the CSS then *stretches* it back to the original resolution. Usually you are right when you say that you should use canvas.width and canvas.height to resize a canvas, but in this special case the CSS style is the right choice. – Philipp Jan 15 '14 at 09:24
  • Ahh, I see now. Nice effect! +1 – markE Jan 15 '14 at 18:24
1

If you only need to scale text you can simply scale the font size.

A couple of notes on that however: fonts, or typefaces, are not just straight forward to scale meaning you will not get a smooth progress. This is because fonts are often optimized for certain sizes so the sizes in between so to speak are a result of the previous and next size. This can make the font look like it's moving around a little when scaled up and is normal and expected.

The approach here uses a simply size scale. If you need an absolute smooth scale for animation purposes you will have to use a very different technique.

The simple way is:

ctx.font = (fontSize * scale).toFixed(0) + 'px sans-serif';

An online demo here.

For animation purposes you would need to do the following:

  • Render a bigger size to an off-screen canvas which is then used to draw the different sizes
  • When the difference is too big and you get problems with interpolation you will have to render several of these cached text images at key sizes so you can switch between them when scaling factor exceeds a certain threshold.

In this demo you can see that at small sizes the pixels gets a bit "clumpy" but otherwise is much smoother than a pure text approach.

This is because the browser uses bi-linear interpolation rather than bi-cubic with canvas (this may or may not change in the future) so it's not able to interpolate properly when the difference gets to big (see below for solution with this issue).

The opposite happens at big sizes as the text gets blurry also due to interpolation.

This is where we would have to switch to a smaller (or bigger) cached version which we then scale within a certain range before we again switch.

The demo is simplified to show only a single cached version. You can see halfway through that this works fine. The principle would be in a full solution (sizes being just examples):

(Update Here is a demo of a switched image during scale).

-- Cached image (100px)
   -- Draw cached image above scaled based on zoom between 51-100 pixels

-- Cached image (50px) generated from 100px version / 2
   -- Draw cached image above scaled based on zoom between 26-50 pixels

-- Cached image (25px) generated from 50px version / 2
   -- Draw cached image above scaled based on zoom between 1-25 pixels

Then use a "sweet spot" (which you find by experiment a little) to toggle between the cached versions before drawing them to screen.

var ctx = canvas.getContext('2d'),
    scale = 1,             /// initial scale
    initialFactor = 6,     /// fixed reduction scale of cached image
    sweetSpot = 1,         /// threshold to switch the cached images

    /// create two off-screen canvases
    ocanvas = document.createElement('canvas'),
    octx = ocanvas.getContext('2d'),
    ocanvas2 = document.createElement('canvas'),
    octx2 = ocanvas2.getContext('2d');

ocanvas.width = 800;
ocanvas.height = 150;
ocanvas2.width = 400;  /// 50% here, but maybe 75% in your case
ocanvas2.height = 75;  /// experiment to find ideal size..

/// draw a big version of text to first off-screen canvas
octx.textBaseline = 'top';
octx.font = '140px sans-serif';
octx.fillText('Cached text on canvas', 10, 10);

/// draw a reduced version of that to second (50%)
octx2.drawImage(ocanvas, 0, 0, 400, 75);

Now we only need to check the sweet spot value to find out when to switch between these versions:

function draw() {

    /// calc dimensions
    var w = ocanvas.width / initialFactor * scale,
        h = ocanvas.height / initialFactor * scale;

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    if (scale >= sweetSpot) {
        ctx.drawImage(ocanvas, 10, 10, w, h);   /// use cached image 1

    } else {
        ctx.drawImage(ocanvas2, 10, 10, w, h);  /// use cached image 2
    }
}

So why not just draw the second cached image with a font? You can do that but then you are back to the issue with fonts being optimized for certain sizes and it would generate a small jump when scaling. If you can live with that then use this as it will provide a little better quality (specially at small sizes). If you need smooth animation you will have to reduce a larger cached version in order to keep the size 100% proportional.

You can see this answer on how to get a large image resized without interpolation problems.

Hope this helps.

Community
  • 1
  • 1