{
	"version": "https://jsonfeed.org/version/1.1",
	"title": "Katashin .info",
	"language": "ja",
	"home_page_url": "https://katashin.info/",
	"feed_url": "https://katashin.info/feed/feed.json",
	"description": "実用的ですぐに開発に役立てることができる Web フロントエンドの話題を書いている Katashin の個人ブログです。",
	"author": {
		"name": "Katashin",
		"url": "https://bsky.app/profile/katashin.bsky.social"
	},
	"items": [
		{
			"id": "https://katashin.info/posts/touch-pointer-cancel/",
			"url": "https://katashin.info/posts/touch-pointer-cancel/",
			"title": "タッチデバイスで pointercancel イベントによるドラッグ中断を回避する方法",
			"content_html": "<p>高度なインタラクションを持つ UI には、画面上のオブジェクトをつまんで移動するような、ドラッグアンドドロップの実装があります。リスト項目の並べ替えや、ホワイトボードアプリでの自由な要素配置などがその代表例です。Pointer Events API（pointerdown、pointermove など）を使用することで、マウス、タッチ、ペンといった多様な入力デバイスに対応したドラッグアンドドロップ機能を実現できます。</p><p>スマートフォンのようなタッチデバイスでは画面のスクロールと競合してドラッグの処理がキャンセルされることがあります。ユーザーが要素をドラッグしようと画面に触れた際、ブラウザーがそのジェスチャーをスクロール操作と解釈すると、pointercancel イベントが発生してドラッグ処理が中断されます。この問題は、スクロール可能な領域内にドラッグ可能な要素を配置した場合に現れます。</p><p>以下のデモは、タッチデバイスでスクロールとドラッグが競合して pointercancel イベントが発生する様子を示しています。点線で囲まれた領域はスクロール可能な要素で、画面上部には最後に発生したポインターイベントの種類が表示されます。タッチデバイスで緑の四角形をドラッグしようとすると、ブラウザーがスクロールを優先し、pointercancel イベントが発生することを確認できます。</p><iframe title=\"スクロールと競合して pointercancel が発生してしまう例\" srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;body{position:absolute;inset:0;overflow:clip;display:flex;flex-direction:column;gap:8px;margin:0;padding:8px;font-family:sans-serif}p{margin:0}#scroller{flex:1 1 0;overflow:auto;border:1px dashed black}.content{position:relative;background:linear-gradient(to bottom, white, gray);height:600px}#rect{position:absolute;top:50px;left:80px;background:green;width:50px;height:50px}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;スクロールと競合して pointercancel が発生してしまう例&lt;/p&gt;&lt;div&gt;PointerEvent:&amp;nbsp;&lt;output id=&quot;output&quot;&gt;&lt;/output&gt;&lt;/div&gt;&lt;div id=&quot;scroller&quot;&gt;&lt;div class=&quot;content&quot;&gt;&lt;div id=&quot;rect&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;script&gt;var scroller=document.getElementById(&quot;scroller&quot;),output=document.getElementById(&quot;output&quot;);function printOutput(t){output.textContent=t.type}scroller.onpointerdown=scroller.onpointermove=scroller.onpointerup=scroller.onpointercancel=printOutput&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:300px></iframe><details><summary>上記のデモのコード</summary><pre class=language-html tabindex=0><code class=language-html><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span><span class=\"token punctuation\">></span></span>PointerEvent:<span class=\"token entity named-entity\" title=&nbsp;>&amp;nbsp;</span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>output</span> <span class=\"token attr-name\">id</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>output<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>output</span><span class=\"token punctuation\">></span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">id</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>scroller<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">class</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>content<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>div</span> <span class=\"token attr-name\">id</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>rect<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>div</span><span class=\"token punctuation\">></span></span></code></pre><pre class=language-css tabindex=0><code class=language-css><span class=\"token selector\">body</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> absolute<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">inset</span><span class=\"token punctuation\">:</span> 0<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> clip<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">display</span><span class=\"token punctuation\">:</span> flex<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">flex-direction</span><span class=\"token punctuation\">:</span> column<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">gap</span><span class=\"token punctuation\">:</span> 8px<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">margin</span><span class=\"token punctuation\">:</span> 0<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">padding</span><span class=\"token punctuation\">:</span> 8px<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">font-family</span><span class=\"token punctuation\">:</span> sans-serif<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">p</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">margin</span><span class=\"token punctuation\">:</span> 0<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">#scroller</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">flex</span><span class=\"token punctuation\">:</span> 1 1 0<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">overflow</span><span class=\"token punctuation\">:</span> auto<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">border</span><span class=\"token punctuation\">:</span> 1px dashed black<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">.content</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> relative<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">background</span><span class=\"token punctuation\">:</span> <span class=\"token function\">linear-gradient</span><span class=\"token punctuation\">(</span>to bottom<span class=\"token punctuation\">,</span> white<span class=\"token punctuation\">,</span> gray<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n  <span class=\"token property\">height</span><span class=\"token punctuation\">:</span> 600px<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token selector\">#rect</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">position</span><span class=\"token punctuation\">:</span> absolute<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">top</span><span class=\"token punctuation\">:</span> 50px<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">left</span><span class=\"token punctuation\">:</span> 80px<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">background</span><span class=\"token punctuation\">:</span> green<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">width</span><span class=\"token punctuation\">:</span> 50px<span class=\"token punctuation\">;</span>\n  <span class=\"token property\">height</span><span class=\"token punctuation\">:</span> 50px<span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre><pre class=language-js tabindex=0><code class=language-js><span class=\"token keyword\">const</span> scroller <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">getElementById</span><span class=\"token punctuation\">(</span><span class=\"token string\">'scroller'</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> output <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">getElementById</span><span class=\"token punctuation\">(</span><span class=\"token string\">'output'</span><span class=\"token punctuation\">)</span>\n\nscroller<span class=\"token punctuation\">.</span>onpointerdown <span class=\"token operator\">=</span>\n  scroller<span class=\"token punctuation\">.</span>onpointermove <span class=\"token operator\">=</span>\n  scroller<span class=\"token punctuation\">.</span>onpointerup <span class=\"token operator\">=</span>\n  scroller<span class=\"token punctuation\">.</span>onpointercancel <span class=\"token operator\">=</span>\n    printOutput\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">printOutput</span><span class=\"token punctuation\">(</span><span class=\"token parameter\">event</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  output<span class=\"token punctuation\">.</span>textContent <span class=\"token operator\">=</span> event<span class=\"token punctuation\">.</span>type\n<span class=\"token punctuation\">}</span></code></pre></details><p>このようにドラッグがキャンセルされる現象の対策として、touch-action を指定する方法と、touch イベントリスナーで <code>preventDefault()</code> する方法があります。いずれもブラウザーのデフォルトタッチ動作を制御し、開発者が意図したインタラクションを実現するためのアプローチです。</p>",
			"date_published": "2025-07-07T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/implicit-type-conversion/",
			"url": "https://katashin.info/posts/implicit-type-conversion/",
			"title": "JavaScript の暗黙の型変換とそれを制御するメソッド",
			"content_html": "<p>JavaScript では演算子の使用時など、様々な場面で暗黙の型変換が行われますが、この挙動は開発者がカスタマイズできます。例えば、Date オブジェクトを比較演算子で比較できるのは、内部で自動的に数値（ミリ秒）へ変換されるためです。</p><pre class=language-javascript tabindex=0><code class=language-javascript><span class=\"token keyword\">const</span> date1 <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Date</span><span class=\"token punctuation\">(</span><span class=\"token string\">'2025-06-30'</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> date2 <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Date</span><span class=\"token punctuation\">(</span><span class=\"token string\">'2025-07-01'</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token comment\">// 内部的には valueOf() が呼ばれてミリ秒に変換される</span>\nconsole<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>date1 <span class=\"token operator\">&lt;</span> date2<span class=\"token punctuation\">)</span> <span class=\"token comment\">// true</span>\nconsole<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>date1 <span class=\"token operator\">></span> date2<span class=\"token punctuation\">)</span> <span class=\"token comment\">// false</span></code></pre>",
			"date_published": "2025-07-01T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/skyline-algorithm/",
			"url": "https://katashin.info/posts/skyline-algorithm/",
			"title": "Skyline アルゴリズムで実現するグリッドを超えた柔軟なレイアウト",
			"content_html": "<p>CSS Grid や Flexbox により、CSS だけで表現できるレイアウトの幅が広がりました。しかし、グリッドの枠を超える柔軟な UI は CSS のみでは実装が困難です。例えば、短いメモと画像を二次元に並べる以下の UI は CSS Grid や Flexbox だけでは表現できません。</p><p><picture><source type=image/avif srcset=\"https://katashin.info/img/gH7Tg_Wi4M-390.avif 390w\"><source type=image/webp srcset=\"https://katashin.info/img/gH7Tg_Wi4M-390.webp 390w\"><img src=https://katashin.info/img/gH7Tg_Wi4M-390.png alt=グリッドで表現できないレイアウト例。メモと画像が2次元配置され、特定の軸に縛られず余白を最小化する並びになっています。下部にメモ入力フォームがあります。 loading=lazy decoding=async width=390 height=481></picture></p><p>CSS Grid も Flexbox も行や列を定義し、その中に要素を配置するため、行や列にとらわれない UI の実装は困難です。よく使用される Masonry レイアウトは Grid より自由度が高いものの、列か行の片方を厳密に並べるため柔軟性に欠けます。</p><p>本記事では Skyline アルゴリズムがこのような柔軟な配置を実現できることを示します。基本的なアルゴリズムを紹介した後、より多くの空きスペースを削減し、より美しいレイアウトを作るための拡張も解説します。</p>",
			"date_published": "2025-06-23T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/vue-3.4-computed/",
			"url": "https://katashin.info/posts/vue-3.4-computed/",
			"title": "Vue 3.4 で変わった computed の再計算アルゴリズム – 処理順序の逆転による最適化",
			"content_html": "<p>Vue 3.4 ではリアクティビティシステムの根幹が改善されました。特に注目すべきは<a href=https://blog.vuejs.org/posts/vue-3-4#more-efficient-reactivity-system><code>computed</code> の挙動変更</a>で、不要な再計算や再レンダリングが大幅に削減され、パフォーマンスが向上しています。</p><p>本記事は<a href=https://katashin.info/posts/eager-computed/ >以前の記事で解説した従来の computed の課題と computedEager の解説</a>を踏まえ、Vue 3.4 での <code>computed</code> の内部実装に焦点を当て、効率化の仕組みを詳しく解説します。</p>",
			"date_published": "2025-06-16T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/position-sticky-issue/",
			"url": "https://katashin.info/posts/position-sticky-issue/",
			"title": "position: sticky が効かない原因を CSS 仕様から読み解く – スクロールコンテナーと包含ブロック",
			"content_html": "<p><code>position: sticky</code> は、スクロール時に要素を画面上に固定表示する CSS プロパティです。ヘッダーやナビゲーションバー、目次など、スクロール中も常に表示しておきたい要素の実装によく使われます。</p><p>しかし、<code>position: sticky</code> を指定しても要素が固定されない問題に遭遇することがあります。この問題を解決するには、CSS の仕様における要素の配置とスクロールの挙動を理解する必要があります。</p><p>この記事では、<code>position: sticky</code> が動作しない主な原因とその解決方法を解説します。</p>",
			"date_published": "2025-06-09T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/wkwebview-service-worker/",
			"url": "https://katashin.info/posts/wkwebview-service-worker/",
			"title": "ハイブリッドアプリの WKWebView で Service Worker を使用する方法",
			"content_html": "<p>ハイブリッドアプリは Web 技術で開発したコンテンツをネイティブアプリの WebView で動作させることで、クロスプラットフォーム対応を実現する開発手法です。しかし、単に Web アプリを表示するだけでなく、ネイティブ側の設定が必要になる場合があります。たとえば iOS の WKWebView では、デフォルトで Service Worker が動作しません。この記事では、WKWebView で Service Worker を有効にする設定方法を解説します。</p>",
			"date_published": "2025-06-02T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/vue-updated-trigger/",
			"url": "https://katashin.info/posts/vue-updated-trigger/",
			"title": "Vue テンプレートが再描画されても onUpdated ライフサイクルフックが実行されないケース",
			"content_html": "<p>Vue コンポーネントで DOM 要素の更新を検知したい場合、<a href=https://ja.vuejs.org/api/composition-api-lifecycle.html#onupdated>onUpdated ライフサイクルフック</a>が使えます。例えば以下のコンポーネントでは、ボタンをクリックするたびに <code>count</code> の値が更新され、その値が表示されている DOM 要素も更新されます。その後、<code>onUpdated</code> に登録されたコールバックが実行され、コンソールに <code>Updated</code> と出力されます。</p><pre class=language-vue tabindex=0><code class=language-vue><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>script</span> <span class=\"token attr-name\">setup</span><span class=\"token punctuation\">></span></span><span class=\"token script\"><span class=\"token language-javascript\">\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> ref<span class=\"token punctuation\">,</span> onUpdated <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'vue'</span>\n\n<span class=\"token keyword\">const</span> count <span class=\"token operator\">=</span> <span class=\"token function\">ref</span><span class=\"token punctuation\">(</span><span class=\"token number\">0</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token function\">onUpdated</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// DOM が更新されたあとに呼ばれる</span>\n  console<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token string\">'Updated'</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n</span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>script</span><span class=\"token punctuation\">></span></span>\n\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>template</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">@click</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>count++<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>{{ count }}<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>template</span><span class=\"token punctuation\">></span></span>\n</code></pre><p>似たライフサイクルフックに <a href=https://ja.vuejs.org/api/composition-api-lifecycle#onbeforeupdate>onBeforeUpdate</a> もあります。こちらは DOM の更新直前に呼ばれるコールバックです。</p><p>この onUpdated や onBeforeUpdate が特定のケースでは実行されない場合があります。この記事ではその現象がどういった時に、なぜ起きるのかを解説し、そのようなケースではどういったコードを書けばよいかを提案します。</p>",
			"date_published": "2025-05-26T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/firebase-emulator/",
			"url": "https://katashin.info/posts/firebase-emulator/",
			"title": "Firebase をローカルで使うエミュレーターの起動、シードデータの設定、データの永続化方法",
			"content_html": "<p>ローカルに自分だけが使える Firebase を起動して、チームメンバーに影響を与えず色々と実験したい場合はありませんか？すでにそういった環境を用意している人でも、エミュレーターを起動するたびにデータを入れ直すことを煩わしく思ってる人もいるのではないでしょうか？</p><p>本記事では、Firebase をローカルで使うためのエミュレーターの起動方法から、エミュレーターへのシードデータのインポート方法や、終了後のデータの永続化方法を解説します。</p>",
			"date_published": "2023-11-28T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/ios-menu-interaction/",
			"url": "https://katashin.info/posts/ios-menu-interaction/",
			"title": "長押しドラッグを活用した iOS のポップオーバーメニューインタラクションを Vue.js で実装する",
			"content_html": "<p>iOS の UI は細かいインタラクションが作り込まれていて、使っていて快適です。例えば、iOS のポップオーバーメニューは、タップではなく長押しでもポップオーバーを開くことができ、そのまま指を離さずにメニュー項目に指を動かせば、その項目を選択することができます。以下のデモはその挙動を再現したものです。</p><iframe title=長押しドラッグで選択できるメニューのデモ srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;.popover-wrapper{position:relative}.popover{position:absolute;width:fit-content;height:fit-content}.popover:is(.v-enter-active, .v-leave-active){transition:0.3s cubic-bezier(0.2, 0, 0, 0.98)}.popover:is(.v-enter-from, .v-leave-to){scale:0;opacity:0}.popover-button{padding:8px 12px;width:100%;border:none;background:none;text-align:left}.popover-button:is(.active, :active){background-color:rgba(0, 0, 0, 0.1)}.wrapper{display:flex;justify-content:flex-end;font-family:sans-serif}.open-button{padding:8px 12px;border-radius:4px;background:none;border:none;font-weight:bold}.open-button:active{background-color:rgba(0, 0, 0, 0.1)}.menu{overflow:hidden;width:200px;background-color:#fff;border-radius:4px;box-shadow:0 0 5px 3px rgba(0, 0, 0, 0.05)}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;script type=&quot;importmap&quot;&gt;{\n    &quot;imports&quot;: {\n      &quot;vue&quot;: &quot;https://unpkg.com/vue@3.3.8/dist/vue.esm-browser.prod.js&quot;\n    }\n  }&lt;/script&gt;&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;&lt;script type=&quot;module&quot;&gt;import{createApp}from&quot;vue&quot;;import{createCommentVNode as _createCommentVNode2,toHandlers as _toHandlers,mergeProps as _mergeProps,createElementVNode as _createElementVNode2,createTextVNode as _createTextVNode,withCtx as _withCtx2,createVNode as _createVNode2,openBlock as _openBlock3,createElementBlock as _createElementBlock3}from&quot;vue&quot;;function awareLongTouch(){let e,t,o=!1,n=!1;document.documentElement.addEventListener(&quot;pointerdown&quot;,(function(a){o=n=!1,e=a,t=setTimeout((()=&gt;{o=!0;const e=new CustomEvent(&quot;longtouchstart&quot;,{bubbles:!0,cancelable:!0,detail:a});a.target.dispatchEvent(e)}),300)})),document.documentElement.addEventListener(&quot;pointermove&quot;,(function(a){if(o||n||!e)return;const r=a.clientX-e.clientX,c=a.clientY-e.clientY;Math.sqrt(r*r+c*c)&gt;10&amp;&amp;(n=!0,clearTimeout(t))})),document.documentElement.addEventListener(&quot;pointerup&quot;,(function(a){if(clearTimeout(t),o){const e=new CustomEvent(&quot;longtouchend&quot;,{bubbles:!0,cancelable:!0,detail:a});a.target.dispatchEvent(e)}o=n=!1,e=void 0})),document.documentElement.addEventListener(&quot;pointerover&quot;,(function(e){if(o){const t=new CustomEvent(&quot;longtouchenter&quot;,{bubbles:!1,cancelable:!0,detail:e});e.target.dispatchEvent(t)}})),document.documentElement.addEventListener(&quot;pointerout&quot;,(function(e){if(o){const t=new CustomEvent(&quot;longtouchleave&quot;,{bubbles:!1,cancelable:!0,detail:e});e.target.dispatchEvent(t)}})),document.documentElement.addEventListener(&quot;touchmove&quot;,(function(e){o&amp;&amp;e.preventDefault()}),{passive:!1})}import{createCommentVNode as _createCommentVNode,renderSlot as _renderSlot,vShow as _vShow,normalizeStyle as _normalizeStyle,createElementVNode as _createElementVNode,withDirectives as _withDirectives,Transition as _Transition,withCtx as _withCtx,createVNode as _createVNode,openBlock as _openBlock,createElementBlock as _createElementBlock}from&quot;vue&quot;;import{ref,computed}from&quot;vue&quot;;var _hoisted_1={class:&quot;popover-wrapper&quot;},Comp={props:{direction:String},setup(e){const t=e,o=ref(!1),n=computed((()=&gt;&quot;top-center&quot;===t.direction?{margin:&quot;auto&quot;,bottom:&quot;100%&quot;,left:&quot;-1000px&quot;,right:&quot;-1000px&quot;,transformOrigin:&quot;bottom center&quot;}:{top:&quot;100%&quot;,right:&quot;0&quot;,transformOrigin:&quot;top right&quot;}));function a(){o.value=!o.value}function r(e){e.target.releasePointerCapture(e.detail.pointerId),o.value=!0}return(e,t)=&gt;(_openBlock(),_createElementBlock(&quot;div&quot;,_hoisted_1,[_createCommentVNode(&quot;\\n      activator スロット\\n      ポップオーバーを開くボタンを差し込む。\\n      on プロパティで click と longtouchstart のリスナーを渡し、\\n      クリックと長押しのどちらでもポップオーバーが開くようにする。\\n    &quot;),_renderSlot(e.$slots,&quot;activator&quot;,{on:{click:a,longtouchstart:r}}),_createCommentVNode(&quot;\\n      デモのために簡単な実装にしているが、実際のアプリでは Teleport や\\n      popover 属性を使って、前面に配置した方がいい。\\n    &quot;),_createVNode(_Transition,{persisted:&quot;&quot;},{default:_withCtx((()=&gt;[_withDirectives(_createElementVNode(&quot;div&quot;,{class:&quot;popover&quot;,style:_normalizeStyle(n.value)},[_createCommentVNode(&quot;\\n          default スロット\\n          ポップオーバーの中身を差し込む\\n        &quot;),_renderSlot(e.$slots,&quot;default&quot;,{toggle:a})],4),[[_vShow,o.value]])])),_:3})]))}},Popover_default=Comp;import{renderSlot as _renderSlot2,normalizeClass as _normalizeClass,openBlock as _openBlock2,createElementBlock as _createElementBlock2}from&quot;vue&quot;;import{ref as ref2}from&quot;vue&quot;;var Comp2={emits:[&quot;click&quot;],setup(e,{emit:t}){const o=t,n=ref2(!1);function a(){n.value=!0}function r(){n.value=!1}function c(){n.value=!1,o(&quot;click&quot;)}return(e,t)=&gt;(_openBlock2(),_createElementBlock2(&quot;button&quot;,{type:&quot;button&quot;,class:_normalizeClass([&quot;popover-button&quot;,{active:n.value}]),onLongtouchenter:a,onLongtouchleave:r,onLongtouchend:c,onClick:t[0]||(t[0]=e=&gt;o(&quot;click&quot;))},[_renderSlot2(e.$slots,&quot;default&quot;)],34))}},PopoverButton_default=Comp2,_hoisted_12={class:&quot;wrapper&quot;},_hoisted_2={class:&quot;menu&quot;},Comp3={setup:e=&gt;(awareLongTouch(),(e,t)=&gt;(_openBlock3(),_createElementBlock3(&quot;div&quot;,_hoisted_12,[_createVNode2(Popover_default,null,{activator:_withCtx2((({on:e})=&gt;[_createCommentVNode2(&#39; v-on=&quot;on&quot; とすることで、クリックか長押しでポップオーバーが開く &#39;),_createElementVNode2(&quot;button&quot;,_mergeProps({type:&quot;button&quot;,class:&quot;open-button&quot;},_toHandlers(e,!0)),&quot;メニュー&quot;,16)])),default:_withCtx2((({toggle:e})=&gt;[_createElementVNode2(&quot;div&quot;,_hoisted_2,[_createCommentVNode2(&quot; 今回はクリックされたら閉じるだけ &quot;),_createVNode2(PopoverButton_default,{onClick:e},{default:_withCtx2((()=&gt;t[0]||(t[0]=[_createTextVNode(&quot;コピー&quot;)]))),_:2,__:[0]},1032,[&quot;onClick&quot;]),_createVNode2(PopoverButton_default,{onClick:e},{default:_withCtx2((()=&gt;t[1]||(t[1]=[_createTextVNode(&quot;ペースト&quot;)]))),_:2,__:[1]},1032,[&quot;onClick&quot;]),_createVNode2(PopoverButton_default,{onClick:e},{default:_withCtx2((()=&gt;t[2]||(t[2]=[_createTextVNode(&quot;削除&quot;)]))),_:2,__:[2]},1032,[&quot;onClick&quot;])])])),_:1})])))};createApp(Comp3).mount(&quot;#app&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=width:400px;height:250px></iframe><p>また、一部のアプリでは、メニュー項目を選択できる領域が拡大されていることがあり、項目に指が触れていなくても選択できるものもあります。例えば、以下のデモはポップオーバーで絵文字を選択できますが、指を絵文字の上まで動かさなくても、少し下に指を動かすだけで選択できます。これによって、指で選択したい項目が隠れるのを防げます。</p><iframe title=長押しドラッグでボタンの境界外にドラッグしても選択できるメニューのデモ srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;.popover-wrapper{position:relative}.popover{position:absolute;width:fit-content;height:fit-content}.popover:is(.v-enter-active, .v-leave-active){transition:0.3s cubic-bezier(0.2, 0, 0, 0.98)}.popover:is(.v-enter-from, .v-leave-to){scale:0;opacity:0}.popover-button{position:relative;padding:8px 12px;width:100%;background:none;border:none;text-align:left;transition:0.2s ease-in-out}.popover-button:is(.active, :active){scale:1.5}.drag-hit{position:absolute;inset:0}.wrapper{margin-top:100px;display:flex;justify-content:center;font-family:sans-serif}.open-button{padding:8px 12px;border-radius:4px;background:none;border:none;font-size:26px}.open-button:active{background-color:rgba(0, 0, 0, 0.1)}.menu{display:flex;width:max-content;background-color:#fff;border-radius:50px;box-shadow:0 0 5px 3px rgba(0, 0, 0, 0.05)}.emoji{font-size:26px}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;script type=&quot;importmap&quot;&gt;{\n    &quot;imports&quot;: {\n      &quot;vue&quot;: &quot;https://unpkg.com/vue@3.3.8/dist/vue.esm-browser.prod.js&quot;\n    }\n  }&lt;/script&gt;&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;&lt;script type=&quot;module&quot;&gt;import{createApp}from&quot;vue&quot;;import{createCommentVNode as _createCommentVNode2,toHandlers as _toHandlers,toDisplayString as _toDisplayString,mergeProps as _mergeProps,createElementVNode as _createElementVNode2,renderList as _renderList,Fragment as _Fragment,openBlock as _openBlock3,createElementBlock as _createElementBlock3,withCtx as _withCtx2,createBlock as _createBlock,createVNode as _createVNode2}from&quot;vue&quot;;import{ref as ref3}from&quot;vue&quot;;function awareLongTouch(){let e,t,o=!1,n=!1;document.documentElement.addEventListener(&quot;pointerdown&quot;,(function(r){o=n=!1,e=r,t=setTimeout((()=&gt;{o=!0;const e=new CustomEvent(&quot;longtouchstart&quot;,{bubbles:!0,cancelable:!0,detail:r});r.target.dispatchEvent(e)}),300)})),document.documentElement.addEventListener(&quot;pointermove&quot;,(function(r){if(o||n||!e)return;const a=r.clientX-e.clientX,l=r.clientY-e.clientY;Math.sqrt(a*a+l*l)&gt;10&amp;&amp;(n=!0,clearTimeout(t))})),document.documentElement.addEventListener(&quot;pointerup&quot;,(function(r){if(clearTimeout(t),o){const e=new CustomEvent(&quot;longtouchend&quot;,{bubbles:!0,cancelable:!0,detail:r});r.target.dispatchEvent(e)}o=n=!1,e=void 0})),document.documentElement.addEventListener(&quot;pointerover&quot;,(function(e){if(o){const t=new CustomEvent(&quot;longtouchenter&quot;,{bubbles:!1,cancelable:!0,detail:e});e.target.dispatchEvent(t)}})),document.documentElement.addEventListener(&quot;pointerout&quot;,(function(e){if(o){const t=new CustomEvent(&quot;longtouchleave&quot;,{bubbles:!1,cancelable:!0,detail:e});e.target.dispatchEvent(t)}})),document.documentElement.addEventListener(&quot;touchmove&quot;,(function(e){o&amp;&amp;e.preventDefault()}),{passive:!1})}import{renderSlot as _renderSlot,vShow as _vShow,normalizeStyle as _normalizeStyle,createElementVNode as _createElementVNode,withDirectives as _withDirectives,Transition as _Transition,withCtx as _withCtx,createVNode as _createVNode,openBlock as _openBlock,createElementBlock as _createElementBlock}from&quot;vue&quot;;import{ref,computed}from&quot;vue&quot;;var _hoisted_1={class:&quot;popover-wrapper&quot;},Comp={props:{direction:String},setup(e){const t=e,o=ref(!1),n=computed((()=&gt;&quot;top-center&quot;===t.direction?{margin:&quot;auto&quot;,bottom:&quot;100%&quot;,left:&quot;-1000px&quot;,right:&quot;-1000px&quot;,transformOrigin:&quot;bottom center&quot;}:{top:&quot;100%&quot;,right:&quot;0&quot;,transformOrigin:&quot;top right&quot;}));function r(){o.value=!o.value}function a(e){e.target.releasePointerCapture(e.detail.pointerId),o.value=!0}return(e,t)=&gt;(_openBlock(),_createElementBlock(&quot;div&quot;,_hoisted_1,[_renderSlot(e.$slots,&quot;activator&quot;,{on:{click:r,longtouchstart:a}}),_createVNode(_Transition,{persisted:&quot;&quot;},{default:_withCtx((()=&gt;[_withDirectives(_createElementVNode(&quot;div&quot;,{class:&quot;popover&quot;,style:_normalizeStyle(n.value)},[_renderSlot(e.$slots,&quot;default&quot;,{toggle:r})],4),[[_vShow,o.value]])])),_:3})]))}},Popover_default=Comp;import{renderSlot as _renderSlot2,createCommentVNode as _createCommentVNode,normalizeStyle as _normalizeStyle2,openBlock as _openBlock2,createElementBlock as _createElementBlock2,normalizeClass as _normalizeClass}from&quot;vue&quot;;import{ref as ref2,computed as computed2,onMounted,onBeforeUnmount}from&quot;vue&quot;;var Comp2={props:{longTouchHitOffset:Object},emits:[&quot;click&quot;],setup(e,{emit:t}){const o=e,n=t,r=ref2(!1),a=ref2(!1),l=computed2((()=&gt;{const{left:e,top:t,right:n,bottom:r}=o.longTouchHitOffset??{};return{left:-(e??0)+&quot;px&quot;,top:-(t??0)+&quot;px&quot;,right:-(n??0)+&quot;px&quot;,bottom:-(r??0)+&quot;px&quot;}}));function c(){r.value=!0}function i(){r.value=!1}function s(){r.value=!1,n(&quot;click&quot;)}function u(){a.value=!0}function m(){a.value=!1}return onMounted((()=&gt;{document.documentElement.addEventListener(&quot;longtouchstart&quot;,u),document.documentElement.addEventListener(&quot;longtouchend&quot;,m)})),onBeforeUnmount((()=&gt;{document.documentElement.removeEventListener(&quot;longtouchstart&quot;,u),document.documentElement.removeEventListener(&quot;longtouchend&quot;,m)})),(e,t)=&gt;(_openBlock2(),_createElementBlock2(&quot;button&quot;,{type:&quot;button&quot;,class:_normalizeClass([&quot;popover-button&quot;,{active:r.value}]),onClick:t[0]||(t[0]=e=&gt;n(&quot;click&quot;))},[_renderSlot2(e.$slots,&quot;default&quot;),_createCommentVNode(&quot;\\n      ドラッグ時の当たり判定を広げる不可視要素\\n      longTouchHitOffset の値によって広げる量を設定する\\n    &quot;),a.value?(_openBlock2(),_createElementBlock2(&quot;div&quot;,{key:0,class:&quot;drag-hit&quot;,style:_normalizeStyle2(l.value),onLongtouchenter:c,onLongtouchleave:i,onLongtouchend:s},null,36)):_createCommentVNode(&quot;v-if&quot;,!0)],2))}},PopoverButton_default=Comp2,_hoisted_12={class:&quot;wrapper&quot;},_hoisted_2=[&quot;textContent&quot;],_hoisted_3={class:&quot;menu&quot;},_hoisted_4=[&quot;textContent&quot;],Comp3={setup(e){awareLongTouch();const t=ref3(&quot;😃&quot;),o=ref3([&quot;😃&quot;,&quot;🤣&quot;,&quot;😇&quot;,&quot;😍&quot;,&quot;🥳&quot;,&quot;😎&quot;]);return(e,n)=&gt;(_openBlock3(),_createElementBlock3(&quot;div&quot;,_hoisted_12,[_createVNode2(Popover_default,{direction:&quot;top-center&quot;},{activator:_withCtx2((({on:e})=&gt;[_createElementVNode2(&quot;button&quot;,_mergeProps({type:&quot;button&quot;,class:&quot;open-button&quot;},_toHandlers(e,!0),{textContent:_toDisplayString(t.value)}),null,16,_hoisted_2)])),default:_withCtx2((({toggle:e})=&gt;[_createElementVNode2(&quot;div&quot;,_hoisted_3,[_createCommentVNode2(&quot; 下側 50px 分、長押しドラッグ時の判定を伸ばす &quot;),(_openBlock3(!0),_createElementBlock3(_Fragment,null,_renderList(o.value,((o,n)=&gt;(_openBlock3(),_createBlock(PopoverButton_default,{key:n,&quot;long-touch-hit-offset&quot;:{bottom:50},onClick:n=&gt;function(e,o){t.value=e,o()}(o,e)},{default:_withCtx2((()=&gt;[_createElementVNode2(&quot;span&quot;,{class:&quot;emoji&quot;,textContent:_toDisplayString(o)},null,8,_hoisted_4)])),_:2},1032,[&quot;onClick&quot;])))),128))])])),_:1})]))}};createApp(Comp3).mount(&quot;#app&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=width:400px;height:250px></iframe><p>この記事では Vue.js を使って、上記のような長押しドラッグによるポップオーバーのメニュー選択インタラクションの実装方法を、実際のコードを交えながら解説します。</p>",
			"date_published": "2023-11-20T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/css-spring-animation-library/",
			"url": "https://katashin.info/posts/css-spring-animation-library/",
			"title": "直感的なオプションでスプリングアニメーションできる JavaScript ライブラリ CSS Spring Animation",
			"content_html": "<p>「難しいことはよくわからないからとりあえずいい感じにアニメーションしてほしい！」</p><p>前回、前々回の記事を読んだ方の中にはそう思った人がいるかもしれません。今まで CSS に数式を書いてアニメーションさせる方法や、iOS で使われているスプリングアニメーションを CSS で実装する方法を紹介しましたが、しっかりと理解をして実装をするにはハードルが高かったように思います。今回は、それらの実装を簡単にできる JavaScript ライブラリ <a href=https://github.com/ktsn/css-spring-animation><strong>CSS Spring Animation</strong></a> を紹介します。</p><p>前回、前々回の記事は以下のリンクから読めます。</p><a class=post-link href=https://katashin.info/posts/css-math-animation/ ><div class=post-link-thumbnail><picture><source type=image/avif srcset=\"https://katashin.info/img/j9IS1khzt8-100.avif 100w\"><source type=image/webp srcset=\"https://katashin.info/img/j9IS1khzt8-100.webp 100w\"><img src=https://katashin.info/img/j9IS1khzt8-100.png alt=\"\" loading=lazy decoding=async width=100 height=52></picture></div><p class=post-link-title-wrapper><strong class=post-link-title>CSS 数式アニメーションで初速も考慮できる表現力の高いイージングを書く</strong></p></a><a class=post-link href=https://katashin.info/posts/spring-animation-math/ ><div class=post-link-thumbnail><picture><source type=image/avif srcset=\"https://katashin.info/img/BxhGx26vPH-100.avif 100w\"><source type=image/webp srcset=\"https://katashin.info/img/BxhGx26vPH-100.webp 100w\"><img src=https://katashin.info/img/BxhGx26vPH-100.png alt=\"\" loading=lazy decoding=async width=100 height=52></picture></div><p class=post-link-title-wrapper><strong class=post-link-title>iOS のスプリングを CSS 数式アニメーションで再現する</strong></p></a>",
			"date_published": "2023-10-25T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/spring-animation-math/",
			"url": "https://katashin.info/posts/spring-animation-math/",
			"title": "iOS のスプリングを CSS 数式アニメーションで再現する",
			"content_html": "<p>「こういうアニメーションを作ってると未来予知したくなるんですよね」</p><p>9月30日の<a href=https://guiland.connpass.com/event/295287/ >複雑 GUI 会</a>に参加した時に、「<a href=https://katashin.info/posts/rubber-band/ >iOS のラバーバンドスクロールを Web で実装する方法</a>」のラバーバンド実装を見せた時にこんな事を言いました。物理演算を取り入れたアニメーションはフレームごとに次の位置を計算するため、例えば1秒後にどこにいるのかを事前に知ることはできないので、数式を使って計算したくなるという話でした。</p><p>この話をしたら、参加者の方に WWDC 2023 で発表された <a href=https://developer.apple.com/videos/play/wwdc2023/10158/ >Animate with springs</a> を教えていただきました。iOS では、<strong>従来物理演算で行われていたスプリングアニメーション（Spring Animation）を数式で表現している</strong>ことが解説されていて、実際の数式も紹介されており、とても興味深いものでした。</p><p>数式によるスプリングアニメーションの実装を Web に持ってこれたらおもしろいのではないかと思い、何度も動画を視聴し、数式の理解を試みました。ある程度 Web で動かせるようになるまで理解が進んだので、本記事ではその数式の解説と、簡単な実装の紹介をします。</p>",
			"date_published": "2023-10-16T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/css-math-animation/",
			"url": "https://katashin.info/posts/css-math-animation/",
			"title": "CSS 数式アニメーションで初速も考慮できる表現力の高いイージングを書く",
			"content_html": "<p><code>cubic-bezier()</code> をやめてこれからは数式で CSS アニメーションを書いていこうと思います。<code>cubic-bezier()</code> には簡単にアニメーションのイージングを書けるというメリットがありますが、凝ったことをしようとすると表現力が足りない問題に直面します。例えば、ユーザーが直前に行った操作に応じてアニメーションに初速をかけたい時、CSS アニメーションでやるのは難しいので、JavaScript で実装するというのが一般的です。</p><p>しかし、最近の CSS では、アニメーションさせたいプロパティに数式を記述することで、初速を考慮したイージングを実現できます。以下のデモは初速を考慮した CSS アニメーションの実装です。（記事執筆時点で Firefox に未実装の機能を使用しているので、Firefox 以外のブラウザーで見てください）</p><iframe title=\"CSS 数式アニメーションを行うデモ\" srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;body{margin:10px auto;width:350px}.ball{width:50px;height:50px;border-radius:50%}.red{background-color:#c00}.green{background-color:#0a0}.blue{background-color:#00c}.red{--v:0}.green{--v:1000}.blue{--v:-500}.ball{/*\n   * 初速を考慮したスプリングっぽいアニメーション。\n   * （デモ用にかなり適当に書いてます）\n   * --v は初速を表すカスタムプロパティ。\n   * --t は経過時間を表すカスタムプロパティで 0 から 1 へと変化する。\n   *//* prettier-ignore */translate:calc(\n    -300px\n      * (\n        (var(--v) / -150 * var(--t) + 1)\n        / (1 + 700 * var(--t) * var(--t) * var(--t) * var(--t))\n      )\n    + 300px\n  )}@property --t{syntax:&#39;&lt;number&gt;&#39;;inherits:false;initial-value:0}.ball{/*\n   * --t を 0 から 1 へと変化させるアニメーションを2秒かけて繰り返す。\n   * timing-function は linear にする必要があることに注意。\n   */animation:time 2s linear infinite}/*\n * --t を 0 から 1 へと変化させる。\n * translate ではなく --t の方を変化させる必要があることに注意。\n */@keyframes time{from{--t:0}to{--t:1}}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;p&gt;初速: 0 px/s&lt;/p&gt;&lt;div class=&quot;ball red&quot;&gt;&lt;/div&gt;&lt;p&gt;初速: +1000 px/s&lt;/p&gt;&lt;div class=&quot;ball green&quot;&gt;&lt;/div&gt;&lt;p&gt;初速: -500 px/s&lt;/p&gt;&lt;div class=&quot;ball blue&quot;&gt;&lt;/div&gt;&lt;script&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:350px></iframe><p>これを突き詰めると、スプリングアニメーション（Spring Animation）のような、従来は JavaScript でフレームごとに描画していたアニメーションも CSS アニメーションで実装できます。</p><a class=post-link href=https://katashin.info/posts/spring-animation-math/ ><div class=post-link-thumbnail><picture><source type=image/avif srcset=\"https://katashin.info/img/BxhGx26vPH-100.avif 100w\"><source type=image/webp srcset=\"https://katashin.info/img/BxhGx26vPH-100.webp 100w\"><img src=https://katashin.info/img/BxhGx26vPH-100.png alt=\"\" loading=lazy decoding=async width=100 height=52></picture></div><p class=post-link-title-wrapper><strong class=post-link-title>iOS のスプリングを CSS 数式アニメーションで再現する</strong></p></a><p>この記事では数式を使ったアニメーションを CSS で行う方法について、実際にデモを実装しながら解説します。</p>",
			"date_published": "2023-10-09T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/tweetbot-interaction/",
			"url": "https://katashin.info/posts/tweetbot-interaction/",
			"title": "今はなき Tweetbot の至高のインタラクションを高校数学と物理を使って再現する",
			"content_html": "<p>iOS の Twitter（現X）クライアントに Tweetbot というアプリがありました。全 Twitter クライアントの中で<strong>インタラクションの出来が群を抜いており</strong>、筆者はこのアプリのインタラクションが気持ち良くて愛用していました。</p><p>筆者が Tweetbot のインタラクションの中で最も好きなのが、ツイートの画像タップで拡大表示した後、<strong>その画像をドラッグすることで、カードのように画面外に飛ばし、拡大表示を閉じれる</strong>というものです。そのカード飛ばしインタラクションを再現したものが以下のデモです。</p><iframe title=\"Tweetbot に実装されていたカード飛ばしインタラクションを再現したもの\" srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;body{background-color:#181818}#wrapper{overflow:hidden;position:absolute;inset:0}#image{position:absolute;left:0;right:0;top:0;bottom:0;margin:auto;max-width:100%;max-height:100%;width:auto;height:auto}#image.cancelling{transition:transform 0.5s cubic-bezier(0.15, 0.5, 0.6, 0.9)}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div id=&quot;wrapper&quot;&gt;&lt;img id=&quot;image&quot; src=&quot;/img/QfjbHk_Vue-600.jpeg&quot; width=&quot;600&quot; height=&quot;375&quot;&gt;&lt;/div&gt;&lt;script&gt;var wrapper=document.getElementById(&quot;wrapper&quot;),image=document.getElementById(&quot;image&quot;),x0=0,y0=0,x=0,y=0,vx=0,vy=0,bx=0,by=0,cx=0,cy=0,dragging=!1,prevDrag=null,rx=()=&gt;x0-cx,ry=()=&gt;y0-cy,r=()=&gt;Math.sqrt(rx()**2+ry()**2),dx=()=&gt;x-x0,dy=()=&gt;y-y0,d=()=&gt;Math.sqrt(dx()**2+dy()**2),angle=()=&gt;Math.atan2(rx()*dy()-dx()*ry(),rx()*dx()+ry()*dy()),m=()=&gt;r()*d()*Math.sin(angle()),w=5e-4;function transform(){return`translate(${dx()}px, ${dy()}px)`+&quot; &quot;+`rotate(${m()*w}deg)`}function transformOrigin(){return`${x0-bx}px ${y0-by}px`}function setStyle(){image.style.transform=transform(),image.style.transformOrigin=transformOrigin()}var decelerationRate=.9988;function project(e){return e/1e3*(decelerationRate/(1-decelerationRate))}function onDragStart(e){e.preventDefault(),e.currentTarget.setPointerCapture(e.pointerId),dragging=!0,image.classList.remove(&quot;cancelling&quot;);const{pageX:t,pageY:r}=e;x0=x=t,y0=y=r;const n=image.getBoundingClientRect();bx=n.left,by=n.top,cx=bx+n.width/2,cy=by+n.height/2}function onDragMove(e){if(!dragging)return;const{pageX:t,pageY:r}=e;x=t,y=r,setStyle();const n=Date.now();if(prevDrag){const e=prevDrag.event.pageX,t=prevDrag.event.pageY,r=(n-prevDrag.timestamp)/1e3;0!==r&amp;&amp;(vx=(x-e)/r,vy=(y-t)/r)}prevDrag={event:e,timestamp:n}}function onDragEnd(){if(!dragging)return;dragging=!1;const e=x+project(vx),t=y+project(vy),r=image.getBoundingClientRect(),n=Math.max(r.width,r.height),a=wrapper.getBoundingClientRect(),i={left:a.left-n,right:a.right+n,top:a.top-n,bottom:a.bottom+n};e&lt;i.left||e&gt;i.right||t&lt;i.top||t&gt;i.bottom?startMomentum(i):cancel()}function startMomentum(e){let t,r;requestAnimationFrame((function n(a){if(r){const n=a-r;x+=(vx*=decelerationRate**n)*(n/1e3),y+=(vy*=decelerationRate**n)*(n/1e3),setStyle();const i=x&lt;e.left||x&gt;e.right||y&lt;e.top||y&gt;e.bottom;if(Math.abs(vx)&lt;1&amp;&amp;Math.abs(vy)&lt;1||i&amp;&amp;a-t&gt;1e3)return void(image.style.transform=&quot;&quot;)}else t=a;r=a,requestAnimationFrame(n)}))}function cancel(){image.style.transform=&quot;&quot;,image.classList.add(&quot;cancelling&quot;),image.addEventListener(&quot;transitionend&quot;,(()=&gt;{image.classList.remove(&quot;cancelling&quot;)}))}wrapper.addEventListener(&quot;touchstart&quot;,(e=&gt;{e.preventDefault()})),wrapper.addEventListener(&quot;pointerdown&quot;,onDragStart),wrapper.addEventListener(&quot;pointermove&quot;,onDragMove),wrapper.addEventListener(&quot;pointerup&quot;,onDragEnd),wrapper.addEventListener(&quot;pointercancel&quot;,onDragEnd)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:500px></iframe><p>このインタラクションの気持ち良さは、ただ画像が飛んでいくだけでなく、<strong>ドラッグに合わせて自然に画像が回転し、ドラッグ後もくるくると回転しながら飛んでいく</strong>ところにあります。実装も興味深く、高校数学や物理で学んだことを組み合わせる必要があります。</p><p>本記事では、この Tweetbot のカード飛ばしインタラクションの実装について解説します。高校までの数学や物理の話が出てきますが、なるべく図を使い、平易な解説を行います。また、本記事の最後に、このインタラクションの HTML、CSS、JavaScript コードの全体を掲載しています。</p>",
			"date_published": "2023-09-25T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/seamless-transition/",
			"url": "https://katashin.info/posts/seamless-transition/",
			"title": "シームレスな画面遷移アニメーションの Vue Router を使った実装パターン",
			"content_html": "<p>以下のようにシームレスな（繋ぎ目のない）画面遷移を実装するとしたら、どのように実装しますか？それぞれのカードをクリックするとカードが拡大され、そのコンテンツ全体が見れるようになります。</p><iframe title=シームレスな画面遷移の例 srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;*,*::before,*::after{box-sizing:border-box}html,body,#app{margin:0;height:100%}body{background-color:#e0e0e0;font-family:sans-serif}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;script src=&quot;https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js&quot;&gt;&lt;/script&gt;&lt;script src=&quot;https://unpkg.com/vue-router@4.2.4/dist/vue-router.global.prod.js&quot;&gt;&lt;/script&gt;&lt;script&gt;const{ref:ref,shallowRef:shallowRef,reactive:reactive,computed:computed,onMounted:onMounted}=Vue,{useRoute:useRoute,useRouter:useRouter}=VueRouter&lt;/script&gt;&lt;div id=&quot;app&quot;&gt;&lt;!-- &lt;transition&gt; でページ全体をアニメーションの対象にする --&gt;&lt;router-view v-slot=&quot;{ Component }&quot;&gt;&lt;transition&gt;&lt;component :is=&quot;Component&quot;&gt;&lt;/component&gt;&lt;/transition&gt;&lt;/router-view&gt;&lt;/div&gt;&lt;script&gt;var homeStyle=document.createElement(&quot;style&quot;);homeStyle.textContent=&quot;.document-list {\\n  display: flex;\\n  align-items: center;\\n  justify-content: center;\\n  flex-wrap: wrap;\\n  gap: 20px;\\n  padding: 20px;\\n  margin: 0;\\n  width: 100%;\\n  height: 100%;\\n  list-style: none;\\n}\\n\\n.document-item {\\n  flex: none;\\n  height: 200px;\\n  width: 150px;\\n}\\n\\n.document-button {\\n  display: flex;\\n  align-items: flex-start;\\n  justify-content: flex-start;\\n  overflow: hidden;\\n  padding: 12px;\\n  width: 100%;\\n  height: 100%;\\n  background-color: #fff;\\n  color: inherit;\\n  text-decoration: none;\\n  box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.1);\\n  transition: 0.6s cubic-bezier(0.65, 0, 0.35, 1);\\n}\\n\\n.document-list.v-leave-active {\\n  /* ページを離れる時にアニメーションさせる */\\n  transition: 0.6s cubic-bezier(0.65, 0, 0.35, 1);\\n}\\n\\n.document-list.v-leave-to {\\n  /* フェードアウトするようにスタイルを指定 */\\n  opacity: 0;\\n}\\n&quot;,document.head.appendChild(homeStyle);var Home={template:&#39;\\n&lt;ul class=&quot;document-list&quot;&gt;\\n  &lt;li class=&quot;document-item&quot; v-for=&quot;document in documents&quot; :key=&quot;document.id&quot;&gt;\\n    &lt;router-link :to=&quot;\\&#39;/\\&#39; + document.id&quot; custom v-slot=&quot;{ href }&quot;&gt;\\n      \\x3c!--\\n        appearId とドキュメントの ID が一致する時に appearStyle を適用する。\\n        onMounted で fromId から要素を特定するために、data-document-id にドキュメント ID を入れておく。\\n      --\\x3e\\n      &lt;a\\n        class=&quot;document-button&quot;\\n        ref=&quot;documentRefs&quot;\\n        :style=&quot;appearId === document.id ? appearStyle : null&quot;\\n        :data-document-id=&quot;document.id&quot;\\n        :href=&quot;href&quot;\\n        @click.prevent=&quot;onClick(document.id, $event)&quot;\\n      &gt;\\n        {{ document.body }}\\n      &lt;/a&gt;\\n    &lt;/router-link&gt;\\n  &lt;/li&gt;\\n&lt;/ul&gt;\\n&#39;,setup(){const e=useRoute(),n=useRouter(),t=computed((()=&gt;store.documents));const o=ref(null),i=ref(null),a=shallowRef([]);return onMounted((()=&gt;{if(e.query.fromId){o.value=e.query.fromId;const t=a.value.find((e=&gt;e.dataset.documentId===o.value)).getBoundingClientRect(),d=Number(e.query.left),u=Number(e.query.top),r=`${d-t.left}px ${u-t.top}px`;i.value={translate:r,width:e.query.width,height:e.query.height,padding:e.query.padding,transition:&quot;none&quot;},n.replace({query:null}),requestAnimationFrame((()=&gt;{o.value=i.value=null}))}})),{documentRefs:a,documents:t,appearId:o,appearStyle:i,onClick:function(e,t){const o=t.currentTarget,i=o.getBoundingClientRect(),a={left:i.left,top:i.top,width:`${i.width}px`,height:`${i.height}px`,padding:window.getComputedStyle(o).padding};n.push({path:`/${e}`,query:a})}}}},docStyle=document.createElement(&quot;style&quot;);docStyle.textContent=&quot;.document-body {\\n  overflow: auto;\\n  position: absolute;\\n  left: 5%;\\n  top: 5%;\\n  padding: 50px 20px 20px 20px;\\n  height: 90%;\\n  width: 90%;\\n  background-color: #fff;\\n  text-align: left;\\n  box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.1);\\n  transition: 0.6s cubic-bezier(0.65, 0, 0.35, 1);\\n}\\n\\n.document-header {\\n  display: flex;\\n  align-items: center;\\n  position: absolute;\\n  top: 10px;\\n  left: 20px;\\n  right: 20px;\\n  height: 40px;\\n  transition: 0.6s cubic-bezier(0.65, 0, 0.35, 1);\\n}\\n\\n.document-close {\\n  color: #0b12e3;\\n}\\n\\n.document-body.v-leave-active {\\n  /* Document ページは全体へのアニメーションは不要なので無効化 */\\n  transition: none;\\n}\\n\\n.document-header.appearing {\\n  /* 「戻る」ボタンの初期スタイルを透明にし、フェードインアニメーションを行う */\\n  opacity: 0;\\n  transition: none;\\n}\\n&quot;,document.head.appendChild(docStyle);var Document={template:&#39;\\n&lt;article ref=&quot;documentBody&quot; class=&quot;document-body&quot; :style=&quot;appearStyle&quot;&gt;\\n  \\x3c!-- appearStyle が設定されている間、「戻る」ボタンを含む要素にも appearing クラスが指定される --\\x3e\\n  &lt;header class=&quot;document-header&quot; :class=&quot;{ appearing: appearStyle }&quot;&gt;\\n    &lt;router-link class=&quot;document-close&quot; to=&quot;/&quot; @click=&quot;onClose&quot;&gt;\\n      戻る\\n    &lt;/router-link&gt;\\n  &lt;/header&gt;\\n  {{ document.body }}\\n&lt;/article&gt;\\n&#39;,setup(){const e=useRouter(),n=useRoute(),t=computed((()=&gt;store.documents.find((e=&gt;e.id===n.params.id))));const o=ref(null),i=shallowRef(null);return onMounted((()=&gt;{if(n.query.left){const t=i.value.getBoundingClientRect(),a=Number(n.query.left),d=Number(n.query.top),u=`${a-t.left}px ${d-t.top}px`;o.value={translate:u,width:n.query.width,height:n.query.height,padding:n.query.padding,transition:&quot;none&quot;},e.replace({query:null}),requestAnimationFrame((()=&gt;{o.value=null}))}})),{documentBody:i,document:t,appearStyle:o,onClose:function(){const t=i.value.getBoundingClientRect(),o={fromId:n.params.id,left:t.left,top:t.top,width:`${t.width}px`,height:`${t.height}px`,padding:window.getComputedStyle(i.value).padding};e.push({path:&quot;/&quot;,query:o})}}}},routes=[{path:&quot;/&quot;,component:Home},{path:&quot;/:id&quot;,component:Document}],store=reactive({documents:[{id:&quot;1&quot;,body:&quot;親譲りの無鉄砲で小供の時から損ばかりしている。小学校に居る時分学校の二階から飛び降りて一週間ほど腰を抜かした事がある。なぜそんな無闇をしたと聞く人があるかも知れぬ。別段深い理由でもない。新築の二階から首を出していたら、同級生の一人が冗談に、いくら威張っても、そこから飛び降りる事は出来まい。弱虫やーい。と囃したからである。小使に負ぶさって帰って来た時、おやじが大きな眼をして二階ぐらいから飛び降りて腰を抜かす奴があるかと云ったから、この次は抜かさずに飛んで見せますと答えた。（青空文庫より）&quot;},{id:&quot;2&quot;,body:&quot;つれづれなるまゝに、日暮らし、硯にむかひて、心にうつりゆくよしなし事を、そこはかとなく書きつくれば、あやしうこそものぐるほしけれ。（Wikipediaより）&quot;},{id:&quot;3&quot;,body:&quot;後ろで大きな爆発音がした。俺は驚いて振り返った。&quot;}]}),router=VueRouter.createRouter({history:VueRouter.createMemoryHistory(&quot;/posts/seamless-transition/demo/&quot;),routes:routes}),app=Vue.createApp({});app.use(router),app.mount(&quot;#app&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:400px></iframe><p>この実装はカード一覧ページと、コンテンツ全体が見れるページの2ページで構成されていて、画面遷移の処理を工夫することで、シームレスな見せ方をしています。</p><p>シームレスな画面遷移は、要素同士の関連性を強調したり、アプリへの没入感を高めるなどのメリットがありますが、実装難易度が高く、コードのメンテナビリティが下がりやすいというデメリットもあります。</p><p>本記事では、このようなシームレスな画面遷移を実装する難しさを説明してから、Vue.js を使った実際のコードを交えながらその実装方法について解説します。</p>",
			"date_published": "2023-09-19T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/window-prompt-vuejs/",
			"url": "https://katashin.info/posts/window-prompt-vuejs/",
			"title": "window.prompt を Vue.js で再発明する",
			"content_html": "<p><code>window.prompt</code> は JavaScript を一行書くだけでユーザーのテキスト入力を取得できる優れた API です。以下のように <code>window.prompt</code> を呼ぶだけで入力フォーム付きのダイアログが開き、戻り値でユーザーが入力した文字列を取得できます。</p><pre class=language-vue tabindex=0><code class=language-vue><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>script</span> <span class=\"token attr-name\">setup</span><span class=\"token punctuation\">></span></span><span class=\"token script\"><span class=\"token language-javascript\">\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> ref <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">'vue'</span>\n\n<span class=\"token keyword\">const</span> list <span class=\"token operator\">=</span> <span class=\"token function\">ref</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token keyword\">function</span> <span class=\"token function\">onAdd</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> input <span class=\"token operator\">=</span> window<span class=\"token punctuation\">.</span><span class=\"token function\">prompt</span><span class=\"token punctuation\">(</span><span class=\"token string\">'追加する文字列を入力してください'</span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">if</span> <span class=\"token punctuation\">(</span>input<span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    list<span class=\"token punctuation\">.</span>value<span class=\"token punctuation\">.</span><span class=\"token function\">push</span><span class=\"token punctuation\">(</span>input<span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n</span></span><span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>script</span><span class=\"token punctuation\">></span></span>\n\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>template</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>button</span> <span class=\"token attr-name\">type</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>button<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">@click</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>onAdd<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>追加<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>button</span><span class=\"token punctuation\">></span></span>\n\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>ul</span><span class=\"token punctuation\">></span></span>\n    <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;</span>li</span> <span class=\"token attr-name\">v-for</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>(item, i) in list<span class=\"token punctuation\">\"</span></span> <span class=\"token attr-name\">:key</span><span class=\"token attr-value\"><span class=\"token punctuation attr-equals\">=</span><span class=\"token punctuation\">\"</span>i<span class=\"token punctuation\">\"</span></span><span class=\"token punctuation\">></span></span>{{ item }}<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>li</span><span class=\"token punctuation\">></span></span>\n  <span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>ul</span><span class=\"token punctuation\">></span></span>\n<span class=\"token tag\"><span class=\"token tag\"><span class=\"token punctuation\">&lt;/</span>template</span><span class=\"token punctuation\">></span></span></code></pre><iframe title=\"window.prompt を使ったダイアログの実装\" srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;script type=&quot;importmap&quot;&gt;{\n    &quot;imports&quot;: {\n      &quot;vue&quot;: &quot;https://unpkg.com/vue@3.3.4/dist/vue.esm-browser.prod.js&quot;\n    }\n  }&lt;/script&gt;&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;&lt;script type=&quot;module&quot;&gt;import{createApp}from&quot;vue&quot;;import{createElementVNode as _createElementVNode,renderList as _renderList,Fragment as _Fragment,openBlock as _openBlock,createElementBlock as _createElementBlock,toDisplayString as _toDisplayString}from&quot;vue&quot;;import{ref}from&quot;vue&quot;;var Comp={setup(e){const t=ref([]);function o(){const e=window.prompt(&quot;追加する文字列を入力してください&quot;);e&amp;&amp;t.value.push(e)}return(e,n)=&gt;(_openBlock(),_createElementBlock(_Fragment,null,[_createElementVNode(&quot;button&quot;,{type:&quot;button&quot;,onClick:o},&quot;追加&quot;),_createElementVNode(&quot;ul&quot;,null,[(_openBlock(!0),_createElementBlock(_Fragment,null,_renderList(t.value,((e,t)=&gt;(_openBlock(),_createElementBlock(&quot;li&quot;,{key:t},_toDisplayString(e),1)))),128))])],64))}};createApp(Comp).mount(&quot;#app&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy></iframe><p>しかし、実際のアプリケーションでは独自ダイアログを実装することがほとんどです。<code>window.prompt</code> によって開かれるダイアログは、デザインや機能をカスタマイズできないためです。独自ダイアログが <code>window.prompt</code> と同様の使い勝手であれば良いのですが、大抵の実装には以降の節で解説する問題点があります。</p><p>本記事では、Vue.js で独自ダイアログを実装する例を通して、その実装によくある問題点を解説し、どうすれば <code>window.prompt</code> のようなシンプルなインターフェースの実装ができるかを解説します。</p>",
			"date_published": "2023-09-11T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/dialog-pitfalls/",
			"url": "https://katashin.info/posts/dialog-pitfalls/",
			"title": "アクションシートの実装から学ぶ &lt;dialog&gt; 要素を使う時の3つの落とし穴",
			"content_html": "<p><code>&lt;dialog&gt;</code> 要素が主要なブラウザすべてに実装され、現実的に使えるようになってきましたが、これをそのまま実際の Web アプリで使うには様々なものが足りません。足りないものを自分で実装していくと、<strong>分かりづらい挙動があったり、癖のある実装が必要なことがあります</strong>。例えば、<code>&lt;dialog&gt;</code> 要素のデフォルトは中央揃えで、コンテンツに合わせたサイズになりますが、これの位置、サイズ調整やアニメーションをする際に落とし穴があります。</p><p>本記事では、以下のアクションシートのようなモーダルを実装する例を通して、<code>&lt;dialog&gt;</code> 要素を使う時の3つの落とし穴を紹介します。</p><iframe title=アクションシートのようなモーダルの実装例 srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;dialog{/* 諸々のデフォルトスタイルを解除 */overflow:visible;top:auto;max-width:none;width:auto;padding:0;border:none;background:none}.inner{/* 見た目に関するスタイルはこちらに移す */padding:16px;background-color:#fff;border-radius:16px 16px 0 0;box-shadow:0 0 8px rgba(0, 0, 0, 0.1)}dialog::backdrop{background-color:rgba(0, 0, 0, 0.3);backdrop-filter:blur(2px)}/* ダイアログと背景に CSS トランジションを設定 */dialog,dialog::backdrop{transition:0.3s cubic-bezier(0.33, 1, 0.68, 1)}/* ダイアログの表示前、非表示後は下に移動 */.show-from,.hide-to{translate:0 100%}/* ダイアログの表示前、非表示後は背景を透明にする */.show-from::backdrop,.hide-to::backdrop{opacity:0}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;button id=&quot;open&quot; type=&quot;button&quot;&gt;開く&lt;/button&gt;&lt;dialog&gt;&lt;!-- inner を増やして見た目のスタイルをすべてこれに設定 --&gt;&lt;div class=&quot;inner&quot;&gt;&lt;p&gt;アクションシートのようなモーダル&lt;/p&gt;&lt;button id=&quot;close&quot; type=&quot;button&quot;&gt;閉じる&lt;/button&gt;&lt;/div&gt;&lt;/dialog&gt;&lt;script&gt;var dialog=document.querySelector(&quot;dialog&quot;);function show(){dialog.classList.add(&quot;show-from&quot;),dialog.showModal(),requestAnimationFrame((()=&gt;{dialog.classList.remove(&quot;show-from&quot;)}))}function close(){dialog.classList.add(&quot;hide-to&quot;),dialog.addEventListener(&quot;transitionend&quot;,(()=&gt;{dialog.classList.remove(&quot;hide-to&quot;),dialog.close()}),{once:!0})}function keydown(e){e.preventDefault(),&quot;Escape&quot;===e.key&amp;&amp;close()}document.querySelector(&quot;#open&quot;).addEventListener(&quot;click&quot;,show),document.querySelector(&quot;#close&quot;).addEventListener(&quot;click&quot;,close),dialog.addEventListener(&quot;keydown&quot;,keydown),dialog.addEventListener(&quot;click&quot;,(e=&gt;{e.target===dialog&amp;&amp;close()}))&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:400px;width:390px></iframe>",
			"date_published": "2023-08-28T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/animation-debug/",
			"url": "https://katashin.info/posts/animation-debug/",
			"title": "たった一つの変数でアニメーションのデバッグを簡単にする実装パターン",
			"content_html": "<p>アニメーションを実装した時、そのアニメーションを観察してどこか違和感を感じた経験はありませんか？例えば、以下のデモは項目の追加時には高さが 0 から伸び、削除時には縮むアニメーションをしますが、これに違和感を感じる人もいるかもしれません。</p><iframe title=バグが含まれているアニメーションの例 srcdoc=\"&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;ul{padding:0;list-style:none}li{overflow:hidden;padding:4px 8px;border-top:1px solid #ccc;background-color:#f5f5f5;height:24px}button{border:1px solid #666;border-radius:4px;background-color:#fff}li:last-child{border-bottom:1px solid #ccc}.v-enter-active,.v-leave-active{transition:0.2s ease-out}.v-enter-from,.v-leave-to{height:0}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;script type=&quot;importmap&quot;&gt;{\n    &quot;imports&quot;: {\n      &quot;vue&quot;: &quot;https://unpkg.com/vue@3.3.4/dist/vue.esm-browser.prod.js&quot;\n    }\n  }&lt;/script&gt;&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;&lt;script type=&quot;module&quot;&gt;import{createApp}from&quot;vue&quot;;import{createElementVNode as _createElementVNode,renderList as _renderList,Fragment as _Fragment,openBlock as _openBlock,createElementBlock as _createElementBlock,toDisplayString as _toDisplayString,createTextVNode as _createTextVNode,TransitionGroup as _TransitionGroup,withCtx as _withCtx,createVNode as _createVNode}from&quot;vue&quot;;import{ref}from&quot;vue&quot;;var _hoisted_1={id:&quot;app&quot;},_hoisted_2=[&quot;onClick&quot;],Comp={setup(e){let t=3;const o=ref([3,2,1]);function n(){const e=++t;o.value.unshift(e)}return(e,t)=&gt;(_openBlock(),_createElementBlock(&quot;div&quot;,_hoisted_1,[_createElementVNode(&quot;button&quot;,{type:&quot;button&quot;,onClick:n},&quot;追加&quot;),_createElementVNode(&quot;ul&quot;,null,[_createVNode(_TransitionGroup,null,{default:_withCtx((()=&gt;[(_openBlock(!0),_createElementBlock(_Fragment,null,_renderList(o.value,((e,t)=&gt;(_openBlock(),_createElementBlock(&quot;li&quot;,{key:e},[_createTextVNode(&quot; 項目 &quot;+_toDisplayString(e)+&quot; &quot;,1),_createElementVNode(&quot;button&quot;,{type:&quot;button&quot;,onClick:e=&gt;function(e){o.value.splice(e,1)}(t)},&quot;削除&quot;,8,_hoisted_2)])))),128))])),_:1})])]))}};createApp(Comp).mount(&quot;#app&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;\" class=code-demo loading=lazy style=height:300px></iframe><p>このような場合、アニメーションの動きが速くて目立たない部分に、意図とは違う挙動、つまりバグが隠れている時があります。本記事では、こういったアニメーションのバグを見つけるデバッグの仕方や、デバッグを簡単にする実装パターンを紹介します。</p>",
			"date_published": "2023-08-21T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/feature-flag/",
			"url": "https://katashin.info/posts/feature-flag/",
			"title": "お祈りデプロイを避けるためのフィーチャーフラグ運用",
			"content_html": "<p>「今日リリースの機能ですが、マージ前に軽くレビューしてもらえますか？」</p><p>同僚にそう言われてプルリクエストを見ると1000行以上の差分がある。たしかにこの機能は大きく、開発の完了まで本番環境にリリースしてしまわないようにブランチを分けていたのだが、そうは言っても差分が大きすぎてレビューが大変そうだ。リリースまでの時間もないし、軽く動作チェックをして、問題がないことを祈ろう……</p><hr><p>チーム開発でレビュワーの経験がある人なら、このような状況に覚えがある人もいるでしょう。しかもこういうのは大抵タイムリミットが短く、最終的には時間がないので問題が起きないことを祈りながら Approve するのです。検証やテストが不十分なまま、問題が起きないことを祈りながら本番環境にデプロイすることを俗に<strong>お祈りデプロイ</strong>と呼ぶこともありますが、これはできたら避けるべきです。</p><p>筆者はこれに対処するため、フィーチャーフラグを用いた開発を数年間試したところ、開発体験が良くなった感触を得ています。この記事ではフィーチャーフラグの解説を交え、筆者が実際に試した開発方法を紹介します。</p>",
			"date_published": "2023-08-14T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/undo/",
			"url": "https://katashin.info/posts/undo/",
			"title": "操作データから逆操作を生成しUndo（元に戻す）機能を実装するパターン",
			"content_html": "<p>リッチなアプリを開発していると、<strong>Undo（元に戻す）</strong> 機能を自分で実装する必要が出てきます。canvas を使った図形の描画などはブラウザデフォルトの Undo 機能が使えず、自分で実装しなければならない代表例です。Undo の実装にはパターンがあり、それを理解することで様々なアプリへの Undo の実装がしやすくなります。</p><p>この記事では、JavaScript による簡単なデザインツールのデモを通して、Undo の実装パターンと、具体的な実装例を解説します。</p>",
			"date_published": "2023-08-07T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/rubber-band/",
			"url": "https://katashin.info/posts/rubber-band/",
			"title": "iOS のラバーバンドスクロールを Web で実装する方法",
			"content_html": "<p>普段 iPhone を使っている人でスクロールが端に到達した時に、少しだけ端を越えていき、その後跳ね返ってくる挙動を意識したことがある人はどれだけいるでしょうか？その挙動をどう実装するか考えたことはありますか？</p><p>この iOS の挙動を<strong>ラバーバンドスクロール</strong>や<strong>バウンススクロール</strong>、<strong>バウンスバック</strong>などと呼びますが、ほとんどの人はあまり意識せずに iOS デバイスを使っていると思います。今では当たり前のこの挙動は、iOS の使っていて気持ちのいい UI に大きく寄与しています。</p><p>本記事では、この挙動をラバーバンド効果と呼び、単純化した例を通じてその実装方法を解説します。</p>",
			"date_published": "2023-07-31T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/visual-viewport-api/",
			"url": "https://katashin.info/posts/visual-viewport-api/",
			"title": "バーチャルキーボード張り付き UI と Visual Viewport API",
			"content_html": "<p>以前は Web で実装するのが難しくてモバイルアプリで実装するしかない UI がありました。その一つがバーチャルキーボードの上に要素を張り付けて配置する UI です。LINE のような画面下に入力フォームがあるチャットアプリはそのような UI の代表例です。</p><p>2023年7月現在は主要なブラウザに <strong>Visual Viewport API</strong> が実装され、バーチャルキーボードの高さを計算し、それを利用した要素の配置が容易です。この記事では Visual Viewport API を使ってスクロールや拡大率によらずバーチャルキーボードの上に要素を張り付ける方法を解説し、この API の前提知識となる Visual Viewport と Layout Viewport について解説します。</p>",
			"date_published": "2023-07-24T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/eager-computed/",
			"url": "https://katashin.info/posts/eager-computed/",
			"title": "Vue.js パフォーマンスチューニングの最終手段 computedEager",
			"content_html": "<ins datetime=2025-06-16>追記: Vue 3.4 以降では computed の値が変わったかどうかが判定されるようになりました。現在はもう computedEager を使う必要はありません。以下の記事でその改善について詳しく解説しています。 </ins><a class=post-link href=https://katashin.info/posts/vue-3.4-computed/ ><div class=post-link-thumbnail><picture><source type=image/avif srcset=\"https://katashin.info/img/v-XTUpyOE5-100.avif 100w\"><source type=image/webp srcset=\"https://katashin.info/img/v-XTUpyOE5-100.webp 100w\"><img src=https://katashin.info/img/v-XTUpyOE5-100.png alt=\"\" loading=lazy decoding=async width=100 height=52></picture></div><p class=post-link-title-wrapper><strong class=post-link-title>Vue 3.4 で変わった computed の再計算アルゴリズム – 処理順序の逆転による最適化</strong></p></a><hr><p>「これを <code>computed</code> のかわりに使うとパフォーマンスが良くなる」</p><p>先日 <code>computedEager</code> というユーティリティが使われているのを見て、使っている理由を聞いたらこういった答えをいただきました。たしかに <code>computedEager</code> でパフォーマンスを改善できる場合がありますが、使い方を間違えると逆に悪化させてしまう可能性もあります。この記事では <code>computedEager</code> の仕組みを深掘りしていき、どのような使い方が有効なのかを解説します。</p>",
			"date_published": "2023-07-17T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/posts/flip-animation/",
			"url": "https://katashin.info/posts/flip-animation/",
			"title": "アニメーションの実装が劇的に簡単になるFLIPテクニック",
			"content_html": "<p>アニメーションの実装はややこしいからCSS Transitionでできなければ実装したくない、そう思っていませんか？FLIPというテクニックを使うことで、CSS Transitionだけでは実装できないケースでも簡単にアニメーションを実装することができます。</p>",
			"date_published": "2023-07-10T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2019/04/28/261/",
			"url": "https://katashin.info/2019/04/28/261/",
			"title": "Vue テンプレート内の式の型チェックと解析ができるまで",
			"content_html": "<p>Vue の TypeScript 対応は <a href=https://jp.vuejs.org/2016/10/01/here-2.0/ >v2.0 から公式に型定義がサポートされるようになったり</a>、<a href=https://jp.vuejs.org/2017/09/23/upcoming-typeScript-changes-in-vue-2.5/ >v2.5 で <code>Vue.extend</code> を使ったときに <code>this</code> の型が推論されるようになったり</a>と、改善が何度も行われています。</p><p>しかし、課題はまだたくさんあり、その中でもよく聞くのが、テンプレート内の式の型チェックがされないという課題でした。TypeScript はテンプレートを解釈できないので当たり前ですが、もしそれが解決できたらより安全になるでしょうし、開発体験も向上すると思います。</p><p>そしてこの課題は Vetur の最新版で解決されました。</p>",
			"date_published": "2019-04-28T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/12/18/247/",
			"url": "https://katashin.info/2018/12/18/247/",
			"title": "任意の背景色に対して読みやすい文字色を選択する方法",
			"content_html": "<p>GitHub の Issue ラベルなど、任意の色の中に文字を入れたい場合があります。このとき文字色が一色のみだと、背景色と似たような色のときに読みづらくなってしまいます。</p><p>以下のスクショでわかるように、GitHub は背景色によって文字色を黒か白のどちらにするかを計算しているようです。</p><p><picture><source type=image/avif srcset=\"https://katashin.info/img/6rUkhaoI1y-480.avif 480w\"><source type=image/webp srcset=\"https://katashin.info/img/6rUkhaoI1y-480.webp 480w\"><img src=https://katashin.info/img/6rUkhaoI1y-480.png alt=\"\" loading=lazy decoding=async width=480 height=404></picture></p><p>個々のサービスの実装についてはわかりませんが、WCAG (Web Content Accessibility Guidelines) で定義されているコントラスト比を使うことで、背景色に対して読みやすい文字色を選択することができます。</p>",
			"date_published": "2018-12-17T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/08/26/236/",
			"url": "https://katashin.info/2018/08/26/236/",
			"title": "GraphQL の情報を雑にまとめる",
			"content_html": "<p>最近 GraphQL を使うことがあり、いろいろ調べたりしていることをメモしておきます。自分が必要なものしかまとめてないので情報には偏りがあります (具体的には Apollo、TypeScript や Ruby あたりに偏ってます)。</p><p>GraphQL / Apollo まわりのテストの話は別の記事で詳しく書くかもしれないです。</p>",
			"date_published": "2018-08-26T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/07/16/233/",
			"url": "https://katashin.info/2018/07/16/233/",
			"title": "Nuxt.js のような自動ルーティングを可能にする Vue CLI プラグインを作った",
			"content_html": "<p><a href=https://ja.nuxtjs.org/ >Nuxt.js</a> という Vue.js で SSR をするアプリケーションが簡単に書けるフレームワークがあります。Nuxt.js は SSR だけでなく、webpack の設定やディレクトリ構造なども最初から決められており、規約がすでに存在することによる開発の効率化の面においても注目されています。</p><p>個人的に Nuxt.js で便利だと感じている機能に<a href=https://ja.nuxtjs.org/guide/routing>ルーティングの自動解決</a>と<a href=https://ja.nuxtjs.org/guide/views#%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88>レイアウト機能</a>があります。通常の Vue Router を使ったアプリではルーティングの設定は自分で書く必要がありますが、Nuxt.js では <code>pages/</code> ディレクトリ以下の構造から自動的にルーティングの設定を生成してくれます。また、Rails のレイアウトのように、各ページごとにレイアウトファイルを指定することができます。</p><p>これらの機能に慣れてしまうと、Nuxt.js を使っていないプロジェクトにおいて自分でルーティングの設定を書くのがとても面倒になってきたので、Nuxt.js じゃなくてもいい感じにする Vue CLI プラグインを書きました。</p><p><a href=https://github.com/ktsn/vue-cli-plugin-auto-routing>vue-cli-plugin-auto-routing</a></p><p>また、vue-cli-plugin-auto-routing は独立したパッケージを組み合わせているので Vue CLI プラグインを使えない環境の場合は以下の2つを直接使うと良いです。</p><p><a href=https://github.com/ktsn/vue-auto-routing>vue-auto-routing</a>: ルーティングの設定をディレクトリ構造から生成する webpack プラグイン <a href=https://github.com/ktsn/vue-router-layout>vue-router-layout</a>: レイアウト機能を提供するコンポーネント</p>",
			"date_published": "2018-07-16T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/07/09/231/",
			"url": "https://katashin.info/2018/07/09/231/",
			"title": "vue-thin-modal v1.0.0 をリリースしました",
			"content_html": "<p>去年から作っていた Vue のモーダルコンポーネント <a href=https://github.com/ktsn/vue-thin-modal>vue-thin-modal</a> の v1.0.0 をリリースしました。仕事でも結構使っていて、特に大きな問題もなく、API も安定しているのでメジャーバージョンを上げました。</p><p>vue-thin-modal は世の中の多くのつらいモーダル実装を見て、つらくならなくするために作ったライブラリです。主に以下のような特徴があります。</p><ul><li><p>モーダルはどこに置いても DOM の実態は <code>&lt;body&gt;</code> 直下にマウントされる (いわゆる Portal)。</p></li><li><p>モーダルが開くと通常のコンテンツ部分はスクロールが止まる。モーダル内のコンテンツがウィンドウサイズを超えてもスクロールできる。</p><ul><li>これで発生する、スクロールバーが消えることによるガタツキを防ぐ実装もしている。</li></ul></li><li><p>モーダルを閉じたときに元のコンテンツにフォーカスを戻す。</p></li><li><p>デフォルトの CSS スタイルが提供されていて、何もしなくてもそれっぽく動く。</p></li><li><p>背景とモーダルコンテンツのトランジションが独立していて、柔軟に設定できる。</p></li><li><p>モーダルの表示はスタックで管理していて、モーダルの上にモーダルとかもやろうと思えばできる (UI 的にどうなんだというのは置いといて)。</p></li></ul><p>モーダルのつらさについては CodeGrid 5周年記念パーティーで話しているので、そのスライドも見てみてください。</p><iframe src=https://slides.com/ktsn/modal/embed width=576 height=420 scrolling=no frameborder=0 webkitallowfullscreen=\"\" mozallowfullscreen=\"\" allowfullscreen=\"\"></iframe><p>vue-thin-modal を作る際には <a href=https://getbootstrap.com/docs/4.0/components/modal/ >Bootstrap Modal</a> や <a href=https://github.com/humaan/Modaal>Modaal</a> をかなり参考にしました。特に Bootstrap Modal の作り込みはすごくて、その完成度の高さに驚いた覚えがあります。</p><p>関連ライブラリとして <a href=https://github.com/ktsn/vuex-modal>vuex-modal</a> というものもあります。こちらは vue-thin-modal を作る前に作ったものですが、今は内部で vue-thin-modal を使用しています。</p><p>自分の欲しい機能はすべて実装したので、これからの展望は特にないですが、バグレポートや機能要望などは歓迎です！</p>",
			"date_published": "2018-07-09T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/06/07/227/",
			"url": "https://katashin.info/2018/06/07/227/",
			"title": "SSR + vue-meta で hydration 直後の変更が反映されない問題の対策",
			"content_html": "",
			"date_published": "2018-06-07T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2018/02/24/221/",
			"url": "https://katashin.info/2018/02/24/221/",
			"title": "TypeScript Compiler API の基本的な使い方、コード例と作ってみたもの",
			"content_html": "<p>2月20日 (火) に <a href=https://connpass.com/event/77100/ >JavaScript メタプログラミング勉強会 Metapro.es</a> という勉強会があり、そこで TypeScript (TS) Compiler API について LT しました。内容は TS Compiler API の基本的な使い方を話したものですが、短い時間で話しきれる内容でなかったのと、TS Compiler API の日本語資料は少ないので、ここに補足記事を書いておきます。</p><iframe src=\"https://slides.com/ktsn/playing-with-typescript-compiler-api/embed?style=dark\" width=576 height=420 scrolling=no frameborder=0 webkitallowfullscreen=\"\" mozallowfullscreen=\"\" allowfullscreen=\"\"></iframe><p>また、<a href=https://github.com/ktsn/ts-compiler-api-examples>example コードを Github に置いている</a>ので、適宜参照・実行しながら読むと理解しやすいかもしれません。</p>",
			"date_published": "2018-02-24T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2017/11/12/218/",
			"url": "https://katashin.info/2017/11/12/218/",
			"title": "Vue のテンプレートの型チェックについて",
			"content_html": "<p>静的型が好きな人と話していると大体テンプレートの型をチェックしたいという話を聞くのですが、Vue には今のところそれをうまく行う方法はありません。</p><p>すこし前に Vue のテンプレートの型チェックについて LT したのですが、これは <code>vue-class-component</code> などの Vue 標準の API から離れた書き方を強制するのでちょっと微妙な感じでした。これは、以前は Vue のコンポーネントの <code>this</code> の型を得るためには、クラス構文を使う必要があったためです。</p><script async=\"\" class=speakerdeck-embed data-id=52aae13bc47a4406bbf88a5d0a39b4f3 data-ratio=1.33333333333333 src=https://speakerdeck.com/assets/embed.js></script><p>しかし、<a href=https://github.com/Microsoft/TypeScript/pull/14141>TypeScript v2.3 に導入された <code>ThisType</code></a> によって、オブジェクトリテラル内部のメソッドの <code>this</code> の型推論が行えるようになったのと、 Vue v2.5 から、<a href=https://jp.vuejs.org/2017/09/23/upcoming-typeScript-changes-in-vue-2.5/ >TypeScript の型定義が大きく改善された</a>ことで、Vue 標準の API を使っても <code>this</code> の型をうまく得ることができるようになりました。</p><p>良い機会なので、上記の LT をした時に作ったものを更新して、標準の API でもテンプレートの型チェックをできるようにしてみました。</p><ul><li><a href=https://github.com/ktsn/typed-vue-template>typed-vue-template</a> - テンプレートを TypeScript として script ブロックに挿入する実装部分</li><li><a href=https://github.com/ktsn/typed-vue-loader>typed-vue-loader</a> - typed-vue-template を webpack loader のインターフェースとして提供してるもの</li></ul><p>typed-vue-loader をクローンして、<code>npm i &amp;&amp; npm run example:dev</code> した後に、<code>example</code> ディレクトリのソースを編集すると動作がわかると思います。</p><blockquote class=twitter-tweet data-lang=en><p lang=und dir=ltr><a href=https://t.co/8DDeyFFnK9>pic.twitter.com/8DDeyFFnK9</a></p>— katashin (@ktsn) <a href=\"https://twitter.com/ktsn/status/929578774668066816?ref_src=twsrc%5Etfw\">November 12, 2017</a></blockquote><script async=\"\" src=https://platform.twitter.com/widgets.js charset=utf-8></script><p>ただし、この実装ではコンパイル時に型チェックができるだけであり、エラーが発生している場所はわからないですし、エディタ上で型情報を利用した補完機能を利用するということはできないです。そのあたりを頑張ろうとして作ったのが以下です。</p><p><a href=https://github.com/ktsn/vue-template-diagnostic>vue-template-diagnostic</a></p><p>vue-template-diagnostic の発想は Angular と同じで、自分で型チェッカーを作ってしまおうというものです。しかし、やはり型チェッカーを再実装するのは結構つらいのと、Vue のテンプレート内部の式はほぼ JavaScript と互換性を持っているので、うまいこと TypeScript で処理させたほうが良さそうだなーと感じ、開発は止まってます。</p><p>エラーの場所がわからないということに目をつぶれば、typed-vue-template でやっていることを AST に対して行う Language Service を作れば、テンプレートの型チェック処理を TypeScript に丸投げしつつエディタからその情報を取れそうな気がするので、次はそれを試してみようかなと考えてます。</p>",
			"date_published": "2017-11-12T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/06/06/213/",
			"url": "https://katashin.info/2016/06/06/213/",
			"title": "TypeScript の恩恵を受けつつ Vue を使いたい その2 (Value オブジェクトを扱う)",
			"content_html": "<p>せっかく型のある TypeScript を使うなら、なるべくプリミティブを使わずに Value オブジェクトを使いたいです。この記事では TypeScript + Vue で Value オブジェクトを扱うためにいろいろと考えたり、試したことを述べます。この記事に書かれていることは Vue 公式にはあまり推奨されない (と思われる) 方法なので、採用する場合には注意してください。</p>",
			"date_published": "2016-06-05T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/05/29/199/",
			"url": "https://katashin.info/2016/05/29/199/",
			"title": "TypeScript の恩恵を受けつつ Vue を使いたい その1",
			"content_html": "<p>最近 TypeScript の恩恵を受けつつ Vue を使うためにいろいろと試行錯誤しています。この記事ではコンポーネントの定義と、コンポーネント内のロジックを再利用可能にするための Mixin の定義を TypeScript でどのように書けばよいかを述べます。</p>",
			"date_published": "2016-05-29T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/05/22/196/",
			"url": "https://katashin.info/2016/05/22/196/",
			"title": "vq にイベントハンドリングの機能を実装した",
			"content_html": "<p>vq v1.1.1 をリリースしました。主な更新点は、イベントハンドリング機能の追加です。</p><ul><li><a href=https://github.com/ktsn/vq>vq - Github</a></li><li><a href=https://www.npmjs.com/package/vq>vq - npm</a></li></ul><p>vq は複雑なアニメーションを簡潔に書くことを目指しているライブラリで、関数型プログラミングの考え方から着想を得ています。今回の更新点以外の詳しい説明は<a href=https://katashin.info/2016/02/08/159>以前書いた記事</a>や README を参照してください。</p>",
			"date_published": "2016-05-22T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/04/24/190/",
			"url": "https://katashin.info/2016/04/24/190/",
			"title": "Vue のコンポーネントと Vuex Store を繋げるためのヘルパ vuex-connect を作った",
			"content_html": "<p>某勉強会中にネタを思いついて、急いで作って LT してその日のうちに npm にアップしたら、翌日<a href=https://twitter.com/vuejs/status/722662426030817281>Vue 公式に紹介されていて</a>だいぶビビったやつです。<a href=https://jp.vuejs.org/ >Vue</a> と <a href=https://github.com/vuejs/vuex>Vuex</a> のヘルパなので、この二つを理解していることが前提になります。</p><p><a href=https://github.com/ktsn/vuex-connect>vuex-connect (Github)</a> <a href=https://www.npmjs.com/package/vuex-connect>vuex-connect (npm)</a></p>",
			"date_published": "2016-04-24T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/04/10/184/",
			"url": "https://katashin.info/2016/04/10/184/",
			"title": "テキスト編集における Selective Undo を実装した",
			"content_html": "<p>Selective Undo とは、その名の通り Undo したい処理を選択することのできる Undo のことです。テキスト編集で Selective Undo をするためのライブラリを書いたので、それを実装する上で学んだことを簡単に述べます。</p><ul><li><a href=https://github.com/ktsn/selective-undo-text>ktsn/selectivie-undo-text: A Selective Undo library for text editing (Github)</a></li><li><a href=https://www.npmjs.com/package/selective-undo-text>selective-undo-text (npm)</a></li><li><a href=http://codepen.io/ktsn/pen/qZxdaY>Demo</a></li></ul>",
			"date_published": "2016-04-10T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/03/28/179/",
			"url": "https://katashin.info/2016/03/28/179/",
			"title": "webpack + Testem でフロントエンド JavaScript のテストを書く",
			"content_html": "<p>webpack を使っているプロジェクトで、テストコードも webpack で依存関係の解決やトランスパイルをしたいということがあったので、その時に行ったことをまとめます。</p>",
			"date_published": "2016-03-28T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/03/05/172/",
			"url": "https://katashin.info/2016/03/05/172/",
			"title": "Vue.js における methods の this は自動的に VM に束縛される",
			"content_html": "<p>執筆当時の環境</p><ul><li>Vue.js v1.0.17</li></ul><p>JavaScript で以下の様なコードを書いた時、<code>onResize</code> 内の <code>this</code> はグローバルオブジェクト (<code>window</code>) となり、<code>this.log('resized')</code> はエラーとなります。</p><pre class=language-js tabindex=0><code class=language-js><span class=\"token keyword\">const</span> obj <span class=\"token operator\">=</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token function-variable function\">log</span><span class=\"token operator\">:</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">str</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    console<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>str<span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n\n  <span class=\"token function-variable function\">onResize</span><span class=\"token operator\">:</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">event</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token string\">'resized'</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span>\n\nwindow<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'resize'</span><span class=\"token punctuation\">,</span> obj<span class=\"token punctuation\">.</span>onResize<span class=\"token punctuation\">)</span> <span class=\"token comment\">// this.log('resized') でエラー</span></code></pre><p>上記のコードの <code>obj</code> を、以下のように Vue.js の VM にするとエラーが起きなくなり、意図した通りに動作するようになります。また、<code>this</code> の値は <code>obj</code> に束縛されています。</p><pre class=language-js tabindex=0><code class=language-js><span class=\"token keyword\">const</span> obj <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Vue</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n  <span class=\"token literal-property property\">methods</span><span class=\"token operator\">:</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token function-variable function\">log</span><span class=\"token operator\">:</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">str</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n      console<span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span>str<span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n\n    <span class=\"token function-variable function\">onResize</span><span class=\"token operator\">:</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">event</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n      <span class=\"token keyword\">this</span><span class=\"token punctuation\">.</span><span class=\"token function\">log</span><span class=\"token punctuation\">(</span><span class=\"token string\">'resized'</span><span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n\nwindow<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'resize'</span><span class=\"token punctuation\">,</span> obj<span class=\"token punctuation\">.</span>onResize<span class=\"token punctuation\">)</span> <span class=\"token comment\">// エラーが発生しない</span></code></pre><p>このことから、Vue.js では methods に渡された関数を VM に束縛していると言えます。実際にコードを追って確かめてみます。</p><p>methods で Github のリポジトリを検索すると、 <code>_initMethods</code> というメソッドが存在しているのがわかります。このメソッドは <code>src/instance/internal/state.js</code> の中に定義されています。<code>_initMethods</code> の中を見てみると、<code>bind(methods[key], this)</code> という記述があります。JavaScript ネイティブの <code>bind</code> ではないですが、どうやらここで <code>this</code> を束縛しているように見えます。</p><p><a href=https://github.com/vuejs/vue/blob/521e8d2754c2e7f172c3c9702fdb74fe993027fb/src/instance/internal/state.js#L258-L265>vue/state.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue</a></p><p>なぜわざわざ独自の bind 関数を使っているのかを調べるために、この <code>bind</code> の定義も見てみました。<code>bind</code> は <code>src/util/lang.js</code> に定義されています。</p><p><a href=https://github.com/vuejs/vue/blob/521e8d2754c2e7f172c3c9702fdb74fe993027fb/src%2Futil%2Flang.js#L212-L229>vue/lang.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue</a></p><p>関数の上のコメントには、ネイティブの bind よりも早いと書いてあります。少し調べてみたところネイティブの bind は、this の束縛の他に型チェックや、引数の束縛なども行うため遅くなっているとのことでした。ただ、遅いとはいってもほとんどのケースでは気にならない違いだと思うので、普通にアプリケーションを作る際には気にしなくても良さそうです。</p><p><a href=http://stackoverflow.com/questions/17638305/why-is-bind-slower-than-a-closure>javascript - Why is bind slower than a closure? - Stack Overflow</a></p>",
			"date_published": "2016-03-05T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/02/28/164/",
			"url": "https://katashin.info/2016/02/28/164/",
			"title": "JavaScript ライブラリを npm で公開するためにやっていること",
			"content_html": "<p>最近、何度か自作のライブラリを npm にアップしています。その時にやっていることを書き留めておきます。</p>",
			"date_published": "2016-02-27T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/02/08/159/",
			"url": "https://katashin.info/2016/02/08/159/",
			"title": "複雑なアニメーションとそれに伴う処理を簡潔に書くことのできるライブラリ vq を作った",
			"content_html": "<p>最近 JavaScript のアニメーションの実装につらみを感じていたので、それを解消するためにライブラリを作りました。 vq というライブラリで、Velocity.js というライブラリのヘルパーという位置づけです。 内部のアニメーションは Velocity.js にまかせていて、vq は記述を簡潔に書けるようにしています。</p><p><a href=https://github.com/ktsn/vq>vq - GitHub</a> <a href=https://www.npmjs.com/package/vq>vq - NPM</a></p>",
			"date_published": "2016-02-07T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/01/31/136/",
			"url": "https://katashin.info/2016/01/31/136/",
			"title": "XCode で動作環境に応じて API の URL などの設定を変更する",
			"content_html": "<p>OSX, iOS アプリでも Rails の RAILS_ENV のように、動作環境に応じて値を変えたいという時があります。 この記事では、アプリから利用する Web API の URL を動作環境に応じて切り替えるのを例に、そのやり方を説明します。執筆時の開発環境は下記のとおりです。</p><ul><li>XCode: Version 7.2</li><li>Swift: version 2.1.1</li></ul>",
			"date_published": "2016-01-31T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/01/16/126/",
			"url": "https://katashin.info/2016/01/16/126/",
			"title": "指定した行数でテキストを省略できるライブラリ Truncator を作った",
			"content_html": "<p>N 文字目以降を省略するというライブラリはたくさんあるのですが、行数指定できるものは見かけないので作りました。N 行以上になった時は省略したいというケースは結構ある気がするんですが、なぜそういうライブラリは無いのだろう......。</p><p><a href=https://www.npmjs.com/package/truncator>Truncator - NPM</a> <a href=https://github.com/ktsn/truncator>Truncator - Github</a></p>",
			"date_published": "2016-01-16T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2016/01/09/121/",
			"url": "https://katashin.info/2016/01/09/121/",
			"title": "Bookshelf のアソシエーションで発生する循環読み込みによるエラーを回避する",
			"content_html": "<p>Node.js の ORM である Bookshelf では<a href=http://bookshelfjs.org/#associations>公式サイト</a>の例のようにアソシエーションを定義することができます。 しかし、モデルの定義を別ファイルに分けるとこの例が動かなくなります。 この記事では、モデルの定義を別ファイルに分けても意図通りに動くコードの書き方の説明と、それを支援する Bookshelf プラグインの紹介をします。</p>",
			"date_published": "2016-01-09T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2015/11/28/97/",
			"url": "https://katashin.info/2015/11/28/97/",
			"title": "State パターンでアニメーションの挙動を制御する",
			"content_html": "<p>あるオブジェクトが絶えずアニメーションをするようなコードを書く時、様々な状態に応じて挙動を変えたいというのはよくあることだと思います。単純に実装すると、状態に対応するフラグを記録しておき、それに応じて条件分岐するという書き方になると思います。しかし、状態の数が増えると、フラグ管理が大変になり、コードがどんどん汚くなっていきます。そこで、State パターンのように、状態ごとに別々の挙動を行う関数を定義し、オブジェクトの状態が遷移した時に実行する関数を切り替えるようにすると、コードが状態ごとに整理され、見やすくなります。</p>",
			"date_published": "2015-11-28T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2015/10/18/72/",
			"url": "https://katashin.info/2015/10/18/72/",
			"title": "Vue.js を使った中規模 Web アプリ向けのディレクトリ構造を考えた",
			"content_html": "<p>最近 Vue.js を使って Web アプリを書いていて、どんなディレクトリ構造だと良いんだろうなーということを考えた結果を書きとめようと思います。<a href=https://docs.google.com/document/u/1/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub>Angular Best Practice for App Structure</a>っぽい感じです。</p>",
			"date_published": "2015-10-18T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2015/01/05/58/",
			"url": "https://katashin.info/2015/01/05/58/",
			"title": "2014年に読んだ技術書の中で良いと思ったもの三冊",
			"content_html": "<p>もう年明けてしまいましたが、僕が2014年に読んだ技術書の中で良いと思ったものを三冊紹介します。ちなみに、筆者は普段 Web 系のコードを主に書いているので、読んでる本もそういったものに偏り気味です。</p>",
			"date_published": "2015-01-04T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2014/07/05/47/",
			"url": "https://katashin.info/2014/07/05/47/",
			"title": "Objective-C でデリゲートメソッド内でコールバックできるようにする",
			"content_html": "<p>Objective-C では非同期処理の完了後の処理をデリゲートメソッドで行うものがあります。デリゲートメソッドで処理するのは、単純なシステムなら問題ありませんが、システムが複雑になってくると、1つのデリゲートメソッド内に多くの条件分岐ができて、コードが読みにくくなる場合があります。こういった時にはデリゲートメソッドの代わりにコールバック関数を用いることで、コードがすっきりします。既に存在するライブラリが、非同期処理完了後の処理をデリゲートメソッドで実行するようにしている場合は、デリゲートメソッド内でコールバック関数を呼ぶようなラッパークラスを書く必要があります。コールバック関数については以前の投稿を読むと分かるかもしれません (<a href=http://katashin.info/2014/01/15/22>Objective-C でコールバックを持つメソッドを実装する方法について</a>)。</p><p>非同期処理完了後の処理をデリゲートメソッドで行うと以下のようになります。例として、WebSocket ライブラリの <a href=https://github.com/square/SocketRocket>square/SocketRocket</a> を用いています。今回は説明を簡単にするために、クライアントが送信したメッセージを受け取ったら、即座に同一のメッセージを返すサーバーを仮定します。</p><pre class=language-objc tabindex=0><code class=language-objc><span class=\"token comment\">// メッセージ送信</span>\n<span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span>sendMessage<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>NSString <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span>message <span class=\"token punctuation\">{</span>\n  <span class=\"token punctuation\">[</span>_socket send<span class=\"token punctuation\">:</span>message<span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// メッセージ受信 (SocketRocket のデリゲートメソッド)</span>\n<span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span>webSocket<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>SRWebSocket <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span>webSocket didReceiveMessage<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>id<span class=\"token punctuation\">)</span>message <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// メッセージ受信後の処理を書く</span>\n  <span class=\"token function\">NSLog</span><span class=\"token punctuation\">(</span><span class=\"token string\">@\"%@\"</span><span class=\"token punctuation\">,</span> message<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre><p>メッセージ受信後に1つの処理のみを行うのであれば上記で充分ですが、場合によって異なる種類の処理を行いたい場合は工夫をする必要があります。例えば、サーバーから受け取るメッセージの種類によって処理を分けたい場合は、サーバーから受け取ったメッセージ内に、type のようなパラメータを加えて、デリゲートメソッド内で条件分岐をするということが考えられます。また、上記の sendMessage メソッドを呼び出すクラスが複数あり、クラス毎にメッセージ受信後の処理が違うというような場合は、デリゲートメソッドのみでは対応できません。こういった場合は、NSNotificationCenter を使った通知で条件分岐をするとうまくいきます。また、sendMessage メソッドの引数でコールバック関数のポインタを渡すようにし、デリゲートメソッド内でそのコールバック関数を呼ぶことでも対応できます。今回は、デリゲートメソッド内でこのコールバック関数を呼ぶ方法を説明します。</p><p>デリゲートメソッド内でコールバック関数を呼ぶには、インスタンス変数にコールバック関数を保存する必要があります。また、非同期処理を行うメソッドは並列で実行される可能性があるため、コールバック関数は複数保存可能で、かつ、デリゲートメソッド内で対応するコールバック関数を判別可能であるべきです。これらは以下の様なコードで実現できます。ここでも、サーバーはクライアントから受け取ったメッセージを即座にそのまま返すと仮定します。また、NSDictionary と JSON の変換部分は擬似コードです。インスタンス変数の初期化部分も適当なので、自分で実装する時は書き換えることをおすすめします。</p><pre class=language-objc tabindex=0><code class=language-objc><span class=\"token comment\">// インスタンス変数</span>\n<span class=\"token keyword\">@property</span> NSMutableDictionary <span class=\"token operator\">*</span>callbacks<span class=\"token punctuation\">;</span>\n<span class=\"token keyword\">@property</span> <span class=\"token keyword\">int</span> <span class=\"token operator\">*</span>callbackID<span class=\"token punctuation\">;</span></code></pre><pre class=language-objc tabindex=0><code class=language-objc><span class=\"token comment\">// 初期化</span>\n<span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span>initialize <span class=\"token punctuation\">{</span>\n  _callbacks <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span>NSMutableDictionary dictionary<span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n  _callbackID <span class=\"token operator\">=</span> <span class=\"token number\">0</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// メッセージ送信</span>\n<span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span>sendMessage<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>NSString <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span>message completion<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">^</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">(</span>NSString <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">)</span>completion <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// メッセージに _callbackID を含める</span>\n  NSDictionary <span class=\"token operator\">*</span>dic <span class=\"token operator\">=</span> <span class=\"token operator\">@</span><span class=\"token punctuation\">{</span><span class=\"token string\">@\"message\"</span><span class=\"token punctuation\">:</span> message<span class=\"token punctuation\">,</span> <span class=\"token string\">@\"id\"</span><span class=\"token punctuation\">:</span> <span class=\"token punctuation\">[</span>NSNumber numberWithInt<span class=\"token punctuation\">:</span> _callbackID<span class=\"token punctuation\">]</span><span class=\"token punctuation\">}</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token comment\">// _callbackID をキーとして、コールバック関数をインスタンス変数に保存</span>\n  <span class=\"token punctuation\">[</span>_callbacks setObject<span class=\"token punctuation\">:</span>completion forKey<span class=\"token punctuation\">:</span><span class=\"token punctuation\">[</span>NSNumber numberWithInt<span class=\"token punctuation\">:</span>_callbackID<span class=\"token punctuation\">]</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n  _callbackID<span class=\"token operator\">++</span><span class=\"token punctuation\">;</span>\n  <span class=\"token punctuation\">[</span>_socket send<span class=\"token punctuation\">:</span><span class=\"token punctuation\">[</span>dic json<span class=\"token punctuation\">]</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span> <span class=\"token comment\">// NSDictionary を JSON に変換して送信 (擬似コード)</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token comment\">// メッセージ受信 (SocketRocket のデリゲートメソッド)</span>\n<span class=\"token operator\">-</span> <span class=\"token punctuation\">(</span><span class=\"token keyword\">void</span><span class=\"token punctuation\">)</span>webSocket<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>SRWebSocket <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span>webSocket didReceiveMessage<span class=\"token punctuation\">:</span><span class=\"token punctuation\">(</span>id<span class=\"token punctuation\">)</span>message <span class=\"token punctuation\">{</span>\n  NSDictionary <span class=\"token operator\">*</span>dic <span class=\"token operator\">=</span> <span class=\"token punctuation\">[</span>message dictionary<span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n  <span class=\"token comment\">// JSON を NSDictionary に変換 (擬似コード)</span>\n  <span class=\"token comment\">// ID に対応するコールバック関数を取得</span>\n  <span class=\"token keyword\">void</span> <span class=\"token punctuation\">(</span><span class=\"token operator\">^</span>callback<span class=\"token punctuation\">)</span><span class=\"token punctuation\">(</span>NSString <span class=\"token operator\">*</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=</span> _callbacks<span class=\"token punctuation\">[</span>dic<span class=\"token punctuation\">[</span><span class=\"token string\">@\"id\"</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">;</span>\n\n  <span class=\"token comment\">// コールバック関数を呼ぶ</span>\n  <span class=\"token function\">callback</span><span class=\"token punctuation\">(</span>dic<span class=\"token punctuation\">[</span><span class=\"token string\">@\"message\"</span><span class=\"token punctuation\">]</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span>\n<span class=\"token punctuation\">}</span></code></pre><p>上記のコードのように、コールバック関数毎に一意な ID を割り当てることができ、デリゲートメソッド内でこの ID を取得することができれば、デリゲートメソッド内でコールバック関数を呼ぶことができます。今回の例では、インスタンス変数に NSMutableDictionary 型の変数 (callbacks) と int 型の変数 (callbackID) を用意してこれを行いました。新しいコールバック関数が与えられる度に、現在の callbackID の値をキーとして、コールバック関数を callbacks 内に保存しています (14行目)。また、サーバーへ送信するメッセージ内に callbackID の値を含めています (10-11行目)。サーバーはメッセージをそのまま返すので、メッセージに含めた callbackID の値をデリゲートメソッド内で取得することができます (25行目)。取得した callbackID を用いてコールバック関数を取得し、実行すれば完了です (25-28行目)。</p><p>今回はサーバーがメッセージをそのまま返すものだったのでこのままでは役に立ちませんが、あるパラメータに与えられた値のみはそのまま返すようなサーバーにすることで実用的になります。また、最終的にデリゲートメソッド内でコールバック関数を取得できれば良いので、考えれば色々なパターンで実装できそうです。</p>",
			"date_published": "2014-07-05T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2014/02/09/24/",
			"url": "https://katashin.info/2014/02/09/24/",
			"title": "box-sizing: border-box; を指定している時の jQuery UI Resizable の調整方法",
			"content_html": "<p>CSS の幅や高さ指定を直感的なものにしてくれる border-box というものがありますが、これを使うと jQuery UI の Resizable の挙動がおかしくなります。この問題は短くて単純なコードで解決することができます。</p><p>通常、CSS の width や height は HTML 要素のコンテンツのサイズを指定します。つまり、border や padding の値は width や height には含まれません。例えば、width が 100px で、左右の padding が 20 px の HTML 要素を表示してみると、見た目上の幅は 140px となるわけです。これは CSS を書いてると結構気持ち悪く感じることがあると思います(他の要素との組み合わせを考える時にめんどくさかったり)。</p><p>上記の問題(?)はCSS で、box-sizing: border-box; を指定することにより解決することができます。つまり、width や height の値を padding や border も含んだものとして指定できるようになります。先の例の width が 100px で、左右の padding が 20px の HTML 要素を考えると、見た目の幅が 100px となり、要素内のコンテンツの幅が 60px になります。</p><p>しかし、box-sizing を border-box にすると、jQuery UI の Resizable の挙動が気持ち悪くなります。具体的には、HTML 要素のサイズを変更するためにドラッグを開始すると、padding や border の値だけ大きさがずれます。このせいで、ドラッグ中のマウスポインタの座標と、HTML 要素の端が一致しないのですごく気持ち悪いです。</p><p>Resizable の border-box 問題に関しては、jQuery 側ではまだ対応されていないみたいなので、自分で対応してみました。以下がそのコードです。jQuery UI のコードを読み解くのは嫌だったので、普通な感じの解法です。$el が border-box と resizable の両方を適用している HTML 要素の jQuery オブジェクトです。</p><pre class=language-javascript tabindex=0><code class=language-javascript>$el<span class=\"token punctuation\">.</span><span class=\"token function\">on</span><span class=\"token punctuation\">(</span><span class=\"token string\">'resize'</span><span class=\"token punctuation\">,</span> <span class=\"token keyword\">function</span> <span class=\"token punctuation\">(</span><span class=\"token parameter\">ev<span class=\"token punctuation\">,</span> ui</span><span class=\"token punctuation\">)</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token comment\">// box-sizing: border-box; に対応</span>\n  ui<span class=\"token punctuation\">.</span>element<span class=\"token punctuation\">.</span><span class=\"token function\">height</span><span class=\"token punctuation\">(</span>ui<span class=\"token punctuation\">.</span>size<span class=\"token punctuation\">.</span>height<span class=\"token punctuation\">)</span>\n  ui<span class=\"token punctuation\">.</span>element<span class=\"token punctuation\">.</span><span class=\"token function\">width</span><span class=\"token punctuation\">(</span>ui<span class=\"token punctuation\">.</span>size<span class=\"token punctuation\">.</span>width<span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span></code></pre><p>HTML 要素がリサイズされた時に発生するイベント &quot;resize&quot; に対してイベントハンドラを割り当て、この中で width や height の値を修正しています。&quot;resize&quot; イベントのイベントハンドラは第2引数に、ui オブジェクトが与えられます。ui.element はリサイズ対象の HTML 要素の jQuery オブジェクトで、ui.size はリサイズ後の HTML 要素のサイズを示しています。ui.size のそれぞれの値を ui.element の width、height 関数に与えるだけで border-box に対応することができます。これは、ui.size の値が padding や border の値を考慮していない値になっており、さらに、jQuery の width、height 関数は同様に padding や border の値を含まないのサイズを指定するようになっているためです(border-box っぽく指定したい時は css 関数で指定できます)。</p><p>border-box と Resizable を併用することで発生するサイズの問題は &quot;resize&quot; イベントのイベントハンドラで HTML 要素の正しいサイズを設定してやると解決できました。偶然だと思いますが、結構短いコードで解決できたのが面白いです。初めに真面目に padding と border を取得していたのは何だったのかと。</p>",
			"date_published": "2014-02-09T00:00:00Z"
		}
		,
		{
			"id": "https://katashin.info/2014/01/15/22/",
			"url": "https://katashin.info/2014/01/15/22/",
			"title": "Objective-C でコールバックを持つメソッドを実装する方法について",
			"content_html": "<p>非同期に結果が返ってくる処理を書く場合、Objective-C では @Protocol を定義して、デリゲートメソッド内で結果をもらうのが良いのかもしれませんが、JavaScript を書いてるとコールバックで結果を取得したいと考えてしまいます。この記事では、Objective-C でコールバック付きのメソッドを作る方法を解説します。(コールバックって呼んでいいのかわかりませんが)</p>",
			"date_published": "2014-01-15T00:00:00Z"
		}
		
	]
}
