feat(companion): render Sagi 50% larger and crop the artwork's empty padding
SAGI_SIZE (the draggable button's real footprint) and SagiAvatar's default `size` both go 60 -> 90, and the viewBox drops the exporter's padded `0 0 1280 1280` canvas for a tight window on the art: the measured bbox over every path is x 331..1114 / y 173..1044, padded out to a square and raised to y 106 so the zzz / bang / sparkle mood overlays stay inside and the box keeps the 1:1 aspect of the square width/height. Path data is not rescaled, so MAX_PUPIL_SHIFT and the eye/mood transform-origins in sagi.css stay valid in the original 1280-unit space.
This commit is contained in:
+1
-1
@@ -2205,7 +2205,7 @@ The mood state machine in `deriveMood` resolves to a single expression using a f
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **`brain.ts`** | Pure, framework-free core. Exposes a `WSMessage` reducer (`reduceSagi`) and a mood state machine (`deriveMood`) with the priority order `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The clock is injected and there are zero side effects, so the brain is fully unit-tested in isolation. |
|
| **`brain.ts`** | Pure, framework-free core. Exposes a `WSMessage` reducer (`reduceSagi`) and a mood state machine (`deriveMood`) with the priority order `disconnected > worried > stuck > happy > thinking > watching > sleeping > idle`. The clock is injected and there are zero side effects, so the brain is fully unit-tested in isolation. |
|
||||||
| **`useSagiBrain.ts`** | The **only** consumer of the global `eventBus`. Wires the pure brain to real timers (idle / sleep / stuck), the speech-bubble queue, mute, and clear-alerts. Produces the derived `{ mood, status, bubble }` the presentational components render. |
|
| **`useSagiBrain.ts`** | The **only** consumer of the global `eventBus`. Wires the pure brain to real timers (idle / sleep / stuck), the speech-bubble queue, mute, and clear-alerts. Produces the derived `{ mood, status, bubble }` the presentational components render. |
|
||||||
| **`SagiAvatar.tsx`** | Pure presentational SVG mascot. The `data-mood` attribute drives CSS; pupils track the cursor. |
|
| **`SagiAvatar.tsx`** | Pure presentational SVG mascot. The `data-mood` attribute drives CSS; pupils track the cursor. Rendered at 90×90 px (`size` default, kept in sync with `SAGI_SIZE` in `useSagiPosition.ts`, which is the draggable button's real footprint) through a `viewBox` cropped to the artwork's measured bounding box instead of the exporter's padded `0 0 1280 1280` canvas. Path data is never rescaled, so every coordinate constant (`MAX_PUPIL_SHIFT`, the `transform-origin` values in `sagi.css`) stays in the original 1280-unit space. |
|
||||||
| **`SpeechBubble.tsx`** | Pure presentational speech bubble. |
|
| **`SpeechBubble.tsx`** | Pure presentational speech bubble. |
|
||||||
| **`SagiPanel.tsx`** | Pure presentational panel: status readout + quick actions + the Ask box. |
|
| **`SagiPanel.tsx`** | Pure presentational panel: status readout + quick actions + the Ask box. |
|
||||||
| **`Sagi.tsx`** | Shell component. Mounted once in `client/src/components/Layout.tsx` as a sibling of `UpdateNotifier`. Owns open/closed state, the `⌘B` / `Esc` shortcuts, reduced-motion detection, and navigation. |
|
| **`Sagi.tsx`** | Shell component. Mounted once in `client/src/components/Layout.tsx` as a sibling of `UpdateNotifier`. Owns open/closed state, the `⌘B` / `Esc` shortcuts, reduced-motion detection, and navigation. |
|
||||||
|
|||||||
@@ -45,7 +45,18 @@ interface SagiAvatarProps {
|
|||||||
size?: number;
|
size?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_PUPIL_SHIFT = 36; // px in the 0-1280 viewBox (scaled from the old 100x100 art)
|
const MAX_PUPIL_SHIFT = 36; // px in the 1280-unit path coordinate space
|
||||||
|
|
||||||
|
// Tight window on the artwork instead of the exporter's `0 0 1280 1280` canvas,
|
||||||
|
// which padded the mascot with ~330 units of empty space on the left and
|
||||||
|
// ~170-240 on the other three sides. Measured bbox over every path is
|
||||||
|
// x 331..1114 / y 173..1044; the top is raised to 106 and the width padded out
|
||||||
|
// to a square so the mood overlays (zzz / bang / sparkle, which sit above and
|
||||||
|
// to the right of the body) stay inside and the box keeps the 1:1 aspect of the
|
||||||
|
// square width/height below. Path data and every coordinate constant that
|
||||||
|
// references it (MAX_PUPIL_SHIFT above, the eye/mood transform-origins in
|
||||||
|
// sagi.css) are untouched — only the window moved.
|
||||||
|
const VIEW_BOX = "253.5 106 938 938";
|
||||||
|
|
||||||
// Module-level last-known cursor position, tracked from the moment this module
|
// Module-level last-known cursor position, tracked from the moment this module
|
||||||
// first loads (well before any avatar mounts). This is what makes eye tracking
|
// first loads (well before any avatar mounts). This is what makes eye tracking
|
||||||
@@ -65,7 +76,7 @@ if (typeof window !== "undefined") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SagiAvatar({ mood, reducedMotion, size = 60 }: SagiAvatarProps) {
|
export function SagiAvatar({ mood, reducedMotion, size = 90 }: SagiAvatarProps) {
|
||||||
const rootRef = useRef<SVGSVGElement | null>(null);
|
const rootRef = useRef<SVGSVGElement | null>(null);
|
||||||
const rafRef = useRef<number>();
|
const rafRef = useRef<number>();
|
||||||
const [pupil, setPupil] = useState({ x: 0, y: 0 });
|
const [pupil, setPupil] = useState({ x: 0, y: 0 });
|
||||||
@@ -129,7 +140,7 @@ export function SagiAvatar({ mood, reducedMotion, size = 60 }: SagiAvatarProps)
|
|||||||
data-reduced={reducedMotion ? "1" : "0"}
|
data-reduced={reducedMotion ? "1" : "0"}
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
viewBox="0 0 1280 1280"
|
viewBox={VIEW_BOX}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`Sagi (${mood})`}
|
aria-label={`Sagi (${mood})`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import { sagiPrefs, type SagiPos } from "./prefs";
|
|||||||
import type { PointerEvent as ReactPointerEvent } from "react";
|
import type { PointerEvent as ReactPointerEvent } from "react";
|
||||||
|
|
||||||
// Avatar footprint + edge gap, in px. SIZE matches SagiAvatar's default size.
|
// Avatar footprint + edge gap, in px. SIZE matches SagiAvatar's default size.
|
||||||
export const SAGI_SIZE = 60;
|
export const SAGI_SIZE = 90;
|
||||||
export const SAGI_MARGIN = 16;
|
export const SAGI_MARGIN = 16;
|
||||||
const DRAG_THRESHOLD = 5;
|
const DRAG_THRESHOLD = 5;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user