Scanat lets users change a model's scale. The model updates immediately, while the camera also needs to adjust its distance so the model remains clearly visible.
The product requirement was simple: after the scale changes, adjust the viewpoint automatically.
The actual design problem was the motion itself. Should the model and camera move together or in sequence? When should the camera start? How long should it move? Which easing curve fits the interaction? These questions were difficult to settle in static mockups, and there was no useful default parameter set to apply directly.
Instead of fixing the timing first, I built a working demo with adjustable timing controls and compared the motion in context.
The camera movement after scaling is a reactive compensation.
The user's action is changing the model scale. The size change is the primary feedback; camera movement follows only to restore a useful view. If both happen at exactly the same time, it becomes harder to read the scale change itself. If the camera reacts too late, the sequence feels disconnected.
This differs from an action such as Reset View. Reset View is an explicit navigation request and can begin immediately. Camera compensation after scaling needs to leave enough time for the model change to register first.
| Dimension | Camera compensation after scaling | Reset View |
|---|---|---|
| Trigger | Reactive response to a model state change | Explicit navigation action |
| State before motion | Usually stationary | May still carry drag inertia |
| Pause before motion | Useful, so the scale change can be read first | Usually unnecessary |
| Easing priority | Soft transition at both start and end | Stronger emphasis on settling into the destination |
The useful question is not simply which easing curve to use, but why the camera is moving in the first place. Different triggers call for different timing and motion behavior.
The final sequence has three stages: scale the model immediately → hold for 0.4 seconds → move the camera over 1.5 seconds.
The short pause separates the two changes. The user first sees the model change size, then sees the camera adjust to the new scale. This makes the relationship between the two actions easier to read.
The camera moves only along the current viewing direction. It changes its distance from the model without changing the viewing angle. Scaling up moves the camera back; scaling down moves it closer. The result reads as a push or pull within the same view rather than a new camera shot.
The animation uses easeInOutCubic. Because the camera starts from rest and ends at rest, both ends need a soft transition. An earlier ease-out version started too abruptly and made the first part of the motion feel jarring.
The most useful part of the demo was not reproducing the final animation. It was turning timing values that normally lived in code into controls that could be adjusted directly.
The hold duration and camera duration were exposed as live sliders. Each change could be tested immediately without editing code and reloading the page.
<input type="range" id="hold" min="0" max="2" step="0.1" value="0.4" />
<input type="range" id="dur" min="0.4" max="3" step="0.1" value="1.5" />Neither 0.4 seconds nor 1.5 seconds was decided at the start. Both came from comparing different combinations in the full animation.
For motion parameters like these, a prototype changes the discussion from "0.5 seconds sounds reasonable" to a direct comparison between 0.3, 0.4, and 0.5 seconds in context.
The camera distance is adjusted using the model's scale ratio:
const ratio = newScale / oldScale;
const toPos = camera.position.clone().multiplyScalar(ratio);The important part is preserving the current viewing direction and changing only the distance. If the model becomes 2x larger, the camera moves back along the same direction by the corresponding amount.
This keeps the motion visually stable and supports the camera's role as compensation. Changing orientation at the same time would introduce another visual event and make the model's scale change harder to read.
Three-stage timing. The demo uses a simple delay and animation state to separate model scaling, the hold, and camera movement:
setTimeout(() => {
pivot.scale.setScalar(pendingScale);
const fromPos = camera.position.clone();
const toPos = camera.position.clone().multiplyScalar(ratio);
setTimeout(() => {
anim = {
fromPos,
toPos,
start: performance.now(),
dur: durMs
};
}, holdMs);
}, loadMs);Easing.
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}This demo did not uncover a new interaction bug. Its value was different: before seeing the motion run, it was difficult to know whether a particular timing sequence actually felt right.
Static mockups can describe the states before and after an animation, and written specifications can record duration and easing. Neither fully communicates the motion between them, especially the pause, acceleration, and relationship between consecutive actions.
For this kind of work, a prototype is not only a way to present the solution. It is also a design tool for turning motion that is difficult to describe into something that can be observed and compared directly.