Compose で Jellyfish を動かす: ImageVectors のアニメーション化と AGSL RenderEffects の適用

私はインターネット上でインスピレーションを与える人々をフォローし、彼らが作るものを見るのが大好きです。そのような人の 1 人がCassie Codesで、彼女はウェブ用の素晴らしいアニメーションを作成しています。彼女の感動的な例の 1 つは、このかわいいアニメーションのクラゲです。
これを見てしばらく夢中になった後、このかわいい小さな生き物は Compose にも命を吹き込む必要があると考え続けました。このブログ投稿では、Jetpack Compose でこれを作成する方法について説明します。最終的なコードはここにあります。ここで紹介するテクニックは、もちろんクラゲだけに関係するものではありません…他の魚にも当てはまります! 冗談ですが、このブログ記事では次の内容について説明します。
- カスタム ImageVectors
- ImageVector パスまたはグループのアニメーション化
- AGSL RenderEffect を使用してコンポーザブルに歪みノイズ効果を適用します。
SVG の分析
このクラゲを実装するには、まず SVG が何で構成されているかを確認し、そのさまざまな部分を複製してみる必要があります。SVG が何を描画しているかを把握する最善の方法は、そのさまざまな部分をコメントアウトして、svg の各セクションがレンダリングする視覚的な結果を確認することです。これを行うには、上記のリンク先の codepen で変更するか、SVG をダウンロードしてテキスト エディターで開きます (テキストが読み取れる形式です)。
それでは、この SVG の概要を見てみましょう。
<!--
Jellyfish SVG, path data removed for brevity
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
<defs>
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle" />
<path class="tentacle" />
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="tentacle"/>
<path class="face" />
<path class="outerJelly"/>
<path id="freckle" />
<path id="freckle"/>
<path id="freckle-4"/>
</g>
<g id="bubbles" fill="#fff">
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble"/>
<path class="bubble" />
</g>
<g class="jellyfish face">
<path class="eye lefteye" fill="#b4bebf" d=""/>
<path class="eye righteye" fill="#b4bebf" d=""/>
<path class="mouth" fill="#d3d3d3" opacity=".72"/>
</g>
</svg>
- SVG を構成するパスとパスのグループ:
- 触手
- 顔 — ブロブと外側のゼリー
- 目 — 開いた状態と閉じた状態のアニメーション
- バブル — クラゲの周囲でランダムにアニメーション化 — サイズとアルファがアニメーション化
- M, m : に移動
- L、l、H、h、V、v : 行先
- C, c, S, s : 3 次ベジエ曲線
- Q, q, T, t:二次ベジエ曲線
- A、a:楕円弧曲線~
- Z, z — パスを閉じる
- クラゲはゆっくりと上下に動くはずです
- クラゲをクリックすると目が点滅するはずです
- クラゲの体には、ぐらつき/ノイズ効果が適用されている必要があります。
この SVG が何で構成されているかを理解したので、Compose で静的バージョンをレンダリングしてみましょう。
カスタム ImageVector の作成
Compose にはImageVectorの概念があり、 SVG と同様にプログラムでベクターを作成できます。変更せずにレンダリングするだけのベクター/SVG の場合、painterResource(R.drawable.vector_image) を使用して VectorDrawable をロードすることもできます。これにより、Compose がレンダリングする ImageVector に変換されます。
クラゲを SVG として xml ファイルにインポートし、painterResource(R.drawable.jelly_fish)
.
これは素晴らしい質問です。クラゲをこの方法でロードすると、SVG の乱気流の側面が取り除かれ、ロードされた XML でイメージがレンダリングされます (こちらのドキュメントで説明されているように)。しかし、パスの個々の部分については、クリック時にパーツをアニメーション化したり、ボディにノイズ効果を適用したりするなど、もう少しやりたいことがあるので、プログラムで構築しますImageVector
。
Compose でこのクラゲをレンダリングするためにd
、魚を構成するパス データ (またはパス上の「 」タグ) をコピーできます。たとえば、最初の触手には次のパス データがあります。
M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
今、あなたはおそらく考えているでしょう — 頭の中に絵を描いて、すべての位置とコマンドを手で覚える必要がありますか? いいえ—まったくありません。Figma や Inkscape など、ほとんどのデザイン プログラムでベクターを作成し、描画結果を SVG にエクスポートして、この情報を自分で取得できます。うわー!
Compose でベクトルを作成するには: を呼び出してrememberVectorPainter
を作成しImageVector
、 を作成し、次に別のを作成して、最初のをその中に最初の触手として配置します。また、クラゲ全体の背景としてを設定しました。Group
jellyfish
Group
tentacles
Path
RadialGradient
次の結果は、放射状のグラデーションの背景で画面に描かれた小さな触手です!

SVG のすべての要素に対してこのプロセスを繰り返します — SVG ファイルからパスのビットを取得し、描画されるパスに色とアルファを適用します。また、パスを論理的に触手、顔、泡など:
これでクラゲ全体が上記でレンダリングされましたImageVector
。

ImageVector パスとグループのアニメーション化
このベクトルの一部をアニメーション化します。
それでは、 の個々のビットをアニメートする方法を見てみましょうImageVector
。
クラゲを上下に動かす
codepen を見ると、クラゲが上下に平行移動 (y 平行移動) していることがわかります。Compose でこれを行うには、無限トランジションと を作成し、translationY
3000 ミリ以上アニメーション化します。次に、クラゲを含むグループを設定し、顔に を設定しますtranslationY
。これにより、上下のアニメーションが生成されます。

すばらしい — の一部がImageVector
上下にアニメーション化され、泡が同じ位置に留まっていることがわかります。
まばたき目 ️
codepen を見ると、それぞれの目にscaleY
とopacity
アニメーションがあることがわかります。Group
これら 2 つの変数を作成し、 にスケールを適用し、 にアルファを適用しましょうPath
。また、これらをクラゲをクリックしたときにのみ適用して、よりインタラクティブなアニメーションにします。
アニメーション状態を保持する 2 つのAnimatableと、クラゲをクリックしたときに呼び出す suspend 関数を作成します。これらのプロパティをアニメーション化して、目をスケーリングおよびフェードします。
クリックするとかわいい点滅アニメーションが表示され、クラゲがほぼ完成しました。

歪み/ノイズ効果の適用
これで、アニメートしたいもののほとんど (上下の動き、まばたき) ができました。クラゲの体にグラグラ効果が適用されている様子を見てみましょう。体と触手はノイズを加えて動き、動きを感じさせます。

SVG とアニメーション コードを見ると、 を使用feTurbulence
してノイズを生成し、それを として SVG に適用していることがわかりfeDisplacementMap
ます。
<filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
<feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
<feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
</filter>
</defs>
<g class="jellyfish" filter="url(#turbulence)">
これを実現するためにAGSLシェーダーを使用できますが、これは Tiramisu 以降 (API 33+) でのみサポートされていることに注意してください。最初に、ウォブルとして機能するシェーダーを作成する必要があります。最初はノイズを使用しません。単純にするために、代わりにマッピング関数のみを使用します。
シェーダーが動作する方法は、個々のピクセルに作用することです。座標 ( fragCoord
) を取得し、その座標でレンダリングされるカラー結果を生成することが期待されます。以下は、コンポーザブルの変換に使用する最初のシェーダーです。
この場合、使用する入力は、現在画面上にレンダリングされているピクセルです。uniform shader contents;
入力として送信する変数を介してこれにアクセスします。入力座標 ( fragCoord
) を取得し、この座標にいくつかの変換を適用します — 時間とともに移動し、通常、移動するために何らかの計算を実行します。
これにより新しい座標が生成されるため、そのfragCoord
位置で正確な色を返す代わりに、入力ピクセルを取得する場所をシフトします。たとえば、 があった場合、return contents.eval(fragCoord)
変更は発生しません — パススルーになります。コンポーザブルの別のポイントからピクセルの色を取得します。これにより、コンポーザブルのコンテンツにぐらつきのある歪み効果が生じます。
これをコンポーザブルで使用するには、このシェーダーをRenderEffect
コンポーザブルのコンテンツにとして適用できます。
を使用createRuntimeShaderEffect
して、 を入力として渡しWOBBLE_SHADER
ます。これは、コンポーザブルの現在のコンテンツを取得し、パラメーター名「 」を使用して、シェーダーへの入力として提供しますcontents
。次に、 内の内容を照会しますWOBBLE_SHADER
。time
変数は時間の経過とともにぐらつきを変化させます (アニメーションを作成します) 。
これを実行すると、全体Image
が歪んでいて、クラゲのように少しぐらついていることがわかります。

顔と泡に効果を適用したくない場合は、それらを別ImageVectors
の に抽出し、それらのベクトルにレンダリング効果を適用することをスキップできます。

ノイズ効果の適用
上で指定したシェーダーは、コンポーザブルのコンテンツに変位を適用するためにノイズ関数を使用していません。ノイズは、より構造化されたランダム関数を使用して変位を適用する方法です。そのようなタイプのノイズの 1 つはパーリン ノイズ (内部でfeTurbulence
使用するもの) です。パーリン ノイズ関数を実行した結果をレンダリングすると、次のようになります。

空間内の各座標のノイズ値を使用し、それを使用して「contents
」シェーダーで新しい座標をクエリします。
Perlin ノイズ関数を使用するようにシェーダーを更新しましょう (この Github リポジトリから適応)。次に、それを使用して、入力座標から出力座標への座標マッピング (つまり、変位マップ) を決定します。
このノイズ関数を適用すると、はるかに良い結果が得られます! クラゲが水の中で動いているように見えます。

しかし、なぜこれを使用するのでしょうか。
この時点で、これはクールだと思うかもしれませんが、そのユースケースでは非常にニッチです、レベッカ。確かに — あなたは仕事で毎日アニメーションのクラゲを作っているわけではないかもしれません (私たちは夢を見ることができますか?)。ただしRenderEffects
、任意の構成可能なツリーに適用できるため、ほぼすべてのものに効果を適用できます。
たとえば、グラデーション テキストやコンポーザブル スクリーン全体にノイズ効果やその他の AGSL 効果を加えたくないのはなぜでしょうか?

まとめ
そのため、このブログ投稿ImageVectors
では、SVG からカスタムを作成し、一部をアニメーション化し、Compose の UI にImageVector
AGSL シェーダーを適用するなど、多くの興味深い概念について説明しました。RenderEffects
Jellyfish の完全なコードについては、ここで完全な要点を確認してください。AGSL RenderEffectsの詳細については、ドキュメントを確認するか、JetLagged サンプルで別の使用例を確認してください。
ご不明な点がございましたら、Mastodon androiddev.social/@riggarooまたはTwitterまでお気軽にお問い合わせください。
Jolanda Verhoef、Nick Butcher、Florina Muntenescu、Romain Guy、 Nader Jawad に感謝します。