Skip to content

Repository files navigation

TurboWarp-Async-Input

A target-scoped asynchronous keyboard, pointer, and accumulated pose input extension for TurboWarp Temporary Variables.

See the illustrated English user guide or the Japanese user guide for setup, examples, listener ownership, and troubleshooting.

Installation

Build or download dist/async-input.js, then load it as a local custom extension in TurboWarp Desktop with Run extension without sandbox enabled. Load TurboWarp's Temporary Variables extension before registering input. Pose input also requires TurboWarp TM with its accumulated pose scoring and change event features enabled.

The versioned npm package contains the reviewed build:

pnpm add --save-exact @kubohiroya/turbowarp-async-input@0.6.0

Load node_modules/@kubohiroya/turbowarp-async-input/dist/async-input.js, or use the version-pinned CDN URL:

https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-async-input@0.6.0/dist/async-input.js

The distributed build enables key and touch blocks through the compile-time asyncInput feature flag in config/feature-flags.ts. Pose blocks additionally require the independently reversible poseInput feature flag, which remains OFF by default.

Composition API

DSL controllers can import @kubohiroya/turbowarp-async-input/composition without registering the Standalone extension or accessing global Scratch, the DOM, or the TurboWarp VM. The caller injects only the event sources it uses:

import {createAsyncInputComposition} from '@kubohiroya/turbowarp-async-input/composition';

const input = createAsyncInputComposition({
  poseSource: tmComposition,
  keySource,
  actorTouchSource,
  qrSource,
  nfcSource,
});
const selectedPose = await input.waitForPoseCandidate({
  candidates: ['help', 'jump'],
  signal,
});
const selectedKey = await input.waitForKeyCandidate({
  candidates: ['Space', 'ArrowRight'],
  signal,
});
const selectedActor = await input.waitForActorTouchCandidate({
  candidates: ['LeftDoor', 'RightDoor'],
  signal,
});
const selectedQr = await input.waitForQrCandidate({
  candidates: ['qr:next'],
  cameraId: 'qr',
  signal,
});
const selectedNfc = await input.waitForNfcCandidate({
  candidates: ['0123456789ABCDEF'],
  readerId: 'right',
  signal,
});

waitForPoseCandidate does not poll the pose source's current value. It subscribes only to version 1 accumulated-pose changes after the wait starts and resolves with exactly one registered candidate. Before subscribing, it calls poseSource.resetAccumulatedPose() so every accepted wait starts a new selection session with zero accumulated score. Empty poses and non-candidates are ignored. Invalid events reject the wait without selecting a pose.

keySource publishes version 1 key observations. Its code is the exact KeyboardEvent.code; it also reports repeat, IME composition, Shift/Ctrl/Alt/Meta modifier, and interactive-focus state. The composition ignores any observation with one of those exclusion flags, then resolves the first code present in candidates. The source must not prevent browser defaults or stop event propagation.

actorTouchSource publishes version 1 renderer-canvas pointer observations. It resolves the topmost drawable to a DSL actor ID by exact, unique actorName matching and reports primary-button, topmost-pick, and uniqueness flags. The composition ignores an observation unless all three flags are true, then resolves the first actor ID present in candidates.

qrSource provides waitForQrText({cameraId, signal}), typically from @kubohiroya/turbowarp-jsqr. cameraId selects a named Camera Source stream such as qr. nfcSource provides waitForNfcIdm({readerId, signal}), typically from @kubohiroya/turbowarp-webusb-pasori. readerId selects a named PaSoRi such as left or right. The composition accepts only values present in candidates; non-candidate scans keep waiting until the source returns another value or the signal is aborted.

Pose, key, actor-touch, QR, and NFC waits share latest-wins ownership within one composition instance. Starting a new valid wait in any mode first unsubscribes and rejects the previous wait with an AbortError; stale events cannot resolve either wait. An already-aborted, invalid, or missing-source request never subscribes and does not replace the current valid wait. Empty and duplicate candidates are rejected. Abort, resolution, rejection, and releaseAll() all unsubscribe immediately.

This API implements only async-input candidate selection. A DSL controller must keep it mutually exclusive with an Actor ordered-pose sequence. If they conflict, the controller cancels the candidate wait and gives the Actor sequence priority. The two modes do not share score or progress state. Scene movement, rewind, stop, live reload, and runtime release must abort the action signal; re-entering the same scene then starts another zero-score selection session. Standalone blocks, opcodes, and the default-OFF poseInput feature flag are unchanged.

Extension ID compatibility

This migration release uses the standards-compliant ID kubohiroyaasyncinput. Existing projects that store twAsyncInput opcodes must apply a schema-aware project migration at the same time; replacing the JavaScript artifact alone would break their existing blocks.

Runtime variable initialization

Listen blocks only register or replace input bindings. Registering or removing a listener does not create, clear, or otherwise initialize its runtime variable. Initialize the variable explicitly with Temporary Variables before registering listeners whenever the project requires a predictable starting state. Use an empty string for an initially unset input value, or a finite number such as 0 before using +, -, *, or / compound assignment.

Initialize a shared runtime variable once before registering all targets that write to it. This avoids one target's registration resetting values already used by another target.

Target ownership

Every binding belongs to the sprite, clone, or stage that executes the registration block. Key bindings are identified by the current target ID and KeyboardEvent.code. Ordinary touch bindings are identified by the current sprite or clone target ID. The actor-touch compatibility block keeps the executing target as owner while binding the pointer hit to the resolved actor target. Two clones of the same sprite can therefore register independent bindings.

The extension uses one window keydown listener, one renderer-canvas pointerdown listener, and one TurboWarp TM runtime-event listener regardless of the number of bindings. Target deletion removes that target's bindings. Green flag, project stop, and runtime disposal remove all bindings.

Keyboard input

Key IDs use KeyboardEvent.code, for example KeyA, Digit1, ArrowLeft, Space, or Enter. Multiple targets may listen for the same key without replacing each other. Re-registering the same key from the same target replaces only that target's binding.

Repeated keydown events, IME composition, and events from input, textarea, select, or editable elements are ignored. The extension does not prevent browser defaults or stop propagation.

Touch input

The ordinary touch registration block always refers to the sprite or clone that executes it. Stage targets are rejected. The renderer's topmost pick result is matched by target ID, so original sprites and clones remain distinct and transparent pixels follow TurboWarp renderer behavior.

For kamishibai DSL integration, listenForActorTouchAndBroadcast resolves a non-stage target whose actorName variable exactly matches the supplied actor name. Missing and duplicate actor names are rejected. This compatibility block is maintained outside the generated public block reference.

Broadcast after input

The key and touch and broadcast variants update the runtime variable first, then start the matching standard Scratch broadcast without waiting for its receiver scripts to finish. The message name is trimmed and must not be empty. If the runtime variable update fails, including an invalid compound arithmetic update, the broadcast is not started. Each matching target-owned key binding starts its own configured broadcast.

Accumulated pose input

The pose registration block listens for TurboWarp TM's TM_ACCUMULATED_POSE_CHANGED version 1 event. It runs only when the selected accumulated pose name changes to the registered name; confidence changes that keep the same accumulated pose do not retrigger it. Leaving a pose and later returning to it triggers the binding again.

Bindings are keyed by the executing target ID and pose name. Multiple targets can listen for the same pose independently, and one target can listen for multiple poses. Registration fails without Temporary Variables or a TurboWarp TM extension that reports accumulated pose event support.

Compound arithmetic

A value beginning with +, -, *, or / performs numeric compound assignment against the runtime variable's latest value:

+2   -> current += 2
-1   -> current -= 1
*3   -> current *= 3
/2   -> current /= 2

Operands must be finite numbers. Missing or non-numeric current values, division by zero, overflow, and other non-finite results leave the current value unchanged. Values without an arithmetic prefix, including an empty value, use exact string assignment.

DSL integration

Actor-aware touch routing can use the compatibility block directly. For example:

action=touchInput:a1,a2:v1,v1:+2,+5

can register each actor by its actorName value while preserving the executing target as the binding owner. Removing either the actor target or the owning target removes the corresponding touch binding.

Pose routing follows the same pattern:

action=poseInput:jump:v1:+2
action=poseInput:jump:
action=poseInput:

These map to registering jump for the current target, removing that target's jump binding, and removing every pose binding owned by the current target.

Blocks

listen for key [KEY_ID] set runtime var [RUNTIME_VAR] to [VALUE]

Registers or replaces a target-owned key binding.

Property Value
Type Command
Opcode listenForKey
Feature flag asyncInput
KEY_ID String, default: KeyA
RUNTIME_VAR String, default: input
VALUE String, default: pressed

listen for key [KEY_ID] set runtime var [RUNTIME_VAR] to [VALUE] and broadcast [MESSAGE]

Registers or replaces a target-owned key binding that broadcasts after updating the runtime variable.

Property Value
Type Command
Opcode listenForKeyAndBroadcast
Feature flag asyncInput
KEY_ID String, default: KeyA
RUNTIME_VAR String, default: input
VALUE String, default: pressed
MESSAGE String, default: message1

stop listening for key [KEY_ID] for this target

Removes this target's binding for one physical key code.

Property Value
Type Command
Opcode stopListeningForKey
Feature flag asyncInput
KEY_ID String, default: KeyA

stop all key listeners registered by this target

Removes every key binding owned by the current target.

Property Value
Type Command
Opcode stopAllKeyListeners
Feature flag asyncInput

listen for touch on this sprite set runtime var [RUNTIME_VAR] to [VALUE]

Registers or replaces the current sprite or clone's pointer binding.

Property Value
Type Command
Opcode listenForTouch
Feature flag asyncInput
RUNTIME_VAR String, default: input
VALUE String, default: pressed

listen for touch on this sprite set runtime var [RUNTIME_VAR] to [VALUE] and broadcast [MESSAGE]

Registers or replaces the current sprite or clone's pointer binding that broadcasts after updating the runtime variable.

Property Value
Type Command
Opcode listenForTouchAndBroadcast
Feature flag asyncInput
RUNTIME_VAR String, default: input
VALUE String, default: pressed
MESSAGE String, default: message1

stop listening for touch on this sprite

Removes the current target's pointer binding.

Property Value
Type Command
Opcode stopListeningForTouch
Feature flag asyncInput

listen for accumulated pose [POSE_NAME] set runtime var [RUNTIME_VAR] to [VALUE]

Registers or replaces a target-owned accumulated pose binding.

Property Value
Type Command
Opcode listenForPose
Feature flag poseInput
POSE_NAME String, default: jump
RUNTIME_VAR String, default: input
VALUE String, default: detected

stop listening for accumulated pose [POSE_NAME] for this target

Removes this target's binding for one accumulated pose name.

Property Value
Type Command
Opcode stopListeningForPose
Feature flag poseInput
POSE_NAME String, default: jump

stop all pose listeners registered by this target

Removes every accumulated pose binding owned by the current target.

Property Value
Type Command
Opcode stopAllPoseListeners
Feature flag poseInput

stop all input listeners registered by this target

Removes every key, pointer, and accumulated pose binding owned by the current target.

Property Value
Type Command
Opcode stopAllInputListeners
Feature flag asyncInput

Development

corepack enable
pnpm install --frozen-lockfile
pnpm run check

Release

Release commits keep package.json, the pinned npm/CDN examples in this README and the two GitHub Pages guides, and CHANGELOG.md on the same version. After the release PR is merged and CI and the Pages deployment succeed, publish the exact package version, create the matching v<version> tag on the merge commit, and create the GitHub Release from that tag. Consumers can roll back by pinning the preceding npm version.

The build produces dist/async-input.js, dist/composition.js, and dist/types/composition.d.ts. Commit rebuilt artifacts whenever extension or Composition source changes.

License

MPL-2.0

About

A target-scoped asynchronous keyboard, pointer, and accumulated pose input extension for TurboWarp Temporary Variables.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages