@fabi-play/engine
A small 2D engine for Fabi Play games: a 720-unit stage, sprites, shapes, text, particles, tweens, sound, the players' hands and avatars, on WebGL (1 or 2) or Canvas 2D.
npm
npm install @fabi-play/sdk @fabi-play/engine
import { connect } from '@fabi-play/sdk'
import { createEngine } from '@fabi-play/engine'
const play = await connect()
const engine = await createEngine({ play })
engine.stage.add(engine.avatars())
engine.start()
play.ready()
@fabi-play/sdk is a peer dependency (the engine draws the avatars with the SDK's copy). ESM only, types included.
Bundlers that resolve the development export condition (Vite and webpack in development) get
dist/index.development.js: readable, with development checks (a warning when a node is used after destroy() or
after a node pool's release()). Production builds get dist/index.js, without them. Bundling the engine's source instead (inside
the workspace), the checks are off unless the bundler defines __DEV__ as true.
Script tag
Load the SDK first, then the engine, each pinned with its SRI hash
(https://cdn.fabiplay.app/engine/<version>/integrity.json):
<script src="https://cdn.fabiplay.app/sdk/0.1.0/fabi-play.umd.min.js" integrity="sha384-…" crossorigin="anonymous"></script>
<script src="https://cdn.fabiplay.app/engine/0.1.0/fabi-play-engine.lite.umd.min.js" integrity="sha384-…" crossorigin="anonymous"></script>
<script>
FabiEngine.createEngine({ play: FabiPlay }).then((engine) => engine.start())
</script>
| File | What |
|---|---|
fabi-play-engine.lite.umd.min.js | recommended: takes the avatars and smoothing from the SDK's script (global FabiEngine) |
fabi-play-engine.umd.min.js | everything inside |
*.umd.js | the same, readable, with the development checks (warnings for nodes used after destroy() or a pool's release()) |
fabi-play-engine[.lite].js | the classic scripts (IIFE): set window.FabiEngine |
fabi-play-engine[.lite].d.ts | types for script-tag games (FabiEngine.Sprite, …) |
Docs: https://docs.fabiplay.app/engine/overview/.
Hands, orientation, safe area
engine.hands() (stage units), engine.touched(node) and the rings of engine.handMarkers() are where the hands
aim, with the protocol's aimPoint: on a TV or a tablet (landscape) on the avatar's hand, or on the real one over
the video, as before; on a phone held upright (engine.orientation === 'portrait'), where the camera image is small
at the bottom, the arms' reach around the shoulders stretched to the whole stage, so arms straight up touch near the
top (y ≈ 0.15 × 720) and arms out reach the sides, held for 300 ms when the camera loses a wrist. The avatars are still
drawn through the view (engine.view), small at the bottom. Games need no change; to draw on the body itself, use the
SDK's toScreen(wrist).
engine.safeArea is what a phone's notch and home bar cover, in fractions of the viewport ({ top, bottom, left, right }, each 0–0.25, zeros on a TV): keep the HUD and targets out of it, e.g. a score at
y = engine.safeArea.top * 720 + 24.
Filters
Any node can go through filters: colour filters, blur and drop shadows draw the same on WebGL and Canvas 2D; custom shaders are WebGL only. Each factory is its own export, so a game bundles only the ones it imports.
import { blur, dropShadow, shader, tint } from '@fabi-play/engine'
card.filters = [dropShadow(0, 8, 6, 'rgba(0,0,0,0.35)')]
const flash = tint('#fff', 0)
enemy.filters = [flash]
engine.tween(flash, { amount: 1 }, 0.05).then(() => engine.tween(flash, { amount: 0 }, 0.2))
if (engine.supports.shaders) flag.filters = [shader({ fragment: '…', uniforms: { uAmp: 0.004 } })]
tint, brightness, contrast, saturate, grayscale, sepia, hueRotate, invert, colorMatrix, blur,
dropShadow, shader. Blur radii are capped per tier (32 / 16 / 0 units: off on the low tier);
engine.supports says what draws here. Guide: https://docs.fabiplay.app/engine/filters/.
Node pools
For games that spawn many nodes a second: nodePool(factory, { max, reset }) gives nodes back reset to how the
factory made them, instead of destroying and making them again. An import of its own (FabiEngine.nodePool on
script tags).
import { nodePool } from '@fabi-play/engine'
const coins = nodePool(() => engine.shape({ type: 'circle', r: 16, fill: '#FBD46A' }), { max: 40 })
const coin = layer.add(coins.get({ x: 200, y: -20 }))
engine.tween(coin, { y: 740 }, 2, 'linear').then(() => coins.release(coin))
Size
pnpm size checks every build against size-budget.json (gzip -9), and what a game importing only createEngine
bundles from the npm build. The first release (0.1.0) carries filters, node pools, the WebGL state cache, structural
cache keys and an allocation-free hot path: the npm index.js went from 30.3 to 40.1 KB gzip during its
development, the script tags by the same ~10 KB (they carry everything). A bundled game pays only for what it
imports: with createEngine alone, 32.9 KB (29.1 before). The filter runtime (the GL passes and programs, the
Canvas 2D plans) comes in with the first filter factory a game imports: one factory adds 3.6 KB, all twelve 5.2 KB.
nodePool is an import of its own: 0.7 KB. The 3.8 KB every game pays (the WebGL state cache, layer bounds,
structural cache keys, the allocation-free hot path, the filter hook and engine.supports) are explained in the
budget file's notes.
License
Fabi Play Kit License: free to use, for making games and tools for Fabi Play only. See LICENSE.