import type { OoAvatar } from "@rakazo/core"; import { encodeOoAvatar, OO_ARMS, OO_BROWS, OO_CHEEKS, OO_CLASSIC_SHAPE_COUNT, OO_EYESHADOWS, OO_EYEWEAR, OO_HEADWEAR, OO_IRISES, OO_LASHES, OO_LINERS, OO_NECKWEAR, OO_PALETTE_HEXES, OO_SPARKLES, OO_SPECIES, ooLookForEditing, ooRandomAvatar, } from "@rakazo/core"; import { useState } from "react"; import { Pressable, ScrollView, Text, View } from "react-native"; import { useI18n } from "../lib/i18n"; import { useMobileTokens } from "../lib/native"; import { OoFigure } from "./oo-figure"; const THUMB = 52; /** Temperament in three taps; web offers the same range as sliders. */ const ENERGY_STEPS = [0.75, 1, 1.35] as const; const PLAYFUL_STEPS = [0.2, 0.5, 0.85] as const; function nearest(steps: readonly number[], value: number): number { return steps.reduce((best, step) => Math.abs(step - value) < Math.abs(best - value) ? step : best, ); } function useLabels() { const { t } = useI18n(); const none = t("None"); return { species: { classic: t("Classic"), cat: t("Cat"), dog: t("Dog"), bunny: t("Bunny"), bear: t("Bear"), robot: t("Robot"), ghost: t("Ghost"), frog: t("Frog"), penguin: t("Penguin"), }, lashes: { none, soft: t("Soft"), full: t("Full"), flick: t("Flick") }, liner: { none, wing: t("Winged") }, eyeshadow: { none, rose: t("Rose"), lilac: t("Lilac"), gold: t("Gold"), mint: t("Mint") }, iris: { classic: t("Classic"), brown: t("Brown"), blue: t("Blue"), green: t("Green"), violet: t("Violet"), }, sparkle: { dot: t("Dot"), star: t("Star"), heart: t("Heart") }, brows: { auto: t("When needed"), fine: t("Fine"), bushy: t("Bushy") }, cheeks: { none, pink: t("Pink"), peach: t("Peach"), freckles: t("Freckles") }, eyewear: { none, round: t("Round"), nerd: t("Nerd"), sun: t("Sunglasses"), heart: t("Heart"), monocle: t("Monocle"), reading: t("Reading glasses"), }, head: { none, chef: t("Chef's hat"), crown: t("Crown"), headphones: t("Headphones"), bow: t("Bow"), beanie: t("Beanie"), flower: t("Flower"), party: t("Party hat"), }, neck: { none, bowtie: t("Bow tie"), scarf: t("Scarf"), bell: t("Bell") }, arms: { glass: t("Glass"), stub: t("Stub"), paw: t("Paw"), robot: t("Robot"), none }, energy: [t("Calm"), t("Steady"), t("Lively")], } as const; } function Row({ title, values, selected, label, look, patch, focus = "body", onSelect, }: { title: string; values: readonly T[]; selected: T; label: (value: T) => string; look: OoAvatar; patch: (value: T) => Partial; focus?: "body" | "face"; onSelect: (value: T) => void; }) { const tokens = useMobileTokens(); return ( {title} {values.map((value) => { const next = { ...look, ...patch(value) }; const active = value === selected; return ( onSelect(value)} style={{ width: THUMB + 8, height: THUMB + 8, borderRadius: 14, alignItems: "center", justifyContent: "center", overflow: "hidden", backgroundColor: tokens.muted, borderWidth: 2, borderColor: active ? tokens.foreground : "transparent", }} > ); })} ); } /** The Oo look editor: species and colour first, everything else one tap further. */ export function OoAtelier({ value, identity, onChange, }: { value: string; identity: string; onChange: (value: string) => void; }) { const { t } = useI18n(); const tokens = useMobileTokens(); const labels = useLabels(); const [moreOpen, setMoreOpen] = useState(false); const look = ooLookForEditing(value, identity); const set = (field: K, next: OoAvatar[K]) => onChange(encodeOoAvatar({ ...look, [field]: next })); const chip = (active: boolean) => ({ minHeight: 36, paddingHorizontal: 14, borderRadius: 18, justifyContent: "center" as const, backgroundColor: active ? tokens.foreground : tokens.muted, }); const chipText = (active: boolean) => ({ color: active ? tokens.background : tokens.foreground, fontSize: 14, }); return ( labels.species[species]} look={look} patch={(species) => ({ species, arms: species === "robot" ? "robot" : look.arms })} onSelect={(species) => onChange( encodeOoAvatar({ ...look, species, arms: species === "robot" ? "robot" : look.arms === "robot" ? "glass" : look.arms, }), ) } /> {look.species === "classic" ? ( index)} selected={look.shape ?? -1} label={(shape) => t("Shape {number}", { number: shape + 1 })} look={look} patch={(shape) => ({ shape })} onSelect={(shape) => set("shape", shape)} /> ) : null} {t("Color")} {OO_PALETTE_HEXES.map((option, index) => { const checked = look.color.toLowerCase() === option.toLowerCase(); return ( set("color", option)} style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: option, borderWidth: 3, borderColor: checked ? tokens.foreground : tokens.border, }} /> ); })} setMoreOpen((open) => !open)} style={chip(moreOpen)} > {t("Customize")} onChange(encodeOoAvatar(ooRandomAvatar()))} style={chip(false)} > {t("Surprise me")} {moreOpen ? ( labels.lashes[lashes]} look={look} patch={(lashes) => ({ lashes })} focus="face" onSelect={(lashes) => set("lashes", lashes)} /> labels.liner[liner]} look={look} patch={(liner) => ({ liner })} focus="face" onSelect={(liner) => set("liner", liner)} /> labels.eyeshadow[eyeshadow]} look={look} patch={(eyeshadow) => ({ eyeshadow })} focus="face" onSelect={(eyeshadow) => set("eyeshadow", eyeshadow)} /> labels.iris[iris]} look={look} patch={(iris) => ({ iris })} focus="face" onSelect={(iris) => set("iris", iris)} /> labels.sparkle[sparkle]} look={look} patch={(sparkle) => ({ sparkle })} focus="face" onSelect={(sparkle) => set("sparkle", sparkle)} /> labels.brows[brows]} look={look} patch={(brows) => ({ brows })} focus="face" onSelect={(brows) => set("brows", brows)} /> labels.cheeks[cheeks]} look={look} patch={(cheeks) => ({ cheeks })} focus="face" onSelect={(cheeks) => set("cheeks", cheeks)} /> labels.eyewear[eyewear]} look={look} patch={(eyewear) => ({ eyewear })} focus="face" onSelect={(eyewear) => set("eyewear", eyewear)} /> labels.head[head]} look={look} patch={(head) => ({ head })} onSelect={(head) => set("head", head)} /> labels.neck[neck]} look={look} patch={(neck) => ({ neck })} onSelect={(neck) => set("neck", neck)} /> labels.arms[arms]} look={look} patch={(arms) => ({ arms })} onSelect={(arms) => set("arms", arms)} /> {( [ ["energy", t("Energy"), ENERGY_STEPS], ["playful", t("Playful"), PLAYFUL_STEPS], ] as const ).map(([field, title, steps]) => { const current = nearest(steps, look[field]); return ( {title} {steps.map((step, index) => ( set(field, step)} style={chip(step === current)} > {labels.energy[index]} ))} ); })} ) : null} ); }