Skip to content

Commit c44b633

Browse files
georginahalpernGeorgina Halpern
andauthored
[GeospatialCamera] Update flyToAsync hop behavior to animate center with parabolic hop (vs scale radius) (#17512)
Rather than using customKeys for customizing the interpolating behavior, allow user to customize any part of the animation via callback. This is now used to animate center rather than radius so that center can have a parabolic hop animation based on passed in scale factor. The problem with previous approach was that just animating radius is not super noticeable if the camera is already very close to ground / pitchedtowards horizon. This approach creates a true parabolic arc motion #17451 Co-authored-by: Georgina Halpern <[email protected]>
1 parent 2bb8818 commit c44b633

File tree

2 files changed

+41
-33
lines changed

2 files changed

+41
-33
lines changed

packages/dev/core/src/Behaviors/Cameras/interpolatingBehavior.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { Animatable } from "../../Animations/animatable.core";
55
import { Animation } from "../../Animations/animation";
66
import type { Camera } from "../../Cameras/camera";
77
import type { IColor3Like, IColor4Like, IMatrixLike, IQuaternionLike, IVector2Like, IVector3Like } from "../../Maths/math.like";
8-
import type { IAnimationKey } from "../../Animations/animationKey";
98

109
export type AllowedAnimValue = number | IVector2Like | IVector3Like | IQuaternionLike | IMatrixLike | IColor3Like | IColor4Like | SizeLike | undefined;
1110

@@ -110,7 +109,7 @@ export class InterpolatingBehavior<C extends Camera = Camera> implements Behavio
110109
properties: Map<K, AllowedAnimValue>,
111110
transitionDuration: number = this.transitionDuration,
112111
easingFn: EasingFunction = this.easingFunction,
113-
customKeys?: Map<K, IAnimationKey[]>
112+
updateAnimation?: (key: string, animation: Animation) => void
114113
): Promise<void> {
115114
const promise = new Promise<void>((resolve) => {
116115
this.stopAllAnimations();
@@ -139,22 +138,14 @@ export class InterpolatingBehavior<C extends Camera = Camera> implements Behavio
139138
};
140139

141140
properties.forEach((value, key) => {
142-
if (value !== undefined) {
141+
if (value !== undefined && camera[key] !== value) {
143142
const propertyName = String(key);
144143
const animation = Animation.CreateAnimation(propertyName, GetAnimationType(value), 60, easingFn);
144+
// Optionally allow caller to further customize the animation
145+
updateAnimation?.(propertyName, animation);
146+
145147
// Pass false for stopCurrent so that we can interpolate multiple properties at once
146-
const animatable = Animation.TransitionTo(
147-
propertyName,
148-
value,
149-
camera,
150-
scene,
151-
60,
152-
animation,
153-
transitionDuration,
154-
() => checkClear(propertyName),
155-
false,
156-
customKeys?.get(key)
157-
);
148+
const animatable = Animation.TransitionTo(propertyName, value, camera, scene, 60, animation, transitionDuration, () => checkClear(propertyName), false);
158149
if (animatable) {
159150
this._animatables.set(propertyName, animatable);
160151
}

packages/dev/core/src/Cameras/geospatialCamera.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ import type { DeepImmutable } from "../types";
88
import { GeospatialLimits } from "./Limits/geospatialLimits";
99
import { ClampCenterFromPolesInPlace, ComputeLocalBasisToRefs, GeospatialCameraMovement } from "./geospatialCameraMovement";
1010
import type { IVector3Like } from "../Maths/math.like";
11-
import { Vector3CopyToRef } from "../Maths/math.vector.functions";
11+
import { Vector3CopyToRef, Vector3Distance } from "../Maths/math.vector.functions";
1212
import { Clamp } from "../Maths/math.scalar.functions";
1313
import type { AllowedAnimValue } from "../Behaviors/Cameras/interpolatingBehavior";
1414
import { InterpolatingBehavior } from "../Behaviors/Cameras/interpolatingBehavior";
1515
import type { EasingFunction } from "../Animations/easing";
16+
import type { Animation } from "../Animations/animation";
1617

1718
type CameraOptions = {
1819
planetRadius: number; // Radius of the planet
@@ -212,7 +213,6 @@ export class GeospatialCamera extends Camera {
212213
this._flyingBehavior.updateProperties(this._flyToTargets);
213214
}
214215

215-
private _customKeys = new Map();
216216
/**
217217
* Animate camera towards passed in property values. If undefined, will use current value
218218
* @param targetYaw
@@ -221,7 +221,7 @@ export class GeospatialCamera extends Camera {
221221
* @param targetCenter
222222
* @param flightDurationMs
223223
* @param easingFunction
224-
* @param overshootRadiusScale If defined, will first fly to radius*scale before flying to targetRadius to create a "bounce" effect
224+
* @param centerHopScale If supplied, will define the parabolic hop height scale for center animation to create a "bounce" effect
225225
* @returns Promise that will return when the animation is complete (or interuppted by pointer input)
226226
*/
227227
public async flyToAsync(
@@ -231,30 +231,47 @@ export class GeospatialCamera extends Camera {
231231
targetCenter?: Vector3,
232232
flightDurationMs: number = 1000,
233233
easingFunction?: EasingFunction,
234-
overshootRadiusScale?: number
234+
centerHopScale?: number
235235
): Promise<void> {
236236
this._flyToTargets.clear();
237-
this._customKeys.clear();
238237

239238
this._flyToTargets.set("yaw", targetYaw);
240239
this._flyToTargets.set("pitch", targetPitch);
241240
this._flyToTargets.set("radius", targetRadius);
242241
this._flyToTargets.set("center", targetCenter);
243242

244-
const overshootRadius = overshootRadiusScale !== undefined ? this.radius * overshootRadiusScale : undefined;
245-
if (overshootRadius !== undefined && overshootRadius !== targetRadius) {
246-
// Start the animation with overshoot radius
247-
const frameRate = 60;
248-
const totalFrames = (flightDurationMs / 1000) * frameRate;
249-
const midFrame = totalFrames / 2;
250-
251-
this._customKeys.set("radius", [
252-
{ frame: 0, value: this.radius },
253-
{ frame: midFrame, value: overshootRadius },
254-
{ frame: totalFrames, value: targetRadius },
255-
]);
243+
let overrideAnimationFunction;
244+
if (targetCenter !== undefined && !targetCenter.equals(this.center)) {
245+
// Animate center directly with custom interpolation
246+
const start = this.center.clone();
247+
const end = targetCenter.clone();
248+
249+
overrideAnimationFunction = (key: string, animation: Animation): void => {
250+
if (key === "center") {
251+
// Override the Vector3 interpolation to use SLERP + hop
252+
animation.vector3InterpolateFunction = (startValue, endValue, gradient) => {
253+
// gradient is the eased value (0 to 1) after easing function is applied
254+
255+
// Slerp between start and end
256+
const newCenter = Vector3.SlerpToRef(start, end, gradient, this._tempCenter);
257+
258+
// Apply parabolic hop if requested
259+
if (centerHopScale && centerHopScale > 0) {
260+
// Parabolic formula: peaks at t=0.5, returns to 0 at gradient=0 and gradient=1
261+
// if hopPeakT = .5 the denominator would be hopPeakT * hopPeakT - hopPeakT, which = -.25
262+
const hopPeakOffset = centerHopScale * Vector3Distance(start, end);
263+
const hopOffset = hopPeakOffset * Clamp((gradient * gradient - gradient) / -0.25);
264+
// Scale the center outward (away from origin)
265+
newCenter.scaleInPlace(1 + hopOffset / newCenter.length());
266+
}
267+
268+
return newCenter;
269+
};
270+
}
271+
};
256272
}
257-
return await this._flyingBehavior.animatePropertiesAsync(this._flyToTargets, flightDurationMs, easingFunction, this._customKeys);
273+
274+
return await this._flyingBehavior.animatePropertiesAsync(this._flyToTargets, flightDurationMs, easingFunction, overrideAnimationFunction);
258275
}
259276

260277
/**

0 commit comments

Comments
 (0)