MIT License

项目介绍

verekia/manapotion 是一个托管在 GitHub 上的开源项目,主要使用 TypeScript 编写,采用 MIT License 许可证,目前获得 256 个 Star、4 次 Fork。

仓库全名
verekia/manapotion
开源许可证
MIT License
Star 数
256
Fork 数
4
未关闭 Issue
18

README 摘要

Mana Potion 🧪 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, TresJS, Threlte, and vanilla…

完整内容见 GitHub 原始 README 或本页 。

常见问题

manapotion 用什么语言开发?

主要使用 TypeScript 编写,采用 MIT License 许可证,目前有 256 个 Star。

manapotion 可以免费商用吗?

仓库采用 MIT License 许可证,可免费使用与修改,商业使用请遵守许可证条款(例如保留版权声明与许可声明)。

如何获取 manapotion 的源码?

可以直接克隆仓库:git clone https://github.com/verekia/manapotion.git,默认分支为 main;安装与运行步骤见仓库 README。

manapotion 最近还在维护吗?

仓库最近一次更新为 2025年11月3日,当前有 18 个未关闭 Issue;Star 数为 256,可作为社区活跃度的参考。

关于

GitHub 原始描述(英文)

🧪 Mana Potion – Toolkit for JavaScript game development and interactive experiences (React, Vue, Svelte, vanilla)

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
266
价值分
154
活跃状态
活跃

语言占比

CSS
Dockerfile
HTML
JavaScript
Shell
Svelte
TypeScript

贡献者

README

Mana Potion

Mana Potion

🧪 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, TresJS, Threlte, and vanilla Three.js, but it can be used in any context.

The library consists of:

Important: Until we hit 1.0.0, expect breaking changes in minor versions.

Demos

Check out the React, Vue, Svelte, and vanilla JS 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 <Listeners /> somewhere in your app:

React, Vue, Svelte

import { Listeners } from '@manapotion/react' // or vue, svelte

const App = () => (
  <>
    <div>Your game</div>
    <Listeners />
  </>
)

Vanilla

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, <MouseMoveListener /> or <FullscreenListener />).

🗿 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:

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:

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

<script setup lang="ts">
import { mouse, browser, keyboard } from '@manapotion/vue'
</script>

<template>
  <div>{{ mouse.buttons.right }}</div>
  <div>{{ browser.isFullscreen }}</div>
  <div>{{ keyboard.codes.KeyW }}</div>
</template>

Svelte

<script lang="ts">
  import { mouse, browser, keyboard } from '@manapotion/svelte'
</script>

  <div>{$mouse.buttons.right}</div>
  <div>{$browser.isFullscreen}</div>
  <div>{$keyboard.codes.KeyW}</div>

Vanilla

There is no reactivity system in vanilla JavaScript, so you can use callbacks to update your app state when the store changes. You can also subscribe to the Zustand store directly to watch for changes:

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 (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):

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 <Listeners />:

const App = () => {
  const handleKeyDown = e => {
    if (e.code === 'Space') {
      jump()
    }
  }

  return (
    <>
      <div>Your game</div>
      <Listeners />
    </>
  )
}

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:

import { Listeners, resetKeyboard, resetMouse } from '@manapotion/react'

const App = () => (
  <Listeners
    => {
      resetKeyboard()
      resetMouse()
    }}
    => {
      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 <Listeners /> or to individual listeners:

React

<Listeners />
/* or */
<FullscreenListener />

Vue

<Listeners @fullscreenChange="handleFullscreenChange" />
<!-- or -->
<FullscreenListener @fullscreen-change="handleFullscreenChange" />

Svelte

<Listeners on:fullscreenChange={handleFullscreenChange} />
<!-- or -->
<FullscreenListener on:fullscreenChange={handleFullscreenChange} />

Vanilla

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.

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

import { useRef } from 'react'
import { useMainLoop } from '@manapotion/react'

import player from './player'

const Player = () => {
  const ref = useRef<HTMLDivElement>(null)

  useMainLoop(({ delta, elapsed }) => {
    ref.current!.style.transform = `translate(${player.x}px, ${player.y}px)`
  })

  return <div ref={ref}>Player</div>
}

Vue

<script setup lang="ts">
import { ref } from 'vue'
import { useMainLoop } from '@manapotion/vue'

import player from './player'

const playerRef = ref<HTMLDivElement | null>(null)

useMainLoop(({ delta, elapsed }) => {
  playerRef.value!.style.transform = `translate(${player.x}px, ${player.y}px)`
})
</script>

<template>
  <div ref="playerRef">Player</div>
</template>

Svelte

<script lang="ts">
  import { useMainLoop } from '@manapotion/svelte'

  import player from './player'

  let playerEl: HTMLDivElement

  useMainLoop(({ delta, elapsed }) => {
    playerEl.style.transform = `translate(${player.x}px, ${player.y}px)`
  })
</script>

<div bind:this={playerEl}>Player</div>

Vanilla

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:

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):

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:

<Listeners
  isPageVisible }) => {
    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 <Canvas> and adding the following component as its child:

const SyncMainLoop = () => {
  const advance = useThree(s => s.advance)

  useMainLoop(
    ({ time }) => advance(time / 1000),
    { stage: STAGE_RENDER } // Or whatever stage you want
  )

  return null
}

// ...

const App = () => (
  <Canvas frameloop="never">
    <SyncMainLoop />
    {/* Your scene */}
  </Canvas>
)

Similarly, if you are using Threlte, set renderMode to manual on your Canvas and call useThrelte().advance() in a useMainLoop.

TresJS will support conditional rendering in v4.

Virtual joysticks

Mana Potion includes 🗿 non-reactive and headless virtual joysticks for mobile controls. Each virtual joystick is associated with a single <JoystickArea />. 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.

You 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.

import { JoystickArea, getJoysticks } from '@manapotion/react'

const MobileUI = () => (
  <JoystickArea
    joystick={getJoysticks().movement}
    mode="follow" // Default
    maxFollowDistance={50} // Default
   
   
   
  />
)

With vanilla JS, use mountJoystickArea instead.

In follow mode, the joystick will follow the user’s finger, which is good for player movement.

Here are the properties that will be updated on your joystick object:

  • 🗿 joystick.isActive
  • 🗿 joystick.identifier
  • 🗿 joystick.origin.x / joystick.origin.y
  • 🗿 joystick.origin.angle
  • 🗿 joystick.origin.distance
  • 🗿 joystick.origin.distanceRatio
  • 🗿 joystick.follow.x / joystick.follow.y
  • 🗿 joystick.follow.angle
  • 🗿 joystick.follow.distance
  • 🗿 joystick.follow.distanceRatio
  • 🗿 joystick.current.x/ joystick.current.y
  • 🗿 joystick.movement.x / joystick.movement.y

See the example of how to style your joystick.

Multitouch within a single area is not supported, but you can create multiple <JoystickArea />. One for movement and one for camera rotation for example.

Browser API Helpers

Mana Potion provides helper functions to reduce some browser APIs boilerplate:

  • enterFullscreen
  • exitFullscreen
  • lockOrientation
  • unlockOrientation
  • lockPointer
  • unlockPointer
  • lockKeys
  • unlockKeys

For 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:

import {
  enterFullscreen,
  exitFullscreen,
  lockOrientation,
  unlockOrientation,
  lockKeys,
  unlockKeys,
  useIsFullscreen,
} from '@manapotion/react'

const FullscreenButton = () => {
  const isFullscreen = useIsFullscreen()

  return (
    <button
      => {
        if (isFullscreen) {
          exitFullscreen()
          unlockKeys()
          unlockOrientation()
        } else {
          enterFullscreen()
          lockOrientation('landscape')
          lockKeys(['Escape', 'KeyW', 'KeyA', 'KeyS', 'KeyD'])
        }
      }}
    >
      Toggle fullscreen
    </button>
  )
}

Note: Locking keys is a Chrome experimental feature 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.

Tailwind

Mana Potion provides a Tailwind theme containing the following screens breakpoints:

  • 5xs: 192px
  • 4xs: 256px
  • 3xs: 320px
  • 2xs: 384px
  • xs: 512px
  • sm: 640px - Tailwind default
  • md: 768px - Tailwind default
  • lg: 1024px - Tailwind default
  • xl: 1280px - Tailwind default
  • xxl: 1536px - Tailwind default
  • 3xl: 1792px
  • 4xl: 2048px
  • 5xl: 2560px

Making 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:

  • xs-h:bg-red-500: Only for screens taller than 512px.
  • sm:xs-h:bg-red-500: Only for screens wider than 640px and taller than 512px.
  • sm:max-md:xs-h:max-sm-h:bg-red-500: Only between 640px to 768px wide and 512px to 640px high.

There is currently a bug in Tailwind 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.

There are also desktop and mobile media queries that you can use to target mobile and desktop devices:

  • desktop:bg-red-500: Only for desktop devices.
  • mobile:bg-red-500: Only for mobile devices (includes tablets).

To add the theme to your Tailwind config:

/** @type {import('tailwindcss').Config} */
import { tailwindTheme as manapotionTheme } from '@manapotion/react' // or vue, svelte, vanilla

export default {
  content: ['./index.html', './src/**/*.tsx'],
  theme: {
    screens: manapotionTheme.screens,
    extend: {
      screens: manapotionTheme.extend.screens,
    },
  },
}

General tips

  • 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).
  • On mobile, clamp your frame rate to 60 FPS. It will prevent high-framerate devices from overheating and saves battery life.
  • If you use Three.js, some math utilites such as clamp, lerp, and smoothstep are included in Three.js

Community

Join the Mana Potion Discord server.

Contributing

See the contributing guide.

License

MIT

Author

Created by @verekia for 🔮 MiniMana.io

Visit 🌐 WebGameDev.com and join the Web Game Dev community.

评论

暂无评论