@gliwy/input
v0.1.0-preview.1
Published
Optional Gliwy input and application-owned camera controls
Readme
Optional input and orbit camera
@gliwy/input is an experimental, private first-party module. It has no package,
renderer, lifecycle, service, player or game dependency. The portable entry exports
createOrbitCamera; @gliwy/input/browser exports createInput and
createOrbitControls. Browser and native qualification are separate. Native DOM
input is unsupported. These are provisional APIs, not stable save formats.
Application-owned composition
import {createOrbitCamera} from '@gliwy/input';
import {createOrbitControls} from '@gliwy/input/browser';
const camera = createOrbitCamera({distance: 5, minDistance: 1, maxDistance: 20});
// App owns this focusable overlay above its renderer container.
const controls = createOrbitControls({
target: document.querySelector('#input-surface'),
camera,
onChange: async (desiredState, transform) => {
await app.submit([{op: 'setTransform', id: 'camera', transform}]);
// Only an actual successful receipt means the renderer is synchronized.
},
onError: error => showPreviewError(error),
});
// Spawn the app's camera from camera.transform() before attaching input.
controls.attach();
// Before stopping/replacing the application or removing the overlay:
controls.detach();
await controls.settled();
await app.stop();The target must have tabindex="0", an accessible label, explicit dimensions,
and touch-action: none when using direct-touch manipulation. The application
owns that element and its styling; the module does not alter them. A transparent
overlay prevents the renderer iframe from taking scene-input focus. Pointer and
keyboard events inside an iframe do not bubble into its parent's input target.
There is no private canvas, renderer handle or cross-frame event forwarding.
Input begins idle. attach() and detach() are repeatable. createInput can be
used independently of the camera: configure 1–32 unique KeyboardEvent codes,
then inspect snapshot() or consume accumulated deltas with read(). It owns
one captured pointer, at most 32 held keys, six button/Shift drag buckets and two
wheel totals, rather than an unbounded event queue. Each accumulated axis is
clamped to maxDelta (default 2048 CSS pixels). Pointer capture is released and
all listeners removed on detach; failed attachment rolls back acquired listeners.
Keyboard and wheel input require the target itself to be focused. Text inputs, textareas, select controls, content-editable paths, composition and OS shortcuts are respected. Blur, focus departure and a hidden document clear held state and pending deltas. Pointer cancel/lost capture discard pending drag deltas. Only accepted events are prevented. Single-pointer mouse, pen and touch dragging is supported by the adapter; multi-touch/pinch is intentionally absent.
Camera values and controls
snapshot() returns detached {yaw, pitch, distance, target} values. setState
validates the complete replacement atomically. orbit(yawDelta, pitchDelta) uses
radians; pan(right, up) uses camera-relative world units; zoom(logDelta) uses
logarithmic distance, so zoom(Math.log(2)) doubles the distance. transform()
returns a detached Y-up, -Z-forward translation/quaternion/unit-scale transform
compatible with the public runtime scene command contract. Headless/native code
can reuse this math without importing DOM code.
Finite settings reject invalid ranges and unknown fields. Defaults are yaw/pitch 0, distance 5, target [0,0,0], pitch [-1.5,1.5], distance [.1,10000]. Configured pitch limits stay strictly between the poles; distance limits are [1e-6,1e6]. Target components and operation arguments are bounded to ±1e9. Orbit wraps yaw, motion clamps pitch/distance/target, and invalid operations do not mutate state. Snapshots can reconstruct the same camera, but no durable persistence/migration promise is made.
- Primary drag: orbit; Shift-primary, middle or secondary drag: pan
- Wheel: zoom; line/page modes normalize to bounded CSS-pixel deltas
- Arrow keys: orbit; W/A/S/D: pan; = and -: zoom
Controls update once per animation-frame opportunity and await at most one
onChange callback. Input arriving during that callback is accumulated, then
coalesced into the next update. Keyboard time gaps are capped at 50 ms; this is a
safety bound, not fixed-tick gameplay or a frame-rate/performance promise. A custom
asynchronous schedule/cancel pair and monotonic millisecond now can provide
deterministic test timing. Desired camera state changes before the callback;
applications own acknowledgement, error UI and any persistence or rollback policy.
Callback failure detaches input and reports onError; an explicit attach can
retry. Detach cancels unsent work, but cannot revoke an already-started external
callback. Await settled() before disposing its destination. Reattachment waits
for an old in-flight callback before permitting another, and fences stale errors.
Verification
node --test packages/input/*.test.js
npm run typecheck
npm run test:input:browserThe last command requires the prebuilt real runtime and installed Chromium. Its isolated packed consumer checks public runtime inspection and actual changed pixels after pointer orbit, pan, wheel zoom and keyboard input, plus typing focus, touch cancellation and repeated attach/detach. Logic/pack/type checks alone never qualify rendered behavior. Existing renderer/example browser proofs stay separate.
Web API references: Pointer Events, UI Events, and HTML focus.
