 
  こんにちはjunです。今日は以下のようなcanvasを用いたアニメーションを作っていきます。 レスポンシブをとりあえず画像の一部をうねらせるだけであれば70行ほどのjsコードですみます。
 
こちらにて実際に動かしています。
細かい実装の説明にうつる前に今回用いる必要な知識と原理について確認します。以下の知識にある程度知見がある人はすっ飛ばしてください。
実装サイトでも見ていただいと思いますが、きちんとアニメーションをしておりまた、動画を流しているわけではありません。このアニメーションはcanvas要素というものをjsで操作することで実装ができます。
画像を波打たせる方法は普通のimgタグやdivでは難しいです。柔軟に簡単に実装するためにcanvasを用います。
このcanvasでのアニメーションは実は見えないスピードで「画像を消しては、再描画、消して、再描画…」というのを行っています。また描画する画像は下部を透明な波線で消してから描画しています。また波が連なり、流れるように見せるために三角関数を用いて波型に消す箇所を計算しています。
ちょっとわかりにくいにので図にしてみます。
 
また1〜6を繰り返すたびにsni(θ)のθを増やしていけば毎回異なる波がうねる様に見えます。この様にして波のアニメーションを実装します。
canvasというのはHTML5で扱われる要素の一つであり、2次元図形・グラフィック・アニメーションをjavaScriptを用いて描画することができます。cssでは解決できない図形やアニメーションを実装することができます。数十年前だとflashが担っていたことをHTMLで行うような感じです。
ではまずはうねらせる画像をcanvas要素に表示させるところまで行います。今回は同階層に
を用意しておきます。sample.jpgは縦横比1:2にトリミングをしておきます。では作っていきましょう。まずは以下のように適当にHTMLを作っておきます。
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>waving image</title>
        <style>
            *{
                margin:0;
                padding:0;
            }
            main{
                background:rgb(240, 255, 255);;
            }
        </style>
    </head>
    <body>
        <main>
            <canvas id="canvas"></canvas>
        </main>
        <script src="./app.js"></script>
    </body>
</html>
描画がされるcanvasを用意して、javascriptで拾えるようにidをつけておきましょう。あとは描画を実行するjsファイルをcanvasより後に書いておきます。
app.jsに以下のようにコードを書きます。
function initAnimation(){
    var canvas = document.getElementById('canvas');
    var ctx = canvas.getContext('2d');
    var imagePath = ('./sample.jpg');
    var image = new Image();
    image.src = imagePath;
    canvas.width = Number(window.innerWidth);
    canvas.height = Number(canvas.width/2);
    image.onload = function(){
          ctx.drawImage(image,0,0,image.width,image.height,0,0,canvas.width,canvas.height);
    }
}
この箇所ではHTMLからcanvas要素を指定して、jsを用いてcanvasの操作を行える様にするおまじないです。以降はこのctx(描画コンテキストインスタンス)に様々な指定を行います。
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
そして
var imagePath = ('./sample.jpg');
var image = new Image();
image.src = imagePath;
canvas.width = Number(window.innerWidth);
canvas.height = Number(canvas.width/2);
image.onload = function(){
        ctx.drawImage(image,0,0,image.width,image.height,0,0,canvas.width,canvas.height);
}
jsのImageオブジェクトを用いてsrcプロパティに映す画像のパスを入れます。canvas.width、canvas.heightでcanvasの大きさを指定します。画像が縦横1:2なので canvasもその比率に沿う様にしました。
image.onload を用いてsrcで指定した画像の読み込みが終わったら、
drawImage() メソッドを用いてcanvasに画像を描画する様にします。
image.onload を使わないと画像が読み込まれる前に描画しようとするので、映されません。
とりあえずここまでくると、canvas要素しかないHTMLにもかかわらず、以下の様に画像が描画されているはずです。
 
drawImage()はcanvasに範囲を指定して画像を描画します。canvas全体に画像を描画するならば必ず、最後の引数まで入力したほうがいいです。(MDNの解説(英語))
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
それぞれの引数を説明すると
Imageインスタンス。つまり描画する対象の画像
という感じです!私の今回の例でいくと
ctx.drawImage(image,0,0,image.width,image.height,0,0,canvas.width,canvas.height);
0,0,image.width,image.height, で画像の(0,0)座標地点から画像の幅と高さ分、画像を切り取るという意味です。つまり画像全体を読み取っているのと同じです。
もし0,0,image.width/2,image.height/2, としたら元画像の1/4だけの部分切り取られた画像が映し出されます。
0,0,canvas.width,canvas.height ここもcanvasの(0,0)座標地点からcanvasの幅と高さ分、画像を貼り付けるという意味です。つまりcanvas全体に画像を貼り付けるのと同じです。
ここは実際にコードを描いて比率をいじって見てください。そうすればここの意味がわかる様になります。
それでは次にこの画像を波線に切り抜きます。切り抜きのイメージとしては上で説明した図の様に、一辺が波線の四角形を描いて、透明に塗り潰します。image.onload以降に以下のコードを加えます。
image.onload = function(){
    initDraw();
}
    
var canvasEndX = canvas.width;
var canvasEndY = canvas.height;
var waveStartPoint = canvasEndY-150;
var amplitude = 30;
var period = 1000;
var degree = 0;
function initDraw(){
    imageSet(image,canvasEndX,canvasEndY);
    waveDrawing(waveStartPoint,canvasEndX,canvasEndY,degree,amplitude,period);
}
function imageSet(imageObj,canvasEndX,canvasEndY){
    var imgWidth = imageObj.width;
    var imgHeight = imageObj.height;
    ctx.drawImage(image,0,0,imgWidth,imgHeight,0,0,canvasEndX,canvasEndY);
}
function waveDrawing(waveStartPoint,canvasEndX,canvasEndY,deg,am,tp){
    var waveStartY = waveStartPoint;
    ctx.globalCompositeOperation = "destination-out";
    ctx.beginPath();
    ctx.moveTo(0, waveStartY);
    for (var x=0; x <= canvasEndX; x+= 1) {
        var y = -am*Math.sin((Math.PI/tp)*(deg+x));
        ctx.lineTo(x, y+waveStartY);
    }
    ctx.lineTo(canvasEndX,canvasEndY);
    ctx.lineTo(0,canvasEndY);
    ctx.closePath();
     ctx.fillStyle = "rgba(255,255,255,1)"; //opacity 1
    ctx.fill();
}
ここでは3つの関数を作成します。
それぞれを解説していきます。
function imageSet(imageObj,canvasEndX,canvasEndY){
     var imgWidth = imageObj.width;
     var imgHeight = imageObj.height;
     ctx.drawImage(image,0,0,imgWidth,imgHeight,0,0,canvasEndX,canvasEndY);
}
この関数は単に画像を描画するだけです。引数に画像オブジェクトとキャンバスの幅・高さ情報をとり、先ほども説明したdrawImage()を用いてキャンバスに画像を描画します。
waveDrawing()という関数で波線のくりぬき処理をかいていきます。
function waveDrawing(waveStartPoint,canvasEndX,canvasEndY,deg,am,tp){
      var waveStartY = waveStartPoint;
      ctx.globalCompositeOperation = "destination-out";
      ctx.beginPath();
      ctx.moveTo(0, waveStartY);
      for (var x=0; x <= canvasEndX; x+= 1) {
           var y = -am*Math.sin((Math.PI/tp)*(deg+x));;
           ctx.lineTo(x, y+waveStartY);
      }
      ctx.lineTo(canvasEndX,canvasEndY);
      ctx.lineTo(0,canvasEndY);
      ctx.closePath();
      ctx.fillStyle = "rgba(255,255,255,1)"; //opacity 1
      ctx.fill();
}
各パラメーターは
最後の3つは高校の三角関数を思い出してください。それほど難しく考えず、amを大きくすれば波が大きくなり、tpの場合は大きいほどなだらかな波になります。
またctxは上部で定義したcanvasインスタンスです。今回はグローバルにしてます。
var waveStartY = waveStartPoint;
ctx.globalCompositeOperation = "destination-out";
ctx.beginPath();
ctx.moveTo(0, waveStartY);
画像を波線に透過させる場合にこの設定 ctx.globalCompositeOperation = "destination-out"; が重要です。MDNの解説
このglobalCompositeOperationはcanvasにおける図形どうしが重ね合わさった際にどう描画するのかを定義します。冒頭で出したこの図を見てみてください。
 
透明のくりぬきは画像の上に、赤線で範囲を指定してその中を透明色に塗りつぶすということをしています。その時に「すでに描画された画像」と「透明色に塗り潰された図形」が重ね合わさっています。
その時にを設定していると、後で描画した図形と重なり合わない部分だけが残る様に描画されます。つまり上図の5の様に一部分だけ透明になります。
 ctx.beginPath();
 ctx.moveTo(0, waveStartY);
 for (var x=0; x <= canvasEndX; x+= 1) {
     var y = -am*Math.sin((Math.PI/tp)*(deg+x));;
     ctx.lineTo(x, y+waveStartY);
 }
 ctx.lineTo(canvasEndX,canvasEndY);
 ctx.lineTo(0,canvasEndY);
 ctx.closePath();
次にくりぬきの範囲を指定します。上図でいうと1〜4を指します。フォトショップやイラストレーターを使っている人なら「パスで選択範囲を指定」という意味がわかると思います。それをここでjsを用いて行っています。
念のために解説すると、パスというのは図形を構成する点(座標)みたいなものです。そして図形はその点を結ぶことで描画できます。四角形であれば点(頂点)は4つあって、それを一筆書きすると四角ができますよね。その一筆書きの順路と位置をこのコードで定義しています。
今回はこのパスの指定を
この様にしています。
上記の方法で選択範囲を指定すれば、あとは塗りつぶすだけです。
ctx.fillStyle = "rgba(255,255,255,1)"; //opacity 1
ctx.fill();
fillStyleで塗り潰しの色を設定できます。ここは実際、globalCompositeOperation = "destination-out";を設定していれば何色でも大丈夫です。ですが念のため透明色を設定。
そしてfill()で指定したスタイル、パスの範囲で塗り潰しを行います。globalCompositeOperation = "destination-out";が設定されているので塗り潰された部分と画像の重なり合う部分以外が残り、重なり部分は透明になります。
ここまで来れば以下の様に波線にくり抜かれた画像が得られます。
 
定義したimageSet()とwaveDrawing()を用いて以下のループを設定します。
function loop(){
      setInterval(function(){
      imageSet(image,canvasEndX,canvasEndY);
      waveDrawing(waveStartPoint,canvasEndX,canvasEndY,degree,amplitude,period);
      degree += 12; //12はなんとなく
    },30)
}
ここで一番大切なのはdeggre +=4の様にwaveDrawing()で用いる初期角度を足していくことです。こうすることで波がウネウネします。degreeの加算が多いほど波が早くなります。50以上にすると荒波になります笑
そしてこのloop()の関数をimage.onloadで呼び出して発火させます。
image.onload = function(){
    initDraw();
    loop();
}
上記をまとめたコードがこちらです。
window.onload = init();
function init(){
    initAnimation();
    function initAnimation(){
        var canvas = document.getElementById('canvas');
        var ctx = canvas.getContext('2d');
        var imagePath = ('./sample.jpg');
        var image = new Image();
        image.src = imagePath;
        //set canvas width and height
        canvas.width = Number(window.innerWidth);
        canvas.height = Number(canvas.width/2);
        image.onload = function(){
                initDraw();
                loop();
            }
            
        var canvasEndX = canvas.width;
        var canvasEndY = canvas.height;
        var waveStartPoint = canvasEndY-150;
        var amplitude = 30;
        var period = 600;
        var degree = 0;
        function initDraw(){
            imageSet(image,canvasEndX,canvasEndY);
            waveDrawing(waveStartPoint,canvasEndX,canvasEndY,degree,amplitude,period);
        }
        function loop(){
            setInterval(function(){
                imageSet(image,canvasEndX,canvasEndY);
                waveDrawing(waveStartPoint,canvasEndX,canvasEndY,degree,amplitude,period);
                degree += 12;
            },30)
        }
        function imageSet(imageObj,canvasEndX,canvasEndY){
            var imgWidth = imageObj.width;
            var imgHeight = imageObj.height;
            ctx.globalCompositeOperation = "destination-over";
            ctx.drawImage(image,0,0,imgWidth,imgHeight,0,0,canvasEndX,canvasEndY);
        }
        function waveDrawing(waveStartPoint,canvasEndX,canvasEndY,deg,am,tp){
            var waveStartY = waveStartPoint;
            ctx.globalCompositeOperation = "destination-out";
            ctx.beginPath();
            ctx.moveTo(0, waveStartY);
            for (var x=0; x <= canvasEndX; x+= 1) {
                var y = -am*Math.sin((Math.PI/tp)*(deg+x));
                ctx.lineTo(x, y+waveStartY);
            }
            ctx.lineTo(canvasEndX,canvasEndY);
            ctx.lineTo(0,canvasEndY);
            ctx.closePath();
            ctx.fillStyle = "rgba(255,255,255,1)"; //opacity 1
            ctx.fill();
        }
    }
}
このコードの場合は
var amplitude = 30;
var period = 600;
function loop(){
      ...
      degree += 12;
      },30)
}
amplitude(振幅)で波の最大の高さ、period(周期)で1波の周期、degree +=で波の速さを変化させることができます。他にもwaveDrawing()で定義した三角関数の式を変えることで単純なサイン波でなく、複雑な波を描画できます。
以上がcanvasを用いて画像をウネウネさせる方法です。canvasではこの様に複雑なアニメーションを用いた描画が可能です。jsで記述するのでFlashのActionScriptとかやっていた人は馴染みがあるかもしれません。