diff --git a/README.md b/README.md index 2b28992..ea3e040 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,13 @@ Unofficial study of a spring-driven character engine. 非官方学习项目,与 xAI / Grok 无关。本仓库只开源机芯,不含角色几何、商标或第三方应用源码。 -The **site** follows [bloub](https://github.com/jeremy-prt/bloub)’s presentation: a left rail, one character in the middle, Customise / Animations / Settings. The **motion** is ours — springs, 39 states, polygon eyes, overlays, login wrap. Not bloub’s measured 14-state engine. +One filled shape that morphs between **39 states**, **polygon eyes** that morph +on their own playlist, overlays on some states, springs on move / squash / blink / +spin. No animation library. + +The **site** follows [bloub](https://github.com/jeremy-prt/bloub)’s presentation: +a left rail, one character in the middle, Customise / Animations / Settings. +The **motion** is ours — not bloub’s measured 14-state engine. ## Running it @@ -14,21 +20,51 @@ pnpm dev Then open http://localhost:5191. -Clone the repo and the chrome still loads. Without `replica/geometry-data.js` the character stays off; the layout does not fall over. Put a file that matches [`replica/geometry.schema.md`](./replica/geometry.schema.md) at `replica/geometry-data.js` (gitignored) to see the bot. +```bash +pnpm build # vue-tsc --noEmit && vite build +``` -The original lab is still there: +Vue 3, Vite, TypeScript, Tailwind 4. No ESLint and no Prettier: `vue-tsc` is the +only gate, so run `pnpm build` before you call something done. -- [`replica/index.html`](./replica/index.html) — playground + HUD -- [`replica/showcase.html`](./replica/showcase.html) — recording layouts -- [`replica/embed.html`](./replica/embed.html) — `` +Clone the repo and the chrome still loads. Without `replica/geometry-data.js` +the character stays off; the layout does not fall over. Put a file that matches +[`replica/geometry.schema.md`](./replica/geometry.schema.md) at +`replica/geometry-data.js` (gitignored) to see the bot. Do not commit extracted geometry, icons, or a third-party app bundle. ## What's on the site -The rail switches three views. **Customise** is 8 body shapes, 11 inks and the rest-face playlist; stills export as PNG / SVG / GIF. **Animations** is bloub’s timeline editor on our 39 states: drag to reorder, pull a card to set duration, save named cycles in `localStorage`, export the sequence as MP4 or GIF. The right-hand tiles append a block. **Settings** is language (French, English, Chinese) and credits. +The rail on the left switches between three views. **Customise** offers 8 body +shapes, 11 inks and 17 rest expressions, kept between visits, plus a manual pose +overlay (drag the body, tune an eye). **Animations** is bloub’s timeline editor +on our 39 states: drag to reorder, pull a card to set duration, save named +cycles in `localStorage`. The right-hand tiles append a block. **Settings** +holds the language (Chinese or English, Chinese by default) and the credits. -The centre avatar is always `GrokCharacter` in `hold` mode. Tiles paint one frame and stop. Nothing from bloub’s `src/bot/` is in this tree. +Anything on screen can be exported: the avatar as PNG, SVG or an animated GIF, +and a whole timeline as GIF or MP4. The still formats need no library at all, +and the video encoder is only fetched the first time you ask for one. + +The centre avatar is always `GrokCharacter` in `hold` mode. Tiles paint one +frame and stop. Nothing from bloub’s `src/bot/` is in this tree. + +Three URLs are worth knowing: + +- [`replica/index.html`](./replica/index.html) — playground + HUD +- [`replica/showcase.html`](./replica/showcase.html) — recording layouts +- [`replica/embed.html`](./replica/embed.html) — `` + +## Why this isn't bloub + +bloub measured 14 states off a video and plays exponential ease-outs with no +overshoot. This repo studies the spring-driven character: 39 states, polygon +eyes, overlays, login wrap. Rounding a spring or swapping a playlist changes +the feel, which is the thing this project is trying to keep. + +Geometry is not in the tree on purpose. The engine runs against +`window.GROK_GEO`; without it the site still stands. ## What the engine does @@ -40,11 +76,17 @@ The centre avatar is always `GrokCharacter` in `hold` mode. Tiles paint one fram Login-disk defaults: `sizePx: 64`, `color: "black"`, `shape: "blob"`. -## Layout +## How it's put together + +`replica/src/` is framework-free. `GrokCharacter` owns the clock; Vue only +mounts it, paints tiles, and (in Customise) overlays the pose editor. Missing +geometry keeps the chrome. `GROK.create` / `` live in +`replica/src/api.js`. | Path | What | |---|---| | `src/` | Vue site (bloub-like chrome) | +| `src/ui/pose/` | manual pose, head surfaces | | `replica/index.html` | playground | | `replica/geometry.schema.md` | `window.GROK_GEO` contract, no data | | `replica/src/math.js` | springs, lerp, polygons | @@ -63,9 +105,39 @@ This repo deliberately does not contain: - unpacked app slices - unpack scripts +## Using the component + +```vue + +``` + +```html + +``` + +```js +const { setState, setShape, setColor, setPaused, spin, bounce, destroy } = GROK.create("#host", { + sizePx: 64, + shape: "blob", + color: "black", + loginWrap: true, + mode: "hold", +}); +``` + +`GROK.create` / `GrokCharacter` default to `mode: "hold"` and will not cycle on +their own. Pass `mode: "onboarding"` for the login beat. Public methods: +`setState` `setShape` `setColor` `setPaused` `spin` `bounce` `destroy`. +`` attributes: `state` `shape` `color` `size` `paused` +`follow="true"` `mode` `emphasis`. + +See [`src/components/Hero.vue`](src/components/Hero.vue) and +[`replica/src/api.js`](replica/src/api.js) for the details. + ## License [MIT](./LICENSE) covers the original engine, site and docs in this repository. It does not grant rights to third-party character art, trademarks or application source; those are not in this repository. -Site chrome is arranged after bloub’s three-column scene. bloub is MIT; “Grok” and “x.ai” belong to their owners. Not affiliated with either. +Site chrome is arranged after bloub’s three-column scene. bloub is MIT; “Grok” +and “x.ai” belong to their owners. Not affiliated with either. diff --git a/index.html b/index.html index 5679b7f..d572ba1 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ - + - study — spring character + study — 弹簧角色 = { fr, en, zh } +const dictionnaires: Record = { zh, en } type Chemins = { [K in keyof T & string]: T[K] extends string ? `${P}${K}` : Chemins }[keyof T & string] -export type Cle = Chemins +export type Cle = Chemins -const courante = ref( - choisirLangue(lis('langue'), typeof navigator === 'undefined' ? [] : (navigator.languages ?? [navigator.language])) -) +const courante = ref(choisirLangue(lis('langue'))) export const langue = computed({ get: () => courante.value, diff --git a/src/i18n/langues.ts b/src/i18n/langues.ts index 5a86ad7..cc5edd3 100644 --- a/src/i18n/langues.ts +++ b/src/i18n/langues.ts @@ -1,12 +1,11 @@ export const LANGUES = [ - { id: 'fr', tag: 'fr', emoji: '🇫🇷', nom: 'Français' }, - { id: 'en', tag: 'en', emoji: '🇬🇧', nom: 'English' }, - { id: 'zh', tag: 'zh-Hans', emoji: '🇨🇳', nom: '简体中文' } + { id: 'zh', tag: 'zh-Hans', emoji: '🇨🇳', nom: '简体中文' }, + { id: 'en', tag: 'en', emoji: '🇬🇧', nom: 'English' } ] as const export type Langue = (typeof LANGUES)[number]['id'] -export const LANGUE_PAR_DEFAUT: Langue = 'en' +export const LANGUE_PAR_DEFAUT: Langue = 'zh' export function estLangue(valeur: string | null | undefined): valeur is Langue { return LANGUES.some((l) => l.id === valeur) @@ -16,16 +15,6 @@ export function tagDe(langue: Langue): string { return LANGUES.find((l) => l.id === langue)!.tag } -export function choisirLangue(memorisee: string | null, preferences: readonly string[]): Langue { - if (estLangue(memorisee)) return memorisee - for (const tag of preferences) { - let base: string - try { - base = new Intl.Locale(tag).language - } catch { - continue - } - if (estLangue(base)) return base - } - return LANGUE_PAR_DEFAUT +export function choisirLangue(memorisee: string | null): Langue { + return estLangue(memorisee) ? memorisee : LANGUE_PAR_DEFAUT } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index c8d56af..80dc095 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1,6 +1,6 @@ -import type fr from './fr' +import type zh from './zh' -const en: typeof fr = { +const en: typeof zh = { app: { name: 'study', title: 'study — spring character', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts deleted file mode 100644 index 0d3479e..0000000 --- a/src/i18n/locales/fr.ts +++ /dev/null @@ -1,196 +0,0 @@ -const fr = { - app: { - name: 'study', - title: 'study — personnage à ressorts', - botAria: 'Personnage animé par ressorts', - missingGeo: - 'Géométrie locale absente. L’interface reste là ; le personnage non. Voir replica/geometry.schema.md.' - }, - rail: { - nav: 'Sections', - customize: 'Personnaliser', - animations: 'Animations', - settings: 'Réglages' - }, - panel: { - animations: 'Animation', - shape: 'Forme', - expression: 'Expression', - color: 'Couleur', - group_lifecycle: 'Cycle', - group_reactions: 'Réactions', - group_agent: 'Agent', - group_product: 'Produit' - }, - shapes: { - blob: 'Blob', - bean: 'Haricot', - egg: 'Œuf', - teardrop: 'Goutte', - cloud: 'Nuage', - leaf: 'Feuille', - squircle: 'Cube', - capsule: 'Capsule' - }, - export: { - action: 'Exporter en PNG', - more: 'Autres formats', - png: 'Télécharger le PNG', - svg: 'Télécharger le SVG', - anime: "Télécharger l'animation SVG", - gif: 'Télécharger le GIF animé', - cycleDetail: 'La vidéo est plus légère et plus fluide ; le GIF passe partout.', - cycleFormat: 'Format', - cycle_mp4: 'Vidéo MP4', - cycle_mp4_aide: 'Léger et fluide, fond obligatoire', - cycle_gif: 'GIF animé', - cycle_gif_aide: 'Lu partout, plus lourd', - cycleProgress: 'Export en cours…', - cycleReessayer: 'Réessayer', - gifTitle: 'Télécharger le GIF animé', - gifDetail: - "Le GIF ne gère la transparence qu'en tout ou rien : sans fond, le contour de la boule est un peu dur.", - gifBackground: 'Fond', - fond_blanc: 'Fond blanc', - fond_blanc_aide: 'Contour lisse, à poser sur du clair', - fond_transparent: 'Fond transparent', - fond_transparent_aide: "S'adapte à tout fond, contour un peu dur", - gifConfirm: 'Télécharger', - copie: "Copier l'image", - copieSvg: 'Copier le SVG', - done: 'Exporté', - copied: 'Copié', - failed: "Échec de l'export" - }, - preview: { - exit: 'Quitter l’aperçu', - open: 'Aperçu', - key: 'Échap' - }, - timeline: { - play: 'Lancer la lecture', - pause: 'Arrêter la lecture', - addAnimation: 'Ajouter une animation', - preview: 'Aperçu', - export: "Exporter la séquence d'animation", - zoom: 'Zoom de la piste', - blockAria: '{state}, {duration}', - blockDurationAria: 'Durée de {state}, {duration}', - blockRemoveAria: 'Retirer {state}' - }, - playback: { - details: 'Détails de l’animation', - show: 'Afficher les détails de l’animation', - hide: 'Masquer les détails de l’animation', - playing: 'En lecture', - paused: 'En pause', - stopped: 'Arrêté', - play: 'Lancer {name}', - pause: 'Mettre {name} en pause', - stop: 'Arrêter {name}', - mode: 'Mode de lecture', - loop: 'boucle', - clips: 'Poses', - current: 'Pose en cours', - stepOf: '{n} / {m}', - summary: 'Les poses d’œil de cette expression, en boucle.', - blinkFirst: 'Premier clignement', - blinkAfter: 'après le lancement', - blinkGap: 'Intervalle du clignement', - blinkRand: 'tirage aléatoire', - blinkOff: 'Désactivé' - }, - dialog: { - cancel: 'Annuler', - nameCreateTitle: 'Nouveau cycle', - nameRenameTitle: 'Renommer le cycle', - nameField: 'Nom du cycle', - nameCreate: 'Créer', - nameRename: 'Renommer', - removeTitle: 'Supprimer « {name} » ?', - removeDetail: - 'Ce montage sera perdu, avec son animation. | Ce montage sera perdu, avec ses {n} animations.', - removeConfirm: 'Supprimer' - }, - cycles: { - defaultName: 'Cycle par défaut', - newName: 'Mon cycle', - menuNew: 'Nouveau cycle', - menuRenameAria: 'Renommer {name}', - menuRemoveAria: 'Supprimer {name}' - }, - states: { - sleeping: 'Sommeil', - waking: 'Réveil', - idle: 'Repos', - listening: 'Écoute', - thinking: 'Réflexion', - searching: 'Recherche', - working: 'Travail', - excited: 'Excité', - surprised: 'Surpris', - suspicious: 'Méfiant', - angry: 'En colère', - drowsy: 'Somnolent', - happy: 'Heureux', - curious: 'Curieux', - confused: 'Confus', - bored: 'Blasé', - proud: 'Fier', - shy: 'Timide', - sad: 'Triste', - laughing: 'Rire', - scared: 'Effrayé', - playful: 'Joueur', - celebrate: 'Célébration', - orbit: 'Orbite', - radar: 'Radar', - progress: 'Progrès', - spawning: 'Apparition', - humming: 'Bourdonnement', - loading: 'Chargement', - dictating: 'Dictée', - writing: 'Écriture', - sending: 'Envoi', - receiving: 'Réception', - uploading: 'Téléversement', - notifying: 'Notification', - alerting: 'Alerte', - dragging: 'Glisser', - bouncing: 'Rebond', - 'powering-down': 'Extinction' - }, - settings: { - language: 'Langue', - about: 'À propos', - lab: 'Laboratoire', - labAria: 'Ouvrir le playground d’origine', - showcase: 'Showcase', - showcaseAria: 'Ouvrir le showcase d’enregistrement', - credits: - 'Étude non officielle. Le mouvement vient du moteur replica ; la mise en page, de bloub. Sans lien avec xAI.', - missing: 'Pas de geometry-data.js — chrome seul, pas de personnage.' - }, - units: { - seconds: '{n} s', - secondsShort: '{n}s' - }, - manual: { - title: 'Réglage manuel', - help: 'Une fois activé, glisse le corps pour le tourner, clique un œil pour L/H/S/R/E, ou les anneaux en haut à droite.', - on: 'Activer le réglage manuel', - reset: 'Réinitialiser', - head: 'Rotation de la tête', - body: 'Corps', - spin: 'Roulis', - eyes: 'Yeux', - width: 'L', - height: 'H', - size: 'S', - spacing: 'Écart', - gizmo: 'Manipulateur de rotation', - plane: 'Déplacer dans le plan' - } -} - -export default fr diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 783ee19..2e26d6b 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -1,6 +1,4 @@ -import type fr from './fr' - -const zh: typeof fr = { +const zh = { app: { name: 'study', title: 'study — 弹簧角色',