🧪 **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.
Mana 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.
The library consists of:
- [**Listeners and a reactive store for inputs and browser state**](#getting-started)
- [**A main loop**](#main-loop)
- [**Headless virtual joysticks**](#virtual-joysticks)
- [**Browser API helpers**](#browser-api-helpers)
- [**Tailwind media queries**](#tailwind)
**Important**: Until we hit 1.0.0, expect breaking changes in minor versions.
## Demos
Check 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.
## Installation
- If you use **React**, install `@manapotion/react`
- If you use **Vue**, install `@manapotion/vue`
- If you use **Svelte**, install `@manapotion/svelte`
- If you don't use these frameworks, install `@manapotion/vanilla`
## Getting started
Add `` somewhere in your app:
**React, Vue, Svelte**
```jsx
import { Listeners } from '@manapotion/react' // or vue, svelte
const App = () => (
<>
Your game
>
)
```
**Vanilla**
```js
import { listeners } from '@manapotion/vanilla'
const unsub = listeners({})
// call unsub() to stop listening
```
This 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, `` or ``).
🗿 **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`:
```jsx
import { getMouse, getKeyboard, getBrowser } from '@manapotion/react' // or vue, svelte, vanilla
const animate = () => {
const { right } = getMouse().buttons
const { KeyW } = getKeyboard().codes
const { isFullscreen } = getBrowser()
// ...
}
```
⚡️ **Reactive** variables can be accessed imperatively too, but also reactively in components to trigger re-renders:
**React**
Use the `useMouse`, `useKeyboard`, and `useBrowser` hooks with a selector to access variables reactively:
```jsx
import { useMouse, useBrowser, useKeyboard } from '@manapotion/react'
const Component = () => {
const isRightButtonDown = useMouse(s => s.buttons.right)
const { KeyW } = useKeyboard(s => s.codes)
const isFullscreen = useBrowser(s => s.isFullscreen)
// Some reactive component
return ( /* ... */ )
}
```
**Vue**
```vue
{{ mouse.buttons.right }}
{{ browser.isFullscreen }}
{{ keyboard.codes.KeyW }}
```
**Svelte**
```svelte
{$mouse.buttons.right}
{$browser.isFullscreen}
{$keyboard.codes.KeyW}
```
**Vanilla**
There 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:
```js
import { mouseStore } from '@manapotion/vanilla'
const unsub = mouseStore.subscribe(state => {
console.log(state.buttons.right)
})
```
Here are the variables available:
Legend: ⚡️ **Reactive**, 🗿 **Non-reactive**, 🚧 **Not implemented yet**
### 🌐 Browser
- ⚡️ `browser.isFullscreen`
- ⚡️ `browser.isPageVisible`
- ⚡️ `browser.isPageFocused`
- ⚡️ `browser.isDesktop` / `browser.isMobile`
- ⚡️ `browser.isLandscape` / `browser.isPortrait`
- 🗿 `browser.width`
- 🗿 `browser.height`
- 🚧 `pointerLockSupported`
### 🖱️ Mouse
- ⚡️ `mouse.buttons.left`
- ⚡️ `mouse.buttons.middle`
- ⚡️ `mouse.buttons.right`
- ⚡️ `mouse.locked`
- 🗿 `mouse.position.x`
- 🗿 `mouse.position.y` (the bottom of the screen is 0)
- 🗿 `mouse.movement.x` (reset after `mouseMovementResetDelay`)
- 🗿 `mouse.movement.y` (going up is positive)
- 🗿 `mouse.wheel.y` (delta, reset after `mouseScrollResetDelay`)
You can import and use `resetMouse` to reinitialize the mouse data.
### ⌨️ Keyboard
- ⚡️ `keyboard.codes`
- ⚡️ `keyboard.keys`
- ⚡️ `keyboard.ctrl`
- ⚡️ `keyboard.shift`
- ⚡️ `keyboard.alt`
- ⚡️ `keyboard.meta`
⚡️ `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?).
Here is how you would handle going forward when the user presses W (or Z on French keyboards):
```js
const animate = () => {
const { KeyW } = getKeyboard().codes
if (KeyW) {
// Go forward
}
}
```
For keyboard events, just like all other events, you can add a custom callback to ``:
```jsx
const App = () => {
const handleKeyDown = e => {
if (e.code === 'Space') {
jump()
}
}
return (
<>
Your game
>
)
}
```
You can import and use `resetKeyboard` to reinitialize the keyboard data.
This is useful to prevent keys from staying pressed when switching between tabs or when the game loses focus:
```jsx
import { Listeners, resetKeyboard, resetMouse } from '@manapotion/react'
const App = () => (
{
resetKeyboard()
resetMouse()
}}
onPageVisibilityChange={() => {
resetKeyboard()
resetMouse()
}}
/>
)
```
If your game requires holding a key to perform some action, this technique can prevent players cheating by holding the key and switching tabs.
### Callbacks
You can provide custom event callbacks to `` or to individual listeners:
**React**
```jsx
/* or */
```
**Vue**
```vue
```
**Svelte**
```svelte
```
**Vanilla**
```js
listeners({ onFullscreenChange: handleFullscreenChange })
// or
mountFullscreenListener({ onFullscreenChange: handleFullscreenChange })
```
Please check the TypeScript types for the available callbacks.
Once 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).
## Main loop
The `useMainLoop` hook can be used to schedule your various systems in a single `requestAnimationFrame` call that you can configure per component:
**React**
```jsx
import { useRef } from 'react'
import { useMainLoop } from '@manapotion/react'
import player from './player'
const Player = () => {
const ref = useRef(null)
useMainLoop(({ delta, elapsed }) => {
ref.current!.style.transform = `translate(${player.x}px, ${player.y}px)`
})
return
Player
}
```
**Vue**
```vue
Player
```
**Svelte**
```svelte
Player
```
**Vanilla**
```ts
import { addMainLoopEffect } from '@manapotion/vanilla'
const unsub = addMainLoopEffect(({ delta, elapsed }) => {
// Your animation loop
})
// call unsub() to stop the animation loop
```
### Throttling
You can throttle some callbacks by passing a `throttle` option to `useMainLoop`/`addMainLoopEffect`:
```jsx
useMainLoop(
({ delta, elapsed }) => {
// Your animation loop
},
{ throttle: 100 } // ms
)
```
### Stages
Organize your main loop into stages to run your systems in a specific order (using arbitrary numbers):
```jsx
export const STAGE_CONTROLS = -5
export const STAGE_PHYSICS = -4
export const STAGE_LOGIC = -2
export const STAGE_RENDER = 0 // Default stage
export const STAGE_UI = 5
export const STAGE_CLEANUP = 10
const HealthBar = () => {
useMainLoop(
() => {
// Adjust health bar width
},
{ stage: STAGE_UI, throttle: 100 }
)
}
const Physics = () => {
useMainLoop(
() => {
// Update physics
},
{ stage: STAGE_PHYSICS }
)
}
```
You can pause and resume the main loop with `pauseMainLoop` and `resumeMainLoop`:
```jsx
{
isPageVisible ? resumeMainLoop() : pauseMainLoop()
}}
/>
```
If 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 `