Hopkit / Reference
Game API reference
Game creation, canvas UI, scenes, input, assets and time from the built TypeScript API.
Start with the playable guide. These signatures come from the
emitted @hopkit/game declarations and regenerate with the site build.
The framework workshop demonstrates runtime ownership, controls,
collision, animation and saves. Working with game systems explains
their integration and links to the editable source contracts.
A character and movement
import { createGame } from "@hopkit/game"
const game = await createGame({ assets: { hero: { image: "/hero.png" } } })
const hero = game.sprite("hero", { x: 320, y: 180, anchor: "center" })
game.update((dt) => {
hero.x += game.input.direction().x * 200 * dt
})
The canvas is created in the document body unless you supply canvas or host.
The default logical size is 640 × 360. WASD and arrows work without declaring
input actions. direction() returns a normalized vector; use its y component
for vertical movement. Custom games can declare their own actions and bindings.
All speeds use logical units per second; durations use seconds and angles radians.
Choose the owner of a position
x, y and position place a sprite once. You can then move the returned Pixi
sprite directly, as in the small example. For a separate game model, use
scene.sprite("hero", { bind: player }). The binding reads the model every
render. bind: () => state.player also follows replacement model objects.
Do not write both the model and sprite transform when using a binding.
scene.add(object) owns a custom Pixi object in world space. scene.root and
scene.hud are ordinary containers for world and screen coordinates. scene.pixi
exposes the application. You can compose nested containers, custom rendering and
editable controllers without a required component system or physics engine.
Scenes and restart
The direct game.sprite, game.update, game.input and UI methods use a default
scene. To reset an imperative example, reset its model or sprite yourself. For
reusable scenes, register game.scene(scene => { ... }); game.restart() reruns
that factory and disposes the previous scene’s work. Restarting an imperative
scene without a factory reports an error instead of pretending to replay code.
game.current exposes the active scene. Scene factories are synchronous. Declared
images and sounds load before createGame resolves; later image loads use
scene.assets.load with lifetime cancellation. Creation errors release partial
resources. signal cancels creation and later disposes the game on abort.
game.pause() freezes simulation and resume() continues it. Canvas buttons
still work while paused, so Resume and Restart can live in the rendered menu.
game.resize(width, height) changes logical dimensions without replacing the GPU
renderer. dispose() releases the game and removes an automatically owned canvas.
The hopkit:created canvas event bubbles with the public game as its detail for
hosts that need to observe creation and attach diagnostics.
Canvas UI
text, rect and button draw into the camera-free screen layer by default.
Supply a Pixi parent to compose your own layout or world-space UI. Buttons expose
a container and label, and support pointer input, keyboard focus and hold/release
callbacks. They belong to the scene and are cleaned up on replacement. Scores,
menus and touch controls belong in the renderer; teaching prose and website
navigation can surround the canvas.
Assets, sound and collections
Image and sound keys are inferred from their declarations. Define
sounds: { jump: "/jump.ogg" } and play with scene.sound("jump"). Declared sounds
are decoded during creation. The browser’s audio activation requirement still
applies. scene.loadSound(url) is the explicit later-loading path; scene.play
accepts an already decoded buffer. Sound lifetime follows its scene.
scene.collection(readItems, { key, create, update }) reconciles views by stable
keys. Removed items destroy their views; replacement models retain the existing
view for their key. Duplicate keys report an error. Gameplay rules and entity
state stay in your own source.
Time, camera and diagnostics
The default clock runs at 60 fixed steps per second with at most eight catch-up steps. It commits input once per step. Fixed stepping does not promise whole-game determinism or interpolate sprite positions automatically.
wait, every, update, render and tween return cancellation functions owned
by the scene. Demand mode sleeps without pending work; timers and tweens keep it
running until they finish. Hiding the page or a supported embedded game suspends
simulation and sound.
Camera follow accepts world bounds and a smoothing speed in inverse seconds;
zero follows immediately. scene.pointer.world converts through the displayed
camera, while screen uses logical viewport coordinates. The canvas inspector
and game.inspect() show inputs, camera, resources and values registered through
scene.inspect. These are diagnostics, not a performance profiler.
FrameAnimation, AnimationController, SnapshotCodec, SaveStore, SaveSlots, useful geometry types and Pixi’s
main display classes are also exported from @hopkit/game. Advanced hosts can
still compose the headless core, browser adapters and renderer independently.
createGame
export declare function createGame<
const A extends AssetDefinitions = {},
const I extends ActionDefinitions = DefaultActions,
const S extends SoundDefinitions = {},
>(options?: GameOptions<A, I, S>): Promise<Game<A, I, S>>
GameOptions
export interface GameOptions<
A extends AssetDefinitions,
I extends ActionDefinitions,
S extends SoundDefinitions = SoundDefinitions,
> {
canvas?: HTMLCanvasElement
host?: HTMLElement
size?: {
width: number
height: number
}
rendering?: "smooth" | "pixel-art"
background?: string | number
assets?: A
sounds?: S
actions?: I
pointerButtons?: Readonly<Record<number, keyof I & string>>
gamepad?: readonly (GamepadBinding & {
action: keyof I & string
})[]
gamepadPolling?: "on-input" | "continuous"
mode?: RuntimeMode
stepSeconds?: number
maxSteps?: number
signal?: AbortSignal
onProgress?: (progress: number) => void
debug?: boolean
}
Game
export interface Game<
A extends AssetDefinitions = AssetDefinitions,
I extends ActionDefinitions = ActionDefinitions,
S extends SoundDefinitions = SoundDefinitions,
> {
readonly canvas: HTMLCanvasElement
readonly pixi: Application
readonly paused: boolean
readonly disposed: boolean
readonly audio: AudioBus
readonly current: GameScene<A, I, S> | undefined
readonly reducedMotion: boolean
readonly pointer: GamePointer
render: GameScene<A, I, S>["render"]
tween: GameScene<A, I, S>["tween"]
wait: GameScene<A, I, S>["wait"]
every: GameScene<A, I, S>["every"]
defer: GameScene<A, I, S>["defer"]
readonly size: Readonly<{
width: number
height: number
}>
text: SceneUI["text"]
rect: SceneUI["rect"]
button: SceneUI["button"]
resize(width: number, height: number): void
readonly input: GameInput<I>
sprite(key: keyof A & string, options?: SpriteOptions): Sprite
update(callback: (seconds: number) => void): Disposer
sound(key: keyof S & string, options?: PlayOptions): AudioVoice | undefined
scene(factory: (scene: GameScene<A, I, S>) => void): void
pause(): void
resume(): void
restart(): void
dispose(): void
invalidate(): void
setMode(mode: RuntimeMode): void
inspect(): GameInspector
}
GameScene
export interface GameScene<
A extends AssetDefinitions = AssetDefinitions,
I extends ActionDefinitions = ActionDefinitions,
S extends SoundDefinitions = SoundDefinitions,
> {
readonly ui: SceneUI
text: SceneUI["text"]
rect: SceneUI["rect"]
button: SceneUI["button"]
readonly size: Readonly<{
width: number
height: number
}>
readonly root: Container
readonly hud: Container
readonly pixi: Application
readonly camera: SceneCamera
readonly input: GameInput<I>
readonly pointer: GamePointer
readonly assets: SceneAssets<A>
readonly audio: AudioBus
readonly reducedMotion: boolean
readonly disposed: boolean
sprite(key: keyof A & string, options?: SpriteOptions): Sprite
add<T extends Container>(object: T): T
update(callback: (seconds: number) => void): Disposer
render(callback: (alpha: number) => void): Disposer
wait(seconds: number, callback: () => void): Disposer
every(seconds: number, callback: () => void): Disposer
tween<T extends object>(
target: T,
to: TweenTarget<T>,
seconds: number,
options?: TweenOptions,
): Disposer
defer(cleanup: Disposer): Disposer
invalidate(): void
inspect(label: string, read: () => InspectedState): Disposer
play(buffer: AudioBuffer, options?: PlayOptions): AudioVoice | undefined
sound(key: keyof S & string, options?: PlayOptions): AudioVoice | undefined
loadSound(url: string, options?: PlayOptions): Promise<AudioVoice | undefined>
collection<Item, Key, View extends Container>(
read: () => Iterable<Item>,
options: Omit<CollectionOptions<Item, Key, View>, "destroy">,
): KeyedCollection<Item, Key, View>
}
SpriteOptions
export interface SpriteOptions {
position?: Vec2
x?: number
y?: number
bind?: Vec2 | (() => Vec2)
anchor?: "center" | "top-left" | Vec2
width?: number
height?: number
frame?: number
rotation?: number
scale?: number
}
GameInspector
export interface GameInspector {
readonly paused: boolean
readonly mode: RuntimeMode
readonly tracked: Readonly<Record<string, InspectedState>>
readonly sceneResources: {
updates: number
renders: number
work: number
objects: number
}
readonly camera:
| {
x: number
y: number
zoom: number
}
| undefined
readonly actions: Readonly<
Record<
string,
{
value: number
held: boolean
pressed: boolean
}
>
>
}
InspectedState
export interface InspectedState {
position?: Vec2
bounds?: Rect
state?: Readonly<Record<string, string | number | boolean | null>>
}
AssetDefinitions
export type AssetDefinitions = Readonly<Record<string, ImageAsset>>
ImageAsset
export interface ImageAsset {
image: string
atlas?: unknown
frameWidth?: number
frameHeight?: number
spacing?: number
margin?: number
filter?: "linear" | "nearest"
extrude?: number
animations?: Readonly<Record<string, readonly number[]>>
}
LoadedImage
export interface LoadedImage {
readonly namedFrames: Readonly<Record<string, Texture>>
readonly clips: Readonly<Record<string, AnimationClip>>
readonly texture: Texture
readonly frames: readonly Texture[]
readonly animations: Readonly<Record<string, readonly number[]>>
}
SceneAssets
export interface SceneAssets<A extends AssetDefinitions> {
bundle<B extends AssetDefinitions>(
definitions: B,
options?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
},
): AssetBundle<B>
texture(key: keyof A & string): Texture
frames(key: keyof A & string): readonly Texture[]
get(key: keyof A & string): LoadedImage
load(
definition: ImageAsset,
options?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
},
): Promise<LoadedImage>
}
AssetBundle
export interface AssetBundle<B extends AssetDefinitions> {
readonly status: "loading" | "ready" | "error" | "released"
readonly progress: number
readonly error: unknown
readonly ready: Promise<
Readonly<{
[K in keyof B]: LoadedImage
}>
>
retry(): Promise<
Readonly<{
[K in keyof B]: LoadedImage
}>
>
release(): void
}
ActionDefinitions
export type ActionDefinitions = Readonly<Record<string, readonly string[]>>
GameInput
export interface GameInput<A extends ActionDefinitions> {
readonly profile: ControlProfile
rebind(profile: unknown): void
createSource(): {
set(action: keyof A & string, value: number): void
clear(): void
dispose(): void
}
gestures(options: GestureOptions): ReturnType<typeof attachGestures>
virtualStick(
options: Omit<VirtualStickOptions, "actions"> & {
actions: Record<"left" | "right" | "up" | "down", keyof A & string>
},
): ReturnType<typeof attachVirtualStick>
direction(mapping?: {
left: keyof A & string
right: keyof A & string
up: keyof A & string
down: keyof A & string
}): Vec2
value(action: keyof A & string): number
held(action: keyof A & string): boolean
pressed(action: keyof A & string): boolean
released(action: keyof A & string): boolean
axis(negative: keyof A & string, positive: keyof A & string): number
set(action: keyof A & string, value: number): void
onPress(action: keyof A & string, callback: () => void): Disposer
onRelease(action: keyof A & string, callback: () => void): Disposer
}
GamePointer
export interface GamePointer {
readonly screen: Vec2 | undefined
readonly world: Vec2 | undefined
readonly down: boolean
}
SceneCamera
export declare class SceneCamera implements Camera2D {
readonly size: {
width: number
height: number
}
x: number
y: number
rotation: number
zoom: number
constructor(size: { width: number; height: number })
get bounds(): Rect | undefined
resize(width: number, height: number): void
follow(
target: Vec2,
options?: {
bounds?: Rect
speed?: number
},
): void
stop(): void
update(dt: number, snap?: boolean): void
toWorld(point: Vec2): Vec2
toScreen(point: Vec2): Vec2
}
TweenOptions
export interface TweenOptions {
ease?: (progress: number) => number
onComplete?: () => void
}
TweenTarget
export type TweenTarget<T extends object> = {
[K in keyof T as T[K] extends number ? K : never]?: number
}
SoundDefinitions
export type SoundDefinitions = Readonly<Record<string, string>>
CanvasTextOptions
export interface CanvasTextOptions {
x?: number
y?: number
fontSize?: number
fill?: ColorSource
fontFamily?: string
fontWeight?: "normal" | "bold"
align?: "left" | "center" | "right"
anchor?: number
wrapWidth?: number
parent?: Container
}
CanvasRectOptions
export interface CanvasRectOptions {
x?: number
y?: number
width: number
height: number
fill?: ColorSource
radius?: number
alpha?: number
parent?: Container
}
CanvasButtonOptions
export interface CanvasButtonOptions extends CanvasRectOptions {
label: string
fontSize?: number
textColor?: ColorSource
disabled?: boolean
onPress?: () => void
onRelease?: () => void
onClick?: () => void
}
CanvasButton
export interface CanvasButton {
readonly view: Container
readonly text: Text
setLabel(label: string): void
setDisabled(disabled: boolean): void
destroy(): void
}
SceneUI
export interface SceneUI {
text(value: string, options?: CanvasTextOptions): Text
rect(options: CanvasRectOptions): Graphics
button(options: CanvasButtonOptions): CanvasButton
}
CollectionOptions
export interface CollectionOptions<Item, Key, View> {
key(item: Item): Key
create(item: Item): View
update?(view: View, item: Item): void
destroy(view: View): void
}
KeyedCollection
export declare class KeyedCollection<Item, Key, View> {
readonly read: () => Iterable<Item>
readonly options: CollectionOptions<Item, Key, View>
constructor(read: () => Iterable<Item>, options: CollectionOptions<Item, Key, View>)
get views(): ReadonlyMap<Key, View>
sync(): void
dispose(): void
}