4

Sorry if the title is confusing. I've tried my best to compose it, if you are going to understand what I am asking, feel free to suggest better title in comment.

The animation I am trying to make can easily be done by video editors, but looks to me not so easy with CSS/JS: First of all, I am not talking about sliding in the image, the image is not moving at all. I want it to appear from side to side, but in a gradient transparent manner. Imagine there is a gradient opacity mask on the image, making the opacity of its left end to be 1, and that of its right end to be 0. When this mask moves from left right, it is the animation I wanna achieve.

I can split the image to several pieces, and manipulate the opacity of each one, there has to be a certain amount pieces to make the whole animation smooth and appeal.

The other way I am thinking is to use canvas, where you can manipulate the image by pixel, as this page suggests, I can do

// get the image data object
var image = ctx.getImageData(0, 0, 500, 200);
// get the image data values 
var imageData = image.data,
length = imageData.length;
// set every fourth value to 50
for(var i=3; i < length; i+=4){  
    imageData[i] = 50;
}
// after the manipulation, reset the data
image.data = imageData;
// and put the imagedata back to the canvas
ctx.putImageData(image, 0, 0);

However the page is only taking about a static image but not animation. I am wondering whether it would undermine performance if I use this approach. Also, this approach involve a lot of ugly calculations.

I think what I want to achieve is not very strange, so is there any javascript plugin to achieve this?

shenkwen
  • 3,536
  • 5
  • 45
  • 85

3 Answers3

2

The animation I am trying to make can easily be done by video editors, [...] the image is not moving at all. I want it to appear from side to side

In the video industry we call this a soft-wipe (AKA soft edged wipe) and it's not too complicated to make.

All you need is an alpha mask that you can make using a linear gradient. Then use translate properties with the context combined with xor composite mode to animate it.

What xor mode does is to invert the alpha channel based on the alpha channel drawn to it. The advantage of this is that the canvas element get transparent as well, so any background can show through. You can keep the default comp. mode as well which will make the background black instead.

The gradient is made like this (the color values does not matter with xor mode, just the alpha channel values):

var g = ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
g.addColorStop(0, "rgba(0,0,0,0)");
g.addColorStop(1, "rgba(0,0,0,1)");
ctx.fillStyle = g;

(see this answer for how to avoid "bright line" artifacts by creating a smoothed gradient).

Now create a function that draws a complete frame based on position t which is a normalized value combined with canvas width - have in mind we need the double width to work with: gradient + room for the gradient to exit -

function render(t) {
  var w = t * ctx.canvas.width;                                // width based on t
  ctx.drawImage(img, 0, 0);                                    // render bg. image
  ctx.translate(-ctx.canvas.width + w, 0);                     // translate on x-axis
  ctx.fillRect(0, 0, ctx.canvas.width * 2, ctx.canvas.height); // render gradient mask
}

Call this in an animation loop until t=2, but optionally set globalCompositeOperation to xor and we're good to go. The animation loop itself will reset transformation for us:

Demo

var ctx = c.getContext("2d"),
    img = new Image,
    t = 0, step = 0.02

// alpha mask
var g = ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
g.addColorStop(0, "rgba(0,0,0,0)");
g.addColorStop(1, "rgba(0,0,0,1)");
ctx.fillStyle = g;
ctx.globalCompositeOperation = "xor";

// load bg image
img.onload = animate;
img.src = "http://i.imgur.com/d0tZU7n.png";

function animate() {
  ctx.setTransform(1,0,0,1,0,0);                  // reset any transformations
  ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
  render(t);
  t += step;
  if (t <= 2) requestAnimationFrame(animate);     // 2 since we need double width
  else {t=0; setTimeout(animate, 2000)};          // just to repeat anim. for demo
}

function render(t) {
  var w = t * ctx.canvas.width;                   // width based on t
  ctx.drawImage(img, 0, 0);
  ctx.translate(-ctx.canvas.width + w, 0);         // translate on x-axis
  ctx.fillRect(0, 0, ctx.canvas.width*2, ctx.canvas.height);
}
body {background:url(http://i.imgur.com/OT99vSA.jpg) repeat}
<canvas width=658 height=325 id=c></canvas>
Community
  • 1
  • 1
  • 1
    I haven't got a chance to digest your answer, but from the snippet I can see this is what I want. Thanks. I may have some follow-up questions in the next few days though. – shenkwen Apr 12 '16 at 03:55
  • @shenkwen no problem. Worth to mention is that in order to get a smoother transition for the gradient you can smooth it like shown in this answer: http://stackoverflow.com/a/30205767/1693593 –  Apr 12 '16 at 08:08
  • In my actual scenario, the image I want to animate is actually texts with transparent background(it it not normal font that browsers support, so I have to use image instead of plain text). It seems to me the "destination-out" mode is good enough, I don't quite understand the advantage of "xor"(as you said, it is to get the canvas element transparent, but isn't it transparent by default?). – shenkwen May 08 '16 at 21:32
  • @shenkwen it's a matter of taste I guess. You can destination-out too. There are many paths to the same result. Some operation may have performance benefits over another, but test for your scenario (xor inverts the alpha) –  May 09 '16 at 12:11
1

Use the context composite property to do the masking via a gradient. Create an offscreen canvas the same size as the image or the size of the display canvas whichever is smallest.

For every frame create the gradient with the appropriate colour stops (CSS color format rgba(red, green, blue, alpha)) to set the alpha values.

Clear the off screen canvas

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

Then set the composite value for the of screen canvas to

ctxOffScreen.globalCompositeOperation = "source-over";

Render the image onto it

ctxOffScreen.drawImage(image, 0, 0, ctxOffScreen.canvas.width, ctxOffScreen.canvas.height);

Then set the comp to

ctxOffScreen.globalCompositeOperation = "destination-in";

This will match the pixels already drawn with the same alpha value that is in what every you draw next (the gradient mask)

Then set the fill style to the gradient you created and draw a rectangle over the top

ctxOffScreen.fillStyle = gradient;
ctxOffScreen.fillRect( 0, 0, ctxOffScreen.canvas.width, ctxOffScreen.canvas.height);

Then just render the offscreen canvas to the onscreen canvas

ctx.drawImage(ctxOffScreen.canvas, 0, 0);

If you use

ctxOffScreen.globalCompositeOperation = "destination-out";

instead of "destination-in" you will invert the mask you created with the gradient.

Blindman67
  • 51,134
  • 11
  • 73
  • 136
0

As an alternative to canvas, you can use svg. CSS gradients are not animateable, however other properties of svg are, so you can find some creative ways to animate gradients after all.

#Mask rect {
  x: 400px;
  transition: 1s;
}

svg:hover #Mask rect {
  x: -400px; 
}

svg {
  border: 2px solid black;
  background-color: #ee3377;
}
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400">
  <defs>
    <linearGradient id="Gradient">
      <stop offset="0" stop-color="white" stop-opacity="0" />
      <stop offset=".5" stop-color="white" stop-opacity="1" />
    </linearGradient>
    <mask id="Mask">
      <rect width="800" height="400" fill="url(#Gradient)"  />
    </mask>
  </defs>
  <image xlink:href="http://i.imgur.com/g3D5jNz.jpg" width="400" height="400" mask="url(#Mask)"></image>
</svg>

You could probably animate the offset property directly, I haven't tested yet.

jered
  • 11,220
  • 2
  • 23
  • 34
  • "CSS gradients are not animateable" **through css**. All [specific attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient#Specific_attributes) of the ``, except `xlink:href` one are animatable. So SMIL and js can animate those. You can also animate the attributes of the `` elements. – Kaiido Apr 12 '16 at 02:04