feat: US-005 - Add overlap blend effect on fanning capsules
This commit is contained in:
+1
-1
@@ -81,7 +81,7 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 5,
|
"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 <g> 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."
|
"notes": "Use framer-motion's useTransform or a progress-based approach to derive blend opacity from fan animation progress. The pill elements are <g> 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."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,3 +109,17 @@
|
|||||||
- Title and subtitle are `<span>` elements inside the `.flex.flex-col.items-center` branding container
|
- Title and subtitle are `<span>` 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
|
- 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 `<g>` 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 `<g>` 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 `<g>` style: `transform 0.6s cubic-bezier(...), opacity 0.3s ease-out`
|
||||||
|
---
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import { motion, useReducedMotion } from 'framer-motion'
|
import { motion, useReducedMotion } from 'framer-motion'
|
||||||
|
|
||||||
interface CvmisLogoProps {
|
interface CvmisLogoProps {
|
||||||
@@ -53,18 +53,27 @@ export function CvmisLogo({ size, cssHeight, animated = false, className }: Cvmi
|
|||||||
const [phase, setPhase] = useState<'rising' | 'fanning' | 'done'>(
|
const [phase, setPhase] = useState<'rising' | 'fanning' | 'done'>(
|
||||||
animated && !prefersReducedMotion ? 'rising' : '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(() => {
|
useEffect(() => {
|
||||||
if (!animated || prefersReducedMotion) return
|
if (!animated || prefersReducedMotion) return
|
||||||
|
|
||||||
const fanTimer = setTimeout(() => setPhase('fanning'), FAN_DELAY_AFTER_RISE_MS)
|
const fanTimer = setTimeout(() => setPhase('fanning'), FAN_DELAY_AFTER_RISE_MS)
|
||||||
const doneTimer = setTimeout(() => setPhase('done'), TOTAL_ANIMATION_MS)
|
const doneTimer = setTimeout(() => setPhase('done'), TOTAL_ANIMATION_MS)
|
||||||
|
const blendTimer = setTimeout(() => setBlendActive(true), blendStartMs)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(fanTimer)
|
clearTimeout(fanTimer)
|
||||||
clearTimeout(doneTimer)
|
clearTimeout(doneTimer)
|
||||||
|
clearTimeout(blendTimer)
|
||||||
}
|
}
|
||||||
}, [animated, prefersReducedMotion])
|
}, [animated, prefersReducedMotion, blendStartMs])
|
||||||
|
|
||||||
const skip = !animated || prefersReducedMotion
|
const skip = !animated || prefersReducedMotion
|
||||||
const isFanned = phase === 'fanning' || phase === 'done'
|
const isFanned = phase === 'fanning' || phase === 'done'
|
||||||
@@ -149,6 +158,32 @@ export function CvmisLogo({ size, cssHeight, animated = false, className }: Cvmi
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
{/* Blend overlays — multiply-blend copies of fanning pills for overlap darkening */}
|
||||||
|
<g
|
||||||
|
style={{
|
||||||
|
transform: leftTransform,
|
||||||
|
transition: skip ? 'none' : `${fanTransition}, opacity ${OVERLAP_BLEND_TRANSITION_DURATION_S}s ease-out`,
|
||||||
|
mixBlendMode: 'multiply',
|
||||||
|
opacity: blendActive ? OVERLAP_BLEND_MAX_OPACITY : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g transform="translate(250, 50)">
|
||||||
|
<rect width="100" height="225" rx="50" fill="#0E7A7D" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
style={{
|
||||||
|
transform: rightTransform,
|
||||||
|
transition: skip ? 'none' : `${fanTransitionDelayed}, opacity ${OVERLAP_BLEND_TRANSITION_DURATION_S}s ease-out`,
|
||||||
|
mixBlendMode: 'multiply',
|
||||||
|
opacity: blendActive ? OVERLAP_BLEND_MAX_OPACITY : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<g transform="translate(250, 50)">
|
||||||
|
<rect width="100" height="225" rx="50" fill="#109E6C" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</motion.g>
|
</motion.g>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user