posev
v1.1.0
Published
Professional-grade, framework-agnostic biomechanical animation SDK for sports analytics, rehabilitation apps, and motion capture systems
Downloads
50
Maintainers
Readme
PoseV — Enterprise Biomechanical Animation SDK
Professional-grade, framework-agnostic biomechanical animation engine for sports analytics, rehabilitation apps, and motion capture systems.
Table of Contents
- Installation
- Quick Start
- Core Features
- Framework Integration
- API Reference
- Plugin Guide
- Movement JSON Standard
- Rig System
- Performance & Production
- Examples
- Contributing
Installation
Via npm
npm install posevVia CDN (UMD)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/posev.umd.min.js"></script>
<script>
const { PoseVCore } = window.PoseV;
</script>Verify Installation
npm list posevQuick Start
Basic Animation (Vanilla JavaScript)
import { PoseVCore } from 'posev';
// 1. Create engine
const engine = new PoseVCore({
debug: true,
strictValidation: true,
});
// 2. Load movement data
await engine.load({
movement: './movements/squat.json',
rig: './rigs/standard-human.json',
});
// 3. Play animation
engine.play();
// 4. Listen to frame updates
engine.on('frameUpdate', (frame) => {
console.log(`Progress: ${(frame.progress * 100).toFixed(1)}%`);
console.log(`Current time: ${frame.currentTime.toFixed(2)}ms`);
// Update your UI or renderer here
renderPose(frame.pose);
});
// 5. Control playback
engine.pause();
engine.seek(0.5); // Seek to 50%
engine.setSpeed(0.5); // Slow motion
engine.play();
// Cleanup
engine.dispose();Vue 3 Integration
<template>
<div class="posev-container">
<canvas ref="canvas" width="800" height="600"></canvas>
<div class="controls">
<button @click="togglePlayPause">
{{ isPlaying ? 'Pause' : 'Play' }}
</button>
<input
type="range"
min="0"
max="1"
:value="progress"
@input="seek"
/>
<span>{{ formatTime(currentTime) }} / {{ formatTime(duration) }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { usePoseV } from 'posev';
const canvas = ref<HTMLCanvasElement>();
const { play, pause, seek, getState } = usePoseV({
movement: './movements/squat.json',
rig: './rigs/standard-human.json',
renderer: {
canvas: canvas.value,
width: 800,
height: 600,
},
autoplay: true,
});
const state = computed(() => getState());
const isPlaying = computed(() => state.value?.playback === 'playing');
const progress = computed(() => state.value?.progress ?? 0);
const currentTime = computed(() => state.value?.currentTime ?? 0);
const duration = computed(() => state.value?.duration ?? 0);
const togglePlayPause = () => {
isPlaying.value ? pause() : play();
};
const formatTime = (ms: number) => {
const seconds = (ms / 1000).toFixed(2);
return seconds;
};
</script>
<style scoped>
.posev-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>React Integration
import { useEffect, useRef, useState } from 'react';
import { PoseVCore, PerformanceMonitor, ResourceManager } from 'posev';
export function PoseVPlayer({ movementUrl, rigUrl }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<PoseVCore | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => {
const perfMonitor = new PerformanceMonitor();
const resourceMgr = new ResourceManager();
const engine = new PoseVCore({
renderer: {
canvas: canvasRef.current!,
type: 'canvas2d',
},
perfMonitor,
resourceMgr,
});
engineRef.current = engine;
engine.load({ movement: movementUrl, rig: rigUrl }).then(() => {
setDuration(engine.getTimeline()?.duration ?? 0);
engine.on('frameUpdate', (frame) => {
setProgress(frame.progress);
});
});
return () => {
engine.dispose();
resourceMgr.cleanup();
};
}, [movementUrl, rigUrl]);
const togglePlayPause = () => {
if (isPlaying) {
engineRef.current?.pause();
} else {
engineRef.current?.play();
}
setIsPlaying(!isPlaying);
};
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
const newProgress = parseFloat(e.target.value);
engineRef.current?.seek(newProgress);
};
return (
<div>
<canvas ref={canvasRef} width={800} height={600} />
<div>
<button onClick={togglePlayPause}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<input
type="range"
min="0"
max="1"
step="0.01"
value={progress}
onChange={handleSeek}
/>
<span>{(progress * 100).toFixed(1)}%</span>
</div>
</div>
);
}Core Features
🎬 Animation Engine
- Keyframe-based animation with interpolation
- Multiple easing modes (linear, cubic, ease-in, ease-out)
- Speed control (0.1x to 3x)
- Precise seeking and time scrubbing
- Frame-accurate playback
🦴 Skeletal Rig System
- Predefined humanoid rigs (standard, compact, custom)
- Joint hierarchy validation
- Bone length and angle constraints
- Forward kinematics support
- Ancestor/descendant queries
🔌 Plugin Architecture
- Event-based plugin system
- Plugin lifecycle (install, activate, deactivate, uninstall)
- Access to engine internals via context
- Error recovery and fallback support
- Built-in plugins: gait analysis, pose detection, motion capture
📊 Biomechanical Validation
- JSON schema validation
- Bone length variance checking
- Angle constraint enforcement
- Duration and timing validation
- Comprehensive error catalog
🎨 Rendering
- Canvas 2D renderer (default)
- WebGL renderer (high-performance)
- Custom renderer support
- Debug visualization mode
- Performance profiling
📱 Mobile Optimized
- <15 KB gzipped delivery
- Conservative performance budgets
- Battery-aware frame rate targeting
- Memory leak prevention
- Resource lifecycle management
Framework Integration
Vue 3 Composition API
Full Composition API support with lifecycle management:
import { usePoseV } from 'posev';
import { ref, onMounted } from 'vue';
const { play, pause, seek, getState } = usePoseV({
movement: 'squat.json',
rig: 'standard-human.json',
autoplay: true,
debug: false,
});
onMounted(() => {
play();
});See Vue Integration Guide for details.
React Hooks (Coming Soon)
const { play, pause, seek } = usePoseV(config);Vanilla JavaScript
Works with any framework:
const engine = new PoseVCore(config);
await engine.load(manifest);
engine.play();API Reference
PoseVCore
Main animation engine class.
Constructor
new PoseVCore(config?: PoseVCoreConfig)Config Options:
interface PoseVCoreConfig {
// Validation
strictValidation?: boolean; // Default: false
// Rendering
renderer?: RendererConfig; // Canvas/WebGL settings
// Production Hardening
errorHandler?: ErrorHandler; // Custom error handler
perfMonitor?: PerformanceMonitor; // Performance tracking
resourceMgr?: ResourceManager; // Memory management
// Debugging
debug?: boolean; // Enable debug mode
logger?: Logger; // Custom logger
}Methods
// Asset Management
await engine.load(manifest: MovementManifest): Promise<void>
await engine.assets.preload(assets: AssetDef[]): Promise<void>
// Playback Control
engine.play(): void
engine.pause(): void
engine.stop(): void
engine.seek(progress: 0..1): void
engine.setSpeed(multiplier: number): void
// State Query
engine.getState(): PlaybackState
engine.getTimeline(): Timeline | null
engine.getPose(time?: number): Pose | null
engine.getProgress(): number
engine.getDuration(): number
// Events
engine.on(event: 'frameUpdate' | 'stateChange' | 'error', listener): Unsubscriber
engine.off(event, listener): void
engine.emit(event, data): void
// Lifecycle
engine.dispose(): voidEvents
// Frame update - fires every animation frame
engine.on('frameUpdate', (frame: FrameData) => {
// { currentTime, progress, pose, deltaTime }
});
// Playback state changed
engine.on('stateChange', (state: PlaybackState) => {
// { timeline, playback, speed, progress }
});
// Error occurred
engine.on('error', (error: PoseVError) => {
// { code, category, severity, message }
});
// Complete - animation finished
engine.on('complete', (stats: PlaybackStats) => {
// { totalTime, frameCount, avgFps }
});Timeline
Animation timeline with keyframes.
interface Timeline {
movement: Movement;
rig: Rig;
keyframes: Keyframe[];
duration: number; // Total duration in ms
fps: number; // Frames per second
interpolationMode: 'linear' | 'cubic';
// Query methods
getPoseAt(time: number): Pose;
getKeyframeAt(time: number): Keyframe;
getCurrentProgress(time: number): 0..1;
}Pose
Skeletal pose data.
interface Pose {
joints: Map<string, Vector3>; // Joint positions
bones?: Map<string, Vector3>; // Bone vectors (optional)
timestamp: number;
getJoint(id: string): Vector3 | undefined;
hasJoint(id: string): boolean;
}Movement
Movement definition (loaded from JSON).
interface Movement {
id: string;
name: string;
description?: string;
duration: number; // in milliseconds
fps: number;
keyframes: KeyframeData[];
// Validation
validate(): ValidationResult;
getKeyframeCount(): number;
getDurationSeconds(): number;
}Rig
Skeletal rig definition.
interface Rig {
id: string;
name: string;
joints: JointDef[];
bones: BoneDef[];
// Query
getJoint(id: string): JointDef | undefined;
getParent(jointId: string): JointDef | undefined;
getChildren(jointId: string): JointDef[];
// Validation
validate(): ValidationResult;
validateBoneLengths(pose: Pose): BoneValidation;
validateAngles(pose: Pose): AngleValidation;
}
interface JointDef {
id: string;
name: string;
parent?: string;
constraints?: AngleConstraint[];
}
interface BoneDef {
id: string;
from: string;
to: string;
minLength: number;
maxLength: number;
idealLength: number;
}Plugin System
interface PoseVPlugin {
// Metadata
id: string;
version: string;
dependencies?: string[];
// Lifecycle
install?(context: PluginContext): Promise<void>;
activate?(context: PluginContext): Promise<void>;
deactivate?(): Promise<void>;
uninstall?(): Promise<void>;
// Optional hook
onFrameUpdate?(frame: FrameData): void | Promise<void>;
onStateChange?(state: PlaybackState): void | Promise<void>;
onError?(error: PoseVError): boolean | Promise<boolean>; // Return true to suppress
}
interface PluginContext {
engine: PoseVCore;
timeline: Timeline;
rig: Rig;
renderer: Renderer;
errorHandler: ErrorHandler;
perfMonitor: PerformanceMonitor;
}Plugin Guide
Writing a Simple Plugin
Create a gait analysis plugin:
import { PoseVPlugin, PluginContext, FrameData } from 'posev';
class GaitAnalysisPlugin implements PoseVPlugin {
id = 'gait-analysis';
version = '1.0.0';
private context: PluginContext | null = null;
private metrics = { stride: 0, cadence: 0, symmetry: 0 };
async install(context: PluginContext) {
this.context = context;
console.log('GaitAnalysisPlugin installed');
}
async activate() {
console.log('GaitAnalysisPlugin activated');
}
async deactivate() {
console.log('GaitAnalysisPlugin deactivated');
}
onFrameUpdate(frame: FrameData) {
// Analyze pose data
const pose = frame.pose;
// Calculate stride length
const leftFoot = pose.getJoint('left_foot');
const rightFoot = pose.getJoint('right_foot');
if (leftFoot && rightFoot) {
const distance = Math.hypot(
rightFoot.x - leftFoot.x,
rightFoot.z - leftFoot.z
);
this.metrics.stride = distance;
}
// Calculate cadence (steps per minute)
this.metrics.cadence = (this.metrics.stride / frame.deltaTime) * 60;
// Emit custom event or update UI
this.context?.engine.emit('gait:metrics', this.metrics);
}
getMetrics() {
return { ...this.metrics };
}
}
// Usage
const plugin = new GaitAnalysisPlugin();
engine.plugins.install(plugin);
engine.on('gait:metrics', (metrics) => {
console.log('Stride length:', metrics.stride.toFixed(2), 'cm');
console.log('Cadence:', metrics.cadence.toFixed(1), 'steps/min');
});Plugin with Dependencies
class AdvancedAnalysisPlugin implements PoseVPlugin {
id = 'advanced-analysis';
version = '1.0.0';
dependencies = ['gait-analysis']; // Requires GaitAnalysisPlugin
async install(context: PluginContext) {
// Install will fail if dependency not available
}
}Error Recovery in Plugins
class RobustPlugin implements PoseVPlugin {
id = 'robust';
version = '1.0.0';
onFrameUpdate(frame: FrameData) {
try {
// Process frame
} catch (error) {
// Log but don't crash
console.error('Plugin error:', error);
}
}
// Return true to suppress error propagation
onError(error: PoseVError) {
if (error.code === 'P_5004') {
// Handle specific error
return true; // Suppress
}
return false; // Propagate
}
}Movement JSON Standard
Format Specification (v1.0.0)
{
"version": "1.0.0",
"metadata": {
"id": "squat",
"name": "Bodyweight Squat",
"description": "Standard squat movement",
"author": "Motion Capture Studio",
"captureDate": "2024-11-23",
"fps": 60,
"duration": 2000,
"units": "centimeters"
},
"rig": {
"id": "standard-human",
"version": "1.0.0"
},
"keyframes": [
{
"time": 0,
"joints": {
"root": { "x": 0, "y": 0, "z": 0 },
"left_hip": { "x": -8, "y": 95, "z": 0 },
"left_knee": { "x": -8, "y": 52, "z": 0 },
"left_ankle": { "x": -8, "y": 2, "z": 0 },
"right_hip": { "x": 8, "y": 95, "z": 0 },
"right_knee": { "x": 8, "y": 52, "z": 0 },
"right_ankle": { "x": 8, "y": 2, "z": 0 }
}
},
{
"time": 1000,
"joints": {
"root": { "x": 0, "y": 0, "z": 0 },
"left_hip": { "x": -8, "y": 60, "z": 0 },
"left_knee": { "x": -8, "y": 20, "z": 0 },
"left_ankle": { "x": -8, "y": 2, "z": 0 },
"right_hip": { "x": 8, "y": 60, "z": 0 },
"right_knee": { "x": 8, "y": 20, "z": 0 },
"right_ankle": { "x": 8, "y": 2, "z": 0 }
}
},
{
"time": 2000,
"joints": {
"root": { "x": 0, "y": 0, "z": 0 },
"left_hip": { "x": -8, "y": 95, "z": 0 },
"left_knee": { "x": -8, "y": 52, "z": 0 },
"left_ankle": { "x": -8, "y": 2, "z": 0 },
"right_hip": { "x": 8, "y": 95, "z": 0 },
"right_knee": { "x": 8, "y": 52, "z": 0 },
"right_ankle": { "x": 8, "y": 2, "z": 0 }
}
}
],
"interpolation": "cubic",
"loops": true,
"tags": ["lower-body", "squat", "strength"]
}Schema Requirements
interface MovementJSON {
// Version and metadata
version: '1.0.0';
metadata: {
id: string; // Unique identifier
name: string; // Human-readable name
description?: string;
author?: string;
captureDate?: string;
fps: number; // Frame rate (30-120)
duration: number; // Total duration in ms
units?: 'centimeters' | 'meters' | 'inches';
};
// Rig reference
rig: {
id: string;
version: string;
};
// Keyframes (minimum 2)
keyframes: Array<{
time: number; // Time in ms (0 to duration)
joints: {
[jointId: string]: { // One entry per joint in rig
x: number;
y: number;
z: number;
};
};
}>;
// Animation settings
interpolation?: 'linear' | 'cubic'; // Default: 'linear'
loops?: boolean; // Default: true
tags?: string[]; // Categorization
}Validation Rules
Time Values
- First keyframe must be at time 0
- Last keyframe must be at duration
- Times must be monotonically increasing
- No duplicate times
Joint Coverage
- Every keyframe must have all rig joints
- Extra joints are ignored
- Missing joints cause validation error
Coordinate Ranges
- Values typically -500 to +500 (units)
- No NaN, Infinity, or undefined
- Positive Z is forward, Y is up
Duration
- Minimum: 100ms
- Maximum: 300,000ms (5 minutes)
- Recommended: 500ms - 30,000ms
Creating Movement Data
import { MovementValidator } from 'posev';
const movementData = {
version: '1.0.0',
metadata: {
id: 'jump',
name: 'Standing Jump',
fps: 60,
duration: 1200,
},
rig: { id: 'standard-human', version: '1.0.0' },
keyframes: [
{ time: 0, joints: { /* ... */ } },
{ time: 600, joints: { /* ... */ } },
{ time: 1200, joints: { /* ... */ } },
],
};
// Validate before use
const validator = new MovementValidator();
const result = validator.validate(movementData);
if (result.valid) {
console.log('✅ Movement data is valid');
} else {
console.error('❌ Validation errors:', result.errors);
}Rig System
Overview
A rig defines the skeletal structure and constraints for animations. PoseV ships with predefined humanoid rigs and supports custom rigs.
Standard Humanoid Rig (v1.0.0)
The default rig with 17 joints and 16 bones:
Root (pelvis)
├── Spine
│ └── Chest
│ ├── Left Shoulder
│ │ └── Left Arm
│ │ └── Left Forearm
│ │ └── Left Hand
│ └── Right Shoulder
│ └── Right Arm
│ └── Right Forearm
│ └── Right Hand
├── Left Hip
│ └── Left Knee
│ └── Left Ankle
│ └── Left Foot
└── Right Hip
└── Right Knee
└── Right Ankle
└── Right FootJoint IDs:
root, spine, chest,
left_shoulder, left_arm, left_forearm, left_hand,
right_shoulder, right_arm, right_forearm, right_hand,
left_hip, left_knee, left_ankle, left_foot,
right_hip, right_knee, right_ankle, right_footUsing Built-in Rigs
import { StandardHumanRig, CompactHumanRig } from 'posev';
// Standard rig (17 joints, full detail)
const rig = new StandardHumanRig();
// Compact rig (11 joints, optimized)
const compactRig = new CompactHumanRig();
// Access rig data
const joints = rig.joints;
const bones = rig.bones;
// Query hierarchy
const parent = rig.getParent('left_knee'); // 'left_hip'
const children = rig.getChildren('spine'); // ['chest']
// Validation
const validation = rig.validate();
if (!validation.valid) {
console.error(validation.errors);
}Rig JSON Format
{
"version": "1.0.0",
"metadata": {
"id": "standard-human",
"name": "Standard Human Rig",
"description": "Biomechanically accurate humanoid rig",
"jointCount": 17
},
"joints": [
{
"id": "root",
"name": "Root/Pelvis",
"parent": null,
"constraints": [
{
"axis": "x",
"min": -30,
"max": 30,
"unit": "degrees"
}
]
},
{
"id": "left_knee",
"name": "Left Knee",
"parent": "left_hip",
"constraints": [
{
"axis": "z",
"min": 0,
"max": 140,
"unit": "degrees"
}
]
}
],
"bones": [
{
"id": "left_femur",
"from": "left_hip",
"to": "left_knee",
"minLength": 35,
"maxLength": 55,
"idealLength": 45,
"unit": "centimeters"
}
]
}Creating a Custom Rig
import { Rig, JointDef, BoneDef } from 'posev';
const customRig = new Rig({
id: 'quadruped',
name: 'Four-Legged Animal',
joints: [
{ id: 'root', name: 'Root', parent: null },
{ id: 'front_left_leg', name: 'Front Left Leg', parent: 'root' },
{ id: 'front_left_knee', name: 'Front Left Knee', parent: 'front_left_leg' },
{ id: 'front_left_foot', name: 'Front Left Foot', parent: 'front_left_knee' },
// ... more joints
],
bones: [
{
id: 'front_left_femur',
from: 'front_left_leg',
to: 'front_left_knee',
minLength: 10,
maxLength: 25,
idealLength: 17,
},
// ... more bones
],
});
// Use with movement data
const movement = await engine.load({
movement: './movements/walk.json',
rig: customRig,
});Rig Validation
const validation = rig.validate();
if (!validation.valid) {
console.error('Rig errors:');
validation.errors.forEach(error => {
console.error(` - ${error.joint}: ${error.message}`);
});
}
// Validate specific pose
const poseValidation = rig.validateBoneLengths(pose);
if (!poseValidation.valid) {
poseValidation.violations.forEach(v => {
console.warn(`Bone ${v.bone} length ${v.actual}cm exceeds max ${v.max}cm`);
});
}
// Validate angles
const angleValidation = rig.validateAngles(pose);
angleValidation.violations.forEach(v => {
console.warn(`Joint ${v.joint} angle ${v.actual}° exceeds max ${v.max}°`);
});Rig Hierarchy Queries
// Get all ancestors
const ancestors = rig.getAncestors('left_foot');
// → ['left_ankle', 'left_knee', 'left_hip', 'root']
// Get all descendants
const descendants = rig.getDescendants('root');
// → [all joints in tree]
// Check relationship
const isDescendant = rig.isDescendant('left_foot', 'root');
// → true
// Get bone connecting two joints
const bone = rig.getBone('left_hip', 'left_knee');
// → { id: 'left_femur', ... }Performance & Production
Bundle Optimization
PoseV uses aggressive tree-shaking and minification:
ESM: 24.2 KB gzipped (79.6% compression)
UMD Min: 13.7 KB gzipped (73% compression)Performance Budgets
Mobile-first conservative targets:
| Metric | Target | Status | |--------|--------|--------| | Frame Time | <16.67ms (60 FPS) | ✅ | | Memory | <50 MB | ✅ | | Load Time | <5 seconds | ✅ | | Max Keyframes | 10,000 | ✅ |
Error Handling
23 predefined error codes across 6 categories:
import { ERROR_CATALOG, ErrorHandler } from 'posev';
const handler = new ErrorHandler();
try {
await engine.load(manifest);
} catch (error) {
handler.handle(error);
if (error.retryable) {
// Implement exponential backoff retry
setTimeout(() => engine.load(manifest), 1000);
}
console.log(handler.getReport());
}Monitoring
Real-time performance monitoring:
import { PerformanceMonitor } from 'posev';
const monitor = new PerformanceMonitor();
engine.on('frameUpdate', (frame) => {
monitor.recordMetric({
fps: 60,
frameTime: 16.7,
memoryUsage: 42,
cpuTime: 14.5,
renderTime: 2.2,
});
});
// Check for violations
const status = monitor.getBudgetStatus();
if (!status.compliant) {
console.warn('Performance degradation:', status.violations);
}
// Generate report
console.log(monitor.getReport());Examples
Full-Featured Rehabilitation App
See examples/rehab-app/ for a complete application with:
- Patient movement recording
- AI-powered form analysis
- Progress tracking
- Export to PT software
Motion Capture Integration
See examples/mocap-integration/ for mocap workflow:
- Real-time pose streaming
- Calibration tools
- File export (BVH, FBX, JSON)
Sports Analytics Dashboard
See examples/sports-dashboard/ for analytics:
- Gait analysis metrics
- Performance comparison
- Video overlay with annotations
- Team performance trending
Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
Development Setup
# Install dependencies
npm install
# Run dev server
npm run dev
# Run tests
npm test
# Run tests in watch mode
npm test -- --watch
# Build for production
npm run build
# Run linter
npm run lint
# Type check
npm run type-checkCreating Issues
Before opening an issue, please check:
- ✅ Existing issues and discussions
- ✅ Documentation and guides
- ✅ Troubleshooting section
Pull Requests
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Ensure all tests pass (
npm test) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
Support
Documentation
Community
- 💬 GitHub Discussions
- 🐛 Bug Reports: GitHub Issues
- 💡 Feature Requests: GitHub Discussions
- 📧 Email: [email protected]
Enterprise Support
Priority bug fixes, custom integration, and SLA support available. Contact: [email protected]
License
MIT © 2024 PoseV Contributors
Changelog
v1.0.0 (2024-11-23)
- ✅ Core animation engine
- ✅ Skeletal rig system
- ✅ Movement JSON standard
- ✅ Plugin architecture
- ✅ Vue 3 integration
- ✅ Canvas 2D rendering
- ✅ Production hardening (error handling, performance monitoring, memory management)
- ✅ Comprehensive test suite (86 tests, 100% passing)
Acknowledgments
Built with ❤️ for sports tech, rehabilitation, and motion analysis professionals.
