简体   繁体   中英

How can I recreate this “wavy” image effect?

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:

the "spirit" area:

在此输入图像描述

the robe area :

在此输入图像描述

the robe area :

在此输入图像描述

Preferably I'd like to control the speed and modulation of the waves parametrically.

To me it looks like some sort of displacement map. I thought about either using a fragment shader threejs \\ seriously.js or using a canvas element to achieve the same image manipulation, but I'm unsure of the algorithm to use.

What would be a way to achieve this?

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.

宽度

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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM