概要
この記事は、Most Creative Expo App Awards受賞作「Callie」がどのようにしてExpo、React Native Reanimated、React Native Skiaのシェーダーを用い、温かく感情面で安全なモバイル体験を実現しているかを紹介するゲスト投稿です。著者はNabiの創業エンジニアでありSenior React Native EngineerのDaehyeon Munです。
Callieとは
Callieは摂食障害から回復する人々を支援するための、感情的に知的なセルフケアコンパニオンアプリです。アプリは起動した瞬間から温かく落ち着いた魅力的な体験を提供することに重点を置いています。Expo、React Native Reanimated、React Native Skiaを活用することで、微妙なビジュアル表現や感情豊かなインタラクションをネイティブ品質で実現し、ユーザーが日常のルーティンを安心感と安定感を持って進められるように設計されています。
製品はエンドツーエンドで完全なネイティブ品質のモバイル体験として構築され、インターフェースやインタラクションの言語、モーションデザイン、シェーダーベースのビジュアルまで、摂食障害回復に伴う特有のニーズを考慮して細部まで作り込まれています。
受賞: Most Creative App of the year - Callie
Callieは2025年のExpo App AwardsでMost Creative Appに選出されました。思慮深いデザインと独特なインタラクションパターンが評価され、現在App Storeで3.2k+のレビュー、平均評価は4.9+と高いエンゲージメントとポジティブな評価を受けています。
一人のモバイルチームがCallieの体験を作るまで
私はNabiに創業エンジニアとして参加し、モバイルアプリ開発全責任を負いました。微妙なインタラクションやアニメーションからシェーダーベースの視覚効果、アプリ全体の雰囲気に至るまで、モバイル体験を一人で設計・実装しました。
洗練されたプロダクトは偶然に生まれるものではありません。細部への目配り、さまざまなアイデアを試す意欲、何度も繰り返すチューニングが必要です。こうした手間には時間と集中が必要であり、初期段階のスタートアップではその投資スペースを見つけるのが最も難しいことが多いです。本記事では、初期段階のスタートアップで一人のモバイル開発者として高速な開発速度を維持しつつ、高品質な体験を提供するために私が取ったアプローチを共有します。
摂食障害の配慮を踏まえた設計と開発
Callieの設計には、摂食障害から回復する人々の特性や感受性を慎重に理解することが不可欠でした。この領域の多くのユーザーは感情的にも脆弱になりやすく、わずかな視覚変化やインタラクションのパターンが「支えになるか」「圧倒するか」を左右します。したがって、全体の体験は唐突な遷移や不要な刺激を避け、一定で予測可能なフローを維持する必要がありました。
デザイン的には、落ち着いたカラーパレット、穏やかな遷移、要求的・侵入的に感じないインタラクションパターンを優先しました。技術的には、静的な画面だけでは意図した微細な落ち着きやニュアンスを伝えきれませんでした。より洗練されたモーションや表現的な視覚要素が、求められる感情的質を届けるために必要でした。
表現力のあるビジュアルのためのカスタムシェーダー
Callieで実現したい体験は静的な画面だけでは表現できませんでした。摂食障害から回復する人々が使うことを考えると、インターフェースの多くは心理的に快適に感じられる必要があり、穏やかで安定し、急激な視覚変化がないことが重要でした。React Native Skiaのシェーダーは、このレベルの微妙さを達成するための自然な選択肢でした。
LottieやRiveなどの手法も検討しました。これらは事前にデザインされたモーショングラフィックには優れていますが、ユーザー入力にリアルタイムで応答したり、心理的ニュアンスをフレームごとに合わせるようなインタラクションには向いていません。
カスタムシェーダーが必要だった理由
Callieでは、各動きが瞬間ごとにどのように感じられるかをピクセルレベルで制御する必要がありました。つまり、単にアニメーションを再生するのではなく、ユーザーの操作に応じてコンテンツを変形させる必要がありました。Skiaシェーダーは、物理的に根ざした動きや柔らかく有機的な遷移を可能にし、従来のアニメーションツールでは実現できない表現力と柔軟性を提供してくれました。
ピクセルレベルの制御: “Callie’s journal about you” の例
Callieの重要な機能の一つに「Callie’s journal about you」があります。この機能はユーザーの活動をAIで解析し、日次の要約と励ましのメッセージを手紙形式で届けます。AI駆動の機能を構築する際に最も重要な考慮点の一つは、体験が冷たく機械的に感じられないようにすることです。可能な限り人間的な温かみを担保する必要がありました。
日記をより個人的で招かれた感じにするため、画面を単なるAI生成テキストのブロックに見せるのではなく、ユーザーに宛てた丁寧な手紙のように見せたいと考えました。そこでシェーダーを使って「ページカール(page curl)」効果を追加しました。この視覚表現はAI生成機能に小さいが意味のある人間的タッチを与え、体験をより温かく、意図的で、満足感のあるものにします。
Page Curl Effect の実装方法
以下は「Callie’s journal about you」画面で使用したページカールシェーダーの説明とコードです。このシェーダーは画像を入力として受け取り、シートがゆっくりとめくれるような視覚を作ります。単にスケールやフェードを使うのではなく、ジオメトリ変換を用いてページが物理的にカールするように見せています。
uniform shader u_image ;
uniform vec2 u_resolution ;
uniform vec2 u_originPos ;
uniform vec2 u_currPos ;
const float PI = 3.14159265359 ;
const float RADIUS = 0.1 ;
const float SHADOW_EXPONENT = 0.2 ;
vec4 main ( vec2 fragCoord ) {
float aspect = u_resolution . x / u_resolution . y ;
vec2 aspectScale = vec2 ( aspect , 1.0 ) ;
vec2 uvScale = vec2 ( u_resolution . x / aspect , u_resolution . y ) ;
vec2 uv = fragCoord * aspectScale / u_resolution ;
vec2 curlTip = u_currPos * aspectScale / u_resolution ;
vec2 curlDir = normalize ( abs ( u_originPos ) - u_currPos ) ;
vec2 origin = clamp ( curlTip - curlDir * curlTip . x / curlDir . x , 0.0 , 1.0 ) ;
float curlLength = clamp ( length ( curlTip - origin ) + ( aspect - ( abs ( u_originPos . x ) / u_resolution . x ) * aspect ) / curlDir . x , 0.0 , aspect / curlDir . x ) ;
if ( curlDir . x < 0.0 ) {
curlLength = distance ( curlTip , origin ) ;
}
float dist = dot ( uv - origin , curlDir ) - curlLength ;
vec2 linePoint = uv - dist * curlDir ;
if ( dist > RADIUS ) {
return vec4 ( 0.0 , 0.0 , 0.0 , 0.0 ) ;
}
else if ( dist >= 0.0 ) {
float theta = asin ( dist / RADIUS ) ;
vec2 backPoint = linePoint + curlDir * ( PI - theta ) * RADIUS ;
vec2 frontPoint = linePoint + curlDir * theta * RADIUS ;
bool isInBounds = backPoint . x <= aspect && backPoint . y <= 1.0 && backPoint . x > 0.0 && backPoint . y > 0.0 ;
uv = isInBounds ? backPoint : frontPoint ;
vec4 fragColor = u_image . eval ( uv * uvScale ) ;
float shadowFactor = pow ( clamp ( ( RADIUS - dist ) / RADIUS , 0.0 , 1.0 ) , SHADOW_EXPONENT ) ;
fragColor . rgb * = shadowFactor ;
return fragColor ;
}
else {
vec2 mappedPoint = linePoint + curlDir * ( abs ( dist ) + PI * RADIUS ) ;
bool isInBounds = mappedPoint . x <= aspect && mappedPoint . y <= 1.0 && mappedPoint . x > 0.0 && mappedPoint . y > 0.0 ;
uv = isInBounds ? mappedPoint : uv ;
return u_image . eval ( uv * uvScale ) ;
}
}
このシェーダーは三つの領域(カールを越えた箇所、カール表面、カール前)に分けてレンダリングし、三角関数や座標変換、ジオメトリベースの変形を組み合わせて自然なページめくりを再現しています。数学的な原理を深く理解したい場合は、William Candillonの「Page Curl Tutorial」を参照することを強くおすすめします。
AIをより人間らしくする
AI駆動の体験で最も重要な側面の一つは、生成される応答のトーンを形作ることでした。モデルが正確な内容を出しても、ある言い回しは過度に直接的、機械的、あるいは感情的に不均衡に感じられることがあります。これを避けるために、AIメッセージがユーザーに届く前に、構成・温かさ・語りの間合いを調整するトーン整形レイヤーを実装しました。
具体的には、誤解を招きかねない判断的に聞こえる表現のフィルタリング、言語を非指示的かつ支援的に保つこと、そして一貫して穏やかな語り口へとAIを誘導するためのルールを含めています。これにより、体験は汎用のAIアシスタントというよりも、思慮深い伴走者に近い感覚になり、ユーザーとCallieとの感情的なつながりが強化されました。
UIにトーンを追加する
言語的なトーンに加えて、UI自体にもトーンを反映させることが重要です。色、モーション、インタラクションのリズムを調整することで、穏やかさや安心感を視覚的に伝えることができます。ページカールのような小さな視覚的タッチは、AIの出力をより人間的で意図的なものに見せる助けになります。
高品質なExpoアプリを作るヒント
- Tip 1: Use worklet-related libraries for a Native Feel
- React NativeでネイティブレベルのUIパフォーマンスを達成するために重要なのは、workletやUIスレッド上で動作するライブラリを選ぶことです。アニメーションやタッチベースのインタラクションがJavaScriptスレッドに依存すると、他の処理と競合してフレーム落ち、入力遅延、ボトルネックの原因になります。
(その他のヒントやベストプラクティスも、同様の観点で選定・採用するとよいでしょう。)
この記事は、感情的な配慮を含むプロダクト設計、カスタムシェーダーによる表現力、AIトーンの整形、そして高品質なネイティブ体験を維持するための実践的な選択についての概要を提供しました。