CAMDOM — Design System
Visual design system of the CAMDOM digital consent protection app: color palette, layer hierarchy, typography, component library, animation engine, sound design, and dark aesthetic.
CAMDOM — Design System
The visual language of the world's first digital condom. A dark, minimal, haptic-first interface built on Shopify React Native Skia, Reanimated generator-based animations, and a 4-color palette that communicates danger, safety, and privacy through material metaphor.
1. Color Palette
The palette is intentionally restrained — four colors that map directly to app states and emotional semantics.
| Token | Hex | Semantic Role |
|---|---|---|
primary | #132037 | Navy — all text, borders, separators. The "ink" of the interface. |
warning | #b2140c | Red — error states, alarm indicators, disconnection alerts. |
background | #9DB0B9 | Teal — default canvas, ball textures, menu backgrounds. The "safe" state color. |
alarmBackground | #06101A | Near-black — alarm mode background. Replaces teal when consent is violated. |
State-Color Mapping
| App State | Background | Ball | Text |
|---|---|---|---|
disconnected | #9DB0B9 (teal) | Black dot texture | #132037 (navy) |
scanning | #9DB0B9 | Black dot, pulsing rings | #132037 |
connecting | #9DB0B9 | Black dot, pulsing rings | #132037 |
connected | #9DB0B9 | Black dot, scaled 2x | #132037 |
synced | #9DB0B9 | Black dot, breathing 300-360 | #132037 |
alarm | #06101A (dark) | Red dot, pulsing 310-360 | #D9D9D9 (light gray) |
Derived Colors (not in theme, used inline)
| Value | Usage |
|---|---|
#BCD4DF | Ball border when connected/synced |
#889AA2 | Ball border when disconnected |
#D9D9D9 | Alarm mode text, alarm button border |
#D9D9D9 | Alarm arrow icons |
2. Layer System
A numeric z-index hierarchy that prevents menu overlap conflicts.
| Layer Name | Value | Component |
|---|---|---|
background | 0 | Canvas, NoisyBlurredBackground |
menuBg | 1000 | Menu backdrop |
menu | 10001 | Full-screen Menu overlay |
lateralMenu | 10002 | 12px-wide sidebar strip |
The lateral menu sits above the full menu. When the menu opens, it covers the entire screen at z-index 10001 with a teal background + noisy blurred orbs. The lateral menu (the thin right-edge strip) operates at 10002, always above.
3. Typography
Primary Typeface: BasicSans
A custom sans-serif family bundled as OTF files in assets/fonts/. 14 variants:
| Weight | File | Usage |
|---|---|---|
| Thin | BasicSans-Thin.otf | — |
| Thin Italic | BasicSans-ThinIt.otf | — |
| ExtraLight | BasicSans-ExtraLight.otf | — |
| ExtraLight Italic | BasicSans-ExtraLightIt.otf | — |
| Light | BasicSans-Light.otf | Lateral menu "Buy a Condom" text (16px) |
| Light Italic | BasicSans-LightIt.otf | — |
| Regular | BasicSans-Regular.otf | Disconnect/Alarm button labels (10px) |
| Regular Italic | BasicSans-RegularIt.otf | — |
| SemiBold | BasicSans-SemiBold.otf | Ball text via Skia Paragraph, lateral "KEEP YOUR PRIVACY PRIVATE" (11px) |
| SemiBold Italic | BasicSans-SemiBoldIt.otf | — |
| Bold | BasicSans-Bold.otf | Menu title (16px uppercase) |
| Bold Italic | BasicSans-BoldIt.otf | — |
| Black | BasicSans-Black.otf | — |
| Black Italic | BasicSans-BlackIt.otf | — |
Secondary Typeface: Space Mono
| Weight | File | Usage |
|---|---|---|
| Regular | SpaceMono-Regular.ttf | Monospace, likely used in version display or debug |
Typography Rules
- All text is UPPERCASE — menu titles, ball labels, lateral text, buttons
- Letter spacing is implicit in the all-caps treatment
- Font sizes range from 10px (buttons) to 16px (menu title, lateral text)
- Ball text uses Skia ParagraphBuilder with
BasicSans-SemiBold, 11px default, 14px in alarm state - Dynamic font sizing:
f:<size><>textprefix in ball text strings allows per-line font size control via the Skia paragraph system
4. Component Library
SwipeDown
Gesture-driven swipe component using react-native-gesture-handler Pan gesture. Clamps vertical translation between 0 and 100px. Used as the interaction metaphor for the onboarding/connection trigger. The main app uses a similar pattern but with withSpring physics for the ball drag.
NoisyBlurredBackground
Built with @shopify/react-native-skia. Renders two AnimatedBall components (one red, one black) on a Canvas with:
ImageShadertextures fromred-dot.pngandblack-dot.pngBlurfilter (default 40px, configurable)withRepeat(withTiming(...))pulsing animation (1300ms cycle, 1.4x scale)- Configurable:
balls,size,displace,darken,opacity,blur - Used as the background for the full Menu overlay with
blur={100}andopacity={0.9}
LateralMenu
A 12px-wide strip fixed to the right edge of the screen. Contains (top to bottom):
- MenuIcon — hamburger/close icon (12x14px), toggles menu open/close
- CamdomLogo — the CAMDOM wordmark, tapping it cancels connection and resets BLE state
- UnlockYourPleasure — rotated -90deg text "KEEP YOUR PRIVACY PRIVATE" (BasicSans-SemiBold, 11px)
- BuyAComdomLateralText — rotated -90deg text "Be Intimate Lighthearted Loving Yourself" with bolded first letters spelling BILLY
- BillyBoyVerticalLogo — BILLY BOY logo at bottom
The lateral menu fades in/out with a 130ms stagger animation. It is visible only when the full menu is closed and no option is selected.
Menu (Full Screen)
Full-screen overlay at z-index 10001. Contains:
- MenuTitle — animated header with top/bottom separators that expand from 0% to 100% width
- MenuList — 4 menu items (How To, About BILLY BOY, Buy a Condom, Options) with staggered fade-in (60ms delay per item) and expanding separators
- MenuContent — renders the selected page (About, HowTo, BuyComdom, Options)
- NoisyBlurredBackground — always rendered behind menu content
Menu open/close animation: 30ms stagger, items fade in sequentially. Each menu button triggers Haptics.impactAsync(ImpactFeedbackStyle.Heavy).
Ball (Core Visual Element)
The primary UI element — a Skia-rendered circle that represents the protection state:
- Textures:
red-dot.png(danger/target) andblack-dot.png(safe/self), applied viaImageShader - Blur: 22px Gaussian blur on the ball texture
- Pulsing: Two layers of pulse animation:
- Inner: 2500ms cycle, scales 1.0-1.1 (Easing.inOut)
- Outer: 1160ms cycle, scales 1.0-1.3 (Easing.in), creates a breathing ring effect
- Scale behavior: Ball radius interpolates from 78px to 360px mapped to scale 1-3x
- Border rings: Inner stroke (0.6px) + outer stroke (0.75px) with opacity animation during scanning/connecting
- Swipe-down arrow: Animated bouncing arrow icon (10x18px) when disconnected, invites user to swipe
Separator
Simple horizontal line component. Height 1-2px, primary color, full width with configurable margin. Used throughout menus as visual dividers.
5. Animation System
Architecture: Generator-Based Worklet Animations
The animation engine is a custom framework built on top of react-native-reanimated. Instead of declarative withTiming chains, it uses JavaScript generators that run as Reanimated worklets, giving imperative control over animation sequencing.
Core Primitives
| Function | Purpose |
|---|---|
timing(value, config) | Eased interpolation from current to target. Default: 600ms, Easing.inOut(ease) |
wait(duration) | Blocks the generator for N milliseconds |
waitUntil(sharedValue, invert?) | Blocks until a boolean shared value becomes true |
parallel(...generators) | Runs multiple generators concurrently, resolves when all complete |
stagger(delay, ...generators) | Runs generators with N-ms offset between each |
timeSincePreviousFrame() | Yields elapsed time since last frame |
Animation Definition Pattern
const animation = makeAnimation(
function* ({ isOpened, transition }) {
"worklet";
let to = 1;
while (true) {
yield* waitUntil(isOpened, !to);
yield* stagger(duration / 2, timing(transition, { to, duration }));
to = to === 1 ? 0 : 1;
}
},
{ isOpened: false, transition: 0 },
);
Each animation is a state machine: it loops forever, waiting for a trigger (waitUntil), then executing a sequence of timed transitions, then flipping the target and waiting again.
useAnimation Hook
const { isOpened, transition } = useAnimation(animation);
- Creates shared values from the initial state
- Uses
useFrameCallbackto drive the generator each frame - Supports pause via optional
SharedValue<boolean>parameter - Auto-restarts generator when it completes
- Cleans up all animations on unmount via
cancelAnimation
Animation Durations by Component
| Component | Stagger | Duration | Notes |
|---|---|---|---|
| LateralMenu | 65ms | 130ms | Fade in/out |
| Menu | 15ms | 30ms | Very fast, snappy feel |
| MenuList items | 60ms | 120ms | Sequential reveal, 120ms delay before items start |
| MenuTitle | 0ms | 240ms | Separators + content animate together |
| UnlockYourPleasure | 65ms | 130ms | Matches LateralMenu |
| BuyAComdomLateralText | 80ms | 160ms | Slightly slower, text reveal |
| BillyBoyVerticalLogo | 80ms | 160ms | Matches lateral text |
| Ball pulse (inner) | — | 2500ms | Breathing 1.0-1.1x |
| Ball pulse (outer) | — | 1160ms | Ring 1.0-1.3x |
| Ball synced breathing | — | 1900ms | Radius 300-360 |
| Ball alarm pulse | — | 300ms+500ms | Aggressive 310-360 |
| Swipe arrow bounce | — | 2000ms | -2 to +2 px vertical |
Skia Interpolation
The Menu components use @shopify/react-native-skia's interpolate for separator width animation:
width: `${interpolate(value, [0, 1], [0, 100])}%`
6. Sound Design
Sound Architecture
Sound is managed by the BleManager class using expo-av (Audio.Sound). All sounds are pre-loaded at construction time and stored in a Map<AppSounds, Audio.SoundObject>.
Audio Configuration
Audio.setAudioModeAsync({
staysActiveInBackground: true,
playsInSilentModeIOS: true,
interruptionModeIOS: DoNotMix,
interruptionModeAndroid: DoNotMix,
shouldDuckAndroid: false,
playThroughEarpieceAndroid: false,
});
Critical: sounds play even in silent mode and at full volume. The manager forces volume to 1.0 before playing any sound, showing the native volume UI if needed.
Sound Types
| Key | File | Trigger | Purpose |
|---|---|---|---|
open | BillyBoy_Camdom_02_Open.mp3 | Scanning starts | Confirmation that protection search has begun |
pair | BillyBoy_Camdom_02_Pair.mp3 | State becomes synced | Devices connected, protection active |
unpair | BillyBoy_Camdom_02_Unpair.mp3 | Disconnect, server stop, client disconnect | Protection ended |
alarm | BillyBoy_Camdom_Alarm_2.mp3 | Consent violated (app backgrounded, disconnect without consent) | Loops continuously until all parties agree to stop |
ping | ping.mp3 | Proximity sensor activation (currently commented out) | Would signal distance monitoring active |
Sound Synchronization
- Server broadcasts sound packets to all connected clients:
sound:1|<packetCode>where packetCode maps 0=open, 1=pair, 2=unpair, 3=alarm, 4=ping - Clients receive and play locally — ensuring all devices in the mesh play the same sound simultaneously
- Alarm is the only looping sound —
setIsLoopingAsync(true)is called specifically for the alarm key - Platform difference: iOS uses
replayAsync(), Android usesplayFromPositionAsync(0) - Alarm stops when all parties hold the stop button simultaneously (consensus mechanism)
Sound-Behavior Coupling
| State Transition | Sound | Haptic |
|---|---|---|
| Disconnected -> Scanning | open | — |
| Synced achieved | pair | — |
| Any disconnect | unpair | NotificationFeedbackType.Success |
| Consent violation | alarm (loop) | NotificationFeedbackType.Error (repeating) |
| Alarm stopped | alarm stops | NotificationFeedbackType.Success |
| Swipe gesture start | — | ImpactFeedbackStyle.Soft |
| Menu button press | — | ImpactFeedbackStyle.Heavy |
| Disconnect button | — | ImpactFeedbackStyle.Heavy |
7. Visual Identity
The Dark Aesthetic
CAMDOM uses a muted, desaturated palette — not the typical "dark mode" of pure blacks and neon accents. The teal background (#9DB0B9) gives the interface a clinical, calm feel. The near-black alarm background (#06101A) creates an immediate visceral contrast when consent is violated.
The design language draws from:
- Medical/clinical UI — clean lines, minimal decoration, functional typography
- Premium product packaging — the BILLY BOY brand association brings a luxury condom aesthetic
- Privacy-by-design — no unnecessary visual elements, everything serves a function
The Ball Metaphor
The central ball is both the primary UI element and the core metaphor:
- Two balls represent two connected devices/people
- Pulling them together (swipe down) initiates protection
- When synced, they merge into one breathing entity
- When alarm triggers, the ball turns red and pulses aggressively against a dark background
- The
NoisyBlurredBackgroundorbs mirror this duality — one red, one black, breathing in and out
Minimal UI Philosophy
- No navigation bars — the lateral menu strip (12px) replaces traditional nav
- No status bars — connection state is communicated through the ball's behavior
- No text labels in the main view — the ball's color, size, and animation convey all information
- Text appears only in the ball (Skia Paragraph) and in the menu system
- Buttons are rare — only Disconnect and Stop Alarm appear, and only when relevant
- The entire interaction model is gesture-based — swipe to connect, hold to disconnect, consensus to stop alarm
Haptic Language
Every user interaction has a corresponding haptic feedback:
ImpactFeedbackStyle.Soft— gesture initiation (light touch)ImpactFeedbackStyle.Heavy— button presses, menu interactions (deliberate action)NotificationFeedbackType.Success— disconnection complete (resolution)NotificationFeedbackType.Error— alarm pulsing (danger signal)
8. Responsive Breakpoints
iOS
| Name | Width | Device |
|---|---|---|
xs | 0 | Below minimum |
sm | 375 | iPhone SE 3 |
md | 390 | iPhone 14 / 15 / 15 Pro |
lg | 414 | iPhone 11 |
xl | 430 | iPhone 15 Pro Max (design reference) |
superLarge | 744 | iPad Mini |
Android
| Name | DPI-adjusted Width | Device |
|---|---|---|
xs | 0 | Below minimum |
sm | 720/scale | Small phone |
md | 1080/scale | Moto G 52 |
lg | 1220/scale | POCO X6 / Pixel 8 Pro |
xl | 1440/scale | Pixel Fold (folded) |
superLarge | 2208/scale | Pixel Fold (unfolded) / Tablet |
The xl breakpoint (430px on iOS) is the Figma design reference. Ball radius adapts: 86px on sm, 96px on larger. Font sizes also adjust: 10px on sm, 11px on larger.
9. Technology Stack (Visual Layer)
| Layer | Technology | Role |
|---|---|---|
| Rendering | @shopify/react-native-skia | Canvas, circles, image shaders, blur, paragraphs |
| Animation | react-native-reanimated | Shared values, worklets, frame callbacks, springs |
| Gestures | react-native-gesture-handler | Pan gestures, touch handling |
| Haptics | expo-haptics | Impact and notification feedback |
| Audio | expo-av | Sound loading, playback, audio mode |
| Styles | react-native-unistyles | Theme system, breakpoints, runtime |
| State | MMKV (react-native-mmkv) | Persistent settings (alarm-on-disconnect, sensitivity) |
| BLE | Custom native module + react-native-ble-plx | GATT server, scanning, advertising, packet protocol |
See Also
- camdom — Main CAMDOM overview and handoff page
diretivas/design-systems/anthropic/index.md— Anthropic Design System (the other approved design system)