Skip to content

Releases: pmndrs/klipp

v0.16.0 - Viewfinder

Choose a tag to compare

@kvvasuu kvvasuu released this 23 Oct 22:26
844b85e

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 - KlippThree and <Klipp> drive an OrthographicCamera too. Each virtual camera has its own orthographicSize, set with Lens and blended like fov, and every piece that frames on screen works with it.
  • Navigation in SequencerCamera - next(), previous() and goTo(index) jump to another step and restart() cuts back to the first one. paused keeps holding the current step.
  • targetOffset in PositionComposer - frames a point offset from the target in its own space, like a head instead of the feet, the same way as in RotationComposer.
  • applyAfter on 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, PositionComposer or RotationComposer, the shake from BasicMultiChannelPerlin and the kicks from ImpulseListener fed back into the damping on the next frame and grew many times over. They now stay the size you set.
  • RotationComposer and PositionComposer frame with the field of view from Lens in 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 KlippThree or <Klipp> drives, for example with drei's makeDefault, 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, RotationComposer and GroupFraming skip a frame whose lens has no view instead of breaking for good.
  • SequencerCamera finishes the blend into a step with a hold of 0 before 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

Choose a tag to compare

@kvvasuu kvvasuu released this 18 Oct 12:05
9e06ab5

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 by weights.
  • klippState.create('manual') with klippState.setActive(state, name) - pick the active camera yourself.
  • <Noise.ImpulseListener>.
  • <VirtualCamera priority> is optional, 0 by 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 / setFromRotation on OrbitFollowBodyThree and PanTiltAimThree take Vector3 / 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, /react or /dom. CameraControlsBodyThree comes from @pmndrs/klipp/three/camera-controls.
  • <Aim.PanTilt>: damping, maxSpeed, recentering, panRange, tiltRange, panWrap, tiltWrap move into pan={{ ... }} and tilt={{ ... }}.
  • maxSpeed of RotationComposer and RotateWithFollowTarget is in degrees per second
  • bindingMode is a string: 'lockToTarget' instead of BindingModes.lockToTarget.
  • GroupFraming takes aspect instead of viewportWidth / viewportHeight.
  • new GroupFramingExtension(members, { positionMode }) instead of a second argument.
  • readTargetPose(out, target, withRotation, slot?) - slot is last and optional.
  • damping.dampVector3 and damping.dampQuaternion take out first.
  • prime of positionComposer, rotationComposer and rotateWithFollowTarget takes only the state; primeFrom() takes no argument.
  • InputSourceMapping, InputAxisMapping and InputControllerConfig take 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

  • OrbitFollow with recenteringTarget: 'trackingTarget' recenters to horizontal.center, measured from behind the target (0 behind, 180 in front).
  • With frameloop="demand", every Body, Aim, Extension and Noise component requests a frame when its props change or it unmounts.
  • HardLockToTarget and Lens with damping ease in from initialState on the first activation.
  • PanTiltAim.setFromRotation lands on the rotation while an axis is still easing, the short way around.
  • <Aim.PanTilt> axes no longer share their default range and recentering objects.
  • TargetGroup.computeBounds returns -1 when no member resolves.

What's Changed

Full Changelog: v0.14.0...v0.15.0

v0.14.0 - New Home

Choose a tag to compare

@kvvasuu kvvasuu released this 16 Oct 16:05

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 with orbitStyle: 'threeRing'. Its horizontal, vertical and radial axes take the mouse, touch, the wheel and pinches, or values from your code. setFromPosition and setFromRotation ease the camera to a spot, and the debug prop draws the orbit. OrbitFollowBody on the core, OrbitFollowBodyThree and OrbitFollowHelperThree in three.js, <Body.OrbitFollow> in React.
  • Wheel and pinch input - wheel and pinch in 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.
  • rotationDamping on Follow - seconds to turn with the target, for every bindingMode but worldSpace.
  • 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 in rotationDampingBypass on the CameraState.
  • 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.inputAxes lists them.
  • InputController from @pmndrs/klipp maps input onto axes without a DOM, for example to replay a recording. InputControllerDom from @pmndrs/klipp/dom builds 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/klipp and change the imports. The entry points stay the same: @pmndrs/klipp/three, /react, /dom and the camera-controls ones. @kvvasuu/klipp is deprecated and gets no more releases.
  • Core functions are grouped into namespaces with short names: createFollowParams is now follow.createParams, updateFollow is follow.update, createTargetPose is targetPose.create, createCameraState is cameraState.create, lerpCameraState is cameraState.lerp, tickKlipp is klippState.tick and composerDebugZones is debugZones.composer. Classes, types, BlendCurves, BlendHints and DEFAULT_BLEND keep their names. Building on the core lists every namespace.
  • InputAxisController is now InputControllerDom, and its lastInput is now input. InputAxisControllerConfig is now InputControllerConfig, and feedInputAxes is inputMapping.feedAxes. The ref of an <InputController> is an InputControllerDom.
  • Sequencer, MixingCamera, StateDrivenCamera and ClearShot are left out of the exports, along with their functions, until they are finished and tested.
  • BlendHints.screenSpaceAimWhenTargetsDiffer and BlendHints.inheritPosition are removed. They were never implemented.
  • Raw pinch input is one field - touchPinchDelta, gestureZoomDelta and wheelZoomDelta in ConsumedInput are replaced by pinchDelta, the change in ln(scale).
  • follow.prime(state, position) no longer takes params.
  • CameraState has a rotationDampingBypass field. A state built with cameraState.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's state is only as fresh as its last update, and its frustum helper moves in steps. Use standbyUpdate: 'always' for a camera whose state you read every frame.
  • Follow's damping delays only the target's position. The camera turns with the target right away, so a damped Follow behind a turning target moves differently than before. Set rotationDamping to delay the turn too.
  • The page keeps scrolling and pinch zooming over the canvas until wheel or pinch is mapped.
  • Wheels that scroll by lines or pages report pixels, so input.wheelDeltaY reads the same in every browser.

🐛 Bug Fixes

  • Looking straight up or down with HardLookAt or RotationComposer keeps a valid, steady rotation.
  • lockToTargetOnAssign captures the target's rotation again when the camera goes live or the mode is chosen, as well as when target is 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 mode back 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.
  • GroupFraming with a single point member frames it when padding is set.
  • Disconnecting input puts back the element's own touch-action and user-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.
  • standbyUpdate on 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 dist and the package exports stay the same.
  • Inactive cameras and those skipped by standbyUpdate skip their per-frame preparation. RotationComposer, Perlin noise and PositionComposer skip 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 KlippThree frames with up to 50 cameras.
  • Golden trajectory tests for Follow and OrbitFollow.

What's Changed

Full Changelog: v0.13.0...v0.14.0

v0.13.0 - One Core

Choose a tag to compare

@kvvasuu kvvasuu released this 02 Oct 10:50

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

  • Klipp runs everything - virtual cameras, priority, blending and the frame loop. Call klipp.update(dt) and put klipp.shot on your camera, or override write to update it only when the shot changes.
  • Every piece is a core class - FollowBody, HardLookAtAim, PositionComposerBody, GroupFramingExtension and the rest take a TargetPose you keep up to date. To read your engine's objects directly, extend one and override readTarget.
  • three.js classes end in Three - KlippThree drives a PerspectiveCamera, and VirtualCameraThree, FollowBodyThree and the other pieces read Object3D targets. Their options take a Vector3, 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.
  • CameraFrustumHelperThree draws a virtual camera's frustum in three.js.
  • Settings factories - createFollowParams, createLensParams and the rest give every piece's defaults.
  • Debug zones in the core - composerDebugZones and groupFramingPaddingBox, for drawing zones with DebugOverlay without React.
  • Duplicate camera names now log a warning.

⚠️ Breaking Changes

  • three.js classes end in Three - FollowBody, HardLockToTargetBody, PositionComposerBody, HardLookAtAim, RotateWithFollowTargetAim, RotationComposerAim, PanTiltAim, GroupFramingExtension and CameraControlsBody from @kvvasuu/klipp/three are now FollowBodyThree, HardLockToTargetBodyThree and so on. This also changes the types of React refs: useRef<FollowBody> is now useRef<FollowBodyThree>.
  • KlippCore is now Klipp, and KlippCoreOptions is now KlippOptions.
  • Klipp - registerCamera, updatePriority, updateHints and tick are replaced by addCamera(name, options), the camera's own priority and hints, and update(dt) with shot.
  • VirtualCameraController is now VirtualCamera.
  • useKlipp() returns the KlippThree instead of { core, registerUpdate, initialCameraState, targets }: useKlipp().core.isBlending is now useKlipp().isBlending.
  • useVirtualCamera() returns the camera instead of { controller, state, initialState }. A <VirtualCamera>'s ref is that same VirtualCameraThree.
  • Custom pieces - registerBody(update) and registerAim(update) are replaced by camera.body = { update } and camera.aim = { update }, or setBody and setAim. registerExtension and registerNoise are replaced by addExtension({ update }) and addNoise({ update }). Every method except the setters returns a function that removes the piece.
  • Types removed - KlippContextValue, VirtualCameraContextValue and VirtualCameraSlots.

🔄 Behavior Changes

  • BlendCurves.easeIn and easeOut are swapped, to match cubicIn and cubicOut from math. "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 with resolved: true, so a new member counts right away. Set it to false to 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. HardLockToTarget and GroupFraming reset on activation like Follow, 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

Full Changelog: v0.12.0...v0.13.0

v0.12.0 - Under the Hood

Choose a tag to compare

@kvvasuu kvvasuu released this 30 Sep 09:33

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/klipp has no dependencies beyond math. 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, /react and /dom. three is now an optional peer dependency too, needed only by /three and /react.
  • Each target is read once per frame, however many cameras and pieces use it.
  • DebugOverlay in /dom draws 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, HardLookAtAim and the other Bodies/Aims, applyCameraState, copyCameraStateFromCamera, resolveTarget*, Vector3Damper and QuaternionDamper. @kvvasuu/klipp/camera-controls is now @kvvasuu/klipp/three/camera-controls.
  • Vectors are arrays - CameraState and class fields like offset use [x, y, z] and [x, y, z, w] instead of Vector3 and Quaternion. Props still accept both.
  • BlendCurves.hardIn / hardOut renamed to cubicIn / cubicOut, matching math.

🔄 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

  • ImpulseField no longer grows without bound - expired impulses are now also dropped on generate, 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

Full Changelog: v0.11.0...v0.12.0

v0.11.0 - Look Around

Choose a tag to compare

@kvvasuu kvvasuu released this 23 Sep 21:26

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. InputSystem collects raw pointer/wheel/touch/Pointer Lock deltas per element; InputAxis shapes 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/range limits, and idle recentering back to center.
  • maxSpeed on every damping prop (Body/Aim/Extension) and on BlendDefinition's damping blend - caps how fast damping can close a gap, independent of the time constant itself.

⚠️ Breaking Changes

  • useVirtualCameraSlots()/useVirtualCameraState() merged into one useVirtualCamera() - returns { controller, state, initialState } instead of two separate hooks. controller replaces the old VirtualCameraSlots return value; initialState (the raw prop, not the already-seeded CameraState) is newly reachable this way.
  • useKlippCore() replaced by useKlipp() - returns { core, registerUpdate, initialCameraState } instead of just the core directly; grab it via useKlipp().core.

🐛 Bug Fixes

  • Follow/PositionComposer/RotationComposer/RotateWithFollowTarget now actually respect initialState - 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-staged pre-commit hook + a prettier --check step in CI.
  • Hooks/contexts split out of VirtualCamera.tsx/Klipp.tsx/InputController.tsx into their own files, for React Fast Refresh compliance.
  • VirtualCameraEventsProps/KlippEventsProps deduplicated 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

Choose a tag to compare

@kvvasuu kvvasuu released this 16 Sep 18:25

v0.10.0 - Recoil

⚠️ Breaking Changes

  • ImpulseManager renamed to ImpulseField - same registry of world-space impulse events, new name.
  • Noise.ImpulseListener is now just ImpulseListener, 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/decayTime replaced by shape + duration - generate() now takes a shape: ImpulseShape ((t) => number) instead of three separate linear-ramp times. Named presets live on ImpulseShapes.recoil/bump/explosion/rumble, or pass your own function.
  • ImpulseField.sampleAt() takes gain as a parameter and returns the felt strength, not a Vector3

✨ New Features

  • ImpulseListener gains shake - an optional secondary rattle, position and rotation, composing a BasicMultiChannelPerlinNoise whose amplitudeGain automatically follows the impulse's current strength every frame. The kick itself stays position-only; shake is how an explosion or rumble also pitches the camera.
  • ImpulseListener gains cameraSpace - rotates the sampled kick by the camera's current orientation, so the same direction always 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) and ImpulseListenerNoise.update (kick alone vs. with shake/cameraSpace, isolated).
  • docs/impulse/ is now its own top-level docs category instead of nested under docs/noise/.

What's Changed

Full Changelog: v0.9.0...v0.10.0

v0.9.0 - Ensemble

Choose a tag to compare

@kvvasuu kvvasuu released this 13 Sep 17:10

v0.9.0 - Ensemble

✨ New Features

  • GroupFraming gains fitMode, minDistance/maxDistance, and framingMode - 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/maxDistance clamp 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

  • GroupFraming now 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

Choose a tag to compare

@kvvasuu kvvasuu released this 12 Sep 20:22

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. Follow writes it, rotated the same way as its own offset (so it reflects whatever the current bindingMode derived), and HardLookAt/RotationComposer read it when building their look-at rotation. Body still never touches rotation and Aim still never touches position - referenceUp is just a value passed between them, interpolated during blends like every other CameraState field.

🐛 Bug Fixes

  • HardLockToTargetBody no longer pops on a retarget mid-blend - it published out.target as the raw, undamped target position while out.position was the damped one still catching up. BlendHints.cylindricalPosition/sphericalPosition derive an orbit offset from position - target, so retargeting mid-catch-up made that offset jump instantly, popping any blend that read it. Fixed by publishing out.target as an exact copy of the (already damped) out.position - HardLockToTargetBody has no persistent offset by definition, so this is always correct.
  • FollowBody had the same bug, fixed the same way in spirit - Follow has a real, persistent, non-zero offset though, so out.target is now derived algebraically from the damped position minus the rotated offset, keeping the offset BlendHints read 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

  • math bumped from a canary prerelease to the first stable ^0.1.0 release.
  • 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

Choose a tag to compare

@kvvasuu kvvasuu released this 10 Sep 19:11

v0.8.0 - Dispatch

⚠️ Breaking Changes

  • react peer bumped to >=19.2.0 (from ^19.0) - the event system below uses useEffectEvent, stable only from React 19.2 onward. Only affects the @kvvasuu/klipp/react entry point; the framework-agnostic @kvvasuu/klipp engine has no React dependency at all, so this doesn't touch it.

✨ New Features

  • Camera Events - KlippCore now extends three's EventDispatcher, dispatching activated/deactivated/blendCreated/blendFinished/cut for every arbitration and blend transition it drives. VirtualCameraController extends EventDispatcher too, re-dispatching only the events one specific camera takes part in - activated when it's incoming, deactivated when it's outgoing, blendCreated/cut on either side, blendFinished when it's the one that just settled live. Two ways to listen to either: <Klipp.Events>/<VirtualCamera.Events> as callback props, or a ref on <VirtualCamera> for addEventListener straight on the underlying VirtualCameraController.

🐛 Bug Fixes

  • CameraControls no longer reconnects on every unrelated re-render - its on* 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 whole camera-controls connection, occasionally dropping an in-progress drag. Now handled via useEffectEvent, so only an actual change in target/domElement/arbitration reconnects.

🔧 Internal Changes

  • Hardened KlippCore.tick()'s event dispatch against reentrancy - a listener that synchronously calls registerCamera/updatePriority can no longer corrupt a later event's payload within the same tick.
  • New tests: KlippCore, VirtualCameraController, VirtualCamera, and CameraControls all 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 ref alternative.

What's Changed

Full Changelog: v0.7.0...v0.8.0