Expo、Reanimated、SkiaでモバイルアプリのAIを人間らしく感じさせる方法
Users • React Native • Development • January 15, 2026 • 13分で読める
Daehyeon Mun ゲスト著者
Most Creative Expo App Awards受賞作品であるCallieが、Expo、Reanimated、Skiaシェーダーを使用して、温かく感情的に安全なモバイル体験を提供する方法をご紹介します。
これは、Expo App Award受賞者であるDaehyeon Mun氏(シニアReact Nativeエンジニア)によるゲスト投稿です。React Nativeコミュニティでは、デザイン重視のエンジニアリングと表現力豊かなインターフェース作業で知られており、「React Nativeでこんなことができるとは知らなかった」と人々に言わせる製品を構築しています。
...
Callieは、摂食障害から回復する人々をサポートするために設計された、感情的に知的なセルフケアコンパニオンです。このアプリは、開いた瞬間から歓迎的に感じられる、温かく、穏やかで、魅力的な体験の創造に焦点を当てています。Expo、React Native Reanimated、React Native Skiaを使用して、Callieは微妙なビジュアルと表現力豊かなインタラクションを提供し、ユーザーが快適さと安定感を持って日常のルーチンをナビゲートできるよう支援します。
この製品は、完全にネイティブ品質のモバイル体験として端から端まで構築されています。インターフェースとインタラクション言語から、モーションデザインとシェーダーベースのビジュアルまで、すべての詳細が摂食障害回復の独特なニーズを念頭に置いて作られました。
年間最優秀クリエイティブアプリ - Callie
Callieは2025年Expo App Awardsの最優秀クリエイティブアプリに選ばれ、その思慮深いデザインと独特なインタラクションパターンが評価されました。現在、App Storeで3,200件以上のレビューを獲得し、平均評価4.9以上を維持しており、ユーザーからの強いエンゲージメントと肯定的な感情を反映しています。
一人のモバイルチームとしてCallieのモバイル体験を作り上げる
私はNabiに創設エンジニアとして参加し、モバイルアプリ開発の全責任を負いました。微妙なインタラクションやアニメーションから、シェーダーベースの視覚効果、アプリ全体の感触まで、モバイル体験全体をエンジニアリングしました。
洗練された製品を作ることは偶然には起こりません。細部への目、異なるアイデアを探求する意欲、そして無数の微調整の反復が必要です。そのレベルのケアには、相当な時間と集中が必要です。そして初期段階のスタートアップでは、そのような洗練に投資するスペースを見つけることが最も困難な部分であることが多いのです。
この記事では、Callieの繊細で感情的に認識するモバイル体験を構築するアプローチと、初期段階のスタートアップで一人のモバイルチームとして、高速な開発速度を維持しながら高品質な製品を提供できた方法を共有します。
摂食障害の感受性を念頭に置いたデザインと開発
Callieのデザインには、摂食障害から回復する人々の特性と感受性の慎重な理解が必要でした。この分野の多くの人々は、感情的な脆弱性が高まっており、小さな視覚的変化やインタラクションパターンでさえ、アプリが支援的に感じられるか圧倒的に感じられるかに影響を与える可能性があります。
このため、全体的な体験は急激な遷移や不必要な刺激を避け、代わりに安定した予測可能な流れの感覚を維持する必要がありました。
デザインの観点から、私たちは穏やかなカラーパレット、優しい遷移、決して要求的や押し付けがましく感じないインタラクションパターンを優先しました。技術的な観点から、静的な画面だけでは、私たちが意図した微妙さのレベルと穏やかさの感覚を伝えるには不十分でした。体験が要求する微妙な感情的品質を提供するために、より洗練されたモーションと表現力豊かな視覚要素が必要でした。
表現力豊かなビジュアルのためのカスタムシェーダー
Callieで達成したかった体験の種類は、静的な画面だけでは表現できませんでした。このアプリは摂食障害から回復する人々によって使用されるため、インターフェースの多くの部分が心理的に快適に感じられる必要がありました—優しく、安定していて、急激な視覚的変化がない。
React Native Skia Shadersは、このレベルの微妙さを達成するための自然な選択となりました。
この微妙な感情的品質をアプリに取り入れるさまざまな方法を探索する中で、LottieやRiveなどの他のアプローチもテストしました。どちらのツールも事前にデザインされたモーショングラフィックスには優れていますが、リアルタイムでユーザー入力に応答したり、フレームごとに心理的な微妙さにマッチしたりする必要があるインタラクションには適していません。
カスタムシェーダー
Callieでは、各動きが瞬間瞬間にどのように感じられるかを形作るために、ピクセルレベルの制御が必要でした。これには、単にアニメーションを再生するのではなく、ユーザーのインタラクションに基づいてコンテンツを変形させることが必要でした。Skiaシェーダーは、私が必要とした表現力の範囲と柔軟性を提供し、従来のアニメーションツールでは不可能な、物理的に根拠のあるモーションと柔らかく有機的な遷移を可能にしました。
ピクセルレベルの制御
Callieでカスタムシェーダーを使用する主要機能の一つが「Callie's journal about you」です。この機能は、AIでユーザーの活動を分析し、手紙の形で励ましの肯定的なメッセージとともに日次サマリーを提供します。
AI駆動機能を構築する際の最も重要な考慮事項の一つは、体験が冷たく機械的に感じられないようにし、代わりに可能な限り人間の温かさを持たせることです。
ジャーナルをより個人的で魅力的に感じさせるために、画面がAI生成テキストのブロックのように見えるのではなく、ユーザーのために特別に書かれた思慮深い手紙のように見えるようにしたいと思いました。
シェーダーを使用して、「ページめくり」効果を追加しました。この視覚的処理は、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's journal about you」画面で使用されるページめくりシェーダーについて説明します。このシェーダーは画像を入力として受け取り、進行値を使用して紙のシートが優しく展開される視覚を作成します。単に画像をスケールしたりフェードしたりするのではなく、アニメーションは幾何学的変換に依存して、ページが物理的にカールし、アンカールしているように見せます。
自然に見えるページめくりを実現するために、この効果は三角関数、座標空間の歪み、幾何学ベースの変形を組み合わせています。これらの計算がスムーズに連携すると、アニメーションは説得力のある紙のような動きを生成します。
この効果の背後にある数学的原理をより深く理解したい場合は、William Candillonによるこのチュートリアルを強くお勧めします:Page Curl Tutorial
AIをより人間らしく感じさせる
AI駆動体験の最も重要な側面の一つは、生成された応答のトーンを形作ることでした。モデルが正確なコンテンツを生成した場合でも、特定の表現が過度に直接的、機械的、または感情的にバランスを欠いていると感じられる可能性がありました。
これを避けるために、各AIメッセージがユーザーに届く前に、構造、温かさ、ペーシングを調整するトーン形成ロジックの層を実装しました。これには、判断的と誤解される可能性のある表現をフィルタリングし、言語が非指示的で支援的であることを確保し、AIを一貫して優しい物語スタイルに導くことが含まれました。
これらの調整により、体験は汎用的なAIアシスタントではなく、思慮深いコンパニオンのように感じられ、ユーザーとCallieの間の感情的なつながりが強化されました。
UIにトーンを追加する
高品質なExpoアプリを構築する方法
ヒント1:ネイティブな感触のためにworklet関連ライブラリを使用する
React NativeでネイティブレベルのUIパフォーマンスを実現するための最も重要なヒントの一つは、workletとUIスレッドで実行されるライブラリを選択することです。アニメーションやタッチベースのインタラクションがJavaScriptスレッドに依存する場合、他のロジックと競合することになり、フレームドロップ、入力遅延、ボトルネックを簡単に引き起こす可能性があります。