feat:语言只留中英并默认中文
去掉法语,首次打开用中文。README 按 bloub 的段落重写,补上嵌入用法。
This commit is contained in:
@@ -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) — `<grok-bot>`
|
||||
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) — `<grok-bot>`
|
||||
|
||||
## 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` / `<grok-bot>` 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
|
||||
<Hero state="idle" shape="blob" color="black" :size="120" />
|
||||
```
|
||||
|
||||
```html
|
||||
<grok-bot state="idle" shape="blob" color="black" size="64"></grok-bot>
|
||||
```
|
||||
|
||||
```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`.
|
||||
`<grok-bot>` 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.
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-Hans">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>study — spring character</title>
|
||||
<title>study — 弹簧角色</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Unofficial study of a spring-driven character engine. Site layout after bloub; motion from the replica engine."
|
||||
|
||||
+3
-6
@@ -2,23 +2,20 @@ import { computed, ref, watchEffect } from 'vue'
|
||||
import { ecris, lis } from '@/ui/stockage'
|
||||
import { formePlurielle, interpoler } from './format'
|
||||
import { choisirLangue, estLangue, type Langue, tagDe } from './langues'
|
||||
import fr from './locales/fr'
|
||||
import en from './locales/en'
|
||||
import zh from './locales/zh'
|
||||
|
||||
export { LANGUES, type Langue } from './langues'
|
||||
|
||||
const dictionnaires: Record<Langue, typeof fr> = { fr, en, zh }
|
||||
const dictionnaires: Record<Langue, typeof zh> = { zh, en }
|
||||
|
||||
type Chemins<T, P extends string = ''> = {
|
||||
[K in keyof T & string]: T[K] extends string ? `${P}${K}` : Chemins<T[K], `${P}${K}.`>
|
||||
}[keyof T & string]
|
||||
|
||||
export type Cle = Chemins<typeof fr>
|
||||
export type Cle = Chemins<typeof zh>
|
||||
|
||||
const courante = ref<Langue>(
|
||||
choisirLangue(lis('langue'), typeof navigator === 'undefined' ? [] : (navigator.languages ?? [navigator.language]))
|
||||
)
|
||||
const courante = ref<Langue>(choisirLangue(lis('langue')))
|
||||
|
||||
export const langue = computed<Langue>({
|
||||
get: () => courante.value,
|
||||
|
||||
+5
-16
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,4 @@
|
||||
import type fr from './fr'
|
||||
|
||||
const zh: typeof fr = {
|
||||
const zh = {
|
||||
app: {
|
||||
name: 'study',
|
||||
title: 'study — 弹簧角色',
|
||||
|
||||
Reference in New Issue
Block a user