Humanity

Edit the world by your favorite way

SVG のパス文字列の t (0 <= t <= 1) 時点での座標と角度を求めたい

どうにかする = 「前の制御点などを見てスムーズな曲線を描く」の意

参考記事

Promise のコンストラクタに渡した async function 内で throw しても rejected な Promise は作られない(常に fulfilled になる)

何を当たり前のことをと思うかもしれませんが、勘違いしてたので恥を晒しておきます。

const result = new Promise(async function executor(resolve, reject) {
  readFile('foo.txt', function(err, result) {
    if (err) {
      reject(err);
    } else {
      resolve(result);
    }
  });
});

(説明のために arrow function を function にして名前を付けているので、元のコードそのままではありません)

Promise のコンストラクタに async function を渡してます。 まぁ普段こういう風には自分も書かない *1 ので違和感はありましたが、こう書いた場合何が問題になるか分かりませんでした。

  • If a Promise executor function is using await, this is usually a sign that it is not actually necessary to use the new Promise constructor, or the scope of the new Promise constructor can be reduced.

(私訳) executor 関数が await を使う場合、大体 new Promise コンストラクタを使用する必要はないというサインです。もしくは new Promise コンストラクタのスコープを減らせる可能性があります。

これは分かります。 Promise は Promise のまま扱えばよくて、以下のように await で値を取り出す必要はないということです(このコードも引用です)。

const result = new Promise(async (resolve, reject) => {
  resolve(await foo);
});

は以下のように書けます。

const result = Promise.resolve(foo);

If an async executor function throws an error, the error will be lost and won’t cause the newly-constructed Promise to reject. This could make it difficult to debug and handle some errors.

(私訳) もし async の executor 関数がエラーを throw した場合、エラーは失われ新しく作られた Promise が reject することはありません。これはデバッグとエラー処理を難しくします。

ただこれがよく分かりませんでした。 Promise のコンストラクタに指定したコールバック内で throw したら reject() 呼んだのと同じ結果にならない?と思ってて、それは正しかったのですが、 結果から言うと今回は reject() 呼んだのと同じ結果になりません。throw されないからです。

なぜなら async function 内で throw した場合は rejected な Promise が返るからです。 つまり簡単に言ってしまうと Promise のコンストラクタでは、指定したコールバックの返り値は見ておらず、エラーが起こったら rejected とする。 それ以外はコールバックに渡した resolve()reject() が呼ばれるのを待つ、って事ですね。

よって async function 内で throw した場合、いつまでも resolve()reject() が呼ばれるのを待つ事になります。

…が実際はいつまでも待つ事にはなりません。 どういう風に検知してるのか分かってないですが、 resolve()reject() も未来永劫呼ばれないと Promise が判断すると unhandled rejection warning という警告が JavaScript 処理系で出ます。 ブラウザが unhandled rejection warning が発生した時にコンソールに出すかは実装依存で、 Node.js の場合は現在はエラー標準出力にメッセージが出ます。

// hello.js
Promise.reject(42);
console.log('hello');
$ node hello.js
hello
(node:31026) UnhandledPromiseRejectionWarning: 42
(node:31026) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:31026) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

警告にも書いてある通り、将来は Node.js プロセスごと異常終了するらしいのでちゃんと処理しましょう。

以下のようにして unhandled rejection を処理する事もできます。 エラーログに吐いたりとか、あるいは異常終了するのが正解な場合もあるかもしれません。

// Node.js の場合
process.on('unhandledRejection', (reason, promise) => {
  console.log('unhandled rejection: promise =', promise, ', reason =', reason);
});

// ブラウザの場合 (引用元: https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection)
window.addEventListener("unhandledrejection", function (event) {
  console.warn("WARNING: Unhandled promise rejection. Shame on you! Reason: "
               + event.reason);
});

詳しく

「コンストラクタに指定したコールバック内で throw した場合、Promise はそれを catch し reject() を呼んだ場合と同じ扱いとする」挙動に関してもうちょっと詳しく調べてみました。 この動作に関しては ECMAScript 25.4.3.1 Promise の項の 10 で規定されています(強調は自分)。

  1. If NewTarget is undefined, throw a TypeError exception.
    • new 演算子付きで呼び出されてなかったら TypeError
  2. If IsCallable(executor) is false, throw a TypeError exception.
    • function など呼び出し可能なオブジェクトでなかったら TypeError
  3. Let promise be OrdinaryCreateFromConstructor(NewTarget, "%PromisePrototype%", «‍[[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]]» ).
    • prototype プロパティから Promise オブジェクトを作成して promise にセット
  4. ReturnIfAbrupt(promise).
    • promise が abrupt completion なら return
  5. Set promise's [[PromiseState]] internal slot to "pending".
    • 内部スロット [[PromiseState]] を "pending" にセット
  6. Set promise's [[PromiseFulfillReactions]] internal slot to a new empty List.
    • 内部スロット [[PromiseFulfillReactions]] を空のリストにセット
  7. Set promise's [[PromiseRejectReactions]] internal slot to a new empty List.
    • 内部スロット [[PromiseRejectReactions]] を空のリストにセット
  8. Let resolvingFunctions be CreateResolvingFunctions(promise).
    • resolvingFunctions を CreateResolvingFunctions(promise) にする
  9. Let completion be Call(executor, undefined, «resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]]»).
    • executor を呼び出し(this = undefined, 第1引数 = resolvingFunctions.[[Resolve]], 第2引数 = resolvingFunctions.[[Reject]])、completion をその結果とする
  10. If completion is an abrupt completion, then
    • もし completion が abrupt completion なら、その時は
    • Let status be Call(resolvingFunctions.[[Reject]], undefined, «completion.[[value]]»).
      • resolvingFunctions.[[Reject]] を呼び出し(this = undefined, 第1引数 = completion.[[value]])、status をその結果とする
    • ReturnIfAbrupt(status).
      • status が abrupt completion なら return
  11. Return promise.
    • promise を返す

今回は 9 で executor 関数が rejected 状態の Promise を返した場合、10 でどのような扱いになるかが知りたいのですが、 その前にまず abrupt completion や ReturnIfAbrupt() といったものが何者なのかを知る必要があります。

まず abrupt completion とは [[type]] が normal 以外の Completion Record です。 Completion Record とは

The Completion type is a Record used to explain the runtime propagation of values and control flow such as the behaviour of statements (break, continue, return and throw) that perform nonlocal transfers of control.

Field Value Meaning
[[type]] One of normal, break, continue, return, or throw The type of completion that occurred.
[[value]] any ECMAScript language value or empty The value that was produced.
[[target]] any ECMAScript string or empty The target label for directed control transfers.

record の名の通り ECMAScript 仕様の中で JavaScript の値を入れる入れ物的な扱いをされてるようです(雑)。 そして [[type]] のそれぞれの値はおそらく皆さんの想像通りです。

今回は executor 関数が rejected 状態の Promise を返した場合どうなるかが知りたいので、 まず 9 の Call()` の結果がどういうオブジェクトになるかを調べます。 以下がCall()` の処理内容です。

Call(F, V, [argumentsList])

  1. ReturnIfAbrupt(F).
  2. If argumentsList was not passed, let argumentsList be a new empty List.
  3. If IsCallable(F) is false, throw a TypeError exception.
  4. Return F.[[Call]](V, argumentsList).

abrupt completion になる可能性があるのは 1,3,4 の場合のみです。 ReturnIfAbrupt() は引数の値が abrupt completion なら return する関数です。 F は executor 関数ですが、実は元の Promise のコンストラクタで 2. If IsCallable(executor) is false, throw a TypeError exception. とすでにチェックしています。 IsCallable()

  1. ReturnIfAbrupt(argument).
  2. If Type(argument) is not Object, return false.
  3. If argument has a [[Call]] internal method, return true.
  4. Return false.

のように abrupt completion かもチェックしているので、1,3 のチェックに引っかかる事はここではありません。 そして 4. Return F.[[Call]](V, argumentsList). により executor 関数の評価結果が返されますが、 async function は常に Promise を返すので、結果無事 Promise のコンストラクタに [[type]] = normal な Completion Record が返され、11. Return promise. が実行されます。 つまり reject() 関数が実行されることはありません。

まとめ

ようするに Promise 内で resolve も reject も使われず unhandled rejection warning になる可能性があるので、 Promise コンストラクタに指定するコールバックに async つけるのやめろって話でした。

途中から分かり切った事に対して半ば意地で仕様をしつこく調べてましたが、仕様の読み方とかが分かったので良かったです。

*1:Promise を返す関数を書いても、わざわざ Promise のコンストラクタに与える関数だけを切り出して Promise に渡したりはしないでしょう

CSS3 アニメーションで猿 (3)

過去記事はこちらからどうぞ。 トップの Vim アイコンをホバーして「Click Me!」をクリックでもいけます。

猿って何?

まず何のことを言ってるのか分からないと思うので、 過去記事を見てもらうか、この動画で概要を把握できます。


CSS3 アニメーションで猿

Vim のアイコンを1回クリックすると上に飛び立って、2回目クリックするとスゥーッと降りてくるというのを実現してみた、という事です。 今回はこれをどうやって実現したか、3回目にもなるのでちょっとだけちゃんと解説してみようと思います。

ソースコード

とりあえず HTML と CSS を冒頭に貼っておきます。

「デザイン設定 → ヘッダ → タイトル下」に以下の HTML を追加。

<div class='vim-icon-parent'>
  <details open='init'></details>
  <div class='zimbu'></div>
  <div class='vim-icon'></div>
  <div class="vim-icon-clickme"><a href='/search?q=CSS3+%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E7%8C%BF'>Click Me!</a></div>
</div>

「デザイン設定 → デザイン CSS」に以下の CSS を追加してます。 自分のはてなフォトライフに上げた画像を直リンクしてるので、 どうしても使いたい方は自分のフォトライフに上げるなりなんなりして下さい (気まぐれに画像消す可能性もあるので保証できない)。 まぁいないと思いますが。

/* 猿が下降する */
@keyframes saru-ga-modoru {
  0% {
    transform: translateY(-999px);
  }
}

/* 猿が上昇する */
@keyframes saru-ga-ugoku {
  0% {
    transform: translateY(0);
  }
  10% {
    transform: translateY(100px);
  }
  50% {
    transform: translateY(-300px);
  }
  60% {
    transform: translateY(-250px);
  }
  70% {
    transform: translateY(-300px);
  }
  80% {
    transform: translateY(-250px);
  }
  100% {
    transform: translateY(-999px);
  }
}

/* 透明にして .vim-icon と重ねる (.vim-icon と同じサイズ) */
.vim-icon-parent details {
  opacity: 0;
  position: absolute;
  left: 0;
  top: 0;
  width: 24px;
  height: 24px;
}
.vim-icon-parent .zimbu {
  will-change: transform;
}
/* 1. 初期表示 */
.vim-icon-parent details[open="init"] + .zimbu {
  animation: none;
}
/* 2. 上昇 */
.vim-icon-parent details:not([open]) + .zimbu {
  animation: saru-ga-ugoku 2.0s cubic-bezier(0,1,1,0) forwards;
}
/* 3. 下降 */
.vim-icon-parent details[open=""] + .zimbu {
  animation: saru-ga-modoru 2.0s ease-out forwards;
}

.vim-icon-parent {
  height: 30px;
  position: relative;
}
.vim-icon {
  display: inline-block;
  vertical-align: middle;
  width: 24px;
  height: 24px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507085743.png");
}
.zimbu {
  width: 394px;
  height: 236px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507093322.png");
}

.vim-icon-parent .zimbu {
  position: fixed;
  left: 0;
  bottom: 0;
  z-index: 99;
}
.vim-icon-parent:not(:hover) .zimbu {
  transform: translateY(240px);
}
.vim-icon-parent:hover .zimbu {
  transform: translateY(0);
}

.vim-icon-parent .vim-icon-clickme {
  position: absolute;
  left: 30px;
  display: inline-block;
  visibility: hidden;
}
.vim-icon-parent .vim-icon-clickme a {
  text-decoration: none;
}
.vim-icon-parent:hover .vim-icon-clickme {
  visibility: visible;
  border: 1px solid white;
  border-radius: 5px;
  padding: 5px 10px;
}

どうやって実現しているか

<details> 要素 + CSS アニメーションです。 前は <details> の代わりに <input type='checkbox'> を使ってたりしてたのですが、後述する理由でやめました。

前提知識

まず前提知識から。

<details> は例えばこういうやつです。

<details>
  <summary>サマリー</summary>
  本文
</details>

サマリー 本文

クリックしてみると開きます。 このように <details> は開閉します。 しかも開閉状態を DOM 上の open 属性で記録するため、CSS から属性セレクタで指定できるのです。 どういう事かというと、

閉じてる時

<details>
  <!-- 省略 -->
</details>

開いてる時

<details open>
  <!-- 省略 -->
</details>

<details open> のように開いてる時は open 属性が付くという事です。 属性が変わるという事は

details[open] {
  /* 開いてる時のスタイル */
}
details:not([open]) {
  /* 閉じてる時のスタイル */
}

のように CSS で指定できるという事です。 このようなタグは HTML と CSS だけで状態を持った動きを実現するのに使えます。 似たようなタグとして <input type='checkbox'> があり、 これも :checked, :not(:checked) を使えばそれぞれ指定できます。 つまりプログラムで言えば <details><input type='checkbox'> を使うのは boolean 変数1つ宣言するのと似たようなものです (=1bit の情報を保存できる)。

…とこれまで思ってたんですが、 <details> 要素に関しては 1bit ではなく3通りの状態を保存できました。 どういう事かというと、 <details open='init'></details>open 属性に何か空文字以外を指定しておくとクリックする度以下の様に変わります。

  1. <details open='init'>
  2. <details>
  3. <details open>

2 移行クリックするたび 2,3 の繰り返しです。 つまり初期表示の時だけ違う状態にする事ができます。 これが今回必要な要素でした。

これで CSS でそれぞれ指定できます。

details[open="init"] {
  /* 1. 初期表示 */
}
details:not([open]) {
  /* 2. 閉じてる時のスタイル */
}
details[open=""] {
  /* 3. 開いてる時のスタイル */
}

<input type='checkbox'> の indeterminate *1 を使って同じ事ができるかと思ったのですが、以下の理由からダメでした。

  • <input type='checkbox' indeterminate> と書けば indeterminate 状態に…できなかった
  • <details> 要素と違って DOM の属性が動的に変わらなかった

ソースコード解説

前提知識を解説した所で本編です。

まず <details>Vim のアイコンを重ねて、Vim のアイコンをクリックしたら <details> もクリックしたことにします。

<div class='vim-icon-parent'>
  <details open='init'></details>
  <!-- 省略 -->
  <div class='vim-icon'></div>
  <!-- 省略 -->
</div>
.vim-icon-parent {
  height: 30px;
  position: relative;
}
.vim-icon-parent .vim-icon {
  display: inline-block;
  vertical-align: middle;
  width: 24px;
  height: 24px;
  background-image: url("//cdn-ak.f.st-hatena.com/images/fotolife/t/tyru/20160507/20160507085743.png");
}
.vim-icon-parent details {
  opacity: 0;               /* 透明にして見えなくする (ただ opacity なのでクリック判定は残る) */
  position: absolute;  /* .vim-icon と重ねる */
  left: 0;
  top: 0;
  width: 24px;    /* .vim-icon と同じ幅 */
  height: 24px;   /* .vim-icon と同じ高さ */
}

あとは CSS アニメーションを適用するだけです。

  1. 初期表示 (<details open='init'>)
  2. 上昇する (<details>)
  3. 下降する (<details open>)
/* 1. 初期表示 */
.vim-icon-parent details[open="init"] + .zimbu {
  animation: none;
}
/* 2. 上昇 */
.vim-icon-parent details:not([open]) + .zimbu {
  animation: saru-ga-ugoku 2.0s cubic-bezier(0,1,1,0) forwards;
}
/* 3. 下降 */
.vim-icon-parent details[open=""] + .zimbu {
  animation: saru-ga-modoru 2.0s ease-out forwards;
}

上昇の時に saru-ga-ugoku, 下降の時に saru-ga-modoru を適用してそれぞれのアニメーションを実行します。 forwards (animation-fill-mode) は @yui540 さんの動画で知ったやつです。ありがとうございます。

www.youtube.com

おまけ:より効率的な CSS アニメーションを書く

.vim-icon-parent .zimbu {
  will-change: transform;
}

will-change プロパティで変わる可能性のあるプロパティを指定すると GPU 使ってくれて速くなったりするらしい。 正直体感はできてないですが… あと position: absoluteleft 使うより transform プロパティで移動した方が他の DOM 要素の位置を再計算する必要がない (ただその DOM 要素だけを移動・変形するだけ) ため速いとかなんとか。 詳しくは以下の記事とか、その他記事が沢山あるので検索して下さい。

感想

当初はちゃんとした記事を書くつもりはなかったですが、色々整理する意味でも書けて良かったと思います。

*1:専用の疑似要素あったんですね…知らなかった…