About this animation
A sleep check-in with a refracting numeral and word-by-word insight, with configurable hours, insight text, palette, and fonts.
Why this one
The number becomes a poster, rather than another metric inside a card. The glass samples the actual digit scene, displaces it, and separates RGB channels while you drag. Completing the check-in turns those values into a readable sentence with bright data spans. The composition adds a timestamp lens and a segmented sleep summary.
How it works
- Render the twelve hour numerals into a memoized four-by-three image atlas on a raster Skia surface, using two independently loaded font sizes. Each tile is fitted to its measured font metrics and advance width, reserving at least 16 points on either side. This preserves CanvasKit typeface ownership and works without an offscreen GPU context.
- A module-level Skia
RuntimeEffectsamples that image. One shared hour position drives clipping, the outgoing numeral, the incoming numeral, and integer notifications to React. - The prism transforms its sampling coordinates, offsets individual RGB channels, and adds bevel highlights inside its signed-distance shape. The circular timestamp lens refracts the same scene.
- Gesture Handler separates the hour pan/tap from the prism drag/tap. Springs are interruptible and specify mass; press feedback uses a separate shared value.
- The numeral enters with a 24-point rise and opacity on a critically damped spring. The prism then slides in and starts refracting. Done crossfades the background with separate outgoing and incoming text windows, so the screens never show overlapping text. A separate shared writing progress reveals sentence words and highlighted data spans.
- The demo owns its timed sequence and stops every pending step on human input. The reusable component has no autoplay timers.
Install
npx expo install @shopify/react-native-skia react-native-reanimated react-native-worklets react-native-gesture-handlerWrap your application in GestureHandlerRootView. For web, configure Skia's CanvasKit loading before importing this component, using your host's Skia loader. The catalog uses lazy loading with Skia initialization. Fonts are supplied by the host; no font dependency is included in the reusable folder.
Usage
import { PrismNumeralInsight } from './prism-numeral-insight';
<PrismNumeralInsight
numeralFont={require('./assets/Display-Bold.ttf')}
insight={[
{ text: 'With ' },
{ text: '8 hours', highlight: true },
{ text: ' of sleep, a calmer morning starts here.' },
]}
onHoursChange={(hours) => saveSleep(hours)}
onDone={() => finishCheckIn()}
/>With external controls and a complete theme:
const ref = useRef<PrismNumeralInsightRef>(null);
<PrismNumeralInsight
ref={ref}
hours={hours}
onHoursChange={setHours}
mood={mood}
onMoodChange={setMood}
colorScheme="auto"
palette={{ accent: '#375CEB', button: '#172A76' }}
insightPalette={{ phase: '#6AE9A5' }}
numeralFont={require('./assets/Display-Bold.ttf')}
displayFontFamily="Display-Bold"
fontFamily="Interface-Regular"
monoFontFamily="Figures-Regular"
refraction={34}
dispersion={4}
formatHours={(n) => `With ${n} hours\nof sleep`}
/>
ref.current?.setHours(8);The host applies safe-area insets. The component measures its own container, limits content width to 480 points, and uses scrollable content when embedded in short or horizontal spaces. It fills its host with flex: 1. Font scaling remains enabled. Supply numeralFont on web: system font matching is guarded, but web does not implement it. The temporary RN numeral fallback does not have refraction until that asset loads.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
hours / onHoursChange | number / (n) => void | 7 / — | Optimistic controlled integer hours, 1–12; updates are reported at integer crossings, not each frame. |
mood / onMoodChange | number / (n) => void | 2 / — | Five moods, indexed 0–4. |
insight | InsightSegment[] | Sleep and coffee example | Each span has text and optional highlight. Include spaces between spans. |
onReady | () => void | — | Notifies the host when layout and the numeral atlas are ready; autoplay can start from the beginning after font loading. |
onDone | () => void | — | Completion callback. |
onInteraction | () => void | — | Human input callback, useful for canceling host autoplay. |
header | ReactNode | — | Your brand/header; kept away from the top-right viewer controls. |
fontFamily | string | System | Body copy and buttons. |
displayFontFamily | string | System | Headings and fallback numeral. |
monoFontFamily | string | System | Timestamp and metadata. |
numeralFont | Skia DataSourceParam | — | Local display font asset for the refracted numeral. |
colorScheme | light | dark | auto | light | Check-in theme; completion defaults to dark. |
palette / insightPalette | Partial<PrismPalette> | Built-in light / dark | Override background, foreground, muted, surface, border, accent, button, buttonText, phase, glassTint. |
refraction / dispersion | number | 28 / 5 | Displacement and RGB separation in points. |
ambientMotion | boolean | true | Slow prism drift; stops on human input. |
duration | number | 560 | Staged crossfade duration, milliseconds. |
formatHours | (n) => string | With n hours\nof sleep | Headline formatter. |
formatHoursAnnouncement | (n) => string | n hours of sleep | Spoken value formatter. |
eyebrow | string | MORNING CHECK-IN | Check-in label. |
adjustmentHint | string | Swipe up or down to adjust | Adjustment help. |
moodPrompt | string | How do you feel today? | Selection prompt. |
moodLabels | string[] | Very low, Low, Okay, Good, Great | Five accessible mood labels. |
doneLabel / backLabel | string | Done / Back to check-in | Navigation buttons. |
insightEyebrow | string | YOUR MORNING, IN FOCUS | Insight label. |
bedtime | string | 6:15 AM | Timestamp lens copy. |
sleepLabel / sleepScore / sleepStatus | string | Sleep / 52 / EXCELLENT | Summary labels and score. |
totalLabel / totalSleep | string | Total sleep / 8 h 15 m | Total sleep metadata. |
sleepPhases | number[] | Seven sample fractions | Phase segment widths; normalized automatically. |
hoursLabel / prismLabel | string | Sleep hours / Move the glass prism | Accessible gesture labels. |
increaseLabel / decreaseLabel | string | Increase / Decrease sleep hours | Accessible step controls. |
style / testID | ViewStyle / string | — / prism-numeral-insight | Root overrides and stable control IDs. |
Ref methods
setHours(n), setMood(n), done(), reset(hours?, mood?) (returns to the check-in and replays its entrance; optional values restore the cycle immediately), and movePrism(offset) (normalized offset from -1 to 1). Programmatic controls do not trigger onInteraction.
Demo timeline
All times start when the measured layout and supplied numeral font are ready. The numeral rises 24 points and fades in with a spring without overshoot, followed by the prism sliding in; the entrance takes approximately 1.2 seconds. Prism movement begins at 1.35 s and changes direction at 1.80 s. Hours change from 7 to 8 at 2.35 s, mood 4 (index 3) is selected at 3.35 s, and Done starts at 4.65 s. The 800 ms background crossfade removes the check-in text before revealing the insight text. The sentence completes at about 6.40 s. The sequence repeats at 9.90 s and replays the entrance. Any touch cancels the sequence. The reusable component still defaults to a 560 ms crossfade.
Compatibility
Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, Skia 2.14.0. Compatible with the installed Expo Go stack; hosts with different native dependencies need a development build.
The shader deliberately uses makeShaderWithChildren with an image shader, rather than the image-filter RuntimeShader API that is unavailable on web.
Accessibility
All controls have roles, labels, and stable IDs. The hour selector supports increment/decrement screen-reader actions, tap-to-advance, and separate 48-point buttons. Mood changes, completed insights, and settled hour adjustments are announced. Hidden screens are removed from accessibility navigation and touch handling.
Reduced motion freezes the prism, removes numeral travel and prism entrance travel, and uses 200 ms fades. The prism gesture stays stationary in that mode. Decorative Skia content does not duplicate the spoken text.
Icons: Lucide (ISC), copied from the repository kit. The Aurora demo brand uses the kit's sparkles glyph and pink-to-violet tile. Geist 800 is used for the demo poster instead of unavailable 700/900 weights; the reusable component accepts any supplied typeface.




