Expo、Reanimated、Skia で AI をモバイルアプリの中で人間らしく見せる
Most Creative Expo App Awards 受賞者の Callie が、Expo、Reanimated、Skia shader を使って、温かみのある情緒的に安全なモバイル体験をどう作り上げたかを紹介します。
日本語
コピー

この記事は、Expo App Award 受賞者でシニア React Native エンジニアの Daehyeon Mun によるゲスト投稿です。彼は React Native コミュニティで、デザインを重視したエンジニアリングスタイルと表現力豊かなインターフェースの作品で知られており、そのプロダクトはいつも「React Native でここまでできるのか」と驚かせてくれます。
...
Callie は、摂食障害からの回復を支える、感情を感知するセルフケアアプリです。
このアプリが目指すのは、温かく、穏やかで、魅力的な体験——開いた瞬間に受け入れられていると感じられるものです。Expo、React Native Reanimated、React Native Skia を活用し、Callie は繊細なビジュアルと表現力豊かなインタラクションを実現し、ユーザーが日々のルーティンの中で心地よさと安定感を得られるよう支えます。
プロダクトはゼロから構築され、完全にネイティブ品質のモバイル体験に仕上がっています。インターフェースとインタラクションの言語から、モーションデザイン、shader ベースのビジュアルエフェクトに至るまで、あらゆるディテールが、摂食障害からの回復期にある人々の特有のニーズを中心に磨き上げられています。

Callie は 2025 年 Expo App Awards の最もクリエイティブなアプリに選ばれました。受賞理由は、その深思熟慮されたデザインと独自のインタラクションパターンです。現在 App Store では 3.2k+ 件を超えるレビューがあり、平均評価は 4.9+ で、ユーザーエンゲージメントと評判の両面で際立っています。
一人で Callie のモバイル体験を支える
私は創業エンジニアとして Nabi に加わり、モバイルアプリの開発を全面的に担当しました。モバイル体験のすべてを私一人で作り上げました——細かなインタラクションやアニメーションから、shader ベースのビジュアルエフェクト、アプリ全体の手触りまで。
洗練されたプロダクトは決して偶然には生まれません。ディテールへの敏感さ、異なるアイデアを試す意欲、そして無数の微調整が必要です。このレベルの取り組みには多大な時間と集中力が要りますが、アーリーステージのスタートアップで最も難しいのは、まさにそのための余白を捻出することです。
この記事では、私が Callie の繊細で感情を感知するモバイル体験をどう構築したか——そしてアーリーステージのスタートアップで唯一のモバイル開発者として、素早いイテレーションを保ちながら、いかにして高品質なプロダクトを届けたかを書いていきます。
設計と開発で摂食障害の当事者の繊細な感情に配慮する
Callie を設計するにあたり、私たちは摂食障害の回復期にある人々の特徴と繊細さを注意深く理解する必要がありました。このグループの多くは情緒がとりわけ不安定で、ほんの小さな視覚的変化やインタラクションの仕方であっても、このアプリが支えになると感じるか、耐え難いものになるかを左右しかねません。だからこそ、体験全体は唐突なトランジションや不要な刺激を避け、常に穏やかで予測可能なリズムを保たなければなりません。
デザインの面では、落ち着いた配色、柔らかなトランジション、そして決して要求されている、踏み込まれていると感じさせないインタラクションを優先しました。技術の面では、静的なインターフェースだけでは、私たちが求める繊細さと穏やかさを伝えきれません。この体験に必要な微妙な情緒的質感を表現するには、より精緻なモーションと表現力豊かなビジュアル要素が必要でした。
カスタム shader で表現力豊かなビジュアルを実現する

私たちが Callie で実現したかった体験は、静的なインターフェースだけでは表現できません。このアプリのユーザーは摂食障害の回復期にある人々であるため、インターフェースの多くの部分が心理的に心地よく感じられる必要があります——柔らかく、穏やかで、唐突な視覚変化が一切ないように。React Native Skia Shaders は、こうした繊細な効果を実現するための自然な選択でした。
この微妙な情緒的質感をアプリに持ち込む方法を探る中で、Lottie や Rive といった他の選択肢も試しました。どちらもあらかじめ設計されたモーショングラフィックスには向いていますが、ユーザー入力にリアルタイムで反応し、心理の微妙な変化にフレーム単位で寄り添う必要があるインタラクションには適していません。
Callie では、あらゆる動きがその瞬間ごとにどう感じられるかを形作るために、ピクセル単位のコントロールが必要でした。これは、単にアニメーションを再生するのではなく、ユーザーのインタラクションに応じてコンテンツを変形させることを意味します。Skia shader は、物理に基づいた動きと、柔らかく有機的なトランジションを実現するのに必要な表現力と柔軟性を与えてくれました——これらは従来のアニメーションツールでは不可能です。
Callie の中でカスタム shader を使っている機能の一つが、「Callie からあなたへの手記」 です。これはユーザーと AI のやり取りの記録を分析し、毎日サマリーを生成して、手紙の形で励ましと前向きな言葉を届けます。AI 機能を作る上で最も大事なのは、体験を冷たく機械的なものにせず、できるだけ人間味を持たせることです。
この手記をより私的な手紙らしく、開きたくなるものにするために、私はこの画面が AI が生成した長いテキストの塊のように見えず、ユーザーだけに向けて丁寧に書かれた一通の手紙のように見えることを望みました。そこで shader を使って「ページめくり」エフェクトを加えました。
このわずかな視覚的処理が、AI が生成する機能にほんの少しではあるが確かな人間味を添え、体験全体をより温かく、より心がこもった、より完成度の高いものにしています。
ページめくりエフェクトの実装方法
/**
* Page Curl Shader Effect
*
* Simulates a realistic page curl by modeling the page as a cylinder.
* The shader divides the screen into three regions:
* 1. Beyond the curl (transparent/hidden)
* 2. The curl surface (cylindrical mapping with shadow)
* 3. Before the curl (normal flat page)
*
* Algorithm:
* - Find the "curl line" perpendicular to curl direction
* - Calculate each pixel's signed distance from the curl line
* - Map pixels onto a cylinder surface based on distance
* - Apply shadow gradient on the curled region
*
* Uniforms:
* u_image - Source texture to be curled
* u_resolution - Canvas resolution (width, height) in pixels
* u_originPos - Fixed corner where curl originates (typically bottom-right)
* u_currPos - Animated curl tip position
*/
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; // Cylinder radius in normalized space
const float SHADOW_EXPONENT = 0.2; // Controls shadow gradient falloff
vec4 main(vec2 fragCoord) {
// 1. Setup coordinate systems
float aspect = u_resolution.x / u_resolution.y;
vec2 aspectScale = vec2(aspect, 1.0); // Scale to square aspect ratio
vec2 uvScale = vec2(u_resolution.x / aspect, u_resolution.y); // For texture sampling
vec2 uv = fragCoord * aspectScale / u_resolution; // Normalized pixel coord [0,1]
// 2. Define curl geometry
vec2 curlTip = u_currPos * aspectScale / u_resolution; // Current curl tip position
vec2 curlDir = normalize(abs(u_originPos) - u_currPos); // Direction from origin to tip
// Find where the curl line intersects the left edge (y-axis)
// This is the "hinge" point where the curl begins
vec2 origin = clamp(
curlTip - curlDir * curlTip.x / curlDir.x,
0.0,
1.0
);
// 3. Calculate curl line length
// Base length from origin to tip, plus adjustment for canvas edge
float curlLength = clamp(
length(curlTip - origin) +
(aspect - (abs(u_originPos.x) / u_resolution.x) * aspect) / curlDir.x,
0.0,
aspect / curlDir.x
);
// Special case: leftward curls don't need edge adjustment
if (curlDir.x < 0.0) {
curlLength = distance(curlTip, origin);
}
// 4. Calculate pixel distance from curl line
// Project pixel onto curl direction, subtract curl length
// Positive = beyond curl, Negative = before curl, Near-zero = on curl surface
float dist = dot(uv - origin, curlDir) - curlLength;
// Find closest point on the curl line to current pixel
vec2 linePoint = uv - dist * curlDir;
// 5. Render based on distance from curl line
// REGION 1: Beyond the curl cylinder → fully transparent
if (dist > RADIUS) {
return vec4(0.0, 0.0, 0.0, 0.0);
}
// REGION 2: On the curl surface [0, RADIUS]
// Map flat coordinates onto cylinder using arc length
else if (dist >= 0.0) {
// Angle around the cylinder (from front to back)
float theta = asin(dist / RADIUS);
// Two possible texture coordinates: back side and front side of cylinder
vec2 backPoint = linePoint + curlDir * (PI - theta) * RADIUS;
vec2 frontPoint = linePoint + curlDir * theta * RADIUS;
// Use back side if it's within bounds, otherwise show front
bool isInBounds =
backPoint.x <= aspect && backPoint.y <= 1.0 &&
backPoint.x > 0.0 && backPoint.y > 0.0;
uv = isInBounds ? backPoint : frontPoint;
// Sample texture at the mapped position
vec4 fragColor = u_image.eval(uv * uvScale);
// Apply shadow gradient: darker near the curl edge (dist=RADIUS), lighter at center (dist=0)
float shadowFactor = pow(
clamp((RADIUS - dist) / RADIUS, 0.0, 1.0),
SHADOW_EXPONENT
);
fragColor.rgb *= shadowFactor;
return fragColor;
}
// REGION 3: Before the curl (dist < 0)
// Show flat page, but account for paper "consumed" by the cylinder
else {
// Offset by the cylinder's circumference (PI * RADIUS)
vec2 mappedPoint = linePoint + curlDir * (abs(dist) + PI * RADIUS);
// Use mapped point if valid, otherwise use original UV
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);
}
}
ここからは、「Callie からあなたへの手記」 画面で使われているページめくり shader について説明します。
この shader は画像を入力として受け取り、progress 値を使って紙がゆっくりと広がる視覚効果を生み出します。単に拡大縮小したりフェードイン・アウトしたりするのではなく、幾何学的な変換によって、ページが本当に巻き上がり、広がっていくように見せています。
自然なページめくりを実現するには、三角関数、座標空間の歪み、ジオメトリベースの変形を組み合わせる必要がある。これらの計算が十分になめらかにつながって初めて、アニメーションは本物の紙のような質感を帯びる。
この効果の背後にある数学をもっと深く知りたいなら、William Candillon のこのチュートリアルが断然おすすめだ:
AI をより人間らしく
AI 体験で最も重要なのは、生成される返答のトーンをどう扱うかだ。モデルの内容が正確であっても、言い回しによっては直接的すぎたり、機械的だったり、感情のバランスを欠いたりすることがある。そこで私たちはトーン調整のレイヤーを追加し、AI のメッセージをユーザーに届ける前に、その構造・温度・リズムを整えるようにした。
これには、評価と受け取られかねない表現を除外すること、言葉が常に指示的でなく支持的であるようにすること、そして AI が一貫して穏やかな語り口を保つよう導くことが含まれる。こうした調整によって、体験は汎用の AI アシスタントではなく思いやりのある相棒に近づき、ユーザーと Callie の感情的なつながりも強くなった。
質の高い Expo アプリの作り方
テクニック 1:worklet 系のライブラリでネイティブの操作感を得る
React Native でネイティブ級の UI パフォーマンスを出すために最も重要なのは、worklet と UI スレッドで動くライブラリを選ぶことだ。アニメーションやタッチ操作が JavaScript スレッドに依存していると、他の処理とリソースを奪い合い、フレーム落ち、入力の遅延、パフォーマンスのボトルネックを招きやすい。
細かなインタラクションが体験の核となる Callie のようなアプリでは、こうした一見小さなパフォーマンス問題が製品全体の操作感をすぐに損なってしまう。
私が強くおすすめするライブラリのひとつが、Mateusz Łopaciński による react-native-sortables だ。
react-native-reanimated と react-native-gesture-handler をベースに作られており、ドラッグ&ドロップの操作は UI スレッド上で直接動くのでなめらかで反応が速く、カスタマイズの幅も広い。
Callie ではこのライブラリでドラッグ&ドロップ式の ToDo リストを実装し、複雑なジェスチャーロジックを一から書くことなく、安定した高品質な UX を提供できている。
テクニック 2:心地よいアニメーションを学ぶ
繊細で心地よいアニメーションを上達させる最善の方法は、とてもシンプルだ。
頭に思い描いたインタラクションを、自分の手で作ってみる。
スキルを磨き続けるために、私はかつて週に少なくともひとつ、新しい UI やインタラクションを実装していた。手を動かすことに勝る近道はなく、試すたびに動き・リズム・視覚的なディテールに対する直感が広がっていく。
以下は私が強くおすすめする学習リソースだ:
彼らのチュートリアルは、核心となる概念やパターンを学ぶのに向いている。基礎を固めたら、同じ考え方で自分のアニメーションを作ってみるといい。そうした練習を重ねるうちに自然と自信がつき、実装できるインタラクションの幅も広がっていく。
時間はユーザーに使う
Callie チームに加わって最初の週で、私が下した最も重要な技術判断のひとつは、既存の React Native CLI プロジェクトを Expo に移行することだった。以前に RN CLI プロジェクトを保守していた経験から、ネイティブ設定の衝突、ビルドの不安定さ、インフラ保守の問題がどれほど頻繁に起き、どれだけのエンジニアリング時間を食うかはよく分かっていた。しかもその投資はユーザー体験には何も寄与しない。
小さなチームで、アプリ全体を担当するエンジニアは私ひとりだったので、速くて信頼できる実行速度が最優先だった。質の高い体験を作るには大量の反復の中で細部を調整し続ける必要があり、私の時間はユーザーが実際に感じ取れる改善に使わなければならない。Expo のマネージドワークフローは、モバイル開発で通常避けて通れないあれこれの手間を省き、初期開発段階をかなり速くしてくれた。
Expo の継続的ネイティブ生成(CNG)
Expo CNG は、これまでネイティブ設定の管理やプラットフォーム固有の保守に費やしていた時間を大幅に短縮した。おかげでエンジニアリング時間の大半を、ユーザー向けの改善、機能開発、UX の最適化、インタラクションの作り込みに充てられている。
アプリ全体をエンジニアひとりで開発しているとき、時間の使い方の効率が製品の生死を直接左右する。CNG を採用してからは、インフラ保守ではなく、ユーザーに直接価値を生む開発に力を注げるようになった。
Expo のビルドサービス
Expo のビルドサービスはリリースフローをさらに簡素化してくれた。コマンドひとつで安定した iOS / Android ビルドを生成し、それぞれのストアに公開できる。ビルドサービスは信頼できて一貫性があり、時間もはるかに節約できるデプロイフローを提供してくれる。
戦略的な選択:OTA 更新で素早く反復する
新機能を追加するたびに App Store の審査を何日も待つと、製品の成長が遅れる。素早い試行錯誤が必要な初期段階では特にそうだ。このボトルネックを避けるため、私たちは最初のリリースに近いうち使うライブラリをすべて同梱し、以降の新機能や改善は Expo の OTA 更新 で届けるようにした。リリースの遅延は一切ない。
ユーザー数がまだ少ないうちは、このやり方でユーザーのフィードバックを素早く取り込め、製品は毎週目に見える形で変わっていった。この短いフィードバックループは Callie の初期開発にとって決定的に重要で、ユーザーが本当に必要とする形へと育てていくことができた。
Expo のインフラを早い段階で採用したことで、私たちはエンジニアリングのオーバーヘッドを大幅に削減でき、浮いた時間をそのままユーザーが実感できる改善に注ぎ込めた。その結果、短期間でよく練られた質の高いプロダクトができあがった。従来の React Native のやり方では、これははるかに難しい。
小さなチームへのアドバイス
この記事が、Callie のように、より速く洗練されたプロダクトを作りたいと考えている小さなチームや個人開発者の助けになればいい。小さなチームでは、実験と反復のための余白を十分に確保し、プロダクトの手触りを決める細部を磨き上げることが何より大事だ。
Expo はそれを可能にしてくれた。モバイル開発をたいてい遅らせるあれこれを減らし、本当に重要なもの——体験そのもの——に時間を割けるようにしてくれる。
小さなチームで、品質を犠牲にせず速く進みたいなら、Expo は安心して勧められるプラットフォームだ。