Aora Bot ships an SVG emotion engine where the assistant only has to output an emotionId
Emotion Ball 是一套面向 AI 助手的表情引擎:32 种状态表情全部由纯 SVG 与原生 JavaScript 实时驱动,零框架、零图片资源。AI 侧只需输出一个 emotionId,小球即可切换到对应表情,可直接用作聊天机器人、桌面宠物、悬浮助手的情绪表达层。
At a glance
- What is it?
- The Emotion Ball engine gives an assistant 32 state expressions drawn as SVG and animated with plain JavaScript, no framework and no build step. The catch is licensing: the ball characters are study-only, while the engine and expression data are dual-licensed and the original-character subproject is separate.
- Who is it for?
- This engine fits a team building a chat widget, a desktop pet or a floating assistant that needs a readable emotional state without pulling in a rendering framework or a build pipeline, since four script tags and one JSON string are the entire integration surface.
- 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 39 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 October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three licence files, and the ball characters are never commercial
Licensing here is the first thing to read, because it is not a single decision. The repository splits the work in two and states it in a licensing note at the top. The visual appearance of the spherical characters in emotion-ball/ is restricted to personal study and research, and that restriction covers the blob, wedge and gem body shapes, the colour schemes and the effect visuals. The note says commercial use is prohibited for that imagery and that commercial authorisation will never be provided for it. The engine source and the expression configuration data, meaning the eye and mouth parameters, the animation primitives and the keyframe sequences, are separately and independently written, and those are dual-licensed: free for non-commercial work, available under an authorisation for commercial use. The mood-mates/ directory is a third case, holding original characters of its own under the dual licence and explicitly outside the restriction above. Three files carry this: LICENSE, LICENSE-COMMERCIAL.md and NOTICE.md. That is also why the repository metadata reports no standard licence identifier.
One gallery at the root drives two engines and three characters
The repository is laid out as a main gallery plus two sub-projects, and the distinction is the point rather than the folder nesting. The root index.html is the gallery, where three characters appear on the same wall of 32 expressions and clicking a character card at the top swaps the whole gallery. That page is driven by two engines working side by side, one for each sub-project, with the site/ directory providing the shell that holds the styles, the copy and the interaction layer that adapts between them. mood-mates/ is the original-character project with its own multi-character architecture, its own gallery page and its own integration documentation. emotion-ball/ is the spherical-character project, with 32 expressions across 3 body shapes and a separate gallery page of its own. Each sub-site is reachable on its own path under the same static server, so you can adopt one engine without dragging in the other, or point at the gallery to compare how the same expression reads across three different character designs.
The emotionId is two digits and published numbers never move
The ID scheme is the contract between your model and the face, and it is designed so integrators can hardcode it. Every emotionId is two digits where the first digit is the group prefix: 00 to 09 covers lifecycle states such as sleeping, waking and standby, 10 to 29 covers emotional reactions such as happy, shy, angry and surprised, 30 to 49 covers agent work states such as thinking, retrieving references, error and task complete, and 50 and above is reserved for custom expressions. Gaps between the groups are deliberately left open so new expressions can be added without collision, and the note is explicit that existing numbers are never rearranged. That is an unusual promise for an art asset library and it is the reason a host can store a preference for expression 21 without checking version first. The default starting expression and the default fallback are both 02, which is the standby state, and the fallback id is configurable per instance.
handleAIMessage takes an object or a string and never white-screens
The AI side of the protocol is one call, and the design goal is that a bad message cannot break the page:
ball.handleAIMessage('{"emotionId":"30","tips":"正在思考用户问题"}');The method accepts either an object or a JSON string, so a host can forward raw model output without parsing it first. If the emotionId is unknown, if the JSON fails to parse, or if a required field is missing, the engine fires an error event and falls back to standby using the configured fallbackId, which defaults to 02. The tips field is optional and is surfaced through a separate tips event, leaving the host to decide how to render the accompanying text. The same three-way failure handling covers the event surface as a whole: a change event carries the new id and its definition, and a host that wants to know what the face is doing listens for those events rather than polling it.
One eye system projected onto three outlines, hidden when it spins away
How the face is drawn is worth understanding because it is what lets one expression set serve three different bodies. The eye system is built from 25 sets of 48-point contour rings, and morphing between them is done by interpolating each point with a spring, which is why the eyes deform rather than snap. The rings are then projected onto the body surface: the eye position is converted by longitude and compressed with a cosine so it sits correctly on a round shape, on a triangle and on a diamond, and an eye that rotates past the back of the body is hidden instead of showing through. Blink timing carries overshoot keyframes, and the pool of rings rotates on its own interval so a held expression still moves. Gaze tracking is page-wide, with exponential smoothing that is independent of frame rate, layered over a permanent slow drift of the eyes so the face never looks frozen when nobody is moving the mouse.
Six animation primitives, at most three stacked on one expression
Expressions are pure data, and the vocabulary is small on purpose. An expression is an eye-ring pool plus animation primitives plus an optional keyframe sequence, and each expression stacks at most three animations chosen from six primitives. sine covers drift, breathing and saccades with amplitude, period and phase. glance is a smoothed square wave that dwells at both ends, which is what reads as looking left then right. pulse scales from zero to its amplitude rhythmically, jitter is pseudo-noise that can decay over time, scan is a fast triangle-wave sweep used for searching or scanning states, and blink closes the eyes periodically, with multiple instances automatically offset in phase so a wall of faces does not blink in unison. A primitive targets eyes, body, left or right and animates one of a fixed set of properties. A keyframe sequence adds a one-off performance on entering the expression, then settles by falling back to the base pose, holding the final frame, or switching to another expression.
Four script tags, one static server, and no build step at all
Setup is a static file server and four script tags in order. The demonstration uses Python's built-in server on a fixed port, after which the gallery and both sub-sites are reachable on their own paths:
python -m http.server 8765Opening index.html directly also works, though the note recommends going through a local server so Google Fonts load as intended. For a host integration, rings.js, emotions.js, ball.js and engine.js are the four files that matter; i18n.js and app.js belong to the gallery page and are not needed. The instance is created against a container element with a starting expression and the idle strategy:
<script src="emotion-ball/js/rings.js"></script>
<script src="emotion-ball/js/emotions.js"></script>
<script src="emotion-ball/js/ball.js"></script>
<script src="emotion-ball/js/engine.js"></script>
<div id="bot" style="width:200px;height:200px"></div>
<script>
var ball = EmotionBall.create(document.getElementById('bot'), {
emotion: '02', idle: true
});
</script>The creation options are where the practical tuning lives. eyeScale multiplies eye size, with a recommendation of 1.5 to 1.8 for instances under 80 pixels so the face stays readable. idle controls the automatic switch to standby or sleep after a timeout and accepts an object for custom durations and target expression. autostart set to false renders a single static frame without entering the animation loop, which is exactly what a thumbnail wants. lite turns off the ribbon and confetti effects and otherwise follows autostart. Custom expressions can be registered at runtime, and the whole configuration set can be exported and imported as JSON.
Every instance shares one animation heartbeat and a viewport switch
Performance guidance is specific enough to be useful. All instances share a single requestAnimationFrame heartbeat, so adding faces does not add animation loops. For a wall of thumbnails the documented pattern is to render statically with autostart off, then call setActive on hover to start the loop and again on mouse-out to stop it, and to drive the same switch from an IntersectionObserver when the face leaves the viewport. The effect methods are the ones a host binds to its own triggers:
ball.setEmotion('21');
ball.setGaze(nx, ny);
ball.setStyle({ sketch: 1 });
ball.spin(3);
ball.burst(24);
ball.bounce();
ball.startTour(ids, 2500);
ball.setActive(false);setGaze takes normalised coordinates in the range minus one to one, which means the host owns the pointermove listener rather than the engine. setStyle switches to the sketch mode, spin throws the ribbon trail, burst fires the particle confetti used for celebration, and startTour auto-plays a list of expressions on an interval. The Electron notes cover the desktop-pet case: a transparent, frameless, always-on-top window with the taskbar skipped, mouse pass-through enabled while still forwarding movement so gaze keeps working, and AI messages arriving over IPC into handleAIMessage.
Editorial conclusion
This engine fits a team building a chat widget, a desktop pet or a floating assistant that needs a readable emotional state without pulling in a rendering framework or a build pipeline, since four script tags and one JSON string are the entire integration surface. It does not fit a commercial product that wants the ball characters themselves, because those visuals are declared study-only and never commercially licensed, and it does not fit anyone who needs a single recognised licence file, since the repository ships three separate licence documents. Before adopting it, read LICENSE, LICENSE-COMMERCIAL.md and NOTICE.md together, pick your emotionId ranges against the reserved gaps in the numbering, and confirm the fallback id of 02 matches the standby expression your host expects.
Frequently asked questions
How many expressions does the Aora Bot emotion engine have?
Thirty-two, in three groups: lifecycle states, emotional reactions such as happy, shy, angry and surprised, and agent work states such as thinking, retrieving references, error and task complete. Every expression is driven by configuration rather than by hand-written animation code.
What happens when the AI sends an emotionId the engine does not know?
The engine fires an error event and falls back to standby, using the instance's fallbackId, which defaults to 02. The same fallback covers a JSON parse failure and a message missing required fields, so the page never goes blank.
Can I use the Emotion Ball characters in a commercial product?
The character visuals are declared study-only, with commercial use prohibited and no commercial authorisation offered for them. The engine source and the expression data are dual-licensed, free for non-commercial use and available under a commercial authorisation, and the mood-mates characters are a separate dual-licensed project outside that restriction.
Which body shapes does the emotion engine support?
Three: a round blob, a triangular wedge and a diamond gem, all driven by the same eye and animation system and adapted to each outline. Theme-coloured instances and a sketch mode are also available.
Does Aora Bot need a build step or any dependencies?
No. It is HTML, SVG and plain JavaScript with no framework, no dependencies and no build step. A host includes four scripts in order and creates an instance against a container element; a static file server is enough to run the showcase sites.
How do I keep many on-screen faces cheap?
All instances share one animation heartbeat, so the loop cost does not grow with the number of faces. For a thumbnail wall, render with autostart off and toggle setActive on hover or from an IntersectionObserver when the face scrolls out of view.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sam70361-aora-bot)