OpenAIExpoJan 15, 2026, 2:30 PM

Making AI feel human in a mobile app with Expo, Reanimated, and Skia

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

openaienmodel: gpt-5-mini-2025-08-07

Making AI feel human in a mobile app with Expo, Reanimated, and Skia

Key Points

  • Page-curl shader humanizes AI-generated letters
  • Tone-shaping layer softens AI responses
  • Run animations on UI-thread (worklets)

Summary

Callie is an emotionally aware self-care app built with Expo, React Native Reanimated, and React Native Skia. The app combines shader-driven visuals, subtle motion, and a tone-shaping layer for AI-generated content to create a calm, human-feeling experience tailored for people recovering from eating disorders. The article explains why Skia shaders were chosen over playback-first tools (Lottie/Rive), shows the page-curl shader approach used for AI “letters,” and outlines practical performance and design trade-offs for a solo mobile engineer.

Key Points

  • Tech stack: Expo + React Native, Reanimated for worklet/UI-thread interactions, and React Native Skia for pixel-level, interactive visuals.
  • UX constraints: prioritize calm palettes, gentle transitions, predictable flow, and avoid abrupt or demanding interactions for sensitive users.
  • Shaders vs. prebaked animations: Skia shaders provide real-time, per-pixel control required for interactions that must respond frame-by-frame to user input.
  • Page curl shader (practical notes): map screen UVs to a cylindrical surface, use uniforms (u_image, u_resolution, u_originPos, u_currPos), apply a shadow gradient based on curl distance, and handle three regions (beyond curl, curl surface, before curl). This produces a natural, letter-like reveal for AI summaries.
  • Tone shaping for AI: post-process model outputs to remove directive or judgmental phrasing, enforce non-directive, warm language, and adjust pacing so responses feel companion-like rather than mechanical.
  • Performance tip: prefer libraries and logic that run on the UI thread/worklets to avoid JS-thread contention (use Reanimated worklets for touch and animation responsiveness).
  • Development workflow: iterate on micro-interactions and shaders; invest time in fine-tuning to achieve polish even on a small team.

Practical takeaways for engineers

  • Use Skia shaders when you need interactive, physically grounded deformations and per-pixel control that pre-baked tools cannot provide.
  • Keep AI language processing as a separate layer that adjusts tone and pacing before UI rendering.
  • Ensure animations and touch handlers run on the UI thread (worklets) to maintain native-feel performance on mobile.
  • Prototype shader effects early and iterate visually; small visual cues (like page curl + shadow) significantly increase perceived warmth.

Full Translation

Translations

A translation section that keeps the flow of the original article.

openaijamodel: gpt-5-mini-2025-08-07

Expo、Reanimated、Skiaを使ってモバイルアプリのAIを人間らしくする

概要

この記事は、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」画面で使用したページカールシェーダーの説明とコードです。このシェーダーは画像を入力として受け取り、シートがゆっくりとめくれるような視覚を作ります。単にスケールやフェードを使うのではなく、ジオメトリ変換を用いてページが物理的にカールするように見せています。

/**
 * 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&#x27;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 &lt;= aspect &amp;&amp; mappedPoint . y &lt;= 1.0 &amp;&amp; mappedPoint . x &gt; 0.0 &amp;&amp; mappedPoint . y &gt; 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トーンの整形、そして高品質なネイティブ体験を維持するための実践的な選択についての概要を提供しました。