Companion: wire real Lottie assets — the always-sleeping cat
- LottiePlayer now auto-crops each asset to its content bbox (union getBBox across 6 frames -> square viewBox), so stock files with empty artboard padding fill the badge instead of floating tiny in the middle. - Wire sleeping-cat.json to every mood: she's always asleep in the corner yet still mumbles tips and cheers through the bubble. napping hardcoded true so she keeps the calm sway + drifting zzz even while "talking". - happy-dog.json parked in the folder as an awake-mascot alternate. - Enable resolveJsonModule for the JSON imports. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -14,6 +14,37 @@ interface Props {
|
||||
fallback: React.ReactNode
|
||||
}
|
||||
|
||||
// Tighten the SVG viewBox to the animation's actual drawn content so an asset
|
||||
// with a lot of empty artboard padding (common with stock Lottie files) fills
|
||||
// the badge instead of floating tiny in the middle. We union the content bbox
|
||||
// across a few frames (the mascot moves) and make it square so it isn't
|
||||
// distorted by the square container.
|
||||
function fitToContent(anim: AnimationItem, svg: SVGSVGElement) {
|
||||
try {
|
||||
const frames = anim.getDuration(true)
|
||||
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity
|
||||
const SAMPLES = 6
|
||||
for (let i = 0; i < SAMPLES; i++) {
|
||||
anim.goToAndStop(Math.floor(((frames - 1) * i) / (SAMPLES - 1)), true)
|
||||
const b = svg.getBBox()
|
||||
if (b.width === 0 || b.height === 0) continue
|
||||
x1 = Math.min(x1, b.x)
|
||||
y1 = Math.min(y1, b.y)
|
||||
x2 = Math.max(x2, b.x + b.width)
|
||||
y2 = Math.max(y2, b.y + b.height)
|
||||
}
|
||||
if (!isFinite(x1)) return
|
||||
const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2
|
||||
const side = Math.max(x2 - x1, y2 - y1) * 1.14 // ~7% breathing room each side
|
||||
svg.setAttribute('viewBox', `${cx - side / 2} ${cy - side / 2} ${side} ${side}`)
|
||||
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
||||
} catch {
|
||||
/* getBBox unsupported / not laid out — leave the original viewBox */
|
||||
} finally {
|
||||
anim.goToAndPlay(0, true)
|
||||
}
|
||||
}
|
||||
|
||||
// A thin wrapper over lottie-web (framework-agnostic, no React peer deps, fully
|
||||
// offline). Reloads the animation whenever the data changes — moods swap by
|
||||
// passing a different `animationData`.
|
||||
@@ -22,16 +53,22 @@ export function LottiePlayer({ animationData, loop = true, className, fallback }
|
||||
const anim = useRef<AnimationItem | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !animationData) return
|
||||
anim.current = lottie.loadAnimation({
|
||||
container: ref.current,
|
||||
const container = ref.current
|
||||
if (!container || !animationData) return
|
||||
const item = lottie.loadAnimation({
|
||||
container,
|
||||
renderer: 'svg',
|
||||
loop,
|
||||
autoplay: true,
|
||||
animationData,
|
||||
})
|
||||
anim.current = item
|
||||
item.addEventListener('DOMLoaded', () => {
|
||||
const svg = container.querySelector('svg')
|
||||
if (svg) fitToContent(item, svg as SVGSVGElement)
|
||||
})
|
||||
return () => {
|
||||
anim.current?.destroy()
|
||||
item.destroy()
|
||||
anim.current = null
|
||||
}
|
||||
}, [animationData, loop])
|
||||
|
||||
Reference in New Issue
Block a user