Repository navigation
Releases: pmndrs/klipp
Release list
v0.16.0 - Viewfinder
This release adds four extensions that fine-tune a shot: CameraOffset, Recomposer, FollowZoom and Confiner.
It also adds orthographic cameras: Klipp drives them like perspective ones, and every piece that frames on screen works with them.
And a SequencerCamera can now be stepped through like a slideshow.
✨ New Features
CameraOffset- moves the camera in its own space after the Body and Aim, for off-center shots like an over-the-shoulder view.Recomposer- turns, rolls and zooms the finished shot, on top of what the Body and Aim do.FollowZoom- changes the field of view to keep a target the same size on screen, for a dolly zoom.Confiner- keeps the camera inside a box or a sphere.- Orthographic cameras -
KlippThreeand<Klipp>drive anOrthographicCameratoo. Each virtual camera has its ownorthographicSize, set withLensand blended likefov, and every piece that frames on screen works with it. - Navigation in
SequencerCamera-next(),previous()andgoTo(index)jump to another step andrestart()cuts back to the first one.pausedkeeps holding the current step. targetOffsetinPositionComposer- frames a point offset from the target in its own space, like a head instead of the feet, the same way as inRotationComposer.applyAfteron an Extension picks when it runs:'start'before the Body, for the lens the Body and Aim frame with, or'body'right after the Body, for what depends on where the Body put the camera, like a limit on where it can go.
🐛 Bug Fixes
- Extensions and Noise no longer build up in a damped Body or Aim. With damping on
HardLockToTarget,PositionComposerorRotationComposer, the shake fromBasicMultiChannelPerlinand the kicks fromImpulseListenerfed back into the damping on the next frame and grew many times over. They now stay the size you set. RotationComposerandPositionComposerframe with the field of view fromLensin the same frame, instead of the last frame's.- Removing an Extension or Noise removes its effect: removing
Lens, for example, takes the camera back to its own field of view instead of keeping the last one. - Swapping the camera
KlippThreeor<Klipp>drives, for example with drei'smakeDefault, gives the new camera the shot on the next frame. It used to wait until the shot changed, so a still shot never reached it. PositionComposer,RotationComposerandGroupFramingskip a frame whose lens has no view instead of breaking for good.SequencerCamerafinishes the blend into a step with aholdof0before moving on. It used to jump to the next step every few frames, so the camera never reached any of them.
What's Changed
- chore: update version to 0.16.0 by @kvvasuu in #126
- feat: CameraOffset, Recomposer, FollowZoom and Confiner extensions by @kvvasuu in #127
- Feat/position composer target offset by @kvvasuu in #128
- feat: support orthographic cameras by @kvvasuu in #129
- feat: add next, previous, goTo, restart and paused to SequencerCamera by @kvvasuu in #130
- feat: SequencerCamera navigation and a car showcase by @kvvasuu in #131
- docs: serve the showcase at the site root and the examples at /examples/ by @kvvasuu in #132
Full Changelog: v0.15.0...v0.16.0
v0.15.0 - Cutting Room
This release adds camera groups: SequencerCamera plays cameras one after another, and MixingCamera mixes them by weight.
It also makes the API consistent across core, three.js and React, so it breaks a lot of names - the table below maps each old one to the new one.
✨ New Features
- Camera groups - one camera to klipp, made of child cameras:
SequencerCamera/<SequencerCamera>- shows its cameras one after another:instructions({ camera, hold, blend? }),loop,isFinished.MixingCamera/<MixingCamera>- mixes its cameras byweights.
klippState.create('manual')withklippState.setActive(state, name)- pick the active camera yourself.<Noise.ImpulseListener>.<VirtualCamera priority>is optional,0by default.<InputController target>takes the axis owner itself, not only a ref.- A drag can drive one axis:
axes: { x: 'pan' }. - Axis settings in class options:
new OrbitFollowBody(target, { radial: { damping: 0.15 } }). setFromPosition/setFromRotationonOrbitFollowBodyThreeandPanTiltAimThreetakeVector3/Quaternion.damping.start,damping.startVector3,impulseListener.update(out, params, now?).PanTiltOptions,PositionComposerOptions,RotationComposerOptions,InputAxisOptions.
⚠️ Breaking Changes
- Core types are exported only from
@pmndrs/klipp, not from/three,/reactor/dom.CameraControlsBodyThreecomes from@pmndrs/klipp/three/camera-controls. <Aim.PanTilt>:damping,maxSpeed,recentering,panRange,tiltRange,panWrap,tiltWrapmove intopan={{ ... }}andtilt={{ ... }}.maxSpeedofRotationComposerandRotateWithFollowTargetis in degrees per secondbindingModeis a string:'lockToTarget'instead ofBindingModes.lockToTarget.GroupFramingtakesaspectinstead ofviewportWidth/viewportHeight.new GroupFramingExtension(members, { positionMode })instead of a second argument.readTargetPose(out, target, withRotation, slot?)-slotis last and optional.damping.dampVector3anddamping.dampQuaterniontakeoutfirst.primeofpositionComposer,rotationComposerandrotateWithFollowTargettakes only the state;primeFrom()takes no argument.InputSourceMapping,InputAxisMappingandInputControllerConfigtake the axis type:InputControllerConfig<string>.
| Before | Now |
|---|---|
klipp.activeCameraId, liveCameraId, activeState |
activeCameraName, liveCameraName, activeCameraState |
klipp.subscribeActiveId, subscribeLiveId |
subscribeActiveCamera, subscribeLiveCamera |
klipp.setDefaultBlend(b), setCustomBlends(b) |
klipp.defaultBlend = b, klipp.customBlends = b |
blendFinished event { liveId } |
{ incoming } |
klipp.blendTargetId, hasEverActivated |
klipp.state.blend |
klipp.addCamera(name, options) |
klipp.add(new VirtualCamera(name, options)) |
KlippEvents, VirtualCameraEvents |
<Klipp.Events>, <VirtualCamera.Events> |
KlippEventsProps, VirtualCameraEventsProps |
CameraTransitionEventProps |
klippState.register(state, { id }) |
{ name } |
klippState activeId, customBlendFromId |
activeName, customBlendFromName |
klippState events activeIdChanged, liveIdChanged |
activeCameraChanged, liveCameraChanged |
blend.targetId, BlendState.liveId |
blend.targetName, liveName |
BlendDriver.liveId, blendTargetId |
liveName, blendTargetName |
DEFAULT_BLEND |
klippState.createParams().defaultBlend |
OrbitFollowAxisSettings |
InputAxisSettings |
TargetGroupPositionMode |
GroupPositionMode |
MouseButton |
MouseButtons |
InputSourceConfig, InputAxisSourceConfig, InputAxisPair |
InputSourceMapping<string>, InputAxisMapping<string>, { x, y } |
ConsumedInput.touchRotateDelta (radians) |
twistDelta (degrees) |
copyCameraStateFromCamera, applyCameraState |
readCameraState, writeCameraState |
CameraControlsOptions |
CameraControlsThreeOptions |
perlinNoise, PerlinNoise* types |
basicMultiChannelPerlin, BasicMultiChannelPerlin* |
panTilt.seed |
panTilt.setFromRotation |
Predictor.predictPositionDelta |
predictDelta |
ImpulseField.sampleAt |
sample |
DebugZone.className |
kind: 'deadZone', 'hardLimit', 'groupFraming' |
hasBlendHint(hints, flag) |
(hints & flag) !== 0 |
Also removed from the exports: isInputAxisOwner, isVector3Like, resolveVec3, resolveTargetSize, InputAxisOwnerContext (use <InputController target>), ImpulseClockSeconds (a number).
🐛 Bug Fixes
OrbitFollowwithrecenteringTarget: 'trackingTarget'recenters tohorizontal.center, measured from behind the target (0behind,180in front).- With
frameloop="demand", every Body, Aim, Extension and Noise component requests a frame when its props change or it unmounts. HardLockToTargetandLenswith damping ease in frominitialStateon the first activation.PanTiltAim.setFromRotationlands on the rotation while an axis is still easing, the short way around.<Aim.PanTilt>axes no longer share their defaultrangeandrecenteringobjects.TargetGroup.computeBoundsreturns-1when no member resolves.
What's Changed
- ci: publish canaries and tagged npm releases by @krispya in #121
- refactor!: API consistency review by @kvvasuu in #122
- Feat/groups by @kvvasuu in #123
- refactor!: name cameras by name in klippState and blend by @kvvasuu in #124
- refactor!: drop addCamera and duplicate exports by @kvvasuu in #125
Full Changelog: v0.14.0...v0.15.0
v0.14.0 - New Home
klipp is now published as @pmndrs/klipp
This release adds OrbitFollow, a Body the user orbits and zooms around a target, with wheel and pinch input to drive it. Cameras off screen now update in turns, so many cameras cost about as much as two.
✨ New Features
OrbitFollow- puts the camera on a sphere around the target, or on a surface through three rings withorbitStyle: 'threeRing'. Itshorizontal,verticalandradialaxes take the mouse, touch, the wheel and pinches, or values from your code.setFromPositionandsetFromRotationease the camera to a spot, and thedebugprop draws the orbit.OrbitFollowBodyon the core,OrbitFollowBodyThreeandOrbitFollowHelperThreein three.js,<Body.OrbitFollow>in React.- Wheel and pinch input -
wheelandpinchin an input controller each drive one axis. A pinch feels the same with two fingers and on a trackpad. standbyUpdate- how a camera updates while another one is on screen:'roundRobin','always'or'never'. A camera that skipped frames catches up with the time it missed, so switching to it stays smooth.rotationDampingonFollow- seconds to turn with the target, for everybindingModebutworldSpace.- Damped Aims follow the Body's own turns - an Aim with damping, like
RotationComposer, follows the turns of an orbit right away, so the target stays in place on screen while the user drags. A custom Body reports its turn inrotationDampingBypasson theCameraState. axis.setValue(value)jumps an axis straight to a value, for a teleport or a reset. Recentering carries on.<InputController>finds axes on the whole camera - placed directly in a<VirtualCamera>, it drives axes by name from any of the camera's pieces. On the core,camera.inputAxeslists them.InputControllerfrom@pmndrs/klippmaps input onto axes without a DOM, for example to replay a recording.InputControllerDomfrom@pmndrs/klipp/dombuilds on it.- Input configs list only the sources they use - leave out the rest instead of setting them to
null.
⚠️ Breaking Changes
- The package is now
@pmndrs/klipp. Install it in place of@kvvasuu/klippand change the imports. The entry points stay the same:@pmndrs/klipp/three,/react,/domand thecamera-controlsones.@kvvasuu/klippis deprecated and gets no more releases. - Core functions are grouped into namespaces with short names:
createFollowParamsis nowfollow.createParams,updateFollowisfollow.update,createTargetPoseistargetPose.create,createCameraStateiscameraState.create,lerpCameraStateiscameraState.lerp,tickKlippisklippState.tickandcomposerDebugZonesisdebugZones.composer. Classes, types,BlendCurves,BlendHintsandDEFAULT_BLENDkeep their names. Building on the core lists every namespace. InputAxisControlleris nowInputControllerDom, and itslastInputis nowinput.InputAxisControllerConfigis nowInputControllerConfig, andfeedInputAxesisinputMapping.feedAxes. Therefof an<InputController>is anInputControllerDom.Sequencer,MixingCamera,StateDrivenCameraandClearShotare left out of the exports, along with their functions, until they are finished and tested.BlendHints.screenSpaceAimWhenTargetsDifferandBlendHints.inheritPositionare removed. They were never implemented.- Raw pinch input is one field -
touchPinchDelta,gestureZoomDeltaandwheelZoomDeltainConsumedInputare replaced bypinchDelta, the change inln(scale). follow.prime(state, position)no longer takesparams.CameraStatehas arotationDampingBypassfield. A state built withcameraState.create()already has it, but one written out by hand needs it added.
🔄 Behavior Changes
- Cameras off screen update in turns by default (
standbyUpdate: 'roundRobin'). An off screen camera'sstateis only as fresh as its last update, and its frustum helper moves in steps. UsestandbyUpdate: 'always'for a camera whosestateyou read every frame. Follow'sdampingdelays only the target's position. The camera turns with the target right away, so a dampedFollowbehind a turning target moves differently than before. SetrotationDampingto delay the turn too.- The page keeps scrolling and pinch zooming over the canvas until
wheelorpinchis mapped. - Wheels that scroll by lines or pages report pixels, so
input.wheelDeltaYreads the same in every browser.
🐛 Bug Fixes
- Looking straight up or down with
HardLookAtorRotationComposerkeeps a valid, steady rotation. lockToTargetOnAssigncaptures the target's rotation again when the camera goes live or the mode is chosen, as well as whentargetis set.- On-demand rendering wakes up for every input an input controller reads and on a
<Klipp mode>change. It also keeps rendering while an axis eases or recenters. - Switching
modeback to'enabled'writes the shot again and takes the camera back, even when the shot didn't change. - A throwing event listener no longer drops the events after it.
GroupFramingwith a single point member frames it whenpaddingis set.- Disconnecting input puts back the element's own
touch-actionanduser-select, also with several controllers on one element.
📖 Documentation
- Docs and examples are at pmndrs.github.io/klipp.
- New pages: OrbitFollow, and Input from data for steering a camera from your code, the network or a recording.
standbyUpdateon the VirtualCamera page, and orbits on the Debugging page.- New examples: OrbitFollow with recentering, three rings, a binding mode, a damped Aim and GroupFraming, wheel and pinch, raw input, touch and areas, input from data, and
standbyUpdate.
🔧 Internal Changes
- Built with Vite, one file per source file, so
distand the package exports stay the same. - Inactive cameras and those skipped by
standbyUpdateskip their per-frame preparation.RotationComposer, Perlin noise andPositionComposerskip the work they don't use. - Mouse, touch, wheel and Safari gesture input are separate sources behind
InputSystem. - Benchmarks are their own package, including whole
KlippThreeframes with up to 50 cameras. - Golden trajectory tests for
FollowandOrbitFollow.
What's Changed
- chore: stop a release whose tag doesn't match package.json by @kvvasuu in #105
- Chore/move to pmndrs by @kvvasuu in #106
- chore: point the docs, examples and repo links at pmndrs by @kvvasuu in #107
- Let cameras off screen update in turns by @kvvasuu in #108
- Prepare only the cameras that update this frame by @kvvasuu in #109
- perf: skip work pieces do not use by @kvvasuu in #110
- refactor: group core functions into namespaces by @kvvasuu in #111
- build: bundle with Vite by @kvvasuu in #112
- Sort imports with prettier by @kvvasuu in #114
- Split benches into their own package by @kvvasuu in #115
- Fix/core edge cases by @kvvasuu in #116
- feat: data-oriented input sources by @kvvasuu in #118
- Feat/orbit follow by @kvvasuu in #119
- chore: rename package to @pmndrs/klipp by @krispya in #120
Full Changelog: v0.13.0...v0.14.0
v0.13.0 - One Core
klipp is now a complete camera system in the core, with integrations as thin layers on top: three.js and React Three Fiber today, with more to come, like TresJS. Klipp runs the cameras, picks the winner and blends with any renderer. KlippThree from @kvvasuu/klipp/three drives a three.js camera, and <Klipp> is built on it.
✨ New Features
Klippruns everything - virtual cameras, priority, blending and the frame loop. Callklipp.update(dt)and putklipp.shoton your camera, or overridewriteto update it only when the shot changes.- Every piece is a core class -
FollowBody,HardLookAtAim,PositionComposerBody,GroupFramingExtensionand the rest take aTargetPoseyou keep up to date. To read your engine's objects directly, extend one and overridereadTarget. - three.js classes end in
Three-KlippThreedrives aPerspectiveCamera, andVirtualCameraThree,FollowBodyThreeand the other pieces readObject3Dtargets. Their options take aVector3, a tuple or a number wherever a setting is a vector. klipp.whileOnScreen(camera, start)connects input only while its camera is on screen, outside React.CameraFrustumHelperThreedraws a virtual camera's frustum in three.js.- Settings factories -
createFollowParams,createLensParamsand the rest give every piece's defaults. - Debug zones in the core -
composerDebugZonesandgroupFramingPaddingBox, for drawing zones withDebugOverlaywithout React. - Duplicate camera names now log a warning.
⚠️ Breaking Changes
- three.js classes end in
Three-FollowBody,HardLockToTargetBody,PositionComposerBody,HardLookAtAim,RotateWithFollowTargetAim,RotationComposerAim,PanTiltAim,GroupFramingExtensionandCameraControlsBodyfrom@kvvasuu/klipp/threeare nowFollowBodyThree,HardLockToTargetBodyThreeand so on. This also changes the types of React refs:useRef<FollowBody>is nowuseRef<FollowBodyThree>. KlippCoreis nowKlipp, andKlippCoreOptionsis nowKlippOptions.Klipp-registerCamera,updatePriority,updateHintsandtickare replaced byaddCamera(name, options), the camera's ownpriorityandhints, andupdate(dt)withshot.VirtualCameraControlleris nowVirtualCamera.useKlipp()returns theKlippThreeinstead of{ core, registerUpdate, initialCameraState, targets }:useKlipp().core.isBlendingis nowuseKlipp().isBlending.useVirtualCamera()returns the camera instead of{ controller, state, initialState }. A<VirtualCamera>'srefis that sameVirtualCameraThree.- Custom pieces -
registerBody(update)andregisterAim(update)are replaced bycamera.body = { update }andcamera.aim = { update }, orsetBodyandsetAim.registerExtensionandregisterNoiseare replaced byaddExtension({ update })andaddNoise({ update }). Every method except the setters returns a function that removes the piece. - Types removed -
KlippContextValue,VirtualCameraContextValueandVirtualCameraSlots.
🔄 Behavior Changes
BlendCurves.easeInandeaseOutare swapped, to matchcubicInandcubicOutfrommath. "In" is now a gentle departure, as in CSS. Your code compiles as before, but these blends move the other way: swap the two names to keep the old motion.createGroupMember()starts withresolved: true, so a new member counts right away. Set it tofalseto skip one.
🐛 Bug Fixes
- Reactivating a camera whose target isn't available yet no longer eases in from the old shot once the target appears.
HardLockToTargetandGroupFramingreset on activation likeFollow, and the composers wait for the target.
📖 Documentation
- Every page shows the core and each integration in tabs. The tab you pick is remembered across pages.
- Your first camera is one step-by-step guide per integration.
- Building on the core covers your own integration and the data and functions underneath.
- Shorter pages and README, without repeated explanations.
🔧 Internal Changes
- three.js and React keep only what is theirs: target reads, camera writes, props and hooks.
- Core tests run without three.js, and tests are typechecked in CI.
- Leaner test suite, with one test per behavior.
What's Changed
- test: core tests without three.js by @kvvasuu in #90
- refactor: express the recoil impulse as an inverted smoothstep by @kvvasuu in #91
- Test/cleanup by @kvvasuu in #92
- Refactor/params defaults by @kvvasuu in #93
- Fix/hardening by @kvvasuu in #94
- test: seed the impulse shake so it never samples exactly zero by @kvvasuu in #96
- Refactor/klipp class by @kvvasuu in #97
- Docs/core react by @kvvasuu in #98
- Docs/tabs by @kvvasuu in #99
- Feat/three vector options by @kvvasuu in #100
- Feat/layer helpers by @kvvasuu in #101
- Refactor/three suffix by @kvvasuu in #102
- docs: sync the integration tabs across pages by @kvvasuu in #103
- docs: create the target in every core example by @kvvasuu in #104
Full Changelog: v0.12.0...v0.13.0
v0.12.0 - Under the Hood
v0.12.0 - Under the Hood
A rewrite of the engine. The core now runs on math as plain data and functions, with no three.js or React. The React components and their props work as before.
✨ New Features
- Framework-agnostic core -
@kvvasuu/klipphas no dependencies beyondmath. Every Body, Aim, Extension and Noise is also available as a function (updateFollow,updateHardLookAt, ...), so klipp works with any engine, an ECS or your own loop. - One entry point per layer -
@kvvasuu/klipp(core),/three,/reactand/dom.threeis now an optional peer dependency too, needed only by/threeand/react. - Each target is read once per frame, however many cameras and pieces use it.
DebugOverlayin/domdraws composer and framing zones without React.- Runs in Node without a bundler - the build is now valid ESM.
⚠️ Breaking Changes
- three.js classes moved to
@kvvasuu/klipp/three-FollowBody,HardLookAtAimand the other Bodies/Aims,applyCameraState,copyCameraStateFromCamera,resolveTarget*,Vector3DamperandQuaternionDamper.@kvvasuu/klipp/camera-controlsis now@kvvasuu/klipp/three/camera-controls. - Vectors are arrays -
CameraStateand class fields likeoffsetuse[x, y, z]and[x, y, z, w]instead ofVector3andQuaternion. Props still accept both. BlendCurves.hardIn/hardOutrenamed tocubicIn/cubicOut, matchingmath.
🔄 Behavior Changes
- Damping uses
math's exact spring. Motion is slightly different from 0.11 (about 1% of a typical move), most noticeably after large jumps.
🐛 Bug Fixes
ImpulseFieldno longer grows without bound - expired impulses are now also dropped ongenerate, not only while a listener samples the field.
🔧 Internal Changes
- No per-frame allocations in the camera loop.
- Simpler docs and example scenes; the examples cap the canvas DPR at 1.5.
- Layer boundaries enforced by lint.
- Golden trajectory tests guard camera motion frame by frame.
- TypeScript 7, Vitest 5, three 0.186.
What's Changed
- Docs/rewrite by @kvvasuu in #56
- Chore/examples cleanup by @kvvasuu in #57
- Perf/allocation audit by @kvvasuu in #58
- Chore/migration safety net by @kvvasuu in #59
- Revert "Chore/migration safety net" by @kvvasuu in #60
- Refactor/layers by @kvvasuu in #72
- Refactor/body modules by @kvvasuu in #73
- refactor: RotateWithFollowTarget and PanTilt on math-only core functions by @kvvasuu in #74
- refactor: RotationComposer on math-only core functions by @kvvasuu in #76
- refactor: Lens and GroupFraming on math-only core functions by @kvvasuu in #77
- Refactor/noise modules by @kvvasuu in #79
- Refactor/aim modules by @kvvasuu in #80
- refactor: camera arbitration and blending as state plus functions by @kvvasuu in #81
- refactor: camera groups as state plus functions by @kvvasuu in #82
- refactor: input axes and input mapping as data plus functions by @kvvasuu in #84
- Chore/math canary by @kvvasuu in #85
- Refactor/math damping by @kvvasuu in #86
- Refactor/blend curves math by @kvvasuu in #87
- Chore/migration docs by @kvvasuu in #88
- Chore/bump dev deps by @kvvasuu in #89
Full Changelog: v0.11.0...v0.12.0
v0.11.0 - Look Around
v0.11.0 - Look Around
Introducing the Input System and the first element based on it - the PanTilt aim.
✨ New Features
InputSystem/InputAxis/InputAxisController/<InputController>- a full input layer for building your own drag/scroll/touch-driven camera controls.InputSystemcollects raw pointer/wheel/touch/Pointer Lock deltas per element;InputAxisshapes a single value (gain, clamp, wrap, damping, idle recentering);InputAxisController/<InputController>map mouse buttons and touch counts onto named axes on any Body/Aim/Extension that exposes them.Aim.PanTilt- mouse/touch look-around (yaw/pitch) built on the new input layer, with an optional rotating reference frame,wrap/rangelimits, and idle recentering back to center.maxSpeedon everydampingprop (Body/Aim/Extension) and onBlendDefinition's damping blend - caps how fast damping can close a gap, independent of the time constant itself.
⚠️ Breaking Changes
useVirtualCameraSlots()/useVirtualCameraState()merged into oneuseVirtualCamera()- returns{ controller, state, initialState }instead of two separate hooks.controllerreplaces the oldVirtualCameraSlotsreturn value;initialState(the raw prop, not the already-seededCameraState) is newly reachable this way.useKlippCore()replaced byuseKlipp()- returns{ core, registerUpdate, initialCameraState }instead of just the core directly; grab it viauseKlipp().core.
🐛 Bug Fixes
Follow/PositionComposer/RotationComposer/RotateWithFollowTargetnow actually respectinitialState- their dampers used to snap straight to the first real target on activation, silently discarding the seeded starting pose. They now ease from it instead.
🔧 Internal Changes
- Prettier + a
lint-stagedpre-commit hook + aprettier --checkstep in CI. - Hooks/contexts split out of
VirtualCamera.tsx/Klipp.tsx/InputController.tsxinto their own files, for React Fast Refresh compliance. VirtualCameraEventsProps/KlippEventsPropsdeduplicated into one shared type.- Public docs stripped of leftover internal implementation detail.
- Simplified and cleaned comments.
What's Changed
- feat: add BlendCurves example scene by @kvvasuu in #44
- feat: add BlendHints example scene by @kvvasuu in #45
- Feat/examples blend damping custom by @kvvasuu in #46
- Feat/max speed by @kvvasuu in #47
- fix: PanTilt seeds pan/tilt from initialState.quaternion via new useVirtualCamera hook by @kvvasuu in #48
- fix: InputAxis recentering waits for release, not just no-delta, and fixes wait: 0 by @kvvasuu in #49
- Feat/pan tilt by @kvvasuu in #50
- Feat/input system by @kvvasuu in #51
- fix: Follow/PositionComposer/RotationComposer/RotateWithFollowTarget prime their dampers from initialState by @kvvasuu in #52
- Chore/prettier lint staged ci by @kvvasuu in #53
- Chore/public api and docs cleanup by @kvvasuu in #54
- Chore/comments cleanup by @kvvasuu in #55
Full Changelog: v0.10.0...v0.11.0
v0.10.0 - Recoil
v0.10.0 - Recoil
⚠️ Breaking Changes
ImpulseManagerrenamed toImpulseField- same registry of world-space impulse events, new name.Noise.ImpulseListeneris now justImpulseListener, imported directly - it's still mechanically a Noise-slot filler, but no longer presented as a Noise variant, since it's its own concept.attackTime/sustainTime/decayTimereplaced byshape+duration-generate()now takes ashape: ImpulseShape((t) => number) instead of three separate linear-ramp times. Named presets live onImpulseShapes.recoil/bump/explosion/rumble, or pass your own function.ImpulseField.sampleAt()takesgainas a parameter and returns the felt strength, not aVector3
✨ New Features
ImpulseListenergainsshake- an optional secondary rattle, position and rotation, composing aBasicMultiChannelPerlinNoisewhoseamplitudeGainautomatically follows the impulse's current strength every frame. The kick itself stays position-only;shakeis how an explosion or rumble also pitches the camera.ImpulseListenergainscameraSpace- rotates the sampled kick by the camera's current orientation, so the samedirectionalways kicks the same way relative to where the camera is looking (weapon recoil, a landing thump), instead of a fixed world-space vector.
🔧 Internal Changes
- New performance benchmarks for
ImpulseField.sampleAt(1/10/50 concurrent events) andImpulseListenerNoise.update(kick alone vs. withshake/cameraSpace, isolated). docs/impulse/is now its own top-level docs category instead of nested underdocs/noise/.
What's Changed
- feat: add PositionComposer example scenes by @kvvasuu in #41
- feat: add RotationComposer example scenes by @kvvasuu in #42
- Feat/impulse by @kvvasuu in #43
Full Changelog: v0.9.0...v0.10.0
v0.9.0 - Ensemble
v0.9.0 - Ensemble
✨ New Features
GroupFraminggainsfitMode,minDistance/maxDistance, andframingMode-fitMode: 'ceiling'(default, unchanged) only dollies back, never closer than Body/Aim already placed the camera;'rigid'always sits exactly at the fit distance, dollying in as the group shrinks too.minDistance/maxDistanceclamp the fit distance this extension computes.framingMode('horizontal'/'vertical'/'horizontalAndVertical') lets the fit ignore one screen axis entirely, for framing that only cares about one dimension.
🐛 Bug Fixes
GroupFramingnow fits sphere/box members per-axis, not an isotropic bound - it used to treat a member's offset from the group center as a single, direction-agnostic length, so an offset mostly along the WIDER screen axis was penalized as if it could be along the narrower one, dollying the camera much farther back than actually needed. Fixed with the exact per-axis frustum-plane distance against the camera's current up/right/forward.GroupFraming's debug padding box now matches that same per-axis math - it used to project a flat point (tan) instead of the sphere the fit actually solves for (sin), so the drawn box sat noticeably farther out than where members actually needed to stay.
🔧 Internal Changes
- New regression tests for the per-axis fit and for
fitMode/minDistance/maxDistance/framingMode.
What's Changed
Full Changelog: v0.8.1...v0.9.0
v0.8.1 - Steadicam
v0.8.1 - Steadicam
✨ New Features
CameraState.referenceUp- a shared "up" hint a Body can hand to a paired Aim, instead of every rotation-building Aim defaulting to a hardcoded world-up.Followwrites it, rotated the same way as its own offset (so it reflects whatever the currentbindingModederived), andHardLookAt/RotationComposerread it when building their look-at rotation. Body still never touches rotation and Aim still never touches position -referenceUpis just a value passed between them, interpolated during blends like every otherCameraStatefield.
🐛 Bug Fixes
HardLockToTargetBodyno longer pops on a retarget mid-blend - it publishedout.targetas the raw, undamped target position whileout.positionwas the damped one still catching up.BlendHints.cylindricalPosition/sphericalPositionderive an orbit offset fromposition - target, so retargeting mid-catch-up made that offset jump instantly, popping any blend that read it. Fixed by publishingout.targetas an exact copy of the (already damped)out.position-HardLockToTargetBodyhas no persistent offset by definition, so this is always correct.FollowBodyhad the same bug, fixed the same way in spirit -Followhas a real, persistent, non-zero offset though, soout.targetis now derived algebraically from the damped position minus the rotated offset, keeping the offsetBlendHintsread always exactly what's configured, regardless of damping lag.
📖 Examples
- New interactive examples site - collection of live demos, each with a Leva control panel and a "spectator" inset watching the primary camera from outside.
Only a handful of pieces are covered so far - more scenes land incrementally.
🔧 Internal Changes
mathbumped from a canary prerelease to the first stable^0.1.0release.- New regression tests for both fixes above, each confirmed to fail against the pre-fix code before the fix landed.
What's Changed
- feat: propagate roll to a paired Aim via CameraState.referenceUp by @kvvasuu in #36
- Fix/hard lock to target blend hint pop by @kvvasuu in #37
- Feat/examples site by @kvvasuu in #38
Full Changelog: v0.8.0...v0.8.1
v0.8.0 - Dispatch
v0.8.0 - Dispatch
⚠️ Breaking Changes
reactpeer bumped to>=19.2.0(from^19.0) - the event system below usesuseEffectEvent, stable only from React 19.2 onward. Only affects the@kvvasuu/klipp/reactentry point; the framework-agnostic@kvvasuu/klippengine has no React dependency at all, so this doesn't touch it.
✨ New Features
- Camera Events -
KlippCorenow extends three'sEventDispatcher, dispatchingactivated/deactivated/blendCreated/blendFinished/cutfor every arbitration and blend transition it drives.VirtualCameraControllerextendsEventDispatchertoo, re-dispatching only the events one specific camera takes part in -activatedwhen it's incoming,deactivatedwhen it's outgoing,blendCreated/cuton either side,blendFinishedwhen it's the one that just settled live. Two ways to listen to either:<Klipp.Events>/<VirtualCamera.Events>as callback props, or arefon<VirtualCamera>foraddEventListenerstraight on the underlyingVirtualCameraController.
🐛 Bug Fixes
CameraControlsno longer reconnects on every unrelated re-render - itson*event props (onControlStart/onUpdate/etc.) used to sit in the connect effect's dependency array, so a fresh inline callback from any parent re-render tore down and rebuilt the wholecamera-controlsconnection, occasionally dropping an in-progress drag. Now handled viauseEffectEvent, so only an actual change in target/domElement/arbitration reconnects.
🔧 Internal Changes
- Hardened
KlippCore.tick()'s event dispatch against reentrancy - a listener that synchronously callsregisterCamera/updatePrioritycan no longer corrupt a later event's payload within the same tick. - New tests:
KlippCore,VirtualCameraController,VirtualCamera, andCameraControlsall get coverage for the new events and the two fixes above. - Docs: a new Events page under Advanced, covering both listening paths, all five events, and the
refalternative.
What's Changed
Full Changelog: v0.7.0...v0.8.0