Align Oo avatar eye spacing with evoo-logo (#173)
* 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> * Format brand-lock spacing test for Biome Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Align mobile Expo packages for install --check in CI Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * ci: retrigger after flaky chart-links E2E Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
f8b4ce4b6d
commit
ba89574c83
@ -3,6 +3,7 @@ import {
|
|||||||
buildOoRig,
|
buildOoRig,
|
||||||
createOoLife,
|
createOoLife,
|
||||||
OO_LIST_MOMENTS,
|
OO_LIST_MOMENTS,
|
||||||
|
OO_RIG_FACE_VIEWBOX,
|
||||||
OO_RIG_INSET,
|
OO_RIG_INSET,
|
||||||
OO_RIG_SIZE,
|
OO_RIG_SIZE,
|
||||||
OO_STILL_TIME,
|
OO_STILL_TIME,
|
||||||
@ -26,7 +27,7 @@ import Svg, {
|
|||||||
} from "react-native-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 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. */
|
/** Native frames redraw the SVG tree, so they run at half the display rate. */
|
||||||
const FRAME_SECONDS = 1 / 30;
|
const FRAME_SECONDS = 1 / 30;
|
||||||
/** Above this size an Oo always moves; smaller ones only for list moments. */
|
/** Above this size an Oo always moves; smaller ones only for list moments. */
|
||||||
|
|||||||
@ -18,34 +18,34 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "57.0.16",
|
"@expo/metro-runtime": "57.0.16",
|
||||||
"@expo/ui": "57.0.21",
|
"@expo/ui": "57.0.22",
|
||||||
"@rakazo/chat-ui": "workspace:*",
|
"@rakazo/chat-ui": "workspace:*",
|
||||||
"@rakazo/contracts": "workspace:*",
|
"@rakazo/contracts": "workspace:*",
|
||||||
"@rakazo/core": "workspace:*",
|
"@rakazo/core": "workspace:*",
|
||||||
"@rakazo/ui-tokens": "workspace:*",
|
"@rakazo/ui-tokens": "workspace:*",
|
||||||
"@react-native-vector-icons/ionicons": "13.1.4",
|
"@react-native-vector-icons/ionicons": "13.1.4",
|
||||||
"@react-native-vector-icons/material-design-icons": "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-application": "57.0.3",
|
||||||
"expo-audio": "57.0.5",
|
"expo-audio": "57.0.5",
|
||||||
"expo-clipboard": "57.0.2",
|
"expo-clipboard": "57.0.2",
|
||||||
"expo-constants": "57.0.20",
|
"expo-constants": "57.0.21",
|
||||||
"expo-document-picker": "57.0.3",
|
"expo-document-picker": "57.0.3",
|
||||||
"expo-file-system": "57.0.7",
|
"expo-file-system": "57.0.7",
|
||||||
"expo-font": "57.0.4",
|
"expo-font": "57.0.4",
|
||||||
"expo-image-picker": "57.0.20",
|
"expo-image-picker": "57.0.20",
|
||||||
"expo-keep-awake": "57.0.2",
|
"expo-keep-awake": "57.0.2",
|
||||||
"expo-linking": "57.0.11",
|
"expo-linking": "57.0.12",
|
||||||
"expo-localization": "57.0.2",
|
"expo-localization": "57.0.2",
|
||||||
"expo-modules-core": "57.0.20",
|
"expo-modules-core": "57.0.21",
|
||||||
"expo-notifications": "57.0.21",
|
"expo-notifications": "57.0.22",
|
||||||
"expo-router": "57.0.24",
|
"expo-router": "57.0.25",
|
||||||
"expo-secure-store": "57.0.4",
|
"expo-secure-store": "57.0.4",
|
||||||
"expo-sharing": "57.0.22",
|
"expo-sharing": "57.0.22",
|
||||||
"expo-splash-screen": "57.0.9",
|
"expo-splash-screen": "57.0.9",
|
||||||
"expo-status-bar": "57.0.1",
|
"expo-status-bar": "57.0.1",
|
||||||
"expo-symbols": "57.0.3",
|
"expo-symbols": "57.0.3",
|
||||||
"expo-updates": "57.0.24",
|
"expo-updates": "57.0.25",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"react-native": "0.86.3",
|
"react-native": "0.86.3",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
normalizeOoLook,
|
normalizeOoLook,
|
||||||
OO_AVATAR_MAX_LENGTH,
|
OO_AVATAR_MAX_LENGTH,
|
||||||
OO_EYE_ASYMMETRY_MIN_RATIO,
|
OO_EYE_ASYMMETRY_MIN_RATIO,
|
||||||
|
OO_EYE_CENTER_SPACING_RATIO,
|
||||||
OO_RIG_EYES,
|
OO_RIG_EYES,
|
||||||
type OoAvatar,
|
type OoAvatar,
|
||||||
ooAvatarColor,
|
ooAvatarColor,
|
||||||
@ -131,4 +132,12 @@ describe("oo rig brand lock", () => {
|
|||||||
expect(ratio).toBeGreaterThanOrEqual(OO_EYE_ASYMMETRY_MIN_RATIO);
|
expect(ratio).toBeGreaterThanOrEqual(OO_EYE_ASYMMETRY_MIN_RATIO);
|
||||||
expect(ooLensRadius("big")).toBeGreaterThan(OO_RIG_EYES.big.r);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 φ.
|
* 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;
|
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 = {
|
export const OO_RIG_EYES = {
|
||||||
big: { cx: 80, cy: 108, r: 21 },
|
big: OO_RIG_EYE_BIG,
|
||||||
small: { cx: 126, cy: 111, r: 21 / OO_GOLDEN_RATIO },
|
small: { ...OO_RIG_EYE_SMALL_CENTER, r: OO_RIG_EYE_SMALL_R },
|
||||||
} as const;
|
} 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). */
|
/** Minimum big÷small ratio for eyes, lenses and closed-eye arcs (Oo trademark). */
|
||||||
export const OO_EYE_ASYMMETRY_MIN_RATIO = 1.45;
|
export const OO_EYE_ASYMMETRY_MIN_RATIO = 1.45;
|
||||||
|
|
||||||
|
|||||||
@ -683,9 +683,9 @@ function cheeks(avatar: OoAvatar): OoShape[] {
|
|||||||
[58, 129, 1.6],
|
[58, 129, 1.6],
|
||||||
[64, 133, 1.6],
|
[64, 133, 1.6],
|
||||||
[69, 128, 1.6],
|
[69, 128, 1.6],
|
||||||
[141, 131, 1.4],
|
[127, 131, 1.4],
|
||||||
[146, 135, 1.4],
|
[132, 135, 1.4],
|
||||||
[150, 130, 1.4],
|
[136, 130, 1.4],
|
||||||
];
|
];
|
||||||
return dots.map(([cx, cy, r]) => ({
|
return dots.map(([cx, cy, r]) => ({
|
||||||
type: "circle",
|
type: "circle",
|
||||||
@ -699,7 +699,7 @@ function cheeks(avatar: OoAvatar): OoShape[] {
|
|||||||
const fill = cheek === "peach" ? "#FFB08A" : PINK;
|
const fill = cheek === "peach" ? "#FFB08A" : PINK;
|
||||||
return [
|
return [
|
||||||
{ type: "ellipse", cx: 62, cy: 131, rx: 9.5, ry: 5.5, fill, opacity: 0.55 },
|
{ 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 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
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.
|
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.
|
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_BIG_R` | 30 | Capital O sclera |
|
||||||
| `EVOO_EYE_SMALL_R` | 18.54 | Lowercase o sclera (~φ⁻¹ of big) |
|
| `EVOO_EYE_SMALL_R` | 18.54 | Lowercase o sclera (~φ⁻¹ of big) |
|
||||||
| `EVOO_EYE_ASYMMETRY_MIN_RATIO` | 1.45 | CI guard — catches accidental symmetrization |
|
| `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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
|
EVOO_EYE_BIG_CX,
|
||||||
|
EVOO_EYE_BIG_CY,
|
||||||
EVOO_EYE_BIG_R,
|
EVOO_EYE_BIG_R,
|
||||||
|
EVOO_EYE_SMALL_CX,
|
||||||
|
EVOO_EYE_SMALL_CY,
|
||||||
EVOO_EYE_SMALL_R,
|
EVOO_EYE_SMALL_R,
|
||||||
EVOO_OO_TRADEMARK,
|
EVOO_OO_TRADEMARK,
|
||||||
|
EVOO_PUPIL_BIG_CX,
|
||||||
|
EVOO_PUPIL_BIG_CY,
|
||||||
EVOO_PUPIL_BIG_R,
|
EVOO_PUPIL_BIG_R,
|
||||||
|
EVOO_PUPIL_SMALL_CX,
|
||||||
|
EVOO_PUPIL_SMALL_CY,
|
||||||
EVOO_PUPIL_SMALL_R,
|
EVOO_PUPIL_SMALL_R,
|
||||||
} from "./brand-lock.js";
|
} from "./brand-lock.js";
|
||||||
import { EVOO_CHARACTER_SPRING, EVOO_POSE_TRANSITION_S } from "./motion-tokens.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";
|
import type { EvooMouthStyle, EvooPose } from "./types.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
EVOO_EYE_BIG_CX,
|
||||||
|
EVOO_EYE_BIG_CY,
|
||||||
EVOO_EYE_BIG_R,
|
EVOO_EYE_BIG_R,
|
||||||
|
EVOO_EYE_SMALL_CX,
|
||||||
|
EVOO_EYE_SMALL_CY,
|
||||||
EVOO_EYE_SMALL_R,
|
EVOO_EYE_SMALL_R,
|
||||||
|
EVOO_PUPIL_BIG_CX,
|
||||||
|
EVOO_PUPIL_BIG_CY,
|
||||||
EVOO_PUPIL_BIG_R,
|
EVOO_PUPIL_BIG_R,
|
||||||
|
EVOO_PUPIL_SMALL_CX,
|
||||||
|
EVOO_PUPIL_SMALL_CY,
|
||||||
EVOO_PUPIL_SMALL_R,
|
EVOO_PUPIL_SMALL_R,
|
||||||
} from "./brand-lock.js";
|
} from "./brand-lock.js";
|
||||||
|
|
||||||
@ -66,8 +82,8 @@ function Brow({
|
|||||||
const worried = raise === "worried";
|
const worried = raise === "worried";
|
||||||
if (side === "left" && !showLeft) return null;
|
if (side === "left" && !showLeft) return null;
|
||||||
if (side === "right" && !showRight) return null;
|
if (side === "right" && !showRight) return null;
|
||||||
const cx = side === "left" ? 44 : 88;
|
const cx = side === "left" ? EVOO_EYE_BIG_CX : EVOO_EYE_SMALL_CX;
|
||||||
const cy = side === "left" ? 28 : 32;
|
const cy = side === "left" ? 28 : EVOO_EYE_SMALL_CY - 28;
|
||||||
const tilt = side === "left" ? (worried ? 8 : -10) : worried ? -8 : 10;
|
const tilt = side === "left" ? (worried ? 8 : -10) : worried ? -8 : 10;
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
@ -108,9 +124,29 @@ function Accessory({
|
|||||||
case "glasses":
|
case "glasses":
|
||||||
return (
|
return (
|
||||||
<g opacity="0.85">
|
<g opacity="0.85">
|
||||||
<ellipse cx="44" cy="54" rx="22" ry="20" fill="none" stroke="#0F172A" strokeWidth="1.8" />
|
<ellipse
|
||||||
<ellipse cx="88" cy="60" rx="14" ry="13" fill="none" stroke="#0F172A" strokeWidth="1.6" />
|
cx={EVOO_EYE_BIG_CX}
|
||||||
<path d="M66 54 L66 58" stroke="#0F172A" strokeWidth="1.6" />
|
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>
|
</g>
|
||||||
);
|
);
|
||||||
case "clipboard":
|
case "clipboard":
|
||||||
@ -580,7 +616,7 @@ export function EvooCharacterSvg({
|
|||||||
className="evoo-eye-big"
|
className="evoo-eye-big"
|
||||||
data-evoo-brand={EVOO_OO_TRADEMARK}
|
data-evoo-brand={EVOO_OO_TRADEMARK}
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "44px 54px",
|
transformOrigin: `${EVOO_EYE_BIG_CX}px ${EVOO_EYE_BIG_CY}px`,
|
||||||
animation:
|
animation:
|
||||||
def.bodyAnimation === "evoo-executive-nod"
|
def.bodyAnimation === "evoo-executive-nod"
|
||||||
? "evoo-executive-nod 2s ease-in-out infinite"
|
? "evoo-executive-nod 2s ease-in-out infinite"
|
||||||
@ -588,8 +624,8 @@ export function EvooCharacterSvg({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="44"
|
cx={EVOO_EYE_BIG_CX}
|
||||||
cy="54"
|
cy={EVOO_EYE_BIG_CY}
|
||||||
rx={EVOO_EYE_BIG_R}
|
rx={EVOO_EYE_BIG_R}
|
||||||
ry={EVOO_EYE_BIG_R}
|
ry={EVOO_EYE_BIG_R}
|
||||||
fill="#FFFFFF"
|
fill="#FFFFFF"
|
||||||
@ -598,14 +634,14 @@ export function EvooCharacterSvg({
|
|||||||
strokeOpacity="0.12"
|
strokeOpacity="0.12"
|
||||||
style={{
|
style={{
|
||||||
filter: "drop-shadow(0 2px 6px rgba(0,0,0,0.22))",
|
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})`,
|
transform: isBlinking ? "scaleY(0.08)" : `scaleY(${eyeOpen})`,
|
||||||
transition: reducedMotion ? "none" : `transform ${transition}`,
|
transition: reducedMotion ? "none" : `transform ${transition}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="38"
|
cx={EVOO_EYE_BIG_CX - 6}
|
||||||
cy="44"
|
cy={EVOO_EYE_BIG_CY - 10}
|
||||||
rx={EVOO_EYE_BIG_R * 0.38}
|
rx={EVOO_EYE_BIG_R * 0.38}
|
||||||
ry={EVOO_EYE_BIG_R * 0.24}
|
ry={EVOO_EYE_BIG_R * 0.24}
|
||||||
fill="#FFFFFF"
|
fill="#FFFFFF"
|
||||||
@ -620,8 +656,8 @@ export function EvooCharacterSvg({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="48"
|
cx={EVOO_PUPIL_BIG_CX}
|
||||||
cy="57"
|
cy={EVOO_PUPIL_BIG_CY}
|
||||||
rx={EVOO_PUPIL_BIG_R + 2.2}
|
rx={EVOO_PUPIL_BIG_R + 2.2}
|
||||||
ry={EVOO_PUPIL_BIG_R + 2.2}
|
ry={EVOO_PUPIL_BIG_R + 2.2}
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -629,21 +665,40 @@ export function EvooCharacterSvg({
|
|||||||
strokeWidth="0.9"
|
strokeWidth="0.9"
|
||||||
opacity="0.35"
|
opacity="0.35"
|
||||||
/>
|
/>
|
||||||
<ellipse cx="48" cy="57" rx={EVOO_PUPIL_BIG_R} ry={EVOO_PUPIL_BIG_R} fill="#0A0F1D" />
|
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="46"
|
cx={EVOO_PUPIL_BIG_CX}
|
||||||
cy="59"
|
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}
|
rx={EVOO_PUPIL_BIG_R * 0.42}
|
||||||
ry={EVOO_PUPIL_BIG_R * 0.28}
|
ry={EVOO_PUPIL_BIG_R * 0.28}
|
||||||
fill="#1E3A5F"
|
fill="#1E3A5F"
|
||||||
opacity="0.55"
|
opacity="0.55"
|
||||||
/>
|
/>
|
||||||
<circle cx="53" cy="49" r="4.6" fill="#FFFFFF" />
|
<circle cx={EVOO_PUPIL_BIG_CX + 5} cy={EVOO_PUPIL_BIG_CY - 8} r="4.6" fill="#FFFFFF" />
|
||||||
<circle cx="42" cy="63" r="2.2" fill="#FFFFFF" opacity="0.85" />
|
<circle
|
||||||
<ellipse cx="50" cy="51" rx="1.8" ry="1.1" fill="#FFFFFF" opacity="0.92" />
|
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") && (
|
{(def.eyeSparkle || pose === "success") && (
|
||||||
<path
|
<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"
|
fill="#10B981"
|
||||||
opacity="0.95"
|
opacity="0.95"
|
||||||
/>
|
/>
|
||||||
@ -656,7 +711,7 @@ export function EvooCharacterSvg({
|
|||||||
className="evoo-eye-small"
|
className="evoo-eye-small"
|
||||||
data-evoo-brand={EVOO_OO_TRADEMARK}
|
data-evoo-brand={EVOO_OO_TRADEMARK}
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "88px 60px",
|
transformOrigin: `${EVOO_EYE_SMALL_CX}px ${EVOO_EYE_SMALL_CY}px`,
|
||||||
animation:
|
animation:
|
||||||
def.bodyAnimation === "evoo-executive-nod"
|
def.bodyAnimation === "evoo-executive-nod"
|
||||||
? "evoo-executive-nod 2s ease-in-out infinite"
|
? "evoo-executive-nod 2s ease-in-out infinite"
|
||||||
@ -664,8 +719,8 @@ export function EvooCharacterSvg({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="88"
|
cx={EVOO_EYE_SMALL_CX}
|
||||||
cy="60"
|
cy={EVOO_EYE_SMALL_CY}
|
||||||
rx={EVOO_EYE_SMALL_R}
|
rx={EVOO_EYE_SMALL_R}
|
||||||
ry={EVOO_EYE_SMALL_R}
|
ry={EVOO_EYE_SMALL_R}
|
||||||
fill="#FFFFFF"
|
fill="#FFFFFF"
|
||||||
@ -674,14 +729,14 @@ export function EvooCharacterSvg({
|
|||||||
strokeOpacity="0.12"
|
strokeOpacity="0.12"
|
||||||
style={{
|
style={{
|
||||||
filter: "drop-shadow(0 2px 5px rgba(0,0,0,0.18))",
|
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})`,
|
transform: isSmallBlinking || isWinking ? "scaleY(0.08)" : `scaleY(${eyeOpen})`,
|
||||||
transition: reducedMotion ? "none" : `transform ${transition}`,
|
transition: reducedMotion ? "none" : `transform ${transition}`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="84.2"
|
cx={EVOO_EYE_SMALL_CX - 3.8}
|
||||||
cy="52.5"
|
cy={EVOO_EYE_SMALL_CY - 7.5}
|
||||||
rx={EVOO_EYE_SMALL_R * 0.38}
|
rx={EVOO_EYE_SMALL_R * 0.38}
|
||||||
ry={EVOO_EYE_SMALL_R * 0.24}
|
ry={EVOO_EYE_SMALL_R * 0.24}
|
||||||
fill="#FFFFFF"
|
fill="#FFFFFF"
|
||||||
@ -696,8 +751,8 @@ export function EvooCharacterSvg({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="90"
|
cx={EVOO_PUPIL_SMALL_CX}
|
||||||
cy="62"
|
cy={EVOO_PUPIL_SMALL_CY}
|
||||||
rx={EVOO_PUPIL_SMALL_R + 1.4}
|
rx={EVOO_PUPIL_SMALL_R + 1.4}
|
||||||
ry={EVOO_PUPIL_SMALL_R + 1.4}
|
ry={EVOO_PUPIL_SMALL_R + 1.4}
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -706,23 +761,41 @@ export function EvooCharacterSvg({
|
|||||||
opacity="0.3"
|
opacity="0.3"
|
||||||
/>
|
/>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="90"
|
cx={EVOO_PUPIL_SMALL_CX}
|
||||||
cy="62"
|
cy={EVOO_PUPIL_SMALL_CY}
|
||||||
rx={EVOO_PUPIL_SMALL_R}
|
rx={EVOO_PUPIL_SMALL_R}
|
||||||
ry={EVOO_PUPIL_SMALL_R}
|
ry={EVOO_PUPIL_SMALL_R}
|
||||||
fill="#0A0F1D"
|
fill="#0A0F1D"
|
||||||
/>
|
/>
|
||||||
<ellipse
|
<ellipse
|
||||||
cx="88.8"
|
cx={EVOO_PUPIL_SMALL_CX - 1.2}
|
||||||
cy="63.2"
|
cy={EVOO_PUPIL_SMALL_CY + 1.2}
|
||||||
rx={EVOO_PUPIL_SMALL_R * 0.4}
|
rx={EVOO_PUPIL_SMALL_R * 0.4}
|
||||||
ry={EVOO_PUPIL_SMALL_R * 0.26}
|
ry={EVOO_PUPIL_SMALL_R * 0.26}
|
||||||
fill="#1E3A5F"
|
fill="#1E3A5F"
|
||||||
opacity="0.5"
|
opacity="0.5"
|
||||||
/>
|
/>
|
||||||
<circle cx="93.2" cy="57" r="2.8" fill="#FFFFFF" />
|
<circle
|
||||||
<circle cx="86.8" cy="65.5" r="1.3" fill="#FFFFFF" opacity="0.85" />
|
cx={EVOO_PUPIL_SMALL_CX + 3.2}
|
||||||
<ellipse cx="91.4" cy="58.2" rx="1.1" ry="0.7" fill="#FFFFFF" opacity="0.88" />
|
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>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
assertEvooEyeBrandLock,
|
assertEvooEyeBrandLock,
|
||||||
EVOO_EYE_ASYMMETRY_MIN_RATIO,
|
EVOO_EYE_ASYMMETRY_MIN_RATIO,
|
||||||
|
EVOO_EYE_BIG_CX,
|
||||||
|
EVOO_EYE_BIG_CY,
|
||||||
EVOO_EYE_BIG_R,
|
EVOO_EYE_BIG_R,
|
||||||
|
EVOO_EYE_CENTER_SPACING_RATIO,
|
||||||
|
EVOO_EYE_SMALL_CX,
|
||||||
|
EVOO_EYE_SMALL_CY,
|
||||||
EVOO_EYE_SMALL_R,
|
EVOO_EYE_SMALL_R,
|
||||||
EVOO_OO_TRADEMARK,
|
EVOO_OO_TRADEMARK,
|
||||||
EVOO_PUPIL_BIG_R,
|
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);
|
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", () => {
|
it("rejects symmetrized eye radii", () => {
|
||||||
expect(() => assertEvooEyeBrandLock(20, 20)).toThrow(/asymmetric Kulleraugen/);
|
expect(() => assertEvooEyeBrandLock(20, 20)).toThrow(/asymmetric Kulleraugen/);
|
||||||
expect(() => assertEvooEyeBrandLock(20, 19)).toThrow(/asymmetry ratio/);
|
expect(() => assertEvooEyeBrandLock(20, 19)).toThrow(/asymmetry ratio/);
|
||||||
|
|||||||
@ -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).
|
* 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:
|
* All mascot surfaces (EvooMascot, BotAvatar, Wordmark) MUST keep:
|
||||||
* 1. Consistent size asymmetry between the two eyes (big O vs small o).
|
* 1. Consistent size asymmetry between the two eyes (big O vs small o).
|
||||||
* 2. Kulleraugen — big, round, glossy googly/cute sclera + pupils.
|
* 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.
|
* 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.
|
* 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. */
|
/** Small eye (lowercase o) — ~φ⁻¹ of big eye; trademark size gap. */
|
||||||
export const EVOO_EYE_SMALL_R = 18.54;
|
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. */
|
/** Pupil radii scale with sclera; golden-ratio proportion preserved. */
|
||||||
export const EVOO_PUPIL_BIG_R = 14.2;
|
export const EVOO_PUPIL_BIG_R = 14.2;
|
||||||
export const EVOO_PUPIL_SMALL_R = EVOO_PUPIL_BIG_R * 0.618034;
|
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. */
|
/** 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_EYE_ASYMMETRY_MIN_RATIO = OO_EYE_ASYMMETRY_MIN_RATIO;
|
||||||
|
|
||||||
export const EVOO_BRAND_LOCK_RULES = [
|
export const EVOO_BRAND_LOCK_RULES = [
|
||||||
"Two eyes only: one big O, one small o — never equal radii.",
|
"Two eyes only: one big O, one small o — never equal radii.",
|
||||||
"Kulleraugen: round white sclera, dark glossy pupil, primary + secondary catchlights.",
|
"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.",
|
"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.",
|
"No Grok-bot emoji face replacement; no symmetric sticker eyes.",
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
buildOoRig,
|
buildOoRig,
|
||||||
createOoLife,
|
createOoLife,
|
||||||
OO_LIST_MOMENTS,
|
OO_LIST_MOMENTS,
|
||||||
|
OO_RIG_FACE_VIEWBOX,
|
||||||
OO_RIG_INSET,
|
OO_RIG_INSET,
|
||||||
OO_RIG_SIZE,
|
OO_RIG_SIZE,
|
||||||
OO_STILL_TIME,
|
OO_STILL_TIME,
|
||||||
@ -34,7 +35,7 @@ export interface OoFigureProps {
|
|||||||
/** The body fills the avatar box; ears, hats and effects may reach past it. */
|
/** 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_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. */
|
/** Above this size an Oo always moves; smaller ones only for list moments. */
|
||||||
const LIVELY_SIZE = 56;
|
const LIVELY_SIZE = 56;
|
||||||
|
|||||||
527
pnpm-lock.yaml
generated
527
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user