Compare commits

..

4 Commits

Author SHA1 Message Date
Cursor Agent
7afa5af3ff
ci: retrigger after flaky chart-links E2E
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 16:08:43 +00:00
Cursor Agent
9b7632613d
Align mobile Expo packages for install --check in CI
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 15:11:51 +00:00
Cursor Agent
54728e52c9
Format brand-lock spacing test for Biome
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 14:12:59 +00:00
Cursor Agent
71613064ea
Align Oo avatar eye spacing with evoo-logo overlap ratio
Derive small-eye placement from evoo-logo.svg center spacing (~0.94 of
r_big + r_small) for the shared rig, EvooMascot brand lock, and face crops.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-10-06 14:12:59 +00:00
11 changed files with 505 additions and 289 deletions

View File

@ -3,6 +3,7 @@ import {
buildOoRig,
createOoLife,
OO_LIST_MOMENTS,
OO_RIG_FACE_VIEWBOX,
OO_RIG_INSET,
OO_RIG_SIZE,
OO_STILL_TIME,
@ -26,7 +27,7 @@ import Svg, {
} from "react-native-svg";
const VIEWBOX = `${OO_RIG_INSET} ${OO_RIG_INSET + 4} ${OO_RIG_SIZE - 2 * OO_RIG_INSET} ${OO_RIG_SIZE - 2 * OO_RIG_INSET}`;
const FACE_VIEWBOX = "47 62 112 112";
const FACE_VIEWBOX = OO_RIG_FACE_VIEWBOX;
/** Native frames redraw the SVG tree, so they run at half the display rate. */
const FRAME_SECONDS = 1 / 30;
/** Above this size an Oo always moves; smaller ones only for list moments. */

View File

@ -18,34 +18,34 @@
},
"dependencies": {
"@expo/metro-runtime": "57.0.16",
"@expo/ui": "57.0.21",
"@expo/ui": "57.0.22",
"@rakazo/chat-ui": "workspace:*",
"@rakazo/contracts": "workspace:*",
"@rakazo/core": "workspace:*",
"@rakazo/ui-tokens": "workspace:*",
"@react-native-vector-icons/ionicons": "13.1.4",
"@react-native-vector-icons/material-design-icons": "13.1.4",
"expo": "57.0.26",
"expo": "57.0.27",
"expo-application": "57.0.3",
"expo-audio": "57.0.5",
"expo-clipboard": "57.0.2",
"expo-constants": "57.0.20",
"expo-constants": "57.0.21",
"expo-document-picker": "57.0.3",
"expo-file-system": "57.0.7",
"expo-font": "57.0.4",
"expo-image-picker": "57.0.20",
"expo-keep-awake": "57.0.2",
"expo-linking": "57.0.11",
"expo-linking": "57.0.12",
"expo-localization": "57.0.2",
"expo-modules-core": "57.0.20",
"expo-notifications": "57.0.21",
"expo-router": "57.0.24",
"expo-modules-core": "57.0.21",
"expo-notifications": "57.0.22",
"expo-router": "57.0.25",
"expo-secure-store": "57.0.4",
"expo-sharing": "57.0.22",
"expo-splash-screen": "57.0.9",
"expo-status-bar": "57.0.1",
"expo-symbols": "57.0.3",
"expo-updates": "57.0.24",
"expo-updates": "57.0.25",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.86.3",

View File

@ -7,6 +7,7 @@ import {
normalizeOoLook,
OO_AVATAR_MAX_LENGTH,
OO_EYE_ASYMMETRY_MIN_RATIO,
OO_EYE_CENTER_SPACING_RATIO,
OO_RIG_EYES,
type OoAvatar,
ooAvatarColor,
@ -131,4 +132,12 @@ describe("oo rig brand lock", () => {
expect(ratio).toBeGreaterThanOrEqual(OO_EYE_ASYMMETRY_MIN_RATIO);
expect(ooLensRadius("big")).toBeGreaterThan(OO_RIG_EYES.big.r);
});
it("matches evoo-logo eye spacing (centers close / slight overlap)", () => {
const { big, small } = OO_RIG_EYES;
const centerDistance = small.cx - big.cx;
const sumRadii = big.r + small.r;
expect(centerDistance / sumRadii).toBeCloseTo(OO_EYE_CENTER_SPACING_RATIO, 3);
expect(centerDistance / sumRadii).toBeLessThan(1);
});
});

View File

@ -257,11 +257,46 @@ export function ooAvatarColor(value: string): string {
* pair never moves when the body changes. The small eye is the big one divided by φ.
*/
export const OO_GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2;
/** Sclera center distance ÷ sum of radii in `apps/web/public/evoo-logo.svg` (slight overlap). */
export const OO_EYE_CENTER_SPACING_RATIO = 55 / (34 + 24.5);
/** Small-eye vertical drop as a fraction of the big sclera radius (same logo). */
export const OO_EYE_SMALL_VERTICAL_OFFSET_RATIO = (60 - 52) / 34;
/** Place the small eye from the big anchor using the logo spacing ratio. */
export function ooEyeCentersFromLogoRatio(
big: Readonly<{ cx: number; cy: number; r: number }>,
smallR: number,
): { cx: number; cy: number } {
const centerDistance = OO_EYE_CENTER_SPACING_RATIO * (big.r + smallR);
return {
cx: big.cx + centerDistance,
cy: big.cy + OO_EYE_SMALL_VERTICAL_OFFSET_RATIO * big.r,
};
}
const OO_RIG_EYE_BIG = { cx: 80, cy: 108, r: 21 } as const;
const OO_RIG_EYE_SMALL_R = OO_RIG_EYE_BIG.r / OO_GOLDEN_RATIO;
const OO_RIG_EYE_SMALL_CENTER = ooEyeCentersFromLogoRatio(OO_RIG_EYE_BIG, OO_RIG_EYE_SMALL_R);
export const OO_RIG_EYES = {
big: { cx: 80, cy: 108, r: 21 },
small: { cx: 126, cy: 111, r: 21 / OO_GOLDEN_RATIO },
big: OO_RIG_EYE_BIG,
small: { ...OO_RIG_EYE_SMALL_CENTER, r: OO_RIG_EYE_SMALL_R },
} as const;
const OO_RIG_FACE_PAD = { side: 12, above: 25, below: 30 } as const;
/** Face crop for eyewear / eye-style pickers; keeps both sclerae in frame at every size. */
export const OO_RIG_FACE_VIEWBOX = (() => {
const { big, small } = OO_RIG_EYES;
const x = Math.floor(Math.min(big.cx - big.r, small.cx - small.r) - OO_RIG_FACE_PAD.side);
const y = Math.floor(big.cy - big.r - OO_RIG_FACE_PAD.above);
const right = Math.ceil(Math.max(big.cx + big.r, small.cx + small.r) + OO_RIG_FACE_PAD.side);
const bottom = Math.ceil(Math.max(big.cy + big.r, small.cy + small.r) + OO_RIG_FACE_PAD.below);
return `${x} ${y} ${right - x} ${bottom - y}`;
})();
/** Minimum big÷small ratio for eyes, lenses and closed-eye arcs (Oo trademark). */
export const OO_EYE_ASYMMETRY_MIN_RATIO = 1.45;

View File

@ -683,9 +683,9 @@ function cheeks(avatar: OoAvatar): OoShape[] {
[58, 129, 1.6],
[64, 133, 1.6],
[69, 128, 1.6],
[141, 131, 1.4],
[146, 135, 1.4],
[150, 130, 1.4],
[127, 131, 1.4],
[132, 135, 1.4],
[136, 130, 1.4],
];
return dots.map(([cx, cy, r]) => ({
type: "circle",
@ -699,7 +699,7 @@ function cheeks(avatar: OoAvatar): OoShape[] {
const fill = cheek === "peach" ? "#FFB08A" : PINK;
return [
{ type: "ellipse", cx: 62, cy: 131, rx: 9.5, ry: 5.5, fill, opacity: 0.55 },
{ type: "ellipse", cx: 144, cy: 133, rx: 7.5, ry: 4.5, fill, opacity: 0.55 },
{ type: "ellipse", cx: 130, cy: 133, rx: 7.5, ry: 4.5, fill, opacity: 0.55 },
];
}

View File

@ -10,6 +10,7 @@ All Oo's — `EvooMascot`, `BotAvatar`, Wordmark, every pose — must stay insta
1. **Consistent size asymmetry** between the two eyes (big O vs small o). Never symmetrize. Never equal radii.
2. **Kulleraugen** — big, round, glossy googly/cute sclera with dark pupils and catchlights.
3. **Logo eye spacing** — sclera centers follow `evoo-logo.svg` (`OO_EYE_CENTER_SPACING_RATIO` ≈ 0.94: close together, slight overlap).
Personality (mouth, hands, accessories, poses) builds **on top** of this signature. Never replace with Grok-bot-style emoji faces.
@ -18,8 +19,9 @@ Personality (mouth, hands, accessories, poses) builds **on top** of this signatu
| `EVOO_EYE_BIG_R` | 30 | Capital O sclera |
| `EVOO_EYE_SMALL_R` | 18.54 | Lowercase o sclera (~φ⁻¹ of big) |
| `EVOO_EYE_ASYMMETRY_MIN_RATIO` | 1.45 | CI guard — catches accidental symmetrization |
| `OO_EYE_CENTER_SPACING_RATIO` | ~0.940 | Center distance ÷ (r_big + r_small) from `evoo-logo.svg` |
Enforced in `evoo-character/brand-lock.ts` + `brand-lock.test.ts`. Poses may adjust `eyeOpen` (squint), `gaze`, brows — **not** eye size parity.
Bot avatars use the same ratio via `OO_RIG_EYES` in `packages/core/src/oo-avatar.ts`. Enforced in `evoo-character/brand-lock.ts`, `oo-avatar.test.ts`, and `brand-lock.test.ts`. Poses may adjust `eyeOpen` (squint), `gaze`, brows — **not** eye size parity or spacing drift.
---

View File

@ -1,8 +1,16 @@
import {
EVOO_EYE_BIG_CX,
EVOO_EYE_BIG_CY,
EVOO_EYE_BIG_R,
EVOO_EYE_SMALL_CX,
EVOO_EYE_SMALL_CY,
EVOO_EYE_SMALL_R,
EVOO_OO_TRADEMARK,
EVOO_PUPIL_BIG_CX,
EVOO_PUPIL_BIG_CY,
EVOO_PUPIL_BIG_R,
EVOO_PUPIL_SMALL_CX,
EVOO_PUPIL_SMALL_CY,
EVOO_PUPIL_SMALL_R,
} from "./brand-lock.js";
import { EVOO_CHARACTER_SPRING, EVOO_POSE_TRANSITION_S } from "./motion-tokens.js";
@ -11,9 +19,17 @@ import { POSE_DEFINITIONS } from "./poses.js";
import type { EvooMouthStyle, EvooPose } from "./types.js";
export {
EVOO_EYE_BIG_CX,
EVOO_EYE_BIG_CY,
EVOO_EYE_BIG_R,
EVOO_EYE_SMALL_CX,
EVOO_EYE_SMALL_CY,
EVOO_EYE_SMALL_R,
EVOO_PUPIL_BIG_CX,
EVOO_PUPIL_BIG_CY,
EVOO_PUPIL_BIG_R,
EVOO_PUPIL_SMALL_CX,
EVOO_PUPIL_SMALL_CY,
EVOO_PUPIL_SMALL_R,
} from "./brand-lock.js";
@ -66,8 +82,8 @@ function Brow({
const worried = raise === "worried";
if (side === "left" && !showLeft) return null;
if (side === "right" && !showRight) return null;
const cx = side === "left" ? 44 : 88;
const cy = side === "left" ? 28 : 32;
const cx = side === "left" ? EVOO_EYE_BIG_CX : EVOO_EYE_SMALL_CX;
const cy = side === "left" ? 28 : EVOO_EYE_SMALL_CY - 28;
const tilt = side === "left" ? (worried ? 8 : -10) : worried ? -8 : 10;
return (
<path
@ -108,9 +124,29 @@ function Accessory({
case "glasses":
return (
<g opacity="0.85">
<ellipse cx="44" cy="54" rx="22" ry="20" fill="none" stroke="#0F172A" strokeWidth="1.8" />
<ellipse cx="88" cy="60" rx="14" ry="13" fill="none" stroke="#0F172A" strokeWidth="1.6" />
<path d="M66 54 L66 58" stroke="#0F172A" strokeWidth="1.6" />
<ellipse
cx={EVOO_EYE_BIG_CX}
cy={EVOO_EYE_BIG_CY}
rx="22"
ry="20"
fill="none"
stroke="#0F172A"
strokeWidth="1.8"
/>
<ellipse
cx={EVOO_EYE_SMALL_CX}
cy={EVOO_EYE_SMALL_CY}
rx="14"
ry="13"
fill="none"
stroke="#0F172A"
strokeWidth="1.6"
/>
<path
d={`M${((EVOO_EYE_BIG_CX + EVOO_EYE_SMALL_CX) / 2).toFixed(1)} ${EVOO_EYE_BIG_CY} L${((EVOO_EYE_BIG_CX + EVOO_EYE_SMALL_CX) / 2).toFixed(1)} ${EVOO_EYE_BIG_CY + 4}`}
stroke="#0F172A"
strokeWidth="1.6"
/>
</g>
);
case "clipboard":
@ -580,7 +616,7 @@ export function EvooCharacterSvg({
className="evoo-eye-big"
data-evoo-brand={EVOO_OO_TRADEMARK}
style={{
transformOrigin: "44px 54px",
transformOrigin: `${EVOO_EYE_BIG_CX}px ${EVOO_EYE_BIG_CY}px`,
animation:
def.bodyAnimation === "evoo-executive-nod"
? "evoo-executive-nod 2s ease-in-out infinite"
@ -588,8 +624,8 @@ export function EvooCharacterSvg({
}}
>
<ellipse
cx="44"
cy="54"
cx={EVOO_EYE_BIG_CX}
cy={EVOO_EYE_BIG_CY}
rx={EVOO_EYE_BIG_R}
ry={EVOO_EYE_BIG_R}
fill="#FFFFFF"
@ -598,14 +634,14 @@ export function EvooCharacterSvg({
strokeOpacity="0.12"
style={{
filter: "drop-shadow(0 2px 6px rgba(0,0,0,0.22))",
transformOrigin: "44px 54px",
transformOrigin: `${EVOO_EYE_BIG_CX}px ${EVOO_EYE_BIG_CY}px`,
transform: isBlinking ? "scaleY(0.08)" : `scaleY(${eyeOpen})`,
transition: reducedMotion ? "none" : `transform ${transition}`,
}}
/>
<ellipse
cx="38"
cy="44"
cx={EVOO_EYE_BIG_CX - 6}
cy={EVOO_EYE_BIG_CY - 10}
rx={EVOO_EYE_BIG_R * 0.38}
ry={EVOO_EYE_BIG_R * 0.24}
fill="#FFFFFF"
@ -620,8 +656,8 @@ export function EvooCharacterSvg({
}}
>
<ellipse
cx="48"
cy="57"
cx={EVOO_PUPIL_BIG_CX}
cy={EVOO_PUPIL_BIG_CY}
rx={EVOO_PUPIL_BIG_R + 2.2}
ry={EVOO_PUPIL_BIG_R + 2.2}
fill="none"
@ -629,21 +665,40 @@ export function EvooCharacterSvg({
strokeWidth="0.9"
opacity="0.35"
/>
<ellipse cx="48" cy="57" rx={EVOO_PUPIL_BIG_R} ry={EVOO_PUPIL_BIG_R} fill="#0A0F1D" />
<ellipse
cx="46"
cy="59"
cx={EVOO_PUPIL_BIG_CX}
cy={EVOO_PUPIL_BIG_CY}
rx={EVOO_PUPIL_BIG_R}
ry={EVOO_PUPIL_BIG_R}
fill="#0A0F1D"
/>
<ellipse
cx={EVOO_PUPIL_BIG_CX - 2}
cy={EVOO_PUPIL_BIG_CY + 2}
rx={EVOO_PUPIL_BIG_R * 0.42}
ry={EVOO_PUPIL_BIG_R * 0.28}
fill="#1E3A5F"
opacity="0.55"
/>
<circle cx="53" cy="49" r="4.6" fill="#FFFFFF" />
<circle cx="42" cy="63" r="2.2" fill="#FFFFFF" opacity="0.85" />
<ellipse cx="50" cy="51" rx="1.8" ry="1.1" fill="#FFFFFF" opacity="0.92" />
<circle cx={EVOO_PUPIL_BIG_CX + 5} cy={EVOO_PUPIL_BIG_CY - 8} r="4.6" fill="#FFFFFF" />
<circle
cx={EVOO_PUPIL_BIG_CX - 6}
cy={EVOO_PUPIL_BIG_CY + 6}
r="2.2"
fill="#FFFFFF"
opacity="0.85"
/>
<ellipse
cx={EVOO_PUPIL_BIG_CX + 2}
cy={EVOO_PUPIL_BIG_CY - 6}
rx="1.8"
ry="1.1"
fill="#FFFFFF"
opacity="0.92"
/>
{(def.eyeSparkle || pose === "success") && (
<path
d="M48 45 L50 49 L54 49 L51 52 L52 56 L48 53 L44 56 L45 52 L42 49 L46 49 Z"
d={`M${EVOO_PUPIL_BIG_CX} ${EVOO_PUPIL_BIG_CY - 12} L${EVOO_PUPIL_BIG_CX + 2} ${EVOO_PUPIL_BIG_CY - 8} L${EVOO_PUPIL_BIG_CX + 6} ${EVOO_PUPIL_BIG_CY - 8} L${EVOO_PUPIL_BIG_CX + 3} ${EVOO_PUPIL_BIG_CY - 5} L${EVOO_PUPIL_BIG_CX + 4} ${EVOO_PUPIL_BIG_CY - 1} L${EVOO_PUPIL_BIG_CX} ${EVOO_PUPIL_BIG_CY - 4} L${EVOO_PUPIL_BIG_CX - 4} ${EVOO_PUPIL_BIG_CY - 1} L${EVOO_PUPIL_BIG_CX - 3} ${EVOO_PUPIL_BIG_CY - 5} L${EVOO_PUPIL_BIG_CX - 6} ${EVOO_PUPIL_BIG_CY - 8} L${EVOO_PUPIL_BIG_CX - 2} ${EVOO_PUPIL_BIG_CY - 8} Z`}
fill="#10B981"
opacity="0.95"
/>
@ -656,7 +711,7 @@ export function EvooCharacterSvg({
className="evoo-eye-small"
data-evoo-brand={EVOO_OO_TRADEMARK}
style={{
transformOrigin: "88px 60px",
transformOrigin: `${EVOO_EYE_SMALL_CX}px ${EVOO_EYE_SMALL_CY}px`,
animation:
def.bodyAnimation === "evoo-executive-nod"
? "evoo-executive-nod 2s ease-in-out infinite"
@ -664,8 +719,8 @@ export function EvooCharacterSvg({
}}
>
<ellipse
cx="88"
cy="60"
cx={EVOO_EYE_SMALL_CX}
cy={EVOO_EYE_SMALL_CY}
rx={EVOO_EYE_SMALL_R}
ry={EVOO_EYE_SMALL_R}
fill="#FFFFFF"
@ -674,14 +729,14 @@ export function EvooCharacterSvg({
strokeOpacity="0.12"
style={{
filter: "drop-shadow(0 2px 5px rgba(0,0,0,0.18))",
transformOrigin: "88px 60px",
transformOrigin: `${EVOO_EYE_SMALL_CX}px ${EVOO_EYE_SMALL_CY}px`,
transform: isSmallBlinking || isWinking ? "scaleY(0.08)" : `scaleY(${eyeOpen})`,
transition: reducedMotion ? "none" : `transform ${transition}`,
}}
/>
<ellipse
cx="84.2"
cy="52.5"
cx={EVOO_EYE_SMALL_CX - 3.8}
cy={EVOO_EYE_SMALL_CY - 7.5}
rx={EVOO_EYE_SMALL_R * 0.38}
ry={EVOO_EYE_SMALL_R * 0.24}
fill="#FFFFFF"
@ -696,8 +751,8 @@ export function EvooCharacterSvg({
}}
>
<ellipse
cx="90"
cy="62"
cx={EVOO_PUPIL_SMALL_CX}
cy={EVOO_PUPIL_SMALL_CY}
rx={EVOO_PUPIL_SMALL_R + 1.4}
ry={EVOO_PUPIL_SMALL_R + 1.4}
fill="none"
@ -706,23 +761,41 @@ export function EvooCharacterSvg({
opacity="0.3"
/>
<ellipse
cx="90"
cy="62"
cx={EVOO_PUPIL_SMALL_CX}
cy={EVOO_PUPIL_SMALL_CY}
rx={EVOO_PUPIL_SMALL_R}
ry={EVOO_PUPIL_SMALL_R}
fill="#0A0F1D"
/>
<ellipse
cx="88.8"
cy="63.2"
cx={EVOO_PUPIL_SMALL_CX - 1.2}
cy={EVOO_PUPIL_SMALL_CY + 1.2}
rx={EVOO_PUPIL_SMALL_R * 0.4}
ry={EVOO_PUPIL_SMALL_R * 0.26}
fill="#1E3A5F"
opacity="0.5"
/>
<circle cx="93.2" cy="57" r="2.8" fill="#FFFFFF" />
<circle cx="86.8" cy="65.5" r="1.3" fill="#FFFFFF" opacity="0.85" />
<ellipse cx="91.4" cy="58.2" rx="1.1" ry="0.7" fill="#FFFFFF" opacity="0.88" />
<circle
cx={EVOO_PUPIL_SMALL_CX + 3.2}
cy={EVOO_PUPIL_SMALL_CY - 5}
r="2.8"
fill="#FFFFFF"
/>
<circle
cx={EVOO_PUPIL_SMALL_CX - 3.2}
cy={EVOO_PUPIL_SMALL_CY + 3.5}
r="1.3"
fill="#FFFFFF"
opacity="0.85"
/>
<ellipse
cx={EVOO_PUPIL_SMALL_CX + 1.4}
cy={EVOO_PUPIL_SMALL_CY - 3.8}
rx="1.1"
ry="0.7"
fill="#FFFFFF"
opacity="0.88"
/>
</g>
)}
</g>

View File

@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest";
import {
assertEvooEyeBrandLock,
EVOO_EYE_ASYMMETRY_MIN_RATIO,
EVOO_EYE_BIG_CX,
EVOO_EYE_BIG_CY,
EVOO_EYE_BIG_R,
EVOO_EYE_CENTER_SPACING_RATIO,
EVOO_EYE_SMALL_CX,
EVOO_EYE_SMALL_CY,
EVOO_EYE_SMALL_R,
EVOO_OO_TRADEMARK,
EVOO_PUPIL_BIG_R,
@ -25,6 +30,13 @@ describe("evOo Oo brand lock", () => {
expect(EVOO_PUPIL_BIG_R / EVOO_PUPIL_SMALL_R).toBeCloseTo(EVOO_EYE_BIG_R / EVOO_EYE_SMALL_R, 1);
});
it("places mascot eyes at the logo spacing ratio", () => {
const centerDistance = EVOO_EYE_SMALL_CX - EVOO_EYE_BIG_CX;
const sumRadii = EVOO_EYE_BIG_R + EVOO_EYE_SMALL_R;
expect(centerDistance / sumRadii).toBeCloseTo(EVOO_EYE_CENTER_SPACING_RATIO, 3);
expect(EVOO_EYE_SMALL_CY).toBeCloseTo(EVOO_EYE_BIG_CY + ((60 - 52) / 34) * EVOO_EYE_BIG_R, 1);
});
it("rejects symmetrized eye radii", () => {
expect(() => assertEvooEyeBrandLock(20, 20)).toThrow(/asymmetric Kulleraugen/);
expect(() => assertEvooEyeBrandLock(20, 19)).toThrow(/asymmetry ratio/);

View File

@ -1,4 +1,8 @@
import { OO_EYE_ASYMMETRY_MIN_RATIO } from "@rakazo/core";
import {
OO_EYE_ASYMMETRY_MIN_RATIO,
OO_EYE_CENTER_SPACING_RATIO,
ooEyeCentersFromLogoRatio,
} from "@rakazo/core";
/**
* evOo brand lock — non-negotiable (Ingo).
@ -8,6 +12,7 @@ import { OO_EYE_ASYMMETRY_MIN_RATIO } from "@rakazo/core";
* All mascot surfaces (EvooMascot, BotAvatar, Wordmark) MUST keep:
* 1. Consistent size asymmetry between the two eyes (big O vs small o).
* 2. Kulleraugen — big, round, glossy googly/cute sclera + pupils.
* 3. Eye center spacing matched to `evoo-logo.svg` (close / slight overlap).
*
* Personality (mouth, hands, accessories, poses) builds ON TOP of this signature.
* Never symmetrize the eyes. Never use Grok-style emoji faces. Never equal eye radii.
@ -22,16 +27,39 @@ export const EVOO_EYE_BIG_R = 30;
/** Small eye (lowercase o) — ~φ⁻¹ of big eye; trademark size gap. */
export const EVOO_EYE_SMALL_R = 18.54;
/** Big sclera anchor in the mascot viewBox (capital O). */
export const EVOO_EYE_BIG_CX = 44;
export const EVOO_EYE_BIG_CY = 54;
const _mascotSmallCenter = ooEyeCentersFromLogoRatio(
{ cx: EVOO_EYE_BIG_CX, cy: EVOO_EYE_BIG_CY, r: EVOO_EYE_BIG_R },
EVOO_EYE_SMALL_R,
);
/** Small sclera anchor derived from the logo spacing ratio. */
export const EVOO_EYE_SMALL_CX = _mascotSmallCenter.cx;
export const EVOO_EYE_SMALL_CY = _mascotSmallCenter.cy;
/** Logo-matched center distance ÷ sum of sclera radii (re-export for docs/tests). */
export const EVOO_EYE_CENTER_SPACING_RATIO = OO_EYE_CENTER_SPACING_RATIO;
/** Pupil radii scale with sclera; golden-ratio proportion preserved. */
export const EVOO_PUPIL_BIG_R = 14.2;
export const EVOO_PUPIL_SMALL_R = EVOO_PUPIL_BIG_R * 0.618034;
/** Pupil anchors track sclera; offsets unchanged from the original mascot art. */
export const EVOO_PUPIL_BIG_CX = EVOO_EYE_BIG_CX + 4;
export const EVOO_PUPIL_BIG_CY = EVOO_EYE_BIG_CY + 3;
export const EVOO_PUPIL_SMALL_CX = EVOO_EYE_SMALL_CX + 0.37;
export const EVOO_PUPIL_SMALL_CY = EVOO_EYE_SMALL_CY + 0.94;
/** Minimum big÷small ratio — catches accidental symmetrization in tests/CI. */
export const EVOO_EYE_ASYMMETRY_MIN_RATIO = OO_EYE_ASYMMETRY_MIN_RATIO;
export const EVOO_BRAND_LOCK_RULES = [
"Two eyes only: one big O, one small o — never equal radii.",
"Kulleraugen: round white sclera, dark glossy pupil, primary + secondary catchlights.",
"Eye spacing follows evoo-logo.svg — centers closer than r_big + r_small (slight overlap).",
"Poses may change gaze, blink, squint (eyeOpen), brows, mouth, hands, accessories — not eye size parity.",
"No Grok-bot emoji face replacement; no symmetric sticker eyes.",
] as const;

View File

@ -3,6 +3,7 @@ import {
buildOoRig,
createOoLife,
OO_LIST_MOMENTS,
OO_RIG_FACE_VIEWBOX,
OO_RIG_INSET,
OO_RIG_SIZE,
OO_STILL_TIME,
@ -34,7 +35,7 @@ export interface OoFigureProps {
/** The body fills the avatar box; ears, hats and effects may reach past it. */
const OO_VIEWBOX = `${OO_RIG_INSET} ${OO_RIG_INSET + 4} ${OO_RIG_SIZE - 2 * OO_RIG_INSET} ${OO_RIG_SIZE - 2 * OO_RIG_INSET}`;
const OO_FACE_VIEWBOX = "47 62 112 112";
const OO_FACE_VIEWBOX = OO_RIG_FACE_VIEWBOX;
/** Above this size an Oo always moves; smaller ones only for list moments. */
const LIVELY_SIZE = 56;

527
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff