Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95826a1f19 | ||
|
|
cd5b21514d | ||
|
|
8c9bf89676 | ||
|
|
b5f5518c1e | ||
|
|
251b3af5ed |
No files matched your search
@@ -1,315 +0,0 @@
|
|||||||
---
|
|
||||||
name: motion-design
|
|
||||||
description: >
|
|
||||||
Applies motion design principles to create emotionally-driven, technically sound animations and transitions.
|
|
||||||
Provides timing, easing, choreography, and Disney animation principles adapted for UI.
|
|
||||||
Use when creating animations, transitions, micro-interactions, loading states, page transitions,
|
|
||||||
scroll-triggered effects, or any motion work. Works with CSS, Framer Motion, GSAP, Lottie, Spring,
|
|
||||||
or any animation system.
|
|
||||||
license: MIT
|
|
||||||
metadata:
|
|
||||||
author: LottieFiles
|
|
||||||
version: "1.0.0"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Motion Design Skill
|
|
||||||
|
|
||||||
## When to Apply
|
|
||||||
|
|
||||||
Use this skill when:
|
|
||||||
- Creating UI animations (buttons, cards, modals, page transitions)
|
|
||||||
- Designing micro-interactions and feedback animations
|
|
||||||
- Building loading, success, or error states
|
|
||||||
- Animating illustrations or decorative elements
|
|
||||||
- Planning scroll-triggered or progress-based animations
|
|
||||||
- Establishing brand motion identity
|
|
||||||
- Choreographing multi-element sequences
|
|
||||||
|
|
||||||
**Decision tree:**
|
|
||||||
1. Does it serve a functional purpose (feedback, guidance)? → Timing rules for responsiveness
|
|
||||||
2. Does it express brand personality? → Motion Personality archetypes
|
|
||||||
3. Does it tell a story or guide attention? → Disney principles + choreography
|
|
||||||
4. Is this a complex multi-element scene? → 1/3 Rule + stagger patterns
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Reference: 8-Step Checklist
|
|
||||||
|
|
||||||
Before creating any animation:
|
|
||||||
|
|
||||||
1. **Emotional target?** — joy, calm, urgency, elegance
|
|
||||||
2. **Motion Personality?** — Playful, Premium, Corporate, Energetic
|
|
||||||
3. **Primary property?** — position, scale, rotation, opacity
|
|
||||||
4. **Duration?** — see duration table below
|
|
||||||
5. **Easing family?** — entrance=decelerate, exit=accelerate
|
|
||||||
6. **Hero element?** — apply staging principles
|
|
||||||
7. **Secondary + ambient layers?** — add richness
|
|
||||||
8. **1/3 rules?** — motion distance, simultaneous elements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Three Pillars (CRITICAL)
|
|
||||||
|
|
||||||
Every animation must satisfy three pillars before any technical decisions:
|
|
||||||
|
|
||||||
| Pillar | Question | Drives |
|
|
||||||
|--------|----------|--------|
|
|
||||||
| **Emotional Intent** | What should the viewer FEEL? | Easing, timing, amplitude |
|
|
||||||
| **Visual Narrative** | What's the micro-story? | Setup → Action → Resolution |
|
|
||||||
| **Motion Craft** | How do we make it believable? | Physics, secondary motion, paths |
|
|
||||||
|
|
||||||
**Three motion layers** (flat animation = missing layers):
|
|
||||||
- **Primary**: Main action the viewer follows
|
|
||||||
- **Secondary**: Supporting richness (shadows, icons shifting)
|
|
||||||
- **Ambient**: Background life (gradients, subtle pulses)
|
|
||||||
|
|
||||||
> Deep dive: [director/core-philosophy.md](director/core-philosophy.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Motion Personality
|
|
||||||
|
|
||||||
Select ONE archetype per project. Apply consistently.
|
|
||||||
|
|
||||||
| Archetype | Duration | Easing | Overshoot | Keywords |
|
|
||||||
|-----------|----------|--------|-----------|----------|
|
|
||||||
| **Playful** | 150-300ms | ease-out-back | 10-20% | fun, whimsical, bouncy, cute |
|
|
||||||
| **Premium** | 350-600ms | cubic-bezier(0.4,0,0.2,1) | 0% | elegant, minimal, luxury, sophisticated |
|
|
||||||
| **Corporate** | 200-400ms | cubic-bezier(0.2,0,0,1) | 0-3% | clean, professional, business, dashboard |
|
|
||||||
| **Energetic** | 100-250ms | ease-out-expo | 15-30% | dynamic, energetic, bold, exciting |
|
|
||||||
|
|
||||||
**Default**: Corporate for UI, Playful for illustrations.
|
|
||||||
|
|
||||||
**Brand Motion Identity** — define three constants:
|
|
||||||
1. **Signature easing**: One curve for 80% of animations
|
|
||||||
2. **Duration palette**: 3 durations (quick / standard / slow)
|
|
||||||
3. **Entrance pattern**: One consistent entry style
|
|
||||||
|
|
||||||
> Deep dive: [director/motion-personality.md](director/motion-personality.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Property Selection
|
|
||||||
|
|
||||||
| Effect Goal | Primary Property | Secondary Properties |
|
|
||||||
|-------------|------------------|---------------------|
|
|
||||||
| Entrance/Exit | position | opacity, scale |
|
|
||||||
| Emphasis/Attention | scale | rotation (subtle), opacity pulse |
|
|
||||||
| State Change | opacity, color | scale (press feedback) |
|
|
||||||
| Direction/Flow | position | rotation (follow path) |
|
|
||||||
| Depth/3D Feel | scale + shadow | position (parallax) |
|
|
||||||
| Loading/Progress | rotation (spinner) | scale, opacity pulse |
|
|
||||||
| Success | scale (pop) | color, rotation (checkmark draw) |
|
|
||||||
| Error/Alert | position (shake) | color, rotation (wobble) |
|
|
||||||
|
|
||||||
**Simplicity threshold**: Use the minimum properties needed. One = direct. Two = polished. Three+ = potentially overwhelming.
|
|
||||||
|
|
||||||
> Deep dive: [reference/property-selection.md](reference/property-selection.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Duration Table
|
|
||||||
|
|
||||||
| Element Type | Duration | Rationale |
|
|
||||||
|-------------|----------|-----------|
|
|
||||||
| Tooltip / micro-feedback | 80-120ms | Must feel instant |
|
|
||||||
| Button press / toggle | 120-180ms | Responsive feedback |
|
|
||||||
| Icon transition | 150-250ms | Clear state change |
|
|
||||||
| Card enter / exit | 200-350ms | Spatial awareness |
|
|
||||||
| Modal / dialog | 300-400ms | Focus shift |
|
|
||||||
| Page transition | 400-600ms | Context switch |
|
|
||||||
| Dramatic reveal | 600-1200ms | Theatrical build |
|
|
||||||
|
|
||||||
**Distance scales duration**: 100px = base. 200px = 1.3x. 400px = 1.6x.
|
|
||||||
|
|
||||||
**Enter > Exit**: Entrances 30-50% longer than exits. Users care about what appears.
|
|
||||||
|
|
||||||
**Interactive feedback**:
|
|
||||||
- Hover: <100ms
|
|
||||||
- Press: <150ms
|
|
||||||
- Release/settle: 200-300ms
|
|
||||||
- Error shake: 300-400ms (2-3 oscillations)
|
|
||||||
|
|
||||||
> Deep dive: [reference/timing-easing-tables.md](reference/timing-easing-tables.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Easing Selection
|
|
||||||
|
|
||||||
**Directional rules**:
|
|
||||||
- **Entrance** → decelerate (fast start, gentle landing): ease-out family
|
|
||||||
- **Exit** → accelerate (gentle start, fast departure): ease-in family
|
|
||||||
- **On-screen** → smooth both ends: ease-in-out family
|
|
||||||
- **Looping ambient** → seamless: sine-based ease-in-out
|
|
||||||
|
|
||||||
**Industry standards**:
|
|
||||||
|
|
||||||
| Standard | Cubic Bezier | Use For |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| Material Design 3 | (0.2, 0, 0, 1) | Default on-screen |
|
|
||||||
| MD3 Emphasized | (0.05, 0.7, 0.1, 1) | Entrances, attention |
|
|
||||||
| MD3 Accelerate | (0.3, 0, 1, 1) | Exits, dismissals |
|
|
||||||
| Apple HIG | (0.25, 0.1, 0.25, 1) | Standard iOS |
|
|
||||||
| Snappy UI | (0.2, 0, 0, 1) | Fast, decisive |
|
|
||||||
| Gentle float | (0.4, 0, 0.2, 1) | Ambient, background |
|
|
||||||
| Bounce settle | (0.175, 0.885, 0.32, 1.275) | Overshoot, playful |
|
|
||||||
|
|
||||||
**Material-based easing**:
|
|
||||||
|
|
||||||
| Material | Duration Scale | Overshoot |
|
|
||||||
|----------|---------------|-----------|
|
|
||||||
| Rigid (metal, stone) | 1.2x | 0% |
|
|
||||||
| Elastic (rubber, gel) | 0.8x | 15-25% |
|
|
||||||
| Fluid (water, paint) | 1.5x | 5% |
|
|
||||||
| Paper (cards, sheets) | 1.0x | 3-5% |
|
|
||||||
| Gas (smoke, fog) | 2.0x | 0% |
|
|
||||||
| Glass (brittle) | 0.9x | 0% |
|
|
||||||
|
|
||||||
> Deep dive: [reference/timing-easing-tables.md](reference/timing-easing-tables.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
### Button Press (Playful)
|
|
||||||
1. **Anticipation**: Scale to 0.97 (50ms, ease-out)
|
|
||||||
2. **Squash**: Scale to [1.04, 0.96] (100ms, ease-in)
|
|
||||||
3. **Follow through**: Overshoots to 1.02, settles to 1.0 (spring, 200ms)
|
|
||||||
4. **Secondary**: Shadow shrinks during press, icon shifts down 2px
|
|
||||||
5. **Total**: ~150ms press + 200ms settle
|
|
||||||
|
|
||||||
### Card Entrance (Premium)
|
|
||||||
1. **Start**: 20px below target, opacity 0
|
|
||||||
2. **Path**: Slight curve (10px X offset at midpoint)
|
|
||||||
3. **Easing**: ease-out-cubic deceleration
|
|
||||||
4. **Follow through**: Shadow arrives 50ms after card
|
|
||||||
5. **Secondary**: Content fades in 100ms after card lands
|
|
||||||
6. **Staging**: Other cards dim to 80%
|
|
||||||
|
|
||||||
### Success State (Playful)
|
|
||||||
1. **Primary**: Scale pop with ease-out-back
|
|
||||||
2. **Secondary**: Checkmark draws in
|
|
||||||
3. **Ambient**: Subtle particle burst
|
|
||||||
4. **Color**: Green fill
|
|
||||||
5. **Total**: 300-400ms
|
|
||||||
|
|
||||||
### Error Shake (Corporate)
|
|
||||||
1. **Primary**: Position oscillates 2-3 times, ±10-15px horizontal
|
|
||||||
2. **Easing**: ease-in-out for sharp stops
|
|
||||||
3. **Color**: Red tint
|
|
||||||
4. **Total**: 300-400ms
|
|
||||||
5. **No overshoot**: Errors feel firm
|
|
||||||
|
|
||||||
> More patterns: [patterns/entrance-exit.md](patterns/entrance-exit.md) | [patterns/state-feedback.md](patterns/state-feedback.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Choreography Essentials
|
|
||||||
|
|
||||||
**Coordinated entry**:
|
|
||||||
- Lead with the hero — primary element enters first or most prominently
|
|
||||||
- Spatial consistency — all elements enter from same direction
|
|
||||||
- Counter-motion — hero moves right → ambient moves left at 20-30% speed
|
|
||||||
|
|
||||||
**1/3 Rule (distance)**: No motion travels more than 1/3 of screen without a keyframe change.
|
|
||||||
|
|
||||||
**1/3 Rule (elements)**: With 3+ elements, no more than 1/3 in active motion simultaneously.
|
|
||||||
|
|
||||||
**Stagger budgets**:
|
|
||||||
|
|
||||||
| Pattern | Delay | Total Budget | Use Case |
|
|
||||||
|---------|-------|-------------|----------|
|
|
||||||
| Micro cascade | 20-40ms | <200ms | List items, grid cells |
|
|
||||||
| Standard | 50-100ms | <400ms | Cards, panels, nav |
|
|
||||||
| Dramatic | 100-200ms | <600ms | Hero sections |
|
|
||||||
| Wave | 30-60ms | <500ms | Data visualizations |
|
|
||||||
|
|
||||||
**Critical**: Total stagger must stay under 500ms.
|
|
||||||
|
|
||||||
> Deep dive: [director/choreography.md](director/choreography.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Emotion-to-Motion Map
|
|
||||||
|
|
||||||
| Emotion | Character | Path | Easing | Duration |
|
|
||||||
|---------|-----------|------|--------|----------|
|
|
||||||
| Joy | Bouncy, arcs | Curved, upward | ease-out-back | 200-400ms |
|
|
||||||
| Calm | Smooth, flowing | Gentle curves | sine ease-in-out | 500-1000ms |
|
|
||||||
| Urgency | Sharp, fast | Straight lines | ease-out | 100-200ms |
|
|
||||||
| Sadness | Slow, downward | Drooping curves | cubic ease-in-out | 600-1200ms |
|
|
||||||
| Surprise | Sudden, expanding | Radial outward | ease-out-expo | 150-300ms |
|
|
||||||
| Elegance | Slow, controlled | Long arcs | (0.4,0,0.2,1) | 400-700ms |
|
|
||||||
| Playfulness | Bouncy, irregular | Arcs, squiggly | ease-out-back | 200-350ms |
|
|
||||||
|
|
||||||
**Path as language**: Angular = tense. Curved = friendly. Spiral = whimsical. Diagonal = purposeful. Vertical = growth/weight. Horizontal = progress.
|
|
||||||
|
|
||||||
> Deep dive: [director/emotion-mapping.md](director/emotion-mapping.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Weight Classification
|
|
||||||
|
|
||||||
| Weight | Examples | Duration | Overshoot | Easing |
|
|
||||||
|--------|----------|----------|-----------|--------|
|
|
||||||
| Heavy | Modals, overlays | 300-500ms | 0% | Gentle, high damping |
|
|
||||||
| Medium | Cards, panels | 200-350ms | 3-5% | Moderate |
|
|
||||||
| Light | Tooltips, badges, icons | 80-200ms | 5-15% | Responsive |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quality Rules
|
|
||||||
|
|
||||||
### CRITICAL — never break
|
|
||||||
1. **Never linear for spatial movement** — always use easing curves (linear only for spinners, progress bars)
|
|
||||||
2. **Never opacity-only** for important state changes — combine with position or scale
|
|
||||||
3. **Never exceed 1/3 screen** without intermediate keyframe
|
|
||||||
4. **Always three motion layers** — primary + secondary + ambient
|
|
||||||
|
|
||||||
### HIGH — strongly follow
|
|
||||||
1. Match duration to element type (see tables)
|
|
||||||
2. Use directional easing (ease-out entrance, ease-in exit)
|
|
||||||
3. Apply Disney principles (especially anticipation, follow-through)
|
|
||||||
4. Maintain consistent personality across scene
|
|
||||||
|
|
||||||
> Full checklist: [reference/quality-checklist.md](reference/quality-checklist.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting Quick Reference
|
|
||||||
|
|
||||||
| Problem | Likely Cause | Fix |
|
|
||||||
|---------|-------------|-----|
|
|
||||||
| Looks robotic | Linear easing or no arcs | Add easing curves + arc paths |
|
|
||||||
| Feels too slow | Duration too long for element type | Check duration table, use ease-out |
|
|
||||||
| Feels cheap/flat | Missing secondary + ambient | Add shadow motion + background life |
|
|
||||||
| Too distracting | Too many elements moving | Apply 1/3 rule, reduce amplitude |
|
|
||||||
| No personality | Generic easing everywhere | Apply personality archetype consistently |
|
|
||||||
|
|
||||||
> Deep dive: [reference/troubleshooting.md](reference/troubleshooting.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Reference
|
|
||||||
|
|
||||||
**Philosophy** (director/):
|
|
||||||
- [core-philosophy.md](director/core-philosophy.md) — Three Pillars deep dive
|
|
||||||
- [decision-framework.md](director/decision-framework.md) — Full decision pipeline
|
|
||||||
- [disney-principles.md](director/disney-principles.md) — 12 principles, UI-adapted
|
|
||||||
- [motion-personality.md](director/motion-personality.md) — 4 archetypes + brand identity
|
|
||||||
- [emotion-mapping.md](director/emotion-mapping.md) — Emotion → motion + color psychology
|
|
||||||
- [choreography.md](director/choreography.md) — Multi-element coordination
|
|
||||||
- [narrative-structure.md](director/narrative-structure.md) — Micro-story framework
|
|
||||||
- [context-adaptation.md](director/context-adaptation.md) — Platform, a11y, performance
|
|
||||||
|
|
||||||
**Reference** (reference/):
|
|
||||||
- [timing-easing-tables.md](reference/timing-easing-tables.md) — Duration + easing lookups
|
|
||||||
- [property-selection.md](reference/property-selection.md) — Property communication guide
|
|
||||||
- [troubleshooting.md](reference/troubleshooting.md) — Animation smells + fixes
|
|
||||||
- [quality-checklist.md](reference/quality-checklist.md) — Evaluation criteria
|
|
||||||
|
|
||||||
**Patterns** (patterns/):
|
|
||||||
- [entrance-exit.md](patterns/entrance-exit.md) — Entrance/exit recipes
|
|
||||||
- [state-feedback.md](patterns/state-feedback.md) — Success, error, loading, hover
|
|
||||||
- [ambient-continuous.md](patterns/ambient-continuous.md) — Looping, breathing, parallax
|
|
||||||
- [multi-element.md](patterns/multi-element.md) — Stagger + choreography recipes
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Choreography
|
|
||||||
|
|
||||||
## Coordinated Entry Rules
|
|
||||||
|
|
||||||
### 1. Lead with the Hero
|
|
||||||
- Hero gets largest displacement and most attention-grabbing easing
|
|
||||||
- Supporting elements are subtler in every dimension
|
|
||||||
|
|
||||||
### 2. Spatial Origin Consistency
|
|
||||||
All elements enter from same direction or shared origin. Mixed directions = chaos.
|
|
||||||
|
|
||||||
### 3. Counter-Motion
|
|
||||||
|
|
||||||
| Hero Motion | Counter-Motion | Speed Ratio |
|
|
||||||
|-------------|---------------|-------------|
|
|
||||||
| Enters left | Background shifts right | 20-30% |
|
|
||||||
| Scales up | Shadow scales down | 10-20% |
|
|
||||||
| Rotates CW | Ambient drifts CCW | 15-25% |
|
|
||||||
| Lifts (Y up) | Shadow spreads + softens | 20-30% |
|
|
||||||
|
|
||||||
## Sequence Structure
|
|
||||||
|
|
||||||
| Phase | Duration Share | What Happens |
|
|
||||||
|-------|--------------|-------------|
|
|
||||||
| Setup | 20-30% | Elements enter, scene establishes |
|
|
||||||
| Action | 30-40% | Primary motion, hero moment |
|
|
||||||
| Resolution | 30-40% | Settle, secondary reactions, breathing |
|
|
||||||
|
|
||||||
Leave 100-200ms stillness after resolution before new motion.
|
|
||||||
|
|
||||||
## The 1/3 Rules
|
|
||||||
|
|
||||||
**Distance**: No motion travels >1/3 screen without intermediate keyframe. Break with direction changes, speed variations, or arc adjustments.
|
|
||||||
|
|
||||||
**Elements**: With 3+ animated elements, max 1/3 active simultaneously. Stagger so element 1 settles as element 3 starts.
|
|
||||||
|
|
||||||
## Stagger Patterns
|
|
||||||
|
|
||||||
| Pattern | Description | Best For |
|
|
||||||
|---------|------------|----------|
|
|
||||||
| Sequential | Reading order | Lists, grids |
|
|
||||||
| Center-out | Radiating from center | Hero content, ripples |
|
|
||||||
| Random | Varied timing | Organic, particle-like |
|
|
||||||
| Wave | Sine-based | Data bars, continuous |
|
|
||||||
| Reverse | Bottom-to-top | Exits, backward nav |
|
|
||||||
|
|
||||||
- All staggered elements use same easing family
|
|
||||||
- Vary only start time, not curve
|
|
||||||
- Optional: last element gets slight overshoot (punctuation)
|
|
||||||
|
|
||||||
## Shared Motion Events
|
|
||||||
|
|
||||||
When multiple elements react to one trigger:
|
|
||||||
- All start within 50ms of each other
|
|
||||||
- Can arrive at different times (staggered landing)
|
|
||||||
- Same easing family; motion originates from trigger point
|
|
||||||
|
|
||||||
## Attention Direction
|
|
||||||
|
|
||||||
| Technique | Implementation |
|
|
||||||
|-----------|---------------|
|
|
||||||
| Leading motion | Animate target before context |
|
|
||||||
| Following motion | Settle on focal point |
|
|
||||||
| Ambient motion | Subtle continuous in periphery |
|
|
||||||
| Pointing motion | Directional toward CTA |
|
|
||||||
|
|
||||||
### Depth Through Speed
|
|
||||||
|
|
||||||
| Layer | Displacement | Speed |
|
|
||||||
|-------|-------------|-------|
|
|
||||||
| Foreground | 1.0x | Fastest |
|
|
||||||
| Midground | 0.5x | Medium |
|
|
||||||
| Background | 0.2x | Slowest |
|
|
||||||
|
|
||||||
## Common Recipes
|
|
||||||
|
|
||||||
### Dashboard Load
|
|
||||||
1. Skeletons fade in (100ms)
|
|
||||||
2. Hero metric (250ms, ease-out, 100ms delay)
|
|
||||||
3. Supporting cards stagger (50ms between, 200ms each)
|
|
||||||
4. Chart data draws in (300ms, starts with cards)
|
|
||||||
5. Ambient pulse on primary metric
|
|
||||||
|
|
||||||
### Modal Open
|
|
||||||
1. Background dims (200ms)
|
|
||||||
2. Modal scales 95%→100% + fades (300ms, 50ms delay)
|
|
||||||
3. Content fades in (200ms, 100ms after modal)
|
|
||||||
4. Close button last (150ms)
|
|
||||||
|
|
||||||
### List Update (item added)
|
|
||||||
1. Existing items shift down (200ms, ease-in-out)
|
|
||||||
2. New item fades+slides from top (250ms, ease-out, 50ms delay)
|
|
||||||
3. Subtle scale overshoot on land (3-5%)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Context Adaptation
|
|
||||||
|
|
||||||
## Platform Scaling
|
|
||||||
|
|
||||||
| Platform | Duration Modifier | Complexity | Physics |
|
|
||||||
|----------|------------------|------------|---------|
|
|
||||||
| Desktop | 1.0x (baseline) | Full | All types |
|
|
||||||
| Tablet | 0.9x | Standard | Most types |
|
|
||||||
| Mobile | 0.8x | Reduced (1-2 properties) | Snappy only |
|
|
||||||
| Watch | 0.6x | Minimal (1 property) | None |
|
|
||||||
| TV/Kiosk | 1.3x | Full | All types |
|
|
||||||
|
|
||||||
**Mobile rules**: prefer opacity + transform; touch feedback <100ms; reduce stagger budgets by 30%; avoid parallax
|
|
||||||
**Desktop opportunities**: hover states, cursor tracking, multi-column stagger, spatial choreography
|
|
||||||
|
|
||||||
## Accessibility
|
|
||||||
|
|
||||||
### prefers-reduced-motion
|
|
||||||
|
|
||||||
| Original Motion | Reduced Alternative |
|
|
||||||
|----------------|-------------------|
|
|
||||||
| Slide entrance | Opacity fade only |
|
|
||||||
| Bounce/spring | Instant or simple ease-out |
|
|
||||||
| Parallax | Static positioning |
|
|
||||||
| Auto-playing | Paused, user-initiated |
|
|
||||||
| Complex choreography | Single fade |
|
|
||||||
| Continuous ambient | Static or subtle opacity pulse |
|
|
||||||
|
|
||||||
Reduced motion means: remove spatial movement, keep opacity, remove spring easing, reduce duration 50%+, never auto-play loops.
|
|
||||||
|
|
||||||
### Vestibular Triggers (avoid or provide alternatives)
|
|
||||||
- Large-scale zoom, full-screen position transitions
|
|
||||||
- Spinning elements >100px, parallax >2 layers, rapid direction changes
|
|
||||||
|
|
||||||
### Cognitive Accessibility
|
|
||||||
- Same interaction = same animation every time
|
|
||||||
- Pause controls for animations >5 seconds
|
|
||||||
- Don't convey critical info through motion alone
|
|
||||||
|
|
||||||
## Performance Budgets
|
|
||||||
|
|
||||||
| Tier | Properties | Max Elements |
|
|
||||||
|------|-----------|-------------|
|
|
||||||
| Optimal | transform, opacity | Unlimited (GPU) |
|
|
||||||
| Good | + color, clip-path | 10-15 |
|
|
||||||
| Acceptable | + width, height, margin | 5-8 |
|
|
||||||
| Avoid | box-shadow, border-radius, filter | 1-3 |
|
|
||||||
|
|
||||||
- Target 60fps (16.67ms/frame); animation logic <10ms/frame
|
|
||||||
- will-change sparingly; keep animated elements <20 per viewport
|
|
||||||
- Stagger reduces peak load vs simultaneous
|
|
||||||
- Fallback: 30fps acceptable for ambient
|
|
||||||
|
|
||||||
## Content Type Adaptation
|
|
||||||
|
|
||||||
| Content Type | Personality | Duration | Motion Density |
|
|
||||||
|-------------|-------------|----------|---------------|
|
|
||||||
| Financial | Corporate/Premium | 250-500ms | Low |
|
|
||||||
| Social media | Playful | 150-300ms | Medium |
|
|
||||||
| Enterprise SaaS | Corporate | 200-400ms | Low |
|
|
||||||
| Gaming | Energetic | 100-250ms | High |
|
|
||||||
| Healthcare | Corporate/Calm | 300-600ms | Very low |
|
|
||||||
| E-commerce | Varies | 200-400ms | Medium |
|
|
||||||
| Editorial | Premium | 350-600ms | Low |
|
|
||||||
| Children's apps | Playful | 150-300ms | High |
|
|
||||||
|
|
||||||
## Responsive Motion
|
|
||||||
|
|
||||||
| Container Width | Max Displacement | Duration |
|
|
||||||
|----------------|-----------------|----------|
|
|
||||||
| <400px | 20% of width | 0.8x |
|
|
||||||
| 400-800px | 25% of width | 1.0x |
|
|
||||||
| 800-1200px | 20% of width | 1.0x |
|
|
||||||
| >1200px | 15% of width | 1.1x |
|
|
||||||
|
|
||||||
- Small viewport: sequential, one element at a time
|
|
||||||
- Medium: standard stagger, 2-3 columns
|
|
||||||
- Large: full choreography, center-out stagger, parallax
|
|
||||||
|
|
||||||
## Dark Mode
|
|
||||||
- Reduce motion intensity 10-20% (bright on dark = more impact)
|
|
||||||
- Subtler ambient motion; careful with opacity values
|
|
||||||
- Avoid pure white flashes
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Core Philosophy
|
|
||||||
|
|
||||||
## Three Pillars
|
|
||||||
|
|
||||||
### Pillar 1: Emotional Intent
|
|
||||||
Define target emotion before choosing any property.
|
|
||||||
|
|
||||||
| Emotion | Character | Timing | Easing |
|
|
||||||
|---------|----------|--------|--------|
|
|
||||||
| Trust | Smooth, predictable | 300-400ms | Gentle curves |
|
|
||||||
| Delight | Bouncy, surprising | 200-300ms | Overshoot |
|
|
||||||
| Urgency | Sharp, direct | 100-200ms | Snappy ease-out |
|
|
||||||
| Calm | Slow, flowing | 500-1000ms | Sine curves |
|
|
||||||
| Surprise | Sudden, explosive | 150-300ms | Exponential |
|
|
||||||
| Confidence | Direct, decisive | 200-400ms | Strong ease-out |
|
|
||||||
|
|
||||||
### Pillar 2: Visual Narrative
|
|
||||||
|
|
||||||
| Phase | Duration Share | Purpose |
|
|
||||||
|-------|--------------|---------|
|
|
||||||
| Setup | 20-30% | Establish context, prepare viewer |
|
|
||||||
| Action | 30-40% | Primary motion, hero moment |
|
|
||||||
| Resolution | 30-40% | Settle, breathe, confirm |
|
|
||||||
|
|
||||||
Even a 200ms tooltip fade has implicit setup→action→resolution.
|
|
||||||
|
|
||||||
### Pillar 3: Motion Craft
|
|
||||||
- Easing curves match emotional intent
|
|
||||||
- Duration proportional to element size and distance
|
|
||||||
- Arcs for organic, straight for mechanical
|
|
||||||
- Secondary motion (shadows, related elements)
|
|
||||||
- Nothing starts and stops all at once
|
|
||||||
|
|
||||||
## Three Motion Layers
|
|
||||||
|
|
||||||
| Layer | Role | Amplitude |
|
|
||||||
|-------|------|-----------|
|
|
||||||
| Primary | Main action viewer follows | 100% |
|
|
||||||
| Secondary | Supporting richness | 30-50% |
|
|
||||||
| Ambient | Background life | 10-20% |
|
|
||||||
|
|
||||||
- Secondary offset 50-100ms from primary, different easing
|
|
||||||
- Ambient is continuous/slow, never demands attention
|
|
||||||
- Primary-only animation feels flat; always add secondary + ambient
|
|
||||||
|
|
||||||
## The 1/3 Screen Rule
|
|
||||||
No motion travels >1/3 screen without intermediate keyframe. Break with direction changes, speed shifts, or arc adjustments.
|
|
||||||
|
|
||||||
## The Attention Budget
|
|
||||||
- One hero motion per scene moment
|
|
||||||
- Max 2-3 elements in active motion simultaneously
|
|
||||||
- Ambient doesn't count against budget
|
|
||||||
- Stagger rather than synchronize
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Decision Framework
|
|
||||||
|
|
||||||
## The First Questions
|
|
||||||
|
|
||||||
### 1. Purpose?
|
|
||||||
|
|
||||||
| Purpose | Approach | Priority |
|
|
||||||
|---------|----------|----------|
|
|
||||||
| Draw attention | Scale, color, pulse | Responsiveness |
|
|
||||||
| Guide sequence | Stagger, directional flow | Clarity |
|
|
||||||
| Provide feedback | Immediate response | Speed |
|
|
||||||
| Create atmosphere | Subtle, continuous | Subtlety |
|
|
||||||
| Explain relationship | Position, connection | Legibility |
|
|
||||||
| Celebrate/reward | Overshoot, particles | Delight |
|
|
||||||
|
|
||||||
### 2. Audience?
|
|
||||||
- Stressed → calm motion; Browsing → richer motion
|
|
||||||
- Task-focused → fast, minimal; Exploring → can be dramatic
|
|
||||||
- Seen 100x/day → fast, subtle; Seen once → can be dramatic
|
|
||||||
|
|
||||||
### 3. Context?
|
|
||||||
- Busy layout → simpler motion; Empty → can be dramatic
|
|
||||||
- Other animations active → coordinate or stagger
|
|
||||||
- Small container → small motion
|
|
||||||
|
|
||||||
## 4-Level Decision Hierarchy
|
|
||||||
|
|
||||||
### Level 1: Motion Category
|
|
||||||
|
|
||||||
| Category | When |
|
|
||||||
|----------|------|
|
|
||||||
| Revealing | Content appearing |
|
|
||||||
| Concealing | Content disappearing |
|
|
||||||
| Transitioning | Between states/views |
|
|
||||||
| Emphasizing | Drawing attention |
|
|
||||||
| Responding | Reacting to interaction |
|
|
||||||
| Ambient | Background atmosphere |
|
|
||||||
|
|
||||||
### Level 2: Motion Personality
|
|
||||||
Match to brand. See [motion-personality.md](motion-personality.md).
|
|
||||||
|
|
||||||
### Level 3: Motion Direction
|
|
||||||
|
|
||||||
| Direction | Communicates |
|
|
||||||
|-----------|-------------|
|
|
||||||
| Upward | Growth, aspiration |
|
|
||||||
| Downward | Settling, completion |
|
|
||||||
| Leftward | Regression, departure |
|
|
||||||
| Rightward | Progression, arrival |
|
|
||||||
| Outward (scale up) | Importance, emergence |
|
|
||||||
| Inward (scale down) | Dismissal, removal |
|
|
||||||
| Center-seeking | Focus, convergence |
|
|
||||||
| Center-fleeing | Distribution, release |
|
|
||||||
|
|
||||||
### Level 4: Implementation Properties
|
|
||||||
Choose animated properties. See [property-selection.md](../reference/property-selection.md).
|
|
||||||
|
|
||||||
## Decision Quick-Path
|
|
||||||
|
|
||||||
```
|
|
||||||
1. PURPOSE → What does this motion DO?
|
|
||||||
├── Feedback → short, ease-out, minimal
|
|
||||||
├── Entrance → ease-out, personality-matched
|
|
||||||
├── Exit → ease-in, 70% of entrance
|
|
||||||
├── Emphasis → scale or color pulse
|
|
||||||
└── Ambient → slow, continuous, sine
|
|
||||||
|
|
||||||
2. PERSONALITY → Match keywords
|
|
||||||
├── Fun/bouncy → Playful
|
|
||||||
├── Elegant/luxury → Premium
|
|
||||||
├── Clean/professional → Corporate
|
|
||||||
└── Bold/dynamic → Energetic
|
|
||||||
|
|
||||||
3. PROPERTIES → Minimum needed (two = sweet spot)
|
|
||||||
|
|
||||||
4. TIMING → Check duration table, adjust for distance/weight
|
|
||||||
|
|
||||||
5. EASING → Entering=ease-out, Leaving=ease-in, On-screen=ease-in-out, Looping=sine
|
|
||||||
```
|
|
||||||
|
|
||||||
## Evaluation Before Delivery
|
|
||||||
|
|
||||||
| Check | Question |
|
|
||||||
|-------|----------|
|
|
||||||
| Purpose | Clear function? |
|
|
||||||
| Personality | Matches brand/context? |
|
|
||||||
| Repetition | Fine on 100th viewing? |
|
|
||||||
| Attention | Respects viewer's focus? |
|
|
||||||
| Hierarchy | Reinforces importance? |
|
|
||||||
| Layers | Primary + secondary + ambient? |
|
|
||||||
| Timing | Duration appropriate? |
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# Disney's 12 Principles — UI Adapted
|
|
||||||
|
|
||||||
## 1. Squash and Stretch
|
|
||||||
|
|
||||||
- Squash: scale ~[1.2, 0.8]; Stretch: ~[0.85, 1.15]
|
|
||||||
- Impact: 2-4 frames (30-65ms); Recovery: 4-8 frames (65-130ms)
|
|
||||||
- Preserve volume: width +20% → height decreases proportionally
|
|
||||||
- Skip for premium/luxury brands
|
|
||||||
|
|
||||||
## 2. Anticipation
|
|
||||||
|
|
||||||
- Small motion opposite to main direction before action
|
|
||||||
- Duration: 100-200ms, magnitude: 10-20% of main action
|
|
||||||
- Button: scale down 3% before expanding; Card: shift 5-10px away first
|
|
||||||
- Skip for micro-feedback (<150ms)
|
|
||||||
|
|
||||||
## 3. Staging
|
|
||||||
|
|
||||||
- Dim non-hero elements to 40-60% opacity; optional 2-4px blur
|
|
||||||
- Hero enters 100-200ms after supporting elements
|
|
||||||
- One primary action per timing beat
|
|
||||||
|
|
||||||
## 4. Straight Ahead vs. Pose to Pose
|
|
||||||
|
|
||||||
| Approach | Feel | Best For |
|
|
||||||
|----------|------|----------|
|
|
||||||
| Straight Ahead | Fluid, spontaneous | Particles, ambient, generative art |
|
|
||||||
| Pose to Pose | Planned, controlled | UI transitions, state changes |
|
|
||||||
|
|
||||||
## 5. Follow Through and Overlapping Action
|
|
||||||
|
|
||||||
- Child delay: 50-150ms behind parent
|
|
||||||
- Trailing elements: offset stop times by 100-200ms
|
|
||||||
- Use spring easing for trailing parts (lower stiffness = more trailing)
|
|
||||||
|
|
||||||
## 6. Slow In and Slow Out
|
|
||||||
|
|
||||||
| Context | Easing | Why |
|
|
||||||
|---------|--------|-----|
|
|
||||||
| Entrance | ease-out | Arrives smoothly |
|
|
||||||
| Exit | ease-in | Departs quickly |
|
|
||||||
| On-screen | ease-in-out | Smooth journey |
|
|
||||||
| Ambient loop | sine ease-in-out | Seamless |
|
|
||||||
|
|
||||||
**NEVER** linear for spatial movement. Linear only for: rotation, progress bars, timers.
|
|
||||||
|
|
||||||
## 7. Arcs
|
|
||||||
|
|
||||||
- Add 10-20px perpendicular offset at path midpoint
|
|
||||||
- Subtle (5px) for corporate, pronounced (20px+) for playful
|
|
||||||
- Mechanical UIs can use straight paths intentionally
|
|
||||||
|
|
||||||
## 8. Secondary Action
|
|
||||||
|
|
||||||
- Amplitude: 30-50% of primary; timing: 50-100ms after primary
|
|
||||||
- Different easing than primary
|
|
||||||
- Examples: card enters → shadow grows; button presses → ripple expands
|
|
||||||
|
|
||||||
## 9. Timing
|
|
||||||
|
|
||||||
| Weight/Mood | Duration |
|
|
||||||
|-------------|----------|
|
|
||||||
| Heavy (modals, pages) | 400-800ms |
|
|
||||||
| Light (tooltips, toggles) | 100-250ms |
|
|
||||||
| Sad/serious | 600ms+ |
|
|
||||||
| Happy/light | 200-400ms |
|
|
||||||
| Urgent | 100-200ms |
|
|
||||||
|
|
||||||
Enter-exit asymmetry: entrances 30-50% longer than exits.
|
|
||||||
|
|
||||||
## 10. Exaggeration
|
|
||||||
|
|
||||||
| Personality | Exaggeration |
|
|
||||||
|-------------|-------------|
|
|
||||||
| Playful | 15-25% |
|
|
||||||
| Energetic | 20-30% |
|
|
||||||
| Corporate | 0-5% |
|
|
||||||
| Premium | 0% |
|
|
||||||
|
|
||||||
- Scale overshoot: 10-30% beyond target; rotation: ±5-15°
|
|
||||||
|
|
||||||
## 11. Solid Drawing
|
|
||||||
|
|
||||||
- Maintain consistent proportions across keyframes
|
|
||||||
- Use scale + rotation together for depth
|
|
||||||
- Shadow behavior matches implied light source
|
|
||||||
|
|
||||||
## 12. Appeal
|
|
||||||
|
|
||||||
- Smooth curves over sharp angles; satisfying timing
|
|
||||||
- Personality consistency across all elements
|
|
||||||
- Appeal killers: jerky motion, inconsistent timing, abrupt stops, uniform animation
|
|
||||||
|
|
||||||
## Combining Principles
|
|
||||||
|
|
||||||
| Recipe | Principles Used |
|
|
||||||
|--------|----------------|
|
|
||||||
| Button press | Anticipation + Squash/Stretch + Follow-Through + Secondary + Timing |
|
|
||||||
| Card entrance | Anticipation + Arcs + Slow In/Out + Follow-Through + Staging |
|
|
||||||
| Success celebration | Exaggeration + Secondary + Timing + Squash/Stretch + Appeal |
|
|
||||||
| Error shake | Timing + Slow In/Out + Staging (no exaggeration) |
|
|
||||||
| Loading spinner | Timing + Slow In/Out + Appeal |
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Emotion-to-Motion Mapping
|
|
||||||
|
|
||||||
## Core Table
|
|
||||||
|
|
||||||
| Emotion | Character | Path | Easing | Duration |
|
|
||||||
|---------|----------|------|--------|----------|
|
|
||||||
| Joy/Delight | Bouncy, arcs, overshoot | Curved, upward | ease-out-back | 200-400ms |
|
|
||||||
| Calm/Serenity | Smooth, flowing | Gentle curves | sine ease-in-out | 500-1000ms |
|
|
||||||
| Urgency/Alert | Sharp, fast, direct | Straight lines | ease-out | 100-200ms |
|
|
||||||
| Sadness/Weight | Slow, downward | Drooping curves | cubic ease-in-out | 600-1200ms |
|
|
||||||
| Surprise/Impact | Sudden, expanding | Radial outward | ease-out-expo | 150-300ms |
|
|
||||||
| Elegance/Grace | Slow, controlled | Long smooth arcs | (0.4, 0, 0.2, 1) | 400-700ms |
|
|
||||||
| Playfulness | Bouncy, irregular | Arcs, squiggly | ease-out-back | 200-350ms |
|
|
||||||
| Confidence | Direct, decisive | Straight, horizontal | ease-out | 200-400ms |
|
|
||||||
| Curiosity | Exploratory, varied | Mixed, circular | varied | 300-500ms |
|
|
||||||
| Tenderness | Soft, gentle | Very subtle curves | soft ease-in-out | 600-1000ms |
|
|
||||||
|
|
||||||
## Path as Emotional Language
|
|
||||||
|
|
||||||
| Path Type | Connotation |
|
|
||||||
|-----------|------------|
|
|
||||||
| Angular/sharp | Tense, urgent, mechanical |
|
|
||||||
| Curved/smooth | Relaxed, friendly, organic |
|
|
||||||
| Spiral | Playful, whimsical |
|
|
||||||
| Straight diagonal | Dynamic, purposeful |
|
|
||||||
| Vertical up | Growth, achievement |
|
|
||||||
| Vertical down | Settling, gravity |
|
|
||||||
| Horizontal | Journey, progress |
|
|
||||||
| Radial outward | Explosion, release |
|
|
||||||
| Radial inward | Focus, convergence |
|
|
||||||
|
|
||||||
## Emotional Intensity
|
|
||||||
|
|
||||||
| Intensity | Characteristics | When |
|
|
||||||
|-----------|----------------|------|
|
|
||||||
| Low | Subtle opacity, tiny shifts | Ambient, routine |
|
|
||||||
| Medium | Visible but not demanding | Most UI interactions |
|
|
||||||
| High | Demands attention, large displacement | Errors, celebrations, onboarding |
|
|
||||||
|
|
||||||
## Color Psychology
|
|
||||||
|
|
||||||
| Color | Emotion | Animation Pairing |
|
|
||||||
|-------|---------|------------------|
|
|
||||||
| Blue | Trust, calm | Smooth, medium transitions |
|
|
||||||
| Green | Success, growth | Upward, expansion, gentle overshoot |
|
|
||||||
| Red | Alert, urgency | Sharp, fast, horizontal shakes |
|
|
||||||
| Orange | Energy, warmth | Bouncy, diagonal paths |
|
|
||||||
| Purple | Premium, mystery | Slow reveals, elegant easing |
|
|
||||||
| Yellow | Optimism, caution | Quick pulses |
|
|
||||||
| Teal | Modern, clarity | Clean, snappy transitions |
|
|
||||||
|
|
||||||
### Color Transition Rules
|
|
||||||
- Success: transition TO green (don't start with it)
|
|
||||||
- Error: flash red then settle (don't sustain)
|
|
||||||
- Warning: pulse yellow/amber for urgency
|
|
||||||
- Neutral: use opacity rather than color change
|
|
||||||
|
|
||||||
## Context-Based Emotion Defaults
|
|
||||||
|
|
||||||
| Context | Default Emotion |
|
|
||||||
|---------|----------------|
|
|
||||||
| Form success | Joy + Confidence |
|
|
||||||
| Validation error | Mild urgency |
|
|
||||||
| Page load | Calm + Confidence |
|
|
||||||
| Navigation | Confidence |
|
|
||||||
| Notification | Mild surprise |
|
|
||||||
| Loading | Calm |
|
|
||||||
| Onboarding | Curiosity + Delight |
|
|
||||||
| Dashboard | Calm + Confidence |
|
|
||||||
| Purchase complete | Joy + Confidence |
|
|
||||||
| Delete/remove | Calm (respectful departure) |
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Motion Personality
|
|
||||||
|
|
||||||
## Four Archetypes
|
|
||||||
|
|
||||||
### Playful
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|-----------|-------|
|
|
||||||
| Duration | 150-300ms |
|
|
||||||
| Easing | ease-out-back / bouncy springs |
|
|
||||||
| Overshoot | 10-20% |
|
|
||||||
| Paths | Arcs and curves, never straight |
|
|
||||||
| Squash-stretch | Yes, on impacts |
|
|
||||||
|
|
||||||
Signature: bounce settle, squash-stretch on press, rotation wobble, bright color pops, varied stagger timing.
|
|
||||||
Use for: children's apps, casual games, social media, celebrations, onboarding, creative tools.
|
|
||||||
|
|
||||||
### Premium / Luxury
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|-----------|-------|
|
|
||||||
| Duration | 350-600ms |
|
|
||||||
| Easing | cubic-bezier(0.4, 0, 0.2, 1) |
|
|
||||||
| Overshoot | 0% |
|
|
||||||
| Paths | Smooth curves, subtle parallax |
|
|
||||||
| Squash-stretch | Never |
|
|
||||||
|
|
||||||
Signature: slow fades, subtle scale (98%→100%), generous pauses, minimal properties (opacity+one), ultra-smooth.
|
|
||||||
Use for: fashion, finance, luxury brands, premium SaaS, portfolios, editorial.
|
|
||||||
|
|
||||||
### Corporate / Professional
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|-----------|-------|
|
|
||||||
| Duration | 200-400ms |
|
|
||||||
| Easing | cubic-bezier(0.2, 0, 0, 1) |
|
|
||||||
| Overshoot | 0-3% |
|
|
||||||
| Paths | Mostly straight, small arcs for emphasis |
|
|
||||||
| Squash-stretch | No |
|
|
||||||
|
|
||||||
Signature: consistent timing, clear state transitions, functional motion, predictable patterns, uniform stagger.
|
|
||||||
Use for: enterprise, dashboards, business tools, admin, healthcare, banking.
|
|
||||||
|
|
||||||
### Energetic / Dynamic
|
|
||||||
|
|
||||||
| Parameter | Value |
|
|
||||||
|-----------|-------|
|
|
||||||
| Duration | 100-250ms |
|
|
||||||
| Easing | ease-out-expo / elastic |
|
|
||||||
| Overshoot | 15-30% |
|
|
||||||
| Paths | Dramatic arcs, large displacement, diagonal |
|
|
||||||
| Squash-stretch | Yes, exaggerated |
|
|
||||||
|
|
||||||
Signature: large scale changes (50-150%), fast color transitions, particle bursts, accelerating stagger, bold edge entrances.
|
|
||||||
Use for: gaming, sports, music, events, marketing, fitness apps.
|
|
||||||
|
|
||||||
## Keyword Matching
|
|
||||||
|
|
||||||
| Keywords | Archetype |
|
|
||||||
|----------|-----------|
|
|
||||||
| fun, whimsical, bouncy, cute, friendly | Playful |
|
|
||||||
| elegant, minimal, luxury, sophisticated | Premium |
|
|
||||||
| clean, professional, business, dashboard | Corporate |
|
|
||||||
| dynamic, energetic, bold, exciting | Energetic |
|
|
||||||
| (unspecified) + UI | Corporate (default) |
|
|
||||||
| (unspecified) + illustration | Playful (default) |
|
|
||||||
|
|
||||||
## Brand Motion Identity
|
|
||||||
|
|
||||||
Define three constants for recognizable motion:
|
|
||||||
|
|
||||||
### 1. Signature Easing (80% of animations)
|
|
||||||
Playful: ease-out-back | Premium: (0.4,0,0.2,1) | Corporate: (0.2,0,0,1) | Energetic: ease-out-expo
|
|
||||||
|
|
||||||
### 2. Duration Palette
|
|
||||||
|
|
||||||
| Tier | Playful | Premium | Corporate | Energetic |
|
|
||||||
|------|---------|---------|-----------|-----------|
|
|
||||||
| Quick | 150ms | 350ms | 200ms | 100ms |
|
|
||||||
| Standard | 250ms | 500ms | 300ms | 180ms |
|
|
||||||
| Slow | 400ms | 800ms | 450ms | 300ms |
|
|
||||||
|
|
||||||
### 3. Entrance Pattern
|
|
||||||
Playful: bounce up from below | Premium: slow fade + scale 98%→100% | Corporate: slide right + opacity | Energetic: snap from edge + overshoot
|
|
||||||
|
|
||||||
## Mixing Archetypes
|
|
||||||
- 90% primary archetype; specific moments can borrow another
|
|
||||||
- Ease into personality shifts, don't snap
|
|
||||||
- Example: corporate dashboard borrows Playful for success state only
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Narrative Structure
|
|
||||||
|
|
||||||
## Four-Act Structure
|
|
||||||
|
|
||||||
### Act 1: Anticipation (10-20%)
|
|
||||||
- Wind-up: small motion opposite to main direction
|
|
||||||
- Gathering: elements pull together before dispersing
|
|
||||||
- Dimming: context fades for focus; Tension: hold compressed ~50ms
|
|
||||||
- Skip for <150ms interactions and hover states
|
|
||||||
|
|
||||||
### Act 2: Action (30-50%)
|
|
||||||
Peak energy. The primary communicative motion.
|
|
||||||
- Fast+direct (sharp easing) → alerts
|
|
||||||
- Smooth+flowing (gentle easing, curves) → transitions
|
|
||||||
- Explosive+expanding (expo, radial) → celebrations
|
|
||||||
- Controlled+precise (linear-ish) → data charts
|
|
||||||
|
|
||||||
### Act 3: Reaction (10-20%)
|
|
||||||
- Shadows adjust: 50-100ms after primary
|
|
||||||
- Siblings shift: 50-150ms after primary
|
|
||||||
- Environment ripples: 100-200ms after primary
|
|
||||||
- Counter-motion: simultaneous with action
|
|
||||||
- Skip for simple toggles/checkboxes
|
|
||||||
|
|
||||||
### Act 4: Resolution (20-30%)
|
|
||||||
- Overshoot settle (spring/back-easing); opacity reaches final (ease-out)
|
|
||||||
- 100-200ms breathing room before next motion
|
|
||||||
- Even 50ms of settling transforms the feel
|
|
||||||
|
|
||||||
## Scaling to Duration
|
|
||||||
|
|
||||||
| Total | Anticipation | Action | Reaction | Resolution |
|
|
||||||
|-------|-------------|--------|----------|------------|
|
|
||||||
| 100-200ms | skip | 60-70% | skip | 30-40% |
|
|
||||||
| 200-400ms | 10-15% | 40-50% | 10-15% | 25-30% |
|
|
||||||
| 400-800ms | 15-20% | 30-40% | 15-20% | 20-25% |
|
|
||||||
| 800ms+ | 20% | 30-35% | 15-20% | 25-30% |
|
|
||||||
|
|
||||||
## Multi-Beat Narratives
|
|
||||||
|
|
||||||
Transitions: overlap (fluid) | sequential (clear) | simultaneous (parallel)
|
|
||||||
|
|
||||||
Progressions:
|
|
||||||
- **Build-Up**: low → rising → peak → settling
|
|
||||||
- **Cycle**: depart → peak → return → repeat
|
|
||||||
- **Impact**: sudden action → ripple → slow settle
|
|
||||||
|
|
||||||
## By Personality
|
|
||||||
|
|
||||||
| Personality | Anticipation | Action | Resolution |
|
|
||||||
|------------|-------------|--------|------------|
|
|
||||||
| Playful | Exaggerated wind-up | Bouncy, overshoot | Wobble settle |
|
|
||||||
| Premium | Subtle tension | Smooth, controlled | Elegant ease |
|
|
||||||
| Corporate | Minimal/none | Direct, efficient | Clean stop |
|
|
||||||
| Energetic | Quick gather | Explosive | Fast snap |
|
|
||||||
|
|
||||||
## Common Patterns
|
|
||||||
|
|
||||||
- **Reveal**: tension (dim, scale 95%) → emerge (100%, full opacity) → surroundings adjust → settled
|
|
||||||
- **Departure**: gather (scale 98%) → exit → close gap → layout settles
|
|
||||||
- **Transformation**: destabilize (vibration) → morph → secondary appears → new state breathing
|
|
||||||
- **Celebration**: compress (50-100ms) → burst (scale, particles) → settle → calm with positive state
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Ambient & Continuous Patterns
|
|
||||||
|
|
||||||
Ambient amplitude: 10-20% of primary motion. Never compete for attention.
|
|
||||||
|
|
||||||
## Breathing / Pulse
|
|
||||||
|
|
||||||
- Scale oscillation 0.98-1.02, sine ease-in-out, 2000-4000ms/cycle
|
|
||||||
- Alt: opacity oscillation 0.7-1.0
|
|
||||||
|
|
||||||
| Context | Scale Range | Duration | Opacity Range |
|
|
||||||
|---------|-----------|----------|--------------|
|
|
||||||
| Active indicator | 0.95-1.05 | 2000ms | 0.6-1.0 |
|
|
||||||
| Waiting/idle | 0.98-1.02 | 3000ms | 0.8-1.0 |
|
|
||||||
| Background element | 0.99-1.01 | 4000ms | 0.9-1.0 |
|
|
||||||
| CTA attention | 0.97-1.03 | 2500ms | 0.7-1.0 |
|
|
||||||
|
|
||||||
Pulsing >±5% scale becomes attention-demanding.
|
|
||||||
|
|
||||||
## Floating / Hovering
|
|
||||||
|
|
||||||
- Y position ±5-15px, sine ease-in-out, 3000-5000ms/cycle
|
|
||||||
- Optional: slight rotation ±2-3°, offset 30% of position cycle
|
|
||||||
|
|
||||||
**Layered floating** (multiple elements — different durations prevent sync):
|
|
||||||
- Element 1: 4000ms, ±10px
|
|
||||||
- Element 2: 5500ms, ±8px (offset 30%)
|
|
||||||
- Element 3: 3500ms, ±12px (offset 60%)
|
|
||||||
|
|
||||||
## Gradient Shift
|
|
||||||
|
|
||||||
- Background-position or gradient angle shift
|
|
||||||
- Duration: 8000-20000ms/cycle; range: ±10-20% position or ±15° angle
|
|
||||||
- Easing: linear or sine; should be imperceptible at a glance
|
|
||||||
|
|
||||||
## Parallax
|
|
||||||
|
|
||||||
| Layer | Speed Ratio | Content |
|
|
||||||
|-------|------------|---------|
|
|
||||||
| Foreground | 1.0x | Interactive, text |
|
|
||||||
| Midground | 0.5x | Decorative, cards |
|
|
||||||
| Background | 0.2x | Patterns, scenery |
|
|
||||||
| Deep background | 0.1x | Texture, atmospheric |
|
|
||||||
|
|
||||||
**Scroll-driven**: total displacement <100px; avoid on mobile; never parallax text
|
|
||||||
**Mouse-driven**: foreground 10-20px max, background 5-10px opposite direction, 100-200ms interpolation
|
|
||||||
|
|
||||||
## Continuous Rotation
|
|
||||||
|
|
||||||
- Spinners: linear easing, 1000-2000ms/revolution
|
|
||||||
|
|
||||||
| Variant | Speed | Use Case |
|
|
||||||
|---------|-------|----------|
|
|
||||||
| Slow spin | 10-30s/rev | Background decoration |
|
|
||||||
| Gear-like | 3-5s/rev | Technical/mechanical |
|
|
||||||
| Orbital | 5-15s/rev | Space/science theme |
|
|
||||||
| Wobble | 2-4s/cycle, sine | Playful idle |
|
|
||||||
|
|
||||||
## Shimmer / Gleam
|
|
||||||
|
|
||||||
- Gradient sweep left-to-right, 1500-2500ms/sweep
|
|
||||||
- Pause 2000-5000ms between sweeps
|
|
||||||
- Opacity gradient: 0%→30%→0%
|
|
||||||
- Use for: skeleton loading, premium accents, "new" badges
|
|
||||||
|
|
||||||
## Particle Ambient
|
|
||||||
|
|
||||||
### Snow/Falling: 10-20 elements, 20-60px/s down, ±10-20px drift, 3-8px, 30-70% opacity
|
|
||||||
### Dust/Motes: 5-10 elements, 10-30px/s mixed, 2-5px, 20-50% opacity
|
|
||||||
### Sparkle/Stars: 8-15 elements, opacity pulse 0→100%→0, 500-1500ms/sparkle, random stagger
|
|
||||||
|
|
||||||
**Performance**: <20 ambient elements, transform+opacity only, larger/fewer over small/numerous
|
|
||||||
|
|
||||||
## Combining Ambient Layers
|
|
||||||
|
|
||||||
| Layer | Type | Example |
|
|
||||||
|-------|------|---------|
|
|
||||||
| Background | Gradient shift or parallax | Slow color temperature change |
|
|
||||||
| Midground | Floating or particles | Gentle floating elements |
|
|
||||||
| Foreground | Breathing or shimmer | Subtle pulse on content |
|
|
||||||
|
|
||||||
Total ambient: max 20% of primary motion's visual energy.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Entrance & Exit Patterns
|
|
||||||
|
|
||||||
## Entrance Strategies
|
|
||||||
|
|
||||||
### 1. Direct Entrance (Slide In)
|
|
||||||
- Position + opacity; offset 20-40px + opacity 0 → final position + opacity 1
|
|
||||||
- Easing: ease-out; duration 200-350ms
|
|
||||||
|
|
||||||
| Personality | Offset | Easing | Overshoot |
|
|
||||||
|------------|--------|--------|-----------|
|
|
||||||
| Playful | 30-50px | ease-out-back | 10-15% |
|
|
||||||
| Premium | 15-25px | (0.4, 0, 0.2, 1) | 0% |
|
|
||||||
| Corporate | 20-30px | (0.2, 0, 0, 1) | 0-3% |
|
|
||||||
| Energetic | 40-80px | ease-out-expo | 15-25% |
|
|
||||||
|
|
||||||
Direction: below=arrival, right=forward, left=back, above=dropdown/authority.
|
|
||||||
|
|
||||||
### 2. Emergent Entrance (Scale In)
|
|
||||||
- Scale + opacity; start 85-95% + opacity 0 → 100% + opacity 1
|
|
||||||
- Duration 250-400ms
|
|
||||||
|
|
||||||
| Personality | Start Scale | Easing | Overshoot |
|
|
||||||
|------------|------------|--------|-----------|
|
|
||||||
| Playful | 70-80% | ease-out-back | 10-20% |
|
|
||||||
| Premium | 95-98% | (0.4, 0, 0.2, 1) | 0% |
|
|
||||||
| Corporate | 90-95% | (0.2, 0, 0, 1) | 0-3% |
|
|
||||||
| Energetic | 50-70% | ease-out-expo | 15-30% |
|
|
||||||
|
|
||||||
Best for: modals, dialogs, notifications, popovers, tooltips.
|
|
||||||
|
|
||||||
### 3. Reveal Entrance (Clip/Mask)
|
|
||||||
- clip-path or mask + opacity; 300-500ms, ease-out
|
|
||||||
- Directions: top-to-bottom (dramatic), L-to-R (reading order), center-out (focus), edge-in (contained)
|
|
||||||
|
|
||||||
### 4. Assembled Entrance (Multi-Part)
|
|
||||||
- Parts arrive from different positions; stagger 50-100ms; total 300-600ms
|
|
||||||
- Best for: icon assembly, logo builds, data viz construction
|
|
||||||
|
|
||||||
## Exit Strategies
|
|
||||||
|
|
||||||
**Rule**: Exits = 65-75% of entrance duration.
|
|
||||||
|
|
||||||
### 1. Direct Exit (Slide Out)
|
|
||||||
- Offset 20-40px + opacity 0; ease-in; 150-250ms
|
|
||||||
|
|
||||||
### 2. Dissolve Exit (Fade Out)
|
|
||||||
- Opacity (+ optional scale to 98%); ease-in; 150-250ms
|
|
||||||
- Best for: gentle departures, backgrounding, crossfades
|
|
||||||
|
|
||||||
### 3. Collapse Exit (Shrink Out)
|
|
||||||
- Scale 85-95% + opacity 0; ease-in; 150-250ms
|
|
||||||
- Best for: deletion, closing modals, dismissal
|
|
||||||
|
|
||||||
### 4. Transfer Exit (Move Away)
|
|
||||||
- Position toward destination + scale shrink; ease-in-out; 250-400ms
|
|
||||||
- Best for: add-to-cart, save-to-collection, move-to-folder
|
|
||||||
|
|
||||||
## Entrance-Exit Continuity
|
|
||||||
- Eye follows naturally from exit to entrance
|
|
||||||
- Exit point near entry point when possible
|
|
||||||
- 100-150ms timing overlap between exit and entrance
|
|
||||||
- Same easing family for paired entrance-exit
|
|
||||||
|
|
||||||
## Common Recipes
|
|
||||||
|
|
||||||
### Notification Slide-In
|
|
||||||
1. Slide from right + opacity (250ms, ease-out)
|
|
||||||
2. Overshoot 3-5% (corporate) or 10-15% (playful)
|
|
||||||
3. Icon appears (100ms, 50ms delay)
|
|
||||||
|
|
||||||
### Toast Dismiss
|
|
||||||
1. Slide toward edge + opacity (180ms, ease-in)
|
|
||||||
2. Remaining toasts shift up (200ms, ease-in-out)
|
|
||||||
|
|
||||||
### Dropdown Open
|
|
||||||
1. Scale Y 0→100% (200ms, ease-out)
|
|
||||||
2. Items stagger fade in (30ms apart, 50ms after container)
|
|
||||||
|
|
||||||
### Page Transition (Forward)
|
|
||||||
1. Current page slides left + fades (300ms, ease-in)
|
|
||||||
2. New page from right + fades in (400ms, ease-out, 100ms delay)
|
|
||||||
3. Shared elements morph (400ms, ease-in-out)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Multi-Element Patterns
|
|
||||||
|
|
||||||
## Stagger Recipes
|
|
||||||
|
|
||||||
### List Items
|
|
||||||
- Slide up 20px + fade (200ms, ease-out); stagger 40-60ms; total <400ms for 8 items
|
|
||||||
|
|
||||||
| Personality | Duration | Stagger | Easing |
|
|
||||||
|------------|---------|---------|--------|
|
|
||||||
| Playful | 250ms | 60ms | ease-out-back |
|
|
||||||
| Premium | 350ms | 80ms | (0.4, 0, 0.2, 1) |
|
|
||||||
| Corporate | 200ms | 50ms | (0.2, 0, 0, 1) |
|
|
||||||
| Energetic | 150ms | 30ms | ease-out-expo |
|
|
||||||
|
|
||||||
### Grid Cards
|
|
||||||
- Scale from 95% + fade (250ms, ease-out); stagger 50-80ms reading order; +20ms per new row
|
|
||||||
- Shadow 50ms after card. Alt patterns: center-out, column-first, random
|
|
||||||
|
|
||||||
### Navigation Items
|
|
||||||
- Slide from side + fade (180ms, ease-out); stagger 30-50ms; total <300ms
|
|
||||||
|
|
||||||
### Dashboard Widgets
|
|
||||||
```
|
|
||||||
0ms — Skeletons visible
|
|
||||||
100ms — Hero metric (250ms, ease-out)
|
|
||||||
200ms — Widgets begin (200ms each, 60ms stagger)
|
|
||||||
350ms — Hero complete, chart draws (300ms)
|
|
||||||
500ms — All landed
|
|
||||||
650ms — Ambient pulse begins
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coordinated Sequences
|
|
||||||
|
|
||||||
### Modal with Content
|
|
||||||
0ms: backdrop dims (200ms) → 50ms: modal scales 95%→100% (300ms) → 200ms: title → 280ms: body → 350ms: buttons → 400ms: close button. Content in reading order.
|
|
||||||
|
|
||||||
### Tab Switch
|
|
||||||
0ms: indicator slides (250ms, ease-in-out) + old fades (150ms) → 100ms: new content from tab direction (200ms) → 150ms: elements stagger (40ms)
|
|
||||||
|
|
||||||
### Accordion
|
|
||||||
Expand: arrow rotates (150ms) + height expands (250ms) + content fades at 50ms (200ms); siblings shift (200ms). Collapse: reverse, ease-in.
|
|
||||||
|
|
||||||
### Page Transition
|
|
||||||
Current slides left+fades (300ms, ease-in) → new from right (400ms, ease-out, 100ms delay) → hero scales in → content staggers (50ms). Optional shared element morph (400ms).
|
|
||||||
|
|
||||||
### Drag and Drop
|
|
||||||
Drag: lift (scale 1.03, 150ms); others shift (200ms). Drop: settle (scale 1.0, 200ms); gaps close.
|
|
||||||
|
|
||||||
## Choreography Rules
|
|
||||||
|
|
||||||
### Timing Overlap
|
|
||||||
0%=methodical | 25%=brisk (standard) | 50%=fluid | 75%=rapid (energetic)
|
|
||||||
|
|
||||||
### Group Rules
|
|
||||||
- Same easing family; different durations/delays fine
|
|
||||||
- Exception: secondary/ambient can differ
|
|
||||||
|
|
||||||
### Shared Origin
|
|
||||||
Motion from trigger point; closer=first; farther=more delay
|
|
||||||
|
|
||||||
### Counter-Motion
|
|
||||||
|
|
||||||
| Hero Does | Counter-Motion |
|
|
||||||
|-----------|---------------|
|
|
||||||
| Slides right | Background left (20-30%) |
|
|
||||||
| Scales up | Shadow spreads |
|
|
||||||
| Lifts up | Shadow drops+softens |
|
|
||||||
| Rotates CW | Ambient CCW |
|
|
||||||
| Expands | Siblings compress |
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# State & Feedback Patterns
|
|
||||||
|
|
||||||
## Button Press
|
|
||||||
|
|
||||||
### Playful
|
|
||||||
- Press: scale 0.95 (60ms, ease-out); Release: overshoot 1.05 (80ms); Settle: 1.0 (120ms, spring)
|
|
||||||
- Secondary: shadow shrinks/grows; color darkens/brightens. Total ~260ms
|
|
||||||
|
|
||||||
### Premium
|
|
||||||
- Press: scale 0.98 (80ms); Release: 1.0 (150ms, no overshoot)
|
|
||||||
- Opacity dims to 90% on press. Total ~230ms
|
|
||||||
|
|
||||||
### Corporate
|
|
||||||
- Press: scale 0.97 (60ms); Release: 1.0 (100ms); overshoot 0-2%
|
|
||||||
- Background darkens 10%. Total ~160ms
|
|
||||||
|
|
||||||
## Hover States
|
|
||||||
|
|
||||||
| Element | Effect | Duration |
|
|
||||||
|---------|--------|----------|
|
|
||||||
| Button | Scale 1.02-1.05 | <100ms |
|
|
||||||
| Card | Scale 1.01-1.02 + shadow lift | <100ms |
|
|
||||||
| Link | Color change + underline | <100ms |
|
|
||||||
| Icon | Scale 1.1 + rotation 2-5° | <100ms |
|
|
||||||
| Image | Scale 1.03 (overflow hidden) | 150ms |
|
|
||||||
|
|
||||||
Hover enter <100ms; hover exit 150-200ms (slower = polished).
|
|
||||||
|
|
||||||
## Toggle / Switch
|
|
||||||
- Thumb slides (120-180ms, ease-in-out); track color transitions simultaneously
|
|
||||||
- Slight squash in movement direction
|
|
||||||
- Playful: bounce at destination; Premium: smooth, no overshoot
|
|
||||||
|
|
||||||
## Success State
|
|
||||||
|
|
||||||
### Checkmark Success
|
|
||||||
1. Container: scale 0.9→1.0 (200ms, ease-out-back, 5-10% overshoot)
|
|
||||||
2. Checkmark: stroke draw (150ms, ease-out, 100ms delay)
|
|
||||||
3. Color: to green (200ms); ambient glow/particles (300ms)
|
|
||||||
4. Total: 400-500ms
|
|
||||||
|
|
||||||
### Confirmation Badge
|
|
||||||
1. Badge scales from 0 (200ms, ease-out-back)
|
|
||||||
2. Text fades in (150ms, 50ms delay)
|
|
||||||
3. Background pulse (300ms, sine)
|
|
||||||
|
|
||||||
### Payment Success
|
|
||||||
1. Spinner → checkmark crossfade (200ms)
|
|
||||||
2. Checkmark draws (200ms); container → success color (200ms)
|
|
||||||
3. Text fades in (200ms, 100ms delay); optional confetti (300-500ms)
|
|
||||||
|
|
||||||
## Error State
|
|
||||||
|
|
||||||
### Error Shake
|
|
||||||
- Horizontal oscillation ±10-15px, 2-3 cycles decreasing amplitude
|
|
||||||
- ease-in-out, 300-400ms total; red tint; no overshoot; settles at origin
|
|
||||||
|
|
||||||
### Inline Validation
|
|
||||||
1. Error text slides down + fades in (200ms)
|
|
||||||
2. Border → red (150ms); icon scales in (150ms, 50ms delay)
|
|
||||||
3. Optional single shake (200ms)
|
|
||||||
|
|
||||||
### Form Submission Error
|
|
||||||
1. Button returns to normal (200ms)
|
|
||||||
2. Error message slides in (250ms)
|
|
||||||
3. Affected fields highlight red (150ms, staggered 30ms)
|
|
||||||
4. Smooth scroll to first error (300ms, ease-in-out)
|
|
||||||
|
|
||||||
## Loading States
|
|
||||||
|
|
||||||
### Spinner
|
|
||||||
- Continuous 360°, linear, 1000-1500ms/rev; optional breathing pulse (2-3s)
|
|
||||||
|
|
||||||
### Skeleton
|
|
||||||
- Gradient sweep L→R, 1500-2000ms; base 10-20% opacity, peak 30-40%
|
|
||||||
|
|
||||||
### Progress Bar
|
|
||||||
- Width/transform, ease-in-out; optional color milestones + shimmer
|
|
||||||
|
|
||||||
### Indeterminate
|
|
||||||
- Oscillating position/width, 1500-2500ms, ease-in-out; continuous, never frantic
|
|
||||||
|
|
||||||
## Warning State
|
|
||||||
1. Yellow/amber border (150ms)
|
|
||||||
2. Warning icon scales in (150ms, subtle overshoot)
|
|
||||||
3. Optional icon pulse (2-3s, sine); text fades in (200ms, 50ms delay)
|
|
||||||
|
|
||||||
## Disabled / Enabled
|
|
||||||
|
|
||||||
- **Disabling**: opacity to 50-60% (200ms); optional scale to 98%
|
|
||||||
- **Enabling**: opacity to 100% (200ms); optional scale pulse 98%→100%
|
|
||||||
|
|
||||||
## Focus States
|
|
||||||
- Focus ring: scale 95%→100% + opacity (150ms)
|
|
||||||
- Card focus: scale 1.01, shadow increase (150ms)
|
|
||||||
- Tab nav focus: <100ms; must work with reduced-motion
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Property Selection
|
|
||||||
|
|
||||||
## Position
|
|
||||||
|
|
||||||
| Direction | Meaning |
|
|
||||||
|-----------|---------|
|
|
||||||
| Upward | Growth, improvement, aspiration |
|
|
||||||
| Downward | Settling, completion, grounding |
|
|
||||||
| Leftward | Regression, backward, departure |
|
|
||||||
| Rightward | Progression, forward, arrival |
|
|
||||||
| Toward center | Focus, convergence |
|
|
||||||
| Away from center | Distribution, expansion |
|
|
||||||
|
|
||||||
## Scale
|
|
||||||
|
|
||||||
| Direction | Meaning |
|
|
||||||
|-----------|---------|
|
|
||||||
| Scale up | Importance, activation, proximity |
|
|
||||||
| Scale down | Deprioritization, distance |
|
|
||||||
| Pulse | Attention, heartbeat, life |
|
|
||||||
| Breathing | Presence, waiting |
|
|
||||||
|
|
||||||
## Rotation
|
|
||||||
|
|
||||||
| Range | Meaning |
|
|
||||||
|-------|---------|
|
|
||||||
| Small (5-15°) | Subtle adjustment |
|
|
||||||
| Medium (45-90°) | Transformation |
|
|
||||||
| Full (360°) | Completion, processing |
|
|
||||||
| Continuous | Ongoing activity |
|
|
||||||
|
|
||||||
## Opacity
|
|
||||||
|
|
||||||
| Direction | Meaning |
|
|
||||||
|-----------|---------|
|
|
||||||
| Fade in | Arrival, enablement |
|
|
||||||
| Fade out | Departure, disablement |
|
|
||||||
| Partial | Secondary, disabled state |
|
|
||||||
|
|
||||||
**Rule**: NEVER opacity alone for important state changes. Combine with position or scale.
|
|
||||||
|
|
||||||
## Color
|
|
||||||
|
|
||||||
| Transition | Meaning |
|
|
||||||
|-----------|---------|
|
|
||||||
| To green | Success, go |
|
|
||||||
| To red | Error, stop |
|
|
||||||
| To blue | Trust, active |
|
|
||||||
| To gray | Disabled, inactive |
|
|
||||||
| Brightening | Activation, focus |
|
|
||||||
| Dimming | Deactivation, background |
|
|
||||||
|
|
||||||
## Combined Properties
|
|
||||||
|
|
||||||
| Combination | Best For |
|
|
||||||
|-------------|----------|
|
|
||||||
| Position + Opacity | Content appearing/disappearing |
|
|
||||||
| Scale + Opacity | Cards, modals, notifications |
|
|
||||||
| Position + Scale | Selected items, focused content |
|
|
||||||
| Rotation + Scale | Celebrations, playful activation |
|
|
||||||
| Position + Rotation | Organic transitions |
|
|
||||||
| Color + Scale | State emphasis |
|
|
||||||
|
|
||||||
Primary property carries meaning; secondary adds polish. Two properties is the sweet spot.
|
|
||||||
|
|
||||||
## Property Selection by Goal
|
|
||||||
|
|
||||||
| Goal | Primary | Secondary | Avoid |
|
|
||||||
|------|---------|-----------|-------|
|
|
||||||
| Entrance | position | opacity | rotation |
|
|
||||||
| Exit | position | opacity | scale up |
|
|
||||||
| Button press | scale | color | position |
|
|
||||||
| Hover | scale or color | opacity | position |
|
|
||||||
| Success | scale | color + opacity | position |
|
|
||||||
| Error | position (shake) | color | scale |
|
|
||||||
| Loading | rotation | opacity | position |
|
|
||||||
| Toggle | position | color | rotation |
|
|
||||||
| Notification | position + scale | opacity | rotation |
|
|
||||||
| Delete | scale + opacity | position | grow |
|
|
||||||
| Selection | scale | color, opacity | rotation |
|
|
||||||
| Progress | position or scale | color | opacity |
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
| Property | Performance |
|
|
||||||
|----------|-------------|
|
|
||||||
| transform (translate, scale, rotate) | Excellent — GPU-accelerated |
|
|
||||||
| opacity | Excellent — GPU-accelerated |
|
|
||||||
| color / background-color | Good — triggers paint |
|
|
||||||
| clip-path | Good — GPU on modern browsers |
|
|
||||||
| width / height | Poor — triggers layout |
|
|
||||||
| margin / padding | Poor — triggers layout |
|
|
||||||
| box-shadow | Poor — expensive paint |
|
|
||||||
|
|
||||||
**Rule**: Prefer transform + opacity for all motion.
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# Quality Checklist
|
|
||||||
|
|
||||||
## Visual Quality
|
|
||||||
- [ ] Elements >40px for motion, >100px for detail
|
|
||||||
- [ ] Readable at full speed without slow-motion
|
|
||||||
- [ ] Clear primary, secondary, ambient layers
|
|
||||||
- [ ] Counter-motion for balance where needed
|
|
||||||
- [ ] Natural arcs (unless intentionally mechanical)
|
|
||||||
- [ ] 1/3 rule (distance): no unbroken motion >1/3 container
|
|
||||||
- [ ] 1/3 rule (density): max 1/3 elements active simultaneously
|
|
||||||
|
|
||||||
## Technical Quality
|
|
||||||
- [ ] No linear easing on spatial movement
|
|
||||||
- [ ] Duration matches element type table
|
|
||||||
- [ ] Ease-out entrances, ease-in exits
|
|
||||||
- [ ] Duration proportional to distance
|
|
||||||
- [ ] Entrance duration >= exit duration
|
|
||||||
- [ ] Not opacity-only for important state changes
|
|
||||||
- [ ] Stagger total <500ms
|
|
||||||
- [ ] Follow-through: child elements offset 50-150ms
|
|
||||||
|
|
||||||
## Emotional Quality
|
|
||||||
- [ ] Target emotion identified before properties
|
|
||||||
- [ ] Personality archetype matches brand
|
|
||||||
- [ ] Setup → action → resolution structure
|
|
||||||
- [ ] Intensity matches interaction importance
|
|
||||||
- [ ] Consistent: same interaction = same motion
|
|
||||||
- [ ] Appropriate on 100th viewing
|
|
||||||
|
|
||||||
## Performance Quality
|
|
||||||
- [ ] Primary motion uses transform + opacity
|
|
||||||
- [ ] <20 animated elements per viewport
|
|
||||||
- [ ] No layout-triggering properties animated
|
|
||||||
- [ ] Elements staggered, not simultaneous
|
|
||||||
- [ ] Maintains 60fps (30fps acceptable for ambient)
|
|
||||||
|
|
||||||
## Accessibility Quality
|
|
||||||
- [ ] prefers-reduced-motion alternative provided
|
|
||||||
- [ ] No vestibular triggers without alternative
|
|
||||||
- [ ] Same interaction = same animation
|
|
||||||
- [ ] Critical info not motion-only
|
|
||||||
- [ ] Animations >5s are pausable
|
|
||||||
|
|
||||||
## Severity Tiers
|
|
||||||
|
|
||||||
### CRITICAL
|
|
||||||
- Linear easing on spatial movement
|
|
||||||
- Opacity-only for important states
|
|
||||||
- Exceeds 1/3 screen rule
|
|
||||||
- Missing primary layer
|
|
||||||
- Stagger >500ms
|
|
||||||
- Layout property animation causing jank
|
|
||||||
|
|
||||||
### HIGH
|
|
||||||
- Missing secondary layer
|
|
||||||
- Duration mismatch with element type
|
|
||||||
- Wrong directional easing
|
|
||||||
- Inconsistent personality
|
|
||||||
- No follow-through
|
|
||||||
- Missing reduced-motion alternative
|
|
||||||
|
|
||||||
### MEDIUM
|
|
||||||
- Missing ambient layer
|
|
||||||
- No anticipation phase
|
|
||||||
- Overshoot mismatch
|
|
||||||
- Could use better arcs
|
|
||||||
- Missing counter-motion
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Timing & Easing Tables
|
|
||||||
|
|
||||||
## Duration by Element Type
|
|
||||||
|
|
||||||
| Element | Duration |
|
|
||||||
|---------|----------|
|
|
||||||
| Tooltip / micro-feedback | 80-120ms |
|
|
||||||
| Button / toggle | 120-180ms |
|
|
||||||
| Icon transition | 150-250ms |
|
|
||||||
| Card enter/exit | 200-350ms |
|
|
||||||
| Modal / dialog | 300-400ms |
|
|
||||||
| Page transition | 400-600ms |
|
|
||||||
| Dramatic reveal | 600-1200ms |
|
|
||||||
| Ambient | 2000-20000ms |
|
|
||||||
|
|
||||||
## Distance-Duration Scaling
|
|
||||||
|
|
||||||
50px=0.8x | 100px=1.0x | 200px=1.3x | 300px=1.5x | 400px=1.6x | Full screen=1.8-2.0x
|
|
||||||
|
|
||||||
## Enter vs. Exit
|
|
||||||
Entrance = base (100%). Exit = 65-75% of entrance.
|
|
||||||
|
|
||||||
## Interactive Feedback
|
|
||||||
|
|
||||||
| Interaction | Max Latency |
|
|
||||||
|------------|-------------|
|
|
||||||
| Hover | <100ms |
|
|
||||||
| Press/tap | <150ms |
|
|
||||||
| Release/settle | 200-300ms |
|
|
||||||
| Error shake | 300-400ms |
|
|
||||||
| Long press | 500-800ms |
|
|
||||||
| Drag start | <50ms |
|
|
||||||
|
|
||||||
## Duration by Personality
|
|
||||||
|
|
||||||
| Personality | Quick | Standard | Slow |
|
|
||||||
|------------|-------|----------|------|
|
|
||||||
| Playful | 150ms | 250ms | 400ms |
|
|
||||||
| Premium | 350ms | 500ms | 800ms |
|
|
||||||
| Corporate | 200ms | 300ms | 450ms |
|
|
||||||
| Energetic | 100ms | 180ms | 300ms |
|
|
||||||
|
|
||||||
## Easing: Directional Rules
|
|
||||||
|
|
||||||
Entrance=ease-out | Exit=ease-in | On-screen=ease-in-out | Looping=sine | Rotation/progress=linear
|
|
||||||
|
|
||||||
## Easing: Industry Standards
|
|
||||||
|
|
||||||
| Name | Cubic Bezier | Use |
|
|
||||||
|------|-------------|-----|
|
|
||||||
| MD3 Standard | (0.2, 0, 0, 1) | Default on-screen |
|
|
||||||
| MD3 Emphasized | (0.05, 0.7, 0.1, 1) | Entrances |
|
|
||||||
| MD3 Accelerate | (0.3, 0, 1, 1) | Exits |
|
|
||||||
| MD3 Decelerate | (0, 0, 0, 1) | Entering |
|
|
||||||
| Apple HIG | (0.25, 0.1, 0.25, 1) | iOS default |
|
|
||||||
| Apple Spring | stiffness:300 damping:20 | Interactive |
|
|
||||||
| Snappy UI | (0.2, 0, 0, 1) | Fast, decisive |
|
|
||||||
| Gentle float | (0.4, 0, 0.2, 1) | Ambient |
|
|
||||||
| Bounce settle | (0.175, 0.885, 0.32, 1.275) | Playful |
|
|
||||||
| Elastic snap | (0.68, -0.55, 0.265, 1.55) | Dramatic |
|
|
||||||
|
|
||||||
## Material-Based Easing
|
|
||||||
|
|
||||||
| Material | Duration Scale | Overshoot |
|
|
||||||
|----------|---------------|-----------|
|
|
||||||
| Rigid (metal) | 1.2x | 0% |
|
|
||||||
| Elastic (rubber) | 0.8x | 15-25% |
|
|
||||||
| Fluid (water) | 1.5x | 5% |
|
|
||||||
| Paper (cards) | 1.0x | 3-5% |
|
|
||||||
| Gas (smoke) | 2.0x | 0% |
|
|
||||||
| Glass | 0.9x | 0% |
|
|
||||||
|
|
||||||
## Spring Parameters
|
|
||||||
|
|
||||||
| Feel | Stiffness | Damping |
|
|
||||||
|------|-----------|---------|
|
|
||||||
| Very stiff | 400+ | 25-30 |
|
|
||||||
| Standard | 250-350 | 18-24 |
|
|
||||||
| Bouncy | 150-250 | 10-15 |
|
|
||||||
| Very bouncy | 100-200 | 5-10 |
|
|
||||||
| Gentle | 100-150 | 20-25 |
|
|
||||||
|
|
||||||
Damping: <1.0=oscillates, 1.0=fastest no-oscillation, >1.0=slow settle.
|
|
||||||
|
|
||||||
## Stagger Patterns
|
|
||||||
|
|
||||||
| Pattern | Delay | Budget |
|
|
||||||
|---------|------|--------|
|
|
||||||
| Micro cascade | 20-40ms | <200ms |
|
|
||||||
| Standard | 50-100ms | <400ms |
|
|
||||||
| Dramatic | 100-200ms | <600ms |
|
|
||||||
| Wave | 30-60ms | <500ms |
|
|
||||||
|
|
||||||
Direction: top-to-bottom (lists) | L-to-R (horizontal) | center-out (hero) | random (organic) | reverse (exits)
|
|
||||||
|
|
||||||
Total stagger MUST stay <500ms.
|
|
||||||
|
|
||||||
## Overshoot Budget
|
|
||||||
|
|
||||||
| Context | Overshoot |
|
|
||||||
|---------|-----------|
|
|
||||||
| Success | 5-10% |
|
|
||||||
| Error | 0% |
|
|
||||||
| Feedback | 2-5% |
|
|
||||||
| Celebration | 15-25% |
|
|
||||||
| Premium | 0% |
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Troubleshooting
|
|
||||||
|
|
||||||
## Looks Robotic
|
|
||||||
- Linear easing → use ease-out (entrance) or ease-in-out
|
|
||||||
- Straight paths → add 10-20px arc at midpoint
|
|
||||||
- Uniform timing → stagger 50-100ms between elements
|
|
||||||
- Everything synced → offset start/stop 50-150ms
|
|
||||||
- No secondary → add shadow, icon reaction, ambient
|
|
||||||
|
|
||||||
## Feels Too Slow
|
|
||||||
- Duration exceeds type budget → check duration table
|
|
||||||
- ease-in-out when ease-out works → ease-out feels faster
|
|
||||||
- Too much anticipation → reduce to 10% or remove
|
|
||||||
- Stagger exceeded → total <500ms
|
|
||||||
- Overshoot settle too long → increase damping
|
|
||||||
|
|
||||||
## Feels Too Fast / Jarring
|
|
||||||
- Duration below minimum → modals 300ms+, pages 400ms+
|
|
||||||
- No easing → add ease-out minimum
|
|
||||||
- Missing resolution → add 50-100ms settle
|
|
||||||
- No anticipation on large motion → add 100-200ms wind-up
|
|
||||||
|
|
||||||
## Feels Cheap / Flat
|
|
||||||
- Only primary motion → add secondary + ambient
|
|
||||||
- Opacity-only → combine with position or scale
|
|
||||||
- Same easing everywhere → vary primary vs secondary
|
|
||||||
- No follow-through → child elements trail 50-150ms
|
|
||||||
- No overshoot → add 3-10%
|
|
||||||
|
|
||||||
## Too Distracting
|
|
||||||
- Too many moving → 1/3 rule
|
|
||||||
- Amplitude too large → reduce to minimum
|
|
||||||
- Competing heroes → one per moment, dim rest
|
|
||||||
- Ambient too prominent → 10-20% amplitude, slower
|
|
||||||
- No breathing room → 100-200ms pause between beats
|
|
||||||
|
|
||||||
## No Personality
|
|
||||||
- Default easing → apply archetype's signature
|
|
||||||
- Same duration for all → use personality palette
|
|
||||||
- No consistent entrance → define one for project
|
|
||||||
- Mixed archetypes → pick one for 90%+
|
|
||||||
|
|
||||||
## Inconsistent Feel
|
|
||||||
- Different easing same-type → standardize per motion type
|
|
||||||
- Duration varies same type → use palette consistently
|
|
||||||
- Entry direction changes → one origin everywhere
|
|
||||||
- Overshoot inconsistent → apply rules consistently
|
|
||||||
|
|
||||||
## Performance (Dropped Frames)
|
|
||||||
- Animating width/height/margin → use transform
|
|
||||||
- Too many elements → <20 per viewport
|
|
||||||
- Complex shadows/filters → simplify or pre-render
|
|
||||||
- No GPU acceleration → transform + opacity
|
|
||||||
- All simultaneous → stagger to spread load
|
|
||||||
|
|
||||||
## Quick Diagnostic
|
|
||||||
|
|
||||||
1. No linear on spatial movement
|
|
||||||
2. Duration matches element type
|
|
||||||
3. Primary + secondary layers
|
|
||||||
4. Consistent personality
|
|
||||||
5. Directional easing correct
|
|
||||||
6. 1/3 screen rule
|
|
||||||
7. 1/3 element rule
|
|
||||||
8. Follow-through present
|
|
||||||
9. Every motion has purpose
|
|
||||||
10. Fine on 100th viewing
|
|
||||||
|
|
||||||
## Personality Mistakes
|
|
||||||
- **Playful**: overshoot >25% = broken; not everything bounces; short+bounce = glitchy
|
|
||||||
- **Premium**: too subtle = invisible; too slow = waiting; zero = broken
|
|
||||||
- **Corporate**: too conservative = boring; playful easing breaks trust; identical = monotonous
|
|
||||||
- **Energetic**: max everywhere = nothing stands out; too many particles; no settle = chaos
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../.agents/skills/motion-design
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* CycleWise AU - scroll animations (Step 1).
|
||||||
|
Purely cosmetic: if GSAP fails to load or the visitor prefers reduced
|
||||||
|
motion, this file does nothing and the page looks exactly as before. */
|
||||||
|
(function(){
|
||||||
|
"use strict";
|
||||||
|
if (!window.gsap || !window.ScrollTrigger) return;
|
||||||
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
|
||||||
|
var done = new WeakSet();
|
||||||
|
|
||||||
|
function reveal(panel){
|
||||||
|
if (!panel || done.has(panel)) return;
|
||||||
|
done.add(panel);
|
||||||
|
|
||||||
|
// Top of the tab: hero text and picture fade up one after another.
|
||||||
|
gsap.from(panel.querySelectorAll(".hero > *"), {
|
||||||
|
opacity: 0, y: 24, duration: 0.7, stagger: 0.12, ease: "power2.out",
|
||||||
|
clearProps: "all"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Everything below: fade up when it scrolls into view.
|
||||||
|
panel.querySelectorAll(
|
||||||
|
".ref-heading, .ref-grid, .timeline, .week-panel, .filmstrip, .cal-card, .detail-card"
|
||||||
|
).forEach(function(el){
|
||||||
|
gsap.from(el, {
|
||||||
|
opacity: 0, y: 32, duration: 0.7, ease: "power2.out",
|
||||||
|
clearProps: "all",
|
||||||
|
scrollTrigger: { trigger: el, start: "top 88%", once: true }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activePanel(){ return document.querySelector(".mode-panel.active"); }
|
||||||
|
|
||||||
|
reveal(activePanel());
|
||||||
|
|
||||||
|
document.querySelectorAll(".mode-btn").forEach(function(btn){
|
||||||
|
btn.addEventListener("click", function(){
|
||||||
|
// Wait one tick so the page's own code switches the tab first.
|
||||||
|
setTimeout(function(){ reveal(activePanel()); ScrollTrigger.refresh(); }, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,539 @@
|
|||||||
|
(function(){
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/* ---------------- date helpers ---------------- */
|
||||||
|
function toDate(y,m,d){ return new Date(y,m,d); }
|
||||||
|
function fromInput(str){
|
||||||
|
if(!str) return null;
|
||||||
|
var p = str.split("-");
|
||||||
|
return new Date(+p[0], +p[1]-1, +p[2]);
|
||||||
|
}
|
||||||
|
function toInputVal(d){
|
||||||
|
return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");
|
||||||
|
}
|
||||||
|
function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; }
|
||||||
|
function daysBetween(a,b){ // b - a, in whole days, ignoring time
|
||||||
|
var A = new Date(a.getFullYear(),a.getMonth(),a.getDate());
|
||||||
|
var B = new Date(b.getFullYear(),b.getMonth(),b.getDate());
|
||||||
|
return Math.round((B-A)/86400000);
|
||||||
|
}
|
||||||
|
function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
|
||||||
|
function mod(n,m){ return ((n % m) + m) % m; }
|
||||||
|
var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
||||||
|
var today = new Date(); today.setHours(0,0,0,0);
|
||||||
|
|
||||||
|
function loadLS(key, fallback){
|
||||||
|
try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; }
|
||||||
|
}
|
||||||
|
function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} }
|
||||||
|
|
||||||
|
/* ---------------- mode switching ---------------- */
|
||||||
|
var modeBtns = document.querySelectorAll(".mode-btn");
|
||||||
|
var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") };
|
||||||
|
modeBtns.forEach(function(btn){
|
||||||
|
btn.addEventListener("click", function(){
|
||||||
|
modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); });
|
||||||
|
btn.classList.add("active"); btn.setAttribute("aria-selected","true");
|
||||||
|
Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); });
|
||||||
|
panels[btn.dataset.mode].classList.add("active");
|
||||||
|
saveLS("ca-mode", btn.dataset.mode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var savedMode = loadLS("ca-mode", null);
|
||||||
|
if(savedMode && panels[savedMode]){
|
||||||
|
modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); });
|
||||||
|
Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
PHASE + OVULATION REFERENCE DATA
|
||||||
|
================================================================ */
|
||||||
|
var PHASES = {
|
||||||
|
menstrual: {
|
||||||
|
title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)",
|
||||||
|
window:"Cycle day 1 through the end of bleeding",
|
||||||
|
desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.",
|
||||||
|
hormone:"Estrogen & progesterone: low", energy:"Energy: lowest",
|
||||||
|
symptoms:"Cramping, fatigue, lower back ache, mood dips",
|
||||||
|
tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone."
|
||||||
|
},
|
||||||
|
follicular: {
|
||||||
|
title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)",
|
||||||
|
window:"Cycle day 1 through ovulation (overlaps menstrual)",
|
||||||
|
desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.",
|
||||||
|
hormone:"Estrogen: rising", energy:"Energy: climbing",
|
||||||
|
symptoms:"Clearer skin, sharper focus, rising motivation",
|
||||||
|
tip:"This is the physiologically best window for starting new training blocks or demanding projects."
|
||||||
|
},
|
||||||
|
ovulatory: {
|
||||||
|
title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)",
|
||||||
|
window:"Roughly one day, mid-cycle",
|
||||||
|
desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.",
|
||||||
|
hormone:"LH: sharp surge", energy:"Energy & libido: peak",
|
||||||
|
symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)",
|
||||||
|
tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released."
|
||||||
|
},
|
||||||
|
luteal: {
|
||||||
|
title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)",
|
||||||
|
window:"Ovulation through the day before your next period",
|
||||||
|
desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.",
|
||||||
|
hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining",
|
||||||
|
symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)",
|
||||||
|
tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// offset relative to ovulation day (0 = ovulation)
|
||||||
|
var OVULATION_DAYS = {
|
||||||
|
"-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."},
|
||||||
|
"-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."},
|
||||||
|
"-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."},
|
||||||
|
"-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."},
|
||||||
|
"-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."},
|
||||||
|
"0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."},
|
||||||
|
"1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."},
|
||||||
|
"2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."},
|
||||||
|
"3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."},
|
||||||
|
"4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."},
|
||||||
|
"5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."}
|
||||||
|
};
|
||||||
|
var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"};
|
||||||
|
|
||||||
|
function phaseForDayInCycle(dic, periodLen, ovulationDic){
|
||||||
|
if(dic < periodLen) return "menstrual";
|
||||||
|
if(dic < ovulationDic) return "follicular";
|
||||||
|
if(dic === ovulationDic) return "ovulatory";
|
||||||
|
return "luteal";
|
||||||
|
}
|
||||||
|
function isFertile(dic, ovulationDic){
|
||||||
|
return dic >= ovulationDic - 5 && dic <= ovulationDic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
PREGNANCY WEEK DATA (1–40)
|
||||||
|
================================================================ */
|
||||||
|
var WEEKS = [
|
||||||
|
{c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."},
|
||||||
|
{c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."},
|
||||||
|
{c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."},
|
||||||
|
{c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."},
|
||||||
|
{c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."},
|
||||||
|
{c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."},
|
||||||
|
{c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."},
|
||||||
|
{c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."},
|
||||||
|
{c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."},
|
||||||
|
{c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."},
|
||||||
|
{c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."},
|
||||||
|
{c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."},
|
||||||
|
{c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."},
|
||||||
|
{c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."},
|
||||||
|
{c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."},
|
||||||
|
{c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."},
|
||||||
|
{c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."},
|
||||||
|
{c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."},
|
||||||
|
{c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."},
|
||||||
|
{c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."},
|
||||||
|
{c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."},
|
||||||
|
{c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."},
|
||||||
|
{c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."},
|
||||||
|
{c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."},
|
||||||
|
{c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."},
|
||||||
|
{c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."},
|
||||||
|
{c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."},
|
||||||
|
{c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."},
|
||||||
|
{c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."},
|
||||||
|
{c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."},
|
||||||
|
{c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."},
|
||||||
|
{c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."},
|
||||||
|
{c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."},
|
||||||
|
{c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."},
|
||||||
|
{c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."},
|
||||||
|
{c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."},
|
||||||
|
{c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."},
|
||||||
|
{c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."},
|
||||||
|
{c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."},
|
||||||
|
{c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."}
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
SVG helpers
|
||||||
|
================================================================ */
|
||||||
|
function polarToXY(cx,cy,r,angleDeg){
|
||||||
|
var rad = (angleDeg-90) * Math.PI/180;
|
||||||
|
return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)];
|
||||||
|
}
|
||||||
|
function arcPath(cx,cy,r,startDeg,endDeg){
|
||||||
|
if(endDeg <= startDeg) endDeg = startDeg + 0.01;
|
||||||
|
var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg);
|
||||||
|
var large = (endDeg-startDeg) > 180 ? 1 : 0;
|
||||||
|
return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
SHARED STATE: period start / cycle length / period length
|
||||||
|
================================================================ */
|
||||||
|
var exampleStart = addDays(today, -10);
|
||||||
|
var startInput = document.getElementById("p-start");
|
||||||
|
var cycleLenInput = document.getElementById("p-cyclelen");
|
||||||
|
var periodLenInput = document.getElementById("p-periodlen");
|
||||||
|
|
||||||
|
var savedStart = loadLS("ca-start", null);
|
||||||
|
var savedCycleLen = loadLS("ca-cyclelen", null);
|
||||||
|
var savedPeriodLen = loadLS("ca-periodlen", null);
|
||||||
|
var usingExample = !savedStart;
|
||||||
|
|
||||||
|
startInput.value = savedStart || toInputVal(exampleStart);
|
||||||
|
cycleLenInput.value = savedCycleLen || 28;
|
||||||
|
periodLenInput.value = savedPeriodLen || 5;
|
||||||
|
|
||||||
|
function getCycleState(){
|
||||||
|
return {
|
||||||
|
start: fromInput(startInput.value) || exampleStart,
|
||||||
|
cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)),
|
||||||
|
periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
var pSelectedDate = new Date(today);
|
||||||
|
var oSelectedDate = new Date(today);
|
||||||
|
|
||||||
|
[startInput,cycleLenInput,periodLenInput].forEach(function(inp){
|
||||||
|
inp.addEventListener("input", function(){
|
||||||
|
saveLS("ca-start", startInput.value);
|
||||||
|
saveLS("ca-cyclelen", cycleLenInput.value);
|
||||||
|
saveLS("ca-periodlen", periodLenInput.value);
|
||||||
|
usingExample = false;
|
||||||
|
document.getElementById("p-example-note").style.display = "none";
|
||||||
|
document.getElementById("pr-example-note").style.display = "none";
|
||||||
|
renderAll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if(!usingExample) document.getElementById("p-example-note").style.display = "none";
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
CALENDAR RENDERING (shared builder)
|
||||||
|
================================================================ */
|
||||||
|
function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){
|
||||||
|
gridEl.innerHTML = "";
|
||||||
|
var year = monthDate.getFullYear(), month = monthDate.getMonth();
|
||||||
|
var firstDow = new Date(year,month,1).getDay();
|
||||||
|
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||||
|
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||||
|
for(var i=0;i<totalCells;i++){
|
||||||
|
var dayNum = i - firstDow + 1;
|
||||||
|
var cell = document.createElement("button");
|
||||||
|
cell.className = "day-cell";
|
||||||
|
if(dayNum < 1 || dayNum > daysInMonth){
|
||||||
|
cell.classList.add("pad");
|
||||||
|
gridEl.appendChild(cell);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var d = new Date(year,month,dayNum);
|
||||||
|
var info = dayClassifier(d);
|
||||||
|
cell.innerHTML = "<span>"+dayNum+"</span>";
|
||||||
|
if(info.period) cell.classList.add("is-period");
|
||||||
|
if(info.fertile) cell.classList.add("is-fertile");
|
||||||
|
if(info.ovulation) cell.classList.add("is-ovulation");
|
||||||
|
if(sameDay(d,today)) cell.classList.add("today");
|
||||||
|
if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected");
|
||||||
|
cell.setAttribute("aria-label", d.toDateString());
|
||||||
|
cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d));
|
||||||
|
gridEl.appendChild(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyForCycle(d, state){
|
||||||
|
var dic = mod(daysBetween(state.start, d), state.cycleLen);
|
||||||
|
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||||
|
return {
|
||||||
|
period: dic < state.periodLen,
|
||||||
|
fertile: isFertile(dic, ovDic),
|
||||||
|
ovulation: dic === ovDic,
|
||||||
|
dic: dic, ovDic: ovDic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- PERIOD tab render ---------------- */
|
||||||
|
function renderPeriodCalendar(){
|
||||||
|
var state = getCycleState();
|
||||||
|
document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear();
|
||||||
|
buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth,
|
||||||
|
function(d){ return classifyForCycle(d, state); },
|
||||||
|
function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); },
|
||||||
|
pSelectedDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPeriodDetail(){
|
||||||
|
var state = getCycleState();
|
||||||
|
var info = classifyForCycle(pSelectedDate, state);
|
||||||
|
var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic);
|
||||||
|
var phase = PHASES[phaseKey];
|
||||||
|
var el = document.getElementById("p-detail");
|
||||||
|
el.innerHTML =
|
||||||
|
'<span class="d-date">'+pSelectedDate.toDateString()+'</span>'+
|
||||||
|
'<span class="pill" style="background:'+phase.soft+';color:'+phase.color+'">'+phase.title+' phase'+(info.fertile?' · fertile window':'')+'</span>'+
|
||||||
|
'<div class="d-phase">Cycle day '+(info.dic+1)+'</div>'+
|
||||||
|
'<p class="d-desc">'+phase.desc+'</p>'+
|
||||||
|
'<div class="d-grid">'+
|
||||||
|
'<div class="d-mini"><div class="k">Hormones</div><div class="v">'+phase.hormone+'</div></div>'+
|
||||||
|
'<div class="d-mini"><div class="k">Typical symptoms</div><div class="v" style="font-weight:500;font-size:0.82rem;">'+phase.symptoms+'</div></div>'+
|
||||||
|
'</div>'+
|
||||||
|
'<p class="d-desc"><strong style="color:var(--ink)">Care note — </strong>'+phase.tip+'</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPeriodStats(){
|
||||||
|
var state = getCycleState();
|
||||||
|
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||||
|
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||||
|
var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen);
|
||||||
|
if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen;
|
||||||
|
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||||
|
var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic);
|
||||||
|
document.getElementById("period-stats").innerHTML =
|
||||||
|
statTile(dic+1, "Cycle day today") +
|
||||||
|
statTile(PHASES[phaseKey].title, "Current phase") +
|
||||||
|
statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") +
|
||||||
|
statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation");
|
||||||
|
|
||||||
|
// hero ring
|
||||||
|
var cx=100, cy=100, r=88;
|
||||||
|
var perStart=0, perEnd = state.periodLen/state.cycleLen*360;
|
||||||
|
var follEnd = ovDic/state.cycleLen*360;
|
||||||
|
var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360;
|
||||||
|
var lutStart = (ovDic+1)/state.cycleLen*360;
|
||||||
|
document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd));
|
||||||
|
document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart));
|
||||||
|
document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen)));
|
||||||
|
document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360));
|
||||||
|
var todayAngle = dic/state.cycleLen*360;
|
||||||
|
var tp = polarToXY(cx,cy,r,todayAngle);
|
||||||
|
document.getElementById("today-marker").setAttribute("cx", tp[0]);
|
||||||
|
document.getElementById("today-marker").setAttribute("cy", tp[1]);
|
||||||
|
document.getElementById("cycle-day-big").textContent = dic+1;
|
||||||
|
// hand the same numbers to the optional 3D ring (ring3d.js)
|
||||||
|
window.cycleRingData = {perEnd:perEnd, fertStart:fertStart, fertEnd:follEnd+(360/state.cycleLen),
|
||||||
|
lutStart:lutStart, todayAngle:todayAngle};
|
||||||
|
window.dispatchEvent(new CustomEvent("cycle:ring", {detail: window.cycleRingData}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function statTile(num,label){
|
||||||
|
return '<div class="stat-tile"><div class="num">'+num+'</div><div class="lbl">'+label+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); });
|
||||||
|
document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); });
|
||||||
|
|
||||||
|
function renderPhaseRefGrid(){
|
||||||
|
var order = ["menstrual","follicular","ovulatory","luteal"];
|
||||||
|
document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){
|
||||||
|
var p = PHASES[key];
|
||||||
|
return '<div class="ref-card"><span class="r-num">0'+(i+1)+'</span><h4 style="color:'+p.color+'">'+p.title+'</h4>'+
|
||||||
|
'<p>'+p.desc+'</p>'+
|
||||||
|
'<div class="tag-row"><span class="tagchip">'+p.window+'</span></div></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- OVULATION tab render ---------------- */
|
||||||
|
var ovSelectedOffset = 0;
|
||||||
|
function renderOvTimeline(){
|
||||||
|
var wrap = document.getElementById("ov-timeline");
|
||||||
|
wrap.innerHTML = "";
|
||||||
|
for(var off=-5; off<=5; off++){
|
||||||
|
var d = OVULATION_DAYS[String(off)];
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":"");
|
||||||
|
btn.innerHTML = '<div class="tl-day">'+(off===0?"OVUL.":(off>0?"+":"")+off)+'</div>'+
|
||||||
|
'<div class="tl-fert" style="background:'+FERT_COLOR[d.fertClass]+'"></div>';
|
||||||
|
btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off));
|
||||||
|
wrap.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderOvDetailFromOffset(){
|
||||||
|
var d = OVULATION_DAYS[String(ovSelectedOffset)];
|
||||||
|
var el = document.getElementById("o-detail");
|
||||||
|
var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation");
|
||||||
|
el.innerHTML =
|
||||||
|
'<span class="d-date">'+label+'</span>'+
|
||||||
|
'<span class="pill" style="background:var(--ovulation-soft);color:'+FERT_COLOR[d.fertClass]+'">Fertility: '+d.fert+'</span>'+
|
||||||
|
'<div class="d-phase">'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'</div>'+
|
||||||
|
'<div class="d-grid">'+
|
||||||
|
'<div class="d-mini"><div class="k">Basal body temp</div><div class="v" style="font-size:0.82rem;">'+d.bbt+'</div></div>'+
|
||||||
|
'<div class="d-mini"><div class="k">Cervical mucus</div><div class="v" style="font-size:0.82rem;">'+d.mucus+'</div></div>'+
|
||||||
|
'</div>'+
|
||||||
|
'<p class="d-desc">'+d.note+'</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOvulationCalendar(){
|
||||||
|
var state = getCycleState();
|
||||||
|
document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear();
|
||||||
|
buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth,
|
||||||
|
function(d){ return classifyForCycle(d, state); },
|
||||||
|
function(d){
|
||||||
|
oSelectedDate = d;
|
||||||
|
var info = classifyForCycle(d, state);
|
||||||
|
var offset = info.dic - info.ovDic;
|
||||||
|
if(offset < -5) offset = -5; if(offset > 5) offset = 5;
|
||||||
|
ovSelectedOffset = offset;
|
||||||
|
renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar();
|
||||||
|
},
|
||||||
|
oSelectedDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOvulationStats(){
|
||||||
|
var state = getCycleState();
|
||||||
|
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
||||||
|
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
||||||
|
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
||||||
|
var fertileStartDic = mod(ovDic-5, state.cycleLen);
|
||||||
|
var inFertile = isFertile(dic, ovDic);
|
||||||
|
var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen);
|
||||||
|
document.getElementById("ovulation-stats").innerHTML =
|
||||||
|
statTile(inFertile ? "Yes" : "No", "In fertile window") +
|
||||||
|
statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") +
|
||||||
|
statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") +
|
||||||
|
statTile("6 days", "Window length");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); });
|
||||||
|
document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); });
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
PREGNANCY tab
|
||||||
|
================================================================ */
|
||||||
|
var lmpInput = document.getElementById("pr-lmp");
|
||||||
|
var exampleLMP = addDays(today, -20*7 - 3);
|
||||||
|
var savedLMP = loadLS("ca-lmp", null);
|
||||||
|
lmpInput.value = savedLMP || toInputVal(exampleLMP);
|
||||||
|
if(savedLMP) document.getElementById("pr-example-note").style.display = "none";
|
||||||
|
|
||||||
|
var slider = document.getElementById("week-slider");
|
||||||
|
var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
|
||||||
|
lmpInput.addEventListener("input", function(){
|
||||||
|
saveLS("ca-lmp", lmpInput.value);
|
||||||
|
document.getElementById("pr-example-note").style.display = "none";
|
||||||
|
document.getElementById("p-example-note").style.display = "none";
|
||||||
|
syncSliderToLMP();
|
||||||
|
renderPregnancy();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; }
|
||||||
|
function currentGestWeek(){
|
||||||
|
var lmp = getLMP();
|
||||||
|
var w = Math.floor(daysBetween(lmp,today)/7) + 1;
|
||||||
|
return Math.max(1, Math.min(40, w));
|
||||||
|
}
|
||||||
|
function syncSliderToLMP(){ slider.value = currentGestWeek(); }
|
||||||
|
syncSliderToLMP();
|
||||||
|
|
||||||
|
function renderPregnancyStats(){
|
||||||
|
var lmp = getLMP();
|
||||||
|
var due = addDays(lmp, 280);
|
||||||
|
var week = currentGestWeek();
|
||||||
|
var daysLeft = Math.max(0, daysBetween(today, due));
|
||||||
|
document.getElementById("pregnancy-stats").innerHTML =
|
||||||
|
statTile(week, "Weeks along") +
|
||||||
|
statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") +
|
||||||
|
statTile(daysLeft, "Days to due date") +
|
||||||
|
statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date");
|
||||||
|
|
||||||
|
var pct = (week-1)/39;
|
||||||
|
var pathLen = 230;
|
||||||
|
var offset = pathLen - pct*pathLen;
|
||||||
|
var path = document.getElementById("pregnancy-progress-path");
|
||||||
|
path.setAttribute("stroke-dashoffset", offset.toFixed(1));
|
||||||
|
try{
|
||||||
|
var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen;
|
||||||
|
var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null;
|
||||||
|
if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); }
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWeekPanel(week){
|
||||||
|
var w = WEEKS[week-1];
|
||||||
|
document.getElementById("wk-badge").textContent = "WEEK " + week;
|
||||||
|
document.getElementById("wk-compare").textContent = "size of " + w.c;
|
||||||
|
document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m;
|
||||||
|
document.getElementById("wk-dev").textContent = w.dev;
|
||||||
|
document.getElementById("wk-body").textContent = w.body;
|
||||||
|
document.getElementById("wk-tip").textContent = w.tip;
|
||||||
|
var tri = week<=13?1:(week<=27?2:3);
|
||||||
|
var triColor = "var(--pregnancy)";
|
||||||
|
document.getElementById("wk-blob").innerHTML =
|
||||||
|
'<svg width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="'+(14+week*0.9)+'" fill="'+ (tri===1?"var(--pregnancy-soft)":tri===2?"var(--pregnancy)":"var(--pregnancy-strong)") +'" opacity="0.85"/></svg>';
|
||||||
|
document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); });
|
||||||
|
var active = document.querySelector('.film-item[data-week="'+week+'"]');
|
||||||
|
if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"});
|
||||||
|
}
|
||||||
|
|
||||||
|
slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); });
|
||||||
|
|
||||||
|
function buildFilmstrip(){
|
||||||
|
var wrap = document.getElementById("week-filmstrip");
|
||||||
|
wrap.innerHTML = "";
|
||||||
|
for(var w=1; w<=40; w++){
|
||||||
|
var item = document.createElement("button");
|
||||||
|
item.className = "film-item";
|
||||||
|
item.dataset.week = w;
|
||||||
|
item.innerHTML = '<div class="fw">WK</div><div class="fn">'+w+'</div>';
|
||||||
|
item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w));
|
||||||
|
wrap.appendChild(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPregnancyCalendar(){
|
||||||
|
var lmp = getLMP();
|
||||||
|
document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear();
|
||||||
|
var grid = document.getElementById("pr-daygrid");
|
||||||
|
grid.innerHTML = "";
|
||||||
|
var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth();
|
||||||
|
var firstDow = new Date(year,month,1).getDay();
|
||||||
|
var daysInMonth = new Date(year,month+1,0).getDate();
|
||||||
|
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
||||||
|
for(var i=0;i<totalCells;i++){
|
||||||
|
var dayNum = i - firstDow + 1;
|
||||||
|
var cell = document.createElement("button");
|
||||||
|
cell.className = "day-cell";
|
||||||
|
if(dayNum<1 || dayNum>daysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; }
|
||||||
|
var d = new Date(year,month,dayNum);
|
||||||
|
var wk = Math.floor(daysBetween(lmp,d)/7)+1;
|
||||||
|
var inRange = wk>=1 && wk<=42;
|
||||||
|
cell.innerHTML = "<span>"+dayNum+"</span>" + (inRange ? '<span class="dot-row" style="position:static;margin-top:2px;font-size:0.55rem;color:var(--pregnancy-strong)">W'+wk+'</span>' : "");
|
||||||
|
if(inRange && wk<=40) cell.classList.add("is-fertile");
|
||||||
|
if(sameDay(d,today)) cell.classList.add("today");
|
||||||
|
cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk));
|
||||||
|
grid.appendChild(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); });
|
||||||
|
document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); });
|
||||||
|
|
||||||
|
function renderPregnancy(){
|
||||||
|
renderPregnancyStats();
|
||||||
|
renderWeekPanel(currentGestWeek());
|
||||||
|
renderPregnancyCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
INIT
|
||||||
|
================================================================ */
|
||||||
|
function renderAll(){
|
||||||
|
renderPeriodStats();
|
||||||
|
renderPeriodCalendar();
|
||||||
|
renderPeriodDetail();
|
||||||
|
renderOvulationStats();
|
||||||
|
renderOvTimeline();
|
||||||
|
renderOvDetailFromOffset();
|
||||||
|
renderOvulationCalendar();
|
||||||
|
renderPregnancy();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPhaseRefGrid();
|
||||||
|
buildFilmstrip();
|
||||||
|
slider.value = currentGestWeek();
|
||||||
|
renderAll();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces) Fraunces-Italic[SOFT,WONK,opsz,wght].ttf: Copyright 2020 The Fraunces Project Authors (github.com/undercasetype/Fraunces)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) WorkSans-Italic[wght].ttf: Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/* Self-hosted fonts (Latin subset), SIL Open Font License 1.1 - see OFL-*.txt */
|
||||||
|
@font-face{font-family:"Fraunces";font-style:normal;font-weight:400;font-display:swap;src:url("fraunces-latin-400-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:normal;font-weight:500;font-display:swap;src:url("fraunces-latin-500-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:normal;font-weight:600;font-display:swap;src:url("fraunces-latin-600-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:normal;font-weight:700;font-display:swap;src:url("fraunces-latin-700-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:normal;font-weight:900;font-display:swap;src:url("fraunces-latin-900-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:italic;font-weight:500;font-display:swap;src:url("fraunces-latin-500-italic.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Fraunces";font-style:italic;font-weight:600;font-display:swap;src:url("fraunces-latin-600-italic.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Work Sans";font-style:normal;font-weight:400;font-display:swap;src:url("work-sans-latin-400-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Work Sans";font-style:normal;font-weight:500;font-display:swap;src:url("work-sans-latin-500-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Work Sans";font-style:normal;font-weight:600;font-display:swap;src:url("work-sans-latin-600-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"Work Sans";font-style:normal;font-weight:700;font-display:swap;src:url("work-sans-latin-700-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:400;font-display:swap;src:url("ibm-plex-mono-latin-400-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:500;font-display:swap;src:url("ibm-plex-mono-latin-500-normal.woff2") format("woff2");}
|
||||||
|
@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:600;font-display:swap;src:url("ibm-plex-mono-latin-600-normal.woff2") format("woff2");}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
-550
@@ -1,7 +1,11 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-AU">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||||||
<title>CycleWise AU</title>
|
<title>CycleWise AU</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,400;0,500;0,600;0,700;0,900;1,500;1,600&family=Work+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
:root{
|
:root{
|
||||||
--paper:#FBF7F2;
|
--paper:#FBF7F2;
|
||||||
@@ -131,6 +135,7 @@ button{font-family:inherit;}
|
|||||||
.mode-panel.active{display:block;animation:rise .4s ease both;}
|
.mode-panel.active{display:block;animation:rise .4s ease both;}
|
||||||
@keyframes rise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
|
@keyframes rise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
|
||||||
@media (prefers-reduced-motion: reduce){.mode-panel.active{animation:none;}}
|
@media (prefers-reduced-motion: reduce){.mode-panel.active{animation:none;}}
|
||||||
|
@media (prefers-color-scheme: dark){.mode-btn[data-mode="pregnancy"].active{color:#1B1417;}}
|
||||||
|
|
||||||
.hero{
|
.hero{
|
||||||
display:grid;grid-template-columns:1.3fr 1fr;gap:40px;
|
display:grid;grid-template-columns:1.3fr 1fr;gap:40px;
|
||||||
@@ -144,7 +149,11 @@ button{font-family:inherit;}
|
|||||||
}
|
}
|
||||||
.hero h1{font-size:clamp(1.9rem,3.4vw,2.7rem);font-weight:600;line-height:1.08;}
|
.hero h1{font-size:clamp(1.9rem,3.4vw,2.7rem);font-weight:600;line-height:1.08;}
|
||||||
.hero p.lede{color:var(--ink-soft);font-size:1.02rem;line-height:1.6;max-width:52ch;margin-top:14px;}
|
.hero p.lede{color:var(--ink-soft);font-size:1.02rem;line-height:1.6;max-width:52ch;margin-top:14px;}
|
||||||
.hero-art{display:flex;justify-content:center;}
|
.hero-art{display:flex;justify-content:center;position:relative;}
|
||||||
|
.hero-art.has-3d{align-items:center;}
|
||||||
|
.hero-art.has-3d .ring3d{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}
|
||||||
|
.hero-art.has-3d svg{position:relative;max-width:300px;}
|
||||||
|
.hero-art.has-3d svg path,.hero-art.has-3d svg circle{display:none;}
|
||||||
.hero-art svg{width:100%;max-width:220px;height:auto;}
|
.hero-art svg{width:100%;max-width:220px;height:auto;}
|
||||||
|
|
||||||
.stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}
|
.stat-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:22px;}
|
||||||
@@ -262,7 +271,7 @@ button{font-family:inherit;}
|
|||||||
text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;font-weight:600;}
|
text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;font-weight:600;}
|
||||||
input[type="range"].week-slider{
|
input[type="range"].week-slider{
|
||||||
width:100%;-webkit-appearance:none;appearance:none;height:8px;border-radius:999px;
|
width:100%;-webkit-appearance:none;appearance:none;height:8px;border-radius:999px;
|
||||||
background:linear-gradient(to right, var(--pregnancy-soft) 0%, var(--pregnancy-soft) 100%);
|
background:linear-gradient(to right, var(--pregnancy) 0, var(--pregnancy) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) 100%);
|
||||||
outline:none;cursor:pointer;
|
outline:none;cursor:pointer;
|
||||||
}
|
}
|
||||||
input[type="range"].week-slider::-webkit-slider-thumb{
|
input[type="range"].week-slider::-webkit-slider-thumb{
|
||||||
@@ -297,6 +306,7 @@ input[type="range"].week-slider::-moz-range-thumb{
|
|||||||
|
|
||||||
.filmstrip{display:flex;gap:8px;overflow-x:auto;padding:4px 2px 20px;margin-top:8px;}
|
.filmstrip{display:flex;gap:8px;overflow-x:auto;padding:4px 2px 20px;margin-top:8px;}
|
||||||
.film-item{
|
.film-item{
|
||||||
|
color:var(--ink);
|
||||||
flex:none;width:64px;height:64px;border-radius:12px;border:1px solid var(--line);
|
flex:none;width:64px;height:64px;border-radius:12px;border:1px solid var(--line);
|
||||||
background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center;
|
background:var(--surface);display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
cursor:pointer;font-family:"IBM Plex Mono",monospace;
|
cursor:pointer;font-family:"IBM Plex Mono",monospace;
|
||||||
@@ -310,7 +320,8 @@ footer.note{
|
|||||||
font-size:0.78rem;color:var(--ink-soft);line-height:1.6;
|
font-size:0.78rem;color:var(--ink-soft);line-height:1.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div class="wordmark">
|
<div class="wordmark">
|
||||||
@@ -324,15 +335,15 @@ footer.note{
|
|||||||
<span class="tag">Menstrual & Pregnancy Charting</span>
|
<span class="tag">Menstrual & Pregnancy Charting</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="mode-switch" role="tablist" aria-label="Tracking mode">
|
<nav class="mode-switch" role="tablist" aria-label="Tracking mode">
|
||||||
<button class="mode-btn active" data-mode="period" role="tab" aria-selected="true">Period</button>
|
<button class="mode-btn active" id="tab-period" aria-controls="mode-period" data-mode="period" role="tab" aria-selected="true">Period</button>
|
||||||
<button class="mode-btn" data-mode="ovulation" role="tab" aria-selected="false">Ovulation</button>
|
<button class="mode-btn" id="tab-ovulation" aria-controls="mode-ovulation" data-mode="ovulation" role="tab" aria-selected="false">Ovulation</button>
|
||||||
<button class="mode-btn" data-mode="pregnancy" role="tab" aria-selected="false">Pregnancy</button>
|
<button class="mode-btn" id="tab-pregnancy" aria-controls="mode-pregnancy" data-mode="pregnancy" role="tab" aria-selected="false">Pregnancy</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- ================= PERIOD ================= -->
|
<!-- ================= PERIOD ================= -->
|
||||||
<section id="mode-period" class="mode-panel active">
|
<section id="mode-period" class="mode-panel active" role="tabpanel" aria-labelledby="tab-period">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow" style="color:var(--period)">01 — Menstrual Cycle</span>
|
<span class="eyebrow" style="color:var(--period)">01 — Menstrual Cycle</span>
|
||||||
@@ -398,7 +409,7 @@ footer.note{
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ================= OVULATION ================= -->
|
<!-- ================= OVULATION ================= -->
|
||||||
<section id="mode-ovulation" class="mode-panel">
|
<section id="mode-ovulation" class="mode-panel" role="tabpanel" aria-labelledby="tab-ovulation">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow" style="color:var(--ovulation)">02 — Fertile Window</span>
|
<span class="eyebrow" style="color:var(--ovulation)">02 — Fertile Window</span>
|
||||||
@@ -446,7 +457,7 @@ footer.note{
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ================= PREGNANCY ================= -->
|
<!-- ================= PREGNANCY ================= -->
|
||||||
<section id="mode-pregnancy" class="mode-panel">
|
<section id="mode-pregnancy" class="mode-panel" role="tabpanel" aria-labelledby="tab-pregnancy">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow" style="color:var(--pregnancy)">03 — Forty Weeks</span>
|
<span class="eyebrow" style="color:var(--pregnancy)">03 — Forty Weeks</span>
|
||||||
@@ -476,7 +487,7 @@ footer.note{
|
|||||||
<span style="color:var(--pregnancy)">Trimester 2 · Weeks 14–27</span>
|
<span style="color:var(--pregnancy)">Trimester 2 · Weeks 14–27</span>
|
||||||
<span style="color:var(--pregnancy)">Trimester 3 · Weeks 28–40</span>
|
<span style="color:var(--pregnancy)">Trimester 3 · Weeks 28–40</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="range" class="week-slider" id="week-slider" min="1" max="40" value="20">
|
<input type="range" class="week-slider" id="week-slider" aria-label="Pregnancy week, 1 to 40" min="1" max="40" value="20">
|
||||||
<div class="week-ticks"><span>Wk 1</span><span>Wk 13</span><span>Wk 27</span><span>Wk 40</span></div>
|
<div class="week-ticks"><span>Wk 1</span><span>Wk 13</span><span>Wk 27</span><span>Wk 40</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -524,540 +535,11 @@ footer.note{
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="app.js"></script>
|
||||||
(function(){
|
<script src="vendor/gsap.min.js"></script>
|
||||||
"use strict";
|
<script src="vendor/ScrollTrigger.min.js"></script>
|
||||||
|
<script src="ring3d.js"></script>
|
||||||
/* ---------------- date helpers ---------------- */
|
<script src="animations.js"></script>
|
||||||
function toDate(y,m,d){ return new Date(y,m,d); }
|
<script src="timeline.js"></script>
|
||||||
function fromInput(str){
|
</body>
|
||||||
if(!str) return null;
|
</html>
|
||||||
var p = str.split("-");
|
|
||||||
return new Date(+p[0], +p[1]-1, +p[2]);
|
|
||||||
}
|
|
||||||
function toInputVal(d){
|
|
||||||
return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0");
|
|
||||||
}
|
|
||||||
function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; }
|
|
||||||
function daysBetween(a,b){ // b - a, in whole days, ignoring time
|
|
||||||
var A = new Date(a.getFullYear(),a.getMonth(),a.getDate());
|
|
||||||
var B = new Date(b.getFullYear(),b.getMonth(),b.getDate());
|
|
||||||
return Math.round((B-A)/86400000);
|
|
||||||
}
|
|
||||||
function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
|
|
||||||
function mod(n,m){ return ((n % m) + m) % m; }
|
|
||||||
var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
||||||
var today = new Date(); today.setHours(0,0,0,0);
|
|
||||||
|
|
||||||
function loadLS(key, fallback){
|
|
||||||
try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; }
|
|
||||||
}
|
|
||||||
function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} }
|
|
||||||
|
|
||||||
/* ---------------- mode switching ---------------- */
|
|
||||||
var modeBtns = document.querySelectorAll(".mode-btn");
|
|
||||||
var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") };
|
|
||||||
modeBtns.forEach(function(btn){
|
|
||||||
btn.addEventListener("click", function(){
|
|
||||||
modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); });
|
|
||||||
btn.classList.add("active"); btn.setAttribute("aria-selected","true");
|
|
||||||
Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); });
|
|
||||||
panels[btn.dataset.mode].classList.add("active");
|
|
||||||
saveLS("ca-mode", btn.dataset.mode);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
var savedMode = loadLS("ca-mode", null);
|
|
||||||
if(savedMode && panels[savedMode]){
|
|
||||||
modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); });
|
|
||||||
Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
PHASE + OVULATION REFERENCE DATA
|
|
||||||
================================================================ */
|
|
||||||
var PHASES = {
|
|
||||||
menstrual: {
|
|
||||||
title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)",
|
|
||||||
window:"Cycle day 1 through the end of bleeding",
|
|
||||||
desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.",
|
|
||||||
hormone:"Estrogen & progesterone: low", energy:"Energy: lowest",
|
|
||||||
symptoms:"Cramping, fatigue, lower back ache, mood dips",
|
|
||||||
tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone."
|
|
||||||
},
|
|
||||||
follicular: {
|
|
||||||
title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)",
|
|
||||||
window:"Cycle day 1 through ovulation (overlaps menstrual)",
|
|
||||||
desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.",
|
|
||||||
hormone:"Estrogen: rising", energy:"Energy: climbing",
|
|
||||||
symptoms:"Clearer skin, sharper focus, rising motivation",
|
|
||||||
tip:"This is the physiologically best window for starting new training blocks or demanding projects."
|
|
||||||
},
|
|
||||||
ovulatory: {
|
|
||||||
title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)",
|
|
||||||
window:"Roughly one day, mid-cycle",
|
|
||||||
desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.",
|
|
||||||
hormone:"LH: sharp surge", energy:"Energy & libido: peak",
|
|
||||||
symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)",
|
|
||||||
tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released."
|
|
||||||
},
|
|
||||||
luteal: {
|
|
||||||
title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)",
|
|
||||||
window:"Ovulation through the day before your next period",
|
|
||||||
desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.",
|
|
||||||
hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining",
|
|
||||||
symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)",
|
|
||||||
tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened."
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// offset relative to ovulation day (0 = ovulation)
|
|
||||||
var OVULATION_DAYS = {
|
|
||||||
"-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."},
|
|
||||||
"-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."},
|
|
||||||
"-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."},
|
|
||||||
"-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."},
|
|
||||||
"-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."},
|
|
||||||
"0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."},
|
|
||||||
"1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."},
|
|
||||||
"2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."},
|
|
||||||
"3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."},
|
|
||||||
"4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."},
|
|
||||||
"5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."}
|
|
||||||
};
|
|
||||||
var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"};
|
|
||||||
|
|
||||||
function phaseForDayInCycle(dic, periodLen, ovulationDic){
|
|
||||||
if(dic < periodLen) return "menstrual";
|
|
||||||
if(dic < ovulationDic) return "follicular";
|
|
||||||
if(dic === ovulationDic) return "ovulatory";
|
|
||||||
return "luteal";
|
|
||||||
}
|
|
||||||
function isFertile(dic, ovulationDic){
|
|
||||||
return dic >= ovulationDic - 5 && dic <= ovulationDic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
PREGNANCY WEEK DATA (1–40)
|
|
||||||
================================================================ */
|
|
||||||
var WEEKS = [
|
|
||||||
{c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."},
|
|
||||||
{c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."},
|
|
||||||
{c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."},
|
|
||||||
{c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."},
|
|
||||||
{c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."},
|
|
||||||
{c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."},
|
|
||||||
{c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."},
|
|
||||||
{c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."},
|
|
||||||
{c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."},
|
|
||||||
{c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."},
|
|
||||||
{c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."},
|
|
||||||
{c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."},
|
|
||||||
{c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."},
|
|
||||||
{c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."},
|
|
||||||
{c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."},
|
|
||||||
{c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."},
|
|
||||||
{c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."},
|
|
||||||
{c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."},
|
|
||||||
{c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."},
|
|
||||||
{c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."},
|
|
||||||
{c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."},
|
|
||||||
{c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."},
|
|
||||||
{c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."},
|
|
||||||
{c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."},
|
|
||||||
{c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."},
|
|
||||||
{c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."},
|
|
||||||
{c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."},
|
|
||||||
{c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."},
|
|
||||||
{c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."},
|
|
||||||
{c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."},
|
|
||||||
{c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."},
|
|
||||||
{c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."},
|
|
||||||
{c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."},
|
|
||||||
{c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."},
|
|
||||||
{c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."},
|
|
||||||
{c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."},
|
|
||||||
{c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."},
|
|
||||||
{c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."},
|
|
||||||
{c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."},
|
|
||||||
{c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."}
|
|
||||||
];
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
SVG helpers
|
|
||||||
================================================================ */
|
|
||||||
function polarToXY(cx,cy,r,angleDeg){
|
|
||||||
var rad = (angleDeg-90) * Math.PI/180;
|
|
||||||
return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)];
|
|
||||||
}
|
|
||||||
function arcPath(cx,cy,r,startDeg,endDeg){
|
|
||||||
if(endDeg <= startDeg) endDeg = startDeg + 0.01;
|
|
||||||
var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg);
|
|
||||||
var large = (endDeg-startDeg) > 180 ? 1 : 0;
|
|
||||||
return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
SHARED STATE: period start / cycle length / period length
|
|
||||||
================================================================ */
|
|
||||||
var exampleStart = addDays(today, -10);
|
|
||||||
var startInput = document.getElementById("p-start");
|
|
||||||
var cycleLenInput = document.getElementById("p-cyclelen");
|
|
||||||
var periodLenInput = document.getElementById("p-periodlen");
|
|
||||||
|
|
||||||
var savedStart = loadLS("ca-start", null);
|
|
||||||
var savedCycleLen = loadLS("ca-cyclelen", null);
|
|
||||||
var savedPeriodLen = loadLS("ca-periodlen", null);
|
|
||||||
var usingExample = !savedStart;
|
|
||||||
|
|
||||||
startInput.value = savedStart || toInputVal(exampleStart);
|
|
||||||
cycleLenInput.value = savedCycleLen || 28;
|
|
||||||
periodLenInput.value = savedPeriodLen || 5;
|
|
||||||
|
|
||||||
function getCycleState(){
|
|
||||||
return {
|
|
||||||
start: fromInput(startInput.value) || exampleStart,
|
|
||||||
cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)),
|
|
||||||
periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
||||||
var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
||||||
var pSelectedDate = new Date(today);
|
|
||||||
var oSelectedDate = new Date(today);
|
|
||||||
|
|
||||||
[startInput,cycleLenInput,periodLenInput].forEach(function(inp){
|
|
||||||
inp.addEventListener("input", function(){
|
|
||||||
saveLS("ca-start", startInput.value);
|
|
||||||
saveLS("ca-cyclelen", cycleLenInput.value);
|
|
||||||
saveLS("ca-periodlen", periodLenInput.value);
|
|
||||||
usingExample = false;
|
|
||||||
document.getElementById("p-example-note").style.display = "none";
|
|
||||||
document.getElementById("pr-example-note").style.display = "none";
|
|
||||||
renderAll();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if(!usingExample) document.getElementById("p-example-note").style.display = "none";
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
CALENDAR RENDERING (shared builder)
|
|
||||||
================================================================ */
|
|
||||||
function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){
|
|
||||||
gridEl.innerHTML = "";
|
|
||||||
var year = monthDate.getFullYear(), month = monthDate.getMonth();
|
|
||||||
var firstDow = new Date(year,month,1).getDay();
|
|
||||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
|
||||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
|
||||||
for(var i=0;i<totalCells;i++){
|
|
||||||
var dayNum = i - firstDow + 1;
|
|
||||||
var cell = document.createElement("button");
|
|
||||||
cell.className = "day-cell";
|
|
||||||
if(dayNum < 1 || dayNum > daysInMonth){
|
|
||||||
cell.classList.add("pad");
|
|
||||||
gridEl.appendChild(cell);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var d = new Date(year,month,dayNum);
|
|
||||||
var info = dayClassifier(d);
|
|
||||||
cell.innerHTML = "<span>"+dayNum+"</span>";
|
|
||||||
if(info.period) cell.classList.add("is-period");
|
|
||||||
if(info.fertile) cell.classList.add("is-fertile");
|
|
||||||
if(info.ovulation) cell.classList.add("is-ovulation");
|
|
||||||
if(sameDay(d,today)) cell.classList.add("today");
|
|
||||||
if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected");
|
|
||||||
cell.setAttribute("aria-label", d.toDateString());
|
|
||||||
cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d));
|
|
||||||
gridEl.appendChild(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function classifyForCycle(d, state){
|
|
||||||
var dic = mod(daysBetween(state.start, d), state.cycleLen);
|
|
||||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
|
||||||
return {
|
|
||||||
period: dic < state.periodLen,
|
|
||||||
fertile: isFertile(dic, ovDic),
|
|
||||||
ovulation: dic === ovDic,
|
|
||||||
dic: dic, ovDic: ovDic
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------- PERIOD tab render ---------------- */
|
|
||||||
function renderPeriodCalendar(){
|
|
||||||
var state = getCycleState();
|
|
||||||
document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear();
|
|
||||||
buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth,
|
|
||||||
function(d){ return classifyForCycle(d, state); },
|
|
||||||
function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); },
|
|
||||||
pSelectedDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPeriodDetail(){
|
|
||||||
var state = getCycleState();
|
|
||||||
var info = classifyForCycle(pSelectedDate, state);
|
|
||||||
var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic);
|
|
||||||
var phase = PHASES[phaseKey];
|
|
||||||
var el = document.getElementById("p-detail");
|
|
||||||
el.innerHTML =
|
|
||||||
'<span class="d-date">'+pSelectedDate.toDateString()+'</span>'+
|
|
||||||
'<span class="pill" style="background:'+phase.soft+';color:'+phase.color+'">'+phase.title+' phase'+(info.fertile?' · fertile window':'')+'</span>'+
|
|
||||||
'<div class="d-phase">Cycle day '+(info.dic+1)+'</div>'+
|
|
||||||
'<p class="d-desc">'+phase.desc+'</p>'+
|
|
||||||
'<div class="d-grid">'+
|
|
||||||
'<div class="d-mini"><div class="k">Hormones</div><div class="v">'+phase.hormone+'</div></div>'+
|
|
||||||
'<div class="d-mini"><div class="k">Typical symptoms</div><div class="v" style="font-weight:500;font-size:0.82rem;">'+phase.symptoms+'</div></div>'+
|
|
||||||
'</div>'+
|
|
||||||
'<p class="d-desc"><strong style="color:var(--ink)">Care note — </strong>'+phase.tip+'</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPeriodStats(){
|
|
||||||
var state = getCycleState();
|
|
||||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
|
||||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
|
||||||
var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen);
|
|
||||||
if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen;
|
|
||||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
|
||||||
var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic);
|
|
||||||
document.getElementById("period-stats").innerHTML =
|
|
||||||
statTile(dic+1, "Cycle day today") +
|
|
||||||
statTile(PHASES[phaseKey].title, "Current phase") +
|
|
||||||
statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") +
|
|
||||||
statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation");
|
|
||||||
|
|
||||||
// hero ring
|
|
||||||
var cx=100, cy=100, r=88;
|
|
||||||
var perStart=0, perEnd = state.periodLen/state.cycleLen*360;
|
|
||||||
var follEnd = ovDic/state.cycleLen*360;
|
|
||||||
var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360;
|
|
||||||
var lutStart = (ovDic+1)/state.cycleLen*360;
|
|
||||||
document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd));
|
|
||||||
document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart));
|
|
||||||
document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen)));
|
|
||||||
document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360));
|
|
||||||
var todayAngle = dic/state.cycleLen*360;
|
|
||||||
var tp = polarToXY(cx,cy,r,todayAngle);
|
|
||||||
document.getElementById("today-marker").setAttribute("cx", tp[0]);
|
|
||||||
document.getElementById("today-marker").setAttribute("cy", tp[1]);
|
|
||||||
document.getElementById("cycle-day-big").textContent = dic+1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function statTile(num,label){
|
|
||||||
return '<div class="stat-tile"><div class="num">'+num+'</div><div class="lbl">'+label+'</div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); });
|
|
||||||
document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); });
|
|
||||||
|
|
||||||
function renderPhaseRefGrid(){
|
|
||||||
var order = ["menstrual","follicular","ovulatory","luteal"];
|
|
||||||
document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){
|
|
||||||
var p = PHASES[key];
|
|
||||||
return '<div class="ref-card"><span class="r-num">0'+(i+1)+'</span><h4 style="color:'+p.color+'">'+p.title+'</h4>'+
|
|
||||||
'<p>'+p.desc+'</p>'+
|
|
||||||
'<div class="tag-row"><span class="tagchip">'+p.window+'</span></div></div>';
|
|
||||||
}).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------- OVULATION tab render ---------------- */
|
|
||||||
var ovSelectedOffset = 0;
|
|
||||||
function renderOvTimeline(){
|
|
||||||
var wrap = document.getElementById("ov-timeline");
|
|
||||||
wrap.innerHTML = "";
|
|
||||||
for(var off=-5; off<=5; off++){
|
|
||||||
var d = OVULATION_DAYS[String(off)];
|
|
||||||
var btn = document.createElement("button");
|
|
||||||
btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":"");
|
|
||||||
btn.innerHTML = '<div class="tl-day">'+(off===0?"OVUL.":(off>0?"+":"")+off)+'</div>'+
|
|
||||||
'<div class="tl-fert" style="background:'+FERT_COLOR[d.fertClass]+'"></div>';
|
|
||||||
btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off));
|
|
||||||
wrap.appendChild(btn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function renderOvDetailFromOffset(){
|
|
||||||
var d = OVULATION_DAYS[String(ovSelectedOffset)];
|
|
||||||
var el = document.getElementById("o-detail");
|
|
||||||
var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation");
|
|
||||||
el.innerHTML =
|
|
||||||
'<span class="d-date">'+label+'</span>'+
|
|
||||||
'<span class="pill" style="background:var(--ovulation-soft);color:'+FERT_COLOR[d.fertClass]+'">Fertility: '+d.fert+'</span>'+
|
|
||||||
'<div class="d-phase">'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'</div>'+
|
|
||||||
'<div class="d-grid">'+
|
|
||||||
'<div class="d-mini"><div class="k">Basal body temp</div><div class="v" style="font-size:0.82rem;">'+d.bbt+'</div></div>'+
|
|
||||||
'<div class="d-mini"><div class="k">Cervical mucus</div><div class="v" style="font-size:0.82rem;">'+d.mucus+'</div></div>'+
|
|
||||||
'</div>'+
|
|
||||||
'<p class="d-desc">'+d.note+'</p>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOvulationCalendar(){
|
|
||||||
var state = getCycleState();
|
|
||||||
document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear();
|
|
||||||
buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth,
|
|
||||||
function(d){ return classifyForCycle(d, state); },
|
|
||||||
function(d){
|
|
||||||
oSelectedDate = d;
|
|
||||||
var info = classifyForCycle(d, state);
|
|
||||||
var offset = info.dic - info.ovDic;
|
|
||||||
if(offset < -5) offset = -5; if(offset > 5) offset = 5;
|
|
||||||
ovSelectedOffset = offset;
|
|
||||||
renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar();
|
|
||||||
},
|
|
||||||
oSelectedDate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOvulationStats(){
|
|
||||||
var state = getCycleState();
|
|
||||||
var dic = mod(daysBetween(state.start, today), state.cycleLen);
|
|
||||||
var ovDic = mod(state.cycleLen - 14, state.cycleLen);
|
|
||||||
var daysToOvulation = mod(ovDic - dic, state.cycleLen);
|
|
||||||
var fertileStartDic = mod(ovDic-5, state.cycleLen);
|
|
||||||
var inFertile = isFertile(dic, ovDic);
|
|
||||||
var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen);
|
|
||||||
document.getElementById("ovulation-stats").innerHTML =
|
|
||||||
statTile(inFertile ? "Yes" : "No", "In fertile window") +
|
|
||||||
statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") +
|
|
||||||
statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") +
|
|
||||||
statTile("6 days", "Window length");
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); });
|
|
||||||
document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); });
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
PREGNANCY tab
|
|
||||||
================================================================ */
|
|
||||||
var lmpInput = document.getElementById("pr-lmp");
|
|
||||||
var exampleLMP = addDays(today, -20*7 - 3);
|
|
||||||
var savedLMP = loadLS("ca-lmp", null);
|
|
||||||
lmpInput.value = savedLMP || toInputVal(exampleLMP);
|
|
||||||
if(savedLMP) document.getElementById("pr-example-note").style.display = "none";
|
|
||||||
|
|
||||||
var slider = document.getElementById("week-slider");
|
|
||||||
var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
||||||
|
|
||||||
lmpInput.addEventListener("input", function(){
|
|
||||||
saveLS("ca-lmp", lmpInput.value);
|
|
||||||
document.getElementById("pr-example-note").style.display = "none";
|
|
||||||
document.getElementById("p-example-note").style.display = "none";
|
|
||||||
syncSliderToLMP();
|
|
||||||
renderPregnancy();
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; }
|
|
||||||
function currentGestWeek(){
|
|
||||||
var lmp = getLMP();
|
|
||||||
var w = Math.floor(daysBetween(lmp,today)/7) + 1;
|
|
||||||
return Math.max(1, Math.min(40, w));
|
|
||||||
}
|
|
||||||
function syncSliderToLMP(){ slider.value = currentGestWeek(); }
|
|
||||||
syncSliderToLMP();
|
|
||||||
|
|
||||||
function renderPregnancyStats(){
|
|
||||||
var lmp = getLMP();
|
|
||||||
var due = addDays(lmp, 280);
|
|
||||||
var week = currentGestWeek();
|
|
||||||
var daysLeft = Math.max(0, daysBetween(today, due));
|
|
||||||
document.getElementById("pregnancy-stats").innerHTML =
|
|
||||||
statTile(week, "Weeks along") +
|
|
||||||
statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") +
|
|
||||||
statTile(daysLeft, "Days to due date") +
|
|
||||||
statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date");
|
|
||||||
|
|
||||||
var pct = (week-1)/39;
|
|
||||||
var pathLen = 230;
|
|
||||||
var offset = pathLen - pct*pathLen;
|
|
||||||
var path = document.getElementById("pregnancy-progress-path");
|
|
||||||
path.setAttribute("stroke-dashoffset", offset.toFixed(1));
|
|
||||||
try{
|
|
||||||
var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen;
|
|
||||||
var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null;
|
|
||||||
if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); }
|
|
||||||
}catch(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderWeekPanel(week){
|
|
||||||
var w = WEEKS[week-1];
|
|
||||||
document.getElementById("wk-badge").textContent = "WEEK " + week;
|
|
||||||
document.getElementById("wk-compare").textContent = "size of " + w.c;
|
|
||||||
document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m;
|
|
||||||
document.getElementById("wk-dev").textContent = w.dev;
|
|
||||||
document.getElementById("wk-body").textContent = w.body;
|
|
||||||
document.getElementById("wk-tip").textContent = w.tip;
|
|
||||||
var tri = week<=13?1:(week<=27?2:3);
|
|
||||||
var triColor = "var(--pregnancy)";
|
|
||||||
document.getElementById("wk-blob").innerHTML =
|
|
||||||
'<svg width="96" height="96" viewBox="0 0 96 96"><circle cx="48" cy="48" r="'+(14+week*0.9)+'" fill="'+ (tri===1?"var(--pregnancy-soft)":tri===2?"var(--pregnancy)":"var(--pregnancy-strong)") +'" opacity="0.85"/></svg>';
|
|
||||||
document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); });
|
|
||||||
var active = document.querySelector('.film-item[data-week="'+week+'"]');
|
|
||||||
if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"});
|
|
||||||
}
|
|
||||||
|
|
||||||
slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); });
|
|
||||||
|
|
||||||
function buildFilmstrip(){
|
|
||||||
var wrap = document.getElementById("week-filmstrip");
|
|
||||||
wrap.innerHTML = "";
|
|
||||||
for(var w=1; w<=40; w++){
|
|
||||||
var item = document.createElement("button");
|
|
||||||
item.className = "film-item";
|
|
||||||
item.dataset.week = w;
|
|
||||||
item.innerHTML = '<div class="fw">WK</div><div class="fn">'+w+'</div>';
|
|
||||||
item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w));
|
|
||||||
wrap.appendChild(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPregnancyCalendar(){
|
|
||||||
var lmp = getLMP();
|
|
||||||
document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear();
|
|
||||||
var grid = document.getElementById("pr-daygrid");
|
|
||||||
grid.innerHTML = "";
|
|
||||||
var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth();
|
|
||||||
var firstDow = new Date(year,month,1).getDay();
|
|
||||||
var daysInMonth = new Date(year,month+1,0).getDate();
|
|
||||||
var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7;
|
|
||||||
for(var i=0;i<totalCells;i++){
|
|
||||||
var dayNum = i - firstDow + 1;
|
|
||||||
var cell = document.createElement("button");
|
|
||||||
cell.className = "day-cell";
|
|
||||||
if(dayNum<1 || dayNum>daysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; }
|
|
||||||
var d = new Date(year,month,dayNum);
|
|
||||||
var wk = Math.floor(daysBetween(lmp,d)/7)+1;
|
|
||||||
var inRange = wk>=1 && wk<=42;
|
|
||||||
cell.innerHTML = "<span>"+dayNum+"</span>" + (inRange ? '<span class="dot-row" style="position:static;margin-top:2px;font-size:0.55rem;color:var(--pregnancy-strong)">W'+wk+'</span>' : "");
|
|
||||||
if(inRange && wk<=40) cell.classList.add("is-fertile");
|
|
||||||
if(sameDay(d,today)) cell.classList.add("today");
|
|
||||||
cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk));
|
|
||||||
grid.appendChild(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); });
|
|
||||||
document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); });
|
|
||||||
|
|
||||||
function renderPregnancy(){
|
|
||||||
renderPregnancyStats();
|
|
||||||
renderWeekPanel(currentGestWeek());
|
|
||||||
renderPregnancyCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================================================
|
|
||||||
INIT
|
|
||||||
================================================================ */
|
|
||||||
function renderAll(){
|
|
||||||
renderPeriodStats();
|
|
||||||
renderPeriodCalendar();
|
|
||||||
renderPeriodDetail();
|
|
||||||
renderOvulationStats();
|
|
||||||
renderOvTimeline();
|
|
||||||
renderOvDetailFromOffset();
|
|
||||||
renderOvulationCalendar();
|
|
||||||
renderPregnancy();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPhaseRefGrid();
|
|
||||||
buildFilmstrip();
|
|
||||||
slider.value = currentGestWeek();
|
|
||||||
renderAll();
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/* CycleWise AU - 3D cycle ring (Step 2).
|
||||||
|
Draws the same four phase arcs as the flat SVG ring, in 3D.
|
||||||
|
The page code sends the numbers via the "cycle:ring" event.
|
||||||
|
If WebGL is missing or anything fails, the flat ring stays visible. */
|
||||||
|
(function(){
|
||||||
|
"use strict";
|
||||||
|
function start(){
|
||||||
|
try {
|
||||||
|
if (!window.THREE) return;
|
||||||
|
var art = document.getElementById("phase-arc-menstrual");
|
||||||
|
art = art && art.closest(".hero-art");
|
||||||
|
if (!art) return;
|
||||||
|
|
||||||
|
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
var svg = art.querySelector("svg");
|
||||||
|
var canvas = document.createElement("canvas");
|
||||||
|
canvas.setAttribute("aria-hidden", "true");
|
||||||
|
canvas.className = "ring3d";
|
||||||
|
|
||||||
|
var renderer;
|
||||||
|
try { renderer = new THREE.WebGLRenderer({canvas: canvas, antialias: true, alpha: true}); }
|
||||||
|
catch(e){ return; } // no WebGL: keep the flat ring
|
||||||
|
|
||||||
|
var R = 1, TUBE = 0.085;
|
||||||
|
var scene = new THREE.Scene();
|
||||||
|
var camera = new THREE.PerspectiveCamera(30, 1, 0.1, 20);
|
||||||
|
camera.position.set(0, 0, 5.2);
|
||||||
|
scene.add(new THREE.AmbientLight(0xffffff, 0.75));
|
||||||
|
var sun = new THREE.DirectionalLight(0xffffff, 1.1);
|
||||||
|
sun.position.set(2, 3, 4);
|
||||||
|
scene.add(sun);
|
||||||
|
|
||||||
|
var group = new THREE.Group();
|
||||||
|
scene.add(group);
|
||||||
|
|
||||||
|
function cssColor(name){
|
||||||
|
var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||||
|
return new THREE.Color(v || "#888888");
|
||||||
|
}
|
||||||
|
function point(deg, r){
|
||||||
|
var a = deg * Math.PI / 180; // 0 deg = top, clockwise
|
||||||
|
return new THREE.Vector3(Math.sin(a) * r, Math.cos(a) * r, 0);
|
||||||
|
}
|
||||||
|
class ArcCurve extends THREE.Curve {
|
||||||
|
constructor(a0, a1){ super(); this.a0 = a0; this.a1 = a1; }
|
||||||
|
getPoint(t, target){
|
||||||
|
return (target || new THREE.Vector3()).copy(point(this.a0 + (this.a1 - this.a0) * t, R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = window.cycleRingData || null;
|
||||||
|
var meshes = [];
|
||||||
|
|
||||||
|
function clear(){
|
||||||
|
meshes.forEach(function(m){
|
||||||
|
group.remove(m); m.geometry.dispose(); m.material.dispose();
|
||||||
|
});
|
||||||
|
meshes = [];
|
||||||
|
}
|
||||||
|
function add(mesh){ group.add(mesh); meshes.push(mesh); }
|
||||||
|
function mat(c){ return new THREE.MeshStandardMaterial({color: c, roughness: 0.45, metalness: 0.05}); }
|
||||||
|
|
||||||
|
function build(){
|
||||||
|
clear();
|
||||||
|
if (!data) return;
|
||||||
|
var cols = {
|
||||||
|
menstrual: cssColor("--period"),
|
||||||
|
follicular: cssColor("--ovulation-soft"),
|
||||||
|
fertile: cssColor("--fertile"),
|
||||||
|
luteal: cssColor("--pregnancy-soft"),
|
||||||
|
ink: cssColor("--ink")
|
||||||
|
};
|
||||||
|
var arcs = [
|
||||||
|
[0, data.perEnd, cols.menstrual],
|
||||||
|
[data.perEnd, data.fertStart, cols.follicular],
|
||||||
|
[data.fertStart, data.fertEnd, cols.fertile],
|
||||||
|
[data.lutStart, 360, cols.luteal]
|
||||||
|
];
|
||||||
|
arcs.forEach(function(a){
|
||||||
|
var end = Math.max(a[1], a[0] + 0.5);
|
||||||
|
var tube = new THREE.Mesh(new THREE.TubeGeometry(new ArcCurve(a[0], end), 48, TUBE, 16, false), mat(a[2]));
|
||||||
|
add(tube);
|
||||||
|
[a[0], end].forEach(function(deg){ // round end caps
|
||||||
|
var cap = new THREE.Mesh(new THREE.SphereGeometry(TUBE, 16, 12), mat(a[2]));
|
||||||
|
cap.position.copy(point(deg, R));
|
||||||
|
add(cap);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
var marker = new THREE.Mesh(new THREE.SphereGeometry(TUBE * 1.9, 24, 16), mat(cols.ink));
|
||||||
|
marker.position.copy(point(data.todayAngle, R));
|
||||||
|
marker.position.z = TUBE * 0.6;
|
||||||
|
add(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize(){
|
||||||
|
var w = svg.getBoundingClientRect().width || 220;
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
|
renderer.setSize(w, w, false);
|
||||||
|
canvas.style.width = canvas.style.height = w + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible = false, raf = 0;
|
||||||
|
function frame(t){
|
||||||
|
raf = 0;
|
||||||
|
var s = t / 1000;
|
||||||
|
group.rotation.x = -0.55 + (reduce ? 0 : Math.sin(s * 0.6) * 0.12);
|
||||||
|
group.rotation.y = reduce ? 0.25 : Math.sin(s * 0.4) * 0.45;
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
if (visible && !reduce) raf = requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
function kick(){ if (!raf) raf = requestAnimationFrame(frame); }
|
||||||
|
|
||||||
|
// Show the 3D ring; hide the flat arcs but keep the "CYCLE DAY" text.
|
||||||
|
art.classList.add("has-3d");
|
||||||
|
art.insertBefore(canvas, svg);
|
||||||
|
resize();
|
||||||
|
build();
|
||||||
|
kick();
|
||||||
|
|
||||||
|
window.addEventListener("cycle:ring", function(e){ data = e.detail; build(); kick(); });
|
||||||
|
window.addEventListener("resize", function(){ resize(); kick(); });
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(){ build(); kick(); });
|
||||||
|
|
||||||
|
// Only animate while the ring is actually on screen (saves battery).
|
||||||
|
new IntersectionObserver(function(entries){
|
||||||
|
visible = entries[0].isIntersecting;
|
||||||
|
if (visible) { resize(); kick(); }
|
||||||
|
}).observe(canvas);
|
||||||
|
} catch(err) {
|
||||||
|
// Anything went wrong: leave the flat ring alone.
|
||||||
|
var c = document.querySelector(".ring3d"); if (c) c.remove();
|
||||||
|
var h = document.querySelector(".hero-art.has-3d"); if (h) h.classList.remove("has-3d");
|
||||||
|
if (window.console) console.warn("3D ring disabled:", err && err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// three.js is big: fetch it only after the page is up, so the page shows first.
|
||||||
|
function loadThree(){
|
||||||
|
var tag = document.createElement("script");
|
||||||
|
tag.src = "vendor/three.min.js";
|
||||||
|
tag.onload = start;
|
||||||
|
document.head.appendChild(tag);
|
||||||
|
}
|
||||||
|
function schedule(){
|
||||||
|
if (window.THREE) { start(); }
|
||||||
|
else if (window.requestIdleCallback) { requestIdleCallback(loadThree, {timeout: 2000}); }
|
||||||
|
else { setTimeout(loadThree, 300); }
|
||||||
|
}
|
||||||
|
if (document.readyState === "complete") schedule();
|
||||||
|
else window.addEventListener("load", schedule);
|
||||||
|
})();
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"skills": {
|
|
||||||
"motion-design": {
|
|
||||||
"source": "LottieFiles/motion-design-skill",
|
|
||||||
"sourceType": "github",
|
|
||||||
"skillPath": "skills/motion-design/SKILL.md",
|
|
||||||
"computedHash": "9fcf9130cc46ee8fd1d8e09a9263347a9d76efbcbfa46591cc4cefaf19bbb453"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
/* CycleWise AU - pregnancy timeline motion (Step 3).
|
||||||
|
- The week slider's track fills with colour up to the current week.
|
||||||
|
It grows the first time you scroll to it, then follows every week change.
|
||||||
|
- The baby-size picture pops a little when the week changes.
|
||||||
|
- The 40 week buttons wave in the first time they appear.
|
||||||
|
Cosmetic only: without GSAP the slider simply shows its normal look. */
|
||||||
|
(function(){
|
||||||
|
"use strict";
|
||||||
|
var slider = document.getElementById("week-slider");
|
||||||
|
var badge = document.getElementById("wk-badge");
|
||||||
|
if (!slider || !badge) return;
|
||||||
|
|
||||||
|
var hasGsap = !!window.gsap;
|
||||||
|
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
var animate = hasGsap && !reduce;
|
||||||
|
|
||||||
|
var fill = { f: 0 };
|
||||||
|
var target = 0;
|
||||||
|
var introDone = false;
|
||||||
|
|
||||||
|
function paint(){ slider.style.setProperty("--f", fill.f.toFixed(4)); }
|
||||||
|
function currentWeek(){
|
||||||
|
var n = parseInt((badge.textContent || "").replace(/\D/g, ""), 10);
|
||||||
|
return isNaN(n) ? (parseInt(slider.value, 10) || 1) : Math.max(1, Math.min(40, n));
|
||||||
|
}
|
||||||
|
function goTo(f){
|
||||||
|
target = f;
|
||||||
|
if (!introDone) return; // wait until the slider is on screen
|
||||||
|
if (!animate) { fill.f = f; paint(); return; }
|
||||||
|
gsap.to(fill, { f: f, duration: 0.6, ease: "power2.out", overwrite: true, onUpdate: paint });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow every week change, whether from the slider, a week button or the calendar.
|
||||||
|
var lastWeek = currentWeek();
|
||||||
|
target = (lastWeek - 1) / 39;
|
||||||
|
new MutationObserver(function(){
|
||||||
|
var w = currentWeek();
|
||||||
|
if (w === lastWeek) return;
|
||||||
|
lastWeek = w;
|
||||||
|
goTo((w - 1) / 39);
|
||||||
|
var blob = document.querySelector("#wk-blob svg");
|
||||||
|
if (animate && blob) {
|
||||||
|
gsap.fromTo(blob, { scale: 0.8, transformOrigin: "50% 50%" },
|
||||||
|
{ scale: 1, duration: 0.5, ease: "back.out(2)", clearProps: "transform" });
|
||||||
|
}
|
||||||
|
}).observe(badge, { childList: true, characterData: true, subtree: true });
|
||||||
|
|
||||||
|
// First time the slider is visible: grow the fill from empty up to this week.
|
||||||
|
new IntersectionObserver(function(entries, obs){
|
||||||
|
if (!entries[0].isIntersecting) return;
|
||||||
|
obs.disconnect();
|
||||||
|
introDone = true;
|
||||||
|
if (!animate) { fill.f = target; paint(); return; }
|
||||||
|
gsap.to(fill, { f: target, duration: 1.2, ease: "power2.out", onUpdate: paint });
|
||||||
|
}).observe(slider);
|
||||||
|
|
||||||
|
// First time the week buttons are visible: wave them in.
|
||||||
|
var strip = document.getElementById("week-filmstrip");
|
||||||
|
if (strip && animate) {
|
||||||
|
new IntersectionObserver(function(entries, obs){
|
||||||
|
if (!entries[0].isIntersecting) return;
|
||||||
|
obs.disconnect();
|
||||||
|
gsap.from(strip.querySelectorAll(".film-item"), {
|
||||||
|
opacity: 0, scale: 0.8, duration: 0.4, stagger: 0.02, ease: "power2.out", clearProps: "all"
|
||||||
|
});
|
||||||
|
}).observe(strip);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/*!
|
||||||
|
* ScrollTrigger 3.12.5
|
||||||
|
* https://gsap.com
|
||||||
|
*
|
||||||
|
* @license Copyright 2024, GreenSock. All rights reserved.
|
||||||
|
* Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership.
|
||||||
|
* @author: Jack Doyle, jack@greensock.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function r(){return Ce||"undefined"!=typeof window&&(Ce=window.gsap)&&Ce.registerPlugin&&Ce}function z(e,t){return~Le.indexOf(e)&&Le[Le.indexOf(e)+1][t]}function A(e){return!!~t.indexOf(e)}function B(e,t,r,n,o){return e.addEventListener(t,r,{passive:!1!==n,capture:!!o})}function C(e,t,r,n){return e.removeEventListener(t,r,!!n)}function F(){return De&&De.isPressed||Ie.cache++}function G(r,n){function dd(e){if(e||0===e){o&&(Se.history.scrollRestoration="manual");var t=De&&De.isPressed;e=dd.v=Math.round(e)||(De&&De.iOS?1:0),r(e),dd.cacheID=Ie.cache,t&&i("ss",e)}else(n||Ie.cache!==dd.cacheID||i("ref"))&&(dd.cacheID=Ie.cache,dd.v=r());return dd.v+dd.offset}return dd.offset=0,r&&dd}function J(e,t){return(t&&t._ctx&&t._ctx.selector||Ce.utils.toArray)(e)[0]||("string"==typeof e&&!1!==Ce.config().nullTargetWarn?console.warn("Element not found:",e):null)}function K(t,e){var r=e.s,n=e.sc;A(t)&&(t=ke.scrollingElement||Pe);var o=Ie.indexOf(t),i=n===Fe.sc?1:2;~o||(o=Ie.push(t)-1),Ie[o+i]||B(t,"scroll",F);var a=Ie[o+i],s=a||(Ie[o+i]=G(z(t,r),!0)||(A(t)?n:G(function(e){return arguments.length?t[r]=e:t[r]})));return s.target=t,a||(s.smooth="smooth"===Ce.getProperty(t,"scrollBehavior")),s}function L(e,t,o){function Cd(e,t){var r=ze();t||n<r-s?(a=i,i=e,l=s,s=r):o?i+=e:i=a+(e-a)/(r-l)*(s-l)}var i=e,a=e,s=ze(),l=s,n=t||50,c=Math.max(500,3*n);return{update:Cd,reset:function reset(){a=i=o?0:i,l=s=0},getVelocity:function getVelocity(e){var t=l,r=a,n=ze();return!e&&0!==e||e===i||Cd(e),s===l||c<n-l?0:(i+(o?r:-r))/((o?n:s)-t)*1e3}}}function M(e,t){return t&&!e._gsapAllow&&e.preventDefault(),e.changedTouches?e.changedTouches[0]:e}function N(e){var t=Math.max.apply(Math,e),r=Math.min.apply(Math,e);return Math.abs(t)>=Math.abs(r)?t:r}function O(){(Ae=Ce.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,Ie),n.push.apply(n,Le),Ie=t,Le=n,i=function _bridge(e,t){return r[e](t)}}()}function P(e){return Ce=e||r(),!Te&&Ce&&"undefined"!=typeof document&&document.body&&(Se=window,Pe=(ke=document).documentElement,Me=ke.body,t=[Se,ke,Pe,Me],Ce.utils.clamp,Be=Ce.core.context||function(){},Oe="onpointerenter"in Me?"pointer":"mouse",Ee=k.isTouch=Se.matchMedia&&Se.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Se||0<navigator.maxTouchPoints||0<navigator.msMaxTouchPoints?2:0,Re=k.eventTypes=("ontouchstart"in Pe?"touchstart,touchmove,touchcancel,touchend":"onpointerdown"in Pe?"pointerdown,pointermove,pointercancel,pointerup":"mousedown,mousemove,mouseup,mouseup").split(","),setTimeout(function(){return o=0},500),O(),Te=1),Te}var Ce,Te,Se,ke,Pe,Me,Ee,Oe,Ae,t,De,Re,Be,o=1,qe=[],Ie=[],Le=[],ze=Date.now,i=function _bridge(e,t){return t},n="scrollLeft",a="scrollTop",Ye={s:n,p:"left",p2:"Left",os:"right",os2:"Right",d:"width",d2:"Width",a:"x",sc:G(function(e){return arguments.length?Se.scrollTo(e,Fe.sc()):Se.pageXOffset||ke[n]||Pe[n]||Me[n]||0})},Fe={s:a,p:"top",p2:"Top",os:"bottom",os2:"Bottom",d:"height",d2:"Height",a:"y",op:Ye,sc:G(function(e){return arguments.length?Se.scrollTo(Ye.sc(),e):Se.pageYOffset||ke[a]||Pe[a]||Me[a]||0})};Ye.op=Fe,Ie.cache=0;var k=(Observer.prototype.init=function init(e){Te||P(Ce)||console.warn("Please gsap.registerPlugin(Observer)"),Ae||O();var o=e.tolerance,a=e.dragMinimum,t=e.type,i=e.target,r=e.lineHeight,n=e.debounce,s=e.preventDefault,l=e.onStop,c=e.onStopDelay,u=e.ignore,f=e.wheelSpeed,d=e.event,p=e.onDragStart,g=e.onDragEnd,h=e.onDrag,v=e.onPress,b=e.onRelease,m=e.onRight,y=e.onLeft,x=e.onUp,w=e.onDown,_=e.onChangeX,T=e.onChangeY,S=e.onChange,k=e.onToggleX,E=e.onToggleY,D=e.onHover,R=e.onHoverEnd,q=e.onMove,I=e.ignoreCheck,z=e.isNormalizer,Y=e.onGestureStart,H=e.onGestureEnd,X=e.onWheel,W=e.onEnable,V=e.onDisable,U=e.onClick,j=e.scrollSpeed,G=e.capture,Q=e.allowClicks,Z=e.lockAxis,$=e.onLockAxis;function cf(){return xe=ze()}function df(e,t){return(se.event=e)&&u&&~u.indexOf(e.target)||t&&he&&"touch"!==e.pointerType||I&&I(e,t)}function ff(){var e=se.deltaX=N(me),t=se.deltaY=N(ye),r=Math.abs(e)>=o,n=Math.abs(t)>=o;S&&(r||n)&&S(se,e,t,me,ye),r&&(m&&0<se.deltaX&&m(se),y&&se.deltaX<0&&y(se),_&&_(se),k&&se.deltaX<0!=le<0&&k(se),le=se.deltaX,me[0]=me[1]=me[2]=0),n&&(w&&0<se.deltaY&&w(se),x&&se.deltaY<0&&x(se),T&&T(se),E&&se.deltaY<0!=ce<0&&E(se),ce=se.deltaY,ye[0]=ye[1]=ye[2]=0),(ne||re)&&(q&&q(se),re&&(h(se),re=!1),ne=!1),ie&&!(ie=!1)&&$&&$(se),oe&&(X(se),oe=!1),ee=0}function gf(e,t,r){me[r]+=e,ye[r]+=t,se._vx.update(e),se._vy.update(t),n?ee=ee||requestAnimationFrame(ff):ff()}function hf(e,t){Z&&!ae&&(se.axis=ae=Math.abs(e)>Math.abs(t)?"xLine truncated
|
||||||
|
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright © 2010-2023 three.js authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/*!
|
||||||
|
* GSAP 3.12.5
|
||||||
|
* https://gsap.com
|
||||||
|
*
|
||||||
|
* @license Copyright 2024, GreenSock. All rights reserved.
|
||||||
|
* Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership.
|
||||||
|
* @author: Jack Doyle, jack@greensock.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&ze}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Vt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Mt(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function ma(){var t,e,r=dt.length,i=dt.slice(0);for(ct={},t=dt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function na(t,e,r,i){dt.length&&!L&&ma(),t.render(e,r,i||L&&e<0&&(t._initted||t._startAt)),dt.length&&!L&&ma()}function oa(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(at).length<2?e:r(t)?t.trim():t}function pa(t){return t}function qa(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function ta(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?ta(t[r]||(t[r]={}),e[r]):e[r]);return t}function ua(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function va(t){var e=t.parent||I,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}(Z(t.keyframes)):qa;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function xa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function ya(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function za(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Aa(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Ca(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ea(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ga(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ha(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function Ia(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ha(t),r._dirty||Aa(r,t)),t}function Ja(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ga(t.rawTime(),e),(!e._dur||Ot(0,e.totalDuration(),r)-e._tTime>X)&&e.render(r,!0)),Aa(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-X}}function Ka(e,r,i,n){return r.parent&&za(r),r._start=ja((t(i)?i:i||e!==I?xt(e,i,r):e._time)+r._delay),r._end=ja(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),xa(e,r,"_first","_last",e._sort?"_start":0),bt(r)||(e._recent=r),n||Ja(e,r),e._ts<0&&Ia(e,e._tTime),e}function La(t,e){return(ot.ScrollTrigger||Q("scrollTrigger",e))&&ot.ScrollTrigger.create(e,t)}function Ma(t,e,r,i,n){return Qt(t,e,n),t._initted?!r&&t._pt&&!L&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==Rt.frame?(dt.push(t),t._lazy=[n,i],1):void 0:1}function Ra(t,e,r,i){var n=t._repeat,a=ja(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:ja(a*(n+1)+t._rDelay*n):a,0<s&&!i&&Ia(t,t._tTime=t._tDur*s),t.parent&&Ha(t),r||Aa(t.parent,t),t}function Sa(t){return t instanceof Xt?Aa(t):Ra(t,t._dur)}function Va(e,r,i){var n,aLine truncated
|
||||||
|
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
console.warn('Scripts "build/three.js" and "build/three.min.js" are deprecated with r150+, and will be removed with r160. Please use ES Modules or alternatives: https://threejs.org/docs/index.html#manual/en/introduction/Installation'),
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2010-2023 Three.js Authors
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="160",n=1,i=2,r=3,s=0,a=1,o=100,l=204,c=205,h=0,u=1,d=2,p=0,m=1,f=2,g=3,_=4,v=5,x=6,y="attached",M="detached",S=300,b=301,T=302,E=303,w=304,A=306,R=1e3,C=1001,L=1002,P=1003,I=1004,U=1005,D=1006,N=1007,O=1008,F=1009,B=1012,z=1013,G=1014,H=1015,V=1016,k=1017,W=1018,X=1020,j=1023,q=1026,Y=1027,Z=1029,J=1031,K=1033,$=33776,Q=33777,tt=33778,et=33779,nt=35840,it=35841,rt=35842,st=35843,at=36196,ot=37492,lt=37496,ct=37808,ht=37809,ut=37810,dt=37811,pt=37812,mt=37813,ft=37814,gt=37815,_t=37816,vt=37817,xt=37818,yt=37819,Mt=37820,St=37821,bt=36492,Tt=36494,Et=36495,wt=36284,At=36285,Rt=36286,Ct=2300,Lt=2301,Pt=2302,It=2400,Ut=2401,Dt=2402,Nt=2500,Ot=2501,Ft=3e3,Bt=3001,zt="",Gt="srgb",Ht="srgb-linear",Vt="display-p3",kt="display-p3-linear",Wt="linear",Xt="srgb",jt="rec709",qt="p3",Yt=7680,Zt=35044,Jt="300 es",Kt=1035,$t=2e3,Qt=2001;class te{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e<i;e++)n[e].call(this,t);t.target=null}}}const ee=["00","01","02","03","04","05","06","07","08","09","0a","0b","0c","0d","0e","0f","10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f","20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f","30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f","40","41","42","43","44","45","46","47","48","49","4a","4b","4c","4d","4e","4f","50","51","52","53","54","55","56","57","58","59","5a","5b","5c","5d","5e","5f","60","61","62","63","64","65","66","67","68","69","6a","6b","6c","6d","6e","6f","70","71","72","73","74","75","76","77","78","79","7a","7b","7c","7d","7e","7f","80","81","82","83","84","85","86","87","88","89","8a","8b","8c","8d","8e","8f","90","91","92","93","94","95","96","97","98","99","9a","9b","9c","9d","9e","9f","a0","a1","a2","a3","a4","a5","a6","a7","a8","a9","aa","ab","ac","ad","ae","af","b0","b1","b2","b3","b4","b5","b6","b7","b8","b9","ba","bb","bc","bd","be","bf","c0","c1","c2","c3","c4","c5","c6","c7","c8","c9","ca","cb","cc","cd","ce","cf","d0","d1","d2","d3","d4","d5","d6","d7","d8","d9","da","db","dc","dd","de","df","e0","e1","e2","e3","e4","e5","e6","e7","e8","e9","ea","eb","ec","ed","ee","ef","f0","f1","f2","f3","f4","f5","f6","f7","f8","f9","fa","fb","fc","fd","fe","ff"];let ne=1234567;const ie=Math.PI/180,re=180/Math.PI;function se(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,i=4294967295*Math.random()|0;return(ee[255&t]+ee[t>>8&255]+ee[t>>16&255]+ee[t>>24&255]+"-"+ee[255&e]+ee[e>>8&255]+"-"+ee[e>>16&15|64]+ee[e>>24&255]+"-"+ee[63&n|128]+ee[n>>8&255]+"-"+ee[n>>16&255]+ee[n>>24&255]+ee[255&i]+ee[i>>8&255]+ee[i>>16&255]+ee[i>>24&255]).toLowerCase()}function ae(t,e,n){return Math.max(e,Math.min(n,t))}function oe(t,e){return(t%e+e)%e}function le(t,e,n){return(1-n)*t+n*e}function ce(t){return 0==(t&t-1)&&0!==t}function he(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}function ue(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return t/4294967295;case Uint16Array:return t/65535;case Uint8Array:return t/255;case Int32Array:return Math.max(t/2147483647,-1);case Int16Array:return Math.max(t/32767,-1);case Int8Array:return Math.max(t/127,-1);default:throw new Error("Invalid component type.")}}function de(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return Math.round(4294967295*t);case Uint16Array:return Math.round(65535*t);case Uint8Array:return Math.round(255*t);case Int32Array:return Math.round(2147483647*t);case Int16Array:return Math.round(32767*t);case Int8Array:return Math.round(127*t);default:throw new Error("Invalid component type.")}}const pe={DEG2RAD:ie,RAD2DEG:re,generateUUID:se,clamp:ae,euclideanModulo:oe,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:le,damp:function(t,e,n,i){return le(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(oe(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatLine truncated
|
||||||
Reference in new issue
Block a user