@kubohiroya/turbowarp-async-input
v0.11.0
Published
A target-scoped asynchronous keyboard, pointer, and accumulated pose input extension for TurboWarp.
Maintainers
Readme
TurboWarp-Async-Input
English | 日本語
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.
Minimal demo
Open in TurboWarp Editor · Download SB3
Click the green flag, click the stage to focus it, then press Space: the counter increases by one and the runtime value becomes pressed. S unregisters the binding; the Stop button also releases it. A new green flag resets the count and registers once. Editing a text field does not trigger the binding. The SB3 declares official Temporary Variables as a dependency, so loading it requires network access. No camera, microphone, or external device is needed.
See samples/README.md for editable sources and regeneration.
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/[email protected]Load node_modules/@kubohiroya/turbowarp-async-input/dist/async-input.js, or use the
version-pinned CDN URL:
https://cdn.jsdelivr.net/npm/@kubohiroya/[email protected]/dist/async-input.jsThe 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
Two independent ID migrations affect this extension. Async Input's own ID changes the opcodes stored in project files. The TurboWarp TM ID that accumulated pose input resolves at runtime changes nothing in project files. Neither migration implies the other.
Async Input's own extension ID
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.
TurboWarp TM extension ID
Accumulated pose input resolves TurboWarp TM through a runtime extension ID. Async Input 0.6.0
moved that lookup from TurboWarp TM's 1.x ID to kubohiroyatm, which TurboWarp TM adopted in
2.0.0. Only matching sides resolve each other:
| Async Input | TurboWarp TM ID it resolves | TurboWarp TM 1.x | TurboWarp TM 2.0.0 and later |
| --- | --- | --- | --- |
| 0.5.x and earlier | the 1.x ID | supported | rejected |
| 0.6.0 and later | kubohiroyatm | rejected | supported |
The 1.x ID is not spelled out here, because this repository keeps legacy pose-era naming out of its
documentation. Consult TurboWarp TM 1.x's own documentation when that value is needed. Every
release from 0.6.0 onward, including the current one, pairs with TurboWarp TM 2.0.0 or later.
A rejected combination withholds accumulated pose input and nothing else. Keyboard, touch, QR, and NFC input never consult TurboWarp TM and behave identically. The failure is not silent either: registration requires a TurboWarp TM extension that reports accumulated pose event support, so an unresolved ID surfaces as a registration failure rather than a binding that never fires.
The poseInput feature flag is off by default in config/feature-flags.ts. A build that leaves it
off never reaches this lookup, so the combination cannot matter there.
Extension ID and event version are separate compatibility axes. Pose input subscribes to
TM_ACCUMULATED_POSE_CHANGED version 1, and a matching extension ID does not by itself guarantee a
matching event version. repo-policy.json holds integrations.turboWarpTMExtensionId and
integrations.accumulatedPoseEvent as the source of truth for both.
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 /= 2Operands 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,+5can 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
npm install --global corepack@latest
corepack enable
pnpm install --frozen-lockfile
pnpm run checkRelease
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
