hopkit

Find your next move

Search the guide, API, and recipes.

    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
    }