---
type: reference
title: "CAMDOM — Design System"
description: "Visual design system of the CAMDOM digital consent protection app: color palette, layer hierarchy, typography, component library, animation engine, sound design, and dark aesthetic."
tags: [camdom, design-system, react-native, skia, reanimated, ble, ux]
timestamp: "2026-07-20"
---

# 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><>text` prefix 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:
- `ImageShader` textures from `red-dot.png` and `black-dot.png`
- `Blur` filter (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}` and `opacity={0.9}`

### LateralMenu

A 12px-wide strip fixed to the right edge of the screen. Contains (top to bottom):
1. **MenuIcon** — hamburger/close icon (12x14px), toggles menu open/close
2. **CamdomLogo** — the CAMDOM wordmark, tapping it cancels connection and resets BLE state
3. **UnlockYourPleasure** — rotated -90deg text "KEEP YOUR PRIVACY PRIVATE" (BasicSans-SemiBold, 11px)
4. **BuyAComdomLateralText** — rotated -90deg text "Be Intimate Lighthearted Loving Yourself" with bolded first letters spelling BILLY
5. **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) and `black-dot.png` (safe/self), applied via `ImageShader`
- **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

```typescript
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

```typescript
const { isOpened, transition } = useAnimation(animation);
```

- Creates shared values from the initial state
- Uses `useFrameCallback` to 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:
```typescript
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

```typescript
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 uses `playFromPositionAsync(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 `NoisyBlurredBackground` orbs 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)
