diff --git a/Ralph/prd.json b/Ralph/prd.json index b56a606..c9fddc4 100644 --- a/Ralph/prd.json +++ b/Ralph/prd.json @@ -81,7 +81,7 @@ "Verify in browser using dev-browser skill" ], "priority": 5, - "passes": false, + "passes": true, "notes": "Use framer-motion's useTransform or a progress-based approach to derive blend opacity from fan animation progress. The pill elements are groups inside the SVG. Apply mixBlendMode: 'multiply' as a style and animate the group's opacity using the timing constants from US-002. The blend should only be visible during/after the fan phase, not during the rise phase." }, { diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 93dad45..265da2c 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -109,3 +109,17 @@ - Title and subtitle are `` elements inside the `.flex.flex-col.items-center` branding container - Weight hierarchy (600 title, 400 subtitle) provides sufficient visual differentiation without needing size contrast as large --- + +## 2026-02-15 - US-005: Add overlap blend effect on fanning capsules +- Added `blendActive` state to CvmisLogo, triggered by timer at `blendStartMs` (50% through fan animation) +- Added two blend overlay `` elements after the main pills: copies of left/right pill shapes with `mixBlendMode: 'multiply'` and opacity transitioning from 0 to 0.2 +- Blend overlays share the same `transform` and `transition` as their corresponding original pills, plus an opacity transition using `OVERLAP_BLEND_TRANSITION_DURATION_S` +- Reduced motion: `blendActive` starts `true`, `transition: 'none'` — final blend state shown immediately +- Browser-verified: blend darkening visible at pill overlap areas, opacity confirmed at 0.2 +- Files changed: `src/components/CvmisLogo.tsx` +- **Learnings for future iterations:** + - `mix-blend-mode` is not CSS-animatable — use overlay elements with animated opacity instead of trying to transition the blend mode + - Blend overlay approach: duplicate the pill shapes (rect only, no icons) as separate `` elements with `mixBlendMode: 'multiply'` and low opacity + - The `useMemo` for `blendStartMs` avoids recalculation — all timing constants are module-level so this is stable + - Combined CSS transition strings work in SVG `` style: `transform 0.6s cubic-bezier(...), opacity 0.3s ease-out` +--- diff --git a/src/components/CvmisLogo.tsx b/src/components/CvmisLogo.tsx index 04d8079..d091acd 100644 --- a/src/components/CvmisLogo.tsx +++ b/src/components/CvmisLogo.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useMemo } from 'react' import { motion, useReducedMotion } from 'framer-motion' interface CvmisLogoProps { @@ -53,18 +53,27 @@ export function CvmisLogo({ size, cssHeight, animated = false, className }: Cvmi const [phase, setPhase] = useState<'rising' | 'fanning' | 'done'>( animated && !prefersReducedMotion ? 'rising' : 'done' ) + const [blendActive, setBlendActive] = useState(!animated || !!prefersReducedMotion) + + // Blend starts at OVERLAY_BLEND_START_PROGRESS through the fan animation + const blendStartMs = useMemo( + () => FAN_DELAY_AFTER_RISE_MS + FAN_DURATION_S * 1000 * OVERLAY_BLEND_START_PROGRESS, + [] + ) useEffect(() => { if (!animated || prefersReducedMotion) return const fanTimer = setTimeout(() => setPhase('fanning'), FAN_DELAY_AFTER_RISE_MS) const doneTimer = setTimeout(() => setPhase('done'), TOTAL_ANIMATION_MS) + const blendTimer = setTimeout(() => setBlendActive(true), blendStartMs) return () => { clearTimeout(fanTimer) clearTimeout(doneTimer) + clearTimeout(blendTimer) } - }, [animated, prefersReducedMotion]) + }, [animated, prefersReducedMotion, blendStartMs]) const skip = !animated || prefersReducedMotion const isFanned = phase === 'fanning' || phase === 'done' @@ -149,6 +158,32 @@ export function CvmisLogo({ size, cssHeight, animated = false, className }: Cvmi /> + + {/* Blend overlays — multiply-blend copies of fanning pills for overlap darkening */} + + + + + + + + + + )