Compare commits
4 Commits
f8b4ce4b6d
...
7afa5af3ff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7afa5af3ff | ||
|
|
9b7632613d | ||
|
|
54728e52c9 | ||
|
|
71613064ea |
@ -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. */
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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/);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
527
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user