Canvas HTML fillText letters to not animate only shadows

Viewed 210

Only want shadows to animate and keep the fillText from animating due to letters pixelating from getting ran over and over.

    var canvas = document.getElementById('canvas')
    var ctx = this.canvas.getContext('2d')
    var width = canvas.width = canvas.scrollWidth
    var height = canvas.height = canvas.scrollHeight
    var start;
    var j=0;
 var makeText = function(){  
    j+=1 

    ctx.shadowColor= 'red'; 
       ctx.shadowOffsetX = j;  //animate
    ctx.shadowOffsetY = j; //animate
    ctx.globalAlpha=0.5;
    ctx.font = "48px serif";
       ctx.fillStyle = "black";
       ctx.fillText('hey you', width/2, height / 2); //Only ran once so letters 
                                                     //don't pixelate!
   }
    
       
 function animateText(timestamp){
  var runtime = timestamp - start;
  var progress = Math.min(runtime / 1400, 1); 
 
  makeText(progress)
    
  if(progress < 1){
   requestAnimationFrame(animateText)
  }else {
   return;
  }
 }
 requestAnimationFrame(function(timestamp){
  start = timestamp;
  animateText(timestamp)
 })
    
<canvas id="canvas" width=500px height=500px></canvas>

My outcome of the process would only have shadows animate and keeping letters where they are

2 Answers

Just draw your own shadows, here is an example:

var canvas = document.getElementById('canvas')
var ctx = this.canvas.getContext('2d')
ctx.font = "68px serif";
var base = {text: 'hey you', x: 10, y: 60 }
var inc = 2;
var j = 30;

var makeText = function() {
  ctx.globalAlpha = 1;
  ctx.fillStyle = "black";
  ctx.fillText(base.text, base.x, base.y); 
}

var makeshadow = function(offset) {
  ctx.fillStyle = "red";
  for (var i = 0; i < offset; i++) {
    ctx.globalAlpha = 1/i;
    ctx.fillText(base.text, base.x + i, base.y + i);
  }
}

function animateText() {  
  ctx.clearRect(0, 0, 999, 999)
  makeshadow(j);
  makeText();  
  j += inc;
  if (j > 35 || j < 3) inc *= -1
}

setInterval(animateText, 50)
<canvas id="canvas" width=300px height=170px></canvas>


And if you add some math in the mix you can get some cool effects:

var canvas = document.getElementById('canvas')
var ctx = this.canvas.getContext('2d')
ctx.font = "68px serif";
var base = {text: '123456', x: 30, y: 80 }
var inc = 5;
var j = 0;

var makeText = function() {
  ctx.globalAlpha = 1;
  ctx.fillStyle = "black";
  ctx.fillText(base.text, base.x, base.y); 
}

var makeshadow = function(offset) {
  ctx.globalAlpha = 0.05;
  ctx.fillStyle = "red";
  for (var i = 0; i < offset; i++)
    ctx.fillText(base.text, base.x + Math.sin(i/5)*10, base.y + Math.cos(i/5)*15);
}

function animateText() {  
  ctx.clearRect(0, 0, 999, 999)
  makeshadow(j);
  makeText();  
  j += inc;
  if (j > 120 || j < 0) inc *= -1
}
setInterval(animateText, 50)
<canvas id="canvas" width=300px height=170px></canvas>

Your main issue (the text pixelisation) is due to you not clearing the canvas between every frames, and drawing again and again over the same position. semi-transparent pixels created by antialiasing mix up to more and more opaque pixels.

But in your situation, it seems that you actually want at-least the shadow to mix up like this.

To do it, one way would be to draw only once your normal text, and to be able to draw only the shadow, behind the current drawing.


Drawing only the shadow of a shape.

One trick to draw only the shadows of your shape is to draw your shape out of the visible viewPort, with shadowOffsets set to the inverse of this position.

var text = 'foo bar';
var ctx = canvas.getContext('2d');
var original_x = 20; // the position it would have been

ctx.font = '30px sans-serif';

var targetPosition = ctx.measureText(text).width + original_x + 2;
// default shadow settings
ctx.shadowColor = 'red';
ctx.shadowBlur = 3;
// just to show what happens
var x = 0;
anim();

function anim() {
  if(++x >= targetPosition) {
    x=0;
    return;
  }
  // if we weren't to show the anim, we would use 'targetPosition'
  // instead of 'x'
  ctx.shadowOffsetX = x;
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.fillText(text, -x + original_x, 30);
  requestAnimationFrame(anim);
}
// restart the anim on click
onclick = function() {
  if(x===0)anim();
};
<canvas id="canvas"></canvas>

Once we have this clear shadow, without our shape drawn on it, we can redraw it as we wish.


Drawing behind the current pixels

The "destination-over" compositing option does just that.

So if we put these together, we can draw behind the normal text, and only draw our shadow behind it at each frame, avoiding antialiasing mix-up.

(Note that we can also keep the clean shadow on an offscreen canvas for performances, since shadow is a really slow operation.)

var text = 'foo bar';
var ctx = canvas.getContext('2d');
ctx.font = '48px sans-serif';
var x = 20;
var y = 40;
var shadow = generateTextShadow(ctx, text, x, y, 'red', 5);

ctx.globalAlpha = 0.5;
ctx.fillText(text, x, y);
// from now on we'll draw behind current content
ctx.globalCompositeOperation = 'destination-over';

var shadow_pos = 0;
anim();
// in the anim, we just draw the shadow at a different offset every frame
function anim() {
  if(shadow_pos++ > 65) return;
  ctx.drawImage(shadow, shadow_pos, shadow_pos);

  requestAnimationFrame(anim);
}

// returns a canvas where only the shadow of the text provided is drawn
function generateTextShadow(original_ctx, text, x, y, color, blur, offsetX, offsetY) {
  var canvas = original_ctx.canvas.cloneNode();
  var ctx = canvas.getContext('2d');
  ctx.font = original_ctx.font;
  var targetPosition = ctx.measureText(text).width + 2;
  // default shadow settings
  ctx.shadowColor = color || 'black';
  ctx.shadowBlur = blur || 0;
  ctx.shadowOffsetX = targetPosition + x +(offsetX ||0);
  ctx.shadowOffsetY = (offsetY || 0);
  ctx.fillText(text, -targetPosition, y);
  return canvas;
}
<canvas id="canvas"></canvas>

Related