How can I recreate this “wavy” image effect?

后端 未结 1 493
梦谈多话
梦谈多话 2020-12-24 04:07

I\'m searching for a way take a an image, or a portion of an image on a webpage and render this or a similar animation effect, where the image is \"wavy\". Examples:

相关标签:
1条回答
  • 2020-12-24 04:29

    Oscillators and displacement

    You can solve this by using oscillators combined with a grid. Each line in the grid is oscillated and the difference between the lines is used to displace a segment of the image.

    The simplest approach, in my opinion, is to create first an oscillator object. It can be very basic, but the point being that an object can be instantiated, and it keeps track of current values locally.

    Step 1: The oscillator object

    function Osc(speed) {
    
        var frame = 0;                           // pseudo frame to enable animation
    
        this.current = function(x) {             // returns current sinus value
            frame += 0.005 * speed;              // value to tweak..
            return Math.sin(frame + x * speed);
        };
    }
    

    For example, expand the object to use frequency, amplitude and speed as parameters. If many are to used, also consider a prototypal approach.

    If we create a simple grid of five positions, where the three middle vertical lines are being oscillated (edges, being 1. and 5. lines), we will get a result like this (non-tweaked values):

    Snap1

    Animated visualization of the oscillated grid-lines:

    function Osc(speed) {
    
      var frame = 0;
    
      this.current = function(x) {
        frame += 0.005 * speed;
        return Math.sin(frame + x * speed);
      };
    }
    
    var canvas = document.querySelector("canvas"),
        ctx = canvas.getContext("2d"),
        w = canvas.width,
        h = canvas.height;
    
    var o1 = new Osc(0.05), // oscillator 1
        o2 = new Osc(0.03), // oscillator 2
        o3 = new Osc(0.06); // oscillator 3
    
    (function loop() {
      ctx.clearRect(0, 0, w, h);
      for (var y = 0; y < h; y++) {
        ctx.fillRect(w * 0.25 + o1.current(y * 0.2) * 10, y, 1, 1);
        ctx.fillRect(w * 0.50 + o2.current(y * 0.2) * 10, y, 1, 1);
        ctx.fillRect(w * 0.75 + o3.current(y * 0.2) * 10, y, 1, 1);
      }
      requestAnimationFrame(loop);
    })();
    <canvas width=230 height=250></canvas>

    Step 2: Use difference between lines in grid for image

    The next step is to simply calculate the difference between the generated points for each line.

    widths

    The math is straight forward, the only thing we need to make sure is that the lines do not overlap as this will create negative widths:

    // initial static values representing the grid line positions:
    var w = canvas.width, h = canvas.height,
        x0 = 0, x1 = w * 0.25, x2 = w * 0.5, x3 = w * 0.75, x4 = w;
    
    // absolute positions for wavy lines
    var lx1 = x1 + o1.current(y*0.2);  // 0.2 is arbitrary and tweak-able
    var lx2 = x2 + o2.current(y*0.2);
    var lx3 = x3 + o3.current(y*0.2);
    
    // calculate each segment's width
    var w0 = lx1;        // - x0
    var w1 = lx2 - lx1;
    var w2 = lx3 - lx2;
    var w3 =  x4 - lx3;
    

    If we now feed these values to drawImage() for destination, using static fixed widths (ie. the grid cell size) for the source, we will get a result like below.

    We don't need to iterate the pixels in the bitmap as drawImage() can be hardware-accelerated, does not need to fulfill CORS requirements, and will do the interpolation for us:

    var img = new Image();
    img.onload = waves;
    img.src = "//i.imgur.com/PwzfNTk.png";
    
    function waves() {
    
      var canvas = document.querySelector("canvas"),
        ctx = canvas.getContext("2d"),
        w = canvas.width,
        h = canvas.height;
    
      ctx.drawImage(this, 0, 0);
    
      var o1 = new Osc(0.05), o2 = new Osc(0.03), o3 = new Osc(0.06),
          x0 = 0, x1 = w * 0.25, x2 = w * 0.5, x3 = w * 0.75, x4 = w;
    
      (function loop() {
        ctx.clearRect(0, 0, w, h);
        for (var y = 0; y < h; y++) {
    
          // segment positions
          var lx1 = x1 + o1.current(y * 0.2) * 3; // scaled to enhance demo effect
          var lx2 = x2 + o2.current(y * 0.2) * 3;
          var lx3 = x3 + o3.current(y * 0.2) * 3;
    
          // segment widths
          var w0 = lx1;
          var w1 = lx2 - lx1;
          var w2 = lx3 - lx2;
          var w3 = x4 - lx3;
    
          // draw image lines ---- source ----   --- destination ---
          ctx.drawImage(img, x0, y, x1     , 1,  0        , y, w0, 1);
          ctx.drawImage(img, x1, y, x2 - x1, 1,  lx1 - 0.5, y, w1, 1);
          ctx.drawImage(img, x2, y, x3 - x2, 1,  lx2 - 1  , y, w2, 1);
          ctx.drawImage(img, x3, y, x4 - x3, 1,  lx3 - 1.5, y, w3, 1);
        }
        requestAnimationFrame(loop);
      })();
    }
    
    function Osc(speed) {
    
      var frame = 0;
    
      this.current = function(x) {
        frame += 0.002 * speed;
        return Math.sin(frame + x * speed * 10);
      };
    }
    <canvas width=230 height=300></canvas>

    Notice that since we are using fractional values we need to compensate with half a pixel to overlap the previous segment as the end-pixel may be interpolated. Otherwise we will get visible wavy lines in the result. We could use integer values, but that will produce a more "jaggy" animation.

    The values of the oscillators will of course need to be tweaked, a grid size defined and so forth.

    The next step will be to repeat the oscillators for the horizontal axis, and use the canvas itself as an image source.

    Optimization and performance

    Using the canvas itself as source caveat

    When you draw something from a canvas to itself the browser have to, according to the specs, make a copy of the current content, use that as source for destination region.

    When a canvas or CanvasRenderingContext2D object is drawn onto itself, the drawing model requires the source to be copied before the image is drawn, so it is possible to copy parts of a canvas or scratch bitmap onto overlapping parts of itself.

    This means that for every drawImage() operation where we use the canvas itself as source, this copy process will happen.

    This can possibly take a hit on performance, so to avoid this we can use a second canvas element to which we first copy the finished vertical pass, then use the second canvas as source for the horizontal pass.

    LUT and value caching

    To increase performance further, cache every value calculation that can be cached. For example, the source width above for each segment (x1-x0 etc.) can be cached to sw variable (or some other name). This is so-called micro-optimization but this is a case where these can matter.

    For sinus values, scale etc. it can be an advantage to cache the calculations into a LUT, or look-up-table. The frequencies can be chosen so that the table length match up at some level. I am not showing this here, but something to consider if the browser struggle to keep up in case the grid is of high resolution.

    Integer values

    Using integer values and turning off image-smoothing is also an option. The result is not so good as with fractional values, but it will give a retro-ish look the animation and perform better.

    Sprite-sheet

    Dynamically pre-generate frames as sprite-sheet as a last resort is possible. This is more memory hungry and has an initial cost, but will work smoothly almost under any situation. The challenge is to find a looping point and not use too much memory.

    Images with alpha-channel

    Avoiding images with alpha-channel (as in the demo below) will help as you will need to clear two times extra, one for off-screen canvas, one for main canvas. Otherwise the previous displacement will show in the background.

    DEMO OF FINAL RESULT

    Here is a complete demo with both vertical and horizontal wavy lines. For simplicity I only use a 4x4 grid.

    The result does not look entirely identical to the examples but should give an idea. It's just a matter of increasing the grid resolution and tweaking the parameters. In addition, the examples given in the question are pre-animated with addition effects and layers which is not possible to achieve with just waves/displacement.

    Other changes are that now the overlap of each segment is spread over the entire segment by just adding 0.5 at beginning, but also at the end. The horizontal pass also to the width difference inline.

    Click Full page when you run the demo below to get a better look at the details.

    var img = new Image();
    img.onload = waves;
    img.src = "//i.imgur.com/nMZoUok.png";
    
    function waves() {
      var canvas = document.querySelector("canvas"),
          ctx = canvas.getContext("2d"),
          w = canvas.width,
          h = canvas.height;
    
      ctx.drawImage(this, 0, 0);
    
      var o1 = new Osc(0.05), o2 = new Osc(0.03), o3 = new Osc(0.06),  // osc. for vert
          o4 = new Osc(0.08), o5 = new Osc(0.04), o6 = new Osc(0.067), // osc. for hori
          
          // source grid lines
          x0 = 0, x1 = w * 0.25, x2 = w * 0.5, x3 = w * 0.75, x4 = w,
          y0 = 0, y1 = h * 0.25, y2 = h * 0.5, y3 = h * 0.75, y4 = h,
          
          // cache source widths/heights
          sw0 = x1, sw1 = x2 - x1, sw2 = x3 - x2, sw3 = x4 - x3,
          sh0 = y1, sh1 = y2 - y1, sh2 = y3 - y2, sh3 = y4 - y3,
          
          vcanvas = document.createElement("canvas"),  // off-screen canvas for 2. pass
          vctx = vcanvas.getContext("2d");
    
      vcanvas.width = w; vcanvas.height = h;           // set to same size as main canvas
    
      (function loop() {
        ctx.clearRect(0, 0, w, h);
        
        for (var y = 0; y < h; y++) {
    
          // segment positions
          var lx1 = x1 + o1.current(y * 0.2) * 2.5,
              lx2 = x2 + o2.current(y * 0.2) * 2,
              lx3 = x3 + o3.current(y * 0.2) * 1.5,
    
              // segment widths
              w0 = lx1,
              w1 = lx2 - lx1,
              w2 = lx3 - lx2,
              w3 =  x4 - lx3;
    
          // draw image lines
          ctx.drawImage(img, x0, y, sw0, 1, 0        , y, w0      , 1);
          ctx.drawImage(img, x1, y, sw1, 1, lx1 - 0.5, y, w1 + 0.5, 1);
          ctx.drawImage(img, x2, y, sw2, 1, lx2 - 0.5, y, w2 + 0.5, 1);
          ctx.drawImage(img, x3, y, sw3, 1, lx3 - 0.5, y, w3 + 0.5, 1);
        }
    
        // pass 1 done, copy to off-screen canvas:
        vctx.clearRect(0, 0, w, h);    // clear off-screen canvas (only if alpha)
        vctx.drawImage(canvas, 0, 0);
        ctx.clearRect(0, 0, w, h);     // clear main (onlyif alpha)
    
        for (var x = 0; x < w; x++) {
          var ly1 = y1 + o4.current(x * 0.32),
              ly2 = y2 + o5.current(x * 0.3) * 2,
              ly3 = y3 + o6.current(x * 0.4) * 1.5;
    
          ctx.drawImage(vcanvas, x, y0, 1, sh0, x, 0        , 1, ly1);
          ctx.drawImage(vcanvas, x, y1, 1, sh1, x, ly1 - 0.5, 1, ly2 - ly1 + 0.5);
          ctx.drawImage(vcanvas, x, y2, 1, sh2, x, ly2 - 0.5, 1, ly3 - ly2 + 0.5);
          ctx.drawImage(vcanvas, x, y3, 1, sh3, x, ly3 - 0.5, 1,  y4 - ly3 + 0.5);
        }
    
        requestAnimationFrame(loop);
      })();
    }
    
    function Osc(speed) {
    
      var frame = 0;
    
      this.current = function(x) {
        frame += 0.002 * speed;
        return Math.sin(frame + x * speed * 10);
      };
    }
    html, body {width:100%;height:100%;background:#555;margin:0;overflow:hidden}
    canvas {background:url(https://i.imgur.com/KbKlmpk.png);background-size:cover;height:100%;width:auto;min-height:300px}
    <canvas width=230 height=300></canvas>

    0 讨论(0)
提交回复
热议问题