milvinae

web & product design.

anime.jsで変化し続けるグラデーションを作る

,
関連項目…
 HTML、SCSS、javascript、anime.js

web上でいろんなエフェクトをつけたい…
でもプラグインを利用すると干渉しあったり変な挙動になったりする…
ReactやVueで作ってもWordpressのようなCMSに適用するのはなんか複雑…

そんな貴方に使っていただきたい知識たちを紹介します。
当サイトでも使われている、グラデーションで変化し続けるエフェクトを、anime.jsで作っていきます。
使うライブラリはanime.jsのみです。

  ~~~~~~~~~~~ anime.js とは? ~~~~~~~~~~~~~~

  シンプルでありながら強力なAPIを備えた軽量Javascriptアニメーションライブラリ(本文まま)
   https://animejs.com/
  何がいいって、NpmとかBuildとかカスタムフィールド作って無理やり追加するとかなしに、シンプルなJavaScriptLibraryであること。
  CMS使ってウェブサイト作っている最中に、エフェクト追加したくなっちゃった、そんな時に簡単に導入できる。
  クラス追加を強制しないし、既存コードにも追加しやすいところも素敵。
  歴史も古いので参考資料がたくさんあるものよいところ。

  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

CodePenをお見せしますので、こちらを見ながら解説していきます。
見づらい場合はCodePenのサイトに移ってみてください。
 https://codepen.io/keiyashi/pen/RwXjamN
気に入ったらLoveボタン、Pin留めよろしくお願いします。

See the Pen transition marble gradient with animejs by keiyashi (@keiyashi) on CodePen.


まず、やりたいこと明らかにしていきましょう。


<要求>

・色が時間変化
 → リニアな変化でなく、2次元的にランダムな変化をさせたい。
・色調は固定
 → 色調(トーン:tone)は色の調子であり、PCCS(日本色研配色体系)ではビビッド・ソフト・ペールなど12の色調群に分けられている。
   色調も変化してしまうと統一感が失われるので、彩度と明度を維持して色相だけ変化させたい。
・滑らかに変化する
 → 色の変化が途切れない。


<情報収集>

・cssと簡単なjsでどうにかならないか?
 → Linear-gradient及びRadial-gradientの指定はできるが、うねうね動き続けるのは難しい。
   mesherで生成するような複数のグラデーションを混ぜて、そのパラメータを時間変化させようとしても切り替わりは滑らかにならない。
・簡単に導入できるライブラリで解決できないか?
 → ReactやVue、Three.jsなどは多彩なことが出来るが導入コストが高い。
   anime.jsはjavascriptライブラリで、Wordpressに追加しやすく応用が利く。サンプルが多い。
   またanime.jsは色の変化もサポートしており、色から色への変化を滑らかにつないでくれる。
   これは使えそうな気がします。
・cssの色空間は?
 → 加法色空間(色を加えていくと白くなる)なので、色を重ねても汚くならない。
   透過をもつhtml要素を重ねることでグラデーションが表現できる。


<方向性>

以上のことから以下の方向での実装を探ります。

・大きい丸(ウィンドウをカバーするくらい大きい丸)を複数準備する
・それぞれの丸にはキーとなる色とTransparent(透明)の放射グラデーションを指定する
・透過グラデーションのある大きい丸を重ねることで、複雑なグラデーションを表現
・丸の位置や大きさを変化させてさらに複雑なグラデーションに!


<実装>

 html…
<div class="grad-board">
  <div id="marble-ball-1" class="marble-ball"></div>
  <div id="marble-ball-2" class="marble-ball"></div>
  <div id="marble-ball-3" class="marble-ball"></div>
  <div id="marble-ball-4" class="marble-ball"></div>
  <div id="marble-ball-5" class="marble-ball"></div>
</div>
シンプルですね。
背景になるgrad-boardと、動く丸1~5があります。

ではcss(scss)をつけていきましょう。
.grad-board{
  width :100dvw;
  height:100dvh;  
  display: block;
  position: relative;
  background-color:white;
  padding:0;
  margin:0;
  overflow:hidden;
  
  @media screen and (orientation:portrait)  { --dot-size : 100dvh;}
  @media screen and (orientation:landscape) { --dot-size : 100dvw;}
  
  .marble-ball{
    position: absolute;
    z-index: 1;
    width: var( --dot-size);
    height: var(--dot-size);
    border-radius: calc(var(--dot-size) / 4);
    transform-origin: center;
    background: radial-gradient(rgb(0,128,128) 0px, transparent 70%);
  }

  #marble-ball-1{
    left: calc(50dvw - var(--dot-size)/2);
    top:  calc(50dvh - var(--dot-size)/2);
  }
  #marble-ball-2{
    left: calc(25dvw - var(--dot-size)/2);
    top:  calc(25dvh - var(--dot-size)/2);
  }
  #marble-ball-3{
    left: calc(25dvw - var(--dot-size)/2);
    top:  calc(75dvh - var(--dot-size)/2);
  }
  #marble-ball-4{
    left: calc(75dvw - var(--dot-size)/2);
    top:  calc(25dvh - var(--dot-size)/2);
  }
  #marble-ball-5{
    left: calc(75dvw - var(--dot-size)/2);
    top:  calc(75dvh - var(--dot-size)/2);
  }
}


grad-boardに、サイズやはみ出た部分の非表示を設定していきます。

丸が重ならないとうまくグラデーションが表現できないので、丸のサイズは描画領域の縦横の大きいほうと同じ直径にしています。
丸のBackgroundColorはJavascriptで変化させるので、初期値は入れなくてもいいかもしれませんが、間違いがあった時に差異がわかりやすいように入れておきます。

また、animationではTranslateXとTranslateYで丸の位置を変化させるため、基準となる位置を指定しておきます。
それぞれの丸にPosition:absoluteを指定し、TopとLeftで初期位置を渡しておきます。
// inspired by "Layered animations with anime.js"
// https://codepen.io/juliangarnier/pen/LMrddV

document.addEventListener("DOMContentLoaded", (event) => {
    layeredAnimation();
});

function layeredAnimation(){
    var layeredAnimationEl = document.querySelector('.grad-board');
    var colorPints = layeredAnimationEl.querySelectorAll('.marble-ball');

    for (var i = 0; i < colorPints.length; i++) {
        animateShape(colorPints[i]);
    }
}

function animateShape(el) {
    var easings = ['easeInOutQuad', 'easeInOutCirc', 'easeInOutSine'];
    var animation = anime.timeline({
        targets: el,
        duration: function() { return anime.random(3000, 4000); },
        easing: function() { return easings[anime.random(0, easings.length - 1)]; },
        complete: function(anim) { animateShape(anim.animatables[0].target); },
    })
    .add({
        translateX: anime.random(-500, 500),
        translateY: anime.random(-400, 400),
        scale: anime.random(100, 200) / 100,
        background: makeColorContext(),
    }, 0);
}
このコードは、anime.jsのサンプル ”Layered animations with anime.js"にインスパイアされて作っています。
丸の動きはサンプルのモノを簡略化して使っています。

このスクリプトの核はanimateShape関数です。
anime.timeline({条件}).add({遷移先})でアニメーションが定義されており、completeで再帰的に同じ関数が呼び出されます。
条件に記載されているのは、
・Targets : アニメーションさせたい対象
・Duration : アニメーションが行われる時間
・Easing : 変化の仕方
・Complete: アニメーション終了後に呼び出される処理
となっています。
{条件}にはLoopやらDelayやらDirectionやら指定ができますが、詳しく知りたい方は本家ドキュメントを参照ください。
時間にかかわる要素はすべてミリ秒での指定です。

{遷移先}では、Targetsで指定された要素がどのように変化してほしいか、が記述されています。
今回はTranslateX、TranslateY、Scale、Backgroundが変化対象として指定されています。

サンプルに従えばBackgroundに色を指定することでアニメートできるようです。
早速、グラデーションを当てて確認してみましょう。
radial-gradient(at 50% 50%, hsl(60, 80%,60%) 0%, transparent 70%)


…色が変わりません。
グラデーションでないhslでは適用しました。
ちょっとanime.jsの中身をのぞいてみます。

…Backgroundで検索をかけてもヒットしません。
ではcolorはどうでしょうか?
…色であるかをチェックする関数、色であればrgbaに変換する関数がありました。
どうやらここを通すことでanime.jsが扱える形に変えているようです。
is.col関数にradial-gradientを色として認識できるように変更します。
変更を加えたanime.jsはこちらに置いておきます。

 https://github.com/keiyashi/animejs_for_marbleGradient

これで行けそうな気がします。
再チャレンジです。
いい感じにグラデーションがかかってますね。

RGBで色を指定すると色調の維持が難しいので、HSLで色を指定しました。
HSLはHue(色相)、Saturation(彩度)、Lightness(明度)の3要素で構成されており、彩度と明度を維持することで色調を維持することが出来ます。
hue : 0~360
saturation/lightness: 0~100%

cssでグラデーションを指定する方法はいくつかありますが、今回は放射グラデーションを使っています。
記述形式はmesherで出力される形式を使ってみます。

ちょっとRadial-gradientの解説しましょう。
放射グラデーションの指定方法はいくつもありますが、基本は同じです。
要素の縦横何%の位置からグラデーションを開始するか、色の指定と基準点からの距離の情報で指定できます。
上の例で行くと、html要素の縦50%・横50%を基準にして、基準点から0%の位置からhslで指定した色が始まり、基準点から70%の位置に向かってTransparentになっていく、という感じです。
3色でも指定できますし、基準点の指定がなくても行けます。
先ほどのグラデーションは次のグラデーションと等価です。
radial-gradient(hsl(60, 80% 60%) 0%, transparent 70%)

じゃあアニメーションに適用してみましょう。
分かりやすくするため、丸は1つだけにしています。
animeのBackgroundにmakeColorContext関数を通して上のグラデーションを渡してみます。

function makeColorContext(){
  return 'radial-gradient(at 50% 50%, hsl(60, 80%,60%) 0%, transparent 70%)';
}

ぉ、ぉおん…?
なんか変ですね…。
at 50% 50% を at 0% 50%に変えてみましょうか。
もっと不可解になってしまいました。
ただ、動きに関していうと終着点は指定通りの位置に来ているように見えます。
もしかするとat X% Y%もアニメーション対象として認識されてしまっているのかもしれません。

一旦at~~の部分を取り外してみます。
位置は望ましいですが色が勝手に変わってしまいます。
確かに時間変化するグラデーションを作りたいですが、勝手に変化してしまうのはいただけません。

hslでの指定がよくないのでしょうか?
rgbでグラデーションを作ってみます。

いい感じですね。
ただし、色調はコントロールしたいので、hslで色を指定してからrgb変換してradial-gradientに渡すようにしましょう。
hslからrgbに変換する関数はanime.jsに記述されているのでanime.js内に下記を追加して呼び出せるようにしておきます。
anime.hsl2Rgb = hslToRgb;

さて、放射グラデーションを時間変化させてみましょう
Backgroundcolorに、ランダムに色を与えるmakeColorContextを実装します。

function makeColorContext(){
    var angleBand = anime.random(-180, 180);
    var hue   = 180 + angleBand;
    var colorText = 'hsl(' + Math.round(hue) + ', ' + 80 + '%, ' + 50 + '%)';
    var outputContext = 'radial-gradient(' + anime.hsl2Rgb(colorText) + ' 0px, transparent 50%)';
    return outputContext;
}
色相は0~360度の色相環で表現されています。
それを踏まえてランダムなRadial-gradientを作りましょう。

180を基準に、-180~180の範囲のランダムな値を合成して、ランダムな色相を作ります。
彩度と明度を固定したhsl形式に色相を指定します。
hslをrgbに変換してradial-gradientを作ります。
以上。

では丸1個の状態で試してみましょう。
とてもいい感じです。

丸5つで動かしながらやってみましょう。
Gifでみると重なっている部分のシャギーが気になりますが、実物はもっとなめらかでいい感じです。
これでランダムに時間変化する2次元グラデーションが出来ました。
きっともっといい作り方があると思います。
いい方法があれば教えてください。

<注意点>
大きい丸がabsolute指定されているので要素の上下関係で上のほうに来てしまうため、下の要素にタッチできなくなる場合があります。
その場合は丸にpointer-events:noneを持たせたりして回避して下さい。