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:
2026-08-20 08:34:18 +07:00
parent ce2797b01f
commit 7c2992fdc8
3 changed files with 16 additions and 5 deletions
+14 -3
View File
@@ -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<SVGSVGElement | null>(null);
const rafRef = useRef<number>();
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})`}
>
@@ -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;