HTML5 canvas smooth text movement animation imposs

2019-03-02 08:36发布

I'd like to have a smooth animation for simple pin with the text on it. This pin is moving slowly around the canvas and I need smooth animation. So my pin consists from the background (filled and shadowed circle) and a text on top of it.

I achieved very smooth movement for the circle itself, but not for the text!

Q: Is it possible to achieve smooth text movement in the HTML5 canvas and how?

What I tried:

Method 0: Just draw the circle, no text on it. Animation is smooth.
Problem: No problem at all, except there is no text.

Method 1: Draw text on top of the circle using canvas method fillText().
Problem: Text is jittering while moving vertically. Horizontal moving does not produce jittering.

Method 2: Draw text to the offscreen canvas, copy canvas as an image on top of the circle. Create offscreen canvas and draw the text on it with sizes twice bigger than the original and then shrink while copying to the screen canvas. This will sharpen the text.
Problem: Text is sharp, but wavy and there is some flickering appears during movement.

Method3: Draw text to the offscreen canvas, copy canvas as an image on top of the circle. Create offscreen canvas and draw the text there. Size of the canvas and the text is the same as on the screen.
Problem: Movement is smooth enough, but text is blurry, out of focus.

My JSFIDDLE: Canvas text jitter animation

My Javascript code:

var canvas = document.getElementById("canvas1");
var ctx = canvas.getContext("2d");

ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "bold 16px Helvetica";
ctx.shadowOffsetX = ctx.shadowOffsetY = 2;
ctx.shadowBlur = 6;

var bgColor="blue";
var textColor="white";
var shadowColor="rgba(0, 0, 0, 0.4)";
var radius=15;

//Draw empty plate for the pin, no text on top
//Problem: NONE, movements are smooth
function drawPinPlate(x, y)
{
  var oldShadow = ctx.shadowColor;
  ctx.shadowColor = shadowColor;
  ctx.fillStyle = bgColor;
    ctx.beginPath();
  ctx.arc(x, y, radius, 0, 2*Math.PI);
  ctx.fill();
  ctx.shadowColor = oldShadow;
}

//method 1: Draw pin with text directly. 
//Draw text using canvas direct text rendering.
//Problem: Text vertical jittering while animating movement
function drawPin1(x, y, name)
{
    drawPinPlate(x, y);
  ctx.fillStyle = textColor;
  ctx.fillText(name, x, y);
}

//method 2: Draw pin with text using offscreen image with resize
//Draw text using text pre-rendered to offscreen canvas.
//Offscreen canvas is twice large than the original and we do resize (shrink) to the original one
//Problem: Text is sharp but some flickering appears during image movement
function drawPin2(x, y, name)
{
    drawPinPlate(x, y);
  ctx.drawImage(offImage1, x - radius, y - radius, radius*2, radius*2);
}

//method 2: Draw pin with text using offscreen image
//Draw text using text pre-rendered to offscreen canvas.
//Offscreen canvas is the same size as the original.
//Problem: Text is looking fuzzy, blurry
function drawPin3(x, y, name)
{
    drawPinPlate(x, y);
  ctx.drawImage(offImage2, x - radius, y - radius);
}


var PIXEL_RATIO = (function ()
{
    var ctx = document.createElement("canvas").getContext("2d"),
        dpr = window.devicePixelRatio || 1,
        bsr = ctx.webkitBackingStorePixelRatio ||
            ctx.mozBackingStorePixelRatio ||
            ctx.msBackingStorePixelRatio ||
            ctx.oBackingStorePixelRatio ||
            ctx.backingStorePixelRatio || 1;

    return dpr / bsr;
})();

//create offscreen canvas
createHiDPICanvas = function(w, h, ratio)
{
    if (!ratio) { ratio = PIXEL_RATIO; }
    var can = document.createElement("canvas");
    can.width = w * ratio;
    can.height = h * ratio;
    can.style.width = w + "px";
    can.style.height = h + "px";
    can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
    return can;
}

//create offscreen canvas with text, size of the canvas is twice larger than the original.
function createPin1(name)
{
    var cnv = createHiDPICanvas(radius*2, radius*2, 2);
  var ctx = cnv.getContext("2d");

    ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.font = "bold 16px Helvetica";

  ctx.fillStyle = textColor;
  ctx.fillText(name, radius, radius);

  return cnv;
}

//create offscreen canvas with text. Text becomes very blurry.
function createPin2(name)
{
    var cnv = createHiDPICanvas(radius*2, radius*2, 1);
  var ctx = cnv.getContext("2d");

    ctx.textAlign = "center";
  ctx.textBaseline = "middle";
  ctx.font = "bold 16px Helvetica";

  ctx.fillStyle = textColor;
  ctx.fillText(name, radius, radius);

  return cnv;
}

var offImage1, offImage2;

offImage1 = createPin1("AB");
offImage2 = createPin2("AB");


var startTime;
var speed = 180;

//render one frame
function render(deltaTime)
{
    var x = (deltaTime / speed / 2) %100;
  var y = (deltaTime / speed) % 200;

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

  for(var i = 0; i<4; i++)
  {
    ctx.fillText(i, 20 + x + i * 50, 15 + y);
  }

  drawPinPlate(20 + x, 40 + y);
  drawPin1(70 + x, 40 + y, "AB");
  drawPin2(120 + x, 40 + y, "AB");
  drawPin3(170 + x, 40 + y, "AB");
}

//Animation loop
function animate()
{
    requestAnimationFrame(animate);
  render(Date.now() - startTime);
}

//alert("You screen pixel ratio = " + PIXEL_RATIO);

startTime = Date.now();
animate();

2条回答
戒情不戒烟
2楼-- · 2019-03-02 09:22

It's because of this:

var radius = 15;

var cnv = createHiDPICanvas(radius*2, radius*2, 2);
var ctx = cnv.getContext("2d");

When you increase the size of the canvas you're creating to createHiDPICanvas(2000,2000, ...), the movement smoothes out. Right now you're creating a very small resolution canvas (30px by 30px), and the text looks jittery because it's moving across a very small range of pixels.

Example: https://jsfiddle.net/6xr4njLm/

Longer explanation of createHiDPICanvas: How do I fix blurry text in my HTML5 canvas?

查看更多
ら.Afraid
3楼-- · 2019-03-02 09:40

You can achieve smooth movement of text, by using your third method a.k.a the offscreen canvas.
However, it is not really possible to achieve smooth text rendering on canvas.

This is because texts are usually rendered with smart smoothing algorithms, at sub-pixel level and that canvas doesn't have access to such sub-pixel level, so it leaves us with blurry texts as a best.

Of course, you could try to implement a font-rasterization algo by yourself, SO user Blindman67 provided a good explanation along with a trying here, but it depends on so much parameters (which device, UA, font etc.) that making it on a moving target is almost a no-go.

So if you need perfect text rendering, on animated content, SVG is your friend. But even in SVG, text animation looks somewhat choppy.

text{
  font: bold 16px Helvetica;
  fill: white;
  }
<svg>
  <g id="group">
  <circle fill="blue" cx="15" cy="15" r="15"/>
  <text y="20" x="15" text-anchor="middle">AB
  </text>
    <animateTransform attributeName="transform"
                          attributeType="XML"
                          type="translate"
                          from="0 0"
                          to="80 150"
                          dur="20s"
                          repeatCount="indefinite"/>
  </g>
</svg>

Otherwise, for canvas, the best you could get would be to double the size of your canvas, and scale it down with CSS after-ward, using the offscreen canvas method.

var canvas = document.getElementById("canvas1");
var ctx = canvas.getContext("2d");

var scale = devicePixelRatio *2;
canvas.width *= scale;
canvas.height *= scale;

function initTextsSprites(texts, radius, padding, bgColor, textColor, shadowColor) {
  // create an offscreen canvas which will be used as a spritesheet
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  radius *= scale;
  padding *= scale;
  
  var d = radius * 2;

  var cw = (d + (padding * 2));
  canvas.width = cw * texts.length;
  canvas.height = d * 2 + padding * 2;

  var topAlignText = 6 * scale; // just because I don't trust textBaseline
  var y;

  //  drawCircles
  ctx.fillStyle = bgColor;
  ctx.shadowOffsetX = ctx.shadowOffsetY = 2;
  ctx.shadowBlur = 6;
  ctx.shadowColor = shadowColor;
  y = (radius * 2) + padding;
  ctx.beginPath();
  texts.forEach(function(t, i) {
    var cx = cw * i + padding * 2;
    ctx.moveTo(cx + radius, y)
    ctx.arc(cx, y, radius, 0, Math.PI * 2);
  })
  ctx.fill();

  // drawBlueTexts
  ctx.textAlign = "center";
  ctx.font = "bold "+(16 * scale)+"px Helvetica";
  ctx.shadowOffsetX = ctx.shadowOffsetY = ctx.shadowBlur = 0;
  y = padding + topAlignText;
  texts.forEach(function(txt, i) {
    var cx = cw * i + padding * 2;
    ctx.fillText(i, cx, y);
  });

  //  drawWhiteTexts
  ctx.fillStyle = 'white';
  var cy = (radius * 2) + padding + topAlignText;
  texts.forEach(function(txt, i) {
    var cx = cw * i + padding * 2;
    ctx.fillText(txt, cx, cy);
  });

  return function(index, x, y, w, h) {
    if (!w) {
      w = cw;
    }
    if (!h) {
      h = canvas.height;
    }
    // return an Array that we will able to apply on drawImage
    return [canvas,
      index * cw, 0, cw, canvas.height, // source
      x, y, w, h // destination
    ];
  };
}

var texts = ['', 'AA', 'AB', 'AC'];

var getTextSprite = initTextsSprites(texts, 15, 12, "blue", "white", "rgba(0, 0, 0, 0.4)");
// just to make them move independently
var objs = texts.map(function(txt) {
  return {
    x: Math.random() * canvas.width,
    y: Math.random() * canvas.height,
    speedX: Math.random() - .5,
    speedY: Math.random() - .5,
    update: function() {
      this.x += this.speedX;
      this.y += this.speedY;
      if (this.x < 0) {
        this.speedX *= -1;
        this.x = 0;
      }
      if (this.y < 0) {
        this.speedY *= -1;
        this.y = 0;
      }
      if (this.x > canvas.width) {
        this.speedX *= -1;
        this.x = canvas.width;
      }
      if (this.y > canvas.height) {
        this.speedY *= -1;
        this.y = canvas.height;
      }
    }
  }
});

function anim() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  objs.forEach(function(o, i) {
    o.update();
    ctx.drawImage.apply(ctx, getTextSprite(i, o.x, o.y));
  })
  requestAnimationFrame(anim);
}
anim();
#canvas1 {
  border: 1px solid;
  width: 500px;
  height: 300px;
}
<canvas id="canvas1" width="500" height="300"></canvas>

查看更多
登录 后发表回答