One document. One vision. One day to ship.
We're killing the over-engineered Skia/Rive/Lottie garden. In its place: The Pressed Botanical Collection — a premium, minimalist scrapbook where your garden grows through layered botanical illustrations, not complex animation.
The feeling: Opening a leather-bound herbarium. Each page reveals a new pressed flower. Quiet. Elegant. Tactile.
Why this wins:
- Zero exotic dependencies (no Skia, no Rive, no Lottie)
- Ships in one day, not one sprint
- Emotional resonance > technical spectacle
- Works identically on iOS and Android with zero platform-specific code
The garden is a stack of 5 absolutely-positioned images. Each layer fades/scales in when the user reaches the corresponding level.
Level 1: Seed → [bare soil]
Level 2: Sprout → + [sprout overlay]
Level 3: Growing → + [small plant overlay]
Level 4: Blooming → + [flowers overlay]
Level 5: Flourishing → + [full bloom + butterflies]
All 5 PNGs share the same dimensions — they simply stack. The visual result is a garden that "fills in" organically. Placeholder rectangles work for day-one dev; real botanical art swaps in later.
No formulas. No weights. No balancing spreadsheets.
| Action | GP |
|---|---|
| Daily check-in | +10 |
| Complete a session/lesson | +25 |
| 7-day streak bonus | +50 |
| First-time achievement | +30 |
Level thresholds (linear):
| Level | Name | GP Required |
|---|---|---|
| 1 | Seed | 0 |
| 2 | Sprout | 50 |
| 3 | Growing | 150 |
| 4 | Blooming | 350 |
| 5 | Flourishing | 700 |
A single pure function computes level from total GP. No state management library. No backend call.
Each completed cycle (reaching level 5) awards a botanical stamp — a collectible card in a grid gallery. Stamps are per-cycle-phase (menstrual, follicular, ovulation, luteal), giving 4 unique illustrations per set.
- Unlocked: Full-color botanical PNG with phase-accent border on cream/paper background.
- Locked: Grayscale silhouette + 🔒.
- Tap interaction: Card flips (
rotateY) to reveal a wellness tip.
This is the long-term retention loop: the garden resets each cycle, but stamps are permanent.
import { Image } from 'expo-image';
<Image
source={layerSource}
contentFit="contain"
transition={300} // built-in crossfade
cachePolicy="memory-disk" // aggressive caching
recyclingKey={`layer-${i}`}
style={StyleSheet.absoluteFill}
/>Why expo-image: shared memory cache, built-in transitions, blurhash placeholders, better perf on large PNGs than RN Image.
import Animated, {
useSharedValue, useAnimatedStyle,
withTiming, withSpring
} from 'react-native-reanimated';
function GardenLayer({ source, visible }: { source: any; visible: boolean }) {
const opacity = useSharedValue(0);
const scale = useSharedValue(0.8);
useEffect(() => {
if (visible) {
opacity.value = withTiming(1, { duration: 600 });
scale.value = withSpring(1, { damping: 15, stiffness: 150 });
}
}, [visible]);
const style = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ scale: scale.value }],
}));
return (
<Animated.View style={[StyleSheet.absoluteFill, style]}>
<Image source={source} style={StyleSheet.absoluteFill} contentFit="contain" />
</Animated.View>
);
}That's the entire animation system. Two shared values. Spring easing for organic feel.
// drizzle/schema/garden.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const unlockedItems = sqliteTable('unlocked_items', {
id: text('id').primaryKey(), // uuid
userId: text('user_id').notNull(),
itemKey: text('item_key').notNull(), // e.g. "stamp_follicular_lavender"
itemType: text('item_type').notNull(), // 'stamp' | 'badge'
unlockedAt: integer('unlocked_at', { mode: 'timestamp' }),
metadata: text('metadata'), // JSON blob for extensibility
});One table. That's the entire persistence layer for collectibles. GP total lives in AsyncStorage as a single integer — no table needed.
// GP persistence — AsyncStorage, nothing more
async function addGP(points: number): Promise<GPState> {
const raw = await AsyncStorage.getItem('gp_total');
const newTotal = (raw ? parseInt(raw, 10) : 0) + points;
await AsyncStorage.setItem('gp_total', String(newTotal));
return computeGPState(newTotal);
}| Pattern | Implementation |
|---|---|
| Haptics | Haptics.impactAsync(Medium) on unlock, Light on tap |
| Spring physics | withSpring({ damping: 15, stiffness: 150 }) everywhere |
| Staggered entry | delay: index * 100 on stamp grid items |
| Muted palette | Sage greens, dusty pinks, warm cream |
| Typography | Serif for names, sans-serif for body |
| Whitespace | ≥ 20px padding between all cards |
| Block | Task | Output |
|---|---|---|
| 0:00–0:30 | Create 5 placeholder PNGs (colored rectangles) | assets/garden-*.png |
| 0:30–1:30 | Build GardenScene — layered image stack component |
Working visual |
| 1:30–2:30 | Add Reanimated fade/scale per layer | Animated transitions |
| 2:30–3:00 | Build GardenScreen with level label + progress bar |
Navigable screen |
| Block | Task | Output |
|---|---|---|
| 3:00–3:45 | Implement computeGPState() pure function + level thresholds |
GP engine |
| 3:45–4:30 | Wire GP to AsyncStorage (addGP / read on mount) |
Persistence |
| 4:30–5:30 | Level-up celebration: haptic + scale bounce + emoji burst | Delight |
| 5:30–6:00 | Polish: progress bar animation, nav entry point, debug +GP button | Shippable MVP |
A working screen where:
- The garden visually grows as GP accumulates (5 layers)
- GP persists across sessions
- Level-ups trigger haptic + animation celebration
- A debug button allows manual GP injection for testing
- Real botanical art (placeholders are fine)
- Stamp collection gallery (day 2)
- Backend sync (AsyncStorage is enough)
- Sound effects (nice-to-have, not MVP)
| Decision | Choice | Rationale |
|---|---|---|
| Rendering | Layered PNGs + absolute positioning | Visual depth, trivial code |
| Animation | react-native-reanimated spring/timing |
Already in Expo, 60fps native thread |
| Images | expo-image |
Cache, transitions, perf |
| GP Engine | Pure function + AsyncStorage | Zero dependencies |
| Collectibles | Drizzle unlocked_items table |
One table, extensible |
| Levels | Fixed 5-tier linear thresholds | No balancing math |
| Killed | Skia, Rive, Lottie, Zustand, Redux, backend API | 🎯 |
Built for Vela. Ship today, enchant tomorrow.