Open-source project
sam70361/aora-bot avatar
sam70361/aora-bot

Emotion Ball: a zero-dependency SVG face for your AI agent

Emotion Ball 是一套面向 AI 助手的表情引擎:32 种状态表情全部由纯 SVG 与原生 JavaScript 实时驱动,零框架、零图片资源。AI 侧只需输出一个 emotionId,小球即可切换到对应表情,可直接用作聊天机器人、桌面宠物、悬浮助手的情绪表达层。

389 stars46 forksJavaScriptNOASSERTION

At a glance

What is it?
Emotion Ball is a vanilla JavaScript and SVG engine that turns a single emotionId from a model into one of 32 procedural expressions. The engineering is tidy and documented, but the licence splits the art from the code, and the art is the part you cannot ship commercially.
Who is it for?
Emotion Ball fits developers who already run an agent and want a lightweight, framework-free state indicator: chat sidebars, Electron desktop pets, floating assistants. It does not fit teams that need the ball characters themselves in a commercial product, since the README reserves the blob, wedge and gem visuals for learning with no commercial licence ever; the separately dual-licensed mood-mates/ characters are the path to look at instead.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 25 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 18, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

What a text-only agent cannot show

An agent that answers in text gives the user no signal about its own state. Is it still searching, did the request fail, or is it thinking? Emotion Ball, the project inside the sam70361/aora-bot repository, is an expression engine for exactly that gap: the README describes it as an emotional expression layer for chatbots, desktop pets and floating assistants. The contract an AI has to honour is tiny. It emits one emotionId, optionally with a short tips string, and the ball on screen switches to the matching face.

The intended user is a developer who already has an agent and wants a visible state indicator, not a full avatar pipeline. The whole stack is HTML, SVG and plain JavaScript with no build step, and the README states it migrates directly into an Electron floating window.

One ten-digit contract: how the emotionId space is organised

The 32 expressions are keyed by two-digit IDs in segmented ranges. IDs 00 to 09 cover lifecycle states such as sleep, wake and idle; 10 to 29 cover emotions like happy, shy, angry and surprised; 30 to 49 cover agent states, including thinking, retrieving information, error and task complete; 50 and above are open for custom expressions. The README makes one commitment that matters if you hardcode IDs: gaps between the groups are held open for new expressions, and existing numbers are never renumbered.

The message path is deliberately forgiving. Your model hands an object or a JSON string to handleAIMessage, and both are accepted. An unknown ID, a parse failure or a missing field does not blank the character: it fires an error event and drops the ball back to the fallback expression, set by the fallbackId option, which defaults to '02'. The tips field is optional; the engine re-emits it through a tips event and leaves the presentation to the host.

Spring interpolation on 48-point contour rings

The faces are not sprite sheets. Each eye is built from contour rings of 48 points, with 25 ring groups in the pool, and the README states that the engine deforms them point by point with spring interpolation. Expressions rotate through their eye-ring pool at randomised intervals, and blinks carry overshoot keyframes instead of a plain open-close cycle.

Two rendering details show how far the procedural approach goes. Eyes are projected onto the body's sphere with longitude conversion and cosine compression, so a fast spin carries them to the back and hides them automatically. Gaze is continuous: the whole page is tracked, smoothed with framerate-independent exponential easing, and layered with a small idle drift so the character never looks frozen. Even the celebration effects are computed live. A fast spin throws 3D ribbon trails with a 5-stop hue gradient, the thinking state keeps a ring band above the head, and the celebration state emits physics particles.

Getting a ball on screen in five minutes

There is nothing to install because there are no dependencies. Serve the repository folder with any static server:

bash
python -m http.server 8765

The root page is the three-character gallery, the Mood Mates sub-site lives under /mood-mates/ and the Emotion Ball sub-site under /emotion-ball/. Double-clicking index.html also works, but the README recommends a local server so Google Fonts load correctly.

Embedding the ball into your own page takes four script tags loaded in a fixed order: emotion-ball/js/rings.js, then emotions.js, then ball.js, then engine.js. The README notes that i18n.js and app.js belong to the gallery site and are not needed by hosts. With a 200 by 200 pixel container div in place, one call renders the ball:

js
var ball = EmotionBall.create(document.getElementById('bot'), {
  emotion: '02', idle: true
});

You should see a cream-coloured blob with idle eyes, and it will manage its own sleep and wake timing from there. Driving it from a model is one call, the same line the README uses:

js
ball.handleAIMessage('{"emotionId":"30","tips":"正在思考用户问题"}');

The tips value in that example is Chinese for thinking about the user's question; the engine does not interpret it, it just hands the string to your UI through the tips event.

Small windows, thumbnail walls and one shared heartbeat

The options that matter most in real layouts concern size and cost. eyeScale defaults to 1, and the README suggests 1.5 to 1.8 for instances below 80 pixels, where eyes otherwise become hard to read; for floating windows of 120 pixels or less it recommends eyeScale 1.5 together with lite: true, which switches off the confetti effects. shape picks between blob, wedge and gem bodies, and color and eyeColor build themed instances on the same engine.

Performance scales through one shared requestAnimationFrame heartbeat, so a tenth instance does not add a tenth loop. For thumbnail grids the documented pattern is autostart: false, which renders a single static frame, then setActive(true) on hover and setActive(false) on leave; an IntersectionObserver can pause balls outside the viewport, and renderStatic() draws one still frame while paused.

The Electron recipe is spelled out: create the window with transparent: true, frame: false, alwaysOnTop: true and skipTaskbar: true, then call win.setIgnoreMouseEvents(true, { forwardMouseMove: true }) so clicks pass through while the ball still follows the pointer. AI messages arrive over IPC, and the README's example wires ipcRenderer.on('emotion', (_, msg) => ball.handleAIMessage(msg)).

New expressions are data, not engine code

Adding a face means writing a configuration object. Each expression combines an eye-ring pool, up to three animation primitives and an optional keyframe sequence, and registers at runtime through ball.registerEmotion(raw). Six primitives are available: sine for drift and breathing, glance for a smooth square wave that pauses at each end, pulse for rhythmic scaling, jitter for decaying noise, scan for the fast triangular sweep used while retrieving, and blink for periodic closure that staggers automatically across instances.

The custom range starts at '50'. The README's own example uses that id, rotates through a pool of four eye rings every 2500 to 4500 milliseconds, and glances the eyes on a 3000 millisecond period:

js
{
  id: '50', name: '自定义', group: 'custom',
  transition: 380,
  gaze: true,
  pool: [2, 11, 17, 19],
  poolMs: [2500, 4500],
  blinkMs: [2500, 5000],
  openness: 1,
  body: { breathe: 0.014, color: '#F6EFE4', zzz: 0, orbit: 0 },
  anims: [ { target: 'eyes', prop: 'lookY', type: 'glance', amp: 6, period: 3000 } ]
}

Because IDs above 49 are open and existing numbers are never renumbered, a custom '50' survives upstream updates. Configurations can be imported and exported, so a face you design travels with the data rather than the code.

The licence splits the project in two

Read the licence before anything else, because it is unusual. GitHub reports NOASSERTION since the repository ships its own documents. For the emotion-ball/ directory, the visual character designs, meaning the blob, wedge and gem body shapes with their colours and effects, are licensed for personal learning and research only, and the README states that commercial use of them is forbidden with no commercial licence ever to be offered. The engine source code and the expression configuration data are separately dual-licensed: free for non-commercial use, with commercial licences available, as detailed in LICENSE, LICENSE-COMMERCIAL.md and NOTICE.md. The mood-mates/ sub-project, holding the original characters Nimbo and Twinkle, is separately dual-licensed and not subject to the ball-art restriction.

That is a real limitation, not paperwork. A team that wants the exact ball look in a shipped product has no licensed path to it. Two more boundaries are worth naming. The gallery depends on Google Fonts, so fully offline deployments need that checked, and the repository tree holds no package.json, so integration is script tags rather than an npm module; teams that need one will have to wrap it themselves. The last push was on 2026-08-24.

Lottie by comparison: authored timelines versus runtime data

The established alternative for character expression is Lottie, the vector animation format originally from Airbnb, played back in the browser by the lottie-web player. A Lottie character is authored in After Effects, exported to a JSON timeline, and played back frame by frame.

The difference in approach is concrete. Adding an expression to a Lottie character is an editor round trip: design the keyframes, re-export, ship a new file. In Emotion Ball the README's claim is that an expression is a data object combining an eye-ring pool and up to three primitives, registered at runtime, so new faces ship as configuration. Lottie offers a designer's freedom, arbitrary vector animation of anything; Emotion Ball offers a fixed three-form character whose faces are computed live and parameterised. For a state indicator on an agent, the second trade usually fits. For a marketing character with bespoke motion, the first does.

Editorial conclusion

Emotion Ball fits developers who already run an agent and want a lightweight, framework-free state indicator: chat sidebars, Electron desktop pets, floating assistants. It does not fit teams that need the ball characters themselves in a commercial product, since the README reserves the blob, wedge and gem visuals for learning with no commercial licence ever; the separately dual-licensed mood-mates/ characters are the path to look at instead. Before adopting, read LICENSE, LICENSE-COMMERCIAL.md and NOTICE.md in that order, view the gallery to judge whether the aesthetic suits your product, and confirm that four script tags fit your build pipeline, because the tree contains no package.json. The last push was on 2026-08-24, and the zero-dependency claim is checkable in the repository's own file list.

Frequently asked questions

Does Emotion Ball need a framework or a build step?

No. The README lists HTML, SVG and plain JavaScript as the whole stack: four script tags create an instance, with no build step and no dependencies.

What happens if my model sends an unknown emotionId?

handleAIMessage fires an error event and switches the ball to the fallback expression, which is '02' unless you pass a different fallbackId. Unparseable JSON and missing fields take the same path.

Can I use Emotion Ball in a commercial product?

The engine source and expression data are dual-licensed: free for non-commercial work, with commercial licences available under LICENSE-COMMERCIAL.md. The ball characters' visual designs in emotion-ball/ are learning-only, and the README states no commercial licence will ever be offered for them.

Official sources

  1. Issues
  2. Project website
  3. README
  4. sam70361/aora-bot on GitHub
Community notes

Community notes