{"id":50452987,"url":"https://github.com/affromero/splattie-widget","last_synced_at":"2026-06-01T01:01:13.200Z","repository":{"id":360952783,"uuid":"1251670993","full_name":"affromero/splattie-widget","owner":"affromero","description":"Interactive 3D Gaussian Splatting web component — like Rive/Lottie for 3D","archived":false,"fork":false,"pushed_at":"2026-05-28T14:25:58.000Z","size":25930,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-28T16:13:50.706Z","etag":null,"topics":["3dgs","avatar","flame","gaussian-splatting","interactive","spark","threejs","typescript","web-component"],"latest_commit_sha":null,"homepage":"https://splattie.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/affromero.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-27T20:04:53.000Z","updated_at":"2026-05-28T15:31:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/affromero/splattie-widget","commit_stats":null,"previous_names":["affromero/splattie-widget"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/affromero/splattie-widget","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fsplattie-widget","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fsplattie-widget/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fsplattie-widget/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fsplattie-widget/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/affromero","download_url":"https://codeload.github.com/affromero/splattie-widget/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/affromero%2Fsplattie-widget/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33755369,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3dgs","avatar","flame","gaussian-splatting","interactive","spark","threejs","typescript","web-component"],"created_at":"2026-06-01T01:00:33.274Z","updated_at":"2026-06-01T01:01:13.188Z","avatar_url":"https://github.com/affromero.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"assets/logo.svg\" alt=\"Splattie\" width=\"100\" /\u003e\n\n# splattie-widget\n\n**Interactive rigged 3D Gaussian Splatting - like Rive/Lottie for 3D**\n\n[![npm](https://img.shields.io/npm/v/@afromero/splattie-widget?color=blue)](https://www.npmjs.com/package/@afromero/splattie-widget)\n[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue?logo=typescript\u0026logoColor=white)](https://typescriptlang.org)\n[![Spark](https://img.shields.io/badge/Spark_2.0-MIT-green)](https://github.com/sparkjsdev/spark)\n[![Three.js](https://img.shields.io/badge/Three.js-r170+-black?logo=three.js)](https://threejs.org)\n[![Tests](https://img.shields.io/badge/tests-91_passing-brightgreen)]()\n[![Bundle](https://img.shields.io/badge/format-.splattie-orange)]()\n\n[Quick Start](#quick-start) · [Format Spec](FORMAT.md) · [API](#api) · [Editor](#visual-editor) · [How It Works](#how-it-works)\n\n\u003c/div\u003e\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/demo.gif\" alt=\"Splattie Widget Demo\" width=\"600\" /\u003e\n\u003c/p\u003e\n\nA web component that makes Gaussian splats **reactive**. One file, one tag. **Heads** track the cursor with their eyes, blink, and emote on hover/click (FLAME rig); **bodies** turn their head and torso toward visitors and pose with two-bone arm IK (SMPL-X rig); **objects** use arbitrary skeletons and sparse LBS weights for cursor-follow and direct pose editing. 60fps, client-side. The widget branches on the bundle's `assetType` — same tag for heads, bodies, and objects.\n\n**[See it live at afromero.co](https://afromero.co)** | **[Create your own at splattie.app](https://splattie.app)**\n\n## Quick Start\n\n```html\n\u003csplattie-widget src=\"asset.splattie\"\u003e\u003c/splattie-widget\u003e\n\u003cscript type=\"module\" src=\"https://unpkg.com/@afromero/splattie-widget\"\u003e\u003c/script\u003e\n```\n\nOr via npm:\n\n```bash\nnpm install @afromero/splattie-widget\n```\n\n```typescript\nimport '@afromero/splattie-widget';\n```\n\n## The `.splattie` Format\n\n\u003e **v0.x experimental.** Core files (PLY, FLAME bones) follow established standards. Expression basis and states may evolve.\n\nA ZIP bundle with a required `manifest.json` that declares every asset\nand locks the file's `formatVersion` to the widget version. See\n[`FORMAT.md`](FORMAT.md) for the full spec.\n\n```\nasset.splattie\n├── manifest.json             # (required) declares every asset + assetType + formatVersion\n├── *.ply or *.spz            # (required) Gaussian splats\n│\n│  # head (assetType: head) — FLAME rig:\n├── bone_tree.json            # (optional) Skeleton for skinning (5 FLAME bones)\n├── lbs_weight_20k.json       # (optional) Per-splat bone weights\n├── expression_basis.bin      # (optional) Blendshape basis\n│\n│  # body (assetType: body) — SMPL-X rig:\n├── skeleton.json             # (optional) 55-joint SMPL-X skeleton (baked-pose rest)\n├── lbs_weights.json          # (optional) Per-gaussian sparse LBS weights\n│\n│  # object (assetType: object) — arbitrary skeleton rig:\n├── skeleton.json             # (optional) Object joint hierarchy + rest positions\n├── lbs_weights.bin           # (optional) Binary sparse per-gaussian LBS weights\n│\n└── states.json               # (optional) Interaction states\n```\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eSplat data\u003c/strong\u003e (\u003ccode\u003e.ply\u003c/code\u003e or \u003ccode\u003e.spz\u003c/code\u003e) - standard 3DGS format\u003c/summary\u003e\n\nEach splat has position, scale, rotation, opacity, and SH color. Auto-detected from file header. Works with any 3DGS method (LAM, DreamGaussian, InstantSplat, etc.). Standard format, unlikely to change.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003ebone_tree.json\u003c/strong\u003e - skeleton hierarchy\u003c/summary\u003e\n\n5 FLAME bones: root \u003e neck \u003e jaw, leftEye, rightEye. Used for SplatSkinning (dual quaternion).\n\n```json\n{\n  \"bones\": [{\n    \"name\": \"root\",\n    \"position\": [x, y, z],\n    \"children\": [{\n      \"name\": \"neck\",\n      \"position\": [x, y, z],\n      \"children\": [\n        { \"name\": \"jaw\", \"position\": [x, y, z] },\n        { \"name\": \"leftEye\", \"position\": [x, y, z] },\n        { \"name\": \"rightEye\", \"position\": [x, y, z] }\n      ]\n    }]\n  }]\n}\n```\n\nStable structure. Bone names are conventions, not hard requirements. Without it: no eye tracking, no jaw animation.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003elbs_weight_20k.json\u003c/strong\u003e - per-splat bone weights\u003c/summary\u003e\n\n2D array `[num_splats][num_bones]`, each row sums to ~1.0. Widget selects top 4 per splat.\n\n```json\n[[0.8, 0.1, 0.05, 0.03, 0.02], ...]\n```\n\nStandard LBS format from FLAME. Without it: bones exist but nothing moves.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eexpression_basis.bin\u003c/strong\u003e - FLAME blendshape basis\u003c/summary\u003e\n\nPer-splat position displacements for each expression coefficient. Moves all splats coherently for smile, lip shapes, etc.\n\n```\nHeader: \"EXPR\" (4B) + num_vertices (u32 LE) + num_expressions (u32 LE)\nData:   float32 LE array, shape (num_vertices, num_expressions, 3)\n```\n\nOptional sidecar `expression_basis.json` with semantic labels:\n```json\n{ \"labels\": [\"jawDown\", \"lipsUp\", \"lipsL\", ...], \"num_expressions\": 50 }\n```\n\nExperimental format, may add compression. Without it: bone-driven expressions still work.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eskeleton.json\u003c/strong\u003e + \u003cstrong\u003elbs_weights.json\u003c/strong\u003e - SMPL-X body rig (bodies)\u003c/summary\u003e\n\nBodies (`assetType: body`) use a 55-joint SMPL-X skeleton instead of FLAME bones:\n\n```json\n// skeleton.json — joints in the BAKED (photographed) rest pose\n{ \"rig\": \"smplx\", \"jointCount\": 55, \"names\": [\"Pelvis\", \"L_Hip\", ...],\n  \"parents\": [-1, 0, 0, ...], \"restPositions\": [[x, y, z], ...] }\n\n// lbs_weights.json — sparse top-K per-gaussian skinning\n{ \"numGaussians\": 40000, \"k\": 4, \"indices\": [...], \"weights\": [...] }\n```\n\nThe body is exported already posed (arms at rest), so the widget's rest pose is the\nidentity. From there it drives **head + torso look-at** toward the cursor and a\n**two-bone arm IK** for editor posing, composing local joint rotations via SMPL-X\nforward kinematics + linear blend skinning. Without these: the gaussians render but\ndon't articulate.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eskeleton.json\u003c/strong\u003e + \u003cstrong\u003elbs_weights.bin\u003c/strong\u003e - object rig\u003c/summary\u003e\n\nObjects (`assetType: object`) use the same manifest-level LBS contract as bodies,\nbut with an arbitrary joint hierarchy:\n\n```json\n// skeleton.json\n{ \"rig\": \"puppeteer-object\", \"jointCount\": 12, \"names\": [\"root\", \"...\"],\n  \"parents\": [-1, 0, ...], \"restPositions\": [[x, y, z], ...] }\n\n// manifest excerpt\n{ \"weights\": { \"file\": \"lbs_weights.bin\", \"format\": \"lbsw-v1\" } }\n```\n\nThe binary weights file stores sparse uint16 joint indices and float16 weights for each\nGaussian. The widget projects terminal joints as editor handles, solves simple\njoint-chain rotations when you drag a handle, and uses root/joint cursor-follow\nsettings for lightweight interactivity.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003estates.json\u003c/strong\u003e - interaction state definitions\u003c/summary\u003e\n\nEach state (idle, hover, click) sets all 5 dimensions simultaneously.\n\n```json\n{\n  \"defaults\": {\n    \"camera\": { \"theta\": 0, \"phi\": 75, \"radius\": 0.5, \"fov\": 60 },\n    \"autoBlink\": { \"interval\": [2000, 7000], \"duration\": 150 }\n  },\n  \"states\": {\n    \"idle\": {\n      \"ghost\": { \"amplitude\": 0.003, \"frequency\": 0.4, \"wobble\": 0.2 },\n      \"expression\": { \"jawOpen\": 0, \"smile\": 0 },\n      \"camera\": { \"theta\": 0, \"phi\": 75, \"radius\": 0.5, \"fov\": 60 },\n      \"rotation\": [0, 0, 0],\n      \"tracking\": { \"eyes\": 1.0, \"head\": 0.1 }\n    },\n    \"hover\": { \"...\" : \"...\" },\n    \"click\": { \"...\" : \"...\" }\n  },\n  \"transitions\": {\n    \"idle-\u003ehover\": { \"duration\": 0.3, \"easing\": \"ease-out\" },\n    \"*-\u003eclick\": { \"duration\": 0.1, \"easing\": \"snap\" }\n  }\n}\n```\n\nMost likely to evolve. Without it: sensible defaults (eyes track, gentle float, auto-blink).\n\u003c/details\u003e\n\n### Creating Your Own\n\n**Visual editor**: `npm run dev`, adjust sliders, click \"Download .splattie\".\n\n**From scratch**: ZIP a `.ply` with any combination of the optional files.\n\n**From a photo or object image**: run the Splattie backend pipeline for heads\n(LAM), bodies (LHM), or objects (TRELLIS + Puppeteer), then bundle the result.\nTry it at [splattie.app](https://splattie.app).\n\n## Five Dimensions of State\n\n| Dimension | Controls | Example |\n|-----------|----------|---------|\n| **Ghost** | Floating/bobbing | Gentle hover on idle, freeze on click |\n| **Expression** | FLAME blendshapes + bones | Smile on hover, surprise on click |\n| **Camera** | Spherical position | Zoom in on hover |\n| **Rotation** | Pitch/yaw/roll | Tilt head on hover |\n| **Tracking** | Cursor-follow intensity | Heads: eyes/head. Bodies: head/torso. Objects: root/joints |\n\nInterpolated between states with configurable easing and duration.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eExpression system details\u003c/strong\u003e\u003c/summary\u003e\n\nTwo layers:\n\n**Bone-driven** (SplatSkinning, 5 FLAME bones):\n- Jaw open/close, neck pitch/yaw/roll\n- Eye gaze direction (left/right, up/down)\n- Brow raise/frown (left/right independently)\n\n**Blendshape-driven** (FLAME expression basis, 10+ PCA coefficients):\n- Moves all 20K splats coherently\n- Smile, lip shapes, jaw articulation, cheek/nose deformation\n- Spatial mask prevents beard/neck from deforming\n\u003c/details\u003e\n\n## API\n\n| Attribute | Description |\n|-----------|-------------|\n| `src` | URL to `.splattie` file (or `.ply`/`.spz`) |\n| `background` | Background color hex (default: `#0e0e14`) |\n| `width` | CSS width (default: `100%`) |\n| `height` | CSS height (default: `400px`) |\n\n```javascript\nwidget.addEventListener('splatload', () =\u003e {});   // ready\nwidget.addEventListener('splathover', () =\u003e {});   // cursor over asset\nwidget.addEventListener('splatclick', () =\u003e {});   // clicked asset\nwidget.addEventListener('splatleave', () =\u003e {});   // cursor left\nwidget.setState('hover');                           // force transition\n```\n\n## Visual Editor\n\n```bash\nnpm run dev  # http://localhost:4002\n```\n\nSliders for all 5 dimensions, camera sphere widget, state tabs with copy-forward, FLAME blendshape controls (heads), on-canvas IK drag handles to pose limbs (bodies), skeleton handles for object pose editing, drag-and-drop `.splattie` upload, export when done.\n\n## How It Works\n\nBuilt on [Spark 2.0](https://github.com/sparkjsdev/spark) (MIT, World Labs).\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eArchitecture\u003c/strong\u003e\u003c/summary\u003e\n\n1. **State machine** with per-dimension interpolation (lerp, slerp, ease curves)\n2. **SplatSkinning** (dual quaternion) driving 5 FLAME bones from expression + cursor data (heads)\n3. **SMPL-X skinning** (55-joint LBS) for bodies — head/torso look-at + analytic two-bone arm IK, composed via forward kinematics\n4. **Generic object skinning** for arbitrary skeletons — root/joint cursor-follow, projected skeleton handles, and drag-to-pose chain solving\n5. **Expression basis** - per-splat position offsets written to Spark's packed buffer (half-float, ~20K splats/frame)\n6. **Hit detection** via `readPixels` after render (pixel-perfect)\n7. **Auto-blink** with randomized interval and sine-curve via SplatEdit\n8. **Gyroscope** tracking on mobile (iOS permission prompt included)\n\u003c/details\u003e\n\n## Mobile\n\nTouch + gyroscope. Eyes follow device orientation on mobile, touch position on tap. Return to center when finger lifts. iOS motion permission requested automatically.\n\n## Browser Support\n\nChrome, Firefox, Safari, Edge. WebGL 2 required. No COOP/COEP headers needed.\n\n## Acknowledgements\n\n- [LAM](https://github.com/aigc3d/LAM) (SIGGRAPH 2025) - single-image 3DGS heads. Zixuan Zeng et al., AIGC3D team\n- [LHM](https://github.com/aigc3d/LHM) (SIGGRAPH 2025) - single-image 3DGS bodies. AIGC3D team\n- [TRELLIS](https://github.com/microsoft/TRELLIS) - single-image 3D asset reconstruction. Microsoft.\n- [Puppeteer](https://github.com/snap-research/Puppeteer) - automatic skeleton and skinning for generated 3D assets. Snap Research.\n- [FLAME](https://flame.is.tue.mpg.de/) - face model (heads). Tianye Li, Timo Bolkart, Michael J. Black, Hao Li, Javier Romero\n- [SMPL-X](https://smpl-x.is.tue.mpg.de/) - body model (bodies). Pavlakos, Choutas, Ghorbani, Bolkart, Osman, Tzionas, Black (MPI)\n- [Spark 2.0](https://github.com/sparkjsdev/spark) - World Labs (MIT)\n- [3D Gaussian Splatting](https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/) - Kerbl, Kopanas, Leimkuhler, Drettakis (INRIA)\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faffromero%2Fsplattie-widget","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faffromero%2Fsplattie-widget","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faffromero%2Fsplattie-widget/lists"}