📝

Svelte の crossfade 解読 (ついでに transition/motion も)

2021/02/16に公開

【更新履歴】
2021.04.10 - 文章の微調整、最新情報との整合性確認

はじめに

Svelteは公式チュートリアルが非常に充実しており、一周すれば仕様の把握には十分でした。

しかしトランジションの中にある "crossfade" だけサンプルコードに謎が多く、ドキュメントの方も「チュートリアルを見てくれ」となぜか投げやり。ネットにも情報が少ないようなので記事にしました。(ちなみに引っかかったのはチュートリアルのココ)

という事でメインはcrossfadeですが、せっかくなので他のも載せてます。

シンプルな transition たち

状態変化が起こると勝手にトランジションをかけてくれる。
cssのパラメータとかも気にしなくて良い。

【今回使ってるもの】fade, scale

<script>
  import { fade, scale } from 'svelte/transition'    // インポート

  let checked =true
</script>

<!-- ======================================================= -->

<!-- checked をトグルさせる処理は適宜書く -->

{#if checked}
  <p transition:fade>Transition - fade</p>    // ┬ ここが本体
  <p transition:scale>Transition - scale</p>  // ┘
{/if}

直接関係しているのは、pタグ内のtransition:fade,transition:scaleの部分のみ。
もっと詳細に duration, easing 等のパラメータを記述する事もできる。また、表示→非表示/非表示→表示 で別々の指定をする事も可能。

数値を動かしてくれる motion

変数に入れた数値を指定の値までイージングをかけながら動かしてくれる。
変数を自分で各パラメータに割り当てる必要はあるが、自由度は高い。

【今回使ってるもの】Tweened

<script>
  import { tweened } from 'svelte/motion'    // インポート
  import { backInOut } from 'svelte/easing'  // イージング指定するならコレも

  // motion の本体 (設定や動かす数値が入ってる)
  const value = tweened( 10, {     // 第1引数が初期値
    duration: 2000,                // ┐
    easing: backInOut,             // ┴ ここはパラメータ (空でも可)
  })
  
  // value 書き換え処理 (10と90を行き来させてる)
  const toggle = ()=>{
    value.set( 100-$value )        // ┬ どっちでも結果は同じ
    value.update( n => 100-n )     // ┘ (どっちかだけ書けばok)
  }
</script>

<!-- ======================================================= -->

<!-- toggle を呼び出す処理は適宜書く -->

<div>
  <p style="width: {$value}%;"></p>    // 使う時は { $ 変数名 }
</div>

<p style="opacity: {$value/100};">opacity</p>

tweened() でパラメータ設定したものを変数(上の例ではvalue)に入れ、その変数内の値を動かしたり取り出して使ったり。

どうやら内部では store (svelte のグローバル変数みたいなもの) を使っているらしく、呼び出す時は $ が必要。書き換える時は set, update を使う。

本題 crossfade

Svelte において、crossfade は transition の一種です。
見た方がイメージしやすいと思うので、Svelte公式のサンプル集にあるコレとかコレをどうぞ。

使いこなせば非常に幅広い実装が可能だと思うんですが(たぶん)、今回はサンプルに出てきたケースに限定して話をします。
簡単に言うと移動元(消失場所)と移動先(出現場所)をイイ感じに繋げてくれるトランジションです。

そう思って先程の公式サンプルを見てもらえれば… あれ、それでも良くわからない?
ではフロントエンドで移動系のリッチUIを実装しようとした時あるあると共に、順を追って掘り下げますね。


1. 要素内移動 (座標指定 → 座標指定)

これは非常に楽なパターン。やり方は山ほどあると思います。
移動元と先が決まっているというのが、アニメーションで一番楽な状況。

2. 元の場所基準での移動

(さっきから図のクオリティが…)
これも比較的楽。transform の translate系を使えば、配置された位置を基準に動かせます。

3. 要素を跨いだ移動

これ。これがダルい。
リスト間の移動だったり、画像のポップアップ(元の位置から拡がるヤツ。Lightboxとは微妙に違う)等が身近な例ですかね。
ブロック要素の積み重ね等で自動的に配置された場所って、数値としては把握してない状態なんですよね。2のように"その位置を基準に特定距離動かす"とかなら良いんですが、そうでない場合はJavaScriptで位置取得からする必要があります。

皆さんこの辺りから「よし、ライブラリ使うか」ってなるのではないかと。

幸いこのような動きを簡単に実装するライブラリは豊富にありますし、各フレームワークにもデフォルトで入っていたりモジュールとして追加できたりします。
そしてそれが Svelte の場合は crossfade ですよ! という事で、ようやく crossfade 自体の話に到達しました。実装例いきましょう。

crossfade 実装

ポイントは移動元を消失させて、移動先に出現させるという点です。
まずはその動きだけ作ってみます。

<script>
  let moved =false
</script>

<!-- ======================================================= -->

<div>                           // block1
  {#if !moved}
    <p></p>
  {/if}
</div>
	
<div>                           // block2
  {#if moved}
    <p></p>
  {/if}
</div>

※ 例のごとく直接関係ある部分のみ抜粋

公式サンプルではDOMと連動させた2つの配列間で、該当要素を片方から削除&もう片方に追加という処理をしています。その結果、データが移動したように見えてますね。

上記の例ではそれを極限までシンプルに変えました。「移動してねーじゃん」と思われるかもしれませんが、今回の検証では問題ありません。

ここでようやく crossfade の出番です

<script>
  import { crossfade } from 'svelte/transition'  // インポート

  let moved =false
  const [send, receive] = crossfade({})  // ここが crossfade の制御
</script>

<!-- ======================================================= -->

<div>                              // block1
  {#if !moved}
    <p
      in:receive={{key:'key1'}}    // ┐
      out:send={{key:'key2'}}      // ┴ ここで相方要素と繋ぐ
    ></p>
  {/if}
</div>
	
<div>                              // block2
  {#if moved}
    <p
      in:receive={{key:'key2'}}
      out:send={{key:'key1'}}
    ></p>
  {/if}
</div>

イメージは、消えた要素の send で指定してある key と、出現した要素の receive で指定してある key が一致したら crossfade が繋げてくれる という感じです。

適当なダミーを追加しても大丈夫ですね。
(下の要素がじわっと上がってくるようにするのが、animate の flip。今回は省略)

ソースはないけど色々検証してみて得た考察

  • crossfade({}) で send, receive を作る部分
  • タグのプロパティで in:receive, out:send を記述する部分

ここは固定だと思います。key は一致/不一致の判定ができれば文字列でも何でも良いのではないかと。この例ではあえて key1, key2 と変えましたが、なんなら全部同じでも問題ありません。

公式サンプルでは、この key にややこしい値を入れているせいで難解なコードになってます。移動元と先は一致させつつ、他の兄弟要素の key とは被らないように配慮した結果でしょうね。

しかし兄弟要素がたくさんあって同じ key を持つ要素が他に存在しても、同時に消える要素, 出現する要素が1つずつなら問題なく動きました

crossfade({}) の中に色々と記述すれば各パラメータの調整もできます。duration で遷移速度の変更などが可能。

fallback は相方 (消える要素から見たら自分の send キーと同じキーを receive に持つ要素) が見つからない場合の transition を指定できます。
例えば「移動の"行き"は crossfade で繋げつつも、"帰り"はそれぞれが fade すれば良い」といった場合は、in, out を片方だけ記述 & fallback を指定 で対応できると思います。

crossfade の使いどころ

ここまで解説しといてアレですが、出番はそんなに多くないかと。UIをリッチにしていく場合でも、なかなか後半になるまで出てこないんじゃないかな…

しかし私はWEBアプリケーションを作るとき

  • 今何が起きたのかを視覚情報としてユーザーへフィードバック
  • "移動" という感覚に近い操作はドラッグ&ドロップで実装

こういった点を非常に重視するタイプなので、crossfade のような機能はありがたいですね。
Svelte は sveltekit のリリースも近い(はず)ですし、今後も追っかけていこうと思ってます。

【追記】Sveltekit 出ましたね (まだベータ版ですが)

最後に

こういう記事は公式やきちんとしたソースを参照して書くべきですが、今回は公式にすら無かったのでやむを得ない。手探りで検証した結果なので、記述として間違っている部分があれば容赦なくコメントで指摘してください。自己満足で間違った情報をネットの海に放り出すようなことはしたくないので、すぐに訂正します。
(ここ数年で「自分の勉強も兼ねた備忘録です」みたいな間違いだらけの記事が激増してますが、正直言って私は好きではない)

Discussion