From 7c2992fdc82fbe743b5fce0e32bdd178074b4f02 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 20 Aug 2026 08:34:18 +0700 Subject: [PATCH] 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. --- ARCHITECTURE.md | 2 +- client/src/components/Sagi/SagiAvatar.tsx | 17 ++++++++++++++--- client/src/components/Sagi/useSagiPosition.ts | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9b56e7f..f6ad347 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. | | **`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. | | **`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. | diff --git a/client/src/components/Sagi/SagiAvatar.tsx b/client/src/components/Sagi/SagiAvatar.tsx index 5f527a8..d83c133 100644 --- a/client/src/components/Sagi/SagiAvatar.tsx +++ b/client/src/components/Sagi/SagiAvatar.tsx @@ -45,7 +45,18 @@ interface SagiAvatarProps { 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 // 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(null); const rafRef = useRef(); 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"} width={size} height={size} - viewBox="0 0 1280 1280" + viewBox={VIEW_BOX} role="img" aria-label={`Sagi (${mood})`} > diff --git a/client/src/components/Sagi/useSagiPosition.ts b/client/src/components/Sagi/useSagiPosition.ts index 53b5213..fdb6c3d 100644 --- a/client/src/components/Sagi/useSagiPosition.ts +++ b/client/src/components/Sagi/useSagiPosition.ts @@ -74,7 +74,7 @@ import { sagiPrefs, type SagiPos } from "./prefs"; import type { PointerEvent as ReactPointerEvent } from "react"; // 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; const DRAG_THRESHOLD = 5;