{"_id":"@djodjonx/gwen-plugin-sprite-anim","_rev":"2-eb5ffdb24b6bfabeee40fc7604b6bb52","name":"@djodjonx/gwen-plugin-sprite-anim","dist-tags":{"latest":"1.0.0"},"versions":{"0.3.7":{"name":"@djodjonx/gwen-plugin-sprite-anim","version":"0.3.7","_id":"@djodjonx/gwen-plugin-sprite-anim@0.3.7","maintainers":[{"name":"djodjonx","email":"djo.moutier@gmail.com"}],"dist":{"shasum":"71f6412181ceca57a1f02bf93789321873e997db","tarball":"https://registry.npmjs.org/@djodjonx/gwen-plugin-sprite-anim/-/gwen-plugin-sprite-anim-0.3.7.tgz","fileCount":6,"integrity":"sha512-eoCmfPcAxxl/pKSreS+I4ZQLeCcw1Ac0yyMSYaX+l5TVJxy9Tf/I33JsqrOAWFQAdgsAiLN1VGLYBPXXBdIDgg==","signatures":[{"sig":"MEQCIGtUq9O4GkVe5hox95lSj4jwjtq1T11MNZVawecuFD5LAiAdTkgQHxzPceuigOPOlpUD0Tv9zhOFqCf0igPgFFZlfg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":62996},"gwen":{"type":"typescript","hookTypes":{"spriteAnim:frame":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"},"spriteAnim:complete":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"},"spriteAnim:transition":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"}},"serviceTypes":{"animator":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimatorService"}},"uiExtensionTypes":{"spriteAnim":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimUIExtension"}}},"main":"./dist/index.js","type":"module","_from":"file:djodjonx-gwen-plugin-sprite-anim-0.3.7.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"dev":"vite","test":"vitest run","bench":"vitest bench --run","build":"vite build","watch":"vite build --watch","typecheck":"tsc --noEmit","test:watch":"vitest","bench:watch":"vitest bench --watch"},"_npmUser":{"name":"djodjonx","email":"djo.moutier@gmail.com"},"_resolved":"/private/var/folders/vc/41dnkkc11kqfyrhq8hmhj9400000gn/T/9f989c7f56e15f4b35182ba2f334b49f/djodjonx-gwen-plugin-sprite-anim-0.3.7.tgz","_integrity":"sha512-eoCmfPcAxxl/pKSreS+I4ZQLeCcw1Ac0yyMSYaX+l5TVJxy9Tf/I33JsqrOAWFQAdgsAiLN1VGLYBPXXBdIDgg==","_npmVersion":"10.9.4","description":"GWEN Sprite Animation Plugin — Animator runtime for defineUI","directories":{},"_nodeVersion":"22.21.0","dependencies":{"@djodjonx/gwen-kit":"0.3.7","@djodjonx/gwen-engine-core":"0.3.7"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^7.3.1","vitest":"^4.0.18","typescript":"^5.9.3","vite-plugin-dts":"^4.5.4"},"peerDependencies":{"@djodjonx/gwen-engine-core":"0.3.7"},"_npmOperationalInternal":{"tmp":"tmp/gwen-plugin-sprite-anim_0.3.7_1773311969478_0.648613899267898","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@djodjonx/gwen-plugin-sprite-anim","version":"1.0.0","description":"GWEN Sprite Animation Plugin — Animator runtime for defineUI","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"dependencies":{"@djodjonx/gwen-engine-core":"1.0.0","@djodjonx/gwen-kit":"1.0.0"},"devDependencies":{"typescript":"^5.9.3","vite":"^7.3.1","vite-plugin-dts":"^4.5.4","vitest":"^4.0.18"},"peerDependencies":{"@djodjonx/gwen-engine-core":"1.0.0"},"gwen":{"type":"typescript","serviceTypes":{"animator":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimatorService"}},"hookTypes":{"spriteAnim:complete":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"},"spriteAnim:frame":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"},"spriteAnim:transition":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimPluginHooks"}},"uiExtensionTypes":{"spriteAnim":{"from":"@djodjonx/gwen-plugin-sprite-anim","exportName":"SpriteAnimUIExtension"}}},"scripts":{"dev":"vite","build":"vite build","test":"vitest run","bench":"vitest bench --run","bench:watch":"vitest bench --watch","test:watch":"vitest","watch":"vite build --watch","typecheck":"tsc --noEmit"},"_id":"@djodjonx/gwen-plugin-sprite-anim@1.0.0","_integrity":"sha512-o3ISwY2Mg8hrsuatfPtt31Z7PxGJ/hAxZ1IxqlUmBBsT/R8zRIC2obUrKUsrsvdy5Jbg2WqtDM7C+PVcRE04Eg==","_resolved":"/private/var/folders/vc/41dnkkc11kqfyrhq8hmhj9400000gn/T/e1b40c7d7967fbf0c8b4cc70c31c3d95/djodjonx-gwen-plugin-sprite-anim-1.0.0.tgz","_from":"file:djodjonx-gwen-plugin-sprite-anim-1.0.0.tgz","_nodeVersion":"22.21.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-o3ISwY2Mg8hrsuatfPtt31Z7PxGJ/hAxZ1IxqlUmBBsT/R8zRIC2obUrKUsrsvdy5Jbg2WqtDM7C+PVcRE04Eg==","shasum":"b27fbe6fc8cdaa298ce43f55f6295c8e61cbd9fd","tarball":"https://registry.npmjs.org/@djodjonx/gwen-plugin-sprite-anim/-/gwen-plugin-sprite-anim-1.0.0.tgz","fileCount":6,"unpackedSize":62996,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDtk4rpis9I+01bVt8SQ+nF00xHIsYMAsyuD9q+o2+sJgIhAKErxK2/psb2whYNo9YNJHQMYBlyw6hhysuvqwSz4Ory"}]},"_npmUser":{"name":"djodjonx","email":"djo.moutier@gmail.com"},"directories":{},"maintainers":[{"name":"djodjonx","email":"djo.moutier@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/gwen-plugin-sprite-anim_1.0.0_1773671195722_0.19816650409349323"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-12T10:39:29.412Z","modified":"2026-03-16T14:26:35.946Z","0.3.7":"2026-03-12T10:39:29.620Z","1.0.0":"2026-03-16T14:26:35.851Z"},"description":"GWEN Sprite Animation Plugin — Animator runtime for defineUI","maintainers":[{"name":"djodjonx","email":"djo.moutier@gmail.com"}],"readme":"# @djodjonx/gwen-plugin-sprite-anim\n\nOfficial GWEN plugin for spritesheet animation with an Animator-like state machine.\n\n## Installation\n\n```bash\npnpm add @djodjonx/gwen-plugin-sprite-anim\n```\n\n## Registration\n\n```ts\nimport { defineConfig } from '@djodjonx/gwen-kit';\nimport { SpriteAnimPlugin } from '@djodjonx/gwen-plugin-sprite-anim';\n\nexport default defineConfig({\n  plugins: [\n    new SpriteAnimPlugin({\n      autoUpdate: true,\n      fixedDelta: 1 / 60,\n      maxSubSteps: 8,\n      maxFrameAdvancesPerEntity: 16,\n    }),\n  ],\n});\n```\n\n## Target DX\n\n- Keep using `defineUI`.\n- Declare `extensions.spriteAnim` for atlas, clips, controller, and transitions.\n- Render through `api.services.get('animator').draw(...)`.\n- Drive logic from systems (`setParam`, `setTrigger`, `setState`, `play`).\n\n## V3 UI example\n\n```ts\nimport { defineUI } from '@djodjonx/gwen-engine-core';\n\nexport const PlayerUI = defineUI({\n  name: 'PlayerUI',\n  extensions: {\n    spriteAnim: {\n      atlas: '/sprites/player.png',\n      frame: { width: 32, height: 32, columns: 8 },\n      clips: {\n        idle: { row: 0, from: 0, to: 3, fps: 8, loop: true },\n        run: { row: 1, from: 0, to: 5, fps: 12, loop: true },\n        shoot: { row: 2, from: 0, to: 2, fps: 16, loop: false, next: 'idle' },\n      },\n      controller: {\n        initial: 'idle',\n        parameters: {\n          moving: { type: 'bool', default: false },\n          shoot: { type: 'trigger' },\n        },\n        states: {\n          idle: { clip: 'idle' },\n          run: { clip: 'run' },\n          shoot: { clip: 'shoot' },\n        },\n        transitions: [\n          { from: 'idle', to: 'run', conditions: [{ param: 'moving', op: '==', value: true }] },\n          { from: 'run', to: 'idle', conditions: [{ param: 'moving', op: '==', value: false }] },\n          { from: '*', to: 'shoot', priority: 1, conditions: [{ param: 'shoot' }] },\n          { from: 'shoot', to: 'idle', hasExitTime: true, exitTime: 0.95 },\n        ],\n      },\n    },\n  },\n\n  render(api, entityId) {\n    const r = api.services.get('renderer');\n    const p = api.getComponent(entityId, 'position') as { x: number; y: number } | null;\n    if (!p) return;\n\n    api.services.get('animator').draw(r.ctx, entityId, p.x, p.y, {\n      pixelSnap: true,\n      cullRect: { x: 0, y: 0, width: r.logicalWidth, height: r.logicalHeight },\n    });\n  },\n});\n```\n\n## Gameplay system example\n\n```ts\nconst animator = api.services.get('animator');\n\nanimator.setParam(playerId, 'moving', isMoving);\nif (didShoot) animator.setTrigger(playerId, 'shoot');\n```\n\n## Exposed hooks\n\n- `spriteAnim:frame` - frame update event\n- `spriteAnim:complete` - clip completed event\n- `spriteAnim:transition` - controller state transition event\n\n## Runtime benchmarks\n\nThe package includes reproducible benchmarks for the `tick()` hot path.\n\n```bash\npnpm --filter @djodjonx/gwen-plugin-sprite-anim bench\n```\n\nMeasured scenarios:\n\n- `clip-only x2k entities (120 frames)`\n- `controller x2k entities + param churn (120 frames)`\n- `controller x10k entities (60 frames)`\n- `attach/detach churn x2k entities (pooling)`\n\nQuick interpretation:\n\n- If your target scenario consistently exceeds ~1.5-2.0 ms/frame CPU for animation, consider a Rust/WASM backend.\n- Otherwise, keep TS/JS and optimize allocation patterns and data layout first.\n- Details: `BENCHMARKS.md`\n\n## Detailed API docs\n\n- `docs/API.md`\n- `docs/hooks.md`\n- `docs/systems.md`\n\n## Troubleshooting Sprite Playback\n\n### Symptom: \"I see left-to-right scrolling instead of animation\"\n\nThis is usually not a runtime bug. It often comes from clip layout and loop boundaries.\n\nWhen you define a clip like this:\n\n```ts\nidle: { row: 0, from: 0, to: 7, fps: 10, loop: true }\n```\n\nthe runtime plays frames in this exact order:\n\n```text\n0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 0 -> ...\n```\n\nIf frame `0` and frame `7` are visually far apart, the `7 -> 0` loop reset can look like horizontal scrolling.\n\n### Recommended fix\n\nUse explicit `frames` sequences and avoid abrupt loop jumps:\n\n```ts\nidle: { frames: [1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2], fps: 9, loop: true }\n```\n\nThis ping-pong pattern removes the hard wrap from the last frame back to the first frame.\n\n### Quick diagnosis checklist\n\n1. Verify atlas frame size and `columns` are correct.\n2. Temporarily force a single frame clip (`frames: [N]`).\n3. If single-frame is stable, the issue is sequence/loop perception, not sampling.\n4. Reduce FPS and/or use explicit frame arrays for smoother perceived motion.\n\n### Practical notes\n\n- Large source frames rendered very small can amplify visual jitter.\n- Frame `0` is sometimes a setup/transition frame; excluding it can improve loops.\n- Use `next: 'idle'` for one-shot clips (`shoot`, `hit`) to avoid accidental wrap artifacts.\n","readmeFilename":"README.md"}