148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import { getVelocity } from './derivative';
|
|
|
|
/** MIT License github.com/pushkine/ */
|
|
export interface SpringParams {
|
|
mass: number; // = 1.0
|
|
damping: number; // = 10.0
|
|
stiffness: number; // = 100.0
|
|
soft: boolean; // = false
|
|
}
|
|
|
|
type seconds = number;
|
|
|
|
export class Spring {
|
|
private currentPosition = 0;
|
|
private targetPosition = 0;
|
|
private currentTime = 0;
|
|
private params: Partial<SpringParams> = {};
|
|
private currentSolver: (t: seconds) => number;
|
|
private getV: (t: seconds) => number;
|
|
private getV2: (t: seconds) => number;
|
|
private queueParams:
|
|
| (Partial<SpringParams> & {
|
|
time: number;
|
|
})
|
|
| undefined;
|
|
private queuePosition:
|
|
| {
|
|
time: number;
|
|
position: number;
|
|
}
|
|
| undefined;
|
|
constructor(currentPosition = 0) {
|
|
this.targetPosition = currentPosition;
|
|
this.currentPosition = this.targetPosition;
|
|
this.currentSolver = () => this.targetPosition;
|
|
this.getV = () => 0;
|
|
this.getV2 = () => 0;
|
|
}
|
|
private resetSolver() {
|
|
const curV = this.getV(this.currentTime);
|
|
this.currentTime = 0;
|
|
this.currentSolver = solveSpring(this.currentPosition, curV, this.targetPosition, 0, this.params);
|
|
this.getV = getVelocity(this.currentSolver);
|
|
this.getV2 = getVelocity(this.getV);
|
|
}
|
|
arrived() {
|
|
return (
|
|
Math.abs(this.targetPosition - this.currentPosition) < 0.01 &&
|
|
this.getV(this.currentTime) < 0.01 &&
|
|
this.getV2(this.currentTime) < 0.01 &&
|
|
this.queueParams === undefined &&
|
|
this.queuePosition === undefined
|
|
);
|
|
}
|
|
setPosition(targetPosition: number) {
|
|
this.targetPosition = targetPosition;
|
|
this.currentPosition = targetPosition;
|
|
this.currentSolver = () => this.targetPosition;
|
|
this.getV = () => 0;
|
|
this.getV2 = () => 0;
|
|
}
|
|
update(delta = 0) {
|
|
this.currentTime += delta;
|
|
this.currentPosition = this.currentSolver(this.currentTime);
|
|
if (this.queueParams) {
|
|
this.queueParams.time -= delta;
|
|
if (this.queueParams.time <= 0) {
|
|
this.updateParams({
|
|
...this.queueParams
|
|
});
|
|
}
|
|
}
|
|
if (this.queuePosition) {
|
|
this.queuePosition.time -= delta;
|
|
if (this.queuePosition.time <= 0) {
|
|
this.setTargetPosition(this.queuePosition.position);
|
|
}
|
|
}
|
|
if (this.arrived()) {
|
|
this.setPosition(this.targetPosition);
|
|
}
|
|
}
|
|
updateParams(params: Partial<SpringParams>, delay = 0) {
|
|
if (delay > 0) {
|
|
this.queueParams = {
|
|
...(this.queuePosition ?? {}),
|
|
...params,
|
|
time: delay
|
|
};
|
|
} else {
|
|
this.queuePosition = undefined;
|
|
this.params = {
|
|
...this.params,
|
|
...params
|
|
};
|
|
this.resetSolver();
|
|
}
|
|
}
|
|
setTargetPosition(targetPosition: number, delay = 0) {
|
|
if (delay > 0) {
|
|
this.queuePosition = {
|
|
...(this.queuePosition ?? {}),
|
|
position: targetPosition,
|
|
time: delay
|
|
};
|
|
} else {
|
|
this.queuePosition = undefined;
|
|
this.targetPosition = targetPosition;
|
|
this.resetSolver();
|
|
}
|
|
}
|
|
getCurrentPosition() {
|
|
return this.currentPosition;
|
|
}
|
|
}
|
|
|
|
function solveSpring(
|
|
from: number,
|
|
velocity: number,
|
|
to: number,
|
|
delay: seconds = 0,
|
|
params?: Partial<SpringParams>
|
|
): (t: seconds) => number {
|
|
const soft = params?.soft ?? false;
|
|
const stiffness = params?.stiffness ?? 100;
|
|
const damping = params?.damping ?? 10;
|
|
const mass = params?.mass ?? 1;
|
|
const delta = to - from;
|
|
if (soft || 1.0 <= damping / (2.0 * Math.sqrt(stiffness * mass))) {
|
|
const angular_frequency = -Math.sqrt(stiffness / mass);
|
|
const leftover = -angular_frequency * delta - velocity;
|
|
return (t: seconds) => {
|
|
t -= delay;
|
|
if (t < 0) return from;
|
|
return to - (delta + t * leftover) * Math.E ** (t * angular_frequency);
|
|
};
|
|
}
|
|
const damping_frequency = Math.sqrt(4.0 * mass * stiffness - damping ** 2.0);
|
|
const leftover = (damping * delta - 2.0 * mass * velocity) / damping_frequency;
|
|
const dfm = (0.5 * damping_frequency) / mass;
|
|
const dm = -(0.5 * damping) / mass;
|
|
return (t: seconds) => {
|
|
t -= delay;
|
|
if (t < 0) return from;
|
|
return to - (Math.cos(t * dfm) * delta + Math.sin(t * dfm) * leftover) * Math.E ** (t * dm);
|
|
};
|
|
}
|