WikifitaGitHub live67e8de5
outro · camdom/camdom-design-system

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.

Baixar raw

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.

TokenHexSemantic Role
primary#132037Navy — all text, borders, separators. The "ink" of the interface.
warning#b2140cRed — error states, alarm indicators, disconnection alerts.
background#9DB0B9Teal — default canvas, ball textures, menu backgrounds. The "safe" state color.
alarmBackground#06101ANear-black — alarm mode background. Replaces teal when consent is violated.

State-Color Mapping

App StateBackgroundBallText
disconnected#9DB0B9 (teal)Black dot texture#132037 (navy)
scanning#9DB0B9Black dot, pulsing rings#132037
connecting#9DB0B9Black dot, pulsing rings#132037
connected#9DB0B9Black dot, scaled 2x#132037
synced#9DB0B9Black dot, breathing 300-360#132037
alarm#06101A (dark)Red dot, pulsing 310-360#D9D9D9 (light gray)

Derived Colors (not in theme, used inline)

ValueUsage
#BCD4DFBall border when connected/synced
#889AA2Ball border when disconnected
#D9D9D9Alarm mode text, alarm button border
#D9D9D9Alarm arrow icons

2. Layer System

A numeric z-index hierarchy that prevents menu overlap conflicts.

Layer NameValueComponent
background0Canvas, NoisyBlurredBackground
menuBg1000Menu backdrop
menu10001Full-screen Menu overlay
lateralMenu1000212px-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:

WeightFileUsage
ThinBasicSans-Thin.otf
Thin ItalicBasicSans-ThinIt.otf
ExtraLightBasicSans-ExtraLight.otf
ExtraLight ItalicBasicSans-ExtraLightIt.otf
LightBasicSans-Light.otfLateral menu "Buy a Condom" text (16px)
Light ItalicBasicSans-LightIt.otf
RegularBasicSans-Regular.otfDisconnect/Alarm button labels (10px)
Regular ItalicBasicSans-RegularIt.otf
SemiBoldBasicSans-SemiBold.otfBall text via Skia Paragraph, lateral "KEEP YOUR PRIVACY PRIVATE" (11px)
SemiBold ItalicBasicSans-SemiBoldIt.otf
BoldBasicSans-Bold.otfMenu title (16px uppercase)
Bold ItalicBasicSans-BoldIt.otf
BlackBasicSans-Black.otf
Black ItalicBasicSans-BlackIt.otf

Secondary Typeface: Space Mono

WeightFileUsage
RegularSpaceMono-Regular.ttfMonospace, 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

FunctionPurpose
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 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

ComponentStaggerDurationNotes
LateralMenu65ms130msFade in/out
Menu15ms30msVery fast, snappy feel
MenuList items60ms120msSequential reveal, 120ms delay before items start
MenuTitle0ms240msSeparators + content animate together
UnlockYourPleasure65ms130msMatches LateralMenu
BuyAComdomLateralText80ms160msSlightly slower, text reveal
BillyBoyVerticalLogo80ms160msMatches lateral text
Ball pulse (inner)2500msBreathing 1.0-1.1x
Ball pulse (outer)1160msRing 1.0-1.3x
Ball synced breathing1900msRadius 300-360
Ball alarm pulse300ms+500msAggressive 310-360
Swipe arrow bounce2000ms-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

KeyFileTriggerPurpose
openBillyBoy_Camdom_02_Open.mp3Scanning startsConfirmation that protection search has begun
pairBillyBoy_Camdom_02_Pair.mp3State becomes syncedDevices connected, protection active
unpairBillyBoy_Camdom_02_Unpair.mp3Disconnect, server stop, client disconnectProtection ended
alarmBillyBoy_Camdom_Alarm_2.mp3Consent violated (app backgrounded, disconnect without consent)Loops continuously until all parties agree to stop
pingping.mp3Proximity 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 soundsetIsLoopingAsync(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 TransitionSoundHaptic
Disconnected -> Scanningopen
Synced achievedpair
Any disconnectunpairNotificationFeedbackType.Success
Consent violationalarm (loop)NotificationFeedbackType.Error (repeating)
Alarm stoppedalarm stopsNotificationFeedbackType.Success
Swipe gesture startImpactFeedbackStyle.Soft
Menu button pressImpactFeedbackStyle.Heavy
Disconnect buttonImpactFeedbackStyle.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

NameWidthDevice
xs0Below minimum
sm375iPhone SE 3
md390iPhone 14 / 15 / 15 Pro
lg414iPhone 11
xl430iPhone 15 Pro Max (design reference)
superLarge744iPad Mini

Android

NameDPI-adjusted WidthDevice
xs0Below minimum
sm720/scaleSmall phone
md1080/scaleMoto G 52
lg1220/scalePOCO X6 / Pixel 8 Pro
xl1440/scalePixel Fold (folded)
superLarge2208/scalePixel 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)

LayerTechnologyRole
Rendering@shopify/react-native-skiaCanvas, circles, image shaders, blur, paragraphs
Animationreact-native-reanimatedShared values, worklets, frame callbacks, springs
Gesturesreact-native-gesture-handlerPan gestures, touch handling
Hapticsexpo-hapticsImpact and notification feedback
Audioexpo-avSound loading, playback, audio mode
Stylesreact-native-unistylesTheme system, breakpoints, runtime
StateMMKV (react-native-mmkv)Persistent settings (alarm-on-disconnect, sensitivity)
BLECustom native module + react-native-ble-plxGATT 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)