{"id":14957053,"url":"https://github.com/verekia/manapotion","last_synced_at":"2025-04-04T12:06:37.245Z","repository":{"id":224773938,"uuid":"764146539","full_name":"verekia/manapotion","owner":"verekia","description":"🧪 Mana Potion – Toolkit for JavaScript game development and interactive experiences (React, Vue, Svelte, vanilla)","archived":false,"fork":false,"pushed_at":"2025-02-28T17:28:23.000Z","size":2431,"stargazers_count":240,"open_issues_count":18,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-28T11:08:18.633Z","etag":null,"topics":["ecs","gamedev","r3f","react","react-js","react-three-fiber","svelte","sveltejs","three-js","threejs","threlte","tresjs","vue","vuejs","web-game-dev"],"latest_commit_sha":null,"homepage":"https://manapotion.v1v2.io","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/verekia.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","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}},"created_at":"2024-02-27T15:04:40.000Z","updated_at":"2025-03-20T06:57:31.000Z","dependencies_parsed_at":"2024-04-11T07:26:21.936Z","dependency_job_id":"9f968dc9-8ac6-41d6-8162-954036bfc49b","html_url":"https://github.com/verekia/manapotion","commit_stats":{"total_commits":354,"total_committers":3,"mean_commits":118.0,"dds":"0.42090395480225984","last_synced_commit":"2b922bd71db10f48f5d5991275c52dc5eb1cd2de"},"previous_names":["verekia/v1v2-gamedev","verekia/manapotion"],"tags_count":100,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verekia%2Fmanapotion","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verekia%2Fmanapotion/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verekia%2Fmanapotion/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verekia%2Fmanapotion/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/verekia","download_url":"https://codeload.github.com/verekia/manapotion/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247174415,"owners_count":20896078,"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":["ecs","gamedev","r3f","react","react-js","react-three-fiber","svelte","sveltejs","three-js","threejs","threlte","tresjs","vue","vuejs","web-game-dev"],"created_at":"2024-09-24T13:14:01.003Z","updated_at":"2025-04-04T12:06:37.215Z","avatar_url":"https://github.com/verekia.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eMana Potion\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"/examples/react/public/mana-potion.webp\" alt=\"Mana Potion\" width=\"162\" height=\"230\" /\u003e\n\u003c/p\u003e\n\n🧪 **Mana Potion** is a toolkit for JavaScript game development and interactive experiences. It is _not_ a game engine or framework but a collection of **low-level utilities and helpers** commonly needed when building games.\n\nMana Potion supports React, Vue, Svelte, and vanilla JavaScript. It is a particularly great fit for people who build games or experiences in [React Three Fiber](https://docs.pmnd.rs/react-three-fiber), [TresJS](https://tresjs.org/), [Threlte](https://threlte.xyz/), and vanilla [Three.js](https://threejs.org/), but it can be used in any context.\n\nThe library consists of:\n\n- [**Listeners and a reactive store for inputs and browser state**](#getting-started)\n- [**A main loop**](#main-loop)\n- [**Headless virtual joysticks**](#virtual-joysticks)\n- [**Browser API helpers**](#browser-api-helpers)\n- [**Tailwind media queries**](#tailwind)\n\u003c!-- - [**Extra hooks for React Three Fiber**](#react-three-fiber) --\u003e\n\n**Important**: Until we hit 1.0.0, expect breaking changes in minor versions.\n\n## Demos\n\nCheck out the [**React**](https://manapotion.v1v2.io/react), [**Vue**](https://manapotion.v1v2.io/vue), [**Svelte**](https://manapotion.v1v2.io/svelte), and [**vanilla JS**](https://manapotion.v1v2.io/vanilla) demos.\n\n## Installation\n\n- If you use **React**, install `@manapotion/react`\n- If you use **Vue**, install `@manapotion/vue`\n- If you use **Svelte**, install `@manapotion/svelte`\n- If you don't use these frameworks, install `@manapotion/vanilla`\n\n## Getting started\n\nAdd `\u003cListeners /\u003e` somewhere in your app:\n\n**React, Vue, Svelte**\n\n```jsx\nimport { Listeners } from '@manapotion/react' // or vue, svelte\n\nconst App = () =\u003e (\n  \u003c\u003e\n    \u003cdiv\u003eYour game\u003c/div\u003e\n    \u003cListeners /\u003e\n  \u003c/\u003e\n)\n```\n\n**Vanilla**\n\n```js\nimport { listeners } from '@manapotion/vanilla'\n\nconst unsub = listeners({})\n\n// call unsub() to stop listening\n```\n\nThis will automatically give you access to some reactive and non-reactive variables. If you do not want to listen to every event supported by the library, you can cherry-pick individual listeners (for example, `\u003cMouseMoveListener /\u003e` or `\u003cFullscreenListener /\u003e`).\n\n🗿 **Non-reactive** variables may be frequently updated and should be accessed imperatively in your main loop or in event handlers via `getMouse`, `getKeyboard`, and `getBrowser`:\n\n```jsx\nimport { getMouse, getKeyboard, getBrowser } from '@manapotion/react' // or vue, svelte, vanilla\n\nconst animate = () =\u003e {\n  const { right } = getMouse().buttons\n  const { KeyW } = getKeyboard().codes\n  const { isFullscreen } = getBrowser()\n  // ...\n}\n```\n\n⚡️ **Reactive** variables can be accessed imperatively too, but also reactively in components to trigger re-renders:\n\n**React**\n\nUse the `useMouse`, `useKeyboard`, and `useBrowser` hooks with a selector to access variables reactively:\n\n```jsx\nimport { useMouse, useBrowser, useKeyboard } from '@manapotion/react'\n\nconst Component = () =\u003e {\n  const isRightButtonDown = useMouse(s =\u003e s.buttons.right)\n  const { KeyW } = useKeyboard(s =\u003e s.codes)\n  const isFullscreen = useBrowser(s =\u003e s.isFullscreen)\n\n  // Some reactive component\n  return ( /* ... */ )\n}\n```\n\n**Vue**\n\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport { mouse, browser, keyboard } from '@manapotion/vue'\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv\u003e{{ mouse.buttons.right }}\u003c/div\u003e\n  \u003cdiv\u003e{{ browser.isFullscreen }}\u003c/div\u003e\n  \u003cdiv\u003e{{ keyboard.codes.KeyW }}\u003c/div\u003e\n\u003c/template\u003e\n```\n\n**Svelte**\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import { mouse, browser, keyboard } from '@manapotion/svelte'\n\u003c/script\u003e\n\n  \u003cdiv\u003e{$mouse.buttons.right}\u003c/div\u003e\n  \u003cdiv\u003e{$browser.isFullscreen}\u003c/div\u003e\n  \u003cdiv\u003e{$keyboard.codes.KeyW}\u003c/div\u003e\n```\n\n**Vanilla**\n\nThere is no reactivity system in vanilla JavaScript, so you can use [callbacks](#callbacks) to update your app state when the store changes. You can also subscribe to the Zustand store directly to watch for changes:\n\n```js\nimport { mouseStore } from '@manapotion/vanilla'\n\nconst unsub = mouseStore.subscribe(state =\u003e {\n  console.log(state.buttons.right)\n})\n```\n\nHere are the variables available:\n\nLegend: ⚡️ **Reactive**, 🗿 **Non-reactive**, 🚧 **Not implemented yet**\n\n### 🌐 Browser\n\n- ⚡️ `browser.isFullscreen`\n- ⚡️ `browser.isPageVisible`\n- ⚡️ `browser.isPageFocused`\n- ⚡️ `browser.isDesktop` / `browser.isMobile`\n- ⚡️ `browser.isLandscape` / `browser.isPortrait`\n- 🗿 `browser.width`\n- 🗿 `browser.height`\n- 🚧 `pointerLockSupported`\n\n### 🖱️ Mouse\n\n- ⚡️ `mouse.buttons.left`\n- ⚡️ `mouse.buttons.middle`\n- ⚡️ `mouse.buttons.right`\n- ⚡️ `mouse.locked`\n- 🗿 `mouse.position.x`\n- 🗿 `mouse.position.y` (the bottom of the screen is 0)\n- 🗿 `mouse.movement.x` (reset after `mouseMovementResetDelay`)\n- 🗿 `mouse.movement.y` (going up is positive)\n- 🗿 `mouse.wheel.y` (delta, reset after `mouseScrollResetDelay`)\n\nYou can import and use `resetMouse` to reinitialize the mouse data.\n\n### ⌨️ Keyboard\n\n- ⚡️ `keyboard.codes`\n- ⚡️ `keyboard.keys`\n- ⚡️ `keyboard.ctrl`\n- ⚡️ `keyboard.shift`\n- ⚡️ `keyboard.alt`\n- ⚡️ `keyboard.meta`\n\n⚡️ `keyboard` contains keys that are available in two versions, `codes` and `keys`. This lets you decide if you want to use the [physical location](https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API#writing_system_keys) (`codes`) of the key or the character being typed as a key (`keys`). Using the physical location is better for game controls such as using WASD to move a character, because it is agnostic to the user's keyboard layout (did you know French keyboards are not QWERTY but AZERTY?).\n\nHere is how you would handle going forward when the user presses W (or Z on French keyboards):\n\n```js\nconst animate = () =\u003e {\n  const { KeyW } = getKeyboard().codes\n\n  if (KeyW) {\n    // Go forward\n  }\n}\n```\n\nFor keyboard events, just like all other events, you can add a custom callback to `\u003cListeners /\u003e`:\n\n```jsx\nconst App = () =\u003e {\n  const handleKeyDown = e =\u003e {\n    if (e.code === 'Space') {\n      jump()\n    }\n  }\n\n  return (\n    \u003c\u003e\n      \u003cdiv\u003eYour game\u003c/div\u003e\n      \u003cListeners onKeyDown={handleKeyDown} /\u003e\n    \u003c/\u003e\n  )\n}\n```\n\nYou can import and use `resetKeyboard` to reinitialize the keyboard data.\n\nThis is useful to prevent keys from staying pressed when switching between tabs or when the game loses focus:\n\n```jsx\nimport { Listeners, resetKeyboard, resetMouse } from '@manapotion/react'\n\nconst App = () =\u003e (\n  \u003cListeners\n    onPageFocusChange={() =\u003e {\n      resetKeyboard()\n      resetMouse()\n    }}\n    onPageVisibilityChange={() =\u003e {\n      resetKeyboard()\n      resetMouse()\n    }}\n  /\u003e\n)\n```\n\nIf your game requires holding a key to perform some action, this technique can prevent players cheating by holding the key and switching tabs.\n\n### Callbacks\n\nYou can provide custom event callbacks to `\u003cListeners /\u003e` or to individual listeners:\n\n**React**\n\n```jsx\n\u003cListeners onFullscreenChange={handleFullscreenChange} /\u003e\n/* or */\n\u003cFullscreenListener onFullscreenChange={handleFullscreenChange} /\u003e\n```\n\n**Vue**\n\n```vue\n\u003cListeners @fullscreenChange=\"handleFullscreenChange\" /\u003e\n\u003c!-- or --\u003e\n\u003cFullscreenListener @fullscreen-change=\"handleFullscreenChange\" /\u003e\n```\n\n**Svelte**\n\n```svelte\n\u003cListeners on:fullscreenChange={handleFullscreenChange} /\u003e\n\u003c!-- or --\u003e\n\u003cFullscreenListener on:fullscreenChange={handleFullscreenChange} /\u003e\n```\n\n**Vanilla**\n\n```js\nlisteners({ onFullscreenChange: handleFullscreenChange })\n// or\nmountFullscreenListener({ onFullscreenChange: handleFullscreenChange })\n```\n\nPlease check the TypeScript types for the available callbacks.\n\nOnce mounted, you cannot modify the callbacks dynamically. If you need to change them, you will need to unmount and remount the component. If you have use cases of callbacks changed dynamically, please let me know on [Discord](https://discord.gg/VXYxGrP8EJ).\n\n## Main loop\n\nThe `useMainLoop` hook can be used to schedule your various systems in a single `requestAnimationFrame` call that you can configure per component:\n\n**React**\n\n```jsx\nimport { useRef } from 'react'\nimport { useMainLoop } from '@manapotion/react'\n\nimport player from './player'\n\nconst Player = () =\u003e {\n  const ref = useRef\u003cHTMLDivElement\u003e(null)\n\n  useMainLoop(({ delta, elapsed }) =\u003e {\n    ref.current!.style.transform = `translate(${player.x}px, ${player.y}px)`\n  })\n\n  return \u003cdiv ref={ref}\u003ePlayer\u003c/div\u003e\n}\n```\n\n**Vue**\n\n```vue\n\u003cscript setup lang=\"ts\"\u003e\nimport { ref } from 'vue'\nimport { useMainLoop } from '@manapotion/vue'\n\nimport player from './player'\n\nconst playerRef = ref\u003cHTMLDivElement | null\u003e(null)\n\nuseMainLoop(({ delta, elapsed }) =\u003e {\n  playerRef.value!.style.transform = `translate(${player.x}px, ${player.y}px)`\n})\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv ref=\"playerRef\"\u003ePlayer\u003c/div\u003e\n\u003c/template\u003e\n```\n\n**Svelte**\n\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import { useMainLoop } from '@manapotion/svelte'\n\n  import player from './player'\n\n  let playerEl: HTMLDivElement\n\n  useMainLoop(({ delta, elapsed }) =\u003e {\n    playerEl.style.transform = `translate(${player.x}px, ${player.y}px)`\n  })\n\u003c/script\u003e\n\n\u003cdiv bind:this={playerEl}\u003ePlayer\u003c/div\u003e\n```\n\n**Vanilla**\n\n```ts\nimport { addMainLoopEffect } from '@manapotion/vanilla'\n\nconst unsub = addMainLoopEffect(({ delta, elapsed }) =\u003e {\n  // Your animation loop\n})\n\n// call unsub() to stop the animation loop\n```\n\n### Throttling\n\nYou can throttle some callbacks by passing a `throttle` option to `useMainLoop`/`addMainLoopEffect`:\n\n```jsx\nuseMainLoop(\n  ({ delta, elapsed }) =\u003e {\n    // Your animation loop\n  },\n  { throttle: 100 } // ms\n)\n```\n\n### Stages\n\nOrganize your main loop into stages to run your systems in a specific order (using arbitrary numbers):\n\n```jsx\nexport const STAGE_CONTROLS = -5\nexport const STAGE_PHYSICS = -4\nexport const STAGE_LOGIC = -2\nexport const STAGE_RENDER = 0 // Default stage\nexport const STAGE_UI = 5\nexport const STAGE_CLEANUP = 10\n\nconst HealthBar = () =\u003e {\n  useMainLoop(\n    () =\u003e {\n      // Adjust health bar width\n    },\n    { stage: STAGE_UI, throttle: 100 }\n  )\n}\n\nconst Physics = () =\u003e {\n  useMainLoop(\n    () =\u003e {\n      // Update physics\n    },\n    { stage: STAGE_PHYSICS }\n  )\n}\n```\n\nYou can pause and resume the main loop with `pauseMainLoop` and `resumeMainLoop`:\n\n```jsx\n\u003cListeners\n  onPageVisibilityChange={({ isPageVisible }) =\u003e {\n    isPageVisible ? resumeMainLoop() : pauseMainLoop()\n  }}\n/\u003e\n```\n\nIf you are using React Three Fiber, you can disable R3F's loop and sync the canvas with Mana Potion's loop by setting `frameloop=\"never\"` on your `\u003cCanvas\u003e` and adding the following component as its child:\n\n```jsx\nconst SyncMainLoop = () =\u003e {\n  const advance = useThree(s =\u003e s.advance)\n\n  useMainLoop(\n    ({ time }) =\u003e advance(time / 1000),\n    { stage: STAGE_RENDER } // Or whatever stage you want\n  )\n\n  return null\n}\n\n// ...\n\nconst App = () =\u003e (\n  \u003cCanvas frameloop=\"never\"\u003e\n    \u003cSyncMainLoop /\u003e\n    {/* Your scene */}\n  \u003c/Canvas\u003e\n)\n```\n\nSimilarly, if you are using Threlte, set `renderMode` to `manual` on your `Canvas` and call `useThrelte().advance()` in a `useMainLoop`.\n\nTresJS will support [conditional rendering](https://github.com/Tresjs/tres/pull/497) in [v4](https://github.com/Tresjs/tres/issues/541).\n\n## Virtual joysticks\n\nMana Potion includes **🗿 non-reactive** and **headless** virtual joysticks for mobile controls. Each virtual joystick is associated with a single `\u003cJoystickArea /\u003e`. You can create your own Joystick objects with `createJoystick()` or use one of the two default ones that are already available on the joysticks store. The default ones are called `movement` and `rotation` joysticks.\n\nYou can choose between 2 modes, `follow` or `origin`, and can adjust the `maxFollowDistance` or `maxOriginDistance`. Use the `onStart`, `onMove`, and `onEnd` callbacks to update your game state and optionally show a joystick on the screen.\n\n```jsx\nimport { JoystickArea, getJoysticks } from '@manapotion/react'\n\nconst MobileUI = () =\u003e (\n  \u003cJoystickArea\n    joystick={getJoysticks().movement}\n    mode=\"follow\" // Default\n    maxFollowDistance={50} // Default\n    onStart={handleStart}\n    onMove={handleMove}\n    onEnd={handleEnd}\n  /\u003e\n)\n```\n\nWith vanilla JS, use `mountJoystickArea` instead.\n\nIn follow mode, the joystick will follow the user's finger, which is good for player movement.\n\nHere are the properties that will be updated on your joystick object:\n\n- 🗿 `joystick.isActive`\n- 🗿 `joystick.identifier`\n- 🗿 `joystick.origin.x` / `joystick.origin.y`\n- 🗿 `joystick.origin.angle`\n- 🗿 `joystick.origin.distance`\n- 🗿 `joystick.origin.distanceRatio`\n- 🗿 `joystick.follow.x` / `joystick.follow.y`\n- 🗿 `joystick.follow.angle`\n- 🗿 `joystick.follow.distance`\n- 🗿 `joystick.follow.distanceRatio`\n- 🗿 `joystick.current.x`/ `joystick.current.y`\n- 🗿 `joystick.movement.x` / `joystick.movement.y`\n\nSee the [example of how to style your joystick](https://github.com/verekia/manapotion/blob/main/examples/react/src/MobileJoystick.tsx).\n\nMultitouch within a single area is not supported, but you can create multiple `\u003cJoystickArea /\u003e`. One for movement and one for camera rotation for example.\n\n## Browser API Helpers\n\nMana Potion provides helper functions to reduce some browser APIs boilerplate:\n\n- `enterFullscreen`\n- `exitFullscreen`\n- `lockOrientation`\n- `unlockOrientation`\n- `lockPointer`\n- `unlockPointer`\n- `lockKeys`\n- `unlockKeys`\n\nFor a fully immersive experience of an FPS game for example, when the player clicks Play or the Fullscreen button, you might want to call multiple helpers in a row like this:\n\n```jsx\nimport {\n  enterFullscreen,\n  exitFullscreen,\n  lockOrientation,\n  unlockOrientation,\n  lockKeys,\n  unlockKeys,\n  useIsFullscreen,\n} from '@manapotion/react'\n\nconst FullscreenButton = () =\u003e {\n  const isFullscreen = useIsFullscreen()\n\n  return (\n    \u003cbutton\n      onClick={() =\u003e {\n        if (isFullscreen) {\n          exitFullscreen()\n          unlockKeys()\n          unlockOrientation()\n        } else {\n          enterFullscreen()\n          lockOrientation('landscape')\n          lockKeys(['Escape', 'KeyW', 'KeyA', 'KeyS', 'KeyD'])\n        }\n      }}\n    \u003e\n      Toggle fullscreen\n    \u003c/button\u003e\n  )\n}\n```\n\n**Note**: Locking keys is a [Chrome experimental feature](https://developer.chrome.com/blog/better-full-screen-mode) to maintain fullscreen when players press Esc (they have to hold it instead). It lets games show in-game dialogs that players can close with Esc without leaving fullscreen.\n\n# Tailwind\n\nMana Potion provides a Tailwind theme containing the following `screens` breakpoints:\n\n- 5xs: 192px\n- 4xs: 256px\n- 3xs: 320px\n- 2xs: 384px\n- xs: 512px\n- sm: 640px - Tailwind default\n- md: 768px - Tailwind default\n- lg: 1024px - Tailwind default\n- xl: 1280px - Tailwind default\n- xxl: 1536px - Tailwind default\n- 3xl: 1792px\n- 4xl: 2048px\n- 5xl: 2560px\n\nMaking games often involves supporting landscape mode on mobile devices, which require height media queries. The same values are used for the height media queries, but with a `*-h` suffix. So you can do:\n\n- `xs-h:bg-red-500`: Only for screens taller than 512px.\n- `sm:xs-h:bg-red-500`: Only for screens wider than 640px and taller than 512px.\n- `sm:max-md:xs-h:max-sm-h:bg-red-500`: Only between 640px to 768px wide and 512px to 640px high.\n\nThere is currently a [bug in Tailwind](https://github.com/tailwindlabs/tailwindcss/issues/13022) preventing `max-*` classes from being generated when using non-pixel values including raw queries, which prevents us from having height media queries. This package contains a fix for this issue.\n\nThere are also `desktop` and `mobile` media queries that you can use to target mobile and desktop devices:\n\n- `desktop:bg-red-500`: Only for desktop devices.\n- `mobile:bg-red-500`: Only for mobile devices (includes tablets).\n\nTo add the theme to your Tailwind config:\n\n```js\n/** @type {import('tailwindcss').Config} */\nimport { tailwindTheme as manapotionTheme } from '@manapotion/react' // or vue, svelte, vanilla\n\nexport default {\n  content: ['./index.html', './src/**/*.tsx'],\n  theme: {\n    screens: manapotionTheme.screens,\n    extend: {\n      screens: manapotionTheme.extend.screens,\n    },\n  },\n}\n```\n\n\u003c!-- ## React Three Fiber\n\n**`@manapotion/r3f`** includes hooks to run logic inside the main R3F `requestAnimationFrame` loop. They are simple hooks around [`addEffect`, `addAfterEffect`, and `addTail`](https://docs.pmnd.rs/react-three-fiber/api/additional-exports). See R3F [loop source](https://github.com/pmndrs/react-three-fiber/blob/master/packages/fiber/src/core/loop.ts).\n\n```jsx\nimport { useFrameEffect, useFrameAfterEffect, useFrameTail } from '@manapotion/r3f'\n\nconst Scene = () =\u003e {\n  useFrameEffect(() =\u003e {\n    // Runs at the beginning of the animation loop\n  })\n\n  useFrameAfterEffect(() =\u003e {\n    // Runs at the end of the animation loop\n  })\n\n  useFrameTail(() =\u003e {\n    // Runs when the animation loop stops\n  })\n\n  // ...\n}\n```\n\nYou can use `useFrameEffect` to animate your UI outside of the `Canvas`, but you might want to throttle it with the `throttle` option:\n\n```jsx\nuseFrameEffect(\n  ({ delta, elapsed }) =\u003e {\n    // Your animation loop\n  },\n  { throttle: 100 } // ms\n)\n``` --\u003e\n\n## General tips\n\n- Clamp your device pixel ratio from 1 to 1.5. The sharpness of a DPR of 2 or more for high-density screens is not worth the performance hit (at least on mobile).\n- On mobile, clamp your frame rate to 60 FPS. It will prevent high-framerate devices from overheating and saves battery life.\n- If you use Three.js, some [math utilites](https://threejs.org/docs/#api/en/math/MathUtils) such as `clamp`, `lerp`, and `smoothstep` are included in Three.js\n\n## Community\n\nJoin the [Mana Potion Discord server](https://discord.gg/VXYxGrP8EJ).\n\n## Contributing\n\nSee the [contributing guide](https://github.com/verekia/manapotion/blob/main/CONTRIBUTING.md).\n\n## License\n\nMIT\n\n## Author\n\nCreated by [@verekia](https://twitter.com/verekia) for 🔮 [MiniMana.io](https://minimana.io/)\n\nVisit 🌐 [WebGameDev.com](https://webgamedev.com/) and [join the Web Game Dev](https://webgamedev.com/discord) community.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fverekia%2Fmanapotion","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fverekia%2Fmanapotion","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fverekia%2Fmanapotion/lists"}