@convai/embody 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @convai/embody
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8bfb1ab: Drag to turn the character, scroll to move in and out — on by default.
8
+
9
+ A 3D character you cannot walk around is a picture of one, and
10
+ `<ConvaiCharacter />` exists to be the batteries-included view. Until now
11
+ every consumer that wanted to look at the side of a head had to drop to
12
+ `CharacterModel` and build its own `Canvas`, which is most of what the
13
+ component was there to spare them.
14
+
15
+ `orbit` accepts `false` for a fixed camera, or an object to tune the limits:
16
+ `minDistance`, `maxDistance`, `minPolarAngle`, `maxPolarAngle`, `enablePan`,
17
+ `enableZoom`, `autoRotate`, `autoRotateSpeed`.
18
+
19
+ The defaults are tuned for one subject — a standing human — rather than for a
20
+ general scene viewer. Panning is off, because it only ever slides the
21
+ character off screen, and the vertical sweep stops a few degrees below eye
22
+ level, because orbiting under the floor shows the inside of a head. It uses
23
+ three's own `OrbitControls` rather than `@react-three/drei`, so it costs no
24
+ new dependency: `three` is already a peer and the `dependencies` block stays
25
+ empty.
26
+
27
+ Minor rather than patch because a character that did not move now moves with
28
+ no code change on the consumer's side.
29
+
3
30
  ## 0.3.0
4
31
 
5
32
  ### Minor Changes
@@ -1,4 +1,5 @@
1
1
  import { type CharacterModelProps } from './CharacterModel.js';
2
+ import { type OrbitOptions } from './OrbitRig.js';
2
3
  export interface ConvaiCharacterProps extends CharacterModelProps {
3
4
  /** Wrapper style. Defaults to filling the parent. */
4
5
  style?: React.CSSProperties;
@@ -14,8 +15,16 @@ export interface ConvaiCharacterProps extends CharacterModelProps {
14
15
  fov?: number;
15
16
  target?: [number, number, number];
16
17
  };
18
+ /**
19
+ * Drag to turn the character, scroll to move in and out. On by default: a
20
+ * 3D character you cannot walk around is a picture of one, and this
21
+ * component's whole job is the batteries-included view.
22
+ *
23
+ * Pass `false` for a fixed camera, or an object to tune the limits.
24
+ */
25
+ orbit?: boolean | OrbitOptions;
17
26
  /** Rendered while the body is still loading. */
18
27
  fallback?: React.ReactNode;
19
28
  }
20
- export declare function ConvaiCharacter({ style, className, background, camera, fallback, ...character }: ConvaiCharacterProps): import("react").JSX.Element;
29
+ export declare function ConvaiCharacter({ style, className, background, camera, orbit, fallback, ...character }: ConvaiCharacterProps): import("react").JSX.Element;
21
30
  export default ConvaiCharacter;
@@ -24,9 +24,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
24
  import { Suspense } from 'react';
25
25
  import { Canvas } from '@react-three/fiber';
26
26
  import { CharacterModel } from './CharacterModel.js';
27
+ import { OrbitRig } from './OrbitRig.js';
27
28
  const DEFAULT_POSITION = [0, 1.45, 3.6];
28
29
  const DEFAULT_TARGET = [0, 0.92, 0];
29
- export function ConvaiCharacter({ style, className, background = '#17181c', camera, fallback = null, ...character }) {
30
+ export function ConvaiCharacter({ style, className, background = '#17181c', camera, orbit = true, fallback = null, ...character }) {
30
31
  const position = camera?.position ?? DEFAULT_POSITION;
31
32
  const target = camera?.target ?? DEFAULT_TARGET;
32
33
  return (_jsxs("div", { className: className, style: { width: '100%', height: '100%', minHeight: '100vh', ...style }, children: [_jsxs(Canvas, { camera: { position, fov: camera?.fov ?? 35, near: 0.05, far: 100 },
@@ -34,7 +35,9 @@ export function ConvaiCharacter({ style, className, background = '#17181c', came
34
35
  // existing page content rather than in a box of its own.
35
36
  gl: { antialias: true, alpha: background === null },
36
37
  // Aim at the character's head rather than the origin at its feet;
37
- // r3f points the camera at [0,0,0] otherwise.
38
- onCreated: ({ camera: cam }) => cam.lookAt(...target), children: [background !== null && _jsx("color", { attach: "background", args: [background] }), _jsx("hemisphereLight", { args: [0xffffff, 0x334455, 1.4] }), _jsx("directionalLight", { position: [2, 4, 3], intensity: 2.2 }), _jsx(Suspense, { fallback: null, children: _jsx(CharacterModel, { ...character }) })] }), fallback] }));
38
+ // r3f points the camera at [0,0,0] otherwise. Harmless when orbit is
39
+ // on — OrbitRig takes the same target and drives the camera from
40
+ // there — and load-bearing when it is off.
41
+ onCreated: ({ camera: cam }) => cam.lookAt(...target), children: [background !== null && _jsx("color", { attach: "background", args: [background] }), _jsx("hemisphereLight", { args: [0xffffff, 0x334455, 1.4] }), _jsx("directionalLight", { position: [2, 4, 3], intensity: 2.2 }), orbit !== false && (_jsx(OrbitRig, { target: target, options: orbit === true ? undefined : orbit })), _jsx(Suspense, { fallback: null, children: _jsx(CharacterModel, { ...character }) })] }), fallback] }));
39
42
  }
40
43
  export default ConvaiCharacter;
@@ -0,0 +1,30 @@
1
+ export interface OrbitOptions {
2
+ /** Closest the camera may come, in metres. */
3
+ minDistance?: number;
4
+ /** Furthest the camera may retreat, in metres. */
5
+ maxDistance?: number;
6
+ /** Highest the camera may rise, in radians from straight up. */
7
+ minPolarAngle?: number;
8
+ /** Lowest the camera may sink. Past `PI/2` it starts looking up from below. */
9
+ maxPolarAngle?: number;
10
+ /** Drag the subject around the frame. Off by default — it loses the character. */
11
+ enablePan?: boolean;
12
+ /** Scroll to dolly in and out. */
13
+ enableZoom?: boolean;
14
+ /** Turn the character slowly on its own, for an idle showcase. */
15
+ autoRotate?: boolean;
16
+ /** Degrees per second when `autoRotate` is on. */
17
+ autoRotateSpeed?: number;
18
+ }
19
+ export interface OrbitRigProps {
20
+ /** The point orbited around — the character's head, not its feet. */
21
+ target: [number, number, number];
22
+ options?: OrbitOptions;
23
+ }
24
+ /**
25
+ * Renders nothing. Lives inside `<Canvas>` so it can reach the camera and the
26
+ * canvas element, which is also why it cannot simply be set up in `onCreated`
27
+ * — damping needs a per-frame `update()`.
28
+ */
29
+ export declare function OrbitRig({ target, options }: OrbitRigProps): null;
30
+ export default OrbitRig;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Drag-to-orbit for the character view.
3
+ *
4
+ * Uses three's own `OrbitControls` rather than `@react-three/drei` so this
5
+ * costs no new dependency: `three` is already a peer, and embody ships with
6
+ * an empty `dependencies` block that is worth keeping empty.
7
+ *
8
+ * The defaults are tuned for one subject — a standing human — rather than for
9
+ * a general scene viewer. Panning is off and the vertical sweep stops just
10
+ * below eye level, because in a character viewer both of those only ever lose
11
+ * the character: pan slides it off screen, and orbiting under the floor shows
12
+ * the inside of a head.
13
+ */
14
+ import { useFrame, useThree } from '@react-three/fiber';
15
+ import { useEffect, useRef, useState } from 'react';
16
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
17
+ /**
18
+ * Roughly 20 degrees above the head down to a few degrees below eye level.
19
+ * `PI/2` is dead level with the orbit target, so the small overshoot buys a
20
+ * slight up-angle without ever reaching the floor.
21
+ */
22
+ const DEFAULTS = {
23
+ minDistance: 0.6,
24
+ maxDistance: 8,
25
+ minPolarAngle: Math.PI * 0.12,
26
+ maxPolarAngle: Math.PI * 0.52,
27
+ enablePan: false,
28
+ enableZoom: true,
29
+ autoRotate: false,
30
+ autoRotateSpeed: 1.2,
31
+ };
32
+ /**
33
+ * Renders nothing. Lives inside `<Canvas>` so it can reach the camera and the
34
+ * canvas element, which is also why it cannot simply be set up in `onCreated`
35
+ * — damping needs a per-frame `update()`.
36
+ */
37
+ export function OrbitRig({ target, options }) {
38
+ const camera = useThree((s) => s.camera);
39
+ const domElement = useThree((s) => s.gl.domElement);
40
+ // Created and disposed in the SAME effect, never in useMemo. Disposal
41
+ // removes the listeners OrbitControls put on the canvas — without it a
42
+ // remounted character leaves a live controller behind and two fight over
43
+ // every drag. But a memoised instance disposed in a cleanup is a trap:
44
+ // React StrictMode runs mount → cleanup → mount in dev, and the second
45
+ // mount reused the same, already-disposed instance. The rig then rendered
46
+ // fine and did nothing until something recreated the controls — a resize,
47
+ // an HMR reload — which is exactly "orbit comes and goes". Pairing
48
+ // construction with disposal means the remount gets a live instance.
49
+ const [controls, setControls] = useState(null);
50
+ const controlsRef = useRef(null);
51
+ useEffect(() => {
52
+ const next = new OrbitControls(camera, domElement);
53
+ controlsRef.current = next;
54
+ setControls(next);
55
+ return () => {
56
+ next.dispose();
57
+ if (controlsRef.current === next)
58
+ controlsRef.current = null;
59
+ };
60
+ }, [camera, domElement]);
61
+ const [tx, ty, tz] = target;
62
+ const { minDistance, maxDistance, minPolarAngle, maxPolarAngle, enablePan, enableZoom, autoRotate, autoRotateSpeed, } = { ...DEFAULTS, ...options };
63
+ useEffect(() => {
64
+ if (!controls)
65
+ return;
66
+ controls.target.set(tx, ty, tz);
67
+ controls.minDistance = minDistance;
68
+ controls.maxDistance = maxDistance;
69
+ controls.minPolarAngle = minPolarAngle;
70
+ controls.maxPolarAngle = maxPolarAngle;
71
+ controls.enablePan = enablePan;
72
+ controls.enableZoom = enableZoom;
73
+ controls.autoRotate = autoRotate;
74
+ controls.autoRotateSpeed = autoRotateSpeed;
75
+ // Damping is what makes a drag feel like weight rather than a jump cut.
76
+ controls.enableDamping = true;
77
+ controls.dampingFactor = 0.08;
78
+ controls.update();
79
+ }, [
80
+ controls, tx, ty, tz,
81
+ minDistance, maxDistance, minPolarAngle, maxPolarAngle,
82
+ enablePan, enableZoom, autoRotate, autoRotateSpeed,
83
+ ]);
84
+ useFrame(() => {
85
+ // The ref, not the state: a frame can run between dispose and the
86
+ // re-render that clears the state, and updating a disposed controller
87
+ // is harmless but pointless.
88
+ controlsRef.current?.update();
89
+ });
90
+ return null;
91
+ }
92
+ export default OrbitRig;
@@ -31,6 +31,8 @@ export { ConvaiCharacter, default } from './ConvaiCharacter.js';
31
31
  export type { ConvaiCharacterProps } from './ConvaiCharacter.js';
32
32
  export { CharacterModel } from './CharacterModel.js';
33
33
  export type { CharacterModelProps } from './CharacterModel.js';
34
+ export { OrbitRig } from './OrbitRig.js';
35
+ export type { OrbitOptions, OrbitRigProps } from './OrbitRig.js';
34
36
  export { useWidgetClient } from './useWidgetClient.js';
35
37
  export type { WidgetClient } from './useWidgetClient.js';
36
38
  export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
@@ -28,6 +28,9 @@
28
28
  export { useCharacter } from './useCharacter.js';
29
29
  export { ConvaiCharacter, default } from './ConvaiCharacter.js';
30
30
  export { CharacterModel } from './CharacterModel.js';
31
+ // Exported for apps that build their own <Canvas> around CharacterModel and
32
+ // still want the orbit behaviour ConvaiCharacter gives for free.
33
+ export { OrbitRig } from './OrbitRig.js';
31
34
  export { useWidgetClient } from './useWidgetClient.js';
32
35
  // ── Chat / conversation surface, re-exported from @convai/web-sdk/react ────
33
36
  export { ConvaiWidget, useConvaiClient, AudioRenderer, AudioContext } from '@convai/web-sdk/react';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@convai/embody",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Give a Convai character a body: resolve 3D characters by characterId and drive them with lipsync and animation. Layered on @convai/web-sdk.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Convai <support@convai.com>",
@@ -79,6 +79,7 @@
79
79
  "@types/react-dom": "^19.2.7",
80
80
  "@types/react-test-renderer": "18.3.1",
81
81
  "@types/three": "^0.182.0",
82
+ "jsdom": "^25.0.1",
82
83
  "react": "^18.3.1",
83
84
  "react-dom": "^18.3.1",
84
85
  "react-test-renderer": "18.3.1",