SVG のパス文字列の t (0 <= t <= 1) 時点での座標と角度を求めたい
Snap.svg 見てみたら直線や曲線を三次ベジェ曲線に変換する Snap.path.toCubic() という関数があり、さらに Snap.path.findDotsAtSegment() という t 時点での XY 座標、角度なんかを含んだ情報を求められる関数があった。Snap.svg すごい。 https://t.co/WcIlUzKmmJ
— tyru🍆 (@_tyru_) August 7, 2018
SVG のパスだけだとコマンドの継ぎ目で曲線が荒いとカメラの角度がいきなりガクッと変わったりする。どうにかするには Catmull-Rom 曲線というのが良いらしくて、Snap.svg はこれに R コマンドを割り当てた独自パス文字列をパースできる。
— tyru🍆 (@_tyru_) August 7, 2018
どうにかする = 「前の制御点などを見てスムーズな曲線を描く」の意
Catmull-Rom 曲線を Canvas 上で実装したデモ
— tyru🍆 (@_tyru_) August 7, 2018
デモ: https://t.co/E33xtR44La
記事: https://t.co/RjkzvuO2N0
参考記事
Promise のコンストラクタに渡した async function 内で throw しても rejected な Promise は作られない(常に fulfilled になる)
何を当たり前のことをと思うかもしれませんが、勘違いしてたので恥を晒しておきます。
こんな使い方する人いるのか "no-async-promise-executor - Rules - ESLint - Pluggable JavaScript linter" https://t.co/BJy6aqMYQ0
— azu (@azu_re) 2018年8月4日
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 thenew Promise
constructor, or the scope of thenew 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 で規定されています(強調は自分)。
- If
NewTarget
is undefined, throw aTypeError
exception.new
演算子付きで呼び出されてなかったら TypeError
- If
IsCallable(executor)
is false, throw aTypeError
exception.- function など呼び出し可能なオブジェクトでなかったら TypeError
- Let
promise
beOrdinaryCreateFromConstructor(NewTarget, "%PromisePrototype%", «[[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]]» )
.- prototype プロパティから Promise オブジェクトを作成して
promise
にセット
- prototype プロパティから Promise オブジェクトを作成して
ReturnIfAbrupt(promise)
.- promise が abrupt completion なら return
- Set
promise
's[[PromiseState]]
internal slot to "pending".- 内部スロット
[[PromiseState]]
を "pending" にセット
- 内部スロット
- Set
promise
's[[PromiseFulfillReactions]]
internal slot to a new empty List.- 内部スロット
[[PromiseFulfillReactions]]
を空のリストにセット
- 内部スロット
- Set
promise
's[[PromiseRejectReactions]]
internal slot to a new empty List.- 内部スロット
[[PromiseRejectReactions]]
を空のリストにセット
- 内部スロット
- Let
resolvingFunctions
beCreateResolvingFunctions(promise)
.- resolvingFunctions を CreateResolvingFunctions(promise) にする
- Let
completion
beCall(executor, undefined, «resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]]»)
.executor
を呼び出し(this =undefined
, 第1引数 =resolvingFunctions.[[Resolve]]
, 第2引数 =resolvingFunctions.[[Reject]]
)、completion
をその結果とする
- If
completion
is an abrupt completion, then- もし
completion
が abrupt completion なら、その時は - Let
status
beCall(resolvingFunctions.[[Reject]], undefined, «completion.[[value]]»)
.resolvingFunctions.[[Reject]]
を呼び出し(this =undefined
, 第1引数 =completion.[[value]]
)、status
をその結果とする
ReturnIfAbrupt(status)
.status
が abrupt completion なら return
- もし
- 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]]
のそれぞれの値はおそらく皆さんの想像通りです。
- throw すると引数が abrupt completion ならその値、もしくは
throw
になる(13.14.1 Runtime Semantics: Evaluation) - return すると引数が abrupt completion ならその値、もしくは
return
になる(13.10.1 Runtime Semantics: Evaluation) - break すると
break
になる(13.9.3 Runtime Semantics: Evaluation) - continue すると
continue
になる(13.8.3 Runtime Semantics: Evaluation)
今回は executor
関数が rejected 状態の Promise を返した場合どうなるかが知りたいので、
まず 9 の Call()` の結果がどういうオブジェクトになるかを調べます。
以下がCall()` の処理内容です。
Call(F, V, [argumentsList])
ReturnIfAbrupt(F)
.- If argumentsList was not passed, let argumentsList be a new empty List.
- If
IsCallable(F)
is false, throw a TypeError exception.- 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()
は
- ReturnIfAbrupt(argument).
- If Type(argument) is not Object, return false.
- If argument has a
[[Call]]
internal method, return true.- 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 つけるのやめろって話でした。
途中から分かり切った事に対して半ば意地で仕様をしつこく調べてましたが、仕様の読み方とかが分かったので良かったです。
CSS3 アニメーションで猿 (3)
過去記事はこちらからどうぞ。 トップの Vim アイコンをホバーして「Click Me!」をクリックでもいけます。
猿って何?
まず何のことを言ってるのか分からないと思うので、 過去記事を見てもらうか、この動画で概要を把握できます。
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
属性に何か空文字以外を指定しておくとクリックする度以下の様に変わります。
<details open='init'>
<details>
<details open>
2 移行クリックするたび 2,3 の繰り返しです。 つまり初期表示の時だけ違う状態にする事ができます。 これが今回必要な要素でした。
ただこれだけだと初期表示時もチェック付いてないんで猿が下りてくる事になる… pic.twitter.com/VqNenKLI82
— tyru🍆 (@_tyru_) July 25, 2018
これで 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 アニメーションを適用するだけです。
- 初期表示 (
<details open='init'>
) - 上昇する (
<details>
) - 下降する (
<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 さんの動画で知ったやつです。ありがとうございます。
おまけ:より効率的な CSS アニメーションを書く
.vim-icon-parent .zimbu { will-change: transform; }
will-change
プロパティで変わる可能性のあるプロパティを指定すると GPU 使ってくれて速くなったりするらしい。
正直体感はできてないですが…
あと position: absolute
と left
使うより transform
プロパティで移動した方が他の DOM 要素の位置を再計算する必要がない (ただその DOM 要素だけを移動・変形するだけ) ため速いとかなんとか。
詳しくは以下の記事とか、その他記事が沢山あるので検索して下さい。
- CSS will-changeプロパティについて知っておくべきこと | POSTD
- スムーズなアニメーションを実装するコツと仕組みを説明するよ。CPUとGPUを理解しハードウェアアクセラレーションを駆使するのだ!(Frontrend Advent Calendar 2013 – 06日目) | Ginpen.com
- Rendering Performance | Web Fundamentals | Google Developers
感想
当初はちゃんとした記事を書くつもりはなかったですが、色々整理する意味でも書けて良かったと思います。
*1:専用の疑似要素あったんですね…知らなかった…