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.
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.0Load 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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 |
Removes this target's binding for one physical key code.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopListeningForKey |
| Feature flag | asyncInput |
KEY_ID |
String, default: KeyA |
Removes every key binding owned by the current target.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopAllKeyListeners |
| Feature flag | asyncInput |
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 |
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 |
Removes the current target's pointer binding.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopListeningForTouch |
| Feature flag | asyncInput |
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 |
Removes this target's binding for one accumulated pose name.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopListeningForPose |
| Feature flag | poseInput |
POSE_NAME |
String, default: jump |
Removes every accumulated pose binding owned by the current target.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopAllPoseListeners |
| Feature flag | poseInput |
Removes every key, pointer, and accumulated pose binding owned by the current target.
| Property | Value |
|---|---|
| Type | Command |
| Opcode | stopAllInputListeners |
| Feature flag | asyncInput |
corepack enable
pnpm install --frozen-lockfile
pnpm run checkRelease 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.
MPL-2.0