Input from data
An axis is plain data, so anything that produces numbers can steer a camera: your own code, an AI, another player over the network, or a recording of earlier input. None of it needs a mouse or a DOM.
Changing an axis
Use setValue for jumps. Writing value directly doesn't last: the axis eases back to where its input left it.
In the same frame
Feed the axes from a function passed to registerUpdate. It runs every frame before the cameras, so the camera answers in that same frame. While your data keeps changing on its own, return true from it, so on-demand rendering keeps asking for frames.
Usage
const remote = { pan: 0, tilt: 0 }; // filled by your network code
klipp.registerUpdate(() => {
aim.pan.applyDelta(remote.pan);
aim.tilt.applyDelta(remote.tilt);
remote.pan = 0;
remote.tilt = 0;
});
aim.pan.setValue(0); // face forward again
Recording and replay
An InputController from @pmndrs/klipp reads its input from its input field. Copy what a live controller read each frame, then play it back into another controller with the same mapping. Here live is an InputControllerDom set up as usual, updated in the recording function:
import { InputController, type ConsumedInput } from '@pmndrs/klipp';
const frames: ConsumedInput[] = [];
klipp.registerUpdate(() => {
live.update();
frames.push({ ...live.input });
});
// later
const replay = new InputController(live.config);
let frame = 0;
klipp.registerUpdate(() => {
if (frame >= frames.length) return false;
Object.assign(replay.input, frames[frame++]);
replay.update();
return true;
});
The replay plays one recorded frame per frame.
const remote = { pan: 0, tilt: 0 }; // filled by your network code
klipp.registerUpdate(() => {
aim.pan.applyDelta(remote.pan);
aim.tilt.applyDelta(remote.tilt);
remote.pan = 0;
remote.tilt = 0;
});
aim.pan.setValue(0); // face forward again
Recording and replay
An InputController from @pmndrs/klipp reads its input from its input field. Copy what a live controller read each frame, then play it back into another controller with the same mapping. Here live is an InputControllerDom set up as usual, updated in the recording function:
import { InputController, type ConsumedInput } from '@pmndrs/klipp';
const frames: ConsumedInput[] = [];
klipp.registerUpdate(() => {
live.update();
frames.push({ ...live.input });
});
// later
const replay = new InputController(live.config);
let frame = 0;
klipp.registerUpdate(() => {
if (frame >= frames.length) return false;
Object.assign(replay.input, frames[frame++]);
replay.update();
return true;
});
The replay plays one recorded frame per frame.
Register the update from a component inside <Klipp>, and reach the axes through the piece's ref:
function RemoteLook({ aim, remote }) {
const klipp = useKlipp();
useEffect(
() =>
klipp.registerUpdate(() => {
aim.current?.pan.applyDelta(remote.pan);
aim.current?.tilt.applyDelta(remote.tilt);
remote.pan = 0;
remote.tilt = 0;
}),
[klipp, aim, remote],
);
return null;
}
With frameloop="demand", data that arrives between frames, for example in a WebSocket message, needs a nudge: call invalidate() after storing it.
socket.onmessage = (message) => {
remote.pan += message.pan;
invalidate();
};
Jumps work the same way: aim.current.pan.setValue(0).