Skip to content

About

Animated GrokBot avatar for the web — a TypeScript/Canvas 2D port of the Flutter widget nasawz/GrokBot. 25 expressions, 18 shapes, 39 states, zero runtime dependencies.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

grokbot-web

This is a port, not original work. It is based on nasawz/GrokBot — a pure Flutter animated avatar widget by nasawz, licensed BSD-3-Clause. All of the design work is theirs: the 25 expression point sets, the 18 shape geometries, the 39 state expression pools and cadences, and the projection, spring and blink math. This repository translates that widget to TypeScript and the Canvas 2D API and adds nothing to its visual design. If you want the Flutter version, go upstream.

An animated GrokBot avatar for the web. 25 expressions, 18 body shapes, 39 behavior states, gaze tracking, head turn, spring expression morphs and blinks — drawn with the Canvas 2D API, in TypeScript, with zero runtime dependencies.

Because the data tables and the math are carried over unchanged, this port and the Flutter original render the same avatar.

简体中文


Install

npm install grokbot-web

Use it

Three entry points, one engine underneath. Pick whichever suits the app.

Plain TypeScript

import { GrokBot } from 'grokbot-web';

const bot = new GrokBot(document.querySelector('canvas')!, {
  state: 'thinking',
  shape: 'blob',
  size: 160,
  autoExpression: true,
});

await bot.blink();
await bot.spin({ turns: 1, durationMs: 1200 });
bot.update({ state: 'excited', gaze: { x: 0.3, y: -0.1 } });
bot.destroy(); // on teardown

Custom element

<script type="module">import 'grokbot-web/element';</script>

<grok-bot state="listening" shape="capsule" size="140" auto-expression></grok-bot>

Attributes mirror the options in kebab-case (gaze-x, gaze-y, eye-scale, spring-frequency, flip-x, show-guides, body-color, eye-color, label). Booleans follow HTML rules — present means true — except auto-blink, which is on by default and is turned off with auto-blink="false". The element also exposes blink(), spin(), reset() and .bot.

React

import { useRef } from 'react';
import { GrokBotView, type GrokBotHandle } from 'grokbot-web/react';
import { darkTheme } from 'grokbot-web';

function Assistant({ busy }: { busy: boolean }) {
  const bot = useRef<GrokBotHandle>(null);
  return (
    <>
      <GrokBotView
        ref={bot}
        state={busy ? 'working' : 'idle'}
        shape="squircle"
        size={200}
        autoExpression
        theme={darkTheme}
        label="Assistant avatar"
      />
      <button onClick={() => bot.current?.blink()}>Blink</button>
    </>
  );
}

The engine is created once per mount and patched on prop changes, so a React re-render never restarts an in-flight morph or blink.


Options

Option Type Default What it does
state GrokBotState 'idle' Picks the expression pool and the automatic cadences
shape GrokBotShape 'blob' Silhouette plus its face geometry correction
expression 0…24 | null null Pins one expression; null hands control back to the state
gaze { x, y } { x: 0, y: 0 } Normalized, clamped to [-1, 1]; maps to about ±13.2 × ±8.4
turn number 0 Base head turn in radians; spin() adds on top
eyeScale number 1 Eye size multiplier, must be > 0
springFrequency number 7 Natural frequency ω of the morph spring, must be > 0
autoBlink boolean true Blink on the state's cadence
autoExpression boolean false Drift through the state's pool while no expression is pinned
flipX boolean false Mirror horizontally
emphasis boolean false Enlarge the eyes by 18%
showGuides boolean false Draw the sphere guide and the projected eye centroids
theme Partial<GrokBotTheme> light preset Merged onto the current palette
size number | 'auto' 'auto' CSS pixels, or track the canvas box with ResizeObserver
ariaLabel string 'GrokBot' Written to the canvas as role="img" + aria-label
respectReducedMotion boolean true Honour prefers-reduced-motion
random () => number Math.random Randomness source, injectable for deterministic tests

Methods

Method What it does
update(patch) Applies a partial options patch, restarting only what changed
blink() One 320 ms blink (42% closing, 58% opening, floor at 4% height)
spin({ turns, durationMs }) Spins whole turns; a new spin completes and replaces the active one
reset() Cancels transient animations and returns to the resting face
render() Paints one frame immediately, outside the loop
destroy() Stops every timer, frame and observer

options, expression and isAnimating are readable properties.


States, shapes, expressions

39 states. Lifecycle: sleeping waking idle listening thinking searching working. Reactions: excited surprised suspicious angry drowsy happy curious confused bored proud shy sad laughing scared playful celebrate. Agent shapes: orbit radar progress. Product lifecycle: spawning humming loading dictating writing sending receiving uploading notifying alerting dragging bouncing powering-down.

18 shapes. blob pebble bean egg squircle tablet capsule cylinder hex gem crystal wedge shield dome arch cloud teardrop leaf.

25 expressions, indexed 0–24. Each is two closed 48-point rings.

The tables are exported, so a picker can be built from them:

import { stateNames, shapeNames, expressionIndexes, stateData } from 'grokbot-web';

stateData.thinking.expressions;     // [8, 16, 14, 17, 5]
stateData.thinking.blinkCadence;    // { min: 3500, max: 7000 }
stateData.sleeping.blinkCadence;    // null — this state never blinks

How a frame is built

state / timers
  → damped spring morph, integrated at a fixed 1/120 s step
  → lerp between two 48-point rings
  → gaze offset + spherical projection for the head turn
  → Canvas 2D: fill body, clip to it, fill both eyes
  • Gaze. Normalized input is clamped and mapped to a face-space offset of about ±13.2 × ±8.4 units.
  • Head turn. Each eye centroid's horizontal offset becomes a longitude on the head sphere. The turn rotates it; cos(longitude) both narrows the eye and hides it once it passes behind the head. Shapes flagged squashOnTurn also compress the body, but not the face inside it.
  • Blink. 320 ms, closing faster than opening.
  • Loop. The animation frame loop runs only while a morph, blink or spin is live. Idle avatars cost nothing.

Differences from the Flutter original

Deliberate, and each for a reason:

  • Canvas 2D, not SVG. The original's CustomPaint model — nested transforms, a clip path, per-frame polygon fills — maps onto Canvas 2D one call at a time. SVG would mean rewriting 96 path points into the DOM every frame, which is slower and drifts from the original's semantics.
  • No separate controller type. A Flutter widget is immutable, so transient animations need an external handle. A GrokBot instance is long-lived and owns both the declarative options and the imperative moments.
  • String literal types, not enums. 'powering-down' is the state's actual name, so it survives JSON, HTML attributes and query params with no mapping layer. This removes upstream's sourceName extension entirely.
  • Cubic(0.645, 0.045, 0.355, 1.0) is reproduced exactly, including the fact that it is not quite point-symmetric — y(0.5) = 0.516875. Spin timing matches upstream frame for frame.
  • Web-only additions. Device pixel ratio handling, ResizeObserver sizing, prefers-reduced-motion support, an injectable random, and a first paint that does not wait for an animation frame — otherwise a bot constructed in a background tab stays blank until the tab is focused.
  • Rings are flat number arrays, not point objects: 4,800 numbers with no per-point allocation on the interpolation path.

Browser support

Any browser with CanvasRenderingContext2D.ellipse — Chrome, Edge, Firefox, Safari 13+. Rounded corners are traced with elliptical arcs rather than roundRect, so no recent-Safari requirement sneaks in. ResizeObserver and matchMedia are feature-detected; without them, pass an explicit size.

Develop

npm install
npm test          # 55 tests: geometry, easing, painter, engine, custom element
npm run typecheck
npm run build
npm run demo      # the playground

The expression table is generated from the upstream Dart source:

git clone https://github.com/nasawz/GrokBot .reference
npm run data

Credits

Every bit of the avatar's design comes from nasawz/GrokBot by nasawz. This repository is a language and rendering-target port of that project:

Carried over unchanged Rewritten for the web
25 expressions (2 eyes x 48 points each) Canvas 2D painter, replacing CustomPaint
18 shape geometries and their face corrections Rounded-corner tracing, replacing RRect
39 state expression pools, expression and blink cadences Animation frame loop, replacing Ticker
Spherical eye projection, spring morph, blink and spin math Options / patch API, replacing widget + controller
Theme presets Sizing, DPR, reduced motion, tests

Generated data files and every ported source file carry an upstream reference in their header comment. scripts/convert_expressions.py shows exactly how the expression table is derived from the upstream Dart source.

If this is useful to you, star the original.

License

BSD 3-Clause, the same as the original, and the upstream copyright notice is retained. See LICENSE.

About

Animated GrokBot avatar for the web — a TypeScript/Canvas 2D port of the Flutter widget nasawz/GrokBot. 25 expressions, 18 shapes, 39 states, zero runtime dependencies.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages