かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

GSAP ScrollTrigger で CSS アニメーションを発火させ、アニメーション終了のコールバックを使いたい

いわゆるガチャガチャ動かしたいという要望のホームページ制作(非 React)で初めて GSAP というアニメーションライブラリを使ってる。 できることがありすぎて、まだ理解しきれてないが今回は ScrollTrigger を使って CSS のクラスを変更しアニメーションを発火させた後に、アニメーション終了を持ってコールバックを実行したかったのメモ

ライブラリ選定のメモ

パララックス・アニメーションライブラリで比較をした

aos vs gsap vs scrollmagic vs t-scroll vs velocityjs vs waypoints vs wowjs | npm trends

多くのライブラリが DT であまりメンテされておらず、ほぼ唯一 GSAP が TypeScript 化されており比較的メンテナンスがされていたので選定した

JavaScript に Intersection information があるので、ある程度のことはこれで実装可能だし、昨今の Web アプリケーションではガチャガチャ動くのはあまり意味をなさない装飾なのでこの分野の需要が減少しているのかも知れない。といっても広告的な "ほうむぺーじ" ではまだまだ必要とされる分野だとも思う (jQueryだってめちゃくちゃ使われてるし)

ScrollTrigger を使ってクラス名を変更し CSS アニメーションを設定する

シンプルな CSS アニメーションを作成する

.js-fadeUp {
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.6s ease-in-out;
}
.js-fadeUp.fade-in {
  opacity: 1;
  transform: translateY(0);
}

.js-fadeup クラスがある要素をターゲットに、.fade-in クラスが付けられるとアニメーションが開始される想定

GSAP でクラス名を変更する

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

// registerPlugin しないとエラーになる
gsap.registerPlugin(ScrollTrigger);

const targets = document.querySelectorAll('.js-fadeUp');
targets.forEach((target, index) => {
  ScrollTrigger.create({
    trigger: target,
    id: `fadeUpContent-${index + 1}`,
    start: 'top-=100 center',
    end: 'bottom top',
    toggleClass: { targets: target, className: 'fade-in' },
    once: true,
  });
});

これで .js-fadeup クラスのある要素がスクロールに応じてフェードインするようになった!

📝 note.

once: Boolean - If true, the ScrollTrigger will kill() itself as soon as the end position is reached once. This causes it to stop listening for scroll events and it becomes eligible for garbage collection. This will only call onEnter a maximum of one time as well. It does not kill the associated animation. It's perfect for times when you only want an animation to play once when scrolling forward and never get reset or replayed. It also sets the toggleActions to "play none none none".
cf. ScrollTrigger | GSAP | Docs & Learning

once を付けておくと一度だけ発火し、kill() されメモリを開放してくれるトノコト。
こってりとしたアニメーションを何度も見せられるのは、あまりユーザー体験が良いとは思わないので once を設定することにした。

CSS アニメーションが完了したらコールバックで対象要素を操作したい

アニメーション完了時に CSS のクラス名を変更するなど、アニメーション完了のコールバックで要素を操作したい
ScrollTrigger: onComplete callback - GSAP - GreenSock このスレッドには refresh イベントリスナーを使えば良いと回答されていたがうまく動作せず、scrollEnd を使ってみたが、アニメーションが発火した要素ごとにイベントが発火するものでは無さそうだった。 cf. static-addEventListener | GSAP | Docs & Learning

onEnter, onLeave コールバックを使えば、対象ごとにコールバックを設定できる

onEnter: Function - A callback for when the scroll position moves forward past the "start" (typically when the trigger is scrolled into view). It receives one parameter onLeave: Function - A callback for when the scroll position moves forward past the "end" (typically when the trigger is scrolled out of view). It receives one parameter
cf. static-addEventListener | GSAP | Docs & Learning

CSS アニメーションの時間を測って即コールバックをするなら onEnter を使いコールバック内で setTimeout で CSS アニメーションの時間分待てば良さそう 厳密さがあまり必要でないなら、onLeave を使う方が無駄にタイマーを使わずに済むので効率的だと思う

import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

const targets = document.querySelectorAll('.js-fadeUp');
targets.forEach((target, index) => {
  ScrollTrigger.create({
    trigger: target,
    id: `fadeUpContent-${index + 1}`,
    start: 'top-=100 center',
    end: 'bottom top',
    toggleClass: { targets: target, className: 'fade-in' },
    once: true,
    // css アニメーションを待って実行させる場合は onEnter を使う
    onEnter: (self) => {
      // self は globalThis.ScrollTrigger 型
      const target = self.vars.trigger; // gsap.DOMTarget | undefined
      // ⚠ HTMLElement 型として扱わないと target.classList が type error になる
      if (!target || target instanceof HTMLElement !== true) {
        return;
      }
      // css animation が終わるまで待つ
      window.setTimeout(() => {
        target.classList.add('fade-in-complete');
      }, 600);
    },
    // css アニメーションを特に考慮しないなら onLeave で十分
    onLeave: (self) => {
      const target = self.vars.trigger;
      if (!target || target instanceof HTMLElement !== true) {
        return;
      }
      target.classList.add('leaved');
    }
  });
});

ポイントとしては self.vars.trigger で取得できる、アニメーションのターゲット要素が gsap.DOMTarget | undefined 型なので classList などを使いたい場合は target instanceof HTMLElement で type guard してあげる必要がある部分

Sample

See the Pen GSAP CSS animation callback by KIKIKI (@kikiki_kiki) on CodePen.

⚠ onLeave はスクロールの高さが足りないと発火しない場合がある

なんとなくな理解だけど、GSAP の ScrollTrigger を使って CSS アニメーション発火した後にコールバックで処理ができるようになった!

おわり


[参考]

WordPress 管理画面の favicon と ツールバーのアイコンをロゴにしたい

⛳️ Goals

WordPress で作ったサイトの管理画面にログインしてる時、とのサイトだっけ?ってならないように管理画面の favicon と ツールバーの home アイコンをロゴに変更したい (納品するサイトの場合、こういうちょっとした部分で評価高くなりますし)

Wordpress Admin

Versions

WordPress 6.7
PHP 8

1. favicon の変更

ログイン画面の head に何かを出力できる login_head アクションと、管理画面の head に何かを出力できる admin_head アクションを使って、favicon の HTML を出力すれば OK

function.php

<?php
add_action('login_head', 'my_custom_admin_head');
add_action('admin_head', 'my_custom_admin_head');
function my_custom_admin_head() {
  $template_uri = get_template_directory_uri();
  $favicon = esc_url( $template_uri . 'favicon.svg');
  echo '<link rel="icon" type="image/svg+xml" href="' .  $favicon . '">';
}

2. ツールバーのアイコンの変更

管理画面に CSS を追加して、ツールバーのアイコンをロゴに変更する WordPress の用意しているアイコンを使う場合は content を変更すれば良いが、今回は自前で用意した SVG のロゴを使用する 先の関数内で CSS を出力するようにすればOK

functions.php

<?php
add_action('login_head', 'my_custom_admin_head');
add_action('admin_head', 'my_custom_admin_head');
function my_custom_admin_head() {
  $template_uri = get_template_directory_uri();
  // favicon を追加
  $favicon = esc_url( $template_uri . 'favicon.svg');
  echo '<link rel="icon" type="image/svg+xml" href="' .  $favicon . '">';
  
  // ツールバーのアイコンをロゴに変更$style = <<< EOM
<style type="text/css">
.wp-admin #wpadminbar #wp-admin-bar-site-name > .ab-item:before {
  content: " ";
  // WordPress の CSS を打ち消すために !important が必要
  background-image: url({$template_uri}/images/logo.svg) !important;
  background-size: 20px 20px;
  background-position: center;
  background-repeat: no-repeat;
  width: 20px;
  height: 30px;
  top: 2px;
  padding: 0;
  box-sizing: border-box;
}
</style>
EOM;

  echo $style;
}

変更したい箇所のセレクタを拾ってきて、background-image でロゴ画像を表示させるようにするだけ
WordPress デフォルトの CSS で !important が使われている場合があるので、セレクタの詳細度で上書きできるように CSS ちからが少し必要になる

これで favicon と ツールバーのアイコンを ロゴに変更することができた!

WordPress 管理画面の favicon と ツールバーのアイコンをロゴに変更

📝 WordPress のアイコンを使う方法

#wpadminbar #wp-admin-bar-wp-logo > .ab-item .ab-icon:before {
  content: '\f120';
}

対象のとなるセレクタの content に使用したいアイコンのコードを指定すれば良い
アイコンのコードは Dashicons | Developer.WordPress.org から取得できる

おわり


[参考]

😇 Webpack production のビルド時に Maximum call stack size exceeded エラーが発生する

Webpack を使って SCSS をコンパイルしているプロジェクトで久々に build したら下記のようなエラーが発生した

$ npx webpack --config ./webpack.config.js --mode production
…
webpack-fix-style-only-entries: removing js from style only module: js/css.js
(node:53965) [DEP_WEBPACK_CHUNK_HAS_ENTRY_MODULE] DeprecationWarning: Chunk.hasEntryModule: Use new ChunkGraph API
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:53965) [DEP_WEBPACK_CHUNK_ENTRY_MODULE] DeprecationWarning: Chunk.entryModule: Use new ChunkGraph API
(node:53965) [DEP_WEBPACK_MODULE_INDEX] DeprecationWarning: Module.index: Use new ModuleGraph API
...
ERROR in [entry] [initial] js/main.js
Maximum call stack size exceeded
RangeError: Maximum call stack size exceeded
    at ModuleGraph.getModuleGraphForModule (/path/to/node_modules/webpack/lib/ModuleGraph.js:842:32)
…
webpack 5.93.0 compiled with 1 error in 3219 ms

なんでや工藤…

version
  • webpack ^5.93.0

webpack.config

参考

// plugins
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
const CompressionPlugin = require('compression-webpack-plugin');
// rules
const tsRules = require('./ts.rules');
const cssRules = require('./css.rules');

module.exports = (env, argv) => {
  const IS_PRODUCTION = argv.mode === 'production';

  return {
    mode: IS_PRODUCTION ? 'production' : 'development',
    devtool: !IS_PRODUCTION ? 'eval-source-map' : false, // https://webpack.js.org/configuration/devtool/
    entry: {
      main: `${DIR}/src/main.ts`,
      css: `${DIR}/scss/styles.scss`,
    },
    output: {
      path: OUTPUT_DIR,
      filename: 'js/[name].js',
    },
    plugins: [
      new FixStyleOnlyEntriesPlugin(),  // /js/css.js を出力しない
      IS_PRODUCTION &&
        new CompressionPlugin({
          test: /\.(css|scss|js)$/,
          algorithm: 'gzip',
          filename: '[path][base].gz',
        }),  // gzip 化 production mode 時のみ
      new MiniCssExtractPlugin({ filename: 'css/styles.css' }),  // `${OUTPUT_DIR}/css/styles.css` として出力
    ].filter(Boolean),
    resolve: { extensions: ['.ts', '.js'] },
    module: { rules: [ tsRules(), cssRules() ] },
  };
};

webpack-fix-style-only-entries プラグインが原因だった

CSS を webpack で build した際に空の css.js ファイルが作られてしまうのを防ぐためのプラグインが production mode でビルドした際のみエラーになっていたっのが原因だった

⚠️ The current package version is not compatible with webpack 5. There is a fork here that is compatible: https://github.com/webdiscus/webpack-remove-empty-scripts
cf. GitHub fqborges/webpack-fix-style-only-entries

Webpack 5 に適合してないので、Fork して作られた webpack-remove-empty-scripts を使ってくれとのこと。
※ webpack-remove-empty-scripts は webpack 5 に適合していているが、webpack 4 には適合して無く 4 の場合は引き続き webpack-fix-style-only-entries を使うのが良さそう

✅️ webpack-fix-style-only-entries を webpack-remove-empty-scripts に置き換える (webpack 5)

単純にプラグインを置き換えるだけで、同じ用に動作する

$ npm uninstall webpack-fix-style-only-entries
$ npm i -D webpack-remove-empty-scripts
webpack.config を修正する
// plugins
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
+ const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
const CompressionPlugin = require('compression-webpack-plugin');
…
module.exports = (env, argv) => {
  const IS_PRODUCTION = argv.mode === 'production';

  return {
    mode: IS_PRODUCTION ? 'production' : 'development',
    ...
    },
    plugins: [
-     new FixStyleOnlyEntriesPlugin(),  // /js/css.js を出力しない
+     new RemoveEmptyScriptsPlugin(),
      IS_PRODUCTION &&
        new CompressionPlugin({

プラグインを変更して production mode でのビルド (npx webpack --config ./webpack.config.js --mode production) がエラーにならず完了すれば OK

無事動作するようになってよかった
おわり ദ്ദിᐢ- ̫-ᐢ₎


[参考]