{"id":22861884,"url":"https://github.com/escapace/snaproll","last_synced_at":"2026-04-15T01:03:01.825Z","repository":{"id":61951518,"uuid":"556470583","full_name":"escapace/snaproll","owner":"escapace","description":"Fixed‑timestep update loop with independent draw rate and quantized interpolation.","archived":false,"fork":false,"pushed_at":"2026-04-08T19:15:08.000Z","size":1610,"stargazers_count":0,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"trunk","last_synced_at":"2026-04-08T21:19:39.483Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://escapace.github.io/snaproll/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/escapace.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2022-10-23T23:14:00.000Z","updated_at":"2026-03-10T17:52:21.000Z","dependencies_parsed_at":"2023-12-23T02:44:48.412Z","dependency_job_id":"b98f19a8-dbdc-4353-9216-ffd7fd44601a","html_url":"https://github.com/escapace/snaproll","commit_stats":{"total_commits":17,"total_committers":3,"mean_commits":5.666666666666667,"dds":"0.47058823529411764","last_synced_commit":"af4fa7a5b5e0bb395260d1b1353c6288179ff34e"},"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/escapace/snaproll","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fsnaproll","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fsnaproll/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fsnaproll/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fsnaproll/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/escapace","download_url":"https://codeload.github.com/escapace/snaproll/tar.gz/refs/heads/trunk","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fsnaproll/sbom","scorecard":{"id":925214,"data":{"date":"2025-08-23T02:50:25Z","repo":{"name":"github.com/escapace/snaproll","commit":"9f6cadea256f40eddc3fb26a578990f5da0bc710"},"scorecard":{"version":"v5.2.1","commit":"ab2f6e92482462fe66246d9e32f642855a691dc1"},"score":6,"checks":[{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yaml:1","Info: detected update tool: RenovateBot: .github/renovate.json5:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dependency-update-tool"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/29 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql-analysis.yaml:23","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yaml:24","Info: jobLevel 'packages' permission set to 'read': .github/workflows/codeql-analysis.yaml:25","Warn: jobLevel 'security-events' permission set to 'write': .github/workflows/codeql-analysis.yaml:26","Info: jobLevel 'contents' permission set to 'read': .github/workflows/dependency-review.yaml:11","Warn: jobLevel 'security-events' permission set to 'write': .github/workflows/scorecard.yaml:22","Info: jobLevel 'actions' permission set to 'read': .github/workflows/scorecard.yaml:19","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecard.yaml:20","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yaml:18","Info: topLevel 'packages' permission set to 'read': .github/workflows/ci.yaml:19","Info: topLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yaml:12","Info: topLevel 'packages' permission set to 'read': .github/workflows/codeql-analysis.yaml:13","Info: found token with 'none' permissions: .github/workflows/dependency-review.yaml:1","Warn: no topLevel permission defined: .github/workflows/release-github-pages.yaml:1","Warn: no topLevel permission defined: .github/workflows/release.yaml:1","Info: found token with 'none' permissions: .github/workflows/scorecard.yaml:1"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#cii-best-practices"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Mozilla Public License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#license"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/escapace/.github/SECURITY.md:1","Info: Found linked content: github.com/escapace/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/escapace/.github/SECURITY.md:1","Info: Found text in security policy: github.com/escapace/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#security-policy"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":3,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'trunk'","Info: 'force pushes' disabled on branch 'trunk'","Warn: 'branch protection settings apply to administrators' is disabled on branch 'trunk'","Warn: could not determine whether codeowners review is allowed","Warn: no status checks found to merge onto branch 'trunk'","Warn: PRs are not required to make changes on branch 'trunk'; or we don't have data to detect it.If you think it might be the latter, make sure to run Scorecard with a PAT or use Repo Rules (that are always public) instead of Branch Protection settings"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 10 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#sast"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#fuzzing"}},{"name":"Contributors","score":0,"reason":"project has 0 contributing companies or organizations -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#contributors"}},{"name":"CI-Tests","score":10,"reason":"10 out of 10 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#ci-tests"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-25T10:57:11.467Z","repository_id":61951518,"created_at":"2025-08-25T10:57:11.468Z","updated_at":"2025-08-25T10:57:11.468Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31821686,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T18:05:02.291Z","status":"ssl_error","status_checked_at":"2026-04-14T18:05:01.765Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":[],"created_at":"2024-12-13T10:11:28.215Z","updated_at":"2026-04-15T01:03:01.810Z","avatar_url":"https://github.com/escapace.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# snaproll\n\nFixed‑timestep update loop with independent draw rate and quantized interpolation.\n\n## Installation\n\n```bash\npnpm add snaproll\n```\n\n## Getting Started\n\n```js\nimport { Snaproll, SnaprollActionType } from 'snaproll'\n\n// Create with custom rates and context\nconst snaproll = new Snaproll({\n  updateRate: 60, // Fixed timestep updates at 60 Hz\n  drawRate: 60, // Draw calls at 60 Hz\n  context: { score: 0 },\n})\n\n// Subscribe to the animation loop\nconst subscription = snaproll.subscribe((context) =\u003e {\n  switch (context.action) {\n    case SnaprollActionType.Begin:\n      // Frame initialization - check if ready to proceed\n      const skipFrame = !document.hasFocus()\n\n      // Return true to skip this entire frame\n      return skipFrame\n    case SnaprollActionType.Update:\n      // Fixed timestep animation logic\n      const coalesce = context.updateStep \u003e= 100\n      const totalTime = coalesce ? context.updateStep * context.timestep : context.timestep\n\n      // Update animation state\n      context.score += coalesce ? context.updateStep : 1\n\n      // Return true to skip remaining updates this frame\n      return coalesce\n    case SnaprollActionType.Draw:\n      // Interpolated drawing with quantized alpha\n      draw(context.alpha)\n      break\n  }\n})\n\n// Start the animation loop\nsnaproll.resume()\n\n// Dynamic rate adjustment\nsnaproll.updateRate = 30 // Reduce to 30 Hz updates\n\n// Manual control\nsubscription.pause() // Pause this subscription\nsubscription.resume() // Resume after\n\n// Reset with new configuration\nsnaproll.reset({\n  updateRate: 120,\n  drawRate: 60,\n  context: { score: 0 },\n  keepSubscriptions: true,\n})\n```\n\n### Two independent rates\n\n- **updateRate** — how often animation logic runs (defaults to 60 Hz).\n- **drawRate** — how often frames draw to screen (defaults to 60 Hz).\n\n### Timing system\n\nSnaproll uses a bang-bang digital PLL (phase-locked loop) that captures requestAnimationFrame edges nearest to the target cadence. The PLL uses `performance.now()` for high-precision timing and computes phase error in target-frame units with a symmetric dead-zone (epsilon = 3e-3 ≈ 50µs at 60Hz) to determine when to advance the target timestamp. This keeps timing error bounded to within ±0.5 target frame periods while decimating 120→60 Hz, 144→48 Hz, 60→30 Hz cleanly without drift.\n\n### Quantized interpolation\n\nDuring the Draw phase, snaproll provides an `alpha` value \\[0, 1) representing fractional progress toward the next update. The alpha value is quantized to a power-of-two grid based on the draw rate. The quantization grid is calculated as `2^⌈log₂(drawRate * 2)⌉`. For example, a 60 Hz draw rate uses a 128-step quantization grid. The alpha is calculated as `((alpha * grid + 0.5) | 0) / grid` clamped to `(grid-1)/grid`, which rounds to the nearest grid step. This controlled quantization improves visual consistency at the cost of temporal precision.\n\n### Configuration\n\nBoth rates accept any positive number and can be changed while running:\n\n```js\nconst snaproll = new Snaproll({\n  updateRate: 60, // animation logic frequency\n  drawRate: 30, // drawing frequency\n})\n\n// Change rates dynamically\nsnaproll.updateRate = 120 // higher precision\nsnaproll.drawRate = 60 // smoother drawing\n```\n\n## Animation loop structure\n\nThe subscription function receives the same context object during each phase of the animation loop. Each phase populates different fields within this shared context object.\n\n### Phase sequence\n\nEach frame follows this order:\n\n1. **Begin** — Frame initialization with current timestamp\n2. **Update** — Animation calculations (may repeat)\n3. **Draw** — Render using interpolated values\n\n### Multiple updates per frame\n\nMultiple Update phases may run before Draw. The `updateStep` field counts down the remaining updates in the current frame, starting from the total number needed and decrementing to 1 on the final update. For example, if 5 updates are needed in a frame, `updateStep` will be 5, then 4, then 3, then 2, then 1 across the five Update calls. This allows subscription functions to make optimization decisions based on update backlog.\n\n```js\ncase SnaprollActionType.Update:\n  console.log(`${context.updateStep} updates remaining this frame`)\n  updateAnimation(context.timestep)\n  break\n```\n\n## Configuration\n\n### Initial setup\n\n```js\nconst snaproll = new Snaproll({\n  updateRate: 120, // Higher precision updates\n  drawRate: 60, // Standard display refresh\n  context: { score: 0 }, // Initial application state\n})\n```\n\n### Runtime rate changes\n\nUpdate rates while the animation runs. The accumulator is preserved during rate changes:\n\n```js\n// Rates take effect immediately, accumulator unchanged\nsnaproll.updateRate = 30 // Slower animation calculations\nsnaproll.drawRate = 120 // Higher refresh rate\n```\n\n### Resetting the loop\n\nUse `reset()` to clear accumulator state and optionally reconfigure:\n\n```js\n// Reset with new configuration\nsnaproll.reset({\n  updateRate: 60,\n  drawRate: 30,\n  keepSubscriptions: true, // Keep existing subscriptions (default)\n  keepContext: true, // Preserve context object (default)\n})\n\n// Reset accumulator only\nsnaproll.reset()\n```\n\nReset behavior:\n\n- Clears internal accumulator state\n- Preserves subscriptions and context by default\n- Resumes automatically if the loop was running\n- Keeps current rates unless new ones provided\n\n### Context management\n\nControl what happens to the context when calling `reset()`:\n\n```js\n// Use provided context object, merge existing context into it\nsnaproll.reset({\n  context: { newField: 'value' },\n  keepContext: true,\n})\n// Result: { newField: 'value', ...existingContextProperties }\n// Note: existing properties overwrite conflicting new ones\n\n// Use provided context object\nsnaproll.reset({\n  context: { onlyField: 'value' },\n  keepContext: false,\n})\n// Result: { onlyField: 'value' }\n\n// Keep existing context unchanged\nsnaproll.reset({ keepContext: true })\n\n// Start with empty context\nsnaproll.reset({ keepContext: false })\n```\n\n## Advanced Usage\n\nReturn `true` from a subscription to control when expensive operations run.\n\n### Skip frames when needed\n\nReturn `true` from Begin to skip the entire frame. The accumulator remains untouched when skipping frames:\n\n```js\ncase SnaprollActionType.Begin:\n  if (!assetsLoaded) {\n    return true  // Skip this frame entirely, accumulator unchanged\n  }\n  break\n```\n\n### Handle update backlog\n\nUse `updateStep` to detect multiple queued updates and optimize accordingly. Zeroing the accumulator prevents spiral of death scenarios where update costs exceed frame budget:\n\n```js\ncase SnaprollActionType.Update: {\n  const coalesce = context.updateStep \u003e= 100\n  // Physics substep merge: combine multiple fixed timesteps into one larger step\n  const totalTime = coalesce ? context.updateStep * context.timestep : context.timestep\n\n  updateAnimation(totalTime)\n\n  // Returning true zeros the accumulator and skips remaining updates and Draw phase for all subscribers\n  return coalesce\n}\n```\n\n### Execution order\n\n- Subscriptions run in the order they were added\n- All subscriptions process each phase before moving to the next\n- Returning `true` from Update affects the current frame for all subscribers\n\n### Subscription management\n\n```js\nconst subscription = snaproll.subscribe((context) =\u003e {\n  // Handle animation phases\n})\n\n// Control individual subscriptions\nsubscription.pause()\nsubscription.resume()\nsubscription.unsubscribe()\n\nsnaproll.pause() // Stop animation, keep subscriptions\nsnaproll.resume() // Resume animation\n```\n\nLoop states:\n\n- **active** — Running animation frames (has active subscriptions)\n- **idle** — Not running (no active subscriptions)\n- **paused** — Stopped by `snaproll.pause()` (subscriptions remain)\n\nUnless paused, snaproll starts automatically when subscriptions are added and becomes idle when all subscriptions are paused or removed.\n\n### Shared context\n\nUse TypeScript declaration merging to add type safety for shared state:\n\n```ts\ndeclare module 'snaproll' {\n  interface SnaprollUserContext {\n    score?: number\n  }\n}\n\n// Now available in all context handlers\nsnaproll.subscribe((context) =\u003e {\n  if (context.action === SnaprollActionType.Update) {\n    context.score += 10\n  }\n})\n```\n\n## SnaprollDrawRateAdvisor\n\nMeasures frame timing to detect display's refresh rate and recommend compatible draw rates.\n\n```js\nimport { SnaprollDrawRateAdvisor } from 'snaproll'\n\n// Create advisor with optional configuration\nconst advisor = new SnaprollDrawRateAdvisor({\n  samples: 90, // Collect 90 frame intervals\n  warmup: 10, // Skip first 10 frames\n  minDraw: 10, // Minimum recommended rate\n})\n\n// Subscribe to recommendations\nconst unsubscribe = advisor.subscribe((response) =\u003e {\n  console.log(`Quality score: ${response.score.toFixed(3)}`)\n  console.log(`Recommended rates: ${response.values.join(', ')} Hz`)\n})\n\n// Start analysis\nadvisor.trigger()\n```\n\nThe advisor provides quality scores \\[0-1] using RF (Robustness × Fit) formula and recommended draw rates sorted descending. Scores ≥0.85 indicate healthy timing with mild jitter; scores \u003c0.60 show strong evidence of blocking/jitter or regime split and recommend re-running.\n\n## Examples\n\nView examples at \u003chttps://escapace.github.io/snaproll/\u003e or see the `examples/` directory:\n\n- **Bouncing Balls** (`canvas-2d-bouncing-balls.vue`) — Canvas animation with interpolated movement\n- **Moving Rectangles** (`css-transform-rectangles.vue`) — CSS transform animation with smooth transitions\n\nEach example demonstrates different aspects of snaproll:\n\n- Frame rate independence\n- Smooth interpolation using alpha values\n- Performance optimization techniques\n- Multiple subscription management\n\n## Timing precision and HTTP headers\n\nFor higher timer precision in simulation or measurement workloads, configure the top-level response with:\n\n```http\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\n```\n\n`Cross-Origin-Embedder-Policy: credentialless` is a possible alternative in some deployments, but support is not universal (notably unavailable in Safari and Safari on iOS).\n\n## API\n\n### class Snaproll [↗](src/snaproll.ts#L460-L700 'Snaproll')\n\nFixed-timestep animation loop with independent draw and update rates.\n\n```typescript\nexport declare class Snaproll\n```\n\n#### new Snaproll\n\nCreates animation loop instance with specified configuration.\n\n```typescript\nconstructor(options?: Partial\u003cSnaprollOptions\u003e);\n```\n\n##### Parameters\n\n| Parameter | Type                                                                                           | Description                            |\n| --------- | ---------------------------------------------------------------------------------------------- | -------------------------------------- |\n| `options` | \u003cpre\u003ePartial\u003c[SnaprollOptions](#interface-snaprolloptions- 'interface SnaprollOptions')\u003e\u003c/pre\u003e | Options applied during initialization. |\n\n##### Remarks\n\nUses the default [updateRate](#snaprolloptionsupdaterate) of 60 Hz, [drawRate](#snaprolloptionsdrawrate) of 60 Hz, and a new shared context object when those entries are not provided.\n\n#### Snaproll.pause\n\nStops animation while keeping subscriptions.\n\n```typescript\npause(): void;\n```\n\n#### Snaproll.reset\n\nResets the animation loop with optional configuration updates.\n\n```typescript\nreset(options?: SnaprollResetOptions): void;\n```\n\n##### Parameters\n\n| Parameter | Type                                                                                                 | Description                                                               |\n| --------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |\n| `options` | \u003cpre\u003e[SnaprollResetOptions](#interface-snaprollresetoptions- 'interface SnaprollResetOptions')\u003c/pre\u003e | Reset options controlling configuration overrides and retention behavior. |\n\n##### Remarks\n\nRetains the current [updateRate](#snaprolloptionsupdaterate) and [drawRate](#snaprolloptionsdrawrate) when those fields are omitted.\n\n- `keepSubscriptions` defaults to `true`, preserving existing subscriptions unless explicitly disabled.\n- `keepContext` defaults to `true`, reusing the current context. Providing [context](#interface-snaprollusercontext-) copies existing entries when `keepContext` stays `true`, or replaces the context when `keepContext` is `false`.\n\n#### Snaproll.resume\n\nResumes animation.\n\n```typescript\nresume(): void;\n```\n\n#### Snaproll.subscribe\n\nRegisters subscription callback for animation frame processing.\n\n```typescript\nsubscribe(value: SnaprollSubscription, options?: {\n  immediate?: boolean;\n}): SnaprollSubscriptionControls;\n```\n\n##### Parameters\n\n| Parameter | Type                                                                                       | Description                                                                                                         |\n| --------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |\n| `value`   | \u003cpre\u003e[SnaprollSubscription](#type-snaprollsubscription- 'type SnaprollSubscription')\u003c/pre\u003e | Callback function to execute during frame processing.                                                               |\n| `options` | \u003cpre\u003e{\u003cbr\u003e immediate?: boolean;\u003cbr\u003e}\u003c/pre\u003e                                                 | Subscription configuration. The `immediate` flag defaults to `true` and activates the subscription on registration. |\n\n##### Returns\n\n[Control object](#interface-snaprollsubscriptioncontrols-) for pausing, resuming, and removing the subscription.\n\n##### Remarks\n\nSubscriptions run in insertion order. New subscriptions start immediately unless `options.immediate` is set to `false`.\n\n#### Snaproll.drawRate\n\nGetter and setter for the current draw rate in Hz.\n\n```typescript\nget drawRate(): number;\nset drawRate(value: number);\n```\n\n#### Snaproll.state\n\nCurrent animation loop state.\n\nReturns 'active' when running animation frames, 'idle' when no active subscriptions, or 'paused' when stopped by pause().\n\n```typescript\nget state(): \"active\" | \"idle\" | \"paused\";\n```\n\n#### Snaproll.updateRate\n\nGetter and setter for the current update rate in Hz.\n\n```typescript\nget updateRate(): number;\nset updateRate(value: number);\n```\n\n### class SnaprollDrawRateAdvisor [↗](src/draw-rate-advisor.ts#L241-L311 'SnaprollDrawRateAdvisor')\n\nProvides draw rates inferred from observed frame periods.\n\nThe recommendation logic is stateless and deterministic: identical period inputs produce identical outputs. Concurrent class instances do not interfere with each other.\n\n```typescript\nexport declare class SnaprollDrawRateAdvisor\n```\n\n#### new SnaprollDrawRateAdvisor\n\nCreates a new draw rate advisor instance.\n\n```typescript\nconstructor(options?: SnaprollDrawRateAdvisorOptions);\n```\n\n##### Parameters\n\n| Parameter | Type                                                                                                                               | Description                           |\n| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |\n| `options` | \u003cpre\u003e[SnaprollDrawRateAdvisorOptions](#interface-snaprolldrawrateadvisoroptions- 'interface SnaprollDrawRateAdvisorOptions')\u003c/pre\u003e | Configuration options for the advisor |\n\n##### Throws\n\nAssertion error if any option values are invalid\n\n#### SnaprollDrawRateAdvisor.dispose\n\nCancels ongoing operations and releases resources.\n\n```typescript\ndispose(): void;\n```\n\n#### SnaprollDrawRateAdvisor.subscribe\n\nRegisters callback for recommendation results.\n\nCallbacks receive SnaprollDrawRateAdvisorResponse when recommendation completes successfully. Callbacks are not invoked if the operation is canceled or produces no recommendations.\n\n```typescript\nsubscribe(subscription: SnaprollDrawRateAdvisorSubscription): () =\u003e void;\n```\n\n##### Parameters\n\n| Parameter      | Type                                                                                                                                    | Description                              |\n| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |\n| `subscription` | \u003cpre\u003e[SnaprollDrawRateAdvisorSubscription](#type-snaprolldrawrateadvisorsubscription- 'type SnaprollDrawRateAdvisorSubscription')\u003c/pre\u003e | Callback function to invoke with results |\n\n##### Returns\n\nFunction that unregisters the subscription when called\n\n#### SnaprollDrawRateAdvisor.trigger\n\nCancels any previous ongoing operation before starting a new one. Results are delivered to registered subscriptions when the operation completes. If the operation is canceled or produces no recommendations, subscriptions are not invoked.\n\n```typescript\ntrigger(): void;\n```\n\n### enum SnaprollActionType [↗](src/snaproll.ts#L8-L12 'SnaprollActionType')\n\nAnimation frame phases.\n\n```typescript\nexport declare enum SnaprollActionType\n```\n\n#### Members\n\n| Member   | Value        |\n| -------- | ------------ |\n| `Begin`  | \u003cpre\u003e0\u003c/pre\u003e |\n| `Update` | \u003cpre\u003e1\u003c/pre\u003e |\n| `Draw`   | \u003cpre\u003e2\u003c/pre\u003e |\n\n### interface SnaprollActionBegin [↗](src/snaproll.ts#L17-L21 'SnaprollActionBegin')\n\nFrame initialization action.\n\n```typescript\nexport interface SnaprollActionBegin\n```\n\n#### SnaprollActionBegin.timestamp\n\nCurrent frame time\n\n```typescript\ntimestamp: number\n```\n\n### interface SnaprollActionDraw [↗](src/snaproll.ts#L38-L42 'SnaprollActionDraw')\n\nInterpolated drawing action.\n\n```typescript\nexport interface SnaprollActionDraw extends Omit\u003cSnaprollActionUpdate, 'action'\u003e\n```\n\n#### SnaprollActionDraw\\.alpha\n\nInterpolation factor \\[0, 1)\n\n```typescript\nalpha: number\n```\n\n### interface SnaprollActionUpdate [↗](src/snaproll.ts#L27-L33 'SnaprollActionUpdate')\n\nFixed timestep animation logic action. The updateStep counts down remaining updates in the current frame.\n\n```typescript\nexport interface SnaprollActionUpdate extends Omit\u003cSnaprollActionBegin, 'action'\u003e\n```\n\n#### SnaprollActionUpdate.timestep\n\nTime to advance per update\n\n```typescript\ntimestep: number\n```\n\n#### SnaprollActionUpdate.updateStep\n\nRemaining updates this frame, counts down to 1\n\n```typescript\nupdateStep: number\n```\n\n### interface SnaprollDrawRateAdvisorOptions [↗](src/draw-rate-advisor.ts#L165-L196 'SnaprollDrawRateAdvisorOptions')\n\n#### SnaprollDrawRateAdvisorOptions.canonicalBases\n\nArray of canonical refresh rates to consider for snapping. Must be non-empty array of positive integers.\n\n```typescript\ncanonicalBases?: readonly number[];\n```\n\n#### SnaprollDrawRateAdvisorOptions.maxDivisor\n\nMaximum count of divisors to consider for candidate generation and output. Must be positive integer ≥ 1.\n\n```typescript\nmaxDivisor?: number;\n```\n\n#### SnaprollDrawRateAdvisorOptions.minDraw\n\nMinimum draw rate to include in results. Must be positive integer ≥ 5.\n\n```typescript\nminDraw?: number;\n```\n\n#### SnaprollDrawRateAdvisorOptions.samples\n\nNumber of frame intervals to collect after warmup. Must be positive integer ≥ 30.\n\n```typescript\nsamples?: number;\n```\n\n#### SnaprollDrawRateAdvisorOptions.warmup\n\nNumber of frames to ignore before sampling. Must be non-negative integer ≥ 0.\n\n```typescript\nwarmup?: number;\n```\n\n### interface SnaprollDrawRateAdvisorResponse [↗](src/draw-rate-advisor.ts#L130-L154 'SnaprollDrawRateAdvisorResponse')\n\n#### SnaprollDrawRateAdvisorResponse.score\n\nQuality score in range \\[0,1] using RF (Robustness × Fit) formula. Higher scores indicate better data consistency and more reliable recommendations.\n\nScore interpretation:\n\n- `0.95–1.00`: Rock-solid. Extremely stable capture, fits a canonical/divisor cleanly\n- `0.85–0.95`: Healthy. Mild jitter only; values are trustworthy\n- `0.75–0.85`: Borderline steady. Noticeable instability or light regime mixing; fine for most uses, re-run if chasing perfection\n- `0.60–0.75`: Shaky. Significant jitter or likely mid-phase change; consider re-running\n- `\u003c 0.60` : Unstable. Strong evidence of blocking/jitter or regime split; re-run recommended\n\n```typescript\nscore: number\n```\n\n#### SnaprollDrawRateAdvisorResponse.values\n\nArray of recommended draw rates (Hz), sorted descending and de-duplicated. All values are integers and ≥ minDraw.\n\n```typescript\nvalues: number[];\n```\n\n### interface SnaprollOptions [↗](src/snaproll.ts#L95-L111 'SnaprollOptions')\n\nConfiguration interface for animation loop.\n\n```typescript\nexport interface SnaprollOptions\n```\n\n#### Remarks\n\ndrawRate and updateRate operate independently, allowing different frequencies for draw and update rates.\n\n#### SnaprollOptions.context\n\nOptional shared state object passed to all subscription callbacks.\n\n```typescript\ncontext?: SnaprollUserContext;\n```\n\n#### SnaprollOptions.drawRate\n\nDraw rate in Hz, controls visual frame timing.\n\n```typescript\ndrawRate: number\n```\n\n#### SnaprollOptions.updateRate\n\nUpdate rate in Hz, determines fixed timestep size.\n\n```typescript\nupdateRate: number\n```\n\n### interface SnaprollResetOptions [↗](src/snaproll.ts#L119-L146 'SnaprollResetOptions')\n\nOptions accepted by [Snaproll.reset](#snaprollreset).\n\n```typescript\nexport interface SnaprollResetOptions extends Partial\u003cSnaprollOptions\u003e\n```\n\n#### Remarks\n\nExtends [SnaprollOptions](#interface-snaprolloptions-) and controls how subscriptions and context objects are preserved.\n\n#### SnaprollResetOptions.context\n\nDetermines the [context](#interface-snaprollusercontext-) to use after reset completes.\n\n```typescript\ncontext?: SnaprollUserContext;\n```\n\n##### Remarks\n\nThe context resolution follows these rules:\n\n- If `keepContext=true` and `context` is provided: applies the provided object and copies existing context into it.\n- If `keepContext=false` and `context` is provided: uses the provided object as-is.\n- If `keepContext=true` and `context` is omitted: reuses the existing context object.\n- If `keepContext=false` and `context` is omitted: creates a new empty context object.\n\n#### SnaprollResetOptions.keepContext\n\nPreserve the existing context object during reset.\n\n```typescript\nkeepContext?: boolean;\n```\n\n#### SnaprollResetOptions.keepSubscriptions\n\nPreserve existing subscription callbacks during reset.\n\n```typescript\nkeepSubscriptions?: boolean;\n```\n\n### interface SnaprollSubscriptionControls [↗](src/snaproll.ts#L63-L70 'SnaprollSubscriptionControls')\n\nControl interface for managing individual animation subscriptions.\n\n```typescript\nexport interface SnaprollSubscriptionControls\n```\n\n#### Remarks\n\nEach subscription operates independently. Pausing one subscription does not affect others. The animation loop continues running as long as any subscription remains active.\n\n#### SnaprollSubscriptionControls.pause\n\nPauses this subscription\n\n```typescript\npause: () =\u003e void;\n```\n\n#### SnaprollSubscriptionControls.resume\n\nResumes this subscription\n\n```typescript\nresume: () =\u003e void;\n```\n\n#### SnaprollSubscriptionControls.unsubscribe\n\nRemoves this subscription\n\n```typescript\nunsubscribe: () =\u003e void;\n```\n\n### interface SnaprollUserContext [↗](src/index.ts#L19 'SnaprollUserContext')\n\nExtension point for application-specific state that the animation loop shares across frames.\n\n```typescript\nexport interface SnaprollUserContext\n```\n\n#### Remarks\n\nAugment this interface via declaration merging so custom properties flow into [SnaprollContext](#type-snaprollcontext-). Snaproll maintains a single context instance per controller; store long-lived data on user fields and rely on [action-specific](#enum-snaprollactiontype-) payloads for phase details.\n\n#### Examples\n\n```ts\ndeclare module 'snaproll' {\n  interface SnaprollUserContext {\n    score: number\n  }\n}\n```\n\n### type SnaprollContext [↗](src/snaproll.ts#L53-L54 'SnaprollContext')\n\nContext object passed to subscription callbacks during animation frames.\n\n```typescript\nexport type SnaprollContext = (SnaprollActionBegin | SnaprollActionDraw | SnaprollActionUpdate) \u0026\n  SnaprollUserContext\n```\n\n#### Remarks\n\nIntersects the action-specific payload with [SnaprollUserContext](#interface-snaprollusercontext-), so that custom state persists across [loop phases](#enum-snaprollactiontype-). Inspect the `action` discriminant to determine which `SnaprollAction*` view is valid while treating application-specific fields as shared state.\n\n### type SnaprollDrawRateAdvisorSubscription [↗](src/draw-rate-advisor.ts#L161-L163 'SnaprollDrawRateAdvisorSubscription')\n\nCallback function invoked when draw rate estimation completes.\n\n```typescript\nexport type SnaprollDrawRateAdvisorSubscription = (\n  response: SnaprollDrawRateAdvisorResponse,\n) =\u003e void\n```\n\n### type SnaprollSubscription [↗](src/snaproll.ts#L84 'SnaprollSubscription')\n\nSubscription callback function.\n\n```typescript\nexport type SnaprollSubscription = (context: SnaprollContext) =\u003e boolean | undefined\n```\n\n#### Remarks\n\nReturn value controls frame execution flow:\n\n- `true` from Begin phase skips the entire frame\n- `true` from Update phase skips remaining updates and draw for current frame\n- `undefined` or `false` continues normal execution\n\n## Acknowledgments\n\n- [Godot](https://github.com/godotengine/godot/blob/master/scene/main/scene_tree.cpp)\n- [PixiJS Ticker](https://github.com/pixijs/pixijs/blob/dev/src/ticker/Ticker.ts)\n- [Babylon.js](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Engines/abstractEngine.ts)\n- [three.js Timer](https://github.com/mrdoob/three.js/blob/dev/src/core/Timer.js)\n- [MainLoop.js](https://github.com/IceCreamYou/MainLoop.js)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescapace%2Fsnaproll","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fescapace%2Fsnaproll","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescapace%2Fsnaproll/lists"}