{"id":16885299,"url":"https://github.com/niklasramo/tikki","last_synced_at":"2025-04-11T12:32:11.935Z","repository":{"id":40597932,"uuid":"492012197","full_name":"niklasramo/tikki","owner":"niklasramo","description":"A minimalistic game/animation loop orchestrator.","archived":false,"fork":false,"pushed_at":"2025-03-12T22:53:03.000Z","size":453,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-25T08:51:16.539Z","etag":null,"topics":["animation","event-emitter","events","frame","loop","raf","requestanimationframe","tick","ticker","typescript"],"latest_commit_sha":null,"homepage":"","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/niklasramo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","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},"funding":{"github":["niklasramo"]}},"created_at":"2022-05-13T19:07:23.000Z","updated_at":"2025-03-12T22:52:47.000Z","dependencies_parsed_at":"2024-03-16T23:52:42.322Z","dependency_job_id":"1070a664-4cfe-42be-81f7-d1b82e14c418","html_url":"https://github.com/niklasramo/tikki","commit_stats":{"total_commits":20,"total_committers":1,"mean_commits":20.0,"dds":0.0,"last_synced_commit":"a82c9ee26ccaa96ee58114c9664119b6366eaa31"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasramo%2Ftikki","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasramo%2Ftikki/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasramo%2Ftikki/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/niklasramo%2Ftikki/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/niklasramo","download_url":"https://codeload.github.com/niklasramo/tikki/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248401955,"owners_count":21097328,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["animation","event-emitter","events","frame","loop","raf","requestanimationframe","tick","ticker","typescript"],"created_at":"2024-10-13T16:34:33.766Z","updated_at":"2025-04-11T12:32:11.913Z","avatar_url":"https://github.com/niklasramo.png","language":"TypeScript","readme":"# Tikki\n\nTikki is a game/animation loop _orchestrator_ that allows you to group frame callbacks into _phases_ and dynamically modify their execution order. It's a simple and powerful abstraction that covers many use cases. Tikki is built on top of [`Eventti`](https://github.com/niklasramo/eventti), a highly optimized and battle-tested event emitter.\n\n- 🎯 Simple and intuitive API.\n- 🪶 Small footprint (~1kB minified and brotlied).\n- ⚙️ Works in Node.js and browser environments out of the box.\n- 🍦 Written in TypeScript with strict type definitions.\n- 🤖 Extensively tested.\n- 💝 Free and open source, MIT Licensed.\n\n## Install\n\n### Node\n\n```\nnpm install tikki eventti\n```\n\n```ts\nimport { Ticker } from 'tikki';\nconst ticker = new Ticker();\n```\n\n### Browser\n\n```html\n\u003cscript type=\"importmap\"\u003e\n  {\n    \"imports\": {\n      \"eventti\": \"https://cdn.jsdelivr.net/npm/eventti@4.0.3/dist/index.js\",\n      \"tikki\": \"https://cdn.jsdelivr.net/npm/tikki@3.0.2/dist/index.js\"\n    }\n  }\n\u003c/script\u003e\n\u003cscript type=\"module\"\u003e\n  import { Ticker } from 'tikki';\n  const ticker = new Ticker();\n\u003c/script\u003e\n```\n\n## Usage\n\nTikki comes in two flavors: `Ticker` and `AutoTicker`.\n\n`Ticker` class is basically just a thin wrapper around [Eventti](https://github.com/niklasramo/eventti)'s `Emitter` with a few tweaks to make it more suitable for our specific use case of orchestrating frame callbacks:\n\n- It replaces the concept of events with _phases_ which are a group of frame callbacks that are executed together. The order of phases can be changed dynamically whenever you want, it's just an array of phase names (`ticker.phases`). This can be useful for e.g. separating game/physics/rendering logic into different phases. You can even provide the same phase multiple times in which case it's callbacks are emitted multiple times on tick.\n- It replaces the `emit` method with a `tick` method, which executes all the frame callbacks of all the phases (in the order defined in `ticker.phases`) with the arguments you provide. You can think of it as a \"batched emit\" method.\n\n`AutoTicker` class extends the `Ticker` class and provides extra features to automatically tick the ticker on every animation frame, so it can be used as a drop-in replacement for your basic animation loop. It defaults to `requestAnimationFrame` and falls back to `setTimeout` in environments where `requestAnimationFrame` is not supported. You can also provide your own `requestFrame` method if you wish.\n\n### Basic usage\n\n```ts\nimport { Ticker, FrameCallback } from 'tikki';\n\n// Define allowed phases. If you don't provide these explicitly then the allowed\n// phases are inferred from the phases you provide to the ticker on\n// instantiation. If you don't provide any phases then any string, number or\n// symbol will be allowed as a valid phase.\ntype Phases = 'a' | 'b' | 'c';\n\n// Define the frame callback type. This is optional, but it's recommended to\n// provide a custom type if you want to enforce the frame callback arguments.\ntype FrameCallback = (time: number, dt: number) =\u003e void;\n\n// Create a ticker instance and define the phases.\nconst ticker = new Ticker\u003cPhases, FrameCallback\u003e({ phases: ['a', 'b', 'c'] });\n\n// Let's create a game loop that ticks the ticker manually.\nlet prevTime = 0;\nlet frameId: number | undefined = undefined;\nfunction gameLoop(time = 0) {\n  frameId = requestAnimationFrame(gameLoop);\n  if (prevTime \u003c time) {\n    const deltaTime = time - prevTime;\n    ticker.tick(time, deltaTime);\n  }\n  prevTime = time;\n  return () =\u003e {\n    cancelAnimationFrame(frameId);\n  };\n}\n\n// Start the game loop.\nlet stopGameLoop = gameLoop();\n\n// Stop the game loop when needed.\n// stopGameLoop();\n\n// And resume it again when needed\n// stopGameLoop = gameLoop(prevTime);\n\n// Add some frame callbacks to the phases.\nconst idA = ticker.on('a', (time, dt) =\u003e console.log('a', time, dt));\nconst idB = ticker.on('b', (time, dt) =\u003e console.log('b', time, dt));\nconst idC = ticker.on('c', (time, dt) =\u003e console.log('c', time, dt));\n\n// Add some frame callbacks to the phases that will be called only once.\nticker.once('a', (time, dt) =\u003e console.log('a once', time, dt));\nticker.once('b', (time, dt) =\u003e console.log('b once', time, dt));\nticker.once('c', (time, dt) =\u003e console.log('c once', time, dt));\n\n// Change the phases dynamically.\nticker.phases = ['c', 'a'];\n\n// Remove a frame callback from a phase by id.\nticker.off('a', idA);\n\n// You can also remove all the callbacks from a specific phase in one go.\nticker.off('b');\n\n// Or just remove all callbacks from the ticker.\nticker.off();\n```\n\n### Automatic ticking\n\nUsing `AutoTicker` is the same as using `Ticker`, but it ticks automatically on every animation frame. You can also pause and unpause the ticker at any time.\n\n```ts\n// Create ticker. It will start ticking automatically right away.\nconst ticker = new AutoTicker({ phases: ['a', 'b', 'c'] });\n\n// Add some frame callbacks to the phases. By default the AutoTicker provides\n// only the time of the frame to the frame callbacks.\nticker.on('a', (time) =\u003e console.log('a', time));\nticker.on('b', (time) =\u003e console.log('b', time));\nticker.on('c', (time) =\u003e console.log('c', time));\n\n// Pause the ticker any time you want.\nticker.paused = true;\n\n// And unpause it again when needed.\nticker.paused = false;\n```\n\n### On-demand ticking\n\n`AutoTicker` also supports on-demand ticking, which means that the ticker will tick only when there are frame callbacks in it. This can be useful if you don't care about the frame time and just want the ticker to tick when there are frame callbacks in it.\n\n```ts\n// Create ticker with onDemand option set to true.\nconst ticker = new AutoTicker({ phases: ['a', 'b', 'c'], onDemand: true });\n\n// Once you add a frame callback to the ticker it will start ticking\n// automatically, and keeps ticking as long as there are frame callbacks in it.\nticker.on('a', (time) =\u003e console.log('a', time));\n\n// If you remove all the frame callbacks from the ticker it will stop ticking.\nticker.off();\n\n// And if you add a frame callback again it will start ticking again.\nticker.on('a', (time) =\u003e console.log('a', time));\n```\n\n### Custom frame request\n\nYou can provide your own frame request to the ticker. This can be useful if you want to e.g. track the delta time between frames and provide it to the frame callbacks.\n\n```typescript\nimport { AutoTicker } from 'tikki';\n\n// Define the frame callback type.\ntype FrameCallback = (time: number, deltaTime: number) =\u003e void;\n\n// Create a custom frame request that tracks time and delta time.\nconst createRequestFrame = () =\u003e {\n  let prevTime = 0;\n\n  // The frame request method should accept a single argument - a callback which\n  // receives any arguments you see fit. These arguments are then passed to the\n  // frame callbacks.\n  return (callback: FrameCallback) =\u003e {\n    const rafId = requestAnimationFrame((time) =\u003e {\n      const deltaTime = prevTime \u003c time ? time - prevTime : 0;\n      prevTime = time;\n      callback(time, deltaTime);\n    });\n\n    // The frame request method should return a function that cancels the\n    // frame request.\n    return () =\u003e {\n      cancelAnimationFrame(rafId);\n    };\n  };\n};\n\n// Provide the custom requestFrame method to the ticker on init.\nconst ticker = new AutoTicker\u003c'test', FrameCallback\u003e({\n  phases: ['test'],\n  requestFrame: createRequestFrame(),\n});\n\n// Add a frame callback to the ticker.\nticker.on('test', (time, deltaTime) =\u003e {\n  console.log(time, deltaTime);\n});\n```\n\nTikki also exports a `createXrRequestFrame` method, which you can use to request [XRSession](https://developer.mozilla.org/en-US/docs/Web/API/XRSession) animation frames.\n\n```typescript\nimport { AutoTicker, createXrRequestFrame, XrFrameCallback } from 'tikki';\n\nconst xrTicker = await navigator.xr?.requestSession('immersive-vr').then((xrSession) =\u003e {\n  return new AutoTicker\u003c'test', XrFrameCallback\u003e({\n    phases: ['test'],\n    requestFrame: createXrRequestFrame(xrSession),\n  });\n});\n```\n\nSometimes you might need to switch the `requestFrame` method on the fly, e.g. when entering/exiting [XRSession](https://developer.mozilla.org/en-US/docs/Web/API/XRSession). Tikki covers this use case and allows you to change the `requestFrame` method dynamically at any time. We just need to inform `AutoTicker` of all the possible `requestFrame` type variations.\n\n```typescript\nimport { AutoTicker, createXrRequestFrame, XrFrameCallback } from 'tikki';\n\n// Define the frame callback types as a union of all the possible frame callback\n// types that the ticker might encounter. Note that due to limits of TypeScript\n// all the variations must have the same number of arguments, but you can use\n// `undefined` to mark optional arguments. Alternatively you can just create a\n// single custom frame callback type that has all the possible arguments and use\n// that.\ntype FrameCallback = ((time: number, frame?: undefined) =\u003e void) | XrFrameCallback;\n\n// Create ticker.\nconst ticker = new AutoTicker\u003c'test', FrameCallback\u003e({\n  phases: ['test'],\n});\n\n// At any point later on we can switch the requestFrame method.\nnavigator.xr?.requestSession('immersive-vr').then((xrSession) =\u003e {\n  ticker.requestFrame = createXrRequestFrame(xrSession);\n});\n\n// We can then check the arguments with type-safety inside the frame callbacks.\nticker.on('test', (time, frame) =\u003e {\n  if (frame) {\n    console.log('XR Frame!', time);\n  } else {\n    console.log('Normal Frame', time);\n  }\n});\n```\n\n## API\n\n- [Ticker](#ticker)\n  - [phases](#tickerphases)\n  - [dedupe](#tickerdedupe)\n  - [getId](#tickergetid)\n  - [on( phase, frameCallback, [ frameCallbackId ] )](#tickeron)\n  - [once( phase, frameCallback, [ frameCallbackId ] )](#tickeronce)\n  - [off( [ phase ], [ frameCallbackId ] )](#tickeroff)\n  - [count( [ phase ] )](#tickercount)\n  - [tick( [ ...args ] )](#tickertick)\n- [AutoTicker](#autoticker)\n  - [paused](#autotickerpaused)\n  - [onDemand](#autotickerondemand)\n  - [requestFrame](#autotickerrequestframe)\n\n### Ticker\n\n`Ticker` class wraps [`Eventti`](https://github.com/niklasramo/eventti)'s API and replaces the `emit` method with a `tick` method.\n\nThe `tick` method loops over the active phases (events) and collects all the frame callbacks (listeners) from them into a queue, and finally processes the queue executing the frame callbacks with the arguments you provide to the `tick` method. You can think of it as a \"batched emit\" method.\n\nAccepts a [`TickerOptions`](#tickeroptions) object as it's only argument.\n\n**Syntax**\n\n```\nconst ticker = new Ticker( [ options ] );\n```\n\n**Options**\n\n- **phases**\n  - See [phases](#tickerphases) docs.\n  - Accepts: [`Phase[]`](#phase).\n  - Optional. Defaults to `[]`.\n- **dedupe**\n  - See [dedupe](#tickerdedupe) docs.\n  - Accepts: [`TickerDedupe`](#tickerdedupe).\n  - Optional. Defaults to `\"add\"`.\n- **getId**\n  - See [getId](#tickergetid) docs.\n  - Accepts: `(frameCallback: FrameCallback) =\u003e FrameCallbackId`.\n  - Optional. Defaults to `() =\u003e Symbol()`.\n\n#### ticker.phases\n\nType: [`Phase[]`](#phase).\n\nAn array of phase names. You can change this array dynamically at any time to change the order of the phases. If you provide the same phase multiple times then it's callbacks are emitted multiple times on tick.\n\n#### ticker.dedupe\n\nType: [`TickerDedupe`](#tickerdedupe).\n\nDefines how a duplicate frame callback id is handled:\n\n- `\"add\"`: the existing callback (of the id) is removed and the new callback is appended to the phase's callback queue.\n- `\"update\"`: the existing callback (of the id) is replaced with the new callback without changing the index of the callback in the phase's callback queue.\n- `\"ignore\"`: the new callback is silently ignored and not added to the phase.\n- `\"throw\"`: as the name suggests an error will be thrown.\n\n#### ticker.getId\n\nType:\n\n```ts\n(frameCallback: FrameCallback) =\u003e FrameCallbackId;\n```\n\nA function which is used to get the frame callback id. By default Tikki uses `Symbol()` to create unique ids, but you can provide your own function if you want to use something else. Receives the frame callback as the first (and only) argument.\n\n#### ticker.on()\n\nAdd a frame callback to a phase.\n\n**Syntax**\n\n```\nticker.on( phase, frameCallback, [ frameCallbackId ] );\n```\n\n**Parameters**\n\n1. **phase**\n   - The name of the phase you want to add the frame callback to.\n   - Accepts: [`Phase`](#phase).\n2. **frameCallback**\n   - A frame callback that will be called on tick (if the phase is active).\n   - Accepts: [`FrameCallback`](#framecallback).\n3. **frameCallbackId**\n   - The id for the frame callback. If not provided, the id will be generated by the `ticker.getId` method.\n   - Accepts: [`FrameCallbackId`](#framecallbackid).\n   - Optional.\n\n**Returns**\n\nA [frame callback id](#framecallbackid), which can be used to remove this specific callback. Unless manually provided via arguments this will be whatever the `ticker.getId` method spits out, and by default it spits out symbols which are guaranteed to be always unique.\n\n#### ticker.once()\n\nAdd a one-off frame callback to a phase. This works identically to the `on` method with the exception that the frame callback is removed immediately after it has been called once. Please refer to the [`on`](#tickeron) method for more information.\n\n**Syntax**\n\n```\nticker.once( phase, frameCallback, [ frameCallbackId ] );\n```\n\n#### ticker.off()\n\nRemove a frame callback or multiple frame callbacks. If no _frameCallbackId_ is provided all frame callbacks for the specified phase will be removed. If no _phase_ is provided all frame callbacks from the ticker will be removed.\n\n**Syntax**\n\n```\nticker.off( [ phase ], [ frameCallbackId ] );\n```\n\n**Parameters**\n\n1. **phase**\n   - The phase you want to remove frame callbacks from.\n   - Accepts: [`Phase`](#phase).\n   - _optional_\n2. **frameCallbackId**\n   - The id of the frame callback you want to remove.\n   - Accepts: [`FrameCallbackId`](#framecallbackid).\n   - _optional_\n\n#### ticker.count()\n\nReturns the frame callback count for a phase if _phase_ is provided. Otherwise returns the frame callback count for the whole ticker.\n\n**Syntax**\n\n```\nticker.count( [ phase ] )\n```\n\n**Parameters**\n\n1. **phase**\n   - The phase you want to get the frame callback count for.\n   - Accepts: [`Phase`](#phase).\n   - Optional.\n\n#### ticker.tick()\n\nCollects all the frame callbacks (in the currently active phases) into a queue and calls the frame callbacks with the arguments you provide to this method.\n\n**Syntax**\n\n```\nticker.tick( [ ...args ] )\n```\n\n**Parameters**\n\n1. **...args**\n   - Any arguments you see fit. Just remember to provide your custom `FrameCallback` type to `Ticker` when using TypeScript, as demonstrated in the example below.\n   - Accepts: `any`.\n   - Optional.\n\n### AutoTicker\n\n`AutoTicker` class extends `Ticker` class and (as the name says) ticks automatically so you don't have to manually call the `tick` method in your own loop. It defaults to `requestAnimationFrame` and falls back to `setTimeout` in environments where `requestAnimationFrame` is not supported.\n\n`AutoTicker` has all the same methods and options as `Ticker`, and a few extra options/properties to control the auto-ticking. Please refer to the [Ticker](#ticker)'s API for anything that's not explicitly documented here. We only document the differences and additions here.\n\nAccepts an [`AutoTickerOptions`](#autotickeroptions) object as it's only argument.\n\n**Syntax**\n\n```\nconst ticker = new AutoTicker( [ options ] );\n```\n\n**Options**\n\n- **paused**\n  - See [paused](#autotickerpaused) docs.\n  - Accepts: `boolean`.\n  - Optional. Defaults to `false`.\n- **onDemand**\n  - See [onDemand](#autotickerondemand) docs.\n  - Accepts: `boolean`.\n  - Optional. Defaults to `false`.\n- **requestFrame**\n  - See [requestFrame](#autotickerrequestframe) docs.\n  - Accepts: [`FrameCallback`](#framecallback).\n  - Optional. Defaults to `createRequestFrame()`, which uses `requestAnimationFrame` (if available) and falls back to `setTimeout`.\n\n#### autoticker.paused\n\nType: `boolean`.\n\nDefines if the ticker is paused. If `true` the ticker won't tick automatically until unpaused. You can change this property dynamically at any time to pause/unpause the ticker.\n\n#### autoticker.onDemand\n\nType: `boolean`.\n\nDefines if the ticker should tick only when there are frame callbacks in the ticker. If `true` the ticker will tick only when there are frame callbacks in it. If `false` the ticker will tick continuously. You can change this property dynamically at any time to switch between on-demand and continuous ticking.\n\n#### autoticker.requestFrame\n\nType: [`RequestFrame`](#requestframe).\n\nDefines the method which is used to request the next frame. You can change this property dynamically at any time to switch the frame request method.\n\n### Types\n\nHere's a list of all the types that you can import from `tikki`.\n\n```ts\nimport {\n  Phase,\n  FrameCallback,\n  FrameCallbackId,\n  TickerDedupe,\n  TickerOptions,\n  AutoTickerOptions,\n  RequestFrame,\n  CancelFrame,\n} from 'tikki';\n```\n\n#### Phase\n\n```ts\ntype Phase = string | number | symbol;\n```\n\n#### FrameCallback\n\n```ts\ntype FrameCallback = (time: number, ...args: any) =\u003e void;\n```\n\n#### FrameCallbackId\n\n```ts\ntype FrameCallbackId = null | string | number | symbol | bigint | Function | Object;\n```\n\n#### TickerDedupe\n\n```ts\ntype TickerDedupe = 'add' | 'update' | 'ignore' | 'throw';\n```\n\n#### TickerOptions\n\n```ts\ninterface TickerOptions\u003cP extends Phase\u003e {\n  phases?: P[];\n  dedupe?: TickerDedupe;\n  getId?: (frameCallback: FrameCallback) =\u003e FrameCallbackId;\n}\n```\n\n#### AutoTickerOptions\n\n```ts\ninterface AutoTickerOptions\u003cP extends Phase, FC extends FrameCallback\u003e extends TickerOptions\u003cP\u003e {\n  paused?: boolean;\n  onDemand?: boolean;\n  requestFrame?: RequestFrame\u003cFC\u003e;\n}\n```\n\n#### RequestFrame\n\n```ts\ntype RequestFrame\u003cFC extends FrameCallback = (time: number) =\u003e void\u003e = (\n  callback: FC,\n) =\u003e CancelFrame;\n```\n\n#### CancelFrame\n\n```ts\ntype CancelFrame = () =\u003e void;\n```\n\n## License\n\nCopyright © 2022-2025, Niklas Rämö (inramo@gmail.com). Licensed under the MIT license.\n","funding_links":["https://github.com/sponsors/niklasramo"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasramo%2Ftikki","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fniklasramo%2Ftikki","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fniklasramo%2Ftikki/lists"}