fix(sagi): anchor mood animations to the mascot, and let the bubble follow it
The seven per-mood `transform-origin: 640px 720px` rules were written in
mascot.svg's 1280-unit space but applied to the root <svg>, where CSS
resolves a length against the 68px CSS box. The pivot landed ~9x the
element away, so `scale(1.035)` ("gentle breathing") swung the whole
mascot ~20px up-left every 4s - and the speech bubble, positioned from
the untransformed box, stayed put while Sagi visibly moved.
- One `transform-origin: 50% 92%` on `.sagi-avatar` (feet pivot); a
percentage so it cannot be misresolved against either reference box.
- Eye-blink and sparkle origins rebased onto `transform-box: view-box`,
whose 0,0 is the viewBox min (253.5 106) - the eyelids were closing
off-target and the sparkle twinkled around a point outside itself.
- `sagi-rock` returns to 0deg; it used to park `thinking` at a
permanent -6deg tilt.
- `.sagi-flyout` gets the avatar button's exact left/top easing so the
bubble glides with the snap instead of teleporting, and the bubble no
longer unmounts mid-drag (the panel still does - dragging a 320px
panel around is jank).
- Reduced motion now also covers `.sagi-bubble-enter` and both
transitions; dropped a `~ *` selector that never matched.
This commit is contained in:
@@ -90,6 +90,7 @@ interface Anchor {
|
||||
size: number;
|
||||
side: "left" | "right";
|
||||
openUp: boolean;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
function usePrefersReducedMotion(): boolean {
|
||||
@@ -154,7 +155,14 @@ function SagiFlyout({ anchor, children }: { anchor: Anchor; children: ReactNode
|
||||
}, [place]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="sagi-flyout" style={style}>
|
||||
<div
|
||||
ref={ref}
|
||||
className="sagi-flyout"
|
||||
// Mirrors the avatar button: eased left/top so the flyout glides along
|
||||
// with the mascot when it snaps to an edge, 1:1 (no easing) mid-drag.
|
||||
data-dragging={anchor.dragging ? "1" : "0"}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -235,11 +243,13 @@ export function Sagi() {
|
||||
size: place.size,
|
||||
side: place.side,
|
||||
openUp: place.openUp,
|
||||
dragging: place.dragging,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Flyouts are hidden while dragging so they don't chase the mascot. */}
|
||||
{/* The panel is too big to drag around, so it hides mid-drag. The bubble
|
||||
does not - it is Sagi talking, and it has to stay by its mouth. */}
|
||||
{!place.dragging && open && (
|
||||
<SagiFlyout anchor={anchor}>
|
||||
<SagiPanel
|
||||
@@ -254,7 +264,7 @@ export function Sagi() {
|
||||
</SagiFlyout>
|
||||
)}
|
||||
|
||||
{!place.dragging && !open && brain.bubble && (
|
||||
{!open && brain.bubble && (
|
||||
<SagiFlyout anchor={anchor}>
|
||||
<SpeechBubble text={brain.bubble} onDismiss={brain.dismissBubble} />
|
||||
</SagiFlyout>
|
||||
|
||||
Reference in New Issue
Block a user