How can I recreate this “wave” image effect?

I am looking for a way to make an image or part of an image on a web page and render this or similar animation effect where the image is “wave”. Examples:

spirit area:

enter image description here

mantle area :

enter image description here

mantle area :

enter image description here

It is advisable that I control the speed and modulation of the waves parametrically.

For me, it looks like some kind of displacement map. I thought about using the fragmentary shader threejs \ seriously. js or using the canvas element to achieve the same image manipulation, but I'm not sure about using the algorithm.

What will be the way to achieve this?

+5
source share
1 answer

Oscillators and Bias

You can solve this by using generators in combination with a grid. Each line in the grid fluctuates, and the difference between the lines is used to offset the image segment.

The simplest approach, in my opinion, is to create the first object of the oscillator. It can be very simple, but the bottom line is that an object can be created and it tracks the current values ​​locally.

Step 1: 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 an object to use frequency, amplitude, and speed as parameters. If many of them are used, also consider a prototype approach.

If we create a simple grid of five positions, in which the three middle vertical lines oscillate (edges that make up lines 1 and 5.), we get the following result (no change):

Snap1

Animated visualization of oscillating 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 the difference between the lines in the grid for the image

The next step is to simply calculate the difference between the created points for each row.

widths

The math is straightforward, we only need to make sure that the lines do not overlap, as this will create a negative width:

 // 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 width var w0 = lx1; // - x0 var w1 = lx2 - lx1; var w2 = lx3 - lx2; var w3 = x4 - lx3; 

If we send these values ​​to drawImage() for the destination using static fixed widths (i.e. mesh cell size) for the source, we get the result as shown below.

We do not need to drawImage() over the pixels in the bitmap, since drawImage() can be hardware accelerated, we do not need to fulfill the CORS requirements and do the interpolation for us:

 var img = new Image(); img.onload = waves; img.src = "https://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> 

Please note: since we use fractional values, we need to compensate for half the pixel in order to overlap the previous segment, since the final pixel can be interpolated. Otherwise, we get visible wavy lines as a result. We could use integer values, but this will create a more “uneven” animation.

Of course, the values ​​of the oscillators must be changed, the grid size, etc.

The next step is to repeat the oscillators for the horizontal axis and use the canvas itself as the image source.

Optimization and performance

Using the canvas itself as a caveat source

When you draw something from the canvas onto yourself, the browser should, in accordance with the specifications , make a copy of the current content, use this as a source for the destination area.

When a canvas or CanvasRenderingContext2D object is drawn on itself, the drawing model requires the source to be copied before the image, so you can copy parts of the canvas or bitmap with scratches to overlapping parts of yourself.

This means that for each drawImage() operation, where we use the canvas itself as a source, this copying process will be performed.

This can affect performance, so to avoid this, we can use the second canvas element, onto which we first copy the finished vertical pass, and then use the second canvas as the source for the horizontal pass.

LUT and value caching

To increase performance, cache all calculations of values ​​that can be cached. For example, the source width is higher for each segment (x1-x0, etc.). It can be cached by the sw variable (or some other name). This is the so-called micro-optimization, but this is the case when it can make a difference.

For sine, scale, etc. It may be an advantage to cache calculations in the LUT or lookup table. Frequencies can be selected so that the length of the table matches at some level. I do not show it here, but something to think about whether the browser should keep up if the grid has a high resolution.

Integer values

Using integer values ​​and disabling image smoothing is also an option. The result is not as good as with fractional values, but it will give retro-ish to watch the animation and work better.

Sprite sheets

Dynamic pre-generation of frames as a sprite as a last resort is possible. This is more than hunger and has an initial cost, but will work smoothly in almost any situation. The challenge is to find the loop point and not use too much memory.

Alpha Images

Avoiding images with an alpha channel (as in the demo version below) will help, since you will need to clear two times longer, one for the screen canvas, one for the main canvas. Otherwise, the previous move will be displayed in the background.

DEMO OF FINAL RESULT

Here is a complete demo with vertical and horizontal wavy lines. For simplicity, I use only the 4x4 series.

The result does not look completely identical to the examples, but should give an idea. It is simply a matter of increasing the resolution of the grid and adjusting the parameters. In addition, the examples provided in the question are pre-animated with the addition of effects and layers that cannot be achieved only with waves / offsets.

Other changes are that now the overlap of each segment extends throughout the segment, simply adding 0.5 at the beginning, but also at the end. Horizontal pass also refers to the difference in width in a row.

Click "Full Page" when you launch the demo version below to better see the details.

 var img = new Image(); img.onload = waves; img.src = "https://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(http://i.imgur.com/KbKlmpk.png);background-size:cover;height:100%;width:auto;min-height:300px} 
 <canvas width=230 height=300></canvas> 
+15
source

All Articles