# Hand Animations
> Lectern's hand animation system uses the **Bedrock animation format** to drive first-person hand and item rendering. Animations can be authored in Blockbench and delivered via resource pack or sent inline from the server.
---
## Overview
The hand animation system provides two levels of first-person hand control:
| Effect | Packet ID | Description |
|---|---|---|
| **HandPose** | `hand_pose` | Static pose — holds out the hand with an item floating and spinning above it. Good for item showcases. |
| **HandAnimation** | `hand_animation` | Keyframed animation — plays a full Bedrock animation on the hand and item. Supports looping, crossfading, and inline loading. |
HandAnimation takes rendering priority over HandPose. If both are active, only the animation renders.
---
## Hand Pose
A simple effect that positions the player's arm in a showcase pose with the item floating, spinning, and bobbing above the palm.
### Server-Side Usage
```java
IScreenManager screen = ILecternAPI.get().getScreenManager();
// Show with defaults (held item, spinSpeed=2.0, bobSpeed=1.5, bobHeight=0.03)
screen.showHandPose(player);
// Show with a specific item and custom values
screen.showHandPose(player, "minecraft:diamond_sword", 3.0f, 1.0f, 0.05f);
// Stop
screen.stopHandPose(player);
```
### Packet Command
```
/packet hand_pose <player> true <itemId> <spinSpeed> <bobSpeed> <bobHeight>
/packet hand_pose <player> false
```
### Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `itemId` | `String` | `""` (held item) | The item to display. Empty string uses the player's held item. |
| `spinSpeed` | `float` | `2.0` | Spin speed multiplier. Higher = faster rotation. |
| `bobSpeed` | `float` | `1.5` | Vertical bob speed multiplier. |
| `bobHeight` | `float` | `0.03` | Vertical bob amplitude in block units. |
---
## Hand Animation
A full keyframed animation system that parses Bedrock `.animation.json` files and applies them to the first-person view. Animations target three bones:
| Bone Name | Controls |
|---|---|
| `right_arm` | The player's right arm (position, rotation). |
| `left_arm` | The player's left arm (position, rotation). |
| `item` | The held item (position, rotation, scale). |
### Server-Side Usage
```java
IScreenManager screen = ILecternAPI.get().getScreenManager();
// Play a named animation (must be loaded via resource pack or inline)
screen.playHandAnimation(player, "animation.hand.inspect");
// Play with full control
screen.playHandAnimation(player, "animation.hand.inspect", "minecraft:diamond_sword", true, 1.5f);
// Crossfade from the current animation to a new one
screen.crossfadeHandAnimation(player, "animation.hand.wave", 0.5f, 1.0f);
// Stop
screen.stopHandAnimation(player);
// Send an animation JSON directly (no resource pack needed)
String json = Files.readString(Path.of("my_animation.json"));
screen.loadInlineHandAnimation(player, "animation.hand.custom", json);
```
### Packet Command
```
/packet hand_animation <player> PLAY <animationName> <itemId> <loop> <speed>
/packet hand_animation <player> STOP
/packet hand_animation <player> CROSSFADE <animationName> <blendDuration> <speed>
/packet hand_animation <player> LOAD_INLINE <animationName> <json>
```
---
## Animation JSON Format
Lectern uses the standard **Bedrock animation format** (`format_version: "1.8.0"`). Animations can be created in [Blockbench](https://blockbench.net/) and exported as Bedrock animations.
### Structure
```json
{
"format_version": "1.8.0",
"animations": {
"animation.hand.inspect": {
"animation_length": 3.0,
"loop": true,
"return_frame": 0.5,
"bones": {
"right_arm": {
"position": { ... },
"rotation": { ... }
},
"item": {
"position": { ... },
"rotation": { ... },
"scale": { ... }
}
}
}
}
}
```
### Loop Modes
| Value | Behavior |
|---|---|
| `false` | Play once and stop. The hand returns to normal after the animation finishes. |
| `true` | Loop continuously until stopped. |
| `"hold_on_last_frame"` | Play once and freeze on the last frame. |
> When using `playHandAnimation()` with `loop=true`, the loop mode is overridden to `LOOP` regardless of what the JSON specifies.
### Return Frame
When `loop` is `true`, you can set `"return_frame"` to a timestamp (in seconds) that the animation jumps back to when it reaches the end, instead of restarting from `0.0`. This lets you create animations with a one-time intro followed by a repeating loop.
```json
{
"animation_length": 3.0,
"loop": true,
"return_frame": 0.5
}
```
In this example, the animation plays `0.0s → 3.0s` on the first pass, then loops `0.5s → 3.0s` on every subsequent pass. The intro from `0.0s` to `0.5s` only plays once.
If `return_frame` is omitted or `0`, the animation loops from the beginning as normal.
### Channels
Each bone can have three animation channels:
| Channel | Format | Description |
|---|---|---|
| `position` | `[x, y, z]` | Position offset in **Blockbench units** (1/16 of a block). Automatically converted to MatrixStack space. |
| `rotation` | `[pitch, yaw, roll]` | Rotation in degrees. |
| `scale` | `[x, y, z]` | Scale multiplier per axis. |
### Keyframe Formats
Keyframes can be specified in two formats:
**Simple array** — a static value with linear interpolation:
```json
"position": {
"0.0": [0, 0, 0],
"0.5": [-4, -3.2, -12.8]
}
```
**Object with interpolation control** — for catmull-rom or step curves:
```json
"rotation": {
"0.0": {
"pre": [0, 0, 0],
"post": [0, 0, 0],
"lerp_mode": "catmullrom"
},
"1.0": {
"pre": [-50, -20, 45],
"post": [-50, -20, 45],
"lerp_mode": "catmullrom"
}
}
```
**Static channel** — a single value applied for the entire animation:
```json
"scale": [0.3, 0.3, 0.3]
```
### Interpolation Modes
| Mode | Description |
|---|---|
| `linear` | Linear interpolation between keyframes. Default. |
| `catmullrom` | Catmull-Rom spline interpolation for smooth curves through keyframes. |
| `step` | Instant jump to the next keyframe value (no interpolation). |
---
## Loading Animations
### Via Resource Pack
Place `.animation.json` files in the `assets/lectern/animations/` directory of a resource pack. They are loaded automatically when the resource pack is applied.
```
assets/
lectern/
animations/
inspect.animation.json
wave.animation.json
custom_effect.animation.json
```
Each file can contain multiple animations. All animations in all files are registered by their full name (e.g. `animation.hand.inspect`).
### Via Inline Loading
Send the animation JSON directly from the server using `loadInlineHandAnimation()`. The animation is registered on the client and available immediately for playback. Inline animations persist until the player disconnects or the effect is reset.
```java
// Read JSON from file, database, config, etc.
String json = "{\"format_version\":\"1.8.0\",\"animations\":{...}}";
// Register it on the client
screen.loadInlineHandAnimation(player, "animation.hand.custom", json);
// Now it can be played
screen.playHandAnimation(player, "animation.hand.custom");
```
---
## Crossfading
Crossfading smoothly blends from the current animation's transforms into a new animation over a specified duration. The current animation's bone transforms are frozen at their current values and linearly interpolated toward the new animation's transforms.
```java
// Currently playing "animation.hand.inspect"
// Blend into "animation.hand.wave" over 0.5 seconds
screen.crossfadeHandAnimation(player, "animation.hand.wave", 0.5f, 1.0f);
```
---
## Example Animation
An inspect animation that raises the arm then continuously spins the item, using `return_frame` to skip the intro on loop:
```json
{
"format_version": "1.8.0",
"animations": {
"animation.hand.inspect": {
"animation_length": 3.0,
"loop": true,
"return_frame": 0.5,
"bones": {
"right_arm": {
"rotation": {
"0.0": [0, 0, 0],
"0.5": [-50, -20, 45],
"1.5": [-40, 10, 30],
"2.5": [-50, -20, 45],
"3.0": [-50, -20, 45]
},
"position": {
"0.0": [0, 0, 0],
"0.5": [-4, -3.2, -12.8],
"3.0": [-4, -3.2, -12.8]
}
},
"item": {
"position": {
"0.0": [0, 0, 0],
"0.5": [-0.8, 8, -2.4],
"3.0": [-0.8, 8, -2.4]
},
"rotation": {
"0.0": [0, 0, 0],
"1.5": [0, 180, 0],
"3.0": [0, 360, 0]
},
"scale": [0.3, 0.3, 0.3]
}
}
}
}
}
```
This animation:
1. **0.0s–0.5s** — Intro: arm raises from default position to the showcase pose (plays once)
2. **0.5s–3.0s** — Loop: arm holds position while the item spins a full 360°
3. **3.0s** — Jumps back to `0.5s` and repeats the spin
---
## Blockbench Workflow
1. Open [Blockbench](https://blockbench.net/) and create a new **Bedrock Entity** project
2. Create bones named `right_arm`, `left_arm`, and/or `item`
3. Switch to the **Animate** tab and create a new animation
4. Set keyframes for position, rotation, and scale on each bone
5. Configure loop mode and animation length
6. Export as **Bedrock Animation** (`.animation.json`)
7. Place the file in `assets/lectern/animations/` or send it inline via the API
> **Coordinate note:** Blockbench positions are in 1/16 block units (pixels). Lectern automatically divides by 16 when applying to the MatrixStack.
---
## Related Pages
- [[IScreenManager]] — API methods for hand pose and hand animation
- [[ILecternAPI]] — Main API entry point